diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-03-25 14:53:23 +0200 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2026-03-27 16:19:01 +0200 |
| commit | 2f0ef2bfe9d36228795ae46eee23031a37ab5957 (patch) | |
| tree | 43ab55fed122346d9c2c8da0fb7eb88812768de2 | |
| parent | e66c23ddf1f70fd7aa31fbc696a361a376ea15d1 (diff) | |
| download | vyos-1x-2f0ef2bfe9d36228795ae46eee23031a37ab5957.tar.gz vyos-1x-2f0ef2bfe9d36228795ae46eee23031a37ab5957.zip | |
vpp: T8419: Disallow cross-membership between VPP and kernel bonding/bridge interfaces
| -rw-r--r-- | python/vyos/vpp/utils.py | 20 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpp.py | 39 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_bonding.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_bridge.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 39 |
5 files changed, 113 insertions, 7 deletions
diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 88b641e66..10940b5a4 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -112,11 +112,14 @@ def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]: return vpp_ifaces -def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]: +def cli_ethernet_with_vifs_ifaces( + config_instance, include_nested_vifs=False +) -> list[str]: """List of all VPP Ethernet interfaces with VIFs Args: config_instance (VyOS Config): VyOS Config instance + include_nested_vifs (bool): Include Q-in-Q/customer VIFs if True Returns: list[str]: list of interfaces @@ -135,17 +138,26 @@ def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]: ifaces: list[str] = [] # Get a list of Ethernet interfaces - for iface in config.get('settings', {}).get('interface', {}).keys(): - ifaces.append(iface) + parent_ifaces = list(config.get('settings', {}).get('interface', {}).keys()) # Add Ethernet interfaces with VIFs - for iface in ifaces: + for iface in parent_ifaces: + ifaces.append(iface) _, iface_config = get_interface_dict( config_instance, ['interfaces', 'ethernet'], ifname=iface ) ifaces.extend([f'{iface}.{vif}' for vif in iface_config.get('vif', {})]) ifaces.extend([f'{iface}.{vif_s}' for vif_s in iface_config.get('vif_s', {})]) + if include_nested_vifs: + for vif_s, vif_s_config in iface_config.get('vif_s', {}).items(): + ifaces.extend( + [ + f'{iface}.{vif_s}.{vif_c}' + for vif_c in vif_s_config.get('vif_c', {}) + ] + ) + return ifaces diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index a98b5ff0b..0e864758d 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -1378,6 +1378,45 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): # Ensure that VPP process is active self.assertTrue(process_named_running(PROCESS_NAME)) + def test_22_no_vpp_kernel_bridge_cross_membership(self): + vlan = '123' + member = f'{interface}.{vlan}' + bridge_iface = 'br1' + + self.cli_commit() + + # Ensure that VPP process is active + self.assertTrue(process_named_running(PROCESS_NAME)) + + # Attempt to add a VPP interface VLAN as a bridge member + self.cli_set(['interfaces', 'ethernet', interface, 'vif', vlan]) + self.cli_set( + ['interfaces', 'bridge', bridge_iface, 'member', 'interface', member] + ) + + # Adding a VPP interface (or its VLAN) as a bridge member is not allowed + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path) + self.cli_commit() + + # Ensure interface is a member of bridge + self.assertTrue(os.path.isdir(f'/sys/class/net/{bridge_iface}/lower_{member}')) + + # Adding a bridge member as a VPP interface is not allowed + # expect raise ConfigError + self.cli_set(base_path + ['settings', 'interface', interface]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['interfaces', 'bridge']) + self.cli_commit() + + # Ensure that VPP process is active + self.assertTrue(process_named_running(PROCESS_NAME)) + if __name__ == '__main__': unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 0b27c8cad..79363f47a 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -44,6 +44,7 @@ from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured from vyos.configdep import set_dependents from vyos.configdep import call_dependents +from vyos.vpp.utils import cli_ifaces_list from vyos import ConfigError from vyos import airbag airbag.enable() @@ -170,6 +171,8 @@ def get_config(config=None): if 'static_arp' in bond: set_dependents('static_arp', conf) + bond['vpp_ifaces'] = cli_ifaces_list(conf) + return bond @@ -205,7 +208,7 @@ def verify(bond): bond_name = bond['ifname'] if dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): - error_msg = f'Can not add interface "{interface}" to bond, ' + error_msg = f'Cannot add interface "{interface}" to bond, ' if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bond') @@ -239,6 +242,12 @@ def verify(bond): continue raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!') + iface_base = interface.split('.')[0] # get the parent interface name + if iface_base in bond['vpp_ifaces']: + raise ConfigError( + error_msg + 'it is already configured as VPP interface' + ) + if mtu := bond.get('mtu'): mtu = int(mtu) max_mtu = int(EthernetIf(interface).get_max_mtu()) diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index c6792404b..b13a7d3fb 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -33,6 +33,7 @@ from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.utils.dict import dict_search from vyos.utils.network import interface_exists +from vyos.vpp.utils import cli_ifaces_list from vyos import ConfigError from vyos import airbag @@ -128,6 +129,8 @@ def get_config(config=None): if 'static_arp' in bridge: set_dependents('static_arp', conf) + bridge['vpp_ifaces'] = cli_ifaces_list(conf) + return bridge def verify(bridge): @@ -153,7 +156,7 @@ def verify(bridge): if dict_search('member.interface', bridge): for interface, interface_config in bridge['member']['interface'].items(): - error_msg = f'Can not add interface "{interface}" to bridge, ' + error_msg = f'Cannot add interface "{interface}" to bridge, ' if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bridge') @@ -190,6 +193,12 @@ def verify(bridge): if interface.startswith('vtun') and not interface_config['valid_ovpn']: raise ConfigError(error_msg + 'OpenVPN device-type must be set to "tap"') + iface_base = interface.split('.')[0] # get the parent interface name + if iface_base in bridge['vpp_ifaces']: + raise ConfigError( + error_msg + 'it is already configured as VPP interface' + ) + if 'enable_vlan' in bridge: if dict_search('vif.1', bridge): raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface') diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index bd8aad3f7..3fff0166b 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -31,7 +31,7 @@ from vyos.base import Warning from vyos.config import Config, config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents -from vyos.configdict import node_changed +from vyos.configdict import node_changed, is_member from vyos.configverify import verify_interface_exists from vyos.configverify import verify_virtual_interface_exists from vyos.ifconfig import Section @@ -66,6 +66,7 @@ from vyos.vpp.config_resource_checks import memory from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map from vyos.vpp.config_filter import iface_filter_eth from vyos.vpp.utils import EthtoolGDrvinfo +from vyos.vpp.utils import cli_ethernet_with_vifs_ifaces from vyos.vpp.configdb import JSONStorage airbag.enable() @@ -394,6 +395,9 @@ def get_config(config=None): # Return to config dictionary config['persist_config'] = eth_ifaces_persist + # list of all Ethernet interfaces with vifs + ifaces_with_vifs = cli_ethernet_with_vifs_ifaces(conf, include_nested_vifs=True) + if 'settings' in config: if 'interface' in config['settings']: interface_rx_mode = config['settings'].get('interface_rx_mode') @@ -426,6 +430,26 @@ def get_config(config=None): # } # ) + # Collect memberships as sets for uniqueness + bond_member = is_member(conf, iface, 'bonding') + bridge_member = is_member(conf, iface, 'bridge') + + # Look for VLAN interfaces of this parent + vlans = [ + vlan_iface + for vlan_iface in ifaces_with_vifs + if vlan_iface.startswith(f'{iface}.') + ] + for vlan_iface in vlans: + bond_member.update(is_member(conf, vlan_iface, 'bonding')) + bridge_member.update(is_member(conf, vlan_iface, 'bridge')) + + # Store as lists + if bond_member: + iface_config['bond_member'] = list(bond_member) + if bridge_member: + iface_config['bridge_member'] = list(bridge_member) + # Get PCI address or device ID if iface_config['driver'] == 'dpdk': if 'dpdk_options' not in iface_config: @@ -617,6 +641,18 @@ def verify(config): 'To proceed at your own risk, enable: "set vpp settings allow-unsupported-nics".' ) + err_message = f'Cannot add {iface} to VPP - ' + if 'bond_member' in iface_config: + raise ConfigError( + err_message + + f'interface (or its VLAN) is a member of bond(s): {", ".join(iface_config["bond_member"])}' + ) + if 'bridge_member' in iface_config: + raise ConfigError( + err_message + + f'interface (or its VLAN) is a member of bridge(s): {", ".join(iface_config["bridge_member"])}' + ) + if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config: if iface_config['xdp_options']['num_rx_queues'] != 'all': rx_queues = iface_config['xdp_api_params']['rxq_num'] @@ -679,6 +715,7 @@ def verify(config): else: verify_interface_exists(config, pppoe_iface) + def generate(config): if not config or 'remove' in config: # Remove old config and return |
