summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@vyos.io>2025-08-12 15:31:00 +0100
committerGitHub <noreply@github.com>2025-08-12 15:31:00 +0100
commit703031e46b7a01f022b5b83766f4aa0bf4353963 (patch)
tree7fba79247d7c1bc373d93c88809ede774b4cbf85 /src
parent33b13061b55a8dbf302dcfaed1cf89b3e0f12e63 (diff)
parent1a83bde3c14ff893960c3d2921279d82fdb21f76 (diff)
downloadvyos-1x-703031e46b7a01f022b5b83766f4aa0bf4353963.tar.gz
vyos-1x-703031e46b7a01f022b5b83766f4aa0bf4353963.zip
Merge pull request #4650 from sever-sever/T7697
T7697: Merge vyos-vpp repo into vyos-1x
Diffstat (limited to 'src')
-rw-r--r--src/completion/list_mem_page_size.py69
-rw-r--r--src/completion/list_vpp_interfaces.py39
-rwxr-xr-xsrc/conf_mode/vpp.py695
-rw-r--r--src/conf_mode/vpp_acl.py400
-rw-r--r--src/conf_mode/vpp_interfaces_bonding.py234
-rw-r--r--src/conf_mode/vpp_interfaces_bridge.py193
-rw-r--r--src/conf_mode/vpp_interfaces_ethernet.py134
-rw-r--r--src/conf_mode/vpp_interfaces_geneve.py193
-rw-r--r--src/conf_mode/vpp_interfaces_gre.py222
-rw-r--r--src/conf_mode/vpp_interfaces_ipip.py211
-rw-r--r--src/conf_mode/vpp_interfaces_loopback.py174
-rw-r--r--src/conf_mode/vpp_interfaces_vxlan.py218
-rw-r--r--src/conf_mode/vpp_interfaces_xconnect.py149
-rw-r--r--src/conf_mode/vpp_kernel-interfaces.py199
-rw-r--r--src/conf_mode/vpp_nat.py488
-rw-r--r--src/conf_mode/vpp_nat_cgnat.py218
-rw-r--r--src/conf_mode/vpp_sflow.py145
-rwxr-xr-xsrc/op_mode/show_vpp_interfaces.py257
-rw-r--r--src/op_mode/show_vpp_nat44.py251
-rw-r--r--src/op_mode/vpp_acl.py342
-rw-r--r--src/op_mode/vpp_nat_cgnat.py140
-rw-r--r--src/systemd/vpp-failure-handler.service9
22 files changed, 4980 insertions, 0 deletions
diff --git a/src/completion/list_mem_page_size.py b/src/completion/list_mem_page_size.py
new file mode 100644
index 000000000..4d2cb11c6
--- /dev/null
+++ b/src/completion/list_mem_page_size.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import argparse
+from vyos.vpp.utils import (
+ get_hugepage_sizes,
+ get_default_hugepage_size,
+ get_default_page_size,
+ bytes_to_human_memory,
+)
+
+
+def get_default_page_sizes() -> list[int]:
+ """
+ Retrieve the system's default page sizes, including huge pages.
+ :return: A list of page sizes in bytes.
+ """
+ page_sizes = []
+ # default system page size
+ page_size = get_default_page_size()
+ if page_size:
+ page_sizes.append(page_size)
+
+ # default huge page size
+ page_size = get_default_hugepage_size()
+ if page_size:
+ page_sizes.append(page_size)
+
+ return page_sizes
+
+
+def list_mem_page_size(hugepage_only=None) -> list[str]:
+ result = []
+ page_sizes = get_hugepage_sizes()
+
+ if not hugepage_only:
+ page_sizes += get_default_page_sizes()
+
+ page_sizes = set(page_sizes)
+ for unit in ['K', 'M', 'G']:
+ for size in page_sizes:
+ if val := bytes_to_human_memory(size, unit):
+ result.append(val)
+
+ return result
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--hugepage_only', type=str, help='List only available hugepage sizes.'
+ )
+ args = parser.parse_args()
+
+ result = list_mem_page_size(args.hugepage_only)
+ print(' '.join(result))
diff --git a/src/completion/list_vpp_interfaces.py b/src/completion/list_vpp_interfaces.py
new file mode 100644
index 000000000..e5751119c
--- /dev/null
+++ b/src/completion/list_vpp_interfaces.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import vpp_ifaces_list
+
+
+def get_vpp_ifaces_names():
+ config = ConfigTreeQuery()
+ if not config.exists('vpp settings interface'):
+ return []
+
+ vpp = VPPControl()
+ vpp_ifaces = vpp_ifaces_list(vpp.api)
+ ifaces_names = [iface['interface_name'] for iface in vpp_ifaces]
+
+ return sorted(ifaces_names)
+
+
+if __name__ == "__main__":
+ ifaces = []
+ ifaces = get_vpp_ifaces_names()
+ print(" ".join(ifaces))
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
new file mode 100755
index 000000000..6e02f9593
--- /dev/null
+++ b/src/conf_mode/vpp.py
@@ -0,0 +1,695 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from pathlib import Path
+
+from pyroute2.iproute import IPRoute
+
+try:
+ from vpp_papi import VPPIOError, VPPValueError
+except ImportError: # pylint: disable=import-error
+ VPPIOError = VPPValueError = None
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.base import Warning
+from vyos.config import Config, config_dict_merge
+from vyos.configdep import set_dependents, call_dependents
+from vyos.configdict import node_changed, leaf_node_changed
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.process import call
+from vyos.utils.system import sysctl_read, sysctl_apply
+
+from vyos.vpp import VPPControl
+from vyos.vpp import control_host
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_dev_driver,
+ verify_vpp_minimum_cpus,
+ verify_vpp_minimum_memory,
+ verify_vpp_settings_cpu_and_corelist_workers,
+ verify_vpp_settings_cpu_corelist_workers,
+ verify_vpp_cpu_main_core,
+ verify_vpp_settings_cpu_skip_cores,
+ verify_vpp_settings_cpu_workers,
+ verify_vpp_nat44_workers,
+ verify_vpp_memory,
+ verify_vpp_statseg_size,
+ verify_vpp_interfaces_dpdk_num_queues,
+ verify_vpp_host_resources,
+)
+from vyos.vpp.config_filter import iface_filter_eth
+from vyos.vpp.utils import EthtoolGDrvinfo
+from vyos.vpp.configdb import JSONStorage
+
+airbag.enable()
+
+service_name = 'vpp'
+service_conf = Path(f'/run/vpp/{service_name}.conf')
+systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf'
+
+dependency_interface_type_map = {
+ 'vpp_interfaces_bonding': 'bonding',
+ 'vpp_interfaces_bridge': 'bridge',
+ 'vpp_interfaces_ethernet': 'ethernet',
+ 'vpp_interfaces_geneve': 'geneve',
+ 'vpp_interfaces_gre': 'gre',
+ 'vpp_interfaces_ipip': 'ipip',
+ 'vpp_interfaces_loopback': 'loopback',
+ 'vpp_interfaces_vxlan': 'vxlan',
+ 'vpp_interfaces_xconnect': 'xconnect',
+}
+
+# dict of drivers that needs to be overrided
+override_drivers: dict[str, str] = {
+ 'hv_netvsc': 'uio_hv_generic',
+ 'ena': 'vfio-pci',
+}
+
+# drivers that does not use PCIe addresses
+not_pci_drv: list[str] = ['hv_netvsc']
+
+# drivers that support interrupt RX mode for DPDK and XDP
+drivers_support_interrupt: dict[str, list] = {
+ 'atlantic': ['dpdk', 'xdp'],
+ 'bnx2x': ['dpdk'],
+ 'e1000': ['dpdk'],
+ 'ena': ['dpdk', 'xdp'],
+ 'i40e': ['dpdk', 'xdp'],
+ 'ice': ['dpdk', 'xdp'],
+ 'igb': ['xdp'],
+ 'igc': ['dpdk', 'xdp'],
+ 'ixgbe': ['dpdk', 'xdp'],
+ 'qede': ['dpdk', 'xdp'],
+ 'vmxnet3': ['xdp'],
+ 'virtio_net': ['xdp'],
+}
+
+
+def get_config(config=None):
+ # use persistent config to store interfaces data between executions
+ # this is required because some interfaces after they are connected
+ # to VPP is really hard or impossible to restore without knowing
+ # their original parameters (like IDs)
+ persist_config = JSONStorage('vpp_conf')
+ eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read(
+ 'eth_ifaces', {}
+ )
+
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp']
+ base_settings = ['vpp', 'settings']
+
+ # find interfaces removed from VPP
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ xconn_members = deps_xconnect_dict(conf)
+
+ removed_ifaces = []
+ tmp = node_changed(conf, base_settings + ['interface'])
+ if tmp:
+ for removed_iface in tmp:
+ to_append = {
+ 'iface_name': removed_iface,
+ 'driver': effective_config['settings']['interface'][removed_iface][
+ 'driver'
+ ],
+ }
+ removed_ifaces.append(to_append)
+ # add an interface to a list of interfaces that need
+ # to be reinitialized after the commit
+ set_dependents('ethernet', conf, removed_iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # sFlow dependency
+ if conf.exists(['vpp', 'sflow']):
+ set_dependents('vpp_sflow', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ if not conf.exists(base):
+ return {
+ 'removed_ifaces': removed_ifaces,
+ 'xconn_members': xconn_members,
+ 'persist_config': eth_ifaces_persist,
+ }
+
+ config = conf.get_config_dict(
+ base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ )
+
+ # Get default values which we need to conditionally update into the
+ # dictionary retrieved.
+ default_values = conf.get_config_defaults(**config.kwargs, recursive=True)
+
+ # delete 'xdp-options' from defaults if driver is DPDK
+ for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
+ if iface_config.get('driver') == 'dpdk':
+ del default_values['settings']['interface'][iface]['xdp_options']
+
+ config = config_dict_merge(default_values, config)
+
+ # Ignore default XML values if config doesn't exists
+ if not conf.exists(base_settings + ['ipsec']):
+ del config['settings']['ipsec']
+
+ # add running config
+ if effective_config:
+ config['effective'] = effective_config
+
+ if 'settings' in config:
+ if 'interface' in config['settings']:
+ for iface, iface_config in config['settings']['interface'].items():
+ # Driver must be configured to continue
+ if 'driver' not in iface_config:
+ raise ConfigError(
+ f'"driver" must be configured for {iface} interface!'
+ )
+
+ old_driver = leaf_node_changed(
+ conf, base_settings + ['interface', iface, 'driver']
+ )
+
+ # Get current kernel module, required for extra verification and
+ # logic for VMBus interfaces
+ config['settings']['interface'][iface]['kernel_module'] = (
+ EthtoolGDrvinfo(iface).driver
+ )
+
+ # filter unsupported config nodes
+ iface_filter_eth(conf, iface)
+ set_dependents('ethernet', conf, iface)
+ # Interfaces with changed driver should be removed/readded
+ if old_driver and old_driver[0] == 'dpdk':
+ removed_ifaces.append(
+ {
+ 'iface_name': iface,
+ 'driver': 'dpdk',
+ }
+ )
+
+ # Get PCI address or device ID
+ if iface_config['driver'] == 'dpdk':
+ if 'dpdk_options' not in iface_config:
+ iface_config['dpdk_options'] = {}
+ # Check in a persistent config first
+ id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get(
+ 'dev_id'
+ )
+ if id_from_persisten_conf:
+ iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf
+ else:
+ try:
+ iface_to_search = iface
+ if old_driver and old_driver[0] == 'xdp':
+ iface_to_search = f'defunct_{iface}'
+ iface_config['dpdk_options']['dev_id'] = (
+ control_host.get_dev_id(iface_to_search)
+ )
+ except Exception:
+ # Return empty address if all attempts failed
+ # We will catch this in verify()
+ iface_config['dpdk_options']['dev_id'] = ''
+ # prepare XDP interface parameters
+ if iface_config['driver'] == 'xdp':
+ xdp_api_params = {
+ 'rxq_size': int(iface_config['xdp_options']['rx_queue_size']),
+ 'txq_size': int(iface_config['xdp_options']['tx_queue_size']),
+ }
+ if iface_config['xdp_options']['num_rx_queues'] == 'all':
+ xdp_api_params['rxq_num'] = 0
+ else:
+ xdp_api_params['rxq_num'] = int(
+ iface_config['xdp_options']['num_rx_queues']
+ )
+ if 'zero-copy' in iface_config['xdp_options']:
+ xdp_api_params['mode'] = 'zero-copy'
+ if 'zero-copy' in iface_config['xdp_options']:
+ xdp_api_params['flags'] = 'no_syscall_lock'
+ iface_config['xdp_api_params'] = xdp_api_params
+
+ if removed_ifaces:
+ config['removed_ifaces'] = removed_ifaces
+ config['xconn_members'] = xconn_members
+
+ # Dependencies
+ for dependency, interface_type in dependency_interface_type_map.items():
+ # if conf.exists(base + ['interfaces', interface_type]):
+ if effective_config.get('interfaces', {}).get(interface_type):
+ for iface, iface_config in (
+ config.get('interfaces', {}).get(interface_type, {}).items()
+ ):
+ # filter unsupported config nodes
+ if interface_type == 'ethernet':
+ iface_filter_eth(conf, iface)
+ set_dependents(dependency, conf, iface)
+
+ # Save important info about all interfaces that cannot be retrieved later
+ # Add new interfaces (only if they are first time seen in a config)
+ for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
+ if iface not in effective_config.get('settings', {}).get('interface', {}):
+ eth_ifaces_persist[iface] = {
+ 'original_driver': config['settings']['interface'][iface][
+ 'kernel_module'
+ ],
+ }
+ eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface)
+ eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface)
+
+ # Get kernel settings for hugepages
+ kernel_memory_settings = conf.get_config_dict(
+ ['system', 'option', 'kernel', 'memory'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ config['kernel_memory_settings'] = kernel_memory_settings
+
+ # Return to config dictionary
+ config['persist_config'] = eth_ifaces_persist
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ return None
+
+ if 'settings' not in config:
+ raise ConfigError('"settings interface" is required but not set!')
+
+ if 'interface' not in config['settings']:
+ raise ConfigError('"settings interface" is required but not set!')
+
+ # check if the system meets minimal requirements
+ verify_vpp_minimum_cpus()
+ verify_vpp_minimum_memory()
+
+ # check if Ethernet interfaces exist
+ ethernet_ifaces = Section.interfaces('ethernet')
+ for iface in config['settings']['interface'].keys():
+ if iface not in ethernet_ifaces:
+ raise ConfigError(f'Interface {iface} does not exist or is not Ethernet!')
+
+ # Resource usage checks
+ workers = 0
+
+ if 'cpu' in config['settings']:
+ cpu_settings = config['settings']['cpu']
+
+ # Check if there are enough CPU cores to skip according to config
+ if 'skip_cores' in cpu_settings:
+ skip_cores = int(cpu_settings['skip_cores'])
+ verify_vpp_settings_cpu_skip_cores(skip_cores)
+
+ # Check whether the workers and corelist-workers are configured properly
+ verify_vpp_settings_cpu_and_corelist_workers(cpu_settings)
+
+ # Check if there are enough CPU cores to add workers
+ if 'workers' in cpu_settings:
+ workers = verify_vpp_settings_cpu_workers(cpu_settings)
+
+ if 'main_core' in cpu_settings:
+ verify_vpp_cpu_main_core(cpu_settings)
+
+ # Check the CPU main core not falling to the corelist-workers
+ if 'corelist_workers' in cpu_settings:
+ workers = verify_vpp_settings_cpu_corelist_workers(cpu_settings)
+
+ if 'workers' in config['settings']['nat44']:
+ verify_vpp_nat44_workers(
+ workers=workers, nat44_workers=config['settings']['nat44']['workers']
+ )
+
+ # Check if available memory is enough for current VPP config
+ verify_vpp_memory(config)
+
+ if 'max_map_count' in config['settings'].get('host_resources', {}):
+ verify_vpp_host_resources(config)
+
+ if 'statseg' in config['settings']:
+ verify_vpp_statseg_size(config['settings'])
+
+ # ensure DPDK/XDP settings are properly configured
+ for iface, iface_config in config['settings']['interface'].items():
+ # check if selected driver is supported, but only for new interfaces
+ if iface not in config.get('effective', {}).get('settings', {}).get(
+ 'interface', {}
+ ):
+ if not verify_dev_driver(iface, iface_config['driver']):
+ raise ConfigError(
+ f'Driver {iface_config["driver"]} is not compatible with interface {iface}!'
+ )
+ if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config:
+ if iface_config['xdp_options']['num_rx_queues'] != 'all':
+ Warning(f'Not all RX queues will be connected to VPP for {iface}!')
+
+ if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config:
+ raise ConfigError('DPDK options are not applicable for XDP driver!')
+
+ if iface_config['driver'] == 'dpdk' and 'xdp_options' in iface_config:
+ raise ConfigError('XDP options are not applicable for DPDK driver!')
+
+ if iface_config['driver'] == 'dpdk' and 'dpdk_options' in iface_config:
+ if 'num_rx_queues' in iface_config['dpdk_options']:
+ rx_queues = int(iface_config['dpdk_options']['num_rx_queues'])
+ verify_vpp_interfaces_dpdk_num_queues(
+ qtype='receive', num_queues=rx_queues, workers=workers
+ )
+
+ if 'num_tx_queues' in iface_config['dpdk_options']:
+ tx_queues = int(iface_config['dpdk_options']['num_tx_queues'])
+ verify_vpp_interfaces_dpdk_num_queues(
+ qtype='transmit', num_queues=tx_queues, workers=workers
+ )
+
+ # RX-mode verification
+ rx_mode = iface_config.get('rx_mode')
+ if rx_mode and rx_mode != 'polling':
+ # By default drivers operate in polling mode. Not all NIC drivers support
+ # RX mode interrupt and adaptive
+ driver = config.get('persist_config').get(iface).get('original_driver')
+ if (
+ driver not in drivers_support_interrupt
+ or iface_config['driver'] not in drivers_support_interrupt[driver]
+ ):
+ raise ConfigError(
+ f'RX mode {rx_mode} is not supported for interface {iface}'
+ )
+
+ # check GRE tunnels as part of the bridge, only tunnel-type teb is allowed
+ # set vpp interfaces bridge br1 member interface gre1
+ # set vpp interfaces gre gre1 tunnel-type teb
+ if 'interfaces' in config:
+ if 'bridge' in config['interfaces']:
+ for iface, iface_config in config['interfaces']['bridge'].items():
+ if 'member' in iface_config:
+ for member in iface_config['member'].get('interface', []):
+ if member.startswith('gre'):
+ if (
+ 'gre' in config['interfaces']
+ and config['interfaces']['gre']
+ .get(member, {})
+ .get('tunnel_type')
+ != 'teb'
+ ):
+ raise ConfigError(
+ f'Only tunnel-type teb is allowed for GRE interfaces in bridge {iface}'
+ )
+
+ # Only one multipoint GRE tunnel is allowed from the same source address
+ # set vpp interfaces gre gre0 mode 'point-to-multipoint'
+ # set vpp interfaces gre gre0 remote '0.0.0.0'
+ # set vpp interfaces gre gre0 source-address '192.0.2.1'
+ # set vpp interfaces gre gre1 mode 'point-to-multipoint'
+ # set vpp interfaces gre gre1 remote '0.0.0.0'
+ # set vpp interfaces gre gre1 source-address '192.0.2.1'
+ if 'gre' in config['interfaces']:
+ for iface, iface_config in config['interfaces']['gre'].items():
+ if iface_config['mode'] == 'point-to-multipoint':
+ for other_iface, other_iface_config in config['interfaces'][
+ 'gre'
+ ].items():
+ if (
+ other_iface_config['mode'] == 'point-to-multipoint'
+ and other_iface_config['source_address']
+ == iface_config['source_address']
+ and iface != other_iface
+ ):
+ raise ConfigError(
+ 'Only one multipoint GRE tunnel is allowed from the same source address'
+ )
+
+ # Check if deleted interfaces are not xconnect memebrs
+ 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'
+ )
+
+
+def generate(config):
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ render(service_conf, 'vpp/startup.conf.j2', config['settings'])
+ render(systemd_override, 'vpp/override.conf.j2', config)
+
+ # apply sysctl values
+ # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf
+ # vm.nr_hugepages are now configured in section
+ # 'set system option kernel memory hugepage-size 2M hugepage-count <count>'
+ sysctl_config: dict[str, str] = {
+ 'vm.max_map_count': config['settings']['host_resources']['max_map_count'],
+ 'vm.hugetlb_shm_group': '0',
+ 'kernel.shmmax': config['settings']['host_resources']['shmmax'],
+ }
+ # we do not want to lower current values
+ for sysctl_key, sysctl_value in sysctl_config.items():
+ # perform check only for quantitative params
+ if sysctl_key == 'vm.hugetlb_shm_group':
+ pass
+ current_value = sysctl_read(sysctl_key)
+ if int(current_value) > int(sysctl_value):
+ sysctl_config[sysctl_key] = current_value
+
+ if not sysctl_apply(sysctl_config):
+ raise ConfigError('Cannot configure sysctl parameters for VPP')
+
+ return None
+
+
+def initialize_interface(iface, driver, iface_config) -> None:
+ # DPDK - rescan PCI to use a proper driver
+ if driver == 'dpdk' and iface_config['original_driver'] not in not_pci_drv:
+ control_host.pci_rescan(iface_config['dev_id'])
+ # rename to the proper name
+ iface_new_name: str = control_host.get_eth_name(iface_config['dev_id'])
+ control_host.rename_iface(iface_new_name, iface)
+
+ # XDP - rename an interface, disable promisc and XDP
+ if driver == 'xdp':
+ control_host.set_promisc(f'defunct_{iface}', 'off')
+ control_host.rename_iface(f'defunct_{iface}', iface)
+ control_host.xdp_remove(iface)
+
+ # Rename Mellanox NIC to a normal name
+ try:
+ if control_host.get_eth_driver(f'defunct_{iface}') == 'mlx5_core':
+ control_host.rename_iface(f'defunct_{iface}', iface)
+ except FileNotFoundError:
+ pass
+
+ # Replace a driver with original for VMBus interfaces and rename it
+ if driver == 'dpdk' and iface_config['original_driver'] in override_drivers:
+ control_host.override_driver(iface_config['bus_id'], iface_config['dev_id'])
+ iface_new_name: str = control_host.get_eth_name(iface_config['dev_id'])
+ control_host.rename_iface(iface_new_name, iface)
+
+
+def apply(config):
+ # Open persistent config
+ # It is required for operations with interfaces
+ persist_config = JSONStorage('vpp_conf')
+ if not config or ('removed_ifaces' in config and 'settings' not in config):
+ # Cleanup persistent config
+ persist_config.delete()
+ # And stop the service
+ call(f'systemctl stop {service_name}.service')
+ else:
+ # Some interfaces required extra preparation before VPP can be started
+ if 'settings' in config and 'interface' in config.get('settings'):
+ for iface, iface_config in config['settings']['interface'].items():
+ if iface_config['driver'] == 'dpdk':
+ # ena interfaces require noiommu mode
+ if iface_config['kernel_module'] == 'ena':
+ control_host.unsafe_noiommu_mode(True)
+
+ if iface_config['kernel_module'] in override_drivers:
+ control_host.override_driver(
+ config['persist_config'][iface]['bus_id'],
+ config['persist_config'][iface]['dev_id'],
+ override_drivers[iface_config['kernel_module']],
+ )
+
+ call('systemctl daemon-reload')
+ call(f'systemctl restart {service_name}.service')
+
+ # Initialize interfaces removed from VPP
+ for iface in config.get('removed_ifaces', []):
+ initialize_interface(
+ iface['iface_name'],
+ iface['driver'],
+ config['persist_config'][iface['iface_name']],
+ )
+
+ # Remove what is not in the config anymore
+ if iface['iface_name'] not in config.get('settings', {}).get('interface', {}):
+ del config['persist_config'][iface['iface_name']]
+
+ if 'settings' in config and 'interface' in config.get('settings'):
+ # connect to VPP
+ # must be performed multiple attempts because API is not available
+ # immediately after the service restart
+ try:
+ vpp_control = VPPControl(attempts=20, interval=500)
+
+ # preconfigure LCP plugin
+ if 'ignore_kernel_routes' in config.get('settings', {}).get('lcp', {}):
+ vpp_control.cli_cmd('lcp param route-no-paths off')
+ else:
+ vpp_control.cli_cmd('lcp param route-no-paths on')
+ # add interfaces
+ iproute = IPRoute()
+ for iface, iface_config in config['settings']['interface'].items():
+ # promisc option for DPDK interfaces
+ if iface_config['driver'] == 'dpdk':
+ if 'promisc' in iface_config['dpdk_options']:
+ if_index = vpp_control.get_sw_if_index(iface)
+ vpp_control.api.sw_interface_set_promisc(
+ sw_if_index=if_index, promisc_on=True
+ )
+ # add XDP interfaces
+ if iface_config['driver'] == 'xdp':
+ control_host.rename_iface(iface, f'defunct_{iface}')
+ vpp_control.xdp_iface_create(
+ host_if=f'defunct_{iface}',
+ name=iface,
+ **iface_config['xdp_api_params'],
+ )
+ # replicate MAC address of a real interface
+ real_mac = control_host.get_eth_mac(f'defunct_{iface}')
+ vpp_control.set_iface_mac(iface, real_mac)
+ if 'promisc' in iface_config['xdp_options']:
+ control_host.set_promisc(f'defunct_{iface}', 'on')
+ control_host.set_status(f'defunct_{iface}', 'up')
+ control_host.flush_ip(f'defunct_{iface}')
+ # Rename Mellanox interfaces to hide them and create LCP properly
+ if (
+ iface in Section.interfaces()
+ and control_host.get_eth_driver(iface) == 'mlx5_core'
+ ):
+ control_host.rename_iface(iface, f'defunct_{iface}')
+ control_host.set_status(f'defunct_{iface}', 'up')
+ control_host.flush_ip(f'defunct_{iface}')
+ # Create lcp
+ if iface not in Section.interfaces():
+ vpp_control.lcp_pair_add(iface, iface)
+
+ # For unknown reasons, if multiple interfaces later try to be
+ # initialized by configuration scripts, some of them may stuck
+ # in an endless UP/DOWN loop
+ # We found two workarounds - pause initialization (requires
+ # main code modifications).
+ # And this one
+ dev_index = iproute.link_lookup(ifname=iface)[0]
+ iproute.link('set', index=dev_index, state='up')
+
+ # Set rx-mode. Should be configured after interface state set to UP
+ rx_mode = iface_config.get('rx_mode')
+ if rx_mode:
+ # to hardware side
+ vpp_control.iface_rxmode(iface, rx_mode)
+ # to kernel side
+ lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get(
+ 'vpp_name_kernel'
+ )
+ vpp_control.iface_rxmode(lcp_name, rx_mode)
+
+ # Syncronize routes via LCP
+ vpp_control.lcp_resync()
+
+ # NAT44 settings
+ nat44_settings = config['settings'].get('nat44', {})
+
+ enable_forwarding = True
+ if 'no_forwarding' in nat44_settings:
+ enable_forwarding = False
+ vpp_control.enable_disable_nat44_forwarding(enable_forwarding)
+
+ vpp_control.set_nat44_session_limit(
+ int(nat44_settings.get('session_limit'))
+ )
+
+ if nat44_settings.get('workers'):
+ bitmask = 0
+ for worker_range in nat44_settings['workers']:
+ worker_numbers = worker_range.split('-')
+ for wid in range(
+ int(worker_numbers[0]), int(worker_numbers[-1]) + 1
+ ):
+ bitmask |= 1 << wid
+ vpp_control.set_nat_workers(bitmask)
+
+ except (VPPIOError, VPPValueError) as e:
+ # if cannot connect to VPP or an error occurred then
+ # we need to stop vpp service and initialize interfaces
+ call(f'systemctl stop {service_name}.service')
+ for iface, iface_config in config['settings']['interface'].items():
+ initialize_interface(
+ iface, iface_config['driver'], config['persist_config'][iface]
+ )
+
+ raise ConfigError(
+ f'An error occurred: {e}. '
+ 'VPP service will be restarted with the previous configuration'
+ )
+
+ # Save persistent config
+ if 'persist_config' in config and config['persist_config']:
+ persist_config.write('eth_ifaces', config['persist_config'])
+
+ # reinitialize interfaces, but not during the first boot
+ if boot_configuration_complete():
+ call_dependents()
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py
new file mode 100644
index 000000000..cbc915226
--- /dev/null
+++ b/src/conf_mode/vpp_acl.py
@@ -0,0 +1,400 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import ipaddress
+
+from vyos import ConfigError
+
+from vyos.configdiff import Diff
+from vyos.configdict import node_changed
+from vyos.config import Config
+from vyos.utils.network import get_protocol_by_name
+
+from vyos.vpp.utils import cli_ifaces_list
+from vyos.vpp.acl import Acl
+
+
+# TCP flag names to bit values
+TCP_FLAGS = {
+ 'FIN': 0x01,
+ 'SYN': 0x02,
+ 'RST': 0x04,
+ 'PSH': 0x08,
+ 'ACK': 0x10,
+ 'URG': 0x20,
+ 'ECN': 0x40,
+ 'CWR': 0x80,
+}
+
+# ACL action flags
+action_map = {
+ 'deny': 0,
+ 'permit': 1,
+ 'permit-reflect': 2,
+}
+
+
+def get_tcp_mask_value(set_flags, unset_flags):
+ mask = 0
+ value = 0
+
+ for flag in set_flags + unset_flags:
+ bit = TCP_FLAGS.get(flag.upper())
+ mask |= bit
+ if flag in set_flags:
+ value |= bit
+
+ return mask, value
+
+
+def get_port_first_last(port_range, protocol):
+ first_port = 0
+ last_port = 65535
+ if not port_range:
+ if protocol in ['icmp', 'ipv6-icmp']:
+ last_port = 255
+ elif '-' not in port_range:
+ first_port = last_port = port_range
+ else:
+ first_port, last_port = port_range.split('-')
+ return int(first_port), int(last_port)
+
+
+def create_ip_rules_list(rules):
+ rules_list = []
+ for rule in rules.values():
+ r = {
+ 'is_permit': action_map[rule.get('action')],
+ 'src_prefix': rule.get('source', {}).get('prefix', ''),
+ 'dst_prefix': rule.get('destination', {}).get('prefix', ''),
+ 'proto': (
+ int(get_protocol_by_name(rule.get('protocol')))
+ if rule.get('protocol') != 'all'
+ else 0
+ ),
+ }
+
+ tcp_flags = rule.get('tcp_flags', {})
+ set_flags = [flag for flag in tcp_flags if flag != 'not']
+ unet_flags = list(tcp_flags.get('not', {}).keys())
+ tcp_mask, tcp_value = get_tcp_mask_value(set_flags, unet_flags)
+ r['tcp_flags_mask'] = tcp_mask
+ r['tcp_flags_value'] = tcp_value
+
+ src_ports = rule.get('source', {}).get('port')
+ src_first_port, src_last_port = get_port_first_last(
+ src_ports, rule.get('protocol')
+ )
+ r['srcport_or_icmptype_first'] = src_first_port
+ r['srcport_or_icmptype_last'] = src_last_port
+
+ dst_ports = rule.get('destination', {}).get('port')
+ dst_first_port, dst_last_port = get_port_first_last(
+ dst_ports, rule.get('protocol')
+ )
+ r['dstport_or_icmpcode_first'] = dst_first_port
+ r['dstport_or_icmpcode_last'] = dst_last_port
+
+ rules_list.append(r)
+
+ return rules_list
+
+
+def create_macip_rules_list(rules):
+ rules_list = []
+ for rule in rules.values():
+ r = {
+ 'is_permit': action_map[rule.get('action')],
+ 'src_prefix': rule.get('prefix', ''),
+ 'src_mac': rule.get('mac_address', ''),
+ 'src_mac_mask': rule.get('mac_mask', ''),
+ }
+ rules_list.append(r)
+
+ return rules_list
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'acl']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dictionary for deletion
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ changed_ip_ifaces = node_changed(
+ conf,
+ base + ['ip', 'interface'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ changed_macip_ifaces = node_changed(
+ conf,
+ base + ['macip', 'interface'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ config.update(
+ {
+ 'changed_ip_ifaces': changed_ip_ifaces,
+ 'changed_macip_ifaces': changed_macip_ifaces,
+ 'vpp_ifaces': cli_ifaces_list(conf),
+ }
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ for acl_type in ['ip', 'macip']:
+ if acl_type in config:
+ acl = config.get(acl_type)
+ if 'tag_name' not in acl:
+ raise ConfigError(f'"tag-name" is required for "acl {acl_type}"')
+
+ for acl_name, acl_config in acl.get('tag_name').items():
+ if 'rule' not in acl_config:
+ raise ConfigError(f'Rules must be configured for ACL {acl_name}')
+
+ for rule, rule_config in acl_config.get('rule').items():
+ err_msg = f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:'
+ if 'action' not in rule_config:
+ raise ConfigError(f'{err_msg} action must be defined')
+
+ for iface, iface_config in acl.get('interface', {}).items():
+ if iface not in config.get('vpp_ifaces'):
+ raise ConfigError(
+ f'{iface} must be a VPP interface for ACL interface'
+ )
+
+ if 'ip' in config:
+ acl = config.get('ip')
+ for acl_name, acl_config in acl.get('tag_name').items():
+ for rule, rule_config in acl_config.get('rule').items():
+ err_msg = (
+ f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:'
+ )
+
+ # verify IPv4 and IPv6 address family
+ src_prefix = rule_config.get('source', {}).get('prefix')
+ dst_prefix = rule_config.get('destination', {}).get('prefix')
+ src = ipaddress.ip_network(src_prefix) if src_prefix else None
+ dst = ipaddress.ip_network(dst_prefix) if dst_prefix else None
+
+ if src and dst:
+ if src.version != dst.version:
+ raise ConfigError(
+ f'{err_msg} source and destination prefixes must be from the same IP family'
+ )
+ elif src or dst:
+ family = src.version if src else dst.version
+ if family == 6:
+ raise ConfigError(
+ f'{err_msg} both source and destination prefixes must be defined for IPv6'
+ )
+
+ # verify protocol
+ protocol = rule_config.get('protocol')
+ if protocol != 'all':
+ proto = get_protocol_by_name(protocol)
+ if not isinstance(proto, int) and (
+ not proto.isdigit() or int(proto) > 147
+ ):
+ raise ConfigError(
+ f'{err_msg} protocol name {protocol} is not valid'
+ )
+
+ # verify TCP flags
+ if 'tcp_flags' in rule_config:
+ if rule_config.get('protocol') != 'tcp':
+ raise ConfigError(
+ f'{err_msg} protocol must be tcp when specifying tcp flags'
+ )
+
+ not_flags = rule_config.get('tcp_flags').get('not', [])
+ if not_flags:
+ duplicates = [
+ flag
+ for flag in rule_config.get('tcp_flags')
+ if flag in not_flags
+ ]
+ if duplicates:
+ raise ConfigError(
+ f'{err_msg} cannot match a tcp flag as set and not set: {duplicates}'
+ )
+
+ for iface, iface_config in acl.get('interface', {}).items():
+ if not any(key in iface_config for key in ('input', 'output')):
+ raise ConfigError(
+ f'Please specify direction input/output for interface {iface}'
+ )
+
+ for direction in ['input', 'output']:
+ if direction in iface_config:
+ iface_acl = iface_config.get(direction)
+ if 'acl_tag' not in iface_acl:
+ raise ConfigError(
+ f'"acl-tag" is required for {direction} interface {iface}'
+ )
+
+ used_names = []
+ for tag, tag_conf in iface_acl.get('acl_tag').items():
+ if 'tag_name' not in tag_conf:
+ raise ConfigError(
+ f'"tag-name" is required for {direction} interface {iface} with acl-tag {tag}'
+ )
+ name = tag_conf.get('tag_name')
+ if name not in acl.get('tag_name').keys():
+ raise ConfigError(
+ f'ACL with tag-name {name} does not exist. '
+ f'Cannot use it for {direction} interface {iface}'
+ )
+ if name in used_names:
+ raise ConfigError(
+ f'ACL with tag-name {name} is already used for {direction} interface {iface}'
+ )
+ used_names.append(name)
+
+ if 'macip' in config:
+ acl = config.get('macip')
+ for iface, iface_config in acl.get('interface', {}).items():
+ if 'tag_name' not in iface_config:
+ raise ConfigError(f'"tag-name" is required for interface {iface}')
+ name = iface_config.get('tag_name')
+ if name not in acl.get('tag_name').keys():
+ raise ConfigError(
+ f'ACL with tag-name {name} does not exist. Cannot use it for interface {iface}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ acl = Acl()
+
+ if 'effective' in config:
+ # Delete ACL ip
+ if 'ip' in config.get('effective'):
+ remove_config_ip = config.get('effective').get('ip')
+
+ # Delete ACL interfaces
+ for interface in config.get('changed_ip_ifaces'):
+ acl.delete_acl_interface(interface)
+
+ # Delete ACLs
+ for acl_name in remove_config_ip.get('tag_name'):
+ if acl_name not in config.get('ip', {}).get('tag_name', {}):
+ acl.delete_acl(acl_name)
+
+ # Delete ACL macip
+ if 'macip' in config.get('effective'):
+ remove_config_macip = config.get('effective').get('macip')
+
+ # Delete ACL interfaces
+ for interface in config.get('changed_macip_ifaces'):
+ acl.delete_acl_macip_interface(interface)
+
+ # Delete ACL macip
+ for acl_name in remove_config_macip.get('tag_name'):
+ if acl_name not in config.get('macip', {}).get('tag_name', {}):
+ acl.delete_acl_macip(acl_name)
+
+ if 'remove' in config:
+ return None
+
+ # Add or replace ACL ip
+ config_ip = config.get('ip', {})
+ for acl_name in config_ip.get('tag_name', {}):
+ rules = create_ip_rules_list(
+ config_ip.get('tag_name').get(acl_name).get('rule')
+ )
+ acl.add_replace_acl(acl_name, rules)
+
+ for iface, iface_config in config_ip.get('interface', {}).items():
+ input_tags = [
+ v['tag_name']
+ for v in iface_config.get('input', {}).get('acl_tag', {}).values()
+ ]
+ output_tags = [
+ v['tag_name']
+ for v in iface_config.get('output', {}).get('acl_tag', {}).values()
+ ]
+ acl.add_acl_interface(iface, input_tags, output_tags)
+
+ # Add or replace ACL macip
+ config_macip = config.get('macip', {})
+ for acl_name in config_macip.get('tag_name', {}):
+ rules = create_macip_rules_list(
+ config_macip.get('tag_name').get(acl_name).get('rule')
+ )
+ acl.add_replace_acl_macip(acl_name, rules)
+
+ for iface, iface_config in config_macip.get('interface', {}).items():
+ acl.add_acl_macip_interface(iface, iface_config.get('tag_name'))
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py
new file mode 100644
index 000000000..098cf8bed
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bonding.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos import ConfigError
+
+from vyos.vpp.interface import BondInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_list, cli_ifaces_lcp_kernel_list
+
+
+def _get_bond_mode(mode_name: str) -> int:
+ """Convert VyOS CLI name bonding mode to VPP compatible"""
+ mode_mapping = {
+ 'round-robin': 1,
+ 'active-backup': 2,
+ 'xor-hash': 3,
+ 'broadcast': 4,
+ '802.3ad': 5,
+ }
+
+ return mode_mapping.get(mode_name, 5)
+
+
+def _get_bond_lb(lb_name: str) -> int:
+ """Convert VyOS CLI name bonding load balance to VPP compatible"""
+ lb_mapping = {
+ 'layer2': 0,
+ 'layer2+3': 2,
+ 'layer3+4': 1,
+ }
+
+ return lb_mapping.get(lb_name, 5)
+
+
+def get_config(config=None) -> dict:
+ """Get Bonding interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: Bonding interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'bonding']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate')
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # convert values to VPP compatible
+ if 'mode' in config:
+ config['mode'] = _get_bond_mode(config['mode'])
+ if 'hash_policy' in config:
+ config['hash_policy'] = _get_bond_lb(config['hash_policy'])
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ if effective_config.get('kernel_interface') or __name__ != '__main__':
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove_vpp' in config:
+ return None
+
+ verify_vpp_remove_kernel_interface(config)
+ verify_vpp_remove_xconnect_interface(config)
+
+ # Member must belong to VPP
+ for iface in config.get('member', {}).get('interface', []):
+ if iface not in config['vpp_ifaces']:
+ raise ConfigError(f'{iface} must be a VPP interface for bonding')
+
+ if 'remove' in config:
+ return None
+
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # remove old members
+ if 'effective' in config:
+ members = config['effective'].get('member', {}).get('interface', [])
+
+ kernel_interface = config['effective'].get('kernel_interface', '')
+ i = BondInterface(ifname, kernel_interface=kernel_interface)
+ for member in members:
+ i.detach_member(interface=member)
+
+ if 'kernel_interface' in config['effective']:
+ i.kernel_delete()
+ # Delete bonding interface
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Create a new one
+ mode = config.get('mode')
+ lb = config.get('hash_policy')
+ members = config.get('member', {}).get('interface', [])
+ mac = config.get('mac', '')
+ kernel_interface = config.get('kernel_interface', '')
+ state = 'up' if 'disable' not in config else 'down'
+
+ i = BondInterface(ifname, mode, lb, mac, kernel_interface, state)
+ i.add()
+ # Add members to bond
+ if members:
+ for member in members:
+ i.add_member(interface=member)
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py
new file mode 100644
index 000000000..64722c10c
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bridge.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos import ConfigError
+from vyos.vpp.interface import BridgeInterface
+from vyos.vpp.utils import iftunnel_transform
+
+
+def get_config(config=None) -> dict:
+ """Get Bridge interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: Bridge interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'bridge']
+ vpp_interfaces = ['vpp', 'settings', 'interface']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global vpp interfaces for verify
+ config['vpp_interfaces'] = conf.get_config_dict(
+ vpp_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # determine which members have been removed
+ interfaces_removed = node_changed(conf, base + [ifname, 'member', 'interface'])
+ if interfaces_removed:
+ config['members_removed'] = interfaces_removed
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ # Check if interface exists in vpp before adding to bridge-domain
+
+ allowed_prefixes = ('bond', 'gre', 'geneve', 'lo', 'vxlan')
+
+ if 'member' in config:
+ bvi_exists = False
+ for member, member_config in (
+ config.get('member', {}).get('interface', {}).items()
+ ):
+ # Check if the interface exists in VPP settings or starts with allowed prefixes
+ if not (
+ member in config.get('vpp_interfaces', {})
+ or member.startswith(allowed_prefixes)
+ ):
+ raise ConfigError(
+ f"Interface '{member}' not found in 'vpp settings interface' or does not start with allowed prefixes {allowed_prefixes}"
+ )
+
+ # Check if BVI is already defined, only one BVI per bridge domain is allowed
+ if 'bvi' in member_config:
+ if bvi_exists:
+ raise ConfigError("Only one BVI per bridge domain is allowed")
+ if not member.startswith('lo'):
+ raise ConfigError("BVI can only be defined on loopback interface")
+ bvi_exists = True
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # vxlan10 in the vpp is vxlan_tunnel10
+ interface_transform_filter = ('geneve', 'vxlan')
+ # update members
+ if 'members_removed' in config:
+ i = BridgeInterface(ifname)
+ for member in config.get('members_removed'):
+ if member.startswith(interface_transform_filter):
+ member = iftunnel_transform(member)
+ if member.startswith('lo'):
+ # interface name in VPP is loopX
+ member = member.replace('lo', 'loop')
+ elif member.startswith('bond'):
+ # interface name in VPP is BondEthernetX
+ member = member.replace('bond', 'BondEthernet')
+ i.detach_member(member=member)
+
+ # Delete bridge domain
+ if 'effective' in config:
+ ifname = config.get('ifname')
+ i = BridgeInterface(ifname)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add bridge domain
+ members = config.get('member', {}).get('interface', '')
+ i = BridgeInterface(ifname)
+ i.add()
+ # Add members to bridge
+ if members:
+ br = BridgeInterface(ifname)
+ port_type = 0
+ for member, member_config in members.items():
+ if member.startswith(interface_transform_filter):
+ member = iftunnel_transform(member)
+ if member.startswith('lo'):
+ # interface name in VPP is loopX
+ member = member.replace('lo', 'loop')
+ if 'bvi' in member_config:
+ port_type = 1
+ elif member.startswith('bond'):
+ # interface name in VPP is BondEthernetX
+ member = member.replace('bond', 'BondEthernet')
+
+ br.add_member(member=member, port_type=port_type)
+ # set default port type 0 (not BVI)
+ port_type = 0
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py
new file mode 100644
index 000000000..81050444e
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ethernet.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdep import set_dependents
+from vyos import ConfigError
+from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface
+
+
+def get_config(config=None) -> dict:
+ """Get Ethernet interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: Ethernet interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'ethernet']
+ base_interfaces_xconnect = ['vpp', 'interfaces', 'xconnect']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not conf.exists(base + [ifname]):
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ config.update({'remove': effective_config})
+ else:
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ # Get global 'vpp interfaces xconnect'
+ config['vpp_interfaces_xconnect'] = conf.get_config_dict(
+ base_interfaces_xconnect,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # Dependency
+ for xcon_name, xcon_config in config.get('vpp_interfaces_xconnect').items():
+ for member_name in xcon_config.get('member', {}).get('interface', []):
+ if member_name == ifname:
+ set_dependents('vpp_interfaces_xconnect', conf, xcon_name)
+
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ verify_vpp_remove_xconnect_interface(config)
+ if 'remove' in config:
+ return None
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ # Delete interface
+ if 'remove' in config:
+ pass
+ # remove_config = config.get('remove')
+ # ifname = config.get('ifname')
+ # src_addr = remove_config.get('source_address')
+ # dst_addr = remove_config.get('remote')
+ # vni = int(remove_config.get('vni'))
+ # v = VXLANInterface(ifname, src_addr, dst_addr, vni)
+ # v.delete()
+ else:
+ pass
+ # ifname = config.get('ifname')
+ # kernel_interface = config.get('kernel_interface', '')
+ # v = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface)
+ # v.add()
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py
new file mode 100644
index 000000000..efac4e65a
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_geneve.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import GeneveInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None) -> dict:
+ """Get Geneve interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ recursive_defaults (bool, optional): Include recursive defaults
+ Returns:
+ dict: Geneve interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'geneve']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # No need to verify anything if vpp is removed
+ if 'remove_vpp' in config:
+ return None
+
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces geneve geneveX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ required_keys = {'source_address', 'remote', 'vni'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # Change 'vpp interfaces geneve geneveX kernel-interface vpp-tunX'
+ # => 'vpp interfaces geneve geneveX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ vni = int(remove_config.get('vni'))
+ i = GeneveInterface(ifname, src_addr, dst_addr, vni)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ vni = int(config.get('vni'))
+ kernel_interface = config.get('kernel_interface', '')
+ i = GeneveInterface(ifname, src_addr, dst_addr, vni, kernel_interface)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py
new file mode 100644
index 000000000..0b60d06b5
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_gre.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import GREInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+ verify_vpp_tunnel_source_address,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces
+
+
+def get_config(config=None) -> dict:
+ """Get GRE interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: GRE interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'gre']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # list of all Ethernet interfaces with vifs
+ config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # No need to verify anything if vpp is removed
+ if 'remove_vpp' in config:
+ return None
+
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces gre greX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ # config removed
+ if 'remove' in config:
+ return None
+
+ # source-address and remote are mandatory options
+ required_keys = {'source_address', 'remote', 'mode', 'tunnel_type'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # verify source address and remote address
+ verify_vpp_tunnel_source_address(config)
+ if config.get('source_address') == config.get('remote'):
+ raise ConfigError('Remote address must not be the same as source address')
+
+ # check multipoint mode
+ if config.get('mode') == 'point-to-multipoint':
+ # For multipoint mode, remote IP must be 0.0.0.0
+ if config.get('remote') != '0.0.0.0':
+ raise ConfigError('For point-to-multipoint mode, remote must be 0.0.0.0')
+
+ # Change 'vpp interfaces gre greX kernel-interface vpp-tunX'
+ # => 'vpp interfaces gre greX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ i = GREInterface(ifname, src_addr, dst_addr)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ kernel_interface = config.get('kernel_interface', '')
+ mode = config.get('mode')
+ tunnel_type = config.get('tunnel_type')
+ state = 'up' if 'disable' not in config else 'down'
+ i = GREInterface(
+ ifname, src_addr, dst_addr, tunnel_type, mode, kernel_interface, state
+ )
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py
new file mode 100644
index 000000000..e4db1e93c
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ipip.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+
+from vyos.vpp.interface import IPIPInterface
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+ verify_vpp_tunnel_source_address,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces
+
+
+def get_config(config=None) -> dict:
+ """Get IPIP interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: IPIP interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'ipip']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # list of all Ethernet interfaces with vifs
+ config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ if config.get('effective', {}).get('kernel_interface'):
+ iface = config.get('kernel_interface')
+ if iface:
+ if iface in config.get('vpp_kernel_interfaces'):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ # No need to verify anything if vpp is removed
+ if 'remove_vpp' in config:
+ return None
+
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces ipip ipipX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ # config removed
+ if 'remove' in config:
+ return None
+
+ # source-address and remote are mandatory options
+ required_keys = {'source_address', 'remote'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # verify source address and remote address
+ verify_vpp_tunnel_source_address(config)
+ if config.get('source_address') == config.get('remote'):
+ raise ConfigError('Remote address must not be the same as source address')
+
+ # Change 'vpp interfaces ipip ipipX kernel-interface vpp-tunX'
+ # => 'vpp interfaces ipip ipipX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ i = IPIPInterface(ifname, src_addr, dst_addr)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ kernel_interface = config.get('kernel_interface', '')
+ state = 'up' if 'disable' not in config else 'down'
+ i = IPIPInterface(ifname, src_addr, dst_addr, kernel_interface, state)
+ i.add()
+
+ if 'kernel_interface' in config:
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py
new file mode 100644
index 000000000..8874ecf2d
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_loopback.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import LoopbackInterface
+from vyos.vpp.config_verify import (
+ verify_vpp_remove_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_exists_kernel_interface,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list
+
+
+def get_config(config=None) -> dict:
+ """Get Loopback interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: Loopback interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'loopback']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # Dependency
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # No need to verify anything if vpp is removed
+ if 'remove_vpp' in config:
+ return None
+
+ verify_vpp_remove_kernel_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ i = LoopbackInterface(ifname)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ kernel_interface = config.get('kernel_interface', '')
+ state = 'up' if 'disable' not in config else 'down'
+ i = LoopbackInterface(ifname, kernel_interface, state)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py
new file mode 100644
index 000000000..355139ffa
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_vxlan.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos import ConfigError
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdep import set_dependents, call_dependents
+from vyos.template import is_interface
+
+from vyos.vpp.interface import VXLANInterface
+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_kernel_interface,
+ verify_vpp_change_kernel_interface,
+ verify_vpp_remove_xconnect_interface,
+ verify_vpp_exists_kernel_interface,
+ verify_vpp_tunnel_source_address,
+)
+from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces
+
+
+def get_config(config=None) -> dict:
+ """Get VXLAN interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: VXLAN interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'vxlan']
+ base_kernel_interfaces = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ if not conf.exists(base + [ifname]):
+ config['remove'] = True
+
+ # Get global 'vpp kernel-interfaces' for verify
+ config['vpp_kernel_interfaces'] = conf.get_config_dict(
+ base_kernel_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface'])
+ if tmp:
+ config['kernel_interface_removed'] = tmp
+
+ # list of all kernel interfaces `vpp interface xxx kernel-interface xxx`
+ config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf)
+
+ # list of all Ethernet interfaces with vifs
+ config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf)
+
+ # Dependency
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ if ifname in config['xconn_members']:
+ for xconn_iface in config['xconn_members'][ifname]:
+ set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)
+
+ config['bridge_members'] = deps_bridge_dict(conf)
+ if ifname in config['bridge_members']:
+ for bridge_iface in config['bridge_members'][ifname]:
+ set_dependents('vpp_interfaces_bridge', conf, bridge_iface)
+
+ if effective_config.get('kernel_interface'):
+ if conf.exists(base + [ifname, 'kernel-interface']):
+ iface = config.get('kernel_interface')
+ if conf.exists(['vpp', 'kernel-interfaces', iface]):
+ set_dependents('vpp_kernel_interface', conf, iface)
+
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
+ config['ifname'] = ifname
+ return config
+
+
+def verify(config):
+ # No need to verify anything if vpp is removed
+ if 'remove_vpp' in config:
+ return None
+
+ # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'.
+ # vpp interfaces vxlan vxlanX kernel-interface vpp-tunX
+ # vpp kernel-interface vpp-tunX
+ verify_vpp_remove_kernel_interface(config)
+
+ verify_vpp_remove_xconnect_interface(config)
+
+ if 'remove' in config:
+ return None
+
+ required_keys = {'source_address', 'remote', 'vni'}
+ if not all(key in config for key in required_keys):
+ missing_keys = required_keys - set(config.keys())
+ raise ConfigError(
+ f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}"
+ )
+
+ # verify source address and remote address
+ verify_vpp_tunnel_source_address(config)
+ if config.get('source_address') == config.get('remote'):
+ raise ConfigError('Remote address must not be the same as source address')
+
+ # Change 'vpp interfaces vxlan greX kernel-interface vpp-tunX'
+ # => 'vpp interfaces gre vxlanX kernel-interface vpp-tunY'
+ # check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ verify_vpp_change_kernel_interface(config)
+ verify_vpp_exists_kernel_interface(config)
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ # Delete interface
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ src_addr = remove_config.get('source_address')
+ dst_addr = remove_config.get('remote')
+ vni = int(remove_config.get('vni'))
+ i = VXLANInterface(ifname, src_addr, dst_addr, vni)
+ i.delete()
+
+ if 'remove' in config:
+ return None
+
+ # Add interface
+ src_addr = config.get('source_address')
+ dst_addr = config.get('remote')
+ vni = int(config.get('vni'))
+ kernel_interface = config.get('kernel_interface', '')
+ state = 'up' if 'disable' not in config else 'down'
+ i = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface, state)
+ i.add()
+
+ # Add kernel-interface (LCP) if interface is not exist
+ if 'kernel_interface' in config and not is_interface(kernel_interface):
+ i.kernel_add()
+
+ call_dependents()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py
new file mode 100644
index 000000000..994332ba2
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_xconnect.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.vpp.config_deps import deps_xconnect_dict
+from vyos.vpp.interface import XconnectInterface
+from vyos.vpp.utils import cli_ifaces_list
+
+
+def get_config(config=None) -> dict:
+ """Get Xconnect interface configuration
+
+ Args:
+ config (vyos.config.Config, optional): The VyOS configuration dictionary
+ Returns:
+ dict: Bridge interface configuration
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'interfaces', 'xconnect']
+ vpp_interfaces = ['vpp', 'settings', 'interface']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ # Get global vpp interfaces for verify
+ config['vpp_interfaces'] = conf.get_config_dict(
+ vpp_interfaces,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ config['xconn_members'] = deps_xconnect_dict(conf)
+ config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate')
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ # Xconnect requires 2 members
+ if len(config.get('member', {}).get('interface')) != 2:
+ raise ConfigError('Cross connect requires 2 members')
+
+ # Member must belong to VPP
+ for iface in config.get('member', {}).get('interface', []):
+ if iface not in config['vpp_ifaces']:
+ raise ConfigError(f'{iface} must be a VPP interface for xconnect')
+
+ # Each interface can belong only to one xconnect
+ for xconn_member, xconn_ifaces in config['xconn_members'].items():
+ if len(xconn_ifaces) > 1:
+ raise ConfigError(
+ f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+
+ # Delete xconnect
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ members = remove_config.get('member', {}).get('interface')
+ i = XconnectInterface(ifname, members=members)
+ i.del_l2_xconnect()
+
+ if 'remove' in config:
+ return None
+
+ # Add xconnect
+ members = config.get('member', {}).get('interface')
+ state = 'up' if 'disable' not in config else 'down'
+ i = XconnectInterface(ifname, members=members, state=state)
+ i.add_l2_xconnect()
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py
new file mode 100644
index 000000000..85706094e
--- /dev/null
+++ b/src/conf_mode/vpp_kernel-interfaces.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import node_changed
+from vyos import ConfigError
+from vyos.ifconfig import Interface
+from vyos.utils.network import interface_exists
+from vyos.utils.process import call
+from vyos.vpp import VPPControl
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'kernel-interfaces']
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dicitonary per interface delete
+ if __name__ == '__main__':
+ effective_config = conf.get_config_dict(
+ base + [ifname],
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ # if a file was started as dependency, we are starting from empty config
+ else:
+ effective_config = {}
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ address_removed = leaf_node_changed(conf, base + [ifname, 'address'])
+ if address_removed:
+ config['address_removed'] = address_removed
+
+ description_removed = leaf_node_changed(conf, base + [ifname, 'description'])
+ if description_removed:
+ config['description_removed'] = {}
+
+ vlans_removed = node_changed(conf, base + [ifname, 'vif'])
+ if vlans_removed:
+ config['vlans_removed'] = vlans_removed
+
+ config['ifname'] = ifname
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ # Interface must exists before it is configured
+ if not interface_exists(config['ifname']):
+ raise ConfigError(
+ f'Interface {config["ifname"]} must be created before using in configuration'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ ifname = config.get('ifname')
+ i = Interface(ifname)
+ # update/remove addresses
+ if 'address_removed' in config:
+ for address in config['address_removed']:
+ i.del_addr(address)
+ # remove description
+ if 'description_removed' in config:
+ i.set_alias('')
+
+ # remove VLANs
+ if 'vlans_removed' in config:
+ for vlan in config['vlans_removed']:
+ call(f'ip link del dev {ifname}.{vlan}')
+
+ # Delete
+ if 'remove' in config:
+ pass
+ else:
+ # Add address
+ if 'address' in config:
+ for address in config['address']:
+ i.add_addr(address)
+ # Set MTU
+ if 'mtu' in config:
+ i.set_mtu(config.get('mtu'))
+ # Set description
+ if 'description' in config:
+ i.set_alias(config.get('description'))
+ # Admin state down
+ if 'disable' in config:
+ i.set_admin_state('down')
+ else:
+ i.set_admin_state('up')
+
+ for vlan, vlan_config in config.get('vif', {}).items():
+ if vlan not in config.get('effective', {}).get('vif', {}).keys():
+ call(
+ f'ip link add link {ifname} name {ifname}.{vlan} type vlan id {vlan}'
+ )
+ call(f'ip link set dev {ifname}.{vlan} up')
+ v = Interface(f'{ifname}.{vlan}')
+
+ # VLAN address
+ addresses_effective = (
+ config.get('effective', {})
+ .get('vif', {})
+ .get(vlan, {})
+ .get('address', [])
+ )
+ addresses_candidate = vlan_config.get('address', [])
+
+ for ipaddr in addresses_effective:
+ if ipaddr not in addresses_candidate:
+ v.del_addr(ipaddr)
+ for ipaddr in addresses_candidate:
+ if ipaddr not in addresses_effective:
+ v.add_addr(ipaddr)
+
+ # VLAN description
+ description_effective = (
+ config.get('effective', {})
+ .get('vif', {})
+ .get(vlan, {})
+ .get('description', '')
+ )
+ description_candidate = vlan_config.get('description', '')
+
+ if description_candidate:
+ v.set_alias(description_candidate)
+ elif description_effective and not description_candidate:
+ v.set_alias('')
+
+ v.set_admin_state('up')
+
+ # Set rx-mode
+ rx_mode = config.get('rx_mode')
+ if rx_mode:
+ vpp_control = VPPControl()
+ lcp_name = vpp_control.lcp_pair_find(kernel_name=ifname).get('vpp_name_kernel')
+ vpp_control.iface_rxmode(lcp_name, rx_mode)
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py
new file mode 100644
index 000000000..7c9fd2765
--- /dev/null
+++ b/src/conf_mode/vpp_nat.py
@@ -0,0 +1,488 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import ipaddress
+
+from vyos import ConfigError
+
+from vyos.configdiff import Diff
+from vyos.configdict import node_changed
+from vyos.config import Config
+from vyos.utils.network import get_interface_address
+
+from vyos.vpp.utils import cli_ifaces_list
+from vyos.vpp.nat.nat44 import Nat44
+
+
+protocol_map = {
+ 'all': 0,
+ 'icmp': 1,
+ 'tcp': 6,
+ 'udp': 17,
+}
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'nat44']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dictionary for deletion
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+ return config
+
+ config_changed = node_changed(
+ conf,
+ base,
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ changed_static_rules = node_changed(
+ conf,
+ base + ['static', 'rule'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ changed_exclude_rules = node_changed(
+ conf,
+ base + ['exclude', 'rule'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ if not config_changed:
+ changed_static_rules = list(config.get('static', {}).get('rule', {}).keys())
+ changed_exclude_rules = list(config.get('exclude', {}).get('rule', {}).keys())
+
+ config.update(
+ {
+ 'changed_static_rules': changed_static_rules,
+ 'changed_exclude_rules': changed_exclude_rules,
+ 'vpp_ifaces': cli_ifaces_list(conf),
+ }
+ )
+
+ if conf.exists(['vpp', 'settings', 'nat44', 'timeout']):
+ timeouts = conf.get_config_dict(
+ ['vpp', 'settings', 'nat44', 'timeout'],
+ key_mangling=('-', '_'),
+ with_defaults=True,
+ )
+ config.update(timeouts)
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ return config
+
+
+def convert_range_to_list_ips(address_range) -> list:
+ """Converts IP range to a list of IPs .
+
+ Example:
+ % ip = IPOperations('192.0.0.1-192.0.2.5')
+ % ip.convert_prefix_to_list_ips()
+ ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5']
+ """
+ if '-' in address_range:
+ start_ip, end_ip = address_range.split('-')
+ start_ip = ipaddress.ip_address(start_ip)
+ end_ip = ipaddress.ip_address(end_ip)
+ return [
+ str(ipaddress.ip_address(ip))
+ for ip in range(int(start_ip), int(end_ip) + 1)
+ ]
+ else:
+ return [address_range]
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ if 'interface' not in config:
+ raise ConfigError('Interfaces must be configured for NAT44')
+
+ required_keys = {'inside', 'outside'}
+ missing_keys = required_keys - set(config['interface'].keys())
+ if missing_keys:
+ raise ConfigError(
+ f'Both inside and outside interfaces must be configured. Please add: {", ".join(missing_keys)}'
+ )
+
+ for interface in config['interface']['inside']:
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for inside NAT interface'
+ )
+ for interface in config['interface']['outside']:
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for outside NAT interface'
+ )
+
+ if not config.get('address_pool', {}).get('translation') and not config.get(
+ 'static', {}
+ ).get('rule'):
+ raise ConfigError('"address-pool translation" or "static rule" is required')
+
+ addresses_translation = []
+ addresses_twice_nat = []
+ if 'address_pool' in config:
+ address_pool = config.get('address_pool')
+ if 'translation' in address_pool:
+ if not address_pool['translation'].get('address') and not address_pool[
+ 'translation'
+ ].get('interface'):
+ raise ConfigError(
+ '"address-pool translation" requires address or interface'
+ )
+
+ for address_range in address_pool['translation'].get('address', []):
+ addresses = convert_range_to_list_ips(address_range)
+ for address in addresses:
+ if address in addresses_translation:
+ raise ConfigError(
+ f'Address {address} is already in use in "address-pool translation address"'
+ )
+ addresses_translation.append(address)
+
+ for interface in address_pool['translation'].get('interface', []):
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for "address-pool translation interface"'
+ )
+ iface_address = (
+ get_interface_address(interface)
+ .get('addr_info', [])[0]
+ .get('local')
+ )
+ addresses_translation.append(iface_address)
+
+ if 'twice_nat' in address_pool:
+ if not address_pool['twice_nat'].get('address') and not address_pool[
+ 'twice_nat'
+ ].get('interface'):
+ raise ConfigError(
+ '"address-pool twice-nat" requires address or interface'
+ )
+
+ for address_range in address_pool['twice_nat'].get('address', []):
+ addresses = convert_range_to_list_ips(address_range)
+ for address in addresses:
+ if address in addresses_twice_nat:
+ raise ConfigError(
+ f'Address {address} is already in use in "address-pool twice-nat address"'
+ )
+ addresses_twice_nat.append(address)
+
+ for interface in address_pool['twice_nat'].get('interface', []):
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for "address-pool twice-nat interface"'
+ )
+ iface_address = (
+ get_interface_address(interface)
+ .get('addr_info', [])[0]
+ .get('local')
+ )
+ addresses_twice_nat.append(iface_address)
+
+ if 'static' in config:
+ addresses_with_ports = set()
+ addresses_without_ports = set()
+ local_addresses = set()
+
+ for rule, rule_config in config['static']['rule'].items():
+ error_msg = f'Configuration error in static rule {rule}:'
+
+ if not rule_config.get('local', {}).get('address'):
+ raise ConfigError(f'{error_msg} local settings require address')
+
+ if not rule_config.get('external', {}).get('address'):
+ raise ConfigError(f'{error_msg} external settings require address')
+
+ has_local_port = 'port' in rule_config.get('local', {})
+ has_external_port = 'port' in rule_config.get('external', {})
+
+ if not has_external_port == has_local_port:
+ raise ConfigError(
+ f'{error_msg} source and destination ports must either '
+ 'both be specified, or neither must be specified'
+ )
+
+ ext_address = rule_config['external']['address']
+ port = rule_config['external'].get('port')
+ local_address = rule_config['local']['address']
+
+ if port:
+ pair = (ext_address, port)
+ if (
+ pair in addresses_with_ports
+ or ext_address in addresses_without_ports
+ ):
+ raise ConfigError(
+ f'{error_msg} external address/port is already in use!'
+ )
+ addresses_with_ports.add(pair)
+ if ext_address not in addresses_translation:
+ raise ConfigError(
+ f'{error_msg} external address {ext_address} is not in "address-pool translation"'
+ )
+
+ else:
+ if ext_address in addresses_without_ports or any(
+ addr == ext_address for addr, _ in addresses_with_ports
+ ):
+ raise ConfigError(
+ f'{error_msg} external address is already in use!'
+ )
+ addresses_without_ports.add(ext_address)
+
+ if local_address in local_addresses:
+ raise ConfigError(
+ f'{error_msg} local address {local_address} is already in use'
+ )
+ local_addresses.add(local_address)
+
+ options = rule_config.get('options', {})
+ if all(key in options for key in ('twice_nat', 'self_twice_nat')):
+ raise ConfigError(
+ f'{error_msg} cannot set both options "twice-nat" and "self-twice-nat"'
+ )
+ if any(key in options for key in ('twice_nat', 'self_twice_nat')):
+ if not has_local_port or rule_config['protocol'] == 'all':
+ raise ConfigError(
+ f'{error_msg} twice-nat/self-twice-nat options require port and protocol to be set'
+ )
+ if not config.get('address_pool', {}).get('twice_nat'):
+ raise ConfigError(
+ f'{error_msg} twice-nat/self-twice-nat options require "address-pool twice-nat" to be set'
+ )
+ if 'twice_nat_address' in options:
+ if not any(key in options for key in ('twice_nat', 'self_twice_nat')):
+ raise ConfigError(
+ f'{error_msg} twice-nat/self-twice-nat option required when twice-nat-address is set'
+ )
+ tn_address = options['twice_nat_address']
+ if tn_address not in addresses_twice_nat:
+ raise ConfigError(
+ f'{error_msg} twice-nat-address {tn_address} is not in "address-pool twice-nat"'
+ )
+
+ if 'exclude' in config:
+ for rule, rule_config in config['exclude']['rule'].items():
+ keys = {'local_address', 'external_interface'}
+ if not any(key in rule_config for key in keys):
+ raise ConfigError(
+ f'Local-address or external-interface must be specified for exclude rule {rule}'
+ )
+ if all(key in rule_config for key in keys):
+ raise ConfigError(
+ f'Cannot set both address and interface for exclude rule {rule}'
+ )
+ if (
+ 'external_interface' in rule_config
+ and rule_config.get('external_interface') not in config['vpp_ifaces']
+ ):
+ raise ConfigError(
+ f'{rule_config["external_interface"]} must be a VPP interface for exclude rule {rule}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ n = Nat44()
+
+ if 'remove' in config:
+ n.disable_nat44_ed()
+ return None
+
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ # Delete inside interfaces
+ for interface in remove_config['interface']['inside']:
+ if interface not in config.get('interface', {}).get('inside', []):
+ n.delete_nat44_interface_inside(interface)
+ # Delete outside interfaces
+ for interface in remove_config['interface']['outside']:
+ if interface not in config.get('interface', {}).get('outside', []):
+ n.delete_nat44_interface_outside(interface)
+ # Delete address pool
+ address_pool = config.get('address_pool', {})
+ for address in (
+ remove_config.get('address_pool', {})
+ .get('translation', {})
+ .get('address', [])
+ ):
+ if address not in address_pool.get('translation', {}).get('address', []):
+ n.delete_nat44_address_range(address, twice_nat=False)
+ for interface in (
+ remove_config.get('address_pool', {})
+ .get('translation', {})
+ .get('interface', [])
+ ):
+ if interface not in address_pool.get('translation', {}).get(
+ 'interface', []
+ ):
+ n.delete_nat44_interface_address(interface, twice_nat=False)
+ for address in (
+ remove_config.get('address_pool', {})
+ .get('twice_nat', {})
+ .get('address', [])
+ ):
+ if address not in address_pool.get('twice_nat', {}).get('address', []):
+ n.delete_nat44_address_range(address, twice_nat=True)
+ for interface in (
+ remove_config.get('address_pool', {})
+ .get('twice_nat', {})
+ .get('interface', [])
+ ):
+ if interface not in address_pool.get('twice_nat', {}).get('interface', []):
+ n.delete_nat44_interface_address(interface, twice_nat=True)
+ # Delete NAT static mapping rules
+ for rule in config['changed_static_rules']:
+ if rule in remove_config.get('static', {}).get('rule', {}):
+ rule_config = remove_config['static']['rule'][rule]
+ n.delete_nat44_static_mapping(
+ local_ip=rule_config.get('local').get('address'),
+ external_ip=rule_config.get('external', {}).get('address', ''),
+ local_port=int(rule_config.get('local', {}).get('port', 0)),
+ external_port=int(rule_config.get('external', {}).get('port', 0)),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ twice_nat='twice_nat' in rule_config.get('options', {}),
+ self_twice_nat='self_twice_nat' in rule_config.get('options', {}),
+ out2in='out_to_in_only' in rule_config.get('options', {}),
+ pool_ip=rule_config.get('options', {}).get('twice_nat_address'),
+ )
+ # Delete NAT exclude rules
+ for rule in config['changed_exclude_rules']:
+ if rule in remove_config.get('exclude', {}).get('rule', {}):
+ rule_config = remove_config['exclude']['rule'][rule]
+ n.delete_nat44_identity_mapping(
+ ip_address=rule_config.get('local_address'),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ port=int(rule_config.get('local_port', 0)),
+ interface=rule_config.get('external_interface'),
+ )
+
+ # Add NAT44
+ n.enable_nat44_ed()
+ # Add inside interfaces
+ for interface in config['interface']['inside']:
+ n.add_nat44_interface_inside(interface)
+ # Add outside interfaces
+ for interface in config['interface']['outside']:
+ n.add_nat44_interface_outside(interface)
+ # Add translation pool
+ for address in (
+ config.get('address_pool', {}).get('translation', {}).get('address', [])
+ ):
+ n.add_nat44_address_range(address, twice_nat=False)
+ for interface in (
+ config.get('address_pool', {}).get('translation', {}).get('interface', [])
+ ):
+ n.add_nat44_interface_address(interface, twice_nat=False)
+ for address in (
+ config.get('address_pool', {}).get('twice_nat', {}).get('address', [])
+ ):
+ n.add_nat44_address_range(address, twice_nat=True)
+ for interface in (
+ config.get('address_pool', {}).get('twice_nat', {}).get('interface', [])
+ ):
+ n.add_nat44_interface_address(interface, twice_nat=True)
+ # Add NAT static mapping rules
+ for rule in config['changed_static_rules']:
+ if rule in config.get('static', {}).get('rule', {}):
+ rule_config = config['static']['rule'][rule]
+ n.add_nat44_static_mapping(
+ local_ip=rule_config.get('local').get('address'),
+ external_ip=rule_config.get('external', {}).get('address', ''),
+ local_port=int(rule_config.get('local', {}).get('port', 0)),
+ external_port=int(rule_config.get('external', {}).get('port', 0)),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ twice_nat='twice_nat' in rule_config.get('options', {}),
+ self_twice_nat='self_twice_nat' in rule_config.get('options', {}),
+ out2in='out_to_in_only' in rule_config.get('options', {}),
+ pool_ip=rule_config.get('options', {}).get('twice_nat_address'),
+ )
+ # Add NAT exclude rules
+ for rule in config['changed_exclude_rules']:
+ if rule in config.get('exclude', {}).get('rule', {}):
+ rule_config = config['exclude']['rule'][rule]
+ n.add_nat44_identity_mapping(
+ ip_address=rule_config.get('local_address'),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ port=int(rule_config.get('local_port', 0)),
+ interface=rule_config.get('external_interface'),
+ )
+ if 'timeout' in config:
+ n.set_nat_timeouts(
+ icmp=int(config.get('timeout').get('icmp')),
+ udp=int(config.get('timeout').get('udp')),
+ tcp_established=int(config.get('timeout').get('tcp_established')),
+ tcp_transitory=int(config.get('timeout').get('tcp_transitory')),
+ )
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_nat_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py
new file mode 100644
index 000000000..be2386ba2
--- /dev/null
+++ b/src/conf_mode/vpp_nat_cgnat.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos import ConfigError
+from vyos.config import Config, config_dict_merge
+from vyos.configdict import node_changed
+from vyos.configdiff import Diff
+from vyos.vpp.utils import cli_ifaces_list
+
+from vyos.vpp.nat.det44 import Det44
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'nat', 'cgnat']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not conf.exists(['vpp']):
+ config['remove_vpp'] = True
+ return config
+
+ # Get effective config as we need full dictionary to delete
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ # Get default values which we need to conditionally update into the
+ # dictionary retrieved.
+ default_values = conf.get_config_defaults(**config.kwargs, recursive=True)
+ config = config_dict_merge(default_values, config)
+
+ config_changed = node_changed(
+ conf,
+ base,
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ changed_rules = node_changed(
+ conf,
+ base + ['rule'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ if not config_changed:
+ changed_rules = list(config.get('rule', {}).keys())
+
+ config.update(
+ {
+ 'changed_rules': changed_rules,
+ 'vpp_ifaces': cli_ifaces_list(conf),
+ }
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config or 'remove_vpp' in config:
+ return None
+
+ if 'interface' not in config:
+ raise ConfigError('Interfaces must be configured for CGNAT')
+ if 'rule' not in config:
+ raise ConfigError('Rules must be configured for CGNAT')
+
+ required_keys = {'inside', 'outside'}
+ missing_keys = required_keys - set(config['interface'].keys())
+ if missing_keys:
+ raise ConfigError(
+ f'Both inside and outside interfaces must be configured. '
+ f'Please add: {", ".join(missing_keys)}'
+ )
+
+ conflict_ifaces = set(config['interface']['inside']).intersection(
+ set(config['interface']['outside'])
+ )
+ if conflict_ifaces:
+ raise ConfigError(
+ f'Interface cannot be both inside and outside. '
+ f'Please choose a side for: {", ".join(conflict_ifaces)} '
+ )
+
+ for interface in config['interface']['inside']:
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for inside CGNAT interface'
+ )
+ for interface in config['interface']['outside']:
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for outside CGNAT interface'
+ )
+
+ required_keys = {'outside_prefix', 'inside_prefix'}
+ for rule in config['rule']:
+ missing_keys = required_keys - set(config['rule'][rule].keys())
+ if missing_keys:
+ raise ConfigError(
+ f'Both inside-prefix and outside-prefix must be configured in rule {rule}. '
+ f'Please add: {", ".join(missing_keys).replace("_", "-")}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ if 'remove_vpp' in config:
+ return None
+
+ cgnat = Det44()
+
+ if 'remove' in config:
+ cgnat.disable_det44_plugin()
+ return None
+
+ if 'effective' in config:
+ remove_config = config.get('effective')
+ # Delete inside interfaces
+ for interface in remove_config['interface']['inside']:
+ if interface not in config.get('interface', {}).get('inside', []):
+ cgnat.delete_det44_interface_inside(interface)
+ # Delete outside interfaces
+ for interface in remove_config['interface']['outside']:
+ if interface not in config.get('interface', {}).get('outside', []):
+ cgnat.delete_det44_interface_outside(interface)
+ # Delete CGNAT rules
+ for rule in config['changed_rules']:
+ if rule in remove_config.get('rule', {}):
+ rule_config = remove_config['rule'][rule]
+ in_addr, in_plen = rule_config['inside_prefix'].split('/')
+ out_addr, out_plen = rule_config['outside_prefix'].split('/')
+ cgnat.delete_det44_mapping(
+ in_addr=in_addr,
+ in_plen=int(in_plen),
+ out_addr=out_addr,
+ out_plen=int(out_plen),
+ )
+
+ # Add DET44
+ cgnat.enable_det44_plugin()
+ # Add inside interfaces
+ for interface in config['interface']['inside']:
+ cgnat.add_det44_interface_inside(interface)
+ # Add outside interfaces
+ for interface in config['interface']['outside']:
+ cgnat.add_det44_interface_outside(interface)
+ # Add CGNAT rules
+ for rule in config['changed_rules']:
+ if rule in config.get('rule', {}):
+ rule_config = config['rule'][rule]
+ in_addr, in_plen = rule_config['inside_prefix'].split('/')
+ out_addr, out_plen = rule_config['outside_prefix'].split('/')
+ cgnat.add_det44_mapping(
+ in_addr=in_addr,
+ in_plen=int(in_plen),
+ out_addr=out_addr,
+ out_plen=int(out_plen),
+ )
+ # Set CGNAT timeouts
+ cgnat.set_det44_timeouts(
+ icmp=int(config['timeout']['icmp']),
+ udp=int(config['timeout']['udp']),
+ tcp_established=int(config['timeout']['tcp_established']),
+ tcp_transitory=int(config['timeout']['tcp_transitory']),
+ )
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_sflow.py b/src/conf_mode/vpp_sflow.py
new file mode 100644
index 000000000..7da4c24d6
--- /dev/null
+++ b/src/conf_mode/vpp_sflow.py
@@ -0,0 +1,145 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.vpp.utils import cli_ifaces_list
+from vyos.vpp import VPPControl
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'sflow']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ # Get effective config as we need full dictionary for deletion
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ # Get system sflow configuration to check for server
+ system_sflow = conf.get_config_dict(
+ ['system', 'sflow'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if system_sflow:
+ config['system_sflow'] = system_sflow
+
+ if not config:
+ config['remove'] = True
+ return config
+
+ # Add list of VPP interfaces to the config
+ config.update({'vpp_ifaces': cli_ifaces_list(conf)})
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config:
+ return None
+
+ # Check if interface section exists
+ if 'interface' not in config:
+ return None
+
+ # Verify that all interfaces specified exist in VPP
+ for interface in config['interface']:
+ if interface not in config['vpp_ifaces']:
+ raise ConfigError(
+ f'{interface} must be a VPP interface for sFlow monitoring'
+ )
+
+ # Verify sample rate is a positive integer
+ if 'sample_rate' in config:
+ try:
+ sample_rate = int(config['sample_rate'])
+ if sample_rate <= 0:
+ raise ConfigError('sFlow sample rate must be a positive integer')
+ except ValueError:
+ raise ConfigError('sFlow sample rate must be a valid integer')
+
+ # Verify that system sflow has enable-vpp defined
+ if 'system_sflow' not in config or 'vpp' not in config.get('system_sflow', {}):
+ raise ConfigError(
+ 'sFlow enable-vpp must be defined under system sflow configuration'
+ )
+
+
+def generate(config):
+ # No templates to render for sFlow
+ pass
+
+
+def apply(config):
+ # Initialize VPP control API
+ vpp = VPPControl(attempts=20, interval=500)
+
+ if 'remove' in config:
+ # Disable sFlow on all interfaces
+ for interface in config.get('effective', {}).get('interface', []):
+ vpp.cli_cmd(f'sflow enable-disable {interface} disable')
+ return None
+
+ # Configure sample rate if specified
+ if 'sample_rate' in config:
+ vpp.cli_cmd(f'sflow sampling-rate {config["sample_rate"]}')
+
+ # Configure interfaces
+ if 'interface' in config:
+ # Enable sFlow on specified interfaces
+ for interface in config['interface']:
+ vpp.cli_cmd(f'sflow enable-disable {interface}')
+
+ # Disable sFlow on interfaces that were removed from config
+ effective_interfaces = config.get('effective', {}).get('interface', [])
+ if effective_interfaces:
+ for interface in effective_interfaces:
+ if interface not in config['interface']:
+ vpp.cli_cmd(f'sflow enable-disable {interface} disable')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py
new file mode 100755
index 000000000..c159ad0aa
--- /dev/null
+++ b/src/op_mode/show_vpp_interfaces.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import argparse
+import json
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import rc_cmd
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import (
+ vpp_ifaces_list,
+ vpp_ip_addresses_by_index,
+ vpp_ifaces_stats,
+)
+
+
+def get_iproute_address_list(interface: str) -> list:
+ """Get data from the Linux command 'ip --json address list dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json address list' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json address list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def get_iproute_link_list(interface):
+ """Get data from the Linux command 'ip --json link show dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json link show' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json link list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def merge_dicts(*dicts) -> dict:
+ """Merge dictionaries into a new dictionary.
+
+ Args:
+ *dicts: Any number of dictionaries.
+
+ Returns:
+ dict: A new dictionary containing all the key-value pairs from the given dictionaries.
+ """
+ merged = {}
+ for dictionary in dicts:
+ merged.update(dictionary)
+ return merged
+
+
+def show_interfaces(interfaces_list: list) -> str:
+ """Get JSON info from linux and represent it in a table format
+ Use tabulate to generate table
+
+ Interface IP Address Mtu S/L Description
+ --------- ---------- --- --- -----------
+ dum0 203.0.113.1/32 1500 u/u
+ 100.64.1.1/24
+ eth0 192.168.122.14/24 1500 u/u WAN
+
+ :return:
+ """
+ table = []
+ for interface in interfaces_list:
+ # Get the data for the interface
+ ip_address_data = get_iproute_address_list(interface)
+ link_data = get_iproute_link_list(interface)
+
+ # Skip this interface if data is not available
+ if not link_data:
+ continue
+ interface_data = merge_dicts(ip_address_data[0], link_data[0])
+
+ # Get the interface name
+ interface_name = interface_data['ifname']
+
+ # Get the IP addresses and their corresponding prefixes
+ ip_info = [
+ (address['local'], address.get('prefixlen', ''))
+ for address in interface_data['addr_info']
+ ]
+
+ # Format the IP addresses with prefixes and line breaks
+ ip_addresses = '\n'.join(f'{ip}/{prefix}' for ip, prefix in ip_info)
+
+ # Get the MAC address
+ mac = interface_data.get('address', 'n/a')
+
+ # Get the MTU
+ mtu = interface_data.get('mtu')
+
+ # Get the state of the interface
+ state = interface_data['operstate'].lower()
+
+ # Get the description of the interface
+ description = interface_data.get('ifalias', '')
+
+ # Create the list of values for the table
+ values = [interface_name, ip_addresses, mac, mtu, state, description]
+
+ # Append the list of values to the table
+ table.append(values)
+
+ # Print the table with IP addresses listed on separate lines
+ headers = ['Interface', 'IP Address', 'MAC', 'MTU', 'State', 'Description']
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_dataplane(interfaces_list: list, filter_type: str = 'all') -> str:
+ table = []
+ interface_dp_filter = ('tun', 'tap')
+ lcp_pair_list = vpp.lcp_pairs_list()
+ vpp_name_kernel_to_kernel_name = {
+ entry['vpp_name_kernel']: entry['kernel_name'] for entry in lcp_pair_list
+ }
+ for interface in interfaces_list:
+ interface_name = interface.get('interface_name')
+ if filter_type == 'no_tun_tap' and interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ if filter_type == 'only_tun_tap' and not interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ kernel_name = vpp_name_kernel_to_kernel_name.get(interface_name, '')
+
+ dp_ip_addresses = vpp_ip_addresses_by_index(
+ vpp.api, interface.get('sw_if_index')
+ )
+ ip_addresses = '\n'.join(dp_ip_addresses)
+
+ mac = str(interface.get('l2_address', 'n/a'))
+ mtu = interface.get('mtu', [])[0]
+ # state
+ flags = interface.get('flags')
+ state = 'up' if flags == 3 else 'down'
+
+ iftype = interface.get('interface_dev_type').split()[0]
+
+ values = [kernel_name, interface_name, iftype, ip_addresses, mac, mtu, state]
+ table.append(values)
+ headers = [
+ 'Kernel',
+ 'Dataplane',
+ 'Type',
+ 'IP Address',
+ 'MAC',
+ 'MTU',
+ 'State',
+ ]
+ table = sorted(table)
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_hardware(intf_name) -> str:
+ if not intf_name:
+ intf_name = ''
+
+ statistics = vpp_ifaces_stats(intf_name)
+ for intf, stats in sorted(statistics.items()):
+ print(f'\n---------------------------------\nInterface {intf}:\n')
+ table = []
+ for k, v in stats.items():
+ if isinstance(v, dict):
+ for i, j in v.items():
+ table.append([f"{k} {i}", j])
+ else:
+ table.append([k, v])
+ print(tabulate(table, tablefmt="presto"))
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Show VPP interfaces')
+ parser.add_argument(
+ '--summary',
+ action='store_true',
+ help='Show summary of VPP interfaces (ethernet and kernel tun)',
+ )
+ parser.add_argument(
+ '--dataplane', action='store_true', help='Show VPP ethernet interfaces'
+ )
+ parser.add_argument(
+ '--kernel', action='store_true', help='Show VPP kernel interfaces'
+ )
+ parser.add_argument(
+ '--iproute', action='store_true', help='Show interfaces (iproute2)'
+ )
+ parser.add_argument(
+ '--hardware',
+ action='store_true',
+ help='Show more detailed statistics for VPP interfaces',
+ )
+ parser.add_argument('--intf-name', action='store', help='Kernel interface name')
+
+ args = parser.parse_args()
+
+ config = ConfigTreeQuery()
+
+ if not config.exists('vpp settings interface'):
+ print('VPP interfaces not configured')
+ exit(0)
+
+ vpp = VPPControl()
+ dp_ifaces_list = vpp_ifaces_list(vpp.api)
+
+ if args.summary:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='all'))
+
+ if args.dataplane:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='no_tun_tap'))
+ exit(0)
+
+ if args.kernel:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='only_tun_tap'))
+
+ if args.iproute:
+ vpp_interfaces = []
+ vpp_ethernet = config.list_nodes('vpp settings interface')
+ vpp_interfaces.extend(vpp_ethernet)
+ if config.exists('vpp kernel-interfaces'):
+ vpp_kernel_interfaces = config.list_nodes('vpp kernel-interfaces')
+ vpp_interfaces.extend(vpp_kernel_interfaces)
+ print(show_interfaces(interfaces_list=vpp_interfaces))
+
+ if args.hardware:
+ show_interfaces_hardware(intf_name=args.intf_name)
diff --git a/src/op_mode/show_vpp_nat44.py b/src/op_mode/show_vpp_nat44.py
new file mode 100644
index 000000000..500280210
--- /dev/null
+++ b/src/op_mode/show_vpp_nat44.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+protocol_map = {
+ 0: 'all',
+ 1: 'icmp',
+ 6: 'tcp',
+ 17: 'udp',
+}
+
+# NAT flags
+flags_map = {
+ 'twice-nat': 0x01,
+ 'self-twice-nat': 0x02,
+ 'out2in-only': 0x04,
+ 'out': 0x10,
+ 'in': 0x20,
+}
+
+
+def _verify(func):
+ """Decorator checks if config for VPP NAT44 exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ base = 'vpp nat44'
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured')
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+def decode_bitmask(bitmask: int) -> list:
+ """Decode a bitmask into a list of flag names"""
+ return [name for name, value in flags_map.items() if bitmask & value]
+
+
+def _get_raw_output(data_dump):
+ data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump]
+ return data
+
+
+def _get_raw_output_sessions(vpp_api):
+ users: list[dict] = vpp_api.nat44_user_dump()
+ sessions_list: list[dict] = []
+ for user in users:
+ ip_address = str(user._asdict().get('ip_address'))
+ user_sessions_dump = vpp_api.nat44_user_session_v3_dump(ip_address=ip_address)
+ user_sessions = [
+ json.loads(json.dumps(session._asdict(), default=str))
+ for session in user_sessions_dump
+ ]
+ sessions_list.extend(user_sessions)
+ return sorted(sessions_list, key=lambda x: x["inside_ip_address"])
+
+
+def _get_formatted_output_sessions(sessions_list):
+ print('NAT44 ED sessions:')
+ print(f'--------------- {len(sessions_list)} sessions ---------------')
+ for session in sessions_list:
+ in_ip_addr = session.get('inside_ip_address')
+ in_port = session.get('inside_port')
+ out_ip_addr = session.get('outside_ip_address')
+ out_port = session.get('outside_port')
+ protocol = protocol_map[session.get('protocol')].upper()
+ last_heard = session.get('last_heard')
+ time_since_last_heard = session.get('time_since_last_heard')
+ total_bytes = session.get('total_bytes')
+ total_pkts = session.get('total_pkts')
+ ext_host_address = session.get('ext_host_address')
+ ext_host_port = session.get('ext_host_port')
+ is_timed_out = session.get('is_timed_out')
+
+ print(f' i2o {in_ip_addr} proto {protocol} port {in_port}')
+ print(f' o2i {out_ip_addr} proto {protocol} port {out_port}')
+ print(f' external host {ext_host_address}:{ext_host_port}')
+ print(
+ f' i2o flow: match: saddr {in_ip_addr} sport {in_port} daddr {ext_host_address} dport {ext_host_port} proto {protocol} rewrite: saddr {out_ip_addr}'
+ + (
+ f' sport {out_port}'
+ if protocol != 'ICMP'
+ else f' daddr {ext_host_address} icmp-id {ext_host_port}'
+ )
+ )
+ print(
+ f' o2i flow: match: saddr {ext_host_address} sport {ext_host_port} daddr {out_ip_addr} dport {out_port} proto {protocol} rewrite: '
+ + (
+ f'daddr {in_ip_addr} dport {in_port}'
+ if protocol != 'ICMP'
+ else f' saddr {ext_host_address} daddr {in_ip_addr} icmp-id {ext_host_port}'
+ )
+ )
+ print(f' last heard {last_heard}')
+ print(f' time since last heard {time_since_last_heard}')
+ print(f' total packets {total_pkts}, total bytes {total_bytes}')
+ if is_timed_out:
+ print(' session timed out')
+ print('\n')
+
+
+def _get_formatted_output_addresses(addresses):
+ twice_nat_address = []
+ translation_address = []
+ for address_info in addresses:
+ address = address_info.get('ip_address')
+ if address_info.get('flags') & flags_map['twice-nat']:
+ twice_nat_address.append(address)
+ else:
+ translation_address.append(address)
+
+ print('NAT44 pool addresses:')
+ for addr in translation_address:
+ print(f' {addr}')
+ print('NAT44 twice-nat pool addresses:')
+ for addr in twice_nat_address:
+ print(f' {addr}')
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ print('NAT44 interfaces:')
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface['sw_if_index'])
+ iface_type = decode_bitmask(interface['flags'])
+ print(f' {name} {" ".join(iface_type)}')
+
+
+def _get_formatted_output_rules(rules_list):
+ data_entries = []
+ for rule in rules_list:
+ external_address = rule.get('external_ip_address')
+ external_port = rule.get('external_port') or ''
+ local_address = rule.get('local_ip_address')
+ local_port = rule.get('local_port') or ''
+ protocol = protocol_map[rule.get('protocol', 0)]
+ options = ' '.join(decode_bitmask(rule.get('flags')))
+
+ values = [
+ external_address,
+ external_port,
+ local_address,
+ local_port,
+ protocol,
+ options,
+ ]
+ data_entries.append(values)
+ headers = [
+ 'External address',
+ 'External port',
+ 'Local address',
+ 'Local port',
+ 'Protocol',
+ 'Options',
+ ]
+ out = sorted(data_entries, key=lambda x: x[2])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def show_sessions(raw: bool):
+ vpp = VPPControl()
+ sessions_list: list[dict] = _get_raw_output_sessions(vpp.api)
+
+ if raw:
+ return sessions_list
+
+ else:
+ return _get_formatted_output_sessions(sessions_list)
+
+
+@_verify
+def show_summary(raw: bool):
+ vpp = VPPControl()
+ return vpp.cli_cmd('show nat44 summary').reply
+
+
+@_verify
+def show_static(raw: bool):
+ vpp = VPPControl()
+ nat_static_dump = vpp.api.nat44_static_mapping_dump()
+ rules_list: list[dict] = _get_raw_output(nat_static_dump)
+
+ if raw:
+ return rules_list
+
+ else:
+ return _get_formatted_output_rules(rules_list)
+
+
+@_verify
+def show_addresses(raw: bool):
+ vpp = VPPControl()
+ addresses_dump = vpp.api.nat44_address_dump()
+ addresses: list[dict] = _get_raw_output(addresses_dump)
+
+ if raw:
+ return addresses
+
+ else:
+ return _get_formatted_output_addresses(addresses)
+
+
+@_verify
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.nat44_interface_dump()
+ interfaces: list[dict] = _get_raw_output(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/vpp_acl.py b/src/op_mode/vpp_acl.py
new file mode 100644
index 000000000..ed9b35ef6
--- /dev/null
+++ b/src/op_mode/vpp_acl.py
@@ -0,0 +1,342 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+import typing
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.config import Config
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+NO_ACL_INDEX = 0xFFFFFFFF
+
+# ACL action flags
+action_map = {
+ 0: 'deny',
+ 1: 'permit',
+ 2: 'permit-reflect',
+}
+
+# TCP flag names to bit values
+TCP_FLAGS = {
+ 'FIN': 0x01,
+ 'SYN': 0x02,
+ 'RST': 0x04,
+ 'PSH': 0x08,
+ 'ACK': 0x10,
+ 'URG': 0x20,
+ 'ECN': 0x40,
+ 'CWR': 0x80,
+}
+
+
+def _verify(target):
+ """Decorator checks if config for VPP NAT CGNAT exists"""
+ from functools import wraps
+
+ if target not in ['ip', 'macip', 'no_target']:
+ raise ValueError('Invalid target')
+
+ def _verify_target(func):
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ path = 'vpp acl'
+ if target == 'ip':
+ path += ' ip'
+ elif target == 'macip':
+ path += ' macip'
+ if not config.exists(path):
+ raise vyos.opmode.UnconfiguredSubsystem(f'"{path}" is not configured')
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+ return _verify_target
+
+
+def _get_acl_tag_by_index(vpp, acl_index):
+ acl = vpp.api.acl_dump(acl_index=acl_index)
+ if acl:
+ return acl[0].tag
+
+ return None
+
+
+def _get_macip_acl_tag_by_index(vpp, acl_index):
+ acl = vpp.api.macip_acl_dump(acl_index=acl_index)
+ if acl:
+ return acl[0].tag
+
+ return None
+
+
+def _get_tcp_flag_states(value, mask):
+ set_flags = []
+ unset_flags = []
+ for flag, bit in TCP_FLAGS.items():
+ if mask & bit: # This flag is being checked
+ if value & bit:
+ set_flags.append(flag)
+ else:
+ unset_flags.append(flag)
+ return sorted(set_flags), sorted(unset_flags)
+
+
+def _get_raw_output_acls(data_dump):
+ out = []
+ for data in data_dump:
+ rules = [json.loads(json.dumps(d._asdict(), default=str)) for d in data.r]
+ out.append(
+ {
+ 'acl_index': data.acl_index,
+ 'tag': data.tag,
+ 'count': data.count,
+ 'r': rules,
+ }
+ )
+ return out
+
+
+def _get_raw_output_interfaces(data_dump):
+ ifaces_list = []
+ for iface in data_dump:
+ if iface.count != 0:
+ ifaces_list.append(json.loads(json.dumps(iface._asdict(), default=str)))
+ return ifaces_list
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ data_entries = []
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface.get('sw_if_index'))
+ input_acls = []
+ for acl_index in interface.get('acls')[: interface.get('n_input')]:
+ input_acls.append(_get_acl_tag_by_index(vpp, int(acl_index)))
+ output_acls = []
+ for acl_index in interface.get('acls')[interface.get('n_input') :]:
+ output_acls.append(_get_acl_tag_by_index(vpp, int(acl_index)))
+ values = [
+ name,
+ '\n'.join(input_acls),
+ '\n'.join(output_acls),
+ ]
+ data_entries.append(values)
+
+ headers = ['Interface', 'Input ACLs', 'Output ACLs']
+ return tabulate(data_entries, headers=headers, tablefmt='simple')
+
+
+def _get_formatted_output_macip_interfaces(vpp, interfaces):
+ data_entries = []
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface.get('sw_if_index'))
+ acl = _get_macip_acl_tag_by_index(vpp, int(interface.get('acls')[0]))
+ data_entries.append([name, acl])
+
+ headers = ['Interface', 'ACL']
+ return tabulate(data_entries, headers=headers, tablefmt='simple')
+
+
+def _get_formatted_output_acls(acls_list):
+ conf = Config()
+
+ for acl in acls_list:
+ acl_index = acl.get('acl_index')
+ tag = acl.get('tag')
+ rules = acl.get('r')
+ print(
+ '\n---------------------------------\n'
+ f'IP ACL "tag-name {tag}" acl_index {acl_index}\n'
+ )
+
+ path = ['vpp', 'acl', 'ip', 'tag-name', tag, 'rule']
+ conf_rules = conf.list_nodes(path)
+ data_entries = []
+ for rule_index, rule in enumerate(rules):
+ srcport_first = str(rule.get('srcport_or_icmptype_first'))
+ srcport_last = str(rule.get('srcport_or_icmptype_last'))
+ dstport_first = str(rule.get('dstport_or_icmpcode_first'))
+ dstport_last = str(rule.get('dstport_or_icmpcode_last'))
+ set_flags, unset_flags = _get_tcp_flag_states(
+ rule.get('tcp_flags_value'), rule.get('tcp_flags_mask')
+ )
+
+ values = [
+ conf_rules[rule_index],
+ action_map.get(rule.get('is_permit')),
+ rule.get('src_prefix'),
+ (
+ f'{srcport_first}-{srcport_last}'
+ if srcport_first != srcport_last
+ else srcport_first
+ ),
+ rule.get('dst_prefix'),
+ (
+ f'{dstport_first}-{dstport_last}'
+ if dstport_first != dstport_last
+ else dstport_first
+ ),
+ rule.get('proto'),
+ '\n'.join(set_flags),
+ '\n'.join(unset_flags),
+ ]
+ data_entries.append(values)
+
+ headers = [
+ 'Rule',
+ 'Action',
+ 'Src prefix',
+ 'Src port',
+ 'Dst prefix',
+ 'Dst port',
+ 'Proto',
+ 'TCP flags set',
+ 'TCP flags not set',
+ ]
+ print(tabulate(data_entries, headers=headers, tablefmt='simple'))
+ print('\n')
+
+
+def _get_formatted_output_macip_acls(acls_list):
+ conf = Config()
+
+ for acl in acls_list:
+ acl_index = acl.get('acl_index')
+ tag = acl.get('tag')
+ rules = acl.get('r')
+ print(
+ '\n---------------------------------\n'
+ f'MACIP ACL "tag-name {tag}" acl_index {acl_index}\n'
+ )
+
+ path = ['vpp', 'acl', 'macip', 'tag-name', tag, 'rule']
+ conf_rules = conf.list_nodes(path)
+ data_entries = []
+ for rule_index, rule in enumerate(rules):
+ values = [
+ conf_rules[rule_index],
+ action_map.get(rule.get('is_permit')),
+ rule.get('src_prefix'),
+ rule.get('src_mac'),
+ rule.get('src_mac_mask'),
+ ]
+ data_entries.append(values)
+
+ headers = [
+ 'Rule',
+ 'Action',
+ 'IP prefix',
+ 'MAC address',
+ 'MAC mask',
+ ]
+ print(tabulate(data_entries, headers=headers, tablefmt='simple'))
+ print('\n')
+
+
+def _find_acl_by_tag(acls, tag_name):
+ return [acl for acl in acls if acl['tag'] == tag_name]
+
+
+@_verify('ip')
+def show_ip_acls(raw: bool, tag_name: typing.Optional[str]):
+ vpp = VPPControl()
+ acls_dump = vpp.api.acl_dump(acl_index=NO_ACL_INDEX)
+ acls: list[dict] = _get_raw_output_acls(acls_dump)
+
+ if tag_name:
+ acls = _find_acl_by_tag(acls, tag_name)
+
+ if raw:
+ return acls
+
+ else:
+ return _get_formatted_output_acls(acls)
+
+
+@_verify('macip')
+def show_macip_acls(raw: bool, tag_name: typing.Optional[str]):
+ vpp = VPPControl()
+ acls_dump = vpp.api.macip_acl_dump(acl_index=NO_ACL_INDEX)
+ acls: list[dict] = _get_raw_output_acls(acls_dump)
+
+ if tag_name:
+ acls = _find_acl_by_tag(acls, tag_name)
+
+ if raw:
+ return acls
+
+ else:
+ return _get_formatted_output_macip_acls(acls)
+
+
+@_verify('ip')
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.acl_interface_list_dump()
+ interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+@_verify('macip')
+def show_macip_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.macip_acl_interface_list_dump()
+ interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_macip_interfaces(vpp, interfaces)
+
+
+@_verify('no_target')
+def show_all_acls(raw: bool):
+ conf = Config()
+ acls_all = {}
+ path = ['vpp', 'acl']
+ if conf.exists(path + ['ip']):
+ ip_acls = show_ip_acls(raw, tag_name=None)
+ acls_all['ip'] = ip_acls
+ if conf.exists(path + ['macip']):
+ macip_acls = show_macip_acls(raw, tag_name=None)
+ acls_all['macip'] = macip_acls
+
+ if raw:
+ return acls_all
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/vpp_nat_cgnat.py b/src/op_mode/vpp_nat_cgnat.py
new file mode 100644
index 000000000..9a7dd2086
--- /dev/null
+++ b/src/op_mode/vpp_nat_cgnat.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+def _verify(func):
+ """Decorator checks if config for VPP NAT CGNAT exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ base = 'vpp nat cgnat'
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured')
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+def _get_raw_output(data_dump):
+ data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump]
+ return data
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ print('CGNAT interfaces:')
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface['sw_if_index'])
+ iface_type = 'in' if interface['is_inside'] else 'out'
+ print(f' {name} {iface_type}')
+
+
+def _get_formatted_output_mappings(rules_list):
+ data_entries = []
+ for rule in rules_list:
+ in_addr = rule.get('in_addr')
+ in_plen = str(rule.get('in_plen'))
+ out_addr = rule.get('out_addr')
+ out_plen = str(rule.get('out_plen'))
+ sharing_ratio = rule.get('sharing_ratio')
+ ports_per_host = rule.get('ports_per_host')
+ ses_num = rule.get('ses_num')
+
+ values = [
+ f'{in_addr}/{in_plen}',
+ f'{out_addr}/{out_plen}',
+ sharing_ratio,
+ ports_per_host,
+ ses_num,
+ ]
+ data_entries.append(values)
+ headers = [
+ 'Inside',
+ 'Outside',
+ 'Sharing ratio',
+ 'Ports per host',
+ 'Sessions',
+ ]
+ out = sorted(data_entries, key=lambda x: x[0])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def show_sessions(raw: bool):
+ vpp = VPPControl()
+ out = vpp.cli_cmd('show det44 sessions').reply
+ out = out.replace('NAT44 deterministic', 'CGNAT')
+ return out
+
+
+@_verify
+def show_mappings(raw: bool):
+ vpp = VPPControl()
+ nat_static_dump = vpp.api.det44_map_dump()
+ rules_list: list[dict] = _get_raw_output(nat_static_dump)
+
+ if raw:
+ return rules_list
+
+ else:
+ return _get_formatted_output_mappings(rules_list)
+
+
+@_verify
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.det44_interface_dump()
+ interfaces: list[dict] = _get_raw_output(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+@_verify
+def clear_session(address: str, port: str, ext_address: str, ext_port: str):
+ vpp = VPPControl()
+ vpp.api.det44_close_session_in(
+ in_addr=address,
+ in_port=int(port),
+ ext_addr=ext_address,
+ ext_port=int(ext_port),
+ )
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/systemd/vpp-failure-handler.service b/src/systemd/vpp-failure-handler.service
new file mode 100644
index 000000000..3a75f9519
--- /dev/null
+++ b/src/systemd/vpp-failure-handler.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Restart VPP on failure
+
+[Service]
+Type=oneshot
+User=root
+Group=vyattacfg
+UMask=0002
+ExecStart=/usr/bin/python3 /usr/libexec/vyos/reset_section.py vpp --reload