From e9fb2078d5ea82e1d9186ee8ef1dd982591954d0 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 19 Apr 2025 15:18:44 +0200 Subject: interface: T7375: SLAAC assigned address is not cleared when removing SLAAC --- python/vyos/ifconfig/interface.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 979b62578..85f994e08 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -909,7 +909,10 @@ class Interface(Control): tmp = self.get_interface('ipv6_autoconf') if tmp == autoconf: return None - return self.set_interface('ipv6_autoconf', autoconf) + rc = self.set_interface('ipv6_autoconf', autoconf) + if autoconf == '0': + self.flush_ipv6_slaac_addrs() + return rc def add_ipv6_eui64_address(self, prefix): """ @@ -1310,6 +1313,34 @@ class Interface(Control): # flush all addresses self._cmd(cmd) + def flush_ipv6_slaac_addrs(self): + """ + Flush all IPv6 addresses installed in response to router advertisement + messages from this interface. + + Will raise an exception on error. + """ + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + tmp = get_interface_address(self.ifname) + if 'addr_info' not in tmp: + return + + # Parse interface IP addresses. Example data: + # {'family': 'inet6', 'local': '2001:db8:1111:0:250:56ff:feb3:38c5', + # 'prefixlen': 64, 'scope': 'global', 'dynamic': True, + # 'mngtmpaddr': True, 'protocol': 'kernel_ra', + # 'valid_life_time': 2591987, 'preferred_life_time': 14387} + for addr_info in tmp['addr_info']: + if 'protocol' not in addr_info: + continue + if (addr_info['protocol'] == 'kernel_ra' and + addr_info['scope'] == 'global'): + # Flush IPv6 addresses installed by router advertisement + ra_addr = f"{addr_info['local']}/{addr_info['prefixlen']}" + cmd = f'{netns_cmd} ip -6 addr del dev {self.ifname} {ra_addr}' + self._cmd(cmd) + def add_to_bridge(self, bridge_dict): """ Adds the interface to the bridge with the passed port config. -- cgit v1.2.3 From 542e3db626ba1184743c4956a340260d0a529c92 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 19 Apr 2025 15:50:37 +0200 Subject: interface: T7375: remove superfluous "ifname = self.ifname" assignment We can reference "self.ifname" in any Python f-ormatted string directly. No need for an interim temporary variable. --- python/vyos/ifconfig/interface.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 85f994e08..85de0947a 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1351,8 +1351,6 @@ class Interface(Control): # drop all interface addresses first self.flush_addrs() - ifname = self.ifname - for bridge, bridge_config in bridge_dict.items(): # add interface to bridge - use Section.klass to get BridgeIf class Section.klass(bridge)(bridge, create=True).add_port(self.ifname) @@ -1368,7 +1366,7 @@ class Interface(Control): bridge_vlan_filter = Section.klass(bridge)(bridge, create=True).get_vlan_filter() if int(bridge_vlan_filter): - cur_vlan_ids = get_vlan_ids(ifname) + cur_vlan_ids = get_vlan_ids(self.ifname) add_vlan = [] native_vlan_id = None allowed_vlan_ids= [] @@ -1391,15 +1389,15 @@ class Interface(Control): # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): - cmd = f'bridge vlan del dev {ifname} vid {vlan} master' + cmd = f'bridge vlan del dev {self.ifname} vid {vlan} master' self._cmd(cmd) for vlan in allowed_vlan_ids: - cmd = f'bridge vlan add dev {ifname} vid {vlan} master' + cmd = f'bridge vlan add dev {self.ifname} vid {vlan} master' self._cmd(cmd) # Setting native VLAN to system if native_vlan_id: - cmd = f'bridge vlan add dev {ifname} vid {native_vlan_id} pvid untagged master' + cmd = f'bridge vlan add dev {self.ifname} vid {native_vlan_id} pvid untagged master' self._cmd(cmd) def set_dhcp(self, enable: bool, vrf_changed: bool=False): @@ -1478,12 +1476,11 @@ class Interface(Control): if enable not in [True, False]: raise ValueError() - ifname = self.ifname config_base = directories['dhcp6_client_dir'] - config_file = f'{config_base}/dhcp6c.{ifname}.conf' - script_file = f'/etc/wide-dhcpv6/dhcp6c.{ifname}.script' # can not live under /run b/c of noexec mount option - systemd_override_file = f'/run/systemd/system/dhcp6c@{ifname}.service.d/10-override.conf' - systemd_service = f'dhcp6c@{ifname}.service' + config_file = f'{config_base}/dhcp6c.{self.ifname}.conf' + script_file = f'/etc/wide-dhcpv6/dhcp6c.{self.ifname}.script' # can not live under /run b/c of noexec mount option + systemd_override_file = f'/run/systemd/system/dhcp6c@{self.ifname}.service.d/10-override.conf' + systemd_service = f'dhcp6c@{self.ifname}.service' # Rendered client configuration files require additional settings config = deepcopy(self.config) -- cgit v1.2.3 From bad519f9f1004e9855e5805473e2e3e8d1fb36ec Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 19 Apr 2025 15:59:55 +0200 Subject: interface: T7375: routes received via SLAAC are not cleared on exit When using SLAAC for IPv6 addresses we will also receive a default route via a RA (Router Advertisement). When we disable SLAAC on a interface the Linux Kernel does not automatically flush all addresses nor the routes received. The Kernel wait's until the addresses/prefixes/routes expire using their lifestime setting. When removing SLAAC from an interface, also remove the auto generated IPv6 address and both the default router received and the connected IP prefix of the SLAAC advertisement. --- python/vyos/ifconfig/interface.py | 47 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 85de0947a..baa45f5bd 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -22,6 +22,7 @@ from copy import deepcopy from glob import glob from ipaddress import IPv4Network +from ipaddress import IPv6Interface from netifaces import ifaddresses # this is not the same as socket.AF_INET/INET6 from netifaces import AF_INET @@ -911,7 +912,8 @@ class Interface(Control): return None rc = self.set_interface('ipv6_autoconf', autoconf) if autoconf == '0': - self.flush_ipv6_slaac_addrs() + flushed = self.flush_ipv6_slaac_addrs() + self.flush_ipv6_slaac_routes(flushed) return rc def add_ipv6_eui64_address(self, prefix): @@ -1313,12 +1315,13 @@ class Interface(Control): # flush all addresses self._cmd(cmd) - def flush_ipv6_slaac_addrs(self): + def flush_ipv6_slaac_addrs(self) -> list: """ Flush all IPv6 addresses installed in response to router advertisement messages from this interface. Will raise an exception on error. + Will return a list of flushed IPv6 addresses. """ netns = get_interface_namespace(self.ifname) netns_cmd = f'ip netns exec {netns}' if netns else '' @@ -1331,6 +1334,7 @@ class Interface(Control): # 'prefixlen': 64, 'scope': 'global', 'dynamic': True, # 'mngtmpaddr': True, 'protocol': 'kernel_ra', # 'valid_life_time': 2591987, 'preferred_life_time': 14387} + flushed = [] for addr_info in tmp['addr_info']: if 'protocol' not in addr_info: continue @@ -1338,8 +1342,47 @@ class Interface(Control): addr_info['scope'] == 'global'): # Flush IPv6 addresses installed by router advertisement ra_addr = f"{addr_info['local']}/{addr_info['prefixlen']}" + flushed.append(ra_addr) cmd = f'{netns_cmd} ip -6 addr del dev {self.ifname} {ra_addr}' self._cmd(cmd) + return flushed + + def flush_ipv6_slaac_routes(self, ra_addrs: list=[]) -> None: + """ + Flush IPv6 default routes installed in response to router advertisement + messages from this interface. + + Will raise an exception on error. + """ + # Do not flush default route if interface uses DHCPv6 in addition to SLAAC + if 'address' in self.config and 'dhcpv6' in self.config['address']: + return None + + # Find IPv6 connected prefixes for flushed SLAAC addresses + connected = [] + for addr in ra_addrs: + connected.append(str(IPv6Interface(addr).network)) + + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + + tmp = self._cmd(f'{netns_cmd} ip -j -6 route show dev {self.ifname}') + tmp = json.loads(tmp) + # Parse interface routes. Example data: + # {'dst': 'default', 'gateway': 'fe80::250:56ff:feb3:cdba', + # 'protocol': 'ra', 'metric': 1024, 'flags': [], 'expires': 1398, + # 'metrics': [{'hoplimit': 64}], 'pref': 'medium'} + for route in tmp: + # If it's a default route received from RA, delete it + if (dict_search('dst', route) == 'default' and + dict_search('protocol', route) == 'ra'): + self._cmd(f'{netns_cmd} ip -6 route del default via {route["gateway"]} dev {self.ifname}') + # Remove connected prefixes received from RA + if dict_search('dst', route) in connected: + # If it's a connected prefix, delete it + self._cmd(f'{netns_cmd} ip -6 route del {route["dst"]} dev {self.ifname}') + + return None def add_to_bridge(self, bridge_dict): """ -- cgit v1.2.3 From 563488b1234560cfd3cb5aa9c8ec3f4b7f10d86b Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 20 Apr 2025 20:59:14 +0200 Subject: sysctl: T7379: always disable IPv6 autoconf and accept_ra during startup --- src/etc/sysctl.d/30-vyos-router.conf | 10 ++++++++++ src/systemd/vyos.target | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 76be41ddc..ef81cebac 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -83,6 +83,16 @@ net.ipv4.conf.default.ignore_routes_with_linkdown=1 net.ipv6.conf.all.ignore_routes_with_linkdown=1 net.ipv6.conf.default.ignore_routes_with_linkdown=1 +# Disable IPv6 interface autoconfigurationnable packet forwarding for IPv6 +net.ipv6.conf.all.autoconf=0 +net.ipv6.conf.default.autoconf=0 +net.ipv6.conf.*.autoconf=0 + +# Disable IPv6 router advertisements +net.ipv6.conf.all.accept_ra=0 +net.ipv6.conf.default.accept_ra=0 +net.ipv6.conf.*.accept_ra=0 + # Enable packet forwarding for IPv6 net.ipv6.conf.all.forwarding=1 diff --git a/src/systemd/vyos.target b/src/systemd/vyos.target index c5d04891d..ea1593fe9 100644 --- a/src/systemd/vyos.target +++ b/src/systemd/vyos.target @@ -1,3 +1,3 @@ [Unit] Description=VyOS target -After=multi-user.target vyos-grub-update.service +After=multi-user.target vyos-grub-update.service systemd-sysctl.service -- cgit v1.2.3 From de44c6aef249b5c3350a5114a38eee3a761f7de0 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 20 Apr 2025 20:59:57 +0200 Subject: interface: T7379: do not request SLAAC default route when only DHCPv6 is set When an interface runs in DHCPv6 only mode, there is no reason to have a default installed that was received via SLAAC. If SLAAC is needed, it should be turned on explicitly. This bug was only triggered during system boot where a DHCPv6 client address and a default route to a link-local address was shown in the system. If DHCPv6 was enabled only on an interface while VyOS was already running - no default route got installed. --- python/vyos/ifconfig/interface.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index baa45f5bd..337e3ec63 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -913,7 +913,7 @@ class Interface(Control): rc = self.set_interface('ipv6_autoconf', autoconf) if autoconf == '0': flushed = self.flush_ipv6_slaac_addrs() - self.flush_ipv6_slaac_routes(flushed) + self.flush_ipv6_slaac_routes(ra_addrs=flushed) return rc def add_ipv6_eui64_address(self, prefix): @@ -1326,7 +1326,7 @@ class Interface(Control): netns = get_interface_namespace(self.ifname) netns_cmd = f'ip netns exec {netns}' if netns else '' tmp = get_interface_address(self.ifname) - if 'addr_info' not in tmp: + if not tmp or 'addr_info' not in tmp: return # Parse interface IP addresses. Example data: @@ -1354,13 +1354,9 @@ class Interface(Control): Will raise an exception on error. """ - # Do not flush default route if interface uses DHCPv6 in addition to SLAAC - if 'address' in self.config and 'dhcpv6' in self.config['address']: - return None - # Find IPv6 connected prefixes for flushed SLAAC addresses connected = [] - for addr in ra_addrs: + for addr in ra_addrs if isinstance(ra_addrs, list) else []: connected.append(str(IPv6Interface(addr).network)) netns = get_interface_namespace(self.ifname) @@ -1865,9 +1861,7 @@ class Interface(Control): # IPv6 router advertisements tmp = dict_search('ipv6.address.autoconf', config) - value = '2' if (tmp != None) else '1' - if 'dhcpv6' in new_addr: - value = '2' + value = '2' if (tmp != None) else '0' self.set_ipv6_accept_ra(value) # IPv6 address autoconfiguration -- cgit v1.2.3