summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViacheslav <v.gletenko@vyos.io>2025-08-07 08:43:24 +0000
committerViacheslav <v.gletenko@vyos.io>2025-08-07 08:43:24 +0000
commit9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720 (patch)
treed93a2fce931a8203219cfacf39b4137c82042c80
parent8b9eabd3b636ab5bc1542843f265718c2e245baf (diff)
parent1550cb9b84595a672d99694b838aa633800452ec (diff)
downloadvyos-1x-9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720.tar.gz
vyos-1x-9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720.zip
T7697: Merge remote-tracking branch 'vpp/current' into vyos-1x
-rw-r--r--data/config-mode-dependencies/vyos-vpp.json69
-rw-r--r--data/templates/vpp/override.conf.j214
-rw-r--r--data/templates/vpp/startup.conf.j2209
-rw-r--r--interface-definitions/include/kernel-interface-tap.xml.i15
-rw-r--r--interface-definitions/include/kernel-interface-tun.xml.i15
-rw-r--r--interface-definitions/include/unformat_log2_page_size.xml.i17
-rw-r--r--interface-definitions/include/unformat_memory_size.xml.i22
-rw-r--r--interface-definitions/include/version/vpp-version.xml.i3
-rw-r--r--interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i25
-rw-r--r--interface-definitions/include/vpp/acl_port_range.xml.i18
-rw-r--r--interface-definitions/include/vpp/acl_prefix.xml.i20
-rw-r--r--interface-definitions/include/vpp/acl_tcp_flags.xml.i50
-rw-r--r--interface-definitions/include/vpp/iface_rx_mode.xml.i25
-rw-r--r--interface-definitions/include/vpp/nat_address_range.xml.i20
-rw-r--r--interface-definitions/include/vpp/nat_interface.xml.i11
-rw-r--r--interface-definitions/include/vpp/nat_protocol.xml.i30
-rw-r--r--interface-definitions/include/vpp/netlink.xml.i45
-rw-r--r--interface-definitions/include/vpp/vif.xml.i21
-rw-r--r--interface-definitions/include/vpp_host_resources.xml.i35
-rw-r--r--interface-definitions/vpp.xml.in1578
-rw-r--r--op-mode-definitions/show_vpp_interfaces.xml.in47
-rw-r--r--op-mode-definitions/show_vpp_nat44.xml.in48
-rw-r--r--op-mode-definitions/vpp_acl.xml.in65
-rw-r--r--op-mode-definitions/vpp_nat_cgnat.xml.in100
-rw-r--r--python/vyos/vpp/__init__.py20
-rw-r--r--python/vyos/vpp/acl/__init__.py3
-rw-r--r--python/vyos/vpp/acl/acl.py106
-rw-r--r--python/vyos/vpp/config_deps.py76
-rw-r--r--python/vyos/vpp/config_filter.py58
-rw-r--r--python/vyos/vpp/config_resource_checks/__init__.py0
-rw-r--r--python/vyos/vpp/config_resource_checks/cpu.py76
-rw-r--r--python/vyos/vpp/config_resource_checks/memory.py153
-rw-r--r--python/vyos/vpp/config_resource_checks/resource_defaults.py43
-rw-r--r--python/vyos/vpp/config_verify.py407
-rw-r--r--python/vyos/vpp/configdb.py206
-rw-r--r--python/vyos/vpp/control_host.py375
-rw-r--r--python/vyos/vpp/control_vpp.py488
-rw-r--r--python/vyos/vpp/interface/__init__.py42
-rw-r--r--python/vyos/vpp/interface/bond.py118
-rw-r--r--python/vyos/vpp/interface/bridge.py128
-rw-r--r--python/vyos/vpp/interface/ethernet.py53
-rw-r--r--python/vyos/vpp/interface/geneve.py90
-rw-r--r--python/vyos/vpp/interface/gre.py140
-rw-r--r--python/vyos/vpp/interface/interface.py49
-rw-r--r--python/vyos/vpp/interface/ipip.py94
-rw-r--r--python/vyos/vpp/interface/loopback.py72
-rw-r--r--python/vyos/vpp/interface/vxlan.py106
-rw-r--r--python/vyos/vpp/interface/wireguard.py55
-rw-r--r--python/vyos/vpp/interface/xconnect.py95
-rw-r--r--python/vyos/vpp/nat/__init__.py4
-rw-r--r--python/vyos/vpp/nat/det44.py106
-rw-r--r--python/vyos/vpp/nat/nat44.py229
-rw-r--r--python/vyos/vpp/utils.py412
-rw-r--r--smoketest/config-tests/vpp49
-rw-r--r--smoketest/configs/vpp128
-rwxr-xr-xsmoketest/scripts/cli/test_vpp.py1422
-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.py691
-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
78 files changed, 12881 insertions, 0 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json
new file mode 100644
index 000000000..32b53508e
--- /dev/null
+++ b/data/config-mode-dependencies/vyos-vpp.json
@@ -0,0 +1,69 @@
+{
+ "vpp": {
+ "ethernet": ["interfaces_ethernet"],
+ "vpp_interfaces_bonding": ["vpp_interfaces_bonding"],
+ "vpp_interfaces_bridge": ["vpp_interfaces_bridge"],
+ "vpp_interfaces_ethernet": ["vpp_interfaces_ethernet"],
+ "vpp_interfaces_geneve": ["vpp_interfaces_geneve"],
+ "vpp_interfaces_gre": ["vpp_interfaces_gre"],
+ "vpp_interfaces_ipip": ["vpp_interfaces_ipip"],
+ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"],
+ "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"],
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"],
+ "vpp_sflow": ["vpp_sflow"]
+ },
+ "vpp_interfaces_bonding": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_ethernet": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_geneve": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_gre": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_ipip": {
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_loopback": {
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ },
+ "vpp_interfaces_vxlan": {
+ "vpp_interfaces_bridge": ["vpp_interfaces_bridge"],
+ "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"],
+ "vpp_acl": ["vpp_acl"],
+ "vpp_nat": ["vpp_nat"],
+ "vpp_nat_cgnat": ["vpp_nat_cgnat"],
+ "vpp_kernel_interface": ["vpp_kernel-interfaces"]
+ }
+}
+
diff --git a/data/templates/vpp/override.conf.j2 b/data/templates/vpp/override.conf.j2
new file mode 100644
index 000000000..04bb7be15
--- /dev/null
+++ b/data/templates/vpp/override.conf.j2
@@ -0,0 +1,14 @@
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=
+ConditionPathExists=/run/vpp/vpp.conf
+OnFailure=vpp-failure-handler.service
+
+[Service]
+EnvironmentFile=
+ExecStart=
+ExecStart=/usr/bin/vpp -c /run/vpp/vpp.conf
+WorkingDirectory=
+WorkingDirectory=/run/vpp
+Restart=no
diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2
new file mode 100644
index 000000000..b7515889a
--- /dev/null
+++ b/data/templates/vpp/startup.conf.j2
@@ -0,0 +1,209 @@
+# Generated by ${vyos_conf_scripts_dir}/vpp.py
+
+unix {
+ nodaemon
+ log /var/log/vpp.log
+ full-coredump
+ cli-listen /run/vpp/cli.sock
+ gid vpp
+ # exec /etc/vpp/bootstrap.vpp
+{% if unix is vyos_defined %}
+{% if unix.poll_sleep_usec is vyos_defined %}
+ poll-sleep-usec {{ unix.poll_sleep_usec }}
+{% endif %}
+{% endif %}
+}
+
+{% if cpu is vyos_defined %}
+cpu {
+{% if cpu.main_core is vyos_defined %}
+ main-core {{ cpu.main_core }}
+{% endif %}
+{% if cpu.corelist_workers is vyos_defined %}
+ corelist-workers {{ cpu.corelist_workers | join(',') }}
+{% endif %}
+{% if cpu.skip_cores is vyos_defined %}
+ skip-cores {{ cpu.skip_cores }}
+{% endif %}
+{% if cpu.workers is vyos_defined %}
+ workers {{ cpu.workers }}
+{% endif %}
+}
+{% endif %}
+
+{% if buffers is vyos_defined %}
+buffers {
+{% if buffers.buffers_per_numa is vyos_defined %}
+ buffers-per-numa {{ buffers.buffers_per_numa }}
+{% endif %}
+{% if buffers.data_size is vyos_defined %}
+ default data-size {{ buffers.data_size }}
+{% endif %}
+{% if buffers.page_size is vyos_defined %}
+ page-size {{ buffers.page_size }}
+{% endif %}
+}
+{% endif %}
+
+{% if ipv6 is vyos_defined %}
+ip6 {
+{% if ipv6.hash_buckets is vyos_defined %}
+ hash-buckets {{ ipv6.hash_buckets }}
+{% endif %}
+{% if ipv6.heap_size is vyos_defined %}
+ heap-size {{ ipv6.heap_size }}
+{% endif %}
+}
+{% endif %}
+
+{% if l2learn is vyos_defined %}
+l2learn {
+{% if l2learn.limit is vyos_defined %}
+ limit {{ l2learn.limit }}
+{% endif %}
+}
+{% endif %}
+
+{% if logging is vyos_defined %}
+logging {
+{% if logging.default_log_level is vyos_defined %}
+ default-log-level {{ logging.default_log_level }}
+ default-syslog-log-level {{ logging.default_log_level }}
+{% endif %}
+}
+{% endif %}
+
+{% if physmem is vyos_defined %}
+physmem {
+{% if physmem.max_size is vyos_defined %}
+ max-size {{ physmem.max_size }}
+{% endif %}
+}
+{% endif %}
+
+plugins {
+ path /usr/lib/x86_64-linux-gnu/vpp_plugins/
+ plugin default { disable }
+ plugin af_xdp_plugin.so { enable }
+ plugin avf_plugin.so { enable }
+ plugin dpdk_plugin.so { enable }
+ plugin vmxnet3_plugin.so { enable }
+ plugin lacp_plugin.so { enable }
+ plugin linux_cp_plugin.so { enable }
+ plugin linux_nl_plugin.so { enable }
+ plugin pppoe_plugin.so { enable }
+ plugin sflow_plugin.so { enable }
+ # NAT uncomment if needed
+ # plugin cnat_plugin.so { enable }
+ plugin nat_plugin.so { enable }
+ plugin nat44_ei_plugin.so { enable }
+ plugin det44_plugin.so { enable }
+ # plugin nat44_ei_plugin.so { enable }
+ # plugin nat64_plugin.so { enable }
+ # plugin nat66_plugin.so { enable }
+ # plugin pnat_plugin.so { enable }
+ # Tunnels
+ plugin geneve_plugin.so { enable }
+ plugin gre_plugin.so { enable }
+ plugin vxlan_plugin.so { enable }
+ # Secure
+ # plugin ikev2_plugin.so { enable }
+ # plugin dns_plugin.so { enable } # Probably required for FQDN peers
+{% if ipsec is vyos_defined %}
+ plugin crypto_ipsecmb_plugin.so { enable }
+ plugin crypto_native_plugin.so { enable }
+ plugin crypto_openssl_plugin.so { enable }
+{% endif %}
+ # plugin wireguard_plugin.so { enable }
+ # ACL
+ plugin acl_plugin.so { enable }
+}
+
+linux-cp {
+ lcp-sync
+ lcp-auto-subint
+{% if lcp.ignore_kernel_routes is not vyos_defined %}
+ route-no-paths
+{% endif %}
+}
+
+{% if lcp.netlink is vyos_defined %}
+linux-nl {
+{% if lcp.netlink.rx_buffer_size is vyos_defined %}
+ nl-rx-buffer-size {{ lcp.netlink.rx_buffer_size }}
+{% endif %}
+{% if lcp.netlink.batch_size is vyos_defined %}
+ nl-batch-size {{ lcp.netlink.batch_size }}
+{% endif %}
+{% if lcp.netlink.batch_delay_ms is vyos_defined %}
+ nl-batch-delay-ms {{ lcp.netlink.batch_delay_ms }}
+{% endif %}
+}
+{% endif %}
+
+{% if statseg is vyos_defined %}
+statseg {
+{% if statseg.size is vyos_defined %}
+ size {{ statseg.size }}
+{% endif %}
+{% if statseg.page_size is vyos_defined %}
+ page-size {{ statseg.page_size }}
+{% endif %}
+}
+{% endif %}
+
+{% if memory is vyos_defined %}
+memory {
+{% if memory.main_heap_size is vyos_defined %}
+ main-heap-size {{ memory.main_heap_size }}
+{% endif %}
+{% if memory.main_heap_page_size is vyos_defined %}
+ main-heap-page-size {{ memory.main_heap_page_size }}
+{% endif %}
+{% if memory.default_hugepage_size is vyos_defined %}
+ default-hugepage-size {{ memory.default_hugepage_size }}
+{% endif %}
+}
+{% endif %}
+
+dpdk {
+ # Whitelist the fake PCI address 0000:00:00.0
+ # This prevents all devices from being added to VPP-DPDK by default
+ dev 0000:00:00.0
+{% for iface, iface_config in interface.items() %}
+{% if iface_config.driver == 'dpdk' %}
+ dev {{ iface_config.dpdk_options.dev_id }} {
+ name {{ iface }}
+{% if iface_config.dpdk_options.num_rx_desc is vyos_defined %}
+ num-rx-desc {{ iface_config.dpdk_options.num_rx_desc }}
+{% endif %}
+{% if iface_config.dpdk_options.num_tx_desc is vyos_defined %}
+ num-tx-desc {{ iface_config.dpdk_options.num_tx_desc }}
+{% endif %}
+{% if iface_config.dpdk_options.num_rx_queues is vyos_defined %}
+ num-rx-queues {{ iface_config.dpdk_options.num_rx_queues }}
+{% endif %}
+{% if iface_config.dpdk_options.num_tx_queues is vyos_defined %}
+ num-tx-queues {{ iface_config.dpdk_options.num_tx_queues }}
+{% endif %}
+ }
+{% endif %}
+{% endfor %}
+ uio-bind-force
+}
+
+{% if ipsec is vyos_defined %}
+linux-xfrm-nl {
+ enable-route-mode-ipsec
+ interface {{ ipsec.interface_type }}
+{% if ipsec.netlink.batch_delay_ms is vyos_defined %}
+ nl-batch-delay-ms {{ ipsec.netlink.batch_delay_ms }}
+{% endif %}
+{% if ipsec.netlink.batch_size is vyos_defined %}
+ nl-batch-size {{ ipsec.netlink.batch_size }}
+{% endif %}
+{% if ipsec.netlink.rx_buffer_size is vyos_defined %}
+ nl-rx-buffer-size {{ ipsec.netlink.rx_buffer_size }}
+{% endif %}
+}
+{% endif %}
diff --git a/interface-definitions/include/kernel-interface-tap.xml.i b/interface-definitions/include/kernel-interface-tap.xml.i
new file mode 100644
index 000000000..83399f1f4
--- /dev/null
+++ b/interface-definitions/include/kernel-interface-tap.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from kernel-interface-tap.xml.i -->
+<leafNode name="kernel-interface">
+ <properties>
+ <help>Kernel interface name</help>
+ <valueHelp>
+ <format>vpptapN</format>
+ <description>Kernel interface name</description>
+ </valueHelp>
+ <constraint>
+ <regex>vpptap\d+</regex>
+ </constraint>
+ <constraintErrorMessage>Kernel interface must start with vpptapN</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/kernel-interface-tun.xml.i b/interface-definitions/include/kernel-interface-tun.xml.i
new file mode 100644
index 000000000..3cbd5ab30
--- /dev/null
+++ b/interface-definitions/include/kernel-interface-tun.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from kernel-interface-tun.xml.i -->
+<leafNode name="kernel-interface">
+ <properties>
+ <help>Kernel interface name</help>
+ <valueHelp>
+ <format>vpptunN</format>
+ <description>Kernel interface name</description>
+ </valueHelp>
+ <constraint>
+ <regex>vpptun\d+</regex>
+ </constraint>
+ <constraintErrorMessage>Kernel interface must start with vpptunN</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/unformat_log2_page_size.xml.i b/interface-definitions/include/unformat_log2_page_size.xml.i
new file mode 100644
index 000000000..ce6f70dc8
--- /dev/null
+++ b/interface-definitions/include/unformat_log2_page_size.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from unformat_log2_page_size.xml.i -->
+<completionHelp>
+ <list>default default-hugepage</list>
+ <script>sudo ${vyos_completion_dir}/list_mem_page_size.py</script>
+</completionHelp>
+<valueHelp>
+ <format>default</format>
+ <description>Default</description>
+</valueHelp>
+<valueHelp>
+ <format>default-hugepage</format>
+ <description>Default huge-page</description>
+</valueHelp>
+<constraint>
+ <regex>(default|default-hugepage|4K|8K|1024K|64K|256K|2048K|4096K|16384K|262144K|1048576K|16777216K|1M|2M|4M|16M|256M|1024M|16384M|1G|16G)</regex>
+</constraint>
+<!-- include end -->
diff --git a/interface-definitions/include/unformat_memory_size.xml.i b/interface-definitions/include/unformat_memory_size.xml.i
new file mode 100644
index 000000000..bb8f4ae67
--- /dev/null
+++ b/interface-definitions/include/unformat_memory_size.xml.i
@@ -0,0 +1,22 @@
+<!-- include start from unformat_memory_size.xml.i -->
+<valueHelp>
+ <format>&lt;number&gt;</format>
+ <description>byte</description>
+</valueHelp>
+<valueHelp>
+ <format>&lt;number&gt;K</format>
+ <description>Kilobyte</description>
+</valueHelp>
+<valueHelp>
+ <format>&lt;number&gt;M</format>
+ <description>Megabyte</description>
+</valueHelp>
+<valueHelp>
+ <format>&lt;number&gt;G</format>
+ <description>Gigabyte</description>
+</valueHelp>
+<constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ <regex>(\d+|\d+K|\d+M|\d+G)</regex>
+</constraint>
+<!-- include end -->
diff --git a/interface-definitions/include/version/vpp-version.xml.i b/interface-definitions/include/version/vpp-version.xml.i
new file mode 100644
index 000000000..96a69c92f
--- /dev/null
+++ b/interface-definitions/include/version/vpp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/vpp-version.xml.i -->
+<syntaxVersion component='vpp' version='1'></syntaxVersion>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i
new file mode 100644
index 000000000..a719a6223
--- /dev/null
+++ b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i
@@ -0,0 +1,25 @@
+<!-- include start from vpp/acl_common_interface_ip_rule.xml.i -->
+<tagNode name="acl-tag">
+ <properties>
+ <help>ACL rule (tag) number</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>Number must be between 1 and 4294967295</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="tag-name">
+ <properties>
+ <help>ACL tag name</help>
+ <completionHelp>
+ <path>vpp acl ip tag-name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/acl_port_range.xml.i b/interface-definitions/include/vpp/acl_port_range.xml.i
new file mode 100644
index 000000000..26bd2de45
--- /dev/null
+++ b/interface-definitions/include/vpp/acl_port_range.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from vpp/acl_port_range.xml.i -->
+<leafNode name="port">
+ <properties>
+ <help>Port number or range</help>
+ <valueHelp>
+ <format>u32:1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>range</format>
+ <description>Numbered port range (e.g., 1001-1005)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="port-range"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/acl_prefix.xml.i b/interface-definitions/include/vpp/acl_prefix.xml.i
new file mode 100644
index 000000000..790153c8f
--- /dev/null
+++ b/interface-definitions/include/vpp/acl_prefix.xml.i
@@ -0,0 +1,20 @@
+<!-- include start from vpp/acl_prefix.xml.i -->
+<leafNode name="prefix">
+ <properties>
+ <help>IP prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
+
diff --git a/interface-definitions/include/vpp/acl_tcp_flags.xml.i b/interface-definitions/include/vpp/acl_tcp_flags.xml.i
new file mode 100644
index 000000000..da17f3fe5
--- /dev/null
+++ b/interface-definitions/include/vpp/acl_tcp_flags.xml.i
@@ -0,0 +1,50 @@
+<!-- include start from vpp/acl_tcp_flags.xml.i -->
+<leafNode name="syn">
+ <properties>
+ <help>Synchronise flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="ack">
+ <properties>
+ <help>Acknowledge flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="fin">
+ <properties>
+ <help>Finish flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="rst">
+ <properties>
+ <help>Reset flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="urg">
+ <properties>
+ <help>Urgent flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="psh">
+ <properties>
+ <help>Push flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="ecn">
+ <properties>
+ <help>Explicit Congestion Notification flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="cwr">
+ <properties>
+ <help>Congestion Window Reduced flag</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/iface_rx_mode.xml.i b/interface-definitions/include/vpp/iface_rx_mode.xml.i
new file mode 100644
index 000000000..374654b08
--- /dev/null
+++ b/interface-definitions/include/vpp/iface_rx_mode.xml.i
@@ -0,0 +1,25 @@
+<!-- include start from vpp/iface_rx_mode.xml.i -->
+<leafNode name="rx-mode">
+ <properties>
+ <help>Receive packet processing mode</help>
+ <completionHelp>
+ <list>polling interrupt adaptive</list>
+ </completionHelp>
+ <valueHelp>
+ <format>polling</format>
+ <description>Constantly check for new data</description>
+ </valueHelp>
+ <valueHelp>
+ <format>interrupt</format>
+ <description>Interrupt mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>adaptive</format>
+ <description>Adaptive mode</description>
+ </valueHelp>
+ <constraint>
+ <regex>(polling|interrupt|adaptive)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/nat_address_range.xml.i b/interface-definitions/include/vpp/nat_address_range.xml.i
new file mode 100644
index 000000000..48648f91a
--- /dev/null
+++ b/interface-definitions/include/vpp/nat_address_range.xml.i
@@ -0,0 +1,20 @@
+<!-- include start from vpp/nat_address_range.xml.i -->
+<leafNode name="address">
+ <properties>
+ <help>IP address or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-range"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/nat_interface.xml.i b/interface-definitions/include/vpp/nat_interface.xml.i
new file mode 100644
index 000000000..20a7356bf
--- /dev/null
+++ b/interface-definitions/include/vpp/nat_interface.xml.i
@@ -0,0 +1,11 @@
+<!-- include start from vpp/nat_interface.xml.i -->
+<leafNode name="interface">
+ <properties>
+ <help>Add IP address from an interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/nat_protocol.xml.i b/interface-definitions/include/vpp/nat_protocol.xml.i
new file mode 100644
index 000000000..b88fd2b62
--- /dev/null
+++ b/interface-definitions/include/vpp/nat_protocol.xml.i
@@ -0,0 +1,30 @@
+<!-- include start from vpp/nat_protocol.xml.i -->
+<leafNode name="protocol">
+ <properties>
+ <help>Protocol</help>
+ <completionHelp>
+ <list>tcp udp icmp all</list>
+ </completionHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All protocols (TCP, UDP, and ICMP)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>icmp</format>
+ <description>Internet Control Message Protocol (ICMP)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>Transmission Control Protocol (TCP)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>User Datagram Protocol (UDP)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(tcp|udp|icmp|all)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>all</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/netlink.xml.i b/interface-definitions/include/vpp/netlink.xml.i
new file mode 100644
index 000000000..903100d00
--- /dev/null
+++ b/interface-definitions/include/vpp/netlink.xml.i
@@ -0,0 +1,45 @@
+<!-- include start from vpp/netlink.xml.i -->
+<node name="netlink">
+ <properties>
+ <help>Netlink options</help>
+ </properties>
+ <children>
+ <leafNode name="rx-buffer-size">
+ <properties>
+ <help>Receive buffer size</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Receive buffer size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="batch-size">
+ <properties>
+ <help>Batch size</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Batch size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="batch-delay-ms">
+ <properties>
+ <help>Batch delay</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Batch delay (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+</children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp/vif.xml.i b/interface-definitions/include/vpp/vif.xml.i
new file mode 100644
index 000000000..8f0e9772f
--- /dev/null
+++ b/interface-definitions/include/vpp/vif.xml.i
@@ -0,0 +1,21 @@
+<!-- include start from vpp/vif.xml.i -->
+<tagNode name="vif">
+ <properties>
+ <help>Virtual Local Area Network (VLAN) ID</help>
+ <valueHelp>
+ <format>u32:0-4094</format>
+ <description>Virtual Local Area Network (VLAN) ID</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface/disable.xml.i>
+ #include <include/interface/mtu-68-16000.xml.i>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i
new file mode 100644
index 000000000..1706c8c87
--- /dev/null
+++ b/interface-definitions/include/vpp_host_resources.xml.i
@@ -0,0 +1,35 @@
+<!-- include start from vpp_host_resources.xml.i -->
+<node name="host-resources">
+ <properties>
+ <help>Host resources control</help>
+ </properties>
+ <children>
+ <leafNode name="max-map-count">
+ <properties>
+ <help>Maximum number of memory map areas a process may have</help>
+ <valueHelp>
+ <format>u32:65535-2147483647</format>
+ <description>Areas count</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 65535-2147483647"/>
+ </constraint>
+ </properties>
+ <defaultValue>65535</defaultValue>
+ </leafNode>
+ <leafNode name="shmmax">
+ <properties>
+ <help>Maximum shared memory segment size that can be created</help>
+ <valueHelp>
+ <format>u32:0-18446744073709551612</format>
+ <description>Size in bytes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-18446744073709551612"/>
+ </constraint>
+ </properties>
+ <defaultValue>2147483648</defaultValue>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in
new file mode 100644
index 000000000..dff56a228
--- /dev/null
+++ b/interface-definitions/vpp.xml.in
@@ -0,0 +1,1578 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpp" owner="${vyos_conf_scripts_dir}/vpp.py">
+ <properties>
+ <help>Accelerated data-plane</help>
+ <priority>295</priority>
+ </properties>
+ <children>
+ <node name='interfaces'>
+ <properties>
+ <help>Network interfaces</help>
+ <priority>300</priority>
+ </properties>
+ <children>
+ <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/vpp_interfaces_bonding.py">
+ <properties>
+ <priority>324</priority>
+ <help>Bonding Interface/Link Aggregation</help>
+ <constraint>
+ <regex>bond[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage>
+ <valueHelp>
+ <format>bondN</format>
+ <description>Bonding interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ <leafNode name="hash-policy">
+ <properties>
+ <help>Bonding transmit hash policy</help>
+ <completionHelp>
+ <list>layer2 layer2+3 layer3+4</list>
+ </completionHelp>
+ <valueHelp>
+ <format>layer2</format>
+ <description>use MAC addresses to generate the hash</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer2+3</format>
+ <description>combine MAC address and IP address to make hash</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer3+4</format>
+ <description>combine IP address and port to make hash</description>
+ </valueHelp>
+ <constraint>
+ <regex>(layer2\+3|layer3\+4|layer2)</regex>
+ </constraint>
+ <constraintErrorMessage>hash-policy must be layer2 layer2+3 layer3+4</constraintErrorMessage>
+ </properties>
+ <defaultValue>layer2</defaultValue>
+ </leafNode>
+ #include <include/interface/mac.xml.i>
+ #include <include/kernel-interface-tun.xml.i>
+ <leafNode name="mode">
+ <properties>
+ <help>Bonding mode</help>
+ <completionHelp>
+ <list>802.3ad active-backup broadcast round-robin xor-hash</list>
+ </completionHelp>
+ <valueHelp>
+ <format>802.3ad</format>
+ <description>IEEE 802.3ad Dynamic link aggregation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>active-backup</format>
+ <description>Fault tolerant: only one slave in the bond is active</description>
+ </valueHelp>
+ <valueHelp>
+ <format>broadcast</format>
+ <description>Fault tolerant: transmits everything on all slave interfaces</description>
+ </valueHelp>
+ <valueHelp>
+ <format>round-robin</format>
+ <description>Load balance: transmit packets in sequential order</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xor-hash</format>
+ <description>Distribute based on MAC address</description>
+ </valueHelp>
+ <constraint>
+ <regex>(802.3ad|active-backup|broadcast|round-robin|xor-hash)</regex>
+ </constraint>
+ <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, or xor</constraintErrorMessage>
+ </properties>
+ <defaultValue>802.3ad</defaultValue>
+ </leafNode>
+ <node name="member">
+ <properties>
+ <help>Bonding member interfaces</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces --bondable</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/vpp_interfaces_bridge.py">
+ <properties>
+ <priority>327</priority>
+ <help>Bridge domain</help>
+ <constraint>
+ <!-- Bridge domain 0 is reserved to vpp default -->
+ <regex>br(?!0$)[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Bridge domain must be named brN and br0 is reserved</constraintErrorMessage>
+ <valueHelp>
+ <format>brN</format>
+ <description>Bridge interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ <node name="member">
+ <properties>
+ <help>Bridge member interfaces</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces --bridgeable</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="bvi">
+ <properties>
+ <help>Bridge Virtual Interface (BVI)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <!-- Disable as geneve interface index in the VPP always 0 and not configured -->
+ <!-- <tagNode name="geneve" owner="${vyos_conf_scripts_dir}/vpp_interfaces_geneve.py">
+ <properties>
+ <priority>325</priority>
+ <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help>
+ <constraint>
+ <regex>geneve[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>GENEVE interface must be named geneveN</constraintErrorMessage>
+ <valueHelp>
+ <format>geneveN</format>
+ <description>GENEVE interface name</description>
+ </valueHelp>
+ </properties>
+ <children> -->
+ <!-- #include <include/source-address-ipv4-ipv6.xml.i> -->
+ <!-- #include <include/interface/tunnel-remote.xml.i> -->
+ <!-- #include <include/vni.xml.i> -->
+ <!-- #include <include/kernel-interface-tun.xml.i> -->
+ <!-- </children> -->
+ <!-- </tagNode> -->
+ <tagNode name="gre" owner="${vyos_conf_scripts_dir}/vpp_interfaces_gre.py">
+ <properties>
+ <priority>325</priority>
+ <help>Generic Network Encapsulation (GRE) Interface</help>
+ <constraint>
+ <regex>gre[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>GRE interface must be named greN</constraintErrorMessage>
+ <valueHelp>
+ <format>greN</format>
+ <description>GRE interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ <leafNode name="mode">
+ <properties>
+ <help>GRE tunnel mode</help>
+ <completionHelp>
+ <list>point-to-point point-to-multipoint</list>
+ </completionHelp>
+ <valueHelp>
+ <format>point-to-point</format>
+ <description>Point to point mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>point-to-multipoint</format>
+ <description>Point to multipoint mode</description>
+ </valueHelp>
+ <constraint>
+ <regex>(point-to-point|point-to-multipoint)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid mode, must be one of: point-to-point or point-to-multipoint</constraintErrorMessage>
+ </properties>
+ <defaultValue>point-to-point</defaultValue>
+ </leafNode>
+ <leafNode name="tunnel-type">
+ <properties>
+ <help>GRE tunnel type</help>
+ <completionHelp>
+ <list>erspan l3 teb</list>
+ </completionHelp>
+ <valueHelp>
+ <format>erspan</format>
+ <description>Encapsulated Remote Switched Port Analyzer</description>
+ </valueHelp>
+ <valueHelp>
+ <format>l3</format>
+ <description>Generic Routing Encapsulation (network layer)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>teb</format>
+ <description>L2 Transparent Ethernet Bridge</description>
+ </valueHelp>
+ <constraint>
+ <regex>(erspan|l3|teb)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid encapsulation, must be one of: l3, teb or erspan</constraintErrorMessage>
+ </properties>
+ <defaultValue>l3</defaultValue>
+ </leafNode>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ #include <include/interface/tunnel-remote.xml.i>
+ #include <include/kernel-interface-tun.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="ipip" owner="${vyos_conf_scripts_dir}/vpp_interfaces_ipip.py">
+ <properties>
+ <priority>325</priority>
+ <help>IP encapsulation tunnel interface</help>
+ <constraint>
+ <regex>ipip[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>IPIP tunnel interface must be named ipipN</constraintErrorMessage>
+ <valueHelp>
+ <format>ipipN</format>
+ <description>IPIP tunnel interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ #include <include/interface/tunnel-remote.xml.i>
+ #include <include/kernel-interface-tun.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/vpp_interfaces_loopback.py">
+ <properties>
+ <priority>325</priority>
+ <help>Loopback Interface</help>
+ <constraint>
+ <regex>lo[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Loopback interface must be named loN</constraintErrorMessage>
+ <valueHelp>
+ <format>loN</format>
+ <description>Loopback interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ #include <include/kernel-interface-tun.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/vpp_interfaces_vxlan.py">
+ <properties>
+ <priority>325</priority>
+ <help>Virtual Extensible LAN (VXLAN) Interface</help>
+ <constraint>
+ <regex>vxlan[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage>
+ <valueHelp>
+ <format>vxlanN</format>
+ <description>VXLAN interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ #include <include/interface/tunnel-remote.xml.i>
+ #include <include/vni.xml.i>
+ #include <include/kernel-interface-tap.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="xconnect" owner="${vyos_conf_scripts_dir}/vpp_interfaces_xconnect.py">
+ <properties>
+ <help>Layer 2 cross connect</help>
+ <priority>325</priority>
+ <constraint>
+ <regex>xcon[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Cross connect must be named xconN</constraintErrorMessage>
+ <valueHelp>
+ <format>xconN</format>
+ <description>Cross connect interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ <node name="member">
+ <properties>
+ <help>Cross connect pair mamber interfaces</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces --bridgeable</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="settings">
+ <properties>
+ <help>VPP settings</help>
+ </properties>
+ <children>
+ <node name="buffers">
+ <properties>
+ <help>Buffer settings</help>
+ </properties>
+ <children>
+ <leafNode name="buffers-per-numa">
+ <properties>
+ <help>Number of buffers per numa node</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Number of buffers</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="data-size">
+ <properties>
+ <help>Size of buffer data area</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Size of buffer data area</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="page-size">
+ <properties>
+ <help>Set the page-size for buffer allocation</help>
+ #include <include/unformat_log2_page_size.xml.i>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="cpu">
+ <properties>
+ <help>CPU settings</help>
+ </properties>
+ <children>
+ <leafNode name="corelist-workers">
+ <properties>
+ <help>List of cores worker threads</help>
+ <valueHelp>
+ <format>&lt;id&gt;</format>
+ <description>CPU core id</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;idN&gt;-&lt;idM&gt;</format>
+ <description>CPU core id range (use '-' as delimiter)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--allow-range --range 0-512"/>
+ </constraint>
+ <constraintErrorMessage>not a valid CPU core value or range</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="main-core">
+ <properties>
+ <help>Main core</help>
+ <valueHelp>
+ <format>u32:0-512</format>
+ <description>Assign main thread to specific core</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-512"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="skip-cores">
+ <properties>
+ <help>Skip cores</help>
+ <valueHelp>
+ <format>u32:1-512</format>
+ <description>Skip cores</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-512"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="workers">
+ <properties>
+ <help>Create worker threads</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Worker threads</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-512"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/vpp_host_resources.xml.i>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface</help>
+ <valueHelp>
+ <format>ethN</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces --type ethernet</script>
+ </completionHelp>
+ <constraint>
+ <validator name="ethernet-interface"/>
+ </constraint>
+ <constraintErrorMessage>Invalid interface name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="driver">
+ <properties>
+ <help>Driver mode</help>
+ <completionHelp>
+ <list>dpdk xdp</list>
+ </completionHelp>
+ <valueHelp>
+ <format>dpdk</format>
+ <description>Data Plane Development Kit (DPDK)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xdp</format>
+ <description>eXpress Data Path (XDP)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(dpdk|xdp)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/vpp/iface_rx_mode.xml.i>
+ <node name="dpdk-options">
+ <properties>
+ <help>DPDK settings</help>
+ </properties>
+ <children>
+ <leafNode name="num-rx-desc">
+ <properties>
+ <help>Receive ring descriptors</help>
+ <valueHelp>
+ <format>u32:256-8192</format>
+ <description>Number of descriptors in receive ring</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 256-8192"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="num-tx-desc">
+ <properties>
+ <help>Tranceive ring descriptors</help>
+ <valueHelp>
+ <format>u32:256-8192</format>
+ <description>Number of descriptors in tranceive ring</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 256-8192"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="num-rx-queues">
+ <properties>
+ <help>Number of receive queues</help>
+ <valueHelp>
+ <format>u32:1-8192</format>
+ <description>Number of receive queues</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-8192"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="num-tx-queues">
+ <properties>
+ <help>Number of tranceive queues</help>
+ <valueHelp>
+ <format>u32:1-8192</format>
+ <description>Number of tranceive queues</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-8192"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="promisc">
+ <properties>
+ <help>Enable promisc mode on a vpp interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="xdp-options">
+ <properties>
+ <help>XDP settings</help>
+ </properties>
+ <children>
+ <leafNode name="no-syscall-lock">
+ <properties>
+ <help>Unlock multithreading in interrupt and adaptive modes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="num-rx-queues">
+ <properties>
+ <help>Number of receive queues</help>
+ <valueHelp>
+ <format>u32:1-65535</format>
+ <description>Number of receive queues to connect to</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All</description>
+ </valueHelp>
+ <constraint>
+ <regex>(all)</regex>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>all</defaultValue>
+ </leafNode>
+ <leafNode name="promisc">
+ <properties>
+ <help>Enable promisc mode on a real interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="rx-queue-size">
+ <properties>
+ <help>Receive queue size</help>
+ <valueHelp>
+ <format>u32:0-65535</format>
+ <description>Size of receive queue</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ <leafNode name="tx-queue-size">
+ <properties>
+ <help>Tranceive queue size</help>
+ <valueHelp>
+ <format>u32:0-65535</format>
+ <description>Size of tranceive queue</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ <leafNode name="zero-copy">
+ <properties>
+ <help>Enable zero-copy mode</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <node name="ipv6">
+ <properties>
+ <help>IPv6 settings</help>
+ </properties>
+ <children>
+ <leafNode name="heap-size">
+ <properties>
+ <help>IPv6 heap size</help>
+ #include <include/unformat_memory_size.xml.i>
+ </properties>
+ <defaultValue>32M</defaultValue>
+ </leafNode>
+ <leafNode name="hash-buckets">
+ <properties>
+ <help>IPv6 forwarding table hash buckets</help>
+ <valueHelp>
+ <format>u32:1-4294967295</format>
+ <description>IPv6 forwarding table hash buckets</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ <defaultValue>65536</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ipsec">
+ <properties>
+ <help>IPsec settings</help>
+ </properties>
+ <children>
+ <leafNode name="interface-type">
+ <properties>
+ <help>IPsec interface type</help>
+ <completionHelp>
+ <list>ipsec ipip</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipsec</format>
+ <description>IPsec interface type</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip</format>
+ <description>IPIP tunnel interface type</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ipsec|ipip)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>ipsec</defaultValue>
+ </leafNode>
+ #include <include/vpp/netlink.xml.i>
+ </children>
+ </node>
+ <node name="l2learn">
+ <properties>
+ <help>Level 2 MAC address learning settings</help>
+ </properties>
+ <children>
+ <leafNode name="limit">
+ <properties>
+ <help>Number of MAC addresses in the L2 FIB</help>
+ <valueHelp>
+ <format>u32:1-4294967295</format>
+ <description>Number of concurent entries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ <defaultValue>4194304</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <node name="lcp">
+ <properties>
+ <help>Linux control plane setting</help>
+ </properties>
+ <children>
+ #include <include/vpp/netlink.xml.i>
+ <leafNode name="ignore-kernel-routes">
+ <properties>
+ <help>Ignore kernel routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="logging">
+ <properties>
+ <help>Logging settings</help>
+ </properties>
+ <children>
+ <leafNode name="default-log-level">
+ <properties>
+ <help>default-log-level</help>
+ <completionHelp>
+ <list>alert crit debug disabled emerg err info notice warn</list>
+ </completionHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Alert</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disabled</format>
+ <description>Disabled</description>
+ </valueHelp>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Notice</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warn</format>
+ <description>Warning</description>
+ </valueHelp>
+ <constraint>
+ <regex>(alert|crit|debug|disabled|emerg|err|info|notice|warn)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="memory">
+ <properties>
+ <help>Memory settings</help>
+ </properties>
+ <children>
+ <leafNode name="main-heap-size">
+ <properties>
+ <help>Main heap size</help>
+ #include <include/unformat_memory_size.xml.i>
+ </properties>
+ <defaultValue>3G</defaultValue>
+ </leafNode>
+ <leafNode name="main-heap-page-size">
+ <properties>
+ <help>Main heap page size</help>
+ #include <include/unformat_log2_page_size.xml.i>
+ </properties>
+ </leafNode>
+ <leafNode name="default-hugepage-size">
+ <properties>
+ <help>Default hugepage size</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_mem_page_size.py --hugepage_only True </script>
+ </completionHelp>
+ </properties>
+ <defaultValue>2M</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <node name="nat44">
+ <properties>
+ <help>NAT settings</help>
+ </properties>
+ <children>
+ <node name="timeout">
+ <properties>
+ <help>NAT44 session timeouts</help>
+ </properties>
+ <children>
+ <leafNode name="icmp">
+ <properties>
+ <help>ICMP timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 60)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>60</defaultValue>
+ </leafNode>
+ <leafNode name="tcp-established">
+ <properties>
+ <help>TCP established timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>TCP established timeout in seconds (default: 7440)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>7440</defaultValue>
+ </leafNode>
+ <leafNode name="tcp-transitory">
+ <properties>
+ <help>TCP transitory timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 240)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>240</defaultValue>
+ </leafNode>
+ <leafNode name="udp">
+ <properties>
+ <help>UDP timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 300)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>300</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="session-limit">
+ <properties>
+ <help>Maximum number of sessions per thread</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of sessions</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>Number of sessions must be between 1 and 4294967295</constraintErrorMessage>
+ </properties>
+ <defaultValue>64512</defaultValue>
+ </leafNode>
+ <leafNode name="workers">
+ <properties>
+ <help>List of NAT workers</help>
+ <valueHelp>
+ <format>&lt;id&gt;</format>
+ <description>Worker id</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;idN&gt;-&lt;idM&gt;</format>
+ <description>Worker id range (use '-' as delimiter)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--allow-range --range 0-512"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid value or range</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-forwarding">
+ <properties>
+ <help>Do not forward packets which do not match existing NAT translations (static or dynamic)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="physmem">
+ <properties>
+ <help>Physical memory settings</help>
+ </properties>
+ <children>
+ <leafNode name="max-size">
+ <properties>
+ <help>Set memory size for protectable memory allocator (pmalloc) memory space</help>
+ #include <include/unformat_memory_size.xml.i>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="statseg">
+ <properties>
+ <help>Stats settings</help>
+ </properties>
+ <children>
+ <leafNode name="size">
+ <properties>
+ <help>Size of stats segment</help>
+ #include <include/unformat_memory_size.xml.i>
+ </properties>
+ </leafNode>
+ <leafNode name="page-size">
+ <properties>
+ <help>Stats page size</help>
+ #include <include/unformat_log2_page_size.xml.i>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="unix">
+ <properties>
+ <help>Unix settings</help>
+ </properties>
+ <children>
+ <leafNode name="poll-sleep-usec">
+ <properties>
+ <help>Add a fixed-sleep between main loop poll</help>
+ <valueHelp>
+ <format>u32:0-500000</format>
+ <description>Sleep interval in microseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-500000"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="sflow" owner="${vyos_conf_scripts_dir}/vpp_sflow.py">
+ <properties>
+ <help>VPP data-plane sFlow</help>
+ <priority>322</priority>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="sample-rate">
+ <properties>
+ <help>sFlow sample rate</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>sFlow sample rate</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="nat">
+ <properties>
+ <help>Network Address Translation (NAT) settings</help>
+ </properties>
+ <children>
+ <node name="cgnat" owner="${vyos_conf_scripts_dir}/vpp_nat_cgnat.py">
+ <properties>
+ <help>Carrier-grade NAT (CGNAT)</help>
+ <priority>331</priority>
+ </properties>
+ <children>
+ <node name="interface">
+ <properties>
+ <help>CGNAT interface setting</help>
+ </properties>
+ <children>
+ <leafNode name="inside">
+ <properties>
+ <help>CGNAT inside interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="outside">
+ <properties>
+ <help>CGNAT outside interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="rule">
+ <properties>
+ <help>Rule number for CGNAT</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of rule</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="inside-prefix">
+ <properties>
+ <help>Inside IPv4 prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="outside-prefix">
+ <properties>
+ <help>Outside IPv4 prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
+ <node name="timeout">
+ <properties>
+ <help>Timeouts for CGNAT sessions</help>
+ </properties>
+ <children>
+ <leafNode name="icmp">
+ <properties>
+ <help>ICMP timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 60)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>60</defaultValue>
+ </leafNode>
+ <leafNode name="tcp-established">
+ <properties>
+ <help>TCP established timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>TCP established timeout in seconds (default: 7440)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>7440</defaultValue>
+ </leafNode>
+ <leafNode name="tcp-transitory">
+ <properties>
+ <help>TCP transitory timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 240)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>240</defaultValue>
+ </leafNode>
+ <leafNode name="udp">
+ <properties>
+ <help>UDP timeout</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Timeout in seconds (default: 300)</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>300</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="nat44" owner="${vyos_conf_scripts_dir}/vpp_nat.py">
+ <properties>
+ <help>NAT44</help>
+ <priority>330</priority>
+ </properties>
+ <children>
+ <node name="interface">
+ <properties>
+ <help>NAT interface setting</help>
+ </properties>
+ <children>
+ <leafNode name="inside">
+ <properties>
+ <help>NAT inside interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="outside">
+ <properties>
+ <help>NAT outside interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="address-pool">
+ <properties>
+ <help>NAT address pool</help>
+ </properties>
+ <children>
+ <node name="translation">
+ <properties>
+ <help>NAT translation pool</help>
+ </properties>
+ <children>
+ #include <include/vpp/nat_address_range.xml.i>
+ #include <include/vpp/nat_interface.xml.i>
+ </children>
+ </node>
+ <node name="twice-nat">
+ <properties>
+ <help>NAT twice-nat pool</help>
+ </properties>
+ <children>
+ #include <include/vpp/nat_address_range.xml.i>
+ #include <include/vpp/nat_interface.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Static NAT rules</help>
+ </properties>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>Rule number for NAT</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of NAT rule</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ <node name="external">
+ <properties>
+ <help>NAT external parameters</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ </children>
+ </node>
+ <node name="local">
+ <properties>
+ <help>NAT local parameters</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ </children>
+ </node>
+ <node name="options">
+ <properties>
+ <help>NAT static mapping options</help>
+ </properties>
+ <children>
+ <leafNode name="twice-nat">
+ <properties>
+ <help>Rewrite source IP addresses on packets sent from outside to inside</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="self-twice-nat">
+ <properties>
+ <help>Rewrite source IP addresses on packets sent only from a local address to an external address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="out-to-in-only">
+ <properties>
+ <help>Only apply rule for traffic from outside to inside interfaces</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="twice-nat-address">
+ <properties>
+ <help>Force use of specific IP address from twice-nat address pool</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/vpp/nat_protocol.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="exclude">
+ <properties>
+ <help>Exclude packets matching these rules from NAT</help>
+ </properties>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>Rule number</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of rule</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="local-address">
+ <properties>
+ <help>IP address of the internal (local) device</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="local-port">
+ <properties>
+ <help>Port number used by connection on internal device</help>
+ <valueHelp>
+ <format>u32:1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/vpp/nat_protocol.xml.i>
+ <leafNode name="external-interface">
+ <properties>
+ <help>External interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="acl" owner="${vyos_conf_scripts_dir}/vpp_acl.py">
+ <properties>
+ <help>Access Control Lists (ACLs)</help>
+ <priority>332</priority>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Access Control List by IPv4/IPv6</help>
+ </properties>
+ <children>
+ <tagNode name="tag-name">
+ <properties>
+ <help>ACL tag name</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Tag name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ <tagNode name="rule">
+ <properties>
+ <help>Rule number for Access control element (ACE)</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of ACE</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>Rule number must be between 1 and 4294967295</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ <leafNode name="action">
+ <properties>
+ <help>Rule action</help>
+ <completionHelp>
+ <list>permit deny permit-reflect</list>
+ </completionHelp>
+ <valueHelp>
+ <format>permit</format>
+ <description>Permit matching traffic</description>
+ </valueHelp>
+ <valueHelp>
+ <format>deny</format>
+ <description>Deny matching traffic</description>
+ </valueHelp>
+ <valueHelp>
+ <format>permit-reflect</format>
+ <description>Permit the matching outbound traffic and allow the reverse traffic</description>
+ </valueHelp>
+ <constraint>
+ <regex>(permit|deny|permit-reflect)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="source">
+ <properties>
+ <help>Source parameters</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_prefix.xml.i>
+ #include <include/vpp/acl_port_range.xml.i>
+ </children>
+ </node>
+ <node name="destination">
+ <properties>
+ <help>Destination parameters</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_prefix.xml.i>
+ #include <include/vpp/acl_port_range.xml.i>
+ </children>
+ </node>
+ <leafNode name="protocol">
+ <properties>
+ <help>Protocol</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_protocols.sh</script>
+ <list>all</list>
+ </completionHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All IP protocols</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;protocol&gt;</format>
+ <description>IP protocol name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-protocol"/>
+ </constraint>
+ </properties>
+ <defaultValue>all</defaultValue>
+ </leafNode>
+ <node name="tcp-flags">
+ <properties>
+ <help>TCP flags</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_tcp_flags.xml.i>
+ <node name="not">
+ <properties>
+ <help>Match flags not set</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_tcp_flags.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Apply an ACL to an interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="input">
+ <properties>
+ <help>Input direction</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_common_interface_ip_rule.xml.i>
+ </children>
+ </node>
+ <node name="output">
+ <properties>
+ <help>Output direction</help>
+ </properties>
+ <children>
+ #include <include/vpp/acl_common_interface_ip_rule.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="macip">
+ <properties>
+ <help>Access Control List by mac address</help>
+ </properties>
+ <children>
+ <tagNode name="tag-name">
+ <properties>
+ <help>ACL tag name</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>ACL name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ <tagNode name="rule">
+ <properties>
+ <help>Rule number for Access control element (ACE)</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of ACE</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>Rule number must be between 1 and 4294967295</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ <leafNode name="action">
+ <properties>
+ <help>Rule action</help>
+ <completionHelp>
+ <list>permit deny</list>
+ </completionHelp>
+ <valueHelp>
+ <format>permit</format>
+ <description>Permit matching traffic</description>
+ </valueHelp>
+ <valueHelp>
+ <format>deny</format>
+ <description>Deny matching traffic</description>
+ </valueHelp>
+ <constraint>
+ <regex>(permit|deny)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/vpp/acl_prefix.xml.i>
+ <leafNode name="prefix">
+ <properties>
+ <help>Source IP prefix</help>
+ </properties>
+ </leafNode>
+ <leafNode name="mac-address">
+ <properties>
+ <help>Source MAC address</help>
+ <valueHelp>
+ <format>macaddr</format>
+ <description>MAC address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mac-mask">
+ <properties>
+ <help>Source MAC mask (default ff:ff:ff:ff:ff:ff)</help>
+ <valueHelp>
+ <format>macaddr</format>
+ <description>MAC mask</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ <defaultValue>ff:ff:ff:ff:ff:ff</defaultValue>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Apply an ACL to an input interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="tag-name">
+ <properties>
+ <help>ACL tag name</help>
+ <completionHelp>
+ <path>vpp acl macip tag-name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <tagNode name="kernel-interfaces" owner="${vyos_conf_scripts_dir}/vpp_kernel-interfaces.py">
+ <properties>
+ <help>VPP kernel interface settings</help>
+ <priority>328</priority>
+ <valueHelp>
+ <format>vpptapN</format>
+ <description>Kernel interface name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vpptunN</format>
+ <description>Kernel interface name</description>
+ </valueHelp>
+ <constraint>
+ <regex>vpp(tun|tap)\d+</regex>
+ </constraint>
+ <constraintErrorMessage>Kernel interface must start with vpp and include tun or tap N</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/interface/address-ipv4-ipv6.xml.i>
+ #include <include/generic-description.xml.i>
+ #include <include/interface/disable.xml.i>
+ #include <include/interface/mtu-68-16000.xml.i>
+ #include <include/vpp/vif.xml.i>
+ #include <include/vpp/iface_rx_mode.xml.i>
+ </children>
+ </tagNode>
+ </children>
+</node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show_vpp_interfaces.xml.in b/op-mode-definitions/show_vpp_interfaces.xml.in
new file mode 100644
index 000000000..75bb05240
--- /dev/null
+++ b/op-mode-definitions/show_vpp_interfaces.xml.in
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vpp">
+ <properties>
+ <help>Show VPP information</help>
+ </properties>
+ <children>
+ <node name="interfaces">
+ <properties>
+ <help>Show VPP interface information</help>
+ </properties>
+ <children>
+ <node name="dataplane">
+ <properties>
+ <help>Show VPP dataplane interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --dataplane</command>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Show VPP kernel interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --kernel</command>
+ </node>
+ <tagNode name="hardware">
+ <properties>
+ <help>Show detailed statistics for specified VPP interface</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_vpp_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <standalone>
+ <help>Show VPP interfaces detailed statistics</help>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --hardware</command>
+ </standalone>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --hardware --intf-name="$5"</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --summary</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show_vpp_nat44.xml.in b/op-mode-definitions/show_vpp_nat44.xml.in
new file mode 100644
index 000000000..96d53fba9
--- /dev/null
+++ b/op-mode-definitions/show_vpp_nat44.xml.in
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vpp">
+ <children>
+ <node name="nat44">
+ <properties>
+ <help>Show VPP NAT44 information</help>
+ </properties>
+ <children>
+ <node name="static">
+ <properties>
+ <help>Show VPP NAT44 static mapping</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_static</command>
+ </node>
+ <node name="sessions">
+ <properties>
+ <help>Show VPP NAT44 sessions</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_sessions</command>
+ </node>
+ <node name="summary">
+ <properties>
+ <help>Show VPP NAT44 summary</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_summary</command>
+ </node>
+ <node name="addresses">
+ <properties>
+ <help>Show VPP NAT44 pool addresses</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_addresses</command>
+ </node>
+ <node name="interfaces">
+ <properties>
+ <help>Show VPP NAT44 interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_interfaces</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/vpp_acl.xml.in b/op-mode-definitions/vpp_acl.xml.in
new file mode 100644
index 000000000..a901a537d
--- /dev/null
+++ b/op-mode-definitions/vpp_acl.xml.in
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vpp">
+ <children>
+ <node name="acl">
+ <properties>
+ <help>Show VPP ACL information</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show VPP ACL by IPv4/IPv6</help>
+ </properties>
+ <children>
+ <tagNode name="tag-name">
+ <properties>
+ <help>Show specified VPP ACL</help>
+ <completionHelp>
+ <path>vpp acl ip tag-name</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls --tag-name="$6"</command>
+ </tagNode>
+ <node name="interface">
+ <properties>
+ <help>Show VPP ACL interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_interfaces</command>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls</command>
+ </node>
+ <node name="macip">
+ <properties>
+ <help>Show VPP ACL by macip</help>
+ </properties>
+ <children>
+ <tagNode name="tag-name">
+ <properties>
+ <help>Show specified VPP ACL</help>
+ <completionHelp>
+ <path>vpp acl macip tag-name</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls --tag-name="$6"</command>
+ </tagNode>
+ <node name="interface">
+ <properties>
+ <help>Show VPP ACL interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_interfaces</command>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls</command>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_all_acls</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/vpp_nat_cgnat.xml.in b/op-mode-definitions/vpp_nat_cgnat.xml.in
new file mode 100644
index 000000000..8af10711f
--- /dev/null
+++ b/op-mode-definitions/vpp_nat_cgnat.xml.in
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vpp">
+ <children>
+ <node name="nat">
+ <properties>
+ <help>Show VPP NAT information</help>
+ </properties>
+ <children>
+ <node name="cgnat">
+ <properties>
+ <help>Show VPP CGNAT information</help>
+ </properties>
+ <children>
+ <node name="mappings">
+ <properties>
+ <help>Show VPP CGNAT mappings</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_mappings</command>
+ </node>
+ <node name="sessions">
+ <properties>
+ <help>Show VPP CGNAT sessions</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_sessions</command>
+ </node>
+ <node name="interfaces">
+ <properties>
+ <help>Show VPP CGNAT interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_interfaces</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="clear">
+ <children>
+ <node name="vpp">
+ <properties>
+ <help>Terminate VPP NAT sessions</help>
+ </properties>
+ <children>
+ <node name="cgnat">
+ <properties>
+ <help>Terminate VPP CGNAT sessions</help>
+ </properties>
+ <children>
+ <tagNode name="inside-address">
+ <properties>
+ <help>Inside IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>Port</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="external-address">
+ <properties>
+ <help>External IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>Port</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py clear_session --address $5 --port $7 --ext-address $9 --ext-port ${11}</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/vpp/__init__.py b/python/vyos/vpp/__init__.py
new file mode 100644
index 000000000..a320ac7d1
--- /dev/null
+++ b/python/vyos/vpp/__init__.py
@@ -0,0 +1,20 @@
+#
+# Copyright (C) 2023 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 .control_vpp import VPPControl
+
+__all__ = ['VPPControl']
diff --git a/python/vyos/vpp/acl/__init__.py b/python/vyos/vpp/acl/__init__.py
new file mode 100644
index 000000000..6bd6cdb23
--- /dev/null
+++ b/python/vyos/vpp/acl/__init__.py
@@ -0,0 +1,3 @@
+from .acl import Acl
+
+__all__ = ['Acl']
diff --git a/python/vyos/vpp/acl/acl.py b/python/vyos/vpp/acl/acl.py
new file mode 100644
index 000000000..9da241f1c
--- /dev/null
+++ b/python/vyos/vpp/acl/acl.py
@@ -0,0 +1,106 @@
+#
+# Copyright (C) 2025 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.vpp import VPPControl
+
+
+NO_ACL_INDEX = 0xFFFFFFFF
+
+
+class Acl:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def get_acl_index_by_tag(self, tag):
+ """Get ACL index by tag name
+ https://github.com/FDio/vpp/blob/21c641f9356da5137760cdc799127064c8c1fd31/src/plugins/acl/acl.api
+ """
+ for acl in self.vpp.api.acl_dump(acl_index=NO_ACL_INDEX):
+ if acl.tag == tag:
+ return acl.acl_index
+ return NO_ACL_INDEX
+
+ def add_replace_acl(self, tag, rules):
+ """Add new ACL or replace existing one"""
+ self.vpp.api.acl_add_replace(
+ tag=tag,
+ acl_index=self.get_acl_index_by_tag(tag),
+ count=len(rules),
+ r=rules,
+ )
+
+ def delete_acl(self, tag):
+ """Delete existing ACL"""
+ self.vpp.api.acl_del(acl_index=self.get_acl_index_by_tag(tag))
+
+ def add_acl_interface(self, interface, input_tags, output_tags):
+ """Add or replace ACLs on interface"""
+ acls = []
+ for tag in input_tags:
+ acl_index = self.get_acl_index_by_tag(tag)
+ acls.append(acl_index)
+ for tag in output_tags:
+ acl_index = self.get_acl_index_by_tag(tag)
+ acls.append(acl_index)
+ self.vpp.api.acl_interface_set_acl_list(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ count=len(acls),
+ n_input=len(input_tags),
+ acls=acls,
+ )
+
+ def delete_acl_interface(self, interface):
+ """Delete ACLs from interface"""
+ self.vpp.api.acl_interface_set_acl_list(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ count=0,
+ )
+
+ def get_macip_acl_index_by_tag(self, tag):
+ """Get macip ACL by tag name"""
+ for acl in self.vpp.api.macip_acl_dump():
+ if acl.tag == tag:
+ return acl.acl_index
+ return NO_ACL_INDEX
+
+ def add_replace_acl_macip(self, tag, rules):
+ """Add or replace existing macip ACL"""
+ self.vpp.api.macip_acl_add_replace(
+ tag=tag,
+ acl_index=self.get_macip_acl_index_by_tag(tag),
+ count=len(rules),
+ r=rules,
+ )
+
+ def delete_acl_macip(self, tag):
+ """Delete existing macip ACL"""
+ self.vpp.api.macip_acl_del(acl_index=self.get_macip_acl_index_by_tag(tag))
+
+ def add_acl_macip_interface(self, interface, tag):
+ """Add or replace macip ACLs on interface"""
+ self.vpp.api.macip_acl_interface_add_del(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ acl_index=self.get_macip_acl_index_by_tag(tag),
+ is_add=True,
+ )
+
+ def delete_acl_macip_interface(self, interface):
+ """Delete macip ACLs from interface"""
+ self.vpp.api.macip_acl_interface_add_del(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=False,
+ )
diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py
new file mode 100644
index 000000000..d826dedbb
--- /dev/null
+++ b/python/vyos/vpp/config_deps.py
@@ -0,0 +1,76 @@
+#
+# Copyright (C) 2023-2024 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.
+
+
+def deps_xconnect_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all xconnect interface members:
+
+ keys: members
+
+ values: xconnect interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ xconn_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'xconnect'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for xconn_name, xconn_config in config.items():
+ for member_name in xconn_config.get('member', {}).get('interface', []):
+ xconn_ifaces_list = xconn_members_dict.get(xconn_name, [])
+ xconn_ifaces_list.append(xconn_name)
+ xconn_members_dict.update({member_name: xconn_ifaces_list})
+
+ return xconn_members_dict
+
+
+def deps_bridge_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all bridge interface members:
+
+ keys: members
+
+ values: bridge interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ bridge_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'bridge'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for bridge_name, bridge_config in config.items():
+ for member_name in bridge_config.get('member', {}).get('interface', []):
+ bridge_ifaces_list = bridge_members_dict.get(bridge_name, [])
+ bridge_ifaces_list.append(bridge_name)
+ bridge_members_dict.update({member_name: bridge_ifaces_list})
+
+ return bridge_members_dict
diff --git a/python/vyos/vpp/config_filter.py b/python/vyos/vpp/config_filter.py
new file mode 100644
index 000000000..4ab9ffda2
--- /dev/null
+++ b/python/vyos/vpp/config_filter.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2024 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.config import Config
+
+
+def iface_filter_eth(config: Config, iface: str) -> None:
+ """Filter out unsupported config nodes from Ethernet interface config
+
+ Args:
+ config (Config): config object
+ iface (str): Ethernet interface name to filter
+ """
+ allowed_nodes: list[str] = [
+ 'address',
+ 'description',
+ 'dhcp-options',
+ 'dhcpv6-options',
+ 'disable',
+ 'eapol',
+ 'hw-id',
+ 'ip',
+ 'ipv6',
+ 'mtu',
+ 'redirect',
+ 'vif',
+ 'vif-s',
+ 'vrf',
+ ]
+
+ # get list of config nides in a session configuration
+ iface_nodes = config._session_config.list_nodes(['interfaces', 'ethernet', iface])
+
+ # clean cached session config
+ if False in config._dict_cache:
+ del config._dict_cache[False]
+
+ # remove unsupported config nodes
+ for cfg_node in iface_nodes:
+ if cfg_node not in allowed_nodes:
+ config._session_config.delete(['interfaces', 'ethernet', iface, cfg_node])
+ print(
+ f'WARNING: {cfg_node} option in {iface} settings is not supported by VPP interfaces. It will be ignored.'
+ )
diff --git a/python/vyos/vpp/config_resource_checks/__init__.py b/python/vyos/vpp/config_resource_checks/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/__init__.py
diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py
new file mode 100644
index 000000000..ee0bace2d
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/cpu.py
@@ -0,0 +1,76 @@
+# Used for validating estimated CPU/physical cores use
+#
+# Copyright (C) 2025 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.utils.cpu import get_available_cpus, get_core_count
+
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+
+
+# Get default value for reserved cpu cores
+reserved_cpus = default_resource_map.get('reserved_cpu_cores')
+
+
+def available_cores_count(cpu_settings: dict) -> int:
+ core_count = get_core_count()
+
+ if cpu_settings.get('main_core'):
+ core_count -= 1
+
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ # The default settings assume that
+ # at least 2 CPU cores should remain reserved for system use
+ # (only in case of current runtime is not smoke test)
+ if skip_cores < reserved_cpus:
+ core_count -= reserved_cpus
+ else:
+ core_count -= skip_cores
+
+ return core_count
+
+
+def available_cores_list(skip_cores: int) -> list:
+ # Available cores are all CPU cores without first N skipped cores that will not be used
+ # Get all available physical cores - use set to filter out unique values
+ cpu_cores = set(map(lambda el: el['cpu'], get_available_cpus()))
+ cpu_cores = list(cpu_cores)
+
+ return cpu_cores[skip_cores:]
+
+
+def worker_cores_list(iface: str, worker_ranges: list) -> list:
+ all_core_numbers = []
+ for worker_range in worker_ranges:
+ core_numbers = worker_range.split('-')
+
+ if int(core_numbers[0]) > int(core_numbers[-1]):
+ raise ValueError(
+ f'Range for "{iface} workers {worker_range}" is not correct'
+ )
+
+ all_core_numbers.extend(range(int(core_numbers[0]), int(core_numbers[-1]) + 1))
+
+ # Check for duplicates
+ duplicates = set(
+ [str(x) for n, x in enumerate(all_core_numbers) if x in all_core_numbers[:n]]
+ )
+ if duplicates:
+ raise ValueError(
+ f'Some workers in "{iface} workers" are duplicated: #{",".join(list(duplicates))}'
+ )
+
+ return all_core_numbers
diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py
new file mode 100644
index 000000000..f8ccf7d47
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/memory.py
@@ -0,0 +1,153 @@
+# Used for memory consumption calculations
+#
+# Copyright (C) 2025 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 re
+
+from vyos.utils.process import cmd
+from vyos.vpp.utils import (
+ human_memory_to_bytes,
+ human_page_memory_to_bytes,
+)
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+
+
+def get_hugepages_info() -> dict:
+ """
+ Returns the information about HugePages for default hugepage size
+ retrieved from /proc/meminfo
+ """
+ info = {}
+ with open('/proc/meminfo', 'r') as meminfo:
+ for line in meminfo:
+ if line.startswith('Huge'):
+ key, value, *_ = line.strip().split()
+ info[key.rstrip(':')] = int(value)
+ return info
+
+
+def get_total_hugepages_memory() -> int:
+ """
+ Returns the total amount of hugepage memory (in bytes)
+ """
+ info = get_hugepages_info()
+ hugepages_total = info.get('HugePages_Total')
+ hugepage_size = info.get('Hugepagesize') * 1024
+
+ return hugepage_size * hugepages_total
+
+
+def get_total_hugepages_count() -> int:
+ """
+ Returns the total count of hugepages
+ """
+ info = get_hugepages_info()
+ return info.get('HugePages_Total')
+
+
+def get_numa_count():
+ """
+ Run `numactl --hardware` and parse the 'available:' line.
+ """
+ out = cmd('numactl --hardware')
+ # e.g. "available: 2 nodes (0-1)"
+ m = re.search(r'available:\s*(\d+)\s+nodes', out)
+ return int(m.group(1)) if m else 0
+
+
+def buffer_size(settings: dict) -> int:
+ numa_count = get_numa_count()
+ buffers_per_numa = int(
+ settings.get('buffers', {}).get(
+ 'buffers_per_numa', default_resource_map.get('buffers_per_numa')
+ )
+ )
+ data_size = int(
+ settings.get('buffers', {}).get(
+ 'data_size', default_resource_map.get('data_size')
+ )
+ )
+ buffers_memory = buffers_per_numa * data_size * numa_count
+ return buffers_memory
+
+
+def main_heap_page_size(settings: dict) -> int:
+ heap_page_size = settings.get('memory', {}).get(
+ 'main_heap_page_size', default_resource_map.get('main_heap_page_size')
+ )
+ return human_page_memory_to_bytes(heap_page_size)
+
+
+def memory_main_heap(settings: dict) -> int:
+ heap_size = settings.get('memory', {}).get(
+ 'main_heap_size', default_resource_map.get('main_heap_size')
+ )
+ return human_memory_to_bytes(heap_size)
+
+
+def ipv6_heap_size(settings: dict) -> int:
+ heap_size = settings.get('ipv6', {}).get(
+ 'heap_size', default_resource_map.get('ipv6_heap_size')
+ )
+ return human_memory_to_bytes(heap_size)
+
+
+def total_heap_size(heap_size: int, heap_page_size: int) -> int:
+ return (heap_size + heap_page_size - 1) & ~(heap_page_size - 1)
+
+
+def statseg_size(settings: dict) -> int:
+ statseg_memory = settings.get('statseg', {}).get(
+ 'size', default_resource_map.get('statseg_heap_size')
+ )
+ return human_memory_to_bytes(statseg_memory)
+
+
+def statseg_page_size(settings: dict) -> int:
+ page_size = settings.get('statseg', {}).get('page_size', 'default')
+ return human_page_memory_to_bytes(page_size)
+
+
+def total_statseg_size(_statseg_size: int, _statseg_page: int) -> int:
+ return (_statseg_size + _statseg_page - 1) & ~(_statseg_page - 1)
+
+
+def total_memory_required(settings: dict) -> int:
+ mem_required = 0
+
+ mem_stats = {
+ 'memory_buffers': buffer_size(settings),
+ 'netlink_buffer_size': int(
+ settings.get('lcp', {}).get(
+ 'rx_buffer_size', default_resource_map.get('netlink_rx_buffer_size')
+ )
+ ),
+ 'heap_size': total_heap_size(
+ heap_size=memory_main_heap(settings),
+ heap_page_size=main_heap_page_size(settings),
+ ),
+ 'statseg_size': total_statseg_size(
+ _statseg_size=statseg_size(settings),
+ _statseg_page=statseg_page_size(settings),
+ ),
+ 'ipv6_heap_size': ipv6_heap_size(settings),
+ }
+
+ for stat in mem_stats:
+ mem_required += mem_stats[stat]
+
+ return mem_required
diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py
new file mode 100644
index 000000000..828683456
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py
@@ -0,0 +1,43 @@
+# Default values for resource consumption checks
+#
+# Copyright (C) 2025 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.
+
+
+default_resource_map = {
+ # Default amount of buffers per NUMA (populated CPU socket)
+ 'buffers_per_numa': 16384,
+ # Default size of buffer (in bytes)
+ 'data_size': 2048,
+ # Default hugepage size for VPP
+ 'hugepage_size': '2M',
+ # Default amount of memory allocated for VPP exclusive usage
+ 'main_heap_size': '3G',
+ # Default main heap page size
+ 'main_heap_page_size': '2M',
+ # Default size of buffers transferred via netlink
+ 'netlink_rx_buffer_size': 212992,
+ # Default amount of memory allocated for VPP stats segment usage
+ 'statseg_heap_size': '96M',
+ # Minimal amount of memory required to start VPP
+ 'min_memory': '8G',
+ # Minimal number of physical CPU cores required to start VPP
+ 'min_cpus': 4,
+ # Reserve at least 2 physical cores
+ 'reserved_cpu_cores': 2,
+ # Default heap size for IPv6
+ 'ipv6_heap_size': '32M',
+}
diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py
new file mode 100644
index 000000000..0e21a5a8a
--- /dev/null
+++ b/python/vyos/vpp/config_verify.py
@@ -0,0 +1,407 @@
+# Used for verifying configuration vpp interfaces
+#
+# Copyright (C) 2023 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 psutil
+
+from vyos import ConfigError
+from vyos.base import Warning
+from vyos.utils.cpu import get_core_count as total_core_count
+
+from vyos.vpp.control_host import get_eth_driver
+from vyos.vpp.config_resource_checks import cpu as cpu_checks, memory as mem_checks
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory
+
+
+def verify_vpp_remove_kernel_interface(config: dict):
+ """Common verify for removed kernel-interfaces.
+ Verify that removed kernel interface are not used in 'vpp kernel-interfaces'.
+
+ Example:
+ delete vpp interfaces gre|vxlan <tag>X kernel-interface vpp-tunX
+ set vpp kernel-interface vpp-tunX
+ """
+ if (
+ 'remove' in config
+ and 'kernel_interface_removed' in config
+ and 'vpp_kernel_interfaces' in config
+ ):
+ removed_interfaces = config['kernel_interface_removed']
+ used_interfaces = config['vpp_kernel_interfaces']
+
+ for interface in removed_interfaces:
+ if interface in used_interfaces:
+ raise ConfigError(
+ f'"{interface}" is still in use within "vpp kernel-interfaces". '
+ 'Please remove it before proceeding.'
+ )
+
+
+def verify_vpp_change_kernel_interface(config: dict):
+ """Common verify for changed kernel-interface
+
+ Example:
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunX'
+ commit
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunY'
+ commit
+
+ check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ """
+ kernel_interface_removed = config.get('kernel_interface_removed', [])
+ vpp_kernel_interfaces = config.get('vpp_kernel_interfaces', {})
+
+ for interface in kernel_interface_removed:
+ if interface in vpp_kernel_interfaces:
+ raise ConfigError(
+ f'interface "{interface}" is still in use within "vpp kernel-interfaces". '
+ f'Please remove it "vpp kernel-interface {interface}" before proceeding.'
+ )
+
+
+def verify_vpp_exists_kernel_interface(config: dict):
+ """Verify is a kernel-interface already created by another VPP LCP pair
+
+ Example:
+ set vpp interfaces vxlan vxlan10 kernel-interface vpp-tun10'
+ commit
+ set vpp interfaces vxlan vxlan20 kernel-interface vpp-tun10'
+ commit
+ """
+ kernel_interface = config.get('kernel_interface', '')
+ vpp_interface = config.get('ifname', '')
+ candidate_kernel_interfaces = config.get('candidate_kernel_interfaces', [])
+
+ for candidate_kernel_iface in candidate_kernel_interfaces:
+ if (
+ vpp_interface != candidate_kernel_iface[0]
+ and kernel_interface == candidate_kernel_iface[1]
+ ):
+ raise ConfigError(
+ f'Kernel interface "{kernel_interface}" is already configured for {candidate_kernel_iface[0]}. '
+ 'Duplicates are not allowed.'
+ )
+
+
+def verify_vpp_remove_xconnect_interface(config: dict):
+ if not config.get('remove'):
+ return
+ for xconn_member, xconn_iface in config.get('xconn_members').items():
+ if xconn_member == config.get('ifname'):
+ raise ConfigError(
+ f'interface "{xconn_member}" is still in use within "vpp interfaces xconnect". '
+ f'Please remove it from "vpp interface xconnect {xconn_iface}" before proceeding.'
+ )
+
+
+def verify_vpp_tunnel_source_address(config: dict):
+ from vyos.utils.network import is_intf_addr_assigned
+
+ address = config.get('source_address')
+ for iface in config.get('vpp_ether_vif_ifaces', []):
+ if is_intf_addr_assigned(iface, address):
+ return True
+
+ raise ConfigError(
+ f'Source address "{address}" is not assigned on any Ethernet or VIF interface!'
+ )
+
+
+def verify_dev_driver(iface_name: str, driver_type: str) -> bool:
+ # Lists of drivers compatible with DPDK and XDP
+ drivers_dpdk: list[str] = [
+ 'atlantic',
+ 'bnx2x',
+ 'e1000',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igc',
+ 'ixgbe',
+ 'liquidio',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ drivers_xdp: list[str] = [
+ 'atlantic',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igb',
+ 'igc',
+ 'ixgbe',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ driver: str = get_eth_driver(iface_name)
+
+ if driver_type == 'dpdk':
+ if driver in drivers_dpdk:
+ return True
+ elif driver_type == 'xdp':
+ if driver in drivers_xdp:
+ return True
+ else:
+ raise ConfigError(f'"Driver type {driver_type} is wrong')
+
+ return False
+
+
+def verify_vpp_minimum_cpus():
+ """
+ Verify that the host system has enough physical CPU cores
+ Current minimal requirement is 4
+ """
+ min_cpus = default_resource_map.get('min_cpus')
+ if total_core_count() < min_cpus:
+ raise ConfigError(
+ 'This system does not meet minimal requirements for VPP. '
+ f'Minimum {min_cpus} CPU cores are required.'
+ )
+
+
+def verify_vpp_minimum_memory():
+ """
+ Verify that the host system has enough RAM
+ Calculate by retrieving the amount of physical memory
+ And the minimal requirement (currently 8 GB). Round before comparing -
+ To avoid situations like when a machine nominally has 8192 MB (8 giga/gibibytes)
+ But the OS sees only 7.75 GB, creating a fail condition for this check
+ """
+ min_mem = default_resource_map.get('min_memory')
+ total_memory = round(psutil.virtual_memory().total / (1024**3))
+ min_memory = round(human_memory_to_bytes(min_mem) / (1024**3))
+
+ if total_memory < min_memory:
+ raise ConfigError(
+ 'This system does not meet minimal requirements for VPP. '
+ f'Minimum {min_memory} GB of RAM are required.'
+ )
+
+
+def verify_vpp_memory(config: dict):
+ main_heap_size = mem_checks.memory_main_heap(config['settings'])
+ main_heap_page_size = mem_checks.main_heap_page_size(config['settings'])
+
+ if main_heap_size < 51 << 20:
+ raise ConfigError('The main heap size must be greater than or equal to 51M')
+
+ readable_heap_page = bytes_to_human_memory(main_heap_page_size, 'K')
+
+ if main_heap_page_size > main_heap_size:
+ raise ConfigError(
+ f'The main heap size must be greater than or equal to page-size ({readable_heap_page})'
+ )
+
+ available_memory = mem_checks.get_total_hugepages_memory()
+ memory_required = mem_checks.total_memory_required(config['settings'])
+
+ if main_heap_size > available_memory:
+ available_memory_in_mb = bytes_to_human_memory(available_memory, 'M')
+ raise ConfigError(
+ f'"memory main-heap-size" must not be greater than hugepages memory. Reduce to {available_memory_in_mb} or less'
+ )
+
+ memory_required = round(memory_required / 1024**3, 1)
+ available_memory = round(available_memory / 1024**3, 1)
+
+ # Allow 10% error margin
+ allowed_margin = memory_required * 0.1
+
+ # Compare HugePage memory with required memory for VPP
+ if memory_required > available_memory + allowed_margin:
+ raise ConfigError(
+ f'Not enough free hugepage memory to start VPP: '
+ f'available: {available_memory} GB, required: {memory_required} GB. '
+ 'Please add kernel memory options for HugePages '
+ '"set system option kernel memory hugepage-size ..." and reboot'
+ )
+
+
+def verify_vpp_settings_cpu_skip_cores(skip_cores: int):
+ cpu_cores = total_core_count()
+
+ # The number of skipped cores must not be greater than
+ # available CPU cores in the system - 1 for main thread
+ if skip_cores > (cpu_cores - 1):
+ raise ConfigError(
+ f'The system does not have enough available CPUs to skip '
+ f'(reduce "cpu skip-cores" to {cpu_cores} or less)'
+ )
+
+
+def verify_vpp_settings_cpu_and_corelist_workers(settings: dict):
+ """
+ `set vpp settings cpu workers` and `set vpp settings cpu corelist-workers`
+ are mutually exclusive!
+ """
+ if (
+ 'corelist_workers' in settings or 'workers' in settings
+ ) and 'main_core' not in settings:
+ raise ConfigError('"cpu main-core" is required but not set!')
+
+ if 'corelist_workers' in settings and 'workers' in settings:
+ raise ConfigError(
+ '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!'
+ )
+
+
+def verify_vpp_cpu_main_core(cpu_settings: dict) -> None:
+ """Check that the main core is available"""
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ available_cores = cpu_checks.available_cores_list(skip_cores)
+ main_core = int(cpu_settings['main_core'])
+
+ if main_core not in available_cores:
+ raise ConfigError(
+ 'Cannot set main core for VPP process: '
+ f'CPU#{main_core} is not available.'
+ )
+
+
+def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int:
+ """
+ Verify that the system has enough available CPU cores
+ to run a given amount of worker processes (1 worker/core)
+ """
+ workers = int(cpu_settings.get('workers', 0))
+ available_cores = cpu_checks.available_cores_count(cpu_settings)
+
+ if workers > available_cores:
+ raise ConfigError(
+ f'Not enough free CPU cores for {workers} VPP workers '
+ f'(reduce to {available_cores} or less)'
+ )
+
+ return workers
+
+
+def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int:
+ """
+ Verify that the CPU cores provided to the config are free and can be used by VPP
+ """
+ workers = cpu_settings.get('corelist_workers')
+ main_core = int(cpu_settings.get('main_core'))
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ available_cores = cpu_checks.available_cores_list(skip_cores)
+ try:
+ all_core_nums = cpu_checks.worker_cores_list(
+ iface='cpu corelist', worker_ranges=workers
+ )
+ except ValueError as e:
+ raise ConfigError(str(e))
+
+ error_msg = 'Cannot set VPP "cpu corelist-workers"'
+
+ if main_core in all_core_nums:
+ raise ConfigError(
+ f'CPU#{main_core} is set as main core and should not '
+ 'be included to the corelist-workers'
+ )
+
+ invalid_cores = [str(el) for el in all_core_nums if el not in available_cores]
+ if invalid_cores:
+ raise ConfigError(
+ f'{error_msg}: CPU# {",".join(invalid_cores)} are not available.'
+ )
+
+ if len(all_core_nums) > cpu_checks.available_cores_count(cpu_settings):
+ raise ConfigError(f'{error_msg}: Not enough free CPUs in the system.')
+
+ return len(all_core_nums)
+
+
+def verify_vpp_nat44_workers(workers: int, nat44_workers: list):
+ if workers < 1:
+ raise ConfigError(
+ '"nat44 workers" requires cpu workers or corelist-workers to be set!'
+ )
+ try:
+ nat_workers = cpu_checks.worker_cores_list(
+ iface='nat44', worker_ranges=nat44_workers
+ )
+ except ValueError as e:
+ raise ConfigError(str(e))
+
+ invalid_workers = [str(el) for el in nat_workers if el not in range(workers)]
+ if invalid_workers:
+ raise ConfigError(
+ f'Cannot set VPP "nat44 workers": worker(s) #{",".join(invalid_workers)} not available. '
+ f'Available worker ids: {",".join(map(str, range(workers)))}'
+ )
+
+
+def verify_vpp_statseg_size(settings: dict):
+ statseg_size = mem_checks.statseg_size(settings)
+
+ if 'size' in settings.get('statseg'):
+ if statseg_size < 1 << 20:
+ raise ConfigError('The statseg size must be greater than or equal to 1M')
+
+ if 'page_size' in settings['statseg']:
+ statseg_page_size = mem_checks.statseg_page_size(settings)
+ if statseg_page_size > statseg_size:
+ readable_statseg_page = bytes_to_human_memory(statseg_page_size, 'K')
+ raise ConfigError(
+ f'The statseg size must be greater than or equal to page-size ({readable_statseg_page})'
+ )
+
+
+def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: int):
+ """
+ Verify that VPP has enough workers to run the given amount of RX/TX queues
+ 1 queue per 1 worker is assumed as default
+ """
+
+ if num_queues > workers:
+ raise ConfigError(
+ f'The number of {qtype} queues cannot be greater than the number of configured VPP workers: '
+ f'workers: {workers}, queues: {num_queues}'
+ )
+
+
+def verify_vpp_host_resources(config: dict):
+ max_map_count = int(config['settings']['host_resources']['max_map_count'])
+
+ # Get HugePages total count
+ hugepages = mem_checks.get_total_hugepages_count()
+
+ if max_map_count < 2 * hugepages:
+ Warning(
+ 'The max-map-count should be greater than or equal to (2 * HugePages_Total) '
+ 'or VPP could work not properly. Please set up '
+ f'"vpp settings host-resources max-map-count" to {2 * hugepages} or higher'
+ )
diff --git a/python/vyos/vpp/configdb.py b/python/vyos/vpp/configdb.py
new file mode 100644
index 000000000..1830937ad
--- /dev/null
+++ b/python/vyos/vpp/configdb.py
@@ -0,0 +1,206 @@
+#
+# Copyright (C) 2024 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 json import loads as json_loads, dumps as json_dumps
+from pathlib import Path
+from typing import Any
+
+STORAGE_LOCATION = '/run/vpp'
+
+
+class JSONStorage:
+ def __init__(self, name: str = '') -> None:
+ """Initiate a file storage
+
+ Args:
+ name (str, optional): Unique storage name. Defaults to '' (generate a name).
+
+ Raises:
+ err: In case a file for storage cannot be created
+ """
+ # If a name is not provided, this is a temporary one-time storage
+ # this use case is strange, but let's allow this
+ if not name:
+ self.__temporary = True
+ self.__cache: dict[Any, Any] = {}
+ self.__locked = False
+ return
+ self.__temporary = False
+
+ self.__storage = Path(f'{STORAGE_LOCATION}/{name}.json')
+ self.__lock_file = Path(f'{STORAGE_LOCATION}/{name}.lock')
+
+ # prepare a folder
+ storage_dir = Path(STORAGE_LOCATION)
+ if not storage_dir.exists():
+ storage_dir.mkdir(parents=True)
+
+ # initialize lock status
+ self.__locked = False
+ if self.__storage_locked():
+ raise FileExistsError(f'Cannot open locked storage: {self.__storage}')
+ self.__lock_file.touch()
+
+ if not self.__storage.exists():
+ try:
+ self.__storage.touch()
+ except Exception as err:
+ print(f'Unable to initiate storage: {err}')
+ raise err
+ # prepare an empty cache
+ self.__cache: dict[Any, Any] = {}
+ else:
+ # load a cache from file
+ self.__cache = self.__load_file()
+
+ def __del__(self) -> None:
+ """Dump data to persistent storage and unlock it"""
+ if self.__temporary:
+ return
+ # dump a cache to storage
+ if self.__cache:
+ self.__dump_file()
+ # or remove a file
+ else:
+ self.__storage.unlink()
+ # unlock a storage
+ self.__lock_file.unlink()
+
+ def __check_types(self, data: Any) -> None:
+ """Check if all the data have supported types
+
+ Args:
+ data (Any): object to validate
+
+ Raises:
+ TypeError: If a data type is not supported
+ """
+ if isinstance(data, str | int | float | bool | None):
+ return
+ if isinstance(data, list):
+ for item in data:
+ self.__check_types(item)
+ return
+ if isinstance(data, dict):
+ for item in data.values():
+ self.__check_types(item)
+ return
+ raise TypeError(f'Object type "{type(data)}" is not allowed')
+
+ def __load_file(self) -> dict[Any, Any]:
+ """Read a file to a dictionary
+
+ Returns:
+ dict[Any, Any]: loaded dict object
+ """
+ data: bytes = self.__storage.read_bytes()
+ return json_loads(data)
+
+ def __dump_file(self) -> None:
+ """Dump cache to a file"""
+ data: str = json_dumps(self.__cache)
+ self.__storage.write_text(data)
+
+ def __lock(self) -> None:
+ """Lock storage
+
+ Raises:
+ FileExistsError: Raised if a storage is already locked
+ """
+ if self.__locked:
+ raise FileExistsError(f'Access is already locked: {self.__storage}')
+ self.__locked = True
+
+ def __unlock(self) -> None:
+ """Unlock storage
+
+ Raises:
+ FileNotFoundError: Raised if a storage is already unlocked
+ """
+ if not self.__locked:
+ raise FileNotFoundError(f'Access is already unlocked: {self.__storage}')
+ self.__locked = False
+
+ def __storage_locked(self) -> bool:
+ """Check if a storage is locked
+
+ Returns:
+ bool: Lock status
+ """
+ if self.__lock_file.exists():
+ return True
+ return False
+
+ def delete(self, key: Any = None) -> None:
+ """Delete data from a storage or a full storage
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+ """
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage locked and delete operation cannot be performed: {self.__storage}'
+ )
+ if key:
+ if key not in self.__cache:
+ raise ValueError(
+ f'Object {key} does not exist in storage {self.__storage}'
+ )
+ del self.__cache[key]
+ else:
+ self.__cache = {}
+
+ def write(self, key: Any, value: Any) -> None:
+ # Check types first
+ self.__check_types(key)
+ self.__check_types(value)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and cannot be written: {self.__storage}'
+ )
+ # write a data to a cache
+ self.__lock()
+ self.__cache[key] = value
+ self.__unlock()
+
+ def read(self, key: Any, default: Any = None) -> Any:
+ """Read data from a storage
+
+ Args:
+ key (Any): key name
+ default (Any, optional): Value to return if a key does not exist. Defaults to None.
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+
+ Returns:
+ Any: Value to return
+ """
+ # Check types first
+ self.__check_types(key)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and it is not safe to read: {self.__storage}'
+ )
+ # read a data from cache
+ self.__lock()
+ data: Any | None = self.__cache.get(key, default)
+ self.__unlock()
+
+ return data
diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py
new file mode 100644
index 000000000..1ac004fbb
--- /dev/null
+++ b/python/vyos/vpp/control_host.py
@@ -0,0 +1,375 @@
+#
+# Copyright (C) 2023 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 re import fullmatch as re_fullmatch
+from subprocess import run
+from time import sleep
+
+from pyroute2 import IPRoute
+
+from vyos.vpp.utils import EthtoolGDrvinfo
+
+
+def pci_rescan(pci_addr: str = '') -> None:
+ """Rescan PCI device by removing it and rescan PCI bus
+
+ If PCI address is not defined - just rescan PCI bus
+
+ Args:
+ address (str, optional): PCI address of device. Defaults to ''.
+ """
+ device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove')
+ if pci_addr:
+ if device_file.exists():
+ device_file.write_text('1')
+ # wait 10 seconds max until device will be removed
+ attempts = 100
+ while device_file.exists() and attempts:
+ attempts -= 1
+ sleep(0.1)
+ if device_file.exists():
+ raise TimeoutError(
+ f'Timeout was reached for removing PCI device {pci_addr}'
+ )
+ else:
+ raise FileNotFoundError(f'PCI device {pci_addr} does not exist')
+ rescan_file = Path('/sys/bus/pci/rescan')
+ rescan_file.write_text('1')
+ if pci_addr:
+ # wait 10 seconds max until device will be installed
+ attempts = 100
+ while not device_file.exists() and attempts:
+ attempts -= 1
+ sleep(0.1)
+ if not device_file.exists():
+ raise TimeoutError(
+ f'Timeout was reached for installing PCI device {pci_addr}'
+ )
+
+
+def unbind_driver(bus_id: str, device_id: str) -> bool:
+ """Unbind a driver from a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+
+ Returns:
+ bool: True if a driver has been unbound, False otherwise
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ if not Path(f'{device_resolved}/driver').exists():
+ return False
+
+ Path(f'{device_resolved}/driver/unbind').write_text(device_id)
+ return True
+
+
+def probe_driver(bus_id: str, device_id: str) -> None:
+ """Probe driver for a device on a bus
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ """
+ Path(f'/sys/bus/{bus_id}/drivers_probe').write_text(device_id)
+
+
+def load_kernel_module(module_name: str) -> None:
+ """Load a kernel module
+
+ Args:
+ module_name (str): module name
+ """
+ # check if a module already loaded
+ if Path(f'/sys/module/{module_name}').exists():
+ return
+
+ # execute modprobe with the specified module name
+ run(['/usr/sbin/modprobe', '-q', module_name], check=True)
+
+
+def override_driver(bus_id: str, device_id: str, driver_name: str = '') -> None:
+ """Override a driver for a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ driver_name (str, optional): Kernel module (driver) name. Defaults to '' - clear an override.
+
+ Raises:
+ FileNotFoundError: A device does not support driver override
+ ChildProcessError: Failed to override a driver
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ # check if a device supports driver override
+ if not Path(f'{device_resolved}/driver_override').exists():
+ raise FileNotFoundError(f'{device_resolved} does not support driver override')
+
+ if driver_name:
+ load_kernel_module(driver_name)
+
+ unbind_driver(bus_id, device_id)
+
+ # vfio-pci needs special approach
+ if driver_name == 'vfio-pci':
+ vendor: str = Path(f'{device_resolved}/vendor').read_text()
+ device: str = Path(f'{device_resolved}/device').read_text()
+ Path('/sys/module/vfio_pci/drivers/pci:vfio-pci/new_id').write_text(
+ f'{vendor} {device}'
+ )
+
+ # override a driver
+ Path(f'{device_resolved}/driver_override').write_text(f'{driver_name}\n')
+
+ # probe a driver
+ probe_driver(bus_id, device_id)
+
+ # check the result
+ if not Path(f'{device_resolved}/driver').exists():
+ raise ChildProcessError(
+ f'Failed to override a driver to {driver_name} for {bus_id}, {device_id}'
+ )
+
+
+def get_bus_name(iface: str) -> str:
+ """Get bus name
+ Works for PCI, VMbus, maybe something else.
+ Does not work for Virtio and other virtual devices
+ (however, it does not seem we need this for such kind of devices).
+
+ Args:
+ iface (str): interface name
+
+ Returns:
+ str: bus name
+ """
+ device_resolved: Path = Path(f'/sys/class/net/{iface}/device').resolve()
+
+ # Iterate upwards until a `bus` directory is found
+ current_path: Path = device_resolved
+ while True:
+ # Check if a bus info is available
+ subsystem_path = Path(f'{current_path}/subsystem')
+ if subsystem_path.is_symlink():
+ # Read the link to determine the bus type
+ bus_path = subsystem_path.resolve()
+ # Check if the parent directory is a 'bus' directory in '/sys/bus/'
+ if bus_path.parent.name == 'bus':
+ # Return only the last name of the path, e.g., 'pci'
+ return bus_path.name
+
+ # Move up one directory level
+ current_path = current_path.parent
+ if current_path == Path('/sys'):
+ break # Stop if we reach the root of /sys without finding a bus type
+
+ return '' # Return None if no bus type was found
+
+
+def get_eth_name(dev_id: str) -> str:
+ """Find Ethernet interface name by PCI address or UUID
+
+ Args:
+ dev_id (str): PCI address or UUID
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: Ethernet interface name
+ """
+ # find all PCI devices with eth* names
+ net_devs: dict[str, str] = {}
+ net_devs_dir = Path('/sys/class/net')
+ regex_filter = r'^/sys/devices/pci[\w/:\.]+/(?P<pci_addr>\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?P<iface_name>eth\d+)$'
+ for dir in net_devs_dir.iterdir():
+ # PCI devices
+ real_dir: str = dir.resolve().as_posix()
+ re_obj = re_fullmatch(regex_filter, real_dir)
+ if re_obj:
+ iface_name: str = re_obj.group('iface_name')
+ iface_addr: str = re_obj.group('pci_addr')
+ net_devs.update({iface_addr: iface_name})
+ # UUID devices
+ else:
+ try:
+ bus_type: str = get_bus_name(dir.name)
+ iface_addr = EthtoolGDrvinfo(dir.name).bus_info_expand(bus_type)
+ net_devs.update({iface_addr: dir.name})
+ except FileNotFoundError:
+ pass
+
+ # match to provided PCI address or UUID and return a name if found
+ if dev_id in net_devs:
+ return net_devs[dev_id]
+ # raise error if device was not found
+ raise FileNotFoundError(
+ f'A device with ID {dev_id} not found in ethernet interfaces'
+ )
+
+
+def get_dev_id(iface: str) -> str:
+ """Get device ID by its interface name
+
+ Args:
+ iface (str): interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: device ID (PCI address or UUID)
+ """
+ try:
+ # Try to get details via ethtool first
+ ethtool_info = EthtoolGDrvinfo(iface)
+ # For devices represented by UUID we need to expand them
+ # to their full representation
+ if ethtool_info.driver == 'hv_netvsc':
+ return ethtool_info.bus_info_expand('vmbus')
+ return ethtool_info.bus_info
+ except Exception:
+ # raise error if a device ID was not found
+ raise FileNotFoundError(f'Cannot find device ID for interface {iface}')
+
+
+def get_eth_driver(iface: str) -> str:
+ """Find kernel module used for Ethernet interface
+
+ Args:
+ iface (str): Ethernet interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: kernel module name
+ """
+ iface_driver: str = ''
+ driver_dir = Path(f'/sys/class/net/{iface}/device/driver/module')
+ if not driver_dir.exists():
+ # raise error if device was not found
+ raise FileNotFoundError(f'PCI device {iface} not found in ethernet interfaces')
+
+ iface_driver: str = driver_dir.resolve().name
+ return iface_driver
+
+
+def unsafe_noiommu_mode(status: bool) -> None:
+ """Control unsafe_noiommu_mode parameter of vfio module
+
+ Args:
+ status (bool): Target status
+
+ Raises:
+ ChildProcessError: Raised if failed to set unsafe_noiommu_mode
+ """
+ param_path = Path('/sys/module/vfio/parameters/enable_unsafe_noiommu_mode')
+ current_status: str = param_path.read_text().strip()
+ target_status: str = 'Y' if status else 'N'
+ if current_status != target_status:
+ param_path.write_text(target_status)
+ if param_path.read_text().strip() != target_status:
+ raise ChildProcessError('Failed to set unsafe_noiommu_mode')
+
+
+def rename_iface(name_old: str, name_new: str) -> None:
+ """Rename interface
+
+ Args:
+ name_old (str): old name
+ name_new (str): new name
+ """
+ run(['ip', 'link', 'set', name_old, 'down'])
+ rename_cmd: list[str] = ['ip', 'link', 'set', name_old, 'name', name_new]
+ run(rename_cmd)
+
+
+def set_promisc(iface_name: str, operation: str) -> None:
+ """Set promisc mode for interface
+
+ Args:
+ iface_name (str): name of an interface
+ operation (str): operation (on, off)
+ """
+ run(['ip', 'link', 'set', iface_name, 'promisc', operation])
+
+
+def set_mtu(iface_name: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name (str): name of an interface
+ mtu (int): MTU
+ """
+ run(['ip', 'link', 'set', iface_name, 'mtu', str(mtu)])
+
+
+def get_eth_mac(iface_name: str) -> str:
+ """Get MAC address of an interface
+
+ Args:
+ iface_name (str): name of an interface
+
+ Raises:
+ FileNotFoundError: interface was not found
+
+ Returns:
+ str: MAC address
+ """
+ dev_addr_path = Path(f'/sys/class/net/{iface_name}/address')
+ if dev_addr_path.exists():
+ return dev_addr_path.read_text().strip()
+ else:
+ # raise error if device was not found
+ raise FileNotFoundError(f'Interface {iface_name} not found')
+
+
+def xdp_remove(iface_name: str) -> None:
+ """Remove XDP BPF program from an interfce
+
+ Args:
+ iface_name (str): name of an interface
+ """
+ run(['ip', 'link', 'set', iface_name, 'xdp', 'off'])
+
+
+def set_status(iface_name: str, status: str) -> None:
+ """Set interface status
+
+ Args:
+ iface_name (str): name of an interface
+ status (str): status - "up" or "down"
+ """
+ run(['ip', 'link', 'set', iface_name, status])
+
+
+def flush_ip(iface_name: str) -> None:
+ """Flush IP addresses from an interface
+
+ Args:
+ iface_name (str): name of an interface
+ """
+ iproute = IPRoute()
+ iproute.flush_addr(label=iface_name)
diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py
new file mode 100644
index 000000000..5959b68b5
--- /dev/null
+++ b/python/vyos/vpp/control_vpp.py
@@ -0,0 +1,488 @@
+#
+# Copyright (C) 2023-2025 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 collections.abc import Callable
+from functools import wraps
+from re import search as re_search, MULTILINE as re_M
+from systemd import journal
+from time import sleep
+from typing import TypeVar, ParamSpec, Literal
+
+from vpp_papi import VPPApiClient
+from vpp_papi import VPPIOError, VPPValueError
+
+# define types for static type checkers
+AnyType = TypeVar('AnyType')
+AnyParam = ParamSpec('AnyParam')
+
+
+class VPPControl:
+ """Control VPP network stack"""
+
+ class _Decorators:
+ """Decorators for VPPControl"""
+
+ @classmethod
+ def api_call(
+ cls, decorated_func: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """Check if API is connected before API call
+
+ Args:
+ decorated_func: function to decorate
+
+ Raises:
+ VPPIOError: Connection to API is not established
+ """
+
+ @wraps(decorated_func)
+ def api_safe_wrapper(
+ cls, *args: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ if not cls.connected:
+ raise VPPIOError(2, 'VPP API is not connected')
+ return decorated_func(cls, *args, **kwargs)
+
+ return api_safe_wrapper
+
+ @classmethod
+ def check_retval(
+ cls, decorated_func: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """Check retval from API response
+
+ Args:
+ decorated_func: function to decorate
+
+ Raises:
+ VPPValueError: raised when retval is not 0
+ """
+
+ @wraps(decorated_func)
+ def check_retval_wrapper(
+ cls, *args: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ return_value = decorated_func(cls, *args, **kwargs)
+ if not return_value.retval == 0:
+ raise VPPValueError(f'VPP API call failed: {return_value.retval}')
+ return return_value
+
+ return check_retval_wrapper
+
+ def __init__(self, attempts: int = 5, interval: int = 1000) -> None:
+ """Create VPP API connection
+
+ Args:
+ attempts (int, optional): attempts to connect. Defaults to 5.
+ interval (int, optional): interval between attempts in ms. Defaults to 1000.
+
+ Raises:
+ VPPIOError: Connection to API cannot be established
+ """
+ self.__vpp_api_client = VPPApiClient()
+ # connect with interval
+ while attempts:
+ try:
+ attempts -= 1
+ self.__vpp_api_client.connect('vpp-vyos')
+ break
+ except (ConnectionRefusedError, FileNotFoundError) as err:
+ error_message = f'VPP API connection timeout: {err}'
+ journal.send(error_message, priority=journal.LOG_ERR)
+ sleep(interval / 1000)
+ # raise exception if connection was not successful in the end
+ if not self.__vpp_api_client.transport.connected:
+ raise VPPIOError(2, 'Cannot connect to VPP API')
+
+ def __del__(self) -> None:
+ """Disconnect from VPP API (destructor)"""
+ self.disconnect()
+
+ def disconnect(self) -> None:
+ """Disconnect from VPP API"""
+ if self.__vpp_api_client.transport.connected:
+ self.__vpp_api_client.disconnect()
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def cli_cmd(self, command: str):
+ """Send raw CLI command
+
+ Args:
+ command (str): command to send
+
+ Returns:
+ vpp_papi.vpp_serializer.cli_inband_reply: CLI reply class
+ """
+ return self.__vpp_api_client.api.cli_inband(cmd=command)
+
+ @_Decorators.api_call
+ def get_mac(self, ifname: str) -> str:
+ """Find MAC address by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ str: MAC address
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.l2_address.mac_string
+ return ''
+
+ @_Decorators.api_call
+ def get_sw_if_index(self, ifname: str) -> int | None:
+ """Find interface index by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ int | None: Interface index or None (if was not fount)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.sw_if_index
+ return None
+
+ @_Decorators.api_call
+ def get_interface_name(self, index: int) -> str | None:
+ """Find interface name by interface index in VPP
+
+ Args:
+ index (int): interface index inside VPP
+
+ Returns:
+ str | None: Interface name or None (if was not found)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.sw_if_index == index:
+ return iface.interface_name
+ return None
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_pair_add(
+ self,
+ iface_name_vpp: str,
+ iface_name_kernel: str,
+ iface_type: Literal['tun', 'tap', ''] = '',
+ ) -> None:
+ """Create LCP interface pair between VPP and kernel
+
+ Args:
+ iface_name_vpp (str): interface name in VPP
+ iface_name_kernel (str): interface name in kernel
+ iface_type (Literal['tun', 'tap', ''], optional): Use explicit interface type in kernel. Defaults to ''.
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, bool | int | str] = {
+ 'is_add': True,
+ 'sw_if_index': iface_index,
+ 'host_if_name': iface_name_kernel,
+ }
+ if iface_type:
+ iface_type_resolve = {'tun': 1, 'tap': 0}
+ api_call_args['host_if_type'] = iface_type_resolve[iface_type]
+ return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_pair_del(self, iface_name_vpp: str, iface_name_kernel: str) -> None:
+ """Delete LCP interface pair between VPP and kernel
+
+ Args:
+ iface_name_vpp (str): interface name in VPP
+ iface_name_kernel (str): interface name in kernel
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2(
+ is_add=False, sw_if_index=iface_index, host_if_name=iface_name_kernel
+ )
+
+ @_Decorators.api_call
+ def lcp_pair_find(
+ self,
+ kernel_name: str = '',
+ vpp_index_hw: int | None = None,
+ vpp_index_kernel: int | None = None,
+ vpp_name_hw: str = '',
+ vpp_name_kernel: str = '',
+ ) -> dict[str, str | int] | None:
+ """Find LCP pair details
+
+ Args:
+ kernel_name (str, optional): Interface name in the kernel. Defaults to ''.
+ vpp_index_hw (int | None, optional): Interface index in VPP (hardware). Defaults to None.
+ vpp_index_kernel (int | None, optional): Interface index in VPP (kernel). Defaults to None.
+ vpp_name_hw (str, optional): Interface name in VPP (hardware). Defaults to ''.
+ vpp_name_kernel (str, optional): Interface name in VPP (to kernel). Defaults to ''.
+
+ Returns:
+ dict[str, str | int] | None: LCP pair details
+ """
+ filter_dict = {}
+ for filter_name, filter_value in locals().items():
+ if filter_value:
+ filter_dict[filter_name] = filter_value
+
+ # Get list of pairs
+ lcp_pairs = self.lcp_pairs_list()
+
+ # Check each pair
+ for pair in lcp_pairs:
+ pair_found = False
+ # For each item provided in function arguments
+ for filter_name, filter_value in filter_dict.items():
+ # Stop if filter value is not as in a current pair
+ if filter_name in pair and pair[filter_name] != filter_value:
+ pair_found = False
+ break
+ # Set flag to True and check the next filter value
+ pair_found = True
+
+ if pair_found:
+ return pair
+
+ return None
+
+ @_Decorators.api_call
+ def lcp_pairs_list(self) -> list[dict[str, str | int]]:
+ """List all LCP pairs
+
+ Returns:
+ list[dict[str, str | int]]: LCP pairs details
+ """
+ lcp_pairs_details = []
+
+ lcp_pairs = self.__vpp_api_client.api.lcp_itf_pair_get()[1]
+ vpp_ifaces = self.__vpp_api_client.api.sw_interface_dump()
+ for pair in lcp_pairs:
+ pair_details = {
+ 'kernel_name': pair.host_if_name,
+ 'vpp_index_hw': pair.phy_sw_if_index,
+ 'vpp_index_kernel': pair.host_sw_if_index,
+ }
+ for vpp_iface in vpp_ifaces:
+ if vpp_iface.sw_if_index == pair_details['vpp_index_hw']:
+ pair_details['vpp_name_hw'] = vpp_iface.interface_name
+ if vpp_iface.sw_if_index == pair_details['vpp_index_kernel']:
+ pair_details['vpp_name_kernel'] = vpp_iface.interface_name
+
+ lcp_pairs_details.append(pair_details)
+
+ return lcp_pairs_details
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_resync(self) -> None:
+ """Resynchronize objects between kernel and VPP via Netlink
+
+ This clears all routes in VPP configured by LCP and re-creates them
+ based on the current state of the kernel.
+ """
+ return self.__vpp_api_client.api.lcp_nl_resync()
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def iface_rxmode(self, iface_name: str, rx_mode: str) -> None:
+ """Set interface rx-mode in VPP
+
+ Args:
+ iface_name (str): interface name in VPP
+ rx_mode (str): mode (polling, interrupt, adaptive)
+ """
+ modes_dict: dict[str, int] = {'polling': 1, 'interrupt': 2, 'adaptive': 3}
+ if rx_mode not in modes_dict:
+ raise VPPValueError(f'Mode {rx_mode} is not known')
+ iface_index = self.get_sw_if_index(iface_name)
+ return self.__vpp_api_client.api.sw_interface_set_rx_mode(
+ sw_if_index=iface_index, mode=modes_dict[rx_mode]
+ )
+
+ @_Decorators.api_call
+ def get_pci_addr(self, ifname: str) -> str:
+ """Find PCI address of interface by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ str: PCI address
+ """
+ hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply
+
+ regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$'
+ re_obj = re_search(regex_filter, hw_info, re_M)
+
+ # return empty string if no interface or no PCI info was found
+ if not hw_info or not re_obj:
+ return ''
+
+ address = re_obj.groupdict().get('address', '')
+
+ # we need to modify address to match kernel style
+ # for example: 0000:06:14.00 -> 0000:06:14.0
+ address_chunks: list[str] = address.split('.')
+ address_normalized: str = f'{address_chunks[0]}.{int(address_chunks[1])}'
+
+ return address_normalized
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_create(
+ self,
+ host_if: str,
+ name: str,
+ rxq_num: int = 0,
+ rxq_size: int = 0,
+ txq_size: int = 0,
+ mode: Literal['auto', 'copy', 'zero-copy'] = 'auto',
+ flags: Literal['no_syscall_lock', ''] = '',
+ ) -> None:
+ """Create XDP interface
+
+ Args:
+ host_if (str): name of an interface in kernel
+ name (str): name of an interface in VPP
+ rxq_num (int, optional): Number of receive queues to connect to. Defaults to 0 (all).
+ rxq_size (int, optional): Size of receive queue. Defaults to 0.
+ txq_size (int, optional): Size of tranceive queue. Defaults to 0.
+ mode (Literal['auto', 'copy', 'zero-copy', optional): Zero-copy mode. Defaults to 'auto'.
+ flags (Literal['no_syscall_lock', ''], optional): Syscall lock mode. Defaults to ''.
+ """
+ api_call_args: dict[str, int | str] = {
+ 'host_if': host_if,
+ 'name': name,
+ 'rxq_num': rxq_num,
+ 'rxq_size': rxq_size,
+ 'txq_size': txq_size,
+ }
+ if mode != 'auto':
+ mode_resolve: dict[str, int] = {'auto': 0, 'copy': 1, 'zero-copy': 2}
+ api_call_args['mode'] = mode_resolve[mode]
+ if flags == 'no_systcall_lock':
+ api_call_args['flags'] = 1
+ return self.__vpp_api_client.api.af_xdp_create_v3(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_delete(self, iface_name_vpp: str) -> None:
+ """Delete XDP interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, int] = {'sw_if_index': iface_index}
+ return self.__vpp_api_client.api.af_xdp_delete(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mac(self, iface_name_vpp: str, mac_address: str) -> None:
+ """Set MAC address of an interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mac_address (str): MAC address
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {
+ 'sw_if_index': iface_index,
+ 'mac_address': mac_address,
+ }
+ return self.__vpp_api_client.api.sw_interface_set_mac_address(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mtu(self, iface_name_vpp: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mtu (int): MTU
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {'sw_if_index': iface_index, 'mtu': mtu}
+ return self.__vpp_api_client.api.hw_interface_set_mtu(**api_call_args)
+
+ @_Decorators.api_call
+ def get_sw_if_dev_type(self, ifname: str) -> int | None:
+ """Find interface device type by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ int | None: Interface device type or None (if was not fount)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.interface_dev_type
+ return None
+
+ @_Decorators.api_call
+ def enable_disable_nat44_forwarding(self, enable: bool) -> None:
+ """Enable/disable NAT44 forwarding
+
+ Args:
+ enable (bool): True if enable, False if disable
+ """
+ self.__vpp_api_client.api.nat44_forwarding_enable_disable(enable=enable)
+
+ @_Decorators.api_call
+ def set_nat44_session_limit(self, session_limit: int) -> None:
+ """Set NAT44 session limit
+
+ Args:
+ session_limit (int): Maximum number of sessions per thread
+ """
+ self.__vpp_api_client.api.nat44_set_session_limit(
+ session_limit=session_limit,
+ )
+
+ @_Decorators.api_call
+ def set_nat_workers(self, workers: int) -> None:
+ """Set NAT44 session limit
+
+ Args:
+ workers (int): Bitmask of workers list
+ """
+ self.__vpp_api_client.api.nat_set_workers(worker_mask=workers)
+
+ @property
+ def connected(self) -> bool:
+ """Check if VPP API is connected
+
+ Returns:
+ bool: True if connected, False if not
+ """
+ return self.__vpp_api_client.transport.connected
+
+ @property
+ @_Decorators.api_call
+ def api(self):
+ """Call API
+
+ Returns:
+ Callable[AnyParam, AnyType]: API functions
+ """
+ return self.__vpp_api_client.api
diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py
new file mode 100644
index 000000000..2048e07ca
--- /dev/null
+++ b/python/vyos/vpp/interface/__init__.py
@@ -0,0 +1,42 @@
+#
+# Copyright (C) 2023 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 .bond import BondInterface
+from .bridge import BridgeInterface
+from .ethernet import EthernetInterface
+from .geneve import GeneveInterface
+from .gre import GREInterface
+from .interface import Interface
+from .ipip import IPIPInterface
+from .loopback import LoopbackInterface
+from .vxlan import VXLANInterface
+from .wireguard import WireguardInterface
+from .xconnect import XconnectInterface
+
+__all__ = [
+ 'BondInterface',
+ 'BridgeInterface',
+ 'EthernetInterface',
+ 'GeneveInterface',
+ 'GREInterface',
+ 'Interface',
+ 'IPIPInterface',
+ 'LoopbackInterface',
+ 'VXLANInterface',
+ 'WireguardInterface',
+ 'XconnectInterface',
+]
diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py
new file mode 100644
index 000000000..6a96bce58
--- /dev/null
+++ b/python/vyos/vpp/interface/bond.py
@@ -0,0 +1,118 @@
+#
+# Copyright (C) 2023-2025 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.vpp.control_host import set_promisc
+from vyos.vpp.interface.interface import Interface
+
+
+class BondInterface(Interface):
+ def __init__(
+ self,
+ ifname,
+ mode: str = '',
+ load_balance: int = 0,
+ mac: str = '',
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('bond'))
+ self.ifname = f'BondEthernet{self.instance}'
+ self.mode = mode
+ self.load_balance = load_balance
+ self.mac = mac
+ self.kernel_interface = kernel_interface
+ self.state = state
+
+ def add(self):
+ """Create Bond interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/bonding/bond.api
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.add()
+ """
+ # Create interface 'bondX'
+ create_args = {
+ 'id': self.instance,
+ 'mode': self.mode,
+ 'lb': self.load_balance,
+ }
+ if self.mac:
+ create_args.update({'use_custom_mac': True, 'mac_address': self.mac})
+ self.vpp.api.bond_create2(**create_args)
+ if self.kernel_interface:
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+ # Set interface state
+ self.set_state(self.state)
+
+ def delete(self):
+ """Delete Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.delete()
+ """
+ bond_if_index = self.vpp.get_sw_if_index(self.ifname)
+ self.vpp.api.bond_delete(sw_if_index=bond_if_index)
+
+ def add_member(self, interface):
+ """Add member to Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.add_member(interface='eth0')
+ """
+ bond_if_index = self.vpp.get_sw_if_index(f'BondEthernet{self.instance}')
+ member_if_index = self.vpp.get_sw_if_index(interface)
+ member_if_type = self.vpp.get_sw_if_dev_type(interface)
+ self.vpp.api.bond_add_member(
+ bond_sw_if_index=bond_if_index, sw_if_index=member_if_index
+ )
+ self.vpp.api.sw_interface_set_promisc(
+ sw_if_index=member_if_index, promisc_on=True
+ )
+ if member_if_type == 'AF_XDP interface':
+ set_promisc(f'defunct_{interface}', 'on')
+
+ def detach_member(self, interface):
+ """Detach member from Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.detach_member(interface='eth0')
+ """
+ member_if_index = self.vpp.get_sw_if_index(interface)
+ self.vpp.api.bond_detach_member(sw_if_index=member_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py
new file mode 100644
index 000000000..5ebe46d45
--- /dev/null
+++ b/python/vyos/vpp/interface/bridge.py
@@ -0,0 +1,128 @@
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+
+
+class BridgeInterface:
+ def __init__(
+ self,
+ ifname: str,
+ flood: bool = True,
+ forward: bool = True,
+ learn: bool = True,
+ uu_flood: bool = True,
+ arp_term: bool = False,
+ ):
+ self.ifname = ifname
+ self.interface_suffix = int(self.ifname.replace('br', ''))
+ self.flood = flood
+ self.forward = forward
+ self.learn = learn
+ self.uu_flood = uu_flood
+ self.arp_term = arp_term
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Bridge interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/l2/l2.api
+
+ Bridge-domain 0 is reserved for the default bridge-domain.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add()
+ """
+ self.vpp.api.bridge_domain_add_del_v2(
+ is_add=True,
+ bd_id=self.interface_suffix,
+ flood=self.flood,
+ forward=self.forward,
+ learn=self.learn,
+ uu_flood=self.uu_flood,
+ arp_term=self.arp_term,
+ )
+
+ def delete(self):
+ """Delete Bridge interface
+
+ Bridge-members must be detached before deleting the bridge interface.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.delete()
+ """
+ self.vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix)
+
+ def add_member(self, member: str | int, port_type: int = 0):
+ """Add member to Bridge interface
+
+ Attaches a VPP interface to the Bridge interface specified by `interface_suffix`.
+ The `member` parameter can be either the name (str) or the index (int) of the network
+ VPP interface to be added as a member to the bridge.
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be added as a member to the bridge.
+ port_type: 0 - Normal port, 1 - BVI port
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add_member(member='eth0')
+ """
+ bridge_index = self.interface_suffix
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = self.vpp.get_sw_if_index(member)
+
+ return self.vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=port_type
+ )
+
+ def detach_member(self, member: str | int):
+ """Detach member from Bridge interface.
+ Bridge-domain 0 is reserved for the default bridge-domain.
+ The `member` parameter can be either the name (str) or the index (int)
+ of the network VPP interface
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be detached from the bridge.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.detach_member(member='eth0')
+ """
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = self.vpp.get_sw_if_index(member)
+
+ return self.vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=0, port_type=0
+ )
diff --git a/python/vyos/vpp/interface/ethernet.py b/python/vyos/vpp/interface/ethernet.py
new file mode 100644
index 000000000..368d94c7f
--- /dev/null
+++ b/python/vyos/vpp/interface/ethernet.py
@@ -0,0 +1,53 @@
+# VyOS implementation of VPP Ethernet interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+
+
+class EthernetInterface:
+ """Interface Ethernet"""
+
+ def __init__(self, ifname, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('eth'))
+ self.ifname = ifname
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ pass
+
+ def delete(self):
+ pass
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py
new file mode 100644
index 000000000..8260b1bfa
--- /dev/null
+++ b/python/vyos/vpp/interface/geneve.py
@@ -0,0 +1,90 @@
+# VyOS implementation of Geneve interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+
+
+def show():
+ """Show Geneve interface
+ Example:
+ from vyos.vpp.interface import geneve
+ geneve.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.geneve_tunnel_dump()
+
+
+class GeneveInterface:
+ def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('geneve'))
+ self.ifname = f'geneve_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Geneve interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/geneve/geneve.api
+
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='geneve25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.add()
+ """
+ return self.vpp.api.geneve_add_del_tunnel2(
+ is_add=True,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def delete(self):
+ """Delete Geneve interface
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.delete()
+ """
+ return self.vpp.api.geneve_add_del_tunnel2(
+ is_add=False,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py
new file mode 100644
index 000000000..5bef280c6
--- /dev/null
+++ b/python/vyos/vpp/interface/gre.py
@@ -0,0 +1,140 @@
+# VyOS implementation of VPP GRE interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show GRE interface
+ Example:
+ from vyos.vpp.interface import gre
+ gre.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.gre_tunnel_dump()
+
+
+class GREInterface(Interface):
+ """
+ Class representing a GRE (Generic Routing Encapsulation) interface.
+
+ Attributes:
+ ifname (str): The interface name.
+ source_address (str): The source IP address for the GRE tunnel.
+ remote (str): The remote IP address for the GRE tunnel.
+ tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'.
+ mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'.
+ kernel_interface (str): The associated kernel interface. Defaults to an empty string.
+ instance (int): The instance number derived from the interface name.
+ vpp (VPPControl): An instance of the VPPControl class for interacting with the VPP API.
+ """
+
+ # Mapping of tunnel types https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api#L25-L35
+ TUNNEL_TYPE_MAP = {
+ 'l3': 0,
+ 'teb': 1,
+ 'erspan': 2,
+ }
+
+ MODE_MAP = {
+ 'point-to-point': 0,
+ 'point-to-multipoint': 1,
+ }
+
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ tunnel_type: str = 'l3',
+ mode: str = 'point-to-point',
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ """
+ Initialize a GREInterface instance.
+
+ Args:
+ ifname (str): The interface name.
+ source_address (str): The source IP address for the GRE tunnel.
+ remote (str): The remote IP address for the GRE tunnel.
+ mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'.
+ tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'.
+ kernel_interface (str): The associated kernel interface. Defaults to an empty string.
+ state (str): The state of the interface. Defaults to 'up'.
+ """
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('gre'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type]
+ self.mode = self.MODE_MAP[mode]
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create GRE interface
+ https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25', tunnel_type='l3')
+ a.add()
+ """
+ self.vpp.api.gre_tunnel_add_del(
+ is_add=True,
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ 'mode': self.mode,
+ 'type': self.tunnel_type,
+ },
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete GRE interface
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.delete()
+ """
+ return self.vpp.api.gre_tunnel_add_del(
+ is_add=False, tunnel={'src': self.src_address, 'dst': self.dst_address}
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py
new file mode 100644
index 000000000..33dc779c2
--- /dev/null
+++ b/python/vyos/vpp/interface/interface.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2025 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.vpp import VPPControl
+
+
+class Interface:
+ def __init__(self, ifname):
+ self.ifname = ifname
+ self.vpp = VPPControl()
+
+ def set_state(self, state: str):
+ """Set interface state to UP or DOWN
+ Args:
+ state (str): The state of the interface. Options are 'up' and 'down'.
+ Example:
+ from vyos.vpp.interface import Interface
+ a = Interface(ifname='eth0')
+ a.set_state(state='up')
+ """
+ if state not in ['up', 'down']:
+ raise ValueError(f"Invalid state: {state}")
+ state_flag = 1 if state == 'up' else 0
+ if_index = self.vpp.get_sw_if_index(self.ifname)
+ self.vpp.api.sw_interface_set_flags(sw_if_index=if_index, flags=state_flag)
+
+ def get_state(self):
+ """Get interface state
+ Example:
+ from vyos.vpp.interface import Interface
+ a = Interface(ifname='eth0')
+ a.get_state()
+ """
+ if_index = self.vpp.get_sw_if_index(self.ifname)
+ return self.vpp.api.sw_interface_dump(sw_if_index=if_index)[0]['flags']
diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py
new file mode 100644
index 000000000..4779a8292
--- /dev/null
+++ b/python/vyos/vpp/interface/ipip.py
@@ -0,0 +1,94 @@
+# VyOS implementation of VPP IPIP interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show IPIP interface
+ Example:
+ from vyos.vpp.interface import ipip
+ ipip.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.ipip_tunnel_dump()
+
+
+class IPIPInterface(Interface):
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('ipip'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create IPIP interface
+ https://github.com/FDio/vpp/blob/stable/2310/src/vnet/ipip/ipip.api
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.add()
+ """
+ self.vpp.api.ipip_add_tunnel(
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ },
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete IPIP interface
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.delete()
+ """
+ ipip_if_index = self.vpp.get_sw_if_index(f'ipip{self.instance}')
+ return self.vpp.api.ipip_del_tunnel(sw_if_index=ipip_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py
new file mode 100644
index 000000000..d417f627d
--- /dev/null
+++ b/python/vyos/vpp/interface/loopback.py
@@ -0,0 +1,72 @@
+# VyOS implementation of VPP Loopback interface
+#
+# Copyright (C) 2023-2025 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.vpp.interface.interface import Interface
+
+
+class LoopbackInterface(Interface):
+ """Interface Loopback"""
+
+ def __init__(self, ifname, kernel_interface: str = '', state: str = 'up'):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('lo'))
+ self.ifname = f'loop{self.instance}'
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create Loopback interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/interface.api
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.add()
+ """
+ self.vpp.api.create_loopback_instance(
+ is_specified=True, user_instance=self.instance
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete Loopback interface
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.delete()
+ """
+ loopback_if_index = self.vpp.get_sw_if_index(f'loop{self.instance}')
+ return self.vpp.api.delete_loopback(sw_if_index=loopback_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py
new file mode 100644
index 000000000..eacc901dc
--- /dev/null
+++ b/python/vyos/vpp/interface/vxlan.py
@@ -0,0 +1,106 @@
+# VyOS implementation of VPP VXLAN interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show VXLAN interface
+ Example:
+ from vyos.vpp.interface import vxlan
+ vxlan.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.vxlan_tunnel_dump()
+
+
+class VXLANInterface(Interface):
+ """Interface VXLAN"""
+
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ vni,
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('vxlan'))
+ self.ifname = f'vxlan_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create VXLAN interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/vxlan/vxlan.api
+
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.add()
+ """
+ self.vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=True,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ instance=self.instance,
+ decap_next_index=1,
+ is_l3=False,
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete VXLAN interface
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.delete()
+ """
+ return self.vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=False,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ is_l3=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23, kernel_interface='vpptap10')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/wireguard.py b/python/vyos/vpp/interface/wireguard.py
new file mode 100644
index 000000000..649ed5b05
--- /dev/null
+++ b/python/vyos/vpp/interface/wireguard.py
@@ -0,0 +1,55 @@
+# VyOS implementation of VPP Wireguard interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+
+
+class WireguardInterface:
+ """Interface Wireguard"""
+
+ def __init__(self, ifname, listen_port=51820, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('wg'))
+ self.ifname = ifname
+ self.listen_port = listen_port
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Wireguard interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/wireguard/wireguard.api
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5', listen_port=51820, generate_key=True)
+ a.add()
+ """
+ self.vpp.api.wireguard_interface_create(
+ generate_key=True,
+ interface={'user_instance': self.instance, 'listen_port': self.listen_port},
+ )
+ if self.kernel_interface:
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def delete(self):
+ """Delete Wireguard interface
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5')
+ a.delete()
+ """
+ wg_if_index = self.vpp.get_sw_if_index(f'wg{self.instance}')
+ return self.vpp.api.wireguard_interface_delete(sw_if_index=wg_if_index)
diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py
new file mode 100644
index 000000000..badece6e8
--- /dev/null
+++ b/python/vyos/vpp/interface/xconnect.py
@@ -0,0 +1,95 @@
+# VyOS implementation of VPP bridge interface
+#
+# Copyright (C) 2023-2025 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.vpp import VPPControl
+from vyos.vpp.utils import iftunnel_transform
+
+
+class XconnectInterface:
+ def __init__(
+ self,
+ ifname: str,
+ members: list = [],
+ state: str = 'up',
+ ):
+ self.ifname = ifname
+ self.members = members
+ self.vpp = VPPControl()
+ self.initial_state = state
+
+ def add_l2_xconnect(self):
+ """Add l2 cross connect
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.add_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = self.vpp.get_sw_if_index(first_member)
+ member_second_if_index = self.vpp.get_sw_if_index(second_member)
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=True,
+ )
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=True,
+ )
+
+ def del_l2_xconnect(self):
+ """Move l2 cross connect member to mode l3 (delte xconnect)
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.del_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = self.vpp.get_sw_if_index(first_member)
+ member_second_if_index = self.vpp.get_sw_if_index(second_member)
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=False,
+ )
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=False,
+ )
diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py
new file mode 100644
index 000000000..16685c9cc
--- /dev/null
+++ b/python/vyos/vpp/nat/__init__.py
@@ -0,0 +1,4 @@
+from .nat44 import Nat44
+from .det44 import Det44
+
+__all__ = ['Nat44', 'Det44']
diff --git a/python/vyos/vpp/nat/det44.py b/python/vyos/vpp/nat/det44.py
new file mode 100644
index 000000000..1046d7cf9
--- /dev/null
+++ b/python/vyos/vpp/nat/det44.py
@@ -0,0 +1,106 @@
+#
+# Copyright (C) 2025 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.vpp import VPPControl
+
+
+class Det44:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def enable_det44_plugin(self):
+ """Enable DET44 plugin
+ Example:
+ from vyos.vpp.nat import Det44
+ det44 = Det44()
+ det44.enable_det44_plugin()
+ https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/det44/det44.api
+ """
+ self.vpp.api.det44_plugin_enable_disable(enable=True)
+
+ def disable_det44_plugin(self):
+ """Disable DET44 plugin"""
+ self.vpp.api.det44_plugin_enable_disable(enable=False)
+
+ def add_det44_interface_outside(self, interface_out):
+ """Add DET44 outside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_inside=False,
+ is_add=True,
+ )
+
+ def delete_det44_interface_outside(self, interface_out):
+ """Delete DET44 outside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_inside=False,
+ is_add=False,
+ )
+
+ def add_det44_interface_inside(self, interface_in):
+ """Add DET44 inside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_inside=True,
+ is_add=True,
+ )
+
+ def delete_det44_interface_inside(self, interface_in):
+ """Delete DET44 inside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_inside=True,
+ is_add=False,
+ )
+
+ def add_det44_mapping(self, in_addr, in_plen, out_addr, out_plen):
+ """Add DET44 mapping"""
+ self.vpp.api.det44_add_del_map(
+ in_addr=in_addr,
+ in_plen=in_plen,
+ out_addr=out_addr,
+ out_plen=out_plen,
+ is_add=True,
+ )
+
+ def delete_det44_mapping(self, in_addr, in_plen, out_addr, out_plen):
+ """Delete DET44 mapping"""
+ self.vpp.api.det44_add_del_map(
+ in_addr=in_addr,
+ in_plen=in_plen,
+ out_addr=out_addr,
+ out_plen=out_plen,
+ is_add=False,
+ )
+
+ def set_det44_timeouts(
+ self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int
+ ):
+ """Set DET44 timeouts
+ Args:
+ tcp_established (int): TCP established timeout
+ tcp_transitory (int): TCP transitory timeout
+ udp (int): UDP timeout
+ icmp (int): ICMP timeout
+ """
+ self.vpp.api.det44_set_timeouts(
+ icmp=icmp,
+ udp=udp,
+ tcp_established=tcp_established,
+ tcp_transitory=tcp_transitory,
+ )
diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py
new file mode 100644
index 000000000..6b02688aa
--- /dev/null
+++ b/python/vyos/vpp/nat/nat44.py
@@ -0,0 +1,229 @@
+#
+# Copyright (C) 2025 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.vpp import VPPControl
+
+
+# NAT44 flags
+NAT_IS_NONE = 0x00
+NAT_IS_TWICE_NAT = 0x01
+NAT_IS_SELF_TWICE_NAT = 0x02
+NAT_IS_OUT2IN_ONLY = 0x04
+NAT_IS_ADDR_ONLY = 0x08
+NAT_IS_OUTSIDE = 0x10
+NAT_IS_INSIDE = 0x20
+
+NO_INTERFACE = 0xFFFFFFFF
+
+
+class Nat44:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def enable_nat44_ed(self):
+ """Enable NAT44 endpoint dependent plugin
+ Example:
+ from vyos.vpp.nat import Nat44
+ nat44 = Nat44()
+ nat44.enable_nat44_ed()
+ https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/nat44-ed/nat44_ed.api
+ """
+ self.vpp.api.nat44_ed_plugin_enable_disable(enable=True)
+
+ def disable_nat44_ed(self):
+ """Disable NAT44 endpoint dependent plugin"""
+ self.vpp.api.nat44_ed_plugin_enable_disable(enable=False)
+
+ def enable_nat44_ei(self):
+ """Enable NAT44 endpoint independent plugin
+ Example:
+ from vyos.vpp.nat import Nat44
+ nat44 = Nat44()
+ nat44.enable_nat44_ei()
+ """
+ self.vpp.api.nat44_ei_plugin_enable_disable(enable=True)
+
+ def add_nat44_interface_inside(self, interface_in):
+ """Add NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_INSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_inside(self, interface_in):
+ """Delete NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_INSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_add=False,
+ )
+
+ def add_nat44_interface_outside(self, interface_out):
+ """Add NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_OUTSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_outside(self, interface_out):
+ """Delete NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_OUTSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_add=False,
+ )
+
+ def add_nat44_address_range(self, addresses, twice_nat):
+ """Add NAT44 address range"""
+ if '-' not in addresses:
+ first_ip_address = last_ip_address = addresses
+ else:
+ first_ip_address, last_ip_address = addresses.split('-')
+ self.vpp.api.nat44_add_del_address_range(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ first_ip_address=first_ip_address,
+ last_ip_address=last_ip_address,
+ is_add=True,
+ )
+
+ def delete_nat44_address_range(self, addresses, twice_nat):
+ """Delete NAT44 address range"""
+ if '-' not in addresses:
+ first_ip_address = last_ip_address = addresses
+ else:
+ first_ip_address, last_ip_address = addresses.split('-')
+ self.vpp.api.nat44_add_del_address_range(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ first_ip_address=first_ip_address,
+ last_ip_address=last_ip_address,
+ is_add=False,
+ )
+
+ def add_nat44_interface_address(self, interface, twice_nat):
+ """Add NAT44 interface address"""
+ self.vpp.api.nat44_add_del_interface_addr(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_address(self, interface, twice_nat):
+ """Delete NAT44 interface address"""
+ self.vpp.api.nat44_add_del_interface_addr(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=False,
+ )
+
+ def add_nat44_static_mapping(
+ self,
+ local_ip,
+ external_ip,
+ local_port,
+ external_port,
+ protocol,
+ twice_nat,
+ self_twice_nat,
+ out2in,
+ pool_ip,
+ ):
+ """Add NAT44 static mapping"""
+ flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE
+ flags |= NAT_IS_TWICE_NAT if twice_nat else 0
+ flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0
+ flags |= NAT_IS_OUT2IN_ONLY if out2in else 0
+ self.vpp.api.nat44_add_del_static_mapping_v2(
+ local_ip_address=local_ip,
+ external_ip_address=external_ip,
+ protocol=protocol,
+ local_port=local_port,
+ external_port=external_port,
+ match_pool=True if pool_ip else False,
+ pool_ip_address=pool_ip if pool_ip else '',
+ flags=flags,
+ is_add=True,
+ )
+
+ def delete_nat44_static_mapping(
+ self,
+ local_ip,
+ external_ip,
+ local_port,
+ external_port,
+ protocol,
+ twice_nat,
+ self_twice_nat,
+ out2in,
+ pool_ip,
+ ):
+ """Delete NAT44 static mapping"""
+ flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE
+ flags |= NAT_IS_TWICE_NAT if twice_nat else 0
+ flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0
+ flags |= NAT_IS_OUT2IN_ONLY if out2in else 0
+ self.vpp.api.nat44_add_del_static_mapping_v2(
+ local_ip_address=local_ip,
+ external_ip_address=external_ip,
+ protocol=protocol,
+ local_port=local_port,
+ external_port=external_port,
+ match_pool=True if pool_ip else False,
+ pool_ip_address=pool_ip if pool_ip else '',
+ flags=flags,
+ is_add=False,
+ )
+
+ def add_nat44_identity_mapping(self, ip_address, protocol, port, interface):
+ """Add NAT44 identity mapping"""
+ self.vpp.api.nat44_add_del_identity_mapping(
+ ip_address=ip_address,
+ protocol=protocol,
+ port=port,
+ sw_if_index=(
+ self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE
+ ),
+ flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE,
+ is_add=True,
+ )
+
+ def delete_nat44_identity_mapping(self, ip_address, protocol, port, interface):
+ """Delete NAT44 identity mapping"""
+ self.vpp.api.nat44_add_del_identity_mapping(
+ ip_address=ip_address,
+ protocol=protocol,
+ port=port,
+ sw_if_index=(
+ self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE
+ ),
+ flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE,
+ is_add=False,
+ )
+
+ def set_nat_timeouts(self, icmp, udp, tcp_established, tcp_transitory):
+ """Set NAT timeouts"""
+ self.vpp.api.nat_set_timeouts(
+ icmp=icmp,
+ udp=udp,
+ tcp_established=tcp_established,
+ tcp_transitory=tcp_transitory,
+ )
+
+ def enable_ipfix(self):
+ """Enable NAT44 IPFIX logging"""
+ self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True)
diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py
new file mode 100644
index 000000000..fe08a98b8
--- /dev/null
+++ b/python/vyos/vpp/utils.py
@@ -0,0 +1,412 @@
+#
+# Copyright (C) 2023 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 ctypes
+import os
+import socket
+from fcntl import ioctl
+from pathlib import Path
+from struct import pack
+
+
+mem_shift = {'K': 10, 'k': 10, 'M': 20, 'm': 20, 'G': 30, 'g': 30}
+
+
+def iftunnel_transform(iface: str) -> str:
+ """Transform interface name from `xxxNN` to `xxx_tunnelNN`
+
+ Args:
+ iface (str): original interface name
+
+ Raises:
+ ValueError: Raised if an interface name does not start with a alpha and ends with decimal digit
+
+ Returns:
+ str: Transformed interface name
+ """
+ # Check format
+ if not iface[0].isascii() or not iface[-1].isdecimal():
+ raise ValueError(f'Wrong interface name format: {iface}')
+ # Transform
+ iface_type: str = iface.rstrip('0123456789')
+ iface_num: str = iface.removeprefix(iface_type)
+ # Return transformed
+ return f'{iface_type}_tunnel{iface_num}'
+
+
+def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]:
+ """List of all VPP interfaces (CLI names)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[str]: list of interfaces
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ vpp_ifaces: list[str] = []
+
+ # Get a list of Ethernet interfaces
+ for iface in config.get('settings', {}).get('interface', {}).keys():
+ vpp_ifaces.append(iface)
+
+ # Get a list of VPP interfaces
+ for iface_type in config.get('interfaces', {}).keys():
+ for iface in config.get('interfaces', {}).get(iface_type, {}).keys():
+ vpp_ifaces.append(iface)
+
+ return vpp_ifaces
+
+
+def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]:
+ """List of all VPP Ethernet interfaces with VIFs
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+
+ Returns:
+ list[str]: list of interfaces
+ """
+ from vyos.configdict import get_interface_dict
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ ifaces: list[str] = []
+
+ # Get a list of Ethernet interfaces
+ for iface in config.get('settings', {}).get('interface', {}).keys():
+ ifaces.append(iface)
+
+ # Add Ethernet interfaces with VIFs
+ for iface in ifaces:
+ _, iface_config = get_interface_dict(
+ config_instance, ['interfaces', 'ethernet'], ifname=iface
+ )
+ ifaces.extend([f'{iface}.{vif}' for vif in iface_config.get('vif', {})])
+ ifaces.extend([f'{iface}.{vif_s}' for vif_s in iface_config.get('vif_s', {})])
+
+ return ifaces
+
+
+def vpp_ifaces_list(vpp_api) -> list[dict]:
+ """List interfaces in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+
+ Returns:
+ list[dict]: list of dictionaries with interfaces
+ """
+ ifaces_list: list[dict] = []
+ sw_ifaces_dump = vpp_api.sw_interface_dump()
+ while sw_ifaces_dump:
+ iface_details = sw_ifaces_dump.pop()
+ ifaces_list.append(iface_details._asdict())
+
+ return ifaces_list
+
+
+def vpp_ip_addresses_by_index(vpp_api, index: str) -> list[str]:
+ """List of IP addresses for interface by its index in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+ index (str): interface index in vpp
+
+ Returns:
+ list[str]: list of IP addresses
+ """
+ ip_addresses_list: list[dict] = []
+ ip_address_dump = vpp_api.ip_address_dump(sw_if_index=index)
+ while ip_address_dump:
+ ip_address_details = ip_address_dump.pop()
+ ip_addresses_list.append(str(ip_address_details._asdict().get('prefix')))
+ return ip_addresses_list
+
+
+def vpp_ifaces_stats(
+ iface_name: str = '',
+) -> dict[str, dict[str, int | dict[str, int]]]:
+ from re import compile as re_compile
+ from vpp_papi import vpp_stats
+
+ def total_value(val_list: vpp_stats.SimpleList) -> int | dict[str, int]:
+ """Helper for aggregation stats from multiple workers
+
+ Args:
+ val_list (vpp_stats.SimpleList): list of stats for all workers
+
+ Returns:
+ int | dict[str, int]: Summary stats
+ """
+ # if all items are int return their sum
+ if all(isinstance(value, int) for value in val_list):
+ return sum(val_list)
+ # if all items are tuple
+ if all(isinstance(value, tuple) for value in val_list):
+ combined_stats = {}
+ # process individual workers
+ for worker_stats in val_list:
+ packets, octets = worker_stats
+ sum_packets = combined_stats.get('packets', 0) + packets
+ sum_octets = combined_stats.get('octets', 0) + octets
+ combined_stats = {'packets': sum_packets, 'bytes': sum_octets}
+ return combined_stats
+
+ # items are something unknown, just return what we received
+ return val_list
+
+ stats = vpp_stats.VPPStats()
+
+ ifaces_stats: dict[str, dict[str, int | dict[str, int]]] = {}
+
+ # prepare parser for stats output
+ regex_parser = re_compile(r'^/interfaces/(?P<iface>[^/]+)/(?P<param>[^/]+)')
+ # get list of available stats and dump them
+ stats_list: list[str] = stats.ls([f'^/interfaces/{iface_name}'])
+ stats_dump: list[dict[str, int]] = stats.dump(stats_list)
+
+ # parse outputs and convert it to a dictionary
+ for stats_key, stats_value in stats_dump.items():
+ parsed_key = regex_parser.search(stats_key).groupdict()
+ iface_name = parsed_key['iface']
+ param = parsed_key['param']
+ stats_item = {param: total_value(stats_value)}
+ if iface_name in ifaces_stats:
+ ifaces_stats[iface_name].update(stats_item)
+ else:
+ ifaces_stats[iface_name] = stats_item
+
+ return ifaces_stats
+
+
+def cli_ifaces_lcp_kernel_list(
+ config_instance, mode: str = 'candidate'
+) -> list[tuple[str, str]]:
+ """List of all VPP kernel-interfaces (CLI names, attached VPP interfaces)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[tuple[str, str]]: list of interfaces ([(vpp_iface, kernel_iface)])
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ lcp_kernel_ifaces: list[tuple[str, str]] = []
+
+ # Get a list with kernel interfaces
+ for ifaces_list in config.get('interfaces', {}).values():
+ for iface_name, iface_settings in ifaces_list.items():
+ if 'kernel_interface' in iface_settings:
+ lcp_kernel_ifaces.append(
+ (iface_name, iface_settings['kernel_interface'])
+ )
+
+ return lcp_kernel_ifaces
+
+
+def get_default_hugepage_size() -> int:
+ """
+ Retrieve the system's default huge page size.
+ :return: The default huge page size in bytes.
+ """
+ page_size = None
+ try:
+ # default huge page size
+ memfd = os.memfd_create('tmp', os.MFD_HUGETLB)
+ st = os.fstat(memfd)
+ page_size = st.st_blksize
+ os.close(memfd)
+ except OSError:
+ pass
+
+ return page_size
+
+
+def get_default_page_size() -> int:
+ """
+ Retrieve the system's default page size.
+ :return: The default page size in bytes.
+ """
+ return os.sysconf('SC_PAGESIZE')
+
+
+def get_hugepage_sizes() -> list[int]:
+ """
+ Retrieve all available huge page sizes from the system.
+ :return: A list of huge page sizes in bytes.
+ """
+ huge_sizes = []
+ path = '/sys/kernel/mm/hugepages/'
+ try:
+ entries = os.listdir(path)
+ for entry in entries:
+ if entry.startswith('hugepages-'):
+ try:
+ size_kb = int(entry.replace('hugepages-', '').replace('kB', ''))
+ huge_sizes.append(size_kb << 10) # Convert KB to bytes
+ except ValueError:
+ pass
+ except FileNotFoundError:
+ pass
+
+ return huge_sizes
+
+
+def human_memory_to_bytes(value: str) -> int:
+ """
+ Convert a human-readable vpp memory format (K, M, G) to a byte value.
+
+ :param value: The string memory size in vpp human-readable format.
+ :return: A int representing the value.
+ """
+ try:
+ return int(value)
+ except ValueError:
+ return int(value[:-1]) << mem_shift[value[-1]]
+
+
+def bytes_to_human_memory(value: int, unit: str) -> str | None:
+ """
+ Convert a byte value to a human-readable format (K, M, G).
+
+ :param value: The size in bytes.
+ :param unit: The unit to convert to ('K', 'M', 'G').
+ :return: A string representing the value in the specified unit, or None if zero.
+ """
+ val = value >> mem_shift[unit]
+ return f'{val}{unit}' if val else None
+
+
+def human_page_memory_to_bytes(value: str) -> int:
+ """
+ Convert a human-readable vpp page size format to a byte value.
+
+ :param value: The string memory size in vpp human-readable format.
+ :return: A int representing the value.
+ """
+ default = {
+ 'default': get_default_page_size,
+ 'default-hugepage': get_default_hugepage_size,
+ }
+ try:
+ return default[value]()
+ except KeyError:
+ return human_memory_to_bytes(value)
+
+
+class EthtoolGDrvinfo:
+ """Return interface details like `ethtol -i` does"""
+
+ # TODO
+ # this probably need to be replaced with a code generator
+ # like ctypeslib or C extension
+ class EthtoolDrvinfo(ctypes.Structure):
+ _fields_ = [
+ ('cmd', ctypes.c_uint32),
+ ('driver', ctypes.c_char * 32), # Driver short name
+ ('version', ctypes.c_char * 32), # Driver version
+ ('fw_version', ctypes.c_char * 32), # Firmware version
+ # Be careful: bus info can be longer than 32 chars and thus truncated
+ ('bus_info', ctypes.c_char * 32), # Bus info.
+ ('erom_version', ctypes.c_char * 32), # Expansion ROM version
+ ('reserved2', ctypes.c_char * 12), # Reserved for future use
+ ('n_priv_flags', ctypes.c_uint32), # Number of private flags
+ ('n_stats', ctypes.c_uint32), # Number of U64 stats
+ ('testinfo_len', ctypes.c_uint32), # Test info length
+ ('eedump_len', ctypes.c_uint32), # EEPROM dump length
+ ('regdump_len', ctypes.c_uint32), # Register dump length
+ ]
+
+ def __init__(self, iface: str):
+ # Constants for ethtool
+ SIOCETHTOOL = 0x8946 # pretend to be ethtool
+ ETHTOOL_GDRVINFO = 0x00000003 # Command to get driver info
+
+ # Create a dummy socket
+ sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # Prepare the request for getting driver info
+ drvinfo = self.EthtoolDrvinfo(cmd=ETHTOOL_GDRVINFO)
+ ifreq: bytes = pack('16sP', iface.encode('utf-8'), ctypes.addressof(drvinfo))
+
+ # Make an ioctl call to get the driver info
+ try:
+ ioctl(sockfd, SIOCETHTOOL, ifreq)
+ except OSError:
+ raise FileNotFoundError(f'There is no Ethernet device: {iface}')
+
+ # Close the socket
+ sockfd.close()
+
+ # save the information
+ self.driver: str = drvinfo.driver.decode('utf-8').strip('\x00')
+ self.version: str = drvinfo.version.decode('utf-8').strip('\x00')
+ self.fw_version: str = drvinfo.fw_version.decode('utf-8').strip('\x00')
+ self.bus_info: str = drvinfo.bus_info.decode('utf-8').strip('\x00')
+ self.erom_version: str = drvinfo.erom_version.decode('utf-8').strip('\x00')
+ self.reserved2: str = drvinfo.reserved2.decode('utf-8').strip('\x00')
+ self.n_priv_flags: int = drvinfo.n_priv_flags
+ self.testinfo_len: int = drvinfo.testinfo_len
+ self.eedump_len: int = drvinfo.eedump_len
+ self.regdump_len: int = drvinfo.regdump_len
+
+ def bus_info_expand(self, bus_name: str) -> str:
+ bus_path = Path(f'/sys/bus/{bus_name}/devices').glob(f'{self.bus_info}*')
+ dev_ids = list(bus_path)
+ if not dev_ids:
+ raise FileNotFoundError(
+ f'No matching IDs on the bus: {self.bus_info} on {bus_name}'
+ )
+ if len(dev_ids) > 1:
+ raise FileNotFoundError(
+ f'There are more than one matching IDs on the bus: {dev_ids} on {bus_name}'
+ )
+ return dev_ids[0].name
diff --git a/smoketest/config-tests/vpp b/smoketest/config-tests/vpp
new file mode 100644
index 000000000..e42568965
--- /dev/null
+++ b/smoketest/config-tests/vpp
@@ -0,0 +1,49 @@
+set interfaces ethernet eth0 description 'WAN'
+set interfaces ethernet eth0 offload gro
+set interfaces ethernet eth0 offload gso
+set interfaces ethernet eth0 offload sg
+set interfaces ethernet eth0 offload tso
+set interfaces ethernet eth1 address '192.0.2.1/30'
+set interfaces ethernet eth1 address '192.0.2.21/30'
+set interfaces ethernet eth1 description 'WAN'
+set interfaces ethernet eth2
+set interfaces ethernet eth3
+set interfaces ethernet eth4 description 'Bonding'
+set interfaces loopback lo
+set system config-management commit-revisions '100'
+set system console device ttyS0 speed '115200'
+set system host-name 'r16'
+set system login user vyos authentication encrypted-password '$6$rounds=656000$FZBlGpWsmSrV2Rvq$YPNAPtk4k6u99FAMxR6cw4DUPCgOomwCgRZRSO5rAoJK8RlMSCkVAFVF3ozL/3mZMfxcuCnwvd5HX6f9V5KzO.'
+set system name-server '203.0.113.1'
+set system option kernel memory hugepage-size 2M hugepage-count '2048'
+set system option time-format '24-hour'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
+set vpp interfaces bonding bond0 hash-policy 'layer2+3'
+set vpp interfaces bonding bond0 member interface 'eth4'
+set vpp interfaces bonding bond0 mode '802.3ad'
+set vpp interfaces gre gre10 description 'Layer-3'
+set vpp interfaces gre gre10 mode 'point-to-point'
+set vpp interfaces gre gre10 remote '192.0.2.2'
+set vpp interfaces gre gre10 source-address '192.0.2.1'
+set vpp interfaces gre gre10 tunnel-type 'l3'
+set vpp interfaces gre gre11 description 'L2-bridge-to-eth2'
+set vpp interfaces gre gre11 remote '192.0.2.123'
+set vpp interfaces gre gre11 source-address '192.0.2.1'
+set vpp interfaces gre gre11 tunnel-type 'teb'
+set vpp interfaces gre gre12 description 'Multipoint'
+set vpp interfaces gre gre12 mode 'point-to-multipoint'
+set vpp interfaces gre gre12 remote '0.0.0.0'
+set vpp interfaces gre gre12 source-address '192.0.2.21'
+set vpp interfaces gre gre12 tunnel-type 'l3'
+set vpp interfaces ipip ipip20 remote '192.0.2.2'
+set vpp interfaces ipip ipip20 source-address '192.0.2.1'
+set vpp interfaces loopback lo12 description 'Loop12'
+set vpp interfaces vxlan vxlan10 remote '192.0.2.2'
+set vpp interfaces vxlan vxlan10 source-address '192.0.2.1'
+set vpp interfaces vxlan vxlan10 vni '10'
+set vpp settings interface eth1 driver 'dpdk'
+set vpp settings interface eth2 driver 'dpdk'
+set vpp settings interface eth3 driver 'dpdk'
+set vpp settings interface eth4 driver 'dpdk'
+set vpp settings unix poll-sleep-usec '12'
diff --git a/smoketest/configs/vpp b/smoketest/configs/vpp
new file mode 100644
index 000000000..472c2919b
--- /dev/null
+++ b/smoketest/configs/vpp
@@ -0,0 +1,128 @@
+interfaces {
+ ethernet eth0 {
+ description "WAN"
+ offload {
+ gro
+ gso
+ sg
+ tso
+ }
+ }
+ ethernet eth1 {
+ address "192.0.2.1/30"
+ address "192.0.2.21/30"
+ description "WAN"
+ }
+ ethernet eth2 {
+ }
+ ethernet eth3 {
+ }
+ ethernet eth4 {
+ description "Bonding"
+ }
+ loopback lo {
+ }
+}
+system {
+ config-management {
+ commit-revisions "100"
+ }
+ console {
+ device ttyS0 {
+ speed "115200"
+ }
+ }
+ host-name "r16"
+ login {
+ user vyos {
+ authentication {
+ encrypted-password "$6$rounds=656000$FZBlGpWsmSrV2Rvq$YPNAPtk4k6u99FAMxR6cw4DUPCgOomwCgRZRSO5rAoJK8RlMSCkVAFVF3ozL/3mZMfxcuCnwvd5HX6f9V5KzO."
+ }
+ }
+ }
+ name-server "203.0.113.1"
+ option {
+ kernel {
+ memory {
+ hugepage-size 2M {
+ hugepage-count "2048"
+ }
+ }
+ }
+ time-format "24-hour"
+ }
+ syslog {
+ global {
+ facility all {
+ level "info"
+ }
+ facility local7 {
+ level "debug"
+ }
+ }
+ }
+}
+vpp {
+ interfaces {
+ bonding bond0 {
+ hash-policy "layer2+3"
+ member {
+ interface "eth4"
+ }
+ mode "802.3ad"
+ }
+ gre gre10 {
+ description "Layer-3"
+ mode "point-to-point"
+ remote "192.0.2.2"
+ source-address "192.0.2.1"
+ tunnel-type "l3"
+ }
+ gre gre11 {
+ description "L2-bridge-to-eth2"
+ remote "192.0.2.123"
+ source-address "192.0.2.1"
+ tunnel-type "teb"
+ }
+ gre gre12 {
+ description "Multipoint"
+ mode "point-to-multipoint"
+ remote "0.0.0.0"
+ source-address "192.0.2.21"
+ tunnel-type "l3"
+ }
+ ipip ipip20 {
+ remote "192.0.2.2"
+ source-address "192.0.2.1"
+ }
+ loopback lo12 {
+ description "Loop12"
+ }
+ vxlan vxlan10 {
+ remote "192.0.2.2"
+ source-address "192.0.2.1"
+ vni "10"
+ }
+ }
+ settings {
+ interface eth1 {
+ driver "dpdk"
+ }
+ interface eth2 {
+ driver "dpdk"
+ }
+ interface eth3 {
+ driver "dpdk"
+ }
+ interface eth4 {
+ driver "dpdk"
+ }
+ unix {
+ poll-sleep-usec "12"
+ }
+ }
+}
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@11:dhcpv6-server@6:dns-dynamic@4:dns-forwarding@4:firewall@18:flow-accounting@2:https@7:ids@1:interfaces@33:ipoe-server@4:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@2:nat@8:nat66@3:nhrp@1:ntp@3:openconnect@3:openvpn@4:ospf@2:pim@1:policy@8:pppoe-server@11:pptp@5:qos@3:quagga@12:reverse-proxy@2:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@28:vpp@1:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2"
+// Release version: 1.5-rolling-202502030007
diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py
new file mode 100755
index 000000000..16302adea
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpp.py
@@ -0,0 +1,1422 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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
+import re
+import sys
+import unittest
+from collections import defaultdict
+
+from json import loads
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.utils.process import rc_cmd
+from vyos.vpp.utils import human_page_memory_to_bytes
+
+sys.path.append(os.getenv('vyos_completion_dir'))
+from list_mem_page_size import list_mem_page_size
+
+PROCESS_NAME = 'vpp_main'
+VPP_CONF = '/run/vpp/vpp.conf'
+base_path = ['vpp']
+driver = 'dpdk'
+interface = 'eth1'
+
+
+def get_vpp_config():
+ config = defaultdict(dict)
+ current_section = None
+
+ with open(VPP_CONF, 'r') as f:
+ for line in f:
+ line = line.strip()
+
+ if not line or line.startswith('#'): # Ignore empty lines and comments
+ continue
+
+ section_match = re.match(r'([a-zA-Z0-9_-]+)\s*{', line)
+ if section_match:
+ current_section = section_match.group(1)
+ config[current_section] = {}
+ continue
+
+ if line == '}': # End of section
+ current_section = None
+ continue
+
+ key_value_match = re.match(r'([a-zA-Z0-9_-]+)\s+(.+)', line)
+ if key_value_match:
+ key, value = key_value_match.groups()
+ if current_section:
+ config[current_section][key] = value
+ else:
+ config[key] = value
+
+ return config
+
+
+def get_address(interface):
+ rc, data = rc_cmd(f'ip --json address show dev {interface}')
+ if rc == 0:
+ data = loads(data)
+ if isinstance(data, list) and len(data) > 0:
+ ip_address = data[0]['addr_info'][0]['local']
+ return ip_address
+
+
+class TestVPP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestVPP, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def setUp(self):
+ self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver])
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10'])
+
+ def tearDown(self):
+ try:
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ finally:
+ # Ensure these cleanup operations always run
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # delete address for Ethernet interface
+ self.cli_delete(['interfaces', 'ethernet', interface, 'address'])
+ self.cli_commit()
+
+ self.assertFalse(os.path.exists(VPP_CONF))
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_vpp_basic(self):
+ main_core = '0'
+ poll_sleep = '0'
+
+ # Main core must be verified
+ # expect raise ConfigError
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', '99'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core])
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', poll_sleep])
+
+ # commit changes
+ self.cli_commit()
+
+ config_entries = (
+ f'poll-sleep-usec {poll_sleep}',
+ f'main-core {main_core}',
+ 'plugin default { disable }',
+ 'plugin dpdk_plugin.so { enable }',
+ 'plugin linux_cp_plugin.so { enable }',
+ 'dev 0000:00:00.0',
+ 'uio-bind-force',
+ )
+
+ # Check configured options
+ config = read_file(VPP_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ # route-no-paths is not present in the output
+ # looks like vpp bug
+ _, out = rc_cmd('sudo vppctl show lcp')
+ required_str = 'lcp route-no-paths on'
+ self.assertIn(required_str, out)
+
+ self.cli_set(base_path + ['settings', 'lcp', 'ignore-kernel-routes'])
+ self.cli_commit()
+
+ # check disabled 'route no path'
+ _, out = rc_cmd('sudo vppctl show lcp')
+ required_str = 'lcp route-no-paths off'
+ self.assertIn(required_str, out)
+
+ def test_02_vpp_vxlan(self):
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ interface_kernel = f'vpptap{vni}'
+ new_interface_kernel = f'vpptap1{vni}'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.3'
+ remote_address = '192.0.2.254'
+ kernel_address = '203.0.113.1'
+
+ self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24'])
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+
+ # remote and source address must not be the same
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', source_address]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/24']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check vxlan interface
+ _, out = rc_cmd('sudo vppctl show vxlan tunnel')
+ required_str = f'[0] instance 23 src {source_address} dst {remote_address} src_port 4789 dst_port 4789 vni {vni}'
+ self.assertIn(required_str, out)
+
+ # update vxlan interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+
+ # source address of the tunnel interface should be configured
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ [
+ 'interfaces',
+ 'ethernet',
+ interface,
+ 'vif',
+ vni,
+ 'address',
+ f'{new_source_address}/24',
+ ]
+ )
+ self.cli_commit()
+
+ # check gre interface after update
+ _, out = rc_cmd('sudo vppctl show vxlan tunnel')
+ required_str = (
+ f'[0] instance {vni} src {new_source_address} dst {remote_address}'
+ )
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # change vpp settings
+ self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '5'])
+ self.cli_commit()
+
+ config = read_file(VPP_CONF)
+ self.assertIn('poll-sleep-usec 5', config)
+
+ # delete vxlan kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update vxlan kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'vxlan',
+ interface_vxlan,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete vxlan kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'vxlan', interface_vxlan, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete vxlan interface
+ self.cli_delete(base_path + ['interfaces', 'vxlan', interface_vxlan])
+ self.cli_commit()
+
+ # delete vif Ethernet interface
+ self.cli_delete(['interfaces', 'ethernet', interface, 'vif'])
+ self.cli_commit()
+
+ def test_03_vpp_gre(self):
+ interface_gre = 'gre12'
+ interface_kernel = 'vpptun12'
+ new_interface_kernel = 'vpptun123'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '192.0.2.254'
+ kernel_address = '10.0.0.0'
+
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'gre', interface_gre, 'remote', remote_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # source address of the tunnel interface should be configured
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ ['interfaces', 'ethernet', interface, 'address', f'{source_address}/24']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check gre interface
+ _, out = rc_cmd('sudo vppctl show gre tunnel')
+ required_str = f'[0] instance 12 src {source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+
+ # update gre interface
+ self.cli_set(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'source-address', new_source_address]
+ )
+
+ self.cli_set(
+ ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24']
+ )
+ self.cli_commit()
+
+ # check gre interface after update
+ _, out = rc_cmd('sudo vppctl show gre tunnel')
+ required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete gre kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update gre kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'gre',
+ interface_gre,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete kernel interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete gre kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'gre', interface_gre, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete gre interface
+ self.cli_delete(base_path + ['interfaces', 'gre', interface_gre])
+ self.cli_commit()
+
+ @unittest.skip('Skipping this test geneve index always is 0')
+ def test_04_vpp_geneve(self):
+ vni = '2'
+ # Must be 'geneve0' to pass smoketest
+ # As geneve interfaces cannot be named with "instance" suffix
+ interface_geneve = 'geneve0'
+ interface_kernel = f'vpptun{vni}'
+ new_interface_kernel = f'vpptun1{vni}'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '203.0.113.10'
+ kernel_address = '10.0.0.1'
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'source-address',
+ source_address,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'geneve', interface_geneve, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve, 'vni', vni])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check geneve interface
+ _, out = rc_cmd('sudo vppctl show geneve tunnel')
+ required_str = f'[0] lcl {source_address} rmt {remote_address} vni {vni}'
+ self.assertIn(required_str, out)
+
+ # update geneve interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+ self.cli_commit()
+
+ # check geneve interface after update
+ _, out = rc_cmd('sudo vppctl show geneve tunnel')
+ required_str = f'[0] lcl {new_source_address} rmt {remote_address} vni {vni}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete geneve kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update gemeve kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'geneve',
+ interface_geneve,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete geneve kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'geneve', interface_geneve, 'kernel-interface']
+ )
+ self.cli_commit()
+
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete geneve interface
+ self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve])
+ self.cli_commit()
+
+ def test_05_vpp_loopback(self):
+ interface_loopback = 'lo11'
+ interface_kernel = 'vpptun11'
+ new_interface_kernel = 'vpptun12'
+ kernel_address = '192.0.2.54'
+
+ self.cli_set(base_path + ['interfaces', 'loopback', interface_loopback])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/25']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check loopback interface
+ _, out = rc_cmd('sudo vppctl show interface loop11')
+ required_str = 'loop11'
+ self.assertIn(required_str, out)
+
+ # delete loopback kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update loopback kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'loopback',
+ interface_loopback,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete loopback kernel-interface
+ self.cli_delete(
+ base_path
+ + ['interfaces', 'loopback', interface_loopback, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete loopback interface
+ self.cli_delete(base_path + ['interfaces', 'loopback', interface_loopback])
+ self.cli_commit()
+
+ @unittest.skip('Skipping temporary bonding, sometimes get recursion T7117')
+ def test_06_vpp_bonding(self):
+ interface_bond = 'bond23'
+ interface_kernel = 'vpptun23'
+ hash = 'layer3+4'
+ mode = '802.3ad'
+ description = 'Interface-Bonding'
+ vlans = ['123', '456']
+ vlan_description = 'My-vlan-123'
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bonding',
+ interface_bond,
+ 'member',
+ 'interface',
+ interface,
+ ]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'bonding', interface_bond, 'hash-policy', hash]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'bonding', interface_bond, 'mode', mode]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # Check for interface state "BondEthernet23 up"
+ _, out = rc_cmd('sudo vppctl show interface')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+ self.assertRegex(
+ normalized_out,
+ r'BondEthernet23\s+\d+\s+up',
+ "Interface BondEthernet23 is not in the expected state 'up'.",
+ )
+
+ # set kernel interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bonding',
+ interface_bond,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'description', description]
+ )
+ for vlan in vlans:
+ self.cli_set(
+ base_path
+ + [
+ 'kernel-interfaces',
+ interface_kernel,
+ 'vif',
+ vlan,
+ 'description',
+ vlan_description,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}'))
+
+ current_alias = read_file(f'/sys/class/net/{interface_kernel}/ifalias')
+ vlan_alias = read_file(f'/sys/class/net/{interface_kernel}.{vlan}/ifalias')
+ self.assertEqual(current_alias, description)
+ self.assertEqual(vlan_alias, vlan_description)
+
+ # check bonding interface
+ _, out = rc_cmd('sudo vppctl show bond details')
+ required_enries = (
+ 'BondEthernet23',
+ 'mode: lacp',
+ 'load balance: l34',
+ 'number of active members: 0',
+ 'number of members: 1',
+ f'{interface}',
+ 'device instance: 0',
+ 'interface id: 23',
+ )
+ for entry in required_enries:
+ self.assertIn(entry, out)
+
+ # check interface state
+ _, out = rc_cmd('sudo vppctl show interface')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+ # Check for interface state "BondEthernet23 up"
+ self.assertRegex(
+ normalized_out,
+ r'BondEthernet23\s+\d+\s+up',
+ "Interface BondEthernet23 is not in the expected state 'up'.",
+ )
+
+ # delete vpp kernel-interface vlan
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel, 'vif'])
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}'))
+
+ # delete vpp kernel-interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete bonding kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'bonding', interface_bond, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete bonding interface
+ self.cli_delete(base_path + ['interfaces', 'bonding'])
+ self.cli_commit()
+
+ # check deleting bonding interface
+ _, out = rc_cmd('sudo vppctl show interface')
+ self.assertNotIn('BondEthernet23', out)
+
+ def test_07_vpp_bridge(self):
+ fake_member = 'eth2'
+ members = [interface]
+ interface_bridge = 'br10'
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ source_address = '192.0.2.1'
+ remote_address = '192.0.2.254'
+
+ self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24'])
+ for member in members:
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ member,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+
+ # Perform assertions based on the normalized output
+ self.assertIn('BD-ID Index BSN Age(min)', normalized_out)
+ self.assertIn('10 1 0 off', normalized_out)
+ self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out)
+ self.assertIn('on on flood on', normalized_out)
+ self.assertIn('Interface If-idx ISN', normalized_out)
+ # Check Interface, If-idx, ISN
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+
+ # Set non exist member
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ fake_member,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ fake_member,
+ ]
+ )
+
+ # Add VXLAN to the bridge
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ interface_vxlan,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+
+ # Perform assertions based on the normalized output
+ self.assertIn('BD-ID Index BSN Age(min)', normalized_out)
+ self.assertIn('10 1 0 off', normalized_out)
+ self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out)
+ self.assertIn('on on flood on', normalized_out)
+ self.assertIn('Interface If-idx ISN', normalized_out)
+ # Check Interface, If-idx, ISN
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+ self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+')
+
+ # Add check dependency ethernet => bridge
+ self.cli_set(
+ base_path + ['settings', 'interface', interface, 'dpdk-options', 'promisc']
+ )
+ self.cli_commit()
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+ self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+')
+ self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+')
+
+ # Add Loopback BVI to the bridge
+ self.cli_set(base_path + ['interfaces', 'loopback', f'lo{vni}'])
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'bridge',
+ interface_bridge,
+ 'member',
+ 'interface',
+ f'lo{vni}',
+ 'bvi',
+ ]
+ )
+ # commit changes
+ self.cli_commit()
+
+ # check bridge interface
+ _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail')
+ # Normalize the output for consistent whitespace
+ normalized_out = re.sub(r'\s+', ' ', out)
+
+ self.assertIn('10 1 0 off', normalized_out)
+ self.assertRegex(out, r'\bloop23\s+\d+\s+\d+\s+\d+\s+\*\s+')
+
+ def test_08_vpp_ipip(self):
+ interface_ipip = 'ipip12'
+ interface_kernel = 'vpptun12'
+ new_interface_kernel = 'vpptun123'
+ source_address = '192.0.2.1'
+ new_source_address = '192.0.2.2'
+ remote_address = '192.0.2.5'
+ kernel_address = '10.0.0.0'
+
+ self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24'])
+ self.cli_set(
+ base_path
+ + ['interfaces', 'ipip', interface_ipip, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path + ['interfaces', 'ipip', interface_ipip, 'remote', remote_address]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ self.cli_set(
+ base_path
+ + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31']
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ current_address = get_address(interface_kernel)
+ self.assertEqual(kernel_address, current_address)
+
+ # check ipip interface
+ _, out = rc_cmd('sudo vppctl show ipip tunnel')
+ required_str = f'[0] instance 12 src {source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+
+ # update ipip interface
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'source-address',
+ new_source_address,
+ ]
+ )
+
+ # source address of the tunnel interface should be configured
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24']
+ )
+ self.cli_commit()
+
+ # check ipip interface after update
+ _, out = rc_cmd('sudo vppctl show ipip tunnel')
+ required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}'
+ self.assertIn(required_str, out)
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+ self.assertEqual(kernel_address, current_address)
+
+ # delete ipip kernel-interface but do not delete 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_delete(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # update ipip kernel-interface but do not change 'vpp kernel-interface'
+ # expect raise ConfigError
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'ipip',
+ interface_ipip,
+ 'kernel-interface',
+ new_interface_kernel,
+ ]
+ )
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete kernel interface
+ self.cli_delete(base_path + ['kernel-interfaces', interface_kernel])
+ self.cli_commit()
+
+ # delete ipip kernel-interface
+ self.cli_delete(
+ base_path + ['interfaces', 'ipip', interface_ipip, 'kernel-interface']
+ )
+ self.cli_commit()
+ self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}'))
+
+ # delete ipip interface
+ self.cli_delete(base_path + ['interfaces', 'ipip', interface_ipip])
+ self.cli_commit()
+
+ def test_09_vpp_xconnect(self):
+ vni = '23'
+ interface_vxlan = f'vxlan{vni}'
+ interface_xconnect = f'xcon{vni}'
+ source_address = '192.0.2.1'
+ remote_address = '192.0.2.254'
+
+ self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24'])
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address]
+ )
+ self.cli_set(
+ base_path
+ + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address]
+ )
+ self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni])
+
+ # Add xconneect
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'xconnect',
+ interface_xconnect,
+ 'member',
+ 'interface',
+ interface,
+ ]
+ )
+
+ # Cross connect interfaces require 2 interfaces
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(
+ base_path
+ + [
+ 'interfaces',
+ 'xconnect',
+ interface_xconnect,
+ 'member',
+ 'interface',
+ interface_vxlan,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # check interface mode
+ _, out = rc_cmd('sudo vppctl show mode')
+ required_str_list = [
+ f'l2 xconnect {interface} vxlan_tunnel{vni}',
+ f'l2 xconnect vxlan_tunnel{vni} {interface}',
+ ]
+ for required_string in required_str_list:
+ self.assertIn(required_string, out)
+
+ # delete xconnect interface
+ self.cli_delete(base_path + ['interfaces', 'xconnect', interface_xconnect])
+ self.cli_commit()
+
+ # check delete xconnect interface
+ _, out = rc_cmd('sudo vppctl show mode')
+ for required_string in required_str_list:
+ self.assertNotIn(required_string, out)
+
+ def test_10_vpp_driver_options(self):
+ dpdk_options = {
+ 'num-rx-desc': '512',
+ 'num-tx-desc': '512',
+ 'num-rx-queues': '1',
+ 'num-tx-queues': '1',
+ }
+ main_core = '0'
+ workers = '1'
+
+ base_interface_path = base_path + ['settings', 'interface', interface]
+
+ for option, value in dpdk_options.items():
+ self.cli_set(base_interface_path + ['dpdk-options', option, value])
+
+ # rx/tx queue configuration expect VPP workers to be set
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core])
+ self.cli_set(base_path + ['settings', 'cpu', 'workers', workers])
+
+ # DPDK driver expect only dpdk-options and not xdp-options to be set
+ # expect raise ConfigError
+ self.cli_set(base_interface_path + ['xdp-options', 'no-syscall-lock'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # delete xdp-options and apply commit
+ self.cli_delete(base_interface_path + ['xdp-options'])
+ self.cli_commit()
+
+ # check dpdk options in config file
+ config = read_file(VPP_CONF)
+
+ for option, value in dpdk_options.items():
+ self.assertIn(f'{option} {value}', config)
+
+ def test_11_vpp_cpu_settings(self):
+ main_core = '2'
+ workers = '1'
+ skip_cores = '1'
+
+ self.cli_set(base_path + ['settings', 'cpu', 'workers', workers])
+
+ # "cpu workers" reqiures main-core to be set
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core])
+
+ self.cli_set(base_path + ['settings', 'cpu', 'skip-cores', '99'])
+
+ # "cpu skip-cores" cannot be more than number of available CPUs - 1
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['settings', 'cpu', 'skip-cores', skip_cores])
+
+ self.cli_commit()
+
+ config_entries = (
+ f'skip-cores {skip_cores}',
+ f'main-core {main_core}',
+ f'workers {workers}',
+ 'dev 0000:00:00.0',
+ )
+
+ # Check configured options
+ config = read_file(VPP_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ def test_12_vpp_cpu_corelist_workers(self):
+ main_core = '0'
+ corelist_workers = ['3']
+
+ for worker in corelist_workers:
+ self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', worker])
+
+ # "cpu corelist-workers" reqiures main-core to be set
+ # expect raise ConfigError
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core])
+
+ # corelist-workers and workers cannot be used at the same time
+ # expect raise ConfigError
+ self.cli_set(base_path + ['settings', 'cpu', 'workers', '2'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['settings', 'cpu', 'workers'])
+
+ # verify corelist-workers are set not correctly
+ # expect raise ConfigError
+ self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', '99-101'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['settings', 'cpu', 'corelist-workers', '99-101'])
+
+ self.cli_commit()
+
+ config_entries = (
+ f'main-core {main_core}',
+ f'corelist-workers {",".join(corelist_workers)}',
+ 'dev 0000:00:00.0',
+ )
+
+ # Check configured options
+ config = read_file(VPP_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ def test_13_1_buffer_page_size(self):
+ sizes = ['default', 'default-hugepage'] + list_mem_page_size()
+ for size in sizes:
+ if human_page_memory_to_bytes(size) >= 1 << 30:
+ continue
+ self.cli_set(base_path + ['settings', 'buffers', 'page-size', size])
+ self.cli_commit()
+
+ conf = get_vpp_config()
+ self.assertEqual(conf['buffers']['page-size'], size)
+
+ def test_13_2_statseg_page_size(self):
+ sizes = ['default', 'default-hugepage'] + list_mem_page_size()
+ for size in sizes:
+ if human_page_memory_to_bytes(size) >= 1 << 30:
+ continue
+ self.cli_set(base_path + ['settings', 'statseg', 'page-size', size])
+ self.cli_commit()
+
+ conf = get_vpp_config()
+ self.assertEqual(conf['statseg']['page-size'], size)
+
+ def test_13_3_mem_page_size(self):
+ sizes = ['default', 'default-hugepage'] + list_mem_page_size()
+ for size in sizes:
+ if human_page_memory_to_bytes(size) >= 1 << 30:
+ continue
+ self.cli_set(
+ base_path + ['settings', 'memory', 'main-heap-page-size', size]
+ )
+ self.cli_commit()
+
+ conf = get_vpp_config()
+ self.assertEqual(conf['memory']['main-heap-page-size'], size)
+
+ def test_14_mem_default_hugepage(self):
+ sizes = list_mem_page_size(hugepage_only=True)
+ for size in sizes:
+ if human_page_memory_to_bytes(size) >= 1 << 30:
+ continue
+ self.cli_set(
+ base_path + ['settings', 'memory', 'default-hugepage-size', size]
+ )
+ self.cli_commit()
+
+ conf = get_vpp_config()
+ self.assertEqual(conf['memory']['default-hugepage-size'], size)
+
+ def test_15_vpp_ipsec_xfrm_nl(self):
+ base_ipsec = base_path + ['settings', 'ipsec']
+ batch_delay = '250'
+ batch_size = '150'
+ rx_buffer_zise = '1024'
+
+ self.cli_set(base_ipsec + ['netlink', 'batch-delay-ms', batch_delay])
+ self.cli_set(base_ipsec + ['netlink', 'batch-size', batch_size])
+ self.cli_set(base_ipsec + ['netlink', 'rx-buffer-size', rx_buffer_zise])
+ self.cli_commit()
+
+ config_entries = (
+ 'linux-xfrm-nl',
+ 'enable-route-mode-ipsec',
+ 'interface ipsec',
+ f'nl-batch-delay-ms {batch_delay}',
+ f'nl-batch-size {batch_size}',
+ f'nl-rx-buffer-size {rx_buffer_zise}',
+ )
+
+ # Check configured options
+ config = read_file(VPP_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ # set IPsec tunnel-type ipip
+ self.cli_set(base_ipsec + ['interface-type', 'ipip'])
+ self.cli_commit()
+
+ config = read_file(VPP_CONF)
+ self.assertIn('interface ipip', config)
+
+ def test_16_vpp_cgnat(self):
+ base_cgnat = base_path + ['nat', 'cgnat']
+ iface_out = 'eth0'
+ iface_inside = 'eth1'
+ timeout_udp = '150'
+ timeout_icmp = '30'
+ timeout_tcp_est = '600'
+ timeout_tcp_trans = '120'
+ inside_prefix = '100.64.0.0/24'
+ outside_prefix = '192.0.2.1/32'
+
+ self.cli_set(base_path + ['settings', 'interface', iface_out, 'driver', driver])
+ self.cli_set(base_cgnat + ['interface', 'inside', iface_inside])
+ self.cli_set(base_cgnat + ['interface', 'outside', iface_out])
+ self.cli_set(base_cgnat + ['rule', '100', 'inside-prefix', inside_prefix])
+ self.cli_set(base_cgnat + ['rule', '100', 'outside-prefix', outside_prefix])
+ self.cli_set(base_cgnat + ['timeout', 'icmp', timeout_icmp])
+ self.cli_set(base_cgnat + ['timeout', 'tcp-established', timeout_tcp_est])
+ self.cli_set(base_cgnat + ['timeout', 'tcp-transitory', timeout_tcp_trans])
+ self.cli_set(base_cgnat + ['timeout', 'udp', timeout_udp])
+ self.cli_commit()
+
+ # Check interfaces
+ _, out = rc_cmd('sudo vppctl show det44 interfaces')
+ self.assertIn(f'{iface_inside} in', out)
+ self.assertIn(f'{iface_out} out', out)
+
+ # Check mappings
+ _, out = rc_cmd('sudo vppctl show det44 mappings')
+ self.assertIn(inside_prefix, out)
+ self.assertIn(outside_prefix, out)
+
+ # Check timeouts
+ _, out = rc_cmd('sudo vppctl show det44 timeouts')
+ self.assertIn(f'udp timeout: {timeout_udp}sec', out)
+ self.assertIn(f'tcp established timeout: {timeout_tcp_est}sec', out)
+ self.assertIn(f'tcp transitory timeout: {timeout_tcp_trans}sec', out)
+ self.assertIn(f'icmp timeout: {timeout_icmp}sec', out)
+
+ def test_17_vpp_nat(self):
+ base_nat = base_path + ['nat44']
+ base_nat_settings = base_path + ['settings', 'nat44']
+ exclude_local_addr = '100.64.0.52'
+ exclude_local_port = '22'
+ iface_out = 'eth0'
+ iface_inside = 'eth1'
+ timeout_udp = '150'
+ timeout_icmp = '30'
+ timeout_tcp_est = '600'
+ timeout_tcp_trans = '120'
+ translation_pool = '192.0.2.1-192.0.2.2'
+ static_ext_addr = '192.0.2.55'
+ static_local_addr = '100.64.0.55'
+ sess_limit = '64000'
+
+ self.cli_set(base_path + ['settings', 'interface', iface_out, 'driver', driver])
+ self.cli_set(base_nat + ['interface', 'inside', iface_inside])
+ self.cli_set(base_nat + ['interface', 'outside', iface_out])
+ self.cli_set(
+ base_nat + ['address-pool', 'translation', 'address', translation_pool]
+ )
+ self.cli_set(
+ base_nat + ['exclude', 'rule', '100', 'local-address', exclude_local_addr]
+ )
+ self.cli_set(
+ base_nat + ['exclude', 'rule', '100', 'local-port', exclude_local_port]
+ )
+ self.cli_set(
+ base_nat + ['static', 'rule', '100', 'external', 'address', static_ext_addr]
+ )
+ self.cli_set(
+ base_nat + ['static', 'rule', '100', 'local', 'address', static_local_addr]
+ )
+
+ self.cli_set(base_nat_settings + ['no-forwarding'])
+ self.cli_set(base_nat_settings + ['session-limit', sess_limit])
+ self.cli_set(base_nat_settings + ['timeout', 'icmp', timeout_icmp])
+ self.cli_set(
+ base_nat_settings + ['timeout', 'tcp-established', timeout_tcp_est]
+ )
+ self.cli_set(
+ base_nat_settings + ['timeout', 'tcp-transitory', timeout_tcp_trans]
+ )
+ self.cli_set(base_nat_settings + ['timeout', 'udp', timeout_udp])
+ self.cli_commit()
+
+ # Check addresses
+ _, out = rc_cmd('sudo vppctl show nat44 addresses')
+ self.assertIn(translation_pool.split('-')[0], out)
+ self.assertIn(translation_pool.split('-')[1], out)
+
+ # Check interfaces
+ _, out = rc_cmd('sudo vppctl show nat44 interfaces')
+ self.assertIn(f'{iface_inside} in', out)
+ self.assertIn(f'{iface_out} out', out)
+
+ # Check mappings
+ _, out = rc_cmd('sudo vppctl show nat44 static mappings')
+ self.assertIn(
+ f'local {static_local_addr} external {static_ext_addr} vrf 0', out
+ )
+ self.assertIn(f'{exclude_local_addr}:{exclude_local_port} vrf 0', out)
+
+ # Check timeouts
+ _, out = rc_cmd('sudo vppctl show nat timeouts')
+ self.assertIn(f'udp timeout: {timeout_udp}sec', out)
+ self.assertIn(f'tcp-established timeout: {timeout_tcp_est}sec', out)
+ self.assertIn(f'tcp-transitory timeout: {timeout_tcp_trans}sec', out)
+ self.assertIn(f'icmp timeout: {timeout_icmp}sec', out)
+
+ # Summary
+ _, out = rc_cmd('sudo vppctl show nat44 summary')
+ self.assertIn(f'max translations per thread: {sess_limit} fib 0', out)
+
+ def test_18_vpp_sflow(self):
+ base_sflow = ['system', 'sflow']
+
+ self.cli_set(base_path + ['sflow', 'interface', interface])
+ self.cli_set(base_sflow + ['interface', interface])
+ self.cli_set(base_sflow + ['server', '127.0.0.1'])
+ self.cli_set(base_sflow + ['vpp'])
+ self.cli_commit()
+
+ # Check sFlow
+ _, out = rc_cmd('sudo vppctl show sflow')
+
+ expected_entries = (
+ 'sflow sampling-direction ingress',
+ f'sflow enable {interface}',
+ 'interfaces enabled: 1',
+ )
+
+ for expected_entry in expected_entries:
+ self.assertIn(expected_entry, out)
+
+ self.cli_delete(base_sflow)
+ self.cli_commit()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/completion/list_mem_page_size.py b/src/completion/list_mem_page_size.py
new file mode 100644
index 000000000..1ed81a15f
--- /dev/null
+++ b/src/completion/list_mem_page_size.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..ea6a77bed
--- /dev/null
+++ b/src/completion/list_vpp_interfaces.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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..0ea8d261b
--- /dev/null
+++ b/src/conf_mode/vpp.py
@@ -0,0 +1,691 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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 import IPRoute
+from vpp_papi import VPPIOError, VPPValueError
+
+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..d6a9150d1
--- /dev/null
+++ b/src/conf_mode/vpp_acl.py
@@ -0,0 +1,400 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..c9aa6276c
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bonding.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..4272db616
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_bridge.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..f56cadc3e
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ethernet.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..12fac83fb
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_geneve.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..5ec6feeb0
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_gre.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..430b7b334
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_ipip.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..92ba110f1
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_loopback.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..c4068818e
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_vxlan.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..220c1d44e
--- /dev/null
+++ b/src/conf_mode/vpp_interfaces_xconnect.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2025 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..f9354c964
--- /dev/null
+++ b/src/conf_mode/vpp_kernel-interfaces.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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..2c0298902
--- /dev/null
+++ b/src/conf_mode/vpp_nat.py
@@ -0,0 +1,488 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..68695b78e
--- /dev/null
+++ b/src/conf_mode/vpp_nat_cgnat.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..ebe91617b
--- /dev/null
+++ b/src/conf_mode/vpp_sflow.py
@@ -0,0 +1,145 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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..b7c42eb58
--- /dev/null
+++ b/src/op_mode/show_vpp_interfaces.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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..d0569ac49
--- /dev/null
+++ b/src/op_mode/show_vpp_nat44.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..7afe96433
--- /dev/null
+++ b/src/op_mode/vpp_acl.py
@@ -0,0 +1,342 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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..5112c70be
--- /dev/null
+++ b/src/op_mode/vpp_nat_cgnat.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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