summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNataliia Solomko <natalirs1985@gmail.com>2026-04-02 13:57:19 +0300
committerNataliia Solomko <natalirs1985@gmail.com>2026-04-02 14:27:22 +0300
commit3139ba759c21800386bc1aa127dd95bc63229362 (patch)
tree01ba37b32bd77866cdc908e1e7eedf6b0a3abcab /src
parent2ad37c4a8a963651bbc183d24662f2571c4682c6 (diff)
downloadvyos-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-xsrc/conf_mode/interfaces_ethernet.py57
-rwxr-xr-xsrc/conf_mode/vpp.py67
-rw-r--r--src/conf_mode/vpp_acl.py10
-rw-r--r--src/conf_mode/vpp_interfaces_bonding.py12
-rw-r--r--src/conf_mode/vpp_interfaces_bridge.py10
-rw-r--r--src/conf_mode/vpp_interfaces_xconnect.py10
-rw-r--r--src/conf_mode/vpp_ipfix.py10
-rw-r--r--src/conf_mode/vpp_nat_cgnat.py10
-rw-r--r--src/conf_mode/vpp_nat_nat44.py10
-rw-r--r--src/conf_mode/vpp_sflow.py10
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', {}):