security/acme-client: fix IPv6 support HTTP-01 and TLS-ALPN-01 challenges

Underlying issue:

IPv6 features multiple scopes restricting where the IP address
is valid [1]. ::1 belongs to link-local scope which is not
allowed to be routed. As result FreeBSD will reject the
connection after rewriting, as it will come from global
scope (the internet) and going to ::1 [2]. This isn't allowed.

The fix / workaround:

Instead of redirecting to ::1, the following is tried:

* If there is a WAN interface with an IPv6 address defined,
  redirect to this address.

  I expect most setup with IPv6 to have a WAN interface with
  a suitable (=allowed scope) IPv6 address.

* Else, only redirect the port and leave the address unchanged.

  This will only work if we are issuing a certificate for
  ourselves (rather than a host behind the firewall).

A better solution would be to pick an arbitrary IPv6 address
of the host with a suitable scope. However, I believe this
would be considerably more complex to implement and test.
I propose we use this simplified approach, at least for now,
which should already work for the vast majority of users.

[1]: https://en.wikipedia.org/wiki/IPv6_address#Address_scopes
[2]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193568
This commit is contained in:
Peter Gerber 2026-02-23 16:43:19 +00:00
parent 3bc852e539
commit 56e2cabb72
No known key found for this signature in database
GPG key ID: BD8D7AE6FF3133A5
3 changed files with 60 additions and 9 deletions

View file

@ -86,6 +86,33 @@ class HttpOpnsense extends Base implements LeValidationInterface
}
}
// Find redirect target for IPv6
//
// Needed because redirecting to ::1 isn't allowed [1].
//
// [1]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193568
$ipv6_redirect_addr = null;
if (is_ipv6_allowed()) {
$backend = new \OPNsense\Core\Backend();
$interface = "wan";
$response = json_decode($backend->configdpRun('interface address', [$interface]));
if (isset($response->$interface)) {
foreach ($response->$interface as $if) {
if (!empty($if->address) && $if->family == "inet6") {
$ipv6_redirect_addr = $if->address;
break;
}
}
}
if ($ipv6_redirect_addr != null) {
LeUtils::log("found IPv6 on WAN interface, will redirect traffic there ({$ipv6_redirect_addr})");
} else {
LeUtils::log("failed to find IPv6 on WAN interface ($interface), will not rewrite target address");
}
}
// Generate rules for all IP addresses
$anchor_rules = "";
if (!empty($iplist)) {
@ -99,7 +126,7 @@ class HttpOpnsense extends Base implements LeValidationInterface
LeUtils::log("using IPv4 address: {$ip}");
} elseif (is_ipv6_allowed() && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
// IPv6
$_dst = '::1';
$_dst = $ipv6_redirect_addr != null ? $ipv6_redirect_addr : $ip;
$_family = 'inet6';
LeUtils::log("using IPv6 address: {$ip}");
} else {

View file

@ -87,6 +87,33 @@ class TlsalpnAcme extends Base implements LeValidationInterface
}
}
// Find redirect target for IPv6
//
// Needed because redirecting to ::1 isn't allowed [1].
//
// [1]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193568
$ipv6_redirect_addr = null;
if (is_ipv6_allowed()) {
$backend = new \OPNsense\Core\Backend();
$interface = "wan";
$response = json_decode($backend->configdpRun('interface address', [$interface]));
if (isset($response->$interface)) {
foreach ($response->$interface as $if) {
if (!empty($if->address) && $if->family == "inet6") {
$ipv6_redirect_addr = $if->address;
break;
}
}
}
if ($ipv6_redirect_addr != null) {
LeUtils::log("found IPv6 on WAN interface, will redirect traffic there ({$ipv6_redirect_addr})");
} else {
LeUtils::log("failed to find IPv6 on WAN interface ($interface), will not rewrite target address");
}
}
// Generate rules for all IP addresses
$anchor_rules = "";
if (!empty($iplist)) {
@ -100,7 +127,7 @@ class TlsalpnAcme extends Base implements LeValidationInterface
LeUtils::log("using IPv4 address: {$ip}");
} elseif (is_ipv6_allowed() && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
// IPv6
$_dst = '::1';
$_dst = $ipv6_redirect_addr != null ? $ipv6_redirect_addr : $ip;
$_family = 'inet6';
LeUtils::log("using IPv6 address: {$ip}");
} else {

View file

@ -7,7 +7,7 @@
# FreeBSD!
server.event-handler = "freebsd-kqueue"
server.network-backend = "writev"
#server.use-ipv6 = "enable"
server.use-ipv6 = "{{ 'enable' if OPNsense.Interfaces.settings.disableipv6 == "0" else 'disable' }}"
# modules to load
server.modules = ( "mod_access", "mod_expire", "mod_deflate", "mod_redirect",
@ -60,13 +60,10 @@ mimetype.assign = (
url.access-deny = ( "~", ".inc" )
# bind to port
server.bind = "127.0.0.1"
server.port = {{OPNsense.AcmeClient.settings.challengePort}}
$SERVER["socket"] == "127.0.0.1:{{OPNsense.AcmeClient.settings.challengePort}}" { }
{% if OPNsense.Interfaces.settings.disableipv6 == "0" %}
# IPv6
$SERVER["socket"] == "[::1]:{{OPNsense.AcmeClient.settings.challengePort}}" { }
$SERVER["socket"] == "0.0.0.0:{{OPNsense.AcmeClient.settings.challengePort}}" { }
{% if OPNsense.Interfaces.settings.disableipv6 == "1" %}
$SERVER["socket"] == "[::]:{{OPNsense.AcmeClient.settings.challengePort}}" { }
{% endif %}
# to help the rc.scripts