From 17c12bde5c6f314311e7524842fd1ddc254009b4 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Tue, 23 Jul 2024 19:03:07 +0200 Subject: interface: T6592: remove interface from conntrack ct_iface_map on deletion We always have had stale interface entries in the ct_iface_map of nftables/ conntrack for any interface that once belonged to a VRF. This commit will always clean the nftables interface map when the interface is deleted from the system. --- python/vyos/ifconfig/interface.py | 61 ++++++++++++++++++++------------------- python/vyos/ifconfig/l2tpv3.py | 12 +++++++- 2 files changed, 43 insertions(+), 30 deletions(-) (limited to 'python') diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 748830004..72d3d3afe 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -383,6 +383,9 @@ class Interface(Control): # can not delete ALL interfaces, see below self.flush_addrs() + # remove interface from conntrack VRF interface map + self._del_interface_from_ct_iface_map() + # --------------------------------------------------------------------- # Any class can define an eternal regex in its definition # interface matching the regex will not be deleted @@ -403,36 +406,20 @@ class Interface(Control): if netns: cmd = f'ip netns exec {netns} {cmd}' return self._cmd(cmd) - def _set_vrf_ct_zone(self, vrf, old_vrf_tableid=None): - """ - Add/Remove rules in nftables to associate traffic in VRF to an - individual conntack zone - """ - # Don't allow for netns yet - if 'netns' in self.config: - return None - - def nft_check_and_run(nft_command): - # Check if deleting is possible first to avoid raising errors - _, err = self._popen(f'nft --check {nft_command}') - if not err: - # Remove map element - self._cmd(f'nft {nft_command}') + def _nft_check_and_run(self, nft_command): + # Check if deleting is possible first to avoid raising errors + _, err = self._popen(f'nft --check {nft_command}') + if not err: + # Remove map element + self._cmd(f'nft {nft_command}') - if vrf: - # Get routing table ID for VRF - vrf_table_id = get_vrf_tableid(vrf) - # Add map element with interface and zone ID - if vrf_table_id: - # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF - if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id): - nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' - nft_check_and_run(nft_del_element) + def _del_interface_from_ct_iface_map(self): + nft_command = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' + self._nft_check_and_run(nft_command) - self._cmd(f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}') - else: - nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' - nft_check_and_run(nft_del_element) + def _add_interface_to_ct_iface_map(self, vrf_table_id: int): + nft_command = f'add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}' + self._nft_check_and_run(nft_command) def get_min_mtu(self): """ @@ -605,6 +592,10 @@ class Interface(Control): >>> Interface('eth0').set_vrf() """ + # Don't allow for netns yet + if 'netns' in self.config: + return False + tmp = self.get_interface('vrf') if tmp == vrf: return False @@ -612,7 +603,19 @@ class Interface(Control): # Get current VRF table ID old_vrf_tableid = get_vrf_tableid(self.ifname) self.set_interface('vrf', vrf) - self._set_vrf_ct_zone(vrf, old_vrf_tableid) + + if vrf: + # Get routing table ID number for VRF + vrf_table_id = get_vrf_tableid(vrf) + # Add map element with interface and zone ID + if vrf_table_id: + # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF + if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id): + self._del_interface_from_ct_iface_map() + self._add_interface_to_ct_iface_map(vrf_table_id) + else: + self._del_interface_from_ct_iface_map() + return True def set_arp_cache_tmo(self, tmo): diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 85a89ef8b..c1f2803ee 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -90,9 +90,17 @@ class L2TPv3If(Interface): """ if self.exists(self.ifname): - # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') + # remove all assigned IP addresses from interface - this is a bit redundant + # as the kernel will remove all addresses on interface deletion + self.flush_addrs() + + # remove interface from conntrack VRF interface map, here explicitly and do not + # rely on the base class implementation as the interface will + # vanish as soon as the l2tp session is deleted + self._del_interface_from_ct_iface_map() + if {'tunnel_id', 'session_id'} <= set(self.config): cmd = 'ip l2tp del session tunnel_id {tunnel_id}' cmd += ' session_id {session_id}' @@ -101,3 +109,5 @@ class L2TPv3If(Interface): if 'tunnel_id' in self.config: cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' self._cmd(cmd.format(**self.config)) + + # No need to call the baseclass as the interface is now already gone -- cgit v1.2.3 From 1c42ee9d16dd49fff2cbde652bf24a38f364526c Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 24 Jul 2024 11:19:16 +0200 Subject: smoketest: T6592: verify no interface stalls in conntrack ct_iface_map on deletion Now that interfaces are deleted from ct_iface_map during deletion it's time to also add a smoketest ensuring there is no entry in the ct_iface_map once an interface was deleted from the CLI. --- python/vyos/utils/network.py | 28 +++++++++++++++++++++++++++ smoketest/scripts/cli/base_interfaces_test.py | 10 +++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) (limited to 'python') diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 8406a5638..8fce08de0 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -569,3 +569,31 @@ def ipv6_prefix_length(low, high): return plen + i + 1 return None + +def get_nft_vrf_zone_mapping() -> dict: + """ + Retrieve current nftables conntrack mapping list from Kernel + + returns: [{'interface': 'red', 'vrf_tableid': 1000}, + {'interface': 'eth2', 'vrf_tableid': 1000}, + {'interface': 'blue', 'vrf_tableid': 2000}] + """ + from json import loads + from jmespath import search + from vyos.utils.process import cmd + output = [] + tmp = loads(cmd('sudo nft -j list table inet vrf_zones')) + # {'nftables': [{'metainfo': {'json_schema_version': 1, + # 'release_name': 'Old Doc Yak #3', + # 'version': '1.0.9'}}, + # {'table': {'family': 'inet', 'handle': 6, 'name': 'vrf_zones'}}, + # {'map': {'elem': [['eth0', 666], + # ['dum0', 666], + # ['wg500', 666], + # ['bond10.666', 666]], + vrf_list = search('nftables[].map.elem | [0]', tmp) + if not vrf_list: + return output + for (vrf_name, vrf_id) in vrf_list: + output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id}) + return output diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 4072fd5c2..012bd0524 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -15,7 +15,6 @@ from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses -from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM @@ -25,13 +24,15 @@ from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.file import read_file from vyos.utils.dict import dict_search +from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_vrf from vyos.utils.network import get_vrf_tableid -from vyos.utils.process import cmd +from vyos.utils.network import interface_exists from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local +from vyos.utils.network import get_nft_vrf_zone_mapping from vyos.xml_ref import cli_defined dhclient_base_dir = directories['isc_dhclient_dir'] @@ -117,8 +118,11 @@ class BasicInterfaceTest: self.cli_commit() # Verify that no previously interface remained on the system + ct_map = get_nft_vrf_zone_mapping() for intf in self._interfaces: - self.assertNotIn(intf, interfaces()) + self.assertFalse(interface_exists(intf)) + for map_entry in ct_map: + self.assertNotEqual(intf, map_entry['interface']) # No daemon started during tests should remain running for daemon in ['dhcp6c', 'dhclient']: -- cgit v1.2.3