Summary
When IPv6 is enabled on a PPPoE interface, VyOS adds a synthetic EUI64-derived link-local address (fe80::<hash>/64) on top of the link-local already assigned by the PPP daemon during IPv6CP negotiation. This extra address may interfere with DHCPv6 prefix delegation on ISPs that validate the source link-local of DHCPv6 packets.
Steps to Reproduce
set interfaces pppoe pppoe0 source-interface eth0.20 set interfaces pppoe pppoe0 authentication username <user> set interfaces pppoe pppoe0 authentication password <pass> set interfaces pppoe pppoe0 ipv6 address autoconf set interfaces pppoe pppoe0 dhcpv6-options pd 0 length 56 set interfaces pppoe pppoe0 dhcpv6-options pd 0 interface <if> address 1 set interfaces pppoe pppoe0 dhcpv6-options pd 0 interface <if> sla-id 1 commit
Observed Behavior
Two link-local addresses appear on the interface:
$ ip -6 addr show pppoe0 inet6 fe80::f94b:90ff:fefa:a73c/64 scope link valid_lft forever preferred_lft forever inet6 fe80::bd27:a8d:ab4a:60af peer fe80::1/128 scope link valid_lft forever preferred_lft forever
The /64 address is synthetic — derived by VyOS from a SHA256 hash of the CPU ID, first Ethernet MAC address, and interface name. The peer fe80::1/128 address is assigned by pppd during IPv6CP negotiation.
Impact
DHCPv6 prefix delegation fails on ISPs that validate the source link-local of DHCPv6 packets.
When two link-local addresses are present, dhcp6c relies on the kernel's RFC 6724 source address selection to pick one. Under Rule 8 (longest prefix match), the synthetic /64 address is preferred: it has a /64 prefix on fe80::/64, so the kernel considers it the owner of the link-local subnet. The IPv6CP-negotiated address has a /128 peer scope and does not compete on prefix length.
As a result, dhcp6c sends Solicit/Request packets from the synthetic address rather than the IPv6CP-negotiated one:
Many ISPs validate that DHCPv6 Solicit/Request packets originate from the same link-local that was negotiated during IPv6CP. Requests arriving from the synthetic address are silently dropped, causing starvation:
Jul 06 21:15:15 perimeter dhcp6c[9126]: dhcp6_reset_timer: reset a timer on pppoe0, state=INIT, timeo=0, retrans=383 Jul 06 21:15:15 perimeter dhcp6c[9126]: client6_send: a new XID (56f7cb) is generated Jul 06 21:15:15 perimeter dhcp6c[9126]: copy_option: set client ID (len 14) Jul 06 21:15:15 perimeter dhcp6c[9126]: copy_option: set elapsed time (len 2) Jul 06 21:15:15 perimeter dhcp6c[9126]: copyout_option: set IA_PD prefix Jul 06 21:15:15 perimeter dhcp6c[9126]: copyout_option: set IA_PD Jul 06 21:15:15 perimeter dhcp6c[9126]: client6_send: send solicit to ff02::1:2%pppoe0 Jul 06 21:15:15 perimeter dhcp6c[9126]: dhcp6_reset_timer: reset a timer on pppoe0, state=SOLICIT, timeo=0, retrans=1088 Jul 06 21:15:16 perimeter dhcp6c[9126]: copy_option: set client ID (len 14) Jul 06 21:15:16 perimeter dhcp6c[9126]: copy_option: set elapsed time (len 2) Jul 06 21:15:16 perimeter dhcp6c[9126]: copyout_option: set IA_PD prefix Jul 06 21:15:16 perimeter dhcp6c[9126]: copyout_option: set IA_PD Jul 06 21:15:16 perimeter dhcp6c[9126]: client6_send: send solicit to ff02::1:2%pppoe0 Jul 06 21:15:16 perimeter dhcp6c[9126]: dhcp6_reset_timer: reset a timer on pppoe0, state=SOLICIT, timeo=1, retrans=2151 Jul 06 21:15:19 perimeter dhcp6c[9126]: copy_option: set client ID (len 14) Jul 06 21:15:19 perimeter dhcp6c[9126]: copy_option: set elapsed time (len 2) Jul 06 21:15:19 perimeter dhcp6c[9126]: copyout_option: set IA_PD prefix Jul 06 21:15:19 perimeter dhcp6c[9126]: copyout_option: set IA_PD Jul 06 21:15:19 perimeter dhcp6c[9126]: client6_send: send solicit to ff02::1:2%pppoe0 Jul 06 21:15:19 perimeter dhcp6c[9126]: dhcp6_reset_timer: reset a timer on pppoe0, state=SOLICIT, timeo=2, retrans=4283 ...
Root Cause
PPPoEIf.get_mac() overrides the base class to return a synthetic MAC address rather than empty, as PPP interfaces have no hardware MAC. This bypasses the guard in Interface.add_ipv6_eui64_address():
def add_ipv6_eui64_address(self, prefix): mac = self.get_mac() if mac: # always True for PPPoE due to the synthetic MAC override eui64 = mac2eui64(mac, prefix) self.add_addr(f'{eui64}/{prefix.split("/")[1]}')
Whether adding a synthetic EUI64 link-local is appropriate on a point-to-point PPP interface is debatable — pppd already provides one via IPv6CP. What is clear is that having two link-locals causes the source address selection issue described above on ISPs that enforce IPv6CP link-local validation.
The no-default-link-local option already suppresses this behaviour on other interface types via Interface.update(), but it is not exposed in the PPPoE interface definition (interface-definitions/interfaces_pppoe.xml.in).
Proposed Fix
Add the missing include to the PPPoE XML interface definition:
<node name="address">
<children>
#include <include/interface/ipv6-address-autoconf.xml.i>
#include <include/interface/ipv6-address-interface-identifier.xml.i>
#include <include/interface/ipv6-address-no-default-link-local.xml.i>
</children>
</node>This fix is intentionally conservative: the synthetic link-local remains the default, and only users affected by ISP-side validation need to opt out explicitly:
set interfaces pppoe pppoe0 ipv6 address no-default-link-local
Test Environment
- Version: VyOS 2026.06.30-0048-rolling
- Hardware: PC Engines apu3
- ISP: DIGI Spain
A pull request with the fix and a smoketest will be linked shortly.