This script was a workaround for a DS-Lite + PPPoE + DHCPv6-PD problem on OPNsense. It is no longer needed. Using it on a current OPNsense will likely make things worse, not better.
On the WAN interface, set IPv4 Configuration Type = None (not PPPoE). Keep IPv6 on DHCPv6 and configure PPPoE as a separate device under Interfaces → Point-to-Point → Devices. That's it — OPNsense's standard dhcp6c path handles everything from there.
The mistake (in case it helps you, fellow pfSense-refugee): replicating the pfSense convention of IPv4 = PPPoE makes OPNsense wait for an IPv4 address that, on DS-Lite, never arrives — which blocks the DHCPv6-PD path that actually carries your connectivity.
- Forum thread with full discussion: https://forum.opnsense.org/index.php?topic=51906.0
- Confirmed by Franco (OPNsense maintainer); this part of the code has been considerably improved.
The script below is preserved for historical context only.
When a WAN interface is configured with both <ipaddr>pppoe</ipaddr> and <ipaddrv6>dhcp6</ipaddrv6> — the standard setup for German DS-Lite providers such as M-net — dhcp6c is never started on the PPPoE interface, and /var/etc/dhcp6c.conf remains empty (0 bytes). No IA_PD request is ever sent, no prefix is delegated, IPv6 never works on LAN, and the DS-Lite GIF tunnel can never be established.
The result is silent: PPPoE comes up cleanly, link-local IPv6 is reachable on pppoe0, dpinger reports the IPv6 WAN gateway as Online — but no global IPv6 ever materializes anywhere.
This appears to be distinct from the known race condition fixed in commit 315153a07 (cf. forum thread 35876) — that fix addresses gif-tunnel reload after late newwanipv6 events. The issue described here happens before that: dhcp6c is never invoked in the first place.
- OPNsense 26.1.2_5 (amd64), fresh install (Q1/2026)
- ISP: M-net München (DS-Lite, RFC 6334; AFTR
2001:a60:0:1::ffff) - WAN encapsulation: VLAN40 → PPPoE → DHCPv6-PD → GIF (4in6 to AFTR)
- Identical setup on pfSense 2.8.1 works without issue on the same hardware/link
/conf/config.xml:
<interfaces>
<wan>
<if>pppoe0</if>
<enable>1</enable>
<ipaddr>pppoe</ipaddr>
<ipaddrv6>dhcp6</ipaddrv6>
<dhcp6-ia-pd-len>8</dhcp6-ia-pd-len>
...
</wan>
<opt1>
<if>gif0</if>
<enable>1</enable>
</opt1>
</interfaces>
<gifs>
<gif>
<if>lan</if>
<ipaddr/>
<tunnel-local-addr>192.0.0.2</tunnel-local-addr>
<tunnel-remote-addr>192.0.0.1</tunnel-remote-addr>
<tunnel-remote-net>29</tunnel-remote-net>
<remote-addr>2001:a60:0:1::ffff</remote-addr>
<gifif>gif0</gifif>
</gif>
</gifs>(That is the exact output of pfopn-convert for a working pfSense DS-Lite config, plus the standard PPPoE definition under <ppps>.)
ifconfig pppoe0showsUP, RUNNING, only link-local IPv6ifconfig vtnet0shows only link-local IPv6 (no track6 prefix)ifconfig gif0shows the interface exists but has no tunnel endpoints and no inet addressls -l /var/etc/dhcp6c.conf→ 0 bytespgrep -lf dhcp6c→ no process running for pppoe0 (only the unrelated background instance, if any)- Only stale
/var/etc/dhcp6c.<old_if>.confmay exist from a previous WAN interface configuration - dpinger reports
WAN_DHCP6as Online (because the M-net edge router's link-local IPv6 responds to pings viapppoe0) - Auto-generated gateway shows up as
OPT1_TUNNELV4(instead of the configuredIPv4_DSLite) because the configureddynamicgateway cannot be resolved at runtime
Three code paths could initialize dhcp6c on the PPPoE interface; all three skip it for the PPPoEv4 + DHCPv6 combination:
// /usr/local/etc/inc/interfaces.inc around line 2527-2538
switch (is_ipv6_allowed() ? ($wancfg['ipaddrv6'] ?? 'none') : 'none') {
case 'slaac':
case 'dhcp6':
mwexecf('/sbin/ifconfig %s inet6 accept_rtadv -ifdisabled up', $device);
if (!interface_ppps_bound($interface)) { // <-- guard
interface_dhcpv6_prepare($interface, $wancfg);
interface_dhcpv6_configure($interface, $wancfg);
}
break;interface_ppps_bound($interface) (called without $family argument) returns $ipv4_mode || $ipv6_mode. For DS-Lite both are true, so the guard short-circuits and dhcpv6_prepare/configure is never called.
// /usr/local/opnsense/scripts/interfaces/ppp-ipv6.php
if (!interface_ppps_bound($interface, $family)) { // family=6
exit(1);
}interface_ppps_bound($interface, 6) for our config:
} elseif ($family == 6) {
/* use this to steer configuration based on newwanipv6 event */
$bound = !$ipv4_mode && $ipv6_mode;
}= !true && true = false → !false = true → exit(1).
So this path is also dead for PPPoEv4 + DHCPv6 setups.
Path 3: rc.newwanipv6 (called by configctl interface newipv6 pppoe0 force fallback in ppp-linkup.sh)
Reading /usr/local/etc/rc.newwanipv6 end-to-end: it reconfigures gif/gre tunnels, refreshes routing, syncs filters, fires VPN/newwanip plugins — but it never calls interface_dhcpv6_prepare() or interface_dhcpv6_configure(). So no dhcp6c.{device}.conf is written, and dhcp6c is not started.
/var/etc/dhcp6c.conf is the concatenation of all dhcp6c.{$_wancfg['if']}.conf files (loop near line 3050). Since none of the three paths above writes dhcp6c.pppoe0.conf, the master file stays empty. The next time something tries to start dhcp6c -c /var/etc/dhcp6c.conf, it has nothing to do.
Franco's earlier fix addresses the case where rc.newwanipv6 fires after interface_dhcpv6_prepare has already run with no IPv6 GUA available, leaving the gif tunnel unconfigured. That fix assumes dhcp6c was started — it patches the gif/track6 dependent reload.
The case described here is one step earlier: dhcp6c itself is never started when both PPPoE-v4 and DHCPv6-PD are configured on the same interface. The fix 315153a07 does not help.
On a fresh OPNsense 26.x install:
- Configure WAN as PPPoE (e.g. valid German DS-Lite credentials) with
<ipaddrv6>dhcp6</ipaddrv6>and<dhcp6-ia-pd-len>set - Boot and let everything settle for several minutes
cat /var/etc/dhcp6c.conf→ emptypgrep -lf dhcp6c→ no process bound to pppoe0ifconfig vtnet0 inet6→ only link-local
/usr/local/etc/rc.syshook.d/start/99-dslite-gif-rebuild (excerpt; full script below):
#!/bin/sh
(
sleep 5
# wait for pppoe0 RUNNING
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
ifconfig pppoe0 2>/dev/null | grep -q RUNNING && break
sleep 5
done
# write dhcp6c.conf manually for pppoe0
cat > /var/etc/dhcp6c.conf <<EOF
interface pppoe0 {
send ia-na 0;
send ia-pd 0;
request domain-name-servers;
request domain-name;
script "/var/etc/dhcp6c_wan_script.sh";
};
id-assoc na 0 { };
id-assoc pd 0 {
prefix ::/56 infinity;
prefix-interface vtnet0 {
sla-id 0;
sla-len 8;
};
};
EOF
# stop any stale dhcp6c, start fresh on pppoe0
pkill -f /usr/local/sbin/dhcp6c 2>/dev/null
sleep 1
/usr/local/sbin/dhcp6c -d -c /var/etc/dhcp6c.conf -p /var/run/dhcp6c.pid pppoe0
# wait for delegated prefix to land on LAN via track6, then rebuild gif0
LAN_V6=""
for i in $(seq 1 60); do
LAN_V6=$(ifconfig vtnet0 inet6 | awk '/inet6 2/{print $2; exit}')
[ -n "$LAN_V6" ] && break
sleep 5
done
[ -z "$LAN_V6" ] && exit 1
ifconfig gif0 destroy 2>/dev/null
ifconfig gif0 create
ifconfig gif0 inet6 tunnel "$LAN_V6" 2001:a60:0:1::ffff
ifconfig gif0 inet 192.0.0.2 192.0.0.1 netmask 255.255.255.248
ifconfig gif0 mtu 1280 up
route add -inet default 192.0.0.1
) &
exit 0With this script in place, boot-to-Internet is reliable in ~10–60 s (whatever M-net needs for IA_PD). It survives PPPoE prefix changes only across reboots — a runtime prefix change (without reboot) would require additional plumbing, which we have not needed yet.
A minimal patch that I think would close this without breaking other PPP modes:
-
In
interface_configure()andppp-ipv6.php, instead of bailing whenever both$ipv4_modeand$ipv6_modeare true, recognise the specific combinationipaddr=pppoe+ipaddrv6 in {dhcp6,slaac}as valid and letinterface_dhcpv6_prepare/configurerun for the PPP interface. -
Equivalently, the
$family == 6branch ofinterface_ppps_bound()could be:$bound = $ipv6_mode;
for cases where the PPP link's IPv6 side does not conflict with a separate v6 setup. The existing
!$ipv4_mode && $ipv6_modelooks intended to exclude pure-PPPoE-v6 from doubled events, but as written it also excludes the entirely legitimate v4-PPPoE-plus-DHCPv6-PD case.
Happy to test any patch on this DS-Lite link and report back.
- Forum thread that documents the earlier (different) race: https://forum.opnsense.org/index.php?topic=35876.0
- Commit
315153a07(Franco) addressing that earlier race - RFC 6334 — DHCPv6 option for DS-Lite (AFTR address)
- M-net documentation noting non-persistent DHCPv6-PD prefixes
Hi, can’t post in the forum today. If this is DS-Lite why set PPPoE on IPv4? We considerably improved that part in our code, so in your case it should be set to None so that IPv6 knows it’s the main connectivity. Otherwise it thinks IPv4 should come up first which likely never does.
Cheers,
Franco