Page MenuHomeVyOS Platform

pppoe: spurious synthetic EUI64 /64 link-local address breaks DHCPv6-PD
Closed, ResolvedPublicBUG

Description

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.

Details

Version
VyOS 2026.06.30-0048-rolling
Is it a breaking change?
Perfectly compatible
Issue type
Bug (incorrect behavior)

Event Timeline

c-po changed the task status from Open to Needs testing.Thu, Sep 24, 7:14 PM
c-po claimed this task.

Interface nicely shows the one only link local address

vyos@vyos:~$ ip addr show dev pppoe0 | strip-private
16: pppoe0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc noqueue state UNKNOWN group default qlen 3
    link/ppp
    inet xxx.xxx.94.232 peer xxxxx.tld/32 scope global pppoe0
       valid_lft forever preferred_lft forever
    inet6 xxxx:xxxx:7ff:3d9b:301a:1390:c80a:2d82/64 scope global dynamic mngtmpaddr proto kernel_ra
       valid_lft 14042sec preferred_lft 1442sec
    inet6 fe80::301a:1390:c80a:2d82 peer fe80::f6cc:55ff:fe89:ea43/128 scope link
       valid_lft forever preferred_lft forever