Captive portal: fix track6 IPv6 redirect host and bracketed redirurl#9973
Conversation
Resolve the live routed interface address for captive portal redirects on track6 interfaces and preserve full bracketed IPv6 literals in redirurl.
…e help text Move the runtime interface address resolution from an inline PHP string in template_helpers.py to a standalone script at scripts/interfaces/get_interface_address.php, following the existing codebase pattern for PHP scripts called from the Python backend. Update the captive portal zone hostname help text to note IPv6-only client compatibility considerations when no hostname is configured.
This file should not have been modified in our branch; restore the original help text from the upstream base.
| @@ -0,0 +1,51 @@ | |||
| #!/usr/local/bin/php | |||
There was a problem hiding this comment.
There’s pluginctl -6 interface for that kind of operation.
There was a problem hiding this comment.
pluginctl -6 calls interfaces_primary_address6(), which returns the link-local address (fe80::...) for track6 interfaces. For captive portal redirects we need the routed GUA, which is what interfaces_routed_address6() returns.
There was a problem hiding this comment.
Which is the reason I think this is over-engineered and premature to address now without getting the IPv6 support out first on dependable scope.
| def interface_routed_address4(self, name: str) -> str: | ||
| return self._runtime_interface_address(name, 'inet') | ||
|
|
||
| def interface_routed_address6(self, name: str) -> str: |
There was a problem hiding this comment.
I would advise not to start doing this in templates too.
There was a problem hiding this comment.
We need this here because captive portal's lighttpd config is generated through the Jinja-based configd path, and that path only sees the static config model unless runtime interface state is exposed explicitly.
For track6, the correct routed IPv6 address is not present in that static model. It only exists in live interface state after DHCPv6-PD has delegated a prefix and the interface has been configured. So without a helper at this layer, the template has no way to emit the correct IPv6 redirect target.
This is not an edge case. DHCPv6-PD with track6 is a normal IPv6 deployment model, and OPNsense has already had to solve the same runtime-address problem elsewhere. webgui.inc, openssh.inc, and unbound.inc already handle live interface address changes through PHP-side regeneration and newwanip hooks. The difference here is only that captive portal renders its final config through Jinja, so the same class of runtime information has to be exposed differently.
There was a problem hiding this comment.
This really needs a different fix for the captive portal template, in practice, when CP is reflecting, it doesn't care which ipv6 address you use as long as its routed and passes the firewall. A manual input might be an option, best practice is and remains adding domain names. Let's focus on the important parts first to avoid getting lost in piles of code which is likely not needed.
There was a problem hiding this comment.
Fair. Thanks for reviewing!
Replace the standalone get_interface_address.php script with calls to pluginctl -4/-6 (for primary addresses) and pluginctl -D (for routed GUA on track6 interfaces where -6 returns a link-local). This addresses review feedback to use existing tools rather than adding new scripts or extending template_helpers with cross-language subprocess calls.
Strip IPv4 runtime resolution (unnecessary since config.xml has the static address) and preserve upstream PR opnsense#9745 template structure. Only two targeted changes on top of swhite2's branch: 1. Runtime IPv6 resolution via get_interface_address.php for track6 interfaces, falling back to conf_inf.ipaddrv6 for static configs 2. Bracketed IPv6 regex fix in [::] socket blocks so redirurl is not truncated at the first colon
lighttpd config does not unescape \\ to \ in double-quoted strings the way PCRE expects. Double backslashes caused the bracket-matching alternative to never fire, so redirurl was truncated at the first colon. Use single backslashes which lighttpd passes through to PCRE correctly, matching the convention used by the rest of this template.
The runtime call to interfaces_routed_address6() handles both track6 and statically configured IPv6 interfaces. The elif fallback to conf_inf.ipaddrv6 is unreachable in practice: for track6 it contains the config token, not a valid address, and for static IPv6 the runtime call already returns the correct GUA.
The host-match regex ([^:/]+) in the IPv6 lighttpd socket blocks truncates bracketed IPv6 literals at the first colon, producing a broken redirurl (e.g. [2601/ instead of [2606:4700:4700::1111]/). Change the capture group to (\[[^\]]+\]|[^:/]+) so a full bracketed literal is matched first, falling back to the original pattern for IPv4 addresses and hostnames. Missed in: 369630d (Captive portal: IPv6 support, opnsense#9745) See also: opnsense#9973
The host-match regex ([^:/]+) in the IPv6 lighttpd socket blocks truncates bracketed IPv6 literals at the first colon, producing a broken redirurl (e.g. [2601/ instead of [2606:4700:4700::1111]/). Change the capture group to (\[[^\]]+\]|[^:/]+) so a full bracketed literal is matched first, falling back to the original pattern for IPv4 addresses and hostnames. Missed in: 369630d (Captive portal: IPv6 support, #9745) See also: #9973
Upstream PR: #9745
Summary
This follow-up to the comment: #9745 (comment) and fixes two IPv6 redirect bugs that are still present on top of
track6, the template currently reads the literalconfig value
track6and falls back to the IPv4 portal host. An IPv6-only clientthen receives a redirect to an unreachable IPv4 address.
host-match regex truncates the value at the first
:, soredirurlbecomesincomplete.
Why
track6is a normal OPNsense IPv6 deployment mode, not a corner case. DHCPv6-PD is required for CE routers by RFC 7084 (WPD-1), and RIPE-690 documents it as the standard operator practice for prefix assignment to end-users. OPNsense documentstrack6as the normal way to place a delegated/64on a LAN.Captive portal users are often deploying on ISP-managed or upstream-managed connections where the firewall receives a delegated prefix rather than a static IPv6 assignment. The portal needs the live routed IPv6 address, not the static config token.
RIPE's deployment survey reports that 37.5% of ISPs still rotate prefixes, so the routed LAN IPv6 can legitimately change underneath the portal. OPNsense already uses runtime address handling in other places for
track6, notably the web GUI binding work in #5966. The captive portal faces the same problem and should use the same pattern.The underlying issue is that the configd template system was designed around
config.xml, which holds static values. With IPv6 and track6, interface addresses are runtime-derived: the delegated prefix comes from DHCPv6-PD and can change, so the LAN address isn't known until the interface is configured. This isn't unique to the captive portal. The web GUI (https://github.com/opnsense/core/blob/master/src/etc/inc/plugins.inc.d/webgui.inc#L36), Unbound (https://github.com/opnsense/core/blob/master/src/etc/inc/plugins.inc.d/unbound.inc#L45), and OpenSSH (https://github.com/opnsense/core/blob/master/src/etc/inc/plugins.inc.d/openssh.inc#L44) all solved the same problem vianewwaniphooks and PHP-side config regeneration (originally driven by #5966).More broadly, this is probably going to keep coming up as IPv6 support continues to mature, since IPv6's end-to-end model means runtime-derived addresses appear in places that IPv4 never required. It might be worth considering a general-purpose mechanism for passing runtime interface data into templates, rather than solving it service-by-service.
The regex fix is independent and corrects IPv6 literal handling directly.
Lab overview
Reproduces two captive portal IPv6 bugs on OPNsense. kea delegates a
/60via DHCPv6-PD, opnsense tracks it on LAN withtrack6, IPv6-only client hits captive portal.Hosts
3d06:bad:b01:200::1/64/60s (::210::/60–::2f0::/60).3d06:bad:b01:200::11/128, LAN:10.240.240.1/24+3d06:bad:b01:210:be24:11ff:fef5:c938/64(track6)3d06:bad:b01:210::902/64Addressing
3d06:bad:b01:200::/643d06:bad:b01:210::/60–::2f0::/603d06:bad:b01:210::/64Test plan
track6LAN with no hostname overridehttp://[lan-ipv6]:8000/redirurlpreserves the full bracketed IPv6 literalTesting matrix for the two fixes
track6LANhttp://10.240.240.1:8000/...http://[3d06:bad:b01:210:be24:11ff:fef5:c938]:8000/...redirurlredirurl=[2601/redirurl=[2606:4700:4700::1111]/