summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2025-04-19 15:59:55 +0200
committerChristian Breunig <christian@breunig.cc>2025-04-22 16:06:19 +0200
commitbad519f9f1004e9855e5805473e2e3e8d1fb36ec (patch)
tree25fd165e5232a784da0312d37c7e1877b62ca1cf /python
parent542e3db626ba1184743c4956a340260d0a529c92 (diff)
downloadvyos-1x-bad519f9f1004e9855e5805473e2e3e8d1fb36ec.tar.gz
vyos-1x-bad519f9f1004e9855e5805473e2e3e8d1fb36ec.zip
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.
Diffstat (limited to 'python')
-rw-r--r--python/vyos/ifconfig/interface.py47
1 files 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):
"""