summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-03-30 21:29:26 +0200
committerGitHub <noreply@github.com>2026-03-30 21:29:26 +0200
commit03b412fd9ca754f3ea61c8c7c210babf4aa4d251 (patch)
tree85f96d1c79a924c5142dbe98c0de7eb235d318c9
parent7a0b2775278338902631ea8bfe51170d5119a876 (diff)
parent2f0ef2bfe9d36228795ae46eee23031a37ab5957 (diff)
downloadvyos-1x-03b412fd9ca754f3ea61c8c7c210babf4aa4d251.tar.gz
vyos-1x-03b412fd9ca754f3ea61c8c7c210babf4aa4d251.zip
Merge pull request #5086 from natali-rs1985/T8419
vpp: T8419: Disallow cross-membership between VPP and kernel bonding/bridge interfaces
-rw-r--r--python/vyos/vpp/utils.py20
-rwxr-xr-xsmoketest/scripts/cli/test_vpp.py39
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py11
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py11
-rwxr-xr-xsrc/conf_mode/vpp.py39
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 108741790..d4238f68c 100755
--- a/smoketest/scripts/cli/test_vpp.py
+++ b/smoketest/scripts/cli/test_vpp.py
@@ -1401,6 +1401,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 4d6cafc9d..55581d6ad 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 2060ecd8b..206d89d84 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 ecf7f09b1..7b41c54fe 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
@@ -67,6 +67,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()
@@ -395,6 +396,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')
@@ -427,6 +431,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:
@@ -610,6 +634,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']
@@ -672,6 +708,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