diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-04-02 13:57:19 +0300 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2026-04-02 14:27:22 +0300 |
| commit | 3139ba759c21800386bc1aa127dd95bc63229362 (patch) | |
| tree | 01ba37b32bd77866cdc908e1e7eedf6b0a3abcab /src | |
| parent | 2ad37c4a8a963651bbc183d24662f2571c4682c6 (diff) | |
| download | vyos-1x-3139ba759c21800386bc1aa127dd95bc63229362.tar.gz vyos-1x-3139ba759c21800386bc1aa127dd95bc63229362.zip | |
vpp: T8438: Add bidirectional interface-in-use validation
Add bidirectional VPP interface reference validation: prevent assigning an interface used by a VPP feature (NAT/ACL/IPFIX/sFlow, etc.)
as a VPP member (bond/bridge/xconnect) and prevent using a VPP member interface in VPP features.
Block interface deletion when it is still referenced by any VPP feature/member.
Fix VLAN subinterface removal checks broken by the recent VPP config tree restructuring.
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/interfaces_ethernet.py | 57 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 67 | ||||
| -rw-r--r-- | src/conf_mode/vpp_acl.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bonding.py | 12 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_bridge.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_interfaces_xconnect.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_ipfix.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_nat_cgnat.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_nat_nat44.py | 10 | ||||
| -rw-r--r-- | src/conf_mode/vpp_sflow.py | 10 |
10 files changed, 105 insertions, 101 deletions
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 2eb36a231..886f9b7b7 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from sys import exit @@ -47,6 +46,7 @@ from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete from vyos.utils.process import is_systemd_service_running +from vyos.vpp.config_verify import verify_vpp_remove_interface from vyos.vpp.control_vpp import VPPControl from vyos import ConfigError from vyos import airbag @@ -176,12 +176,20 @@ def get_config(config=None): ethernet['flowtable_interfaces'] = get_flowtable_interfaces(conf) - ethernet['vpp'] = conf.get_config_dict( + vpp_config = conf.get_config_dict( ['vpp'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, ) + if vpp_config: + ethernet['vpp'] = vpp_config + ethernet['vpp']['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) # Protocols static arp dependency if 'static_arp' in ethernet: @@ -353,23 +361,11 @@ def verify_flowtable(ethernet: dict): def verify_vpp_remove_vif(ethernet: dict): """Ensure that VIF interfaces being removed are not used by VPP features""" - vpp_paths_pattern = re.compile( - # Known paths that already use VLAN interfaces - r'(nat\.cgnat\.interface\.inside)|' - r'(nat\.cgnat\.interface\.outside)|' - r'(nat\.nat44\.interface\.inside)|' - r'(nat\.nat44\.interface\.outside)|' - # Potential paths for VLAN interfaces - r'(nat\.nat44\.address_pool\.translation\.interface)|' - r'(nat\.nat44\.address_pool\.twice_nat\.interface)|' - r'(nat\.nat44\.exclude\.rule\.(\d)+\.external_interface)|' - r'(interfaces\.bonding\.bond(\d)+\.member\.interface)|' - r'(interfaces\.bridge\.br(\d)+\.member\.interface)|' - r'(interfaces\.xconnect\.xcon(\d)+\.member\.interface)|' - r'(acl\.ip\.interface)|' - r'(acl\.mac\.interface)' - ) ifname = ethernet['ifname'] + vpp_config = ethernet.get('vpp') + + if not vpp_config: + return vlan_names = [ f'{ifname}.{vif_id}' @@ -377,29 +373,8 @@ def verify_vpp_remove_vif(ethernet: dict): for vif_id in ethernet.get(vif_group, []) ] - if not vlan_names: - return - - vpp_flat = dict_to_paths_values(ethernet.get('vpp', {})) - - candidate_keys = [] - for key, value in vpp_flat.items(): - # Normalize values to list for consistent processing - values = value if isinstance(value, list) else [value] - if any(vlan in values for vlan in vlan_names): - candidate_keys.append((key, values)) - - if not candidate_keys: - return - - for key, values in candidate_keys: - if vpp_paths_pattern.fullmatch(key): - used_vlans = [v for v in vlan_names if v in values] - if used_vlans: - raise ConfigError( - f'Cannot delete interface "{used_vlans[0]}", ' - f'it is still in use by "vpp {key.replace(".", " ")}"' - ) + for vlan in vlan_names: + verify_vpp_remove_interface(vlan, vpp_config) def verify(ethernet): verify_flowtable(ethernet) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 7b41c54fe..dc9ba9b7c 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -49,10 +49,8 @@ from vyos.utils.process import is_systemd_service_active from vyos.vpp import VPPControl from vyos.vpp import control_host from vyos.vpp import VppNotRunningError -from vyos.vpp.config_deps import deps_bond_dict -from vyos.vpp.config_deps import deps_bridge_dict -from vyos.vpp.config_deps import deps_xconnect_dict from vyos.vpp.config_verify import ( + verify_vpp_remove_interface, verify_vpp_minimum_cpus, verify_vpp_minimum_memory, verify_vpp_cpu_cores, @@ -218,33 +216,6 @@ def _get_max_xdp_rx_queues(config: dict): return 1 -def _check_removed_interfaces(config: dict, feature_name: str, interfaces_config: dict): - """ - Check if removed interfaces are used in any feature configuration - - Args: - config: The main configuration dictionary - feature_name: Human-readable feature name for error messages - interfaces_config: The interfaces dictionary from the feature config - Example: - _check_removed_interfaces(config, 'IPFIX monitoring', config.get('ipfix', {}).get('interface', {})) - """ - if ( - 'removed_ifaces' not in config - or not config['removed_ifaces'] - or not interfaces_config - ): - return - - for removed_iface in config['removed_ifaces']: - iface_name = removed_iface.get('iface_name') - if iface_name and iface_name in interfaces_config: - raise ConfigError( - f'Cannot remove interface {iface_name} - it is currently configured for {feature_name}. ' - f'Remove it from {feature_name} configuration first.' - ) - - def _is_device_allowed(config: dict, iface: str): """ Determines if a network interface device is allowed to be used @@ -295,10 +266,6 @@ def get_config(config=None): no_tag_node_value_mangle=True, ) - xconn_members = deps_xconnect_dict(conf) - bridge_members = deps_bridge_dict(conf) - bond_members = deps_bond_dict(conf) - removed_ifaces = [] tmp = node_changed(conf, base_settings + ['interface']) if tmp: @@ -337,9 +304,6 @@ def get_config(config=None): set_dependents('pppoe_server', conf) return { 'removed_ifaces': removed_ifaces, - 'xconn_members': xconn_members, - 'bridge_members': bridge_members, - 'bond_members': bond_members, 'persist_config': eth_ifaces_persist, 'interfaces_vpp': interfaces_config, 'pppoe_ifaces': pppoe_ifaces, @@ -503,9 +467,6 @@ def get_config(config=None): if removed_ifaces: config['removed_ifaces'] = removed_ifaces - config['xconn_members'] = xconn_members - config['bridge_members'] = bridge_members - config['bond_members'] = bond_members config['interfaces_vpp'] = interfaces_config @@ -557,11 +518,6 @@ def get_config(config=None): def verify(config): - # Check remove VPP interface that used in IPFIX - _check_removed_interfaces( - config, 'IPFIX monitoring', config.get('ipfix', {}).get('interface', {}) - ) - if config.get('interfaces_vpp') and 'remove' in config: raise ConfigError( 'VPP cannot be removed while VPP interfaces exist. Remove all "interfaces vpp" first!' @@ -580,6 +536,12 @@ def verify(config): if not config or 'remove' in config: return None + # Check removed interfaces (and their VLANs) against all VPP features + for removed_iface in config.get('removed_ifaces', []): + verify_vpp_remove_interface( + removed_iface['iface_name'], config, match_vlans=True + ) + if 'settings' not in config: raise ConfigError('"settings interface" is required but not set!') @@ -685,21 +647,6 @@ def verify(config): f'RX mode {rx_mode} is not supported for interface {iface}' ) - # Check if deleted interfaces are not xconnect/bridge/bond members - for iface_config in config.get('removed_ifaces', []): - if iface_config['iface_name'] in config.get('xconn_members', {}): - raise ConfigError( - f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed' - ) - if iface_config['iface_name'] in config.get('bridge_members', {}): - raise ConfigError( - f'Interface {iface_config["iface_name"]} is a bridge member and cannot be removed' - ) - if iface_config['iface_name'] in config.get('bond_members', {}): - raise ConfigError( - f'Interface {iface_config["iface_name"]} is a bond member and cannot be removed' - ) - verify_routes_count(config['settings']) for pppoe_iface in config.get('changed_pppoe_ifaces', []): diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py index 4d9ada4a1..ccbe11763 100644 --- a/src/conf_mode/vpp_acl.py +++ b/src/conf_mode/vpp_acl.py @@ -27,6 +27,7 @@ from vyos.utils.network import get_protocol_by_name from vyos.vpp.utils import cli_ifaces_list from vyos.vpp.acl import Acl +from vyos.vpp.config_verify import verify_vpp_interface_not_a_member # TCP flag names to bit values @@ -190,6 +191,14 @@ def get_config(config=None) -> dict: if effective_config: config.update({'effective': effective_config}) + # VPP interface membership data for member-conflict checks + config['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -217,6 +226,7 @@ def verify(config): raise ConfigError( f'{iface} must be a VPP interface for ACL interface' ) + verify_vpp_interface_not_a_member(iface, config) if 'ip' in config: acl = config.get('ip') diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py index 51272c087..7dba9fb3b 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -32,6 +32,8 @@ from vyos.vpp.config_deps import deps_xconnect_dict from vyos.vpp.config_verify import verify_member_conflicts from vyos.vpp.config_verify import verify_vpp_remove_bridge_interface from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface +from vyos.vpp.config_verify import verify_vpp_remove_interface +from vyos.vpp.config_verify import verify_vpp_interface_not_in_feature from vyos.vpp.utils import cli_ifaces_list @@ -98,6 +100,13 @@ def get_config(config=None) -> dict: get_first_key=True, no_tag_node_value_mangle=True, ) + # VPP config for member-in-feature checks + config['vpp'] = conf.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) config['bond_members'] = deps_bond_dict(conf) @@ -150,6 +159,7 @@ def verify(config): verify_vpp_remove_bridge_interface(config) if 'deleted' in config: + verify_vpp_remove_interface(ifname, config.get('vpp'), match_vlans=True) return None if not is_systemd_service_active('vpp.service'): @@ -170,6 +180,7 @@ def verify(config): ) verify_member_conflicts(iface, config, 'bond') + verify_vpp_interface_not_in_feature(iface, config.get('vpp')) if mtu := config.get('mtu'): mtu = int(mtu) @@ -199,6 +210,7 @@ def verify(config): raise ConfigError( f'Cannot remove interface {vif_iface}: it is still in use by the PPPoE server' ) + verify_vpp_remove_interface(vif_iface, config.get('vpp')) verify_mtu_ipv6(config) diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py index 25fad5e5b..4d65690ce 100644 --- a/src/conf_mode/vpp_interfaces_bridge.py +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -26,6 +26,7 @@ from vyos.vpp.config_deps import deps_bond_dict from vyos.vpp.config_deps import deps_bridge_dict from vyos.vpp.config_deps import deps_xconnect_dict from vyos.vpp.config_verify import verify_member_conflicts +from vyos.vpp.config_verify import verify_vpp_interface_not_in_feature def get_config(config=None) -> dict: @@ -71,6 +72,14 @@ def get_config(config=None) -> dict: config['bridge_members'] = deps_bridge_dict(conf) config['xconn_members'] = deps_xconnect_dict(conf) + # VPP config for member-in-feature checks + config['vpp'] = conf.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -108,6 +117,7 @@ def verify(config): ) verify_member_conflicts(member, config, 'bridge') + verify_vpp_interface_not_in_feature(member, config.get('vpp')) # Check if BVI is already defined, only one BVI per bridge domain is allowed if 'bvi' in member_config: diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py index 91fa27c1b..29f2da520 100644 --- a/src/conf_mode/vpp_interfaces_xconnect.py +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -26,6 +26,7 @@ from vyos.vpp.config_deps import deps_bond_dict from vyos.vpp.config_deps import deps_bridge_dict from vyos.vpp.config_deps import deps_xconnect_dict from vyos.vpp.config_verify import verify_member_conflicts +from vyos.vpp.config_verify import verify_vpp_interface_not_in_feature from vyos.vpp.utils import cli_ifaces_list @@ -67,6 +68,14 @@ def get_config(config=None) -> dict: config['xconn_members'] = deps_xconnect_dict(conf) config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + # VPP config for member-in-feature checks + config['vpp'] = conf.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -100,6 +109,7 @@ def verify(config): ) verify_member_conflicts(iface, config, 'xconn') + verify_vpp_interface_not_in_feature(iface, config.get('vpp')) def generate(config): diff --git a/src/conf_mode/vpp_ipfix.py b/src/conf_mode/vpp_ipfix.py index b69651ee4..8a7633389 100644 --- a/src/conf_mode/vpp_ipfix.py +++ b/src/conf_mode/vpp_ipfix.py @@ -21,6 +21,7 @@ from vyos.vpp.ipfix import IPFIX from vyos.vpp.utils import cli_ifaces_list from vyos.vpp.utils import vpp_iface_name_transform from vyos.vpp.control_vpp import VPPControl +from vyos.vpp.config_verify import verify_vpp_interface_not_a_member def get_config(config=None) -> dict: @@ -60,6 +61,14 @@ def get_config(config=None) -> dict: # Add list of VPP interfaces to the config config.update({'vpp_ifaces': cli_ifaces_list(conf)}) + # VPP interface membership data for member-conflict checks + config['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -81,6 +90,7 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for IPFIX monitoring' ) + verify_vpp_interface_not_a_member(interface, config) # Verify that at least one collector is configured if 'collector' not in config: diff --git a/src/conf_mode/vpp_nat_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py index 2044a07b9..838d13239 100644 --- a/src/conf_mode/vpp_nat_cgnat.py +++ b/src/conf_mode/vpp_nat_cgnat.py @@ -26,6 +26,7 @@ from vyos.vpp.utils import vpp_iface_name_transform from vyos.vpp.nat.det44 import Det44 from vyos.vpp.control_vpp import VPPControl from vyos.vpp.config_verify import verify_nat_interfaces +from vyos.vpp.config_verify import verify_vpp_interface_not_a_member protocol_map = { 'all': 0, @@ -118,6 +119,14 @@ def get_config(config=None) -> dict: if effective_config: config.update({'effective': effective_config}) + # VPP interface membership data for member-conflict checks + config['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -157,6 +166,7 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for {direction} CGNAT interface' ) + verify_vpp_interface_not_a_member(interface, config) required_keys = {'outside_prefix', 'inside_prefix'} for rule in config['rule']: diff --git a/src/conf_mode/vpp_nat_nat44.py b/src/conf_mode/vpp_nat_nat44.py index 7f0b27541..8d69ee786 100644 --- a/src/conf_mode/vpp_nat_nat44.py +++ b/src/conf_mode/vpp_nat_nat44.py @@ -30,6 +30,7 @@ from vyos.vpp.utils import vpp_iface_name_transform from vyos.vpp.nat.nat44 import Nat44 from vyos.vpp.control_vpp import VPPControl from vyos.vpp.config_verify import verify_nat_interfaces +from vyos.vpp.config_verify import verify_vpp_interface_not_a_member protocol_map = { @@ -124,6 +125,14 @@ def get_config(config=None) -> dict: if effective_config: config.update({'effective': effective_config}) + # VPP interface membership data for member-conflict checks + config['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -171,6 +180,7 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for {direction} NAT interface' ) + verify_vpp_interface_not_a_member(interface, config) if not config.get('address_pool', {}).get('translation') and not config.get( 'static', {} diff --git a/src/conf_mode/vpp_sflow.py b/src/conf_mode/vpp_sflow.py index df9d0365f..de592f514 100644 --- a/src/conf_mode/vpp_sflow.py +++ b/src/conf_mode/vpp_sflow.py @@ -19,6 +19,7 @@ from vyos import ConfigError from vyos.config import Config from vyos.vpp.utils import cli_ifaces_list from vyos.vpp.sflow import SFlow +from vyos.vpp.config_verify import verify_vpp_interface_not_a_member def get_config(config=None) -> dict: @@ -70,6 +71,14 @@ def get_config(config=None) -> dict: # Add list of VPP interfaces to the config config.update({'vpp_ifaces': cli_ifaces_list(conf)}) + # VPP interface membership data for member-conflict checks + config['interfaces_vpp'] = conf.get_config_dict( + ['interfaces', 'vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return config @@ -87,6 +96,7 @@ def verify(config): raise ConfigError( f'{interface} must be a VPP interface for sFlow monitoring' ) + verify_vpp_interface_not_a_member(interface, config) # Verify that system sflow has enable-vpp defined if 'system_sflow' not in config or 'vpp' not in config.get('system_sflow', {}): |
