summaryrefslogtreecommitdiff
path: root/smoketest/scripts/cli
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-09-24 19:01:00 +0000
committerChristian Breunig <christian@breunig.cc>2026-09-24 19:01:00 +0000
commit53b661d60e50edd1079a2f55e0cb3e44e76553e2 (patch)
tree21f5420f71cf4cbaace8943ac4047761dea34414 /smoketest/scripts/cli
parente0ca5c66796337dd992d6572019ea6f4edb475dc (diff)
downloadvyos-1x-53b661d60e50edd1079a2f55e0cb3e44e76553e2.tar.gz
vyos-1x-53b661d60e50edd1079a2f55e0cb3e44e76553e2.zip
ppp: T9060: do not derive a link-local address from a synthetic MAC
A PPP link has no hardware address. RFC 5072 defines the IPv6 interface identifier of such a link as the one negotiated via IPV6CP, and that negotiation is also what guarantees its uniqueness - which is why duplicate address detection is skipped for the link-local address on PPP links. VyOS additionally derived an EUI-64 link-local address from a synthetic MAC address and assigned it to PPPoE and SSTP client interfaces. The peer knows nothing about that address and its uniqueness was never verified. With two link-local addresses present, RFC 6724 source address selection falls through to rule 8 and may pick the synthetic one, so DHCPv6 prefix delegation and router solicitations leave from an address the BRAS never negotiated. ISPs validating the source address silently drop them and delegation stalls. Both interface types now carry only the address negotiated via IPV6CP. A leftover address on an established session is removed on commit.
Diffstat (limited to 'smoketest/scripts/cli')
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pppoe.py80
1 files changed, 74 insertions, 6 deletions
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index 2c240d20e..dfdd3c21f 100755
--- a/smoketest/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -24,8 +24,12 @@ from ipaddress import IPv6Network
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import PPPoEIf
+from vyos.ifconfig.interface import link_local_prefix
from vyos.utils.dict import dict_search_recursive
+from vyos.utils.misc import wait_for
from vyos.utils.network import get_interface_address
+from vyos.utils.network import mac2eui64
from vyos.xml_ref import default_value
config_file: str = '/etc/ppp/peers/{}'
@@ -73,6 +77,19 @@ def wait_for_interface(interface: str, timeout=connect_timeout) -> bool:
return False
return True
+def get_interface_addresses(interface, family) -> list:
+ """ Return the list of addresses of a given family assigned to interface """
+ tmp = get_interface_address(interface)
+ if not tmp or 'addr_info' not in tmp:
+ return []
+ return [addr['local'] for addr in tmp['addr_info']
+ if 'family' in addr and addr['family'] == family]
+
+def has_global_ipv6_address(interface) -> bool:
+ """ Check if the interface got a non link-local IPv6 address assigned """
+ return any(not IPv6Address(addr).is_link_local
+ for addr in get_interface_addresses(interface, 'inet6'))
+
# add a classmethod to setup a temporaray PPPoE server for "proper" validation
class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -137,14 +154,20 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
def _verify_interface_address(self, interface):
# Verify that the assigned IPv4/IPv6 addresses from the BRAS (PPPoE
- # server) are from the assigned pools
- for address in get_interface_address(interface):
- if 'family' in address and address['family'] == 'inet':
+ # server) are from the assigned pools - 'local' is our own address
+ tmp = get_interface_address(interface)
+ self.assertIn('addr_info', tmp)
+
+ for addr_info in tmp['addr_info']:
+ if 'family' not in addr_info:
+ continue
+
+ if addr_info['family'] == 'inet':
# The PPPoE assigned IPv4 address must be from our pool
- self.assertIn(IPv4Address(address['address']), IPv4Network(ipv4_pool))
- elif 'family' in address and address['family'] == 'inet6':
+ self.assertIn(IPv4Address(addr_info['local']), IPv4Network(ipv4_pool))
+ elif addr_info['family'] == 'inet6':
# The PPPoE assigned IPv6 address must be from our pool
- ipv6 = IPv6Address(address['address'])
+ ipv6 = IPv6Address(addr_info['local'])
if not ipv6.is_link_local:
self.assertIn(ipv6, IPv6Network(ipv6_pool))
@@ -327,6 +350,51 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_delete(['interfaces', 'dummy', delegate_if])
+ def test_pppoe_ipv6_link_local(self):
+ # T9060: the IPv6 interface identifier of a PPP link is negotiated
+ # with the peer via IPV6CP (RFC 5072). A second, EUI-64 derived
+ # link-local address is unknown to the BRAS, and RFC 6724 source
+ # address selection may pick it for e.g. DHCPv6-PD.
+ for interface in self._interfaces:
+ (user, passwd) = self.u_p_dict[interface]
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'no-peer-dns'])
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+ # Only with the "ipv6" node present pppd is told to negotiate
+ # IPV6CP - without it the peer config contains "noipv6"
+ self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf'])
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ self.assertTrue(wait_for_interface(interface),
+ msg=f'Interface {interface} not found after {connect_timeout} seconds!')
+
+ # The link-local address is assigned before the router
+ # solicitation is sent out - once a global IPv6 address is present
+ # we know we are not testing too early
+ self.assertTrue(wait_for(has_global_ipv6_address, interface,
+ interval=0.250, timeout=connect_timeout),
+ msg=f'Interface {interface} got no global IPv6 address!')
+
+ link_local = [addr for addr in get_interface_addresses(interface, 'inet6')
+ if IPv6Address(addr).is_link_local]
+
+ # There must be exactly one link-local address - the one negotiated
+ # via IPV6CP
+ self.assertEqual(len(link_local), 1)
+
+ # ... and it must not be the one derived from the synthetic MAC
+ # address of the interface
+ eui64 = mac2eui64(PPPoEIf(interface).get_mac(), link_local_prefix)
+ self.assertNotIn(eui64, link_local)
+
+ # Validate and verify assigned IP addresses
+ self._verify_interface_address(interface)
+
def test_pppoe_options(self):
# Verify access-concentrator and service-name CLI options