From 56e2cabb72837d5fd0752a5339801a58f7e29869 Mon Sep 17 00:00:00 2001 From: Peter Gerber Date: Mon, 23 Feb 2026 16:43:19 +0000 Subject: [PATCH] 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 --- .../AcmeClient/LeValidation/HttpOpnsense.php | 29 ++++++++++++++++++- .../AcmeClient/LeValidation/TlsalpnAcme.php | 29 ++++++++++++++++++- .../AcmeClient/lighttpd-acme-challenge.conf | 11 +++---- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php index 9216823ca..c03af9cc1 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php @@ -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 { diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php index dacae5f9b..0ae3bdae4 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php @@ -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 { diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf index 605f7e3fe..c85693435 100644 --- a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf @@ -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