Skip to content

Instantly share code, notes, and snippets.

@tobinger
Last active May 16, 2026 14:24
Show Gist options
  • Select an option

  • Save tobinger/89b3f4d7cdddf98571e20876792eb081 to your computer and use it in GitHub Desktop.

Select an option

Save tobinger/89b3f4d7cdddf98571e20876792eb081 to your computer and use it in GitHub Desktop.
DS-Lite (PPPoEv4 + DHCPv6-PD) silently broken: dhcp6c never started on pppoe interface

⚠️ Obsolete — do not use

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.

The actual fix

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.

Background

The script below is preserved for historical context only.

Summary

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.

Environment

  • 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

Configuration that triggers the bug

/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>.)

Symptoms

  • ifconfig pppoe0 shows UP, RUNNING, only link-local IPv6
  • ifconfig vtnet0 shows only link-local IPv6 (no track6 prefix)
  • ifconfig gif0 shows the interface exists but has no tunnel endpoints and no inet address
  • ls -l /var/etc/dhcp6c.conf → 0 bytes
  • pgrep -lf dhcp6c → no process running for pppoe0 (only the unrelated background instance, if any)
  • Only stale /var/etc/dhcp6c.<old_if>.conf may exist from a previous WAN interface configuration
  • dpinger reports WAN_DHCP6 as Online (because the M-net edge router's link-local IPv6 responds to pings via pppoe0)
  • Auto-generated gateway shows up as OPT1_TUNNELV4 (instead of the configured IPv4_DSLite) because the configured dynamic gateway cannot be resolved at runtime

Root cause analysis

Three code paths could initialize dhcp6c on the PPPoE interface; all three skip it for the PPPoEv4 + DHCPv6 combination:

Path 1: interface_configure() in interfaces.inc

// /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.

Path 2: ppp-ipv6.php (called by ppp-linkup.sh after IPv6CP completion)

// /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.

Net effect

/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.

Relation to commit 315153a07 / forum 35876

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.

Reproducer

On a fresh OPNsense 26.x install:

  1. Configure WAN as PPPoE (e.g. valid German DS-Lite credentials) with <ipaddrv6>dhcp6</ipaddrv6> and <dhcp6-ia-pd-len> set
  2. Boot and let everything settle for several minutes
  3. cat /var/etc/dhcp6c.conf → empty
  4. pgrep -lf dhcp6c → no process bound to pppoe0
  5. ifconfig vtnet0 inet6 → only link-local

Workaround we are running

/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 0

With 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.

Suggested fix direction

A minimal patch that I think would close this without breaking other PPP modes:

  • In interface_configure() and ppp-ipv6.php, instead of bailing whenever both $ipv4_mode and $ipv6_mode are true, recognise the specific combination ipaddr=pppoe + ipaddrv6 in {dhcp6,slaac} as valid and let interface_dhcpv6_prepare/configure run for the PPP interface.

  • Equivalently, the $family == 6 branch of interface_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_mode looks 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.

References

  • 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
@fichtner

fichtner commented May 16, 2026 •

Copy link
Copy Markdown

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

@tobinger

Copy link
Copy Markdown
Author

Wow - Why do it the hard way when there's an easy one?! Thanks for the prompt reply, Franco!
SOLVED.

Cheers,
Tobi

@fichtner

Copy link
Copy Markdown

Hi Tobi, glad it works. 😊 Cheers, Franco

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment