From d7cad8ea121162dd54722f2c4db72c458dfcf02d Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Tue, 7 Jan 2025 23:30:14 +0000 Subject: Add XML CLI for conf and op-mode --- .../include/kernel-interface-tap.xml.i | 15 +++++++ .../include/kernel-interface-tun.xml.i | 15 +++++++ .../include/unformat_log2_page_size.xml.i | 29 +++++++++++++ .../include/unformat_memory_size.xml.i | 22 ++++++++++ .../include/version/vpp-version.xml.i | 3 ++ .../include/vpp/iface_rx_mode.xml.i | 25 +++++++++++ interface-definitions/include/vpp/vif.xml.i | 21 ++++++++++ .../include/vpp_host_resources.xml.i | 48 ++++++++++++++++++++++ 8 files changed, 178 insertions(+) create mode 100644 interface-definitions/include/kernel-interface-tap.xml.i create mode 100644 interface-definitions/include/kernel-interface-tun.xml.i create mode 100644 interface-definitions/include/unformat_log2_page_size.xml.i create mode 100644 interface-definitions/include/unformat_memory_size.xml.i create mode 100644 interface-definitions/include/version/vpp-version.xml.i create mode 100644 interface-definitions/include/vpp/iface_rx_mode.xml.i create mode 100644 interface-definitions/include/vpp/vif.xml.i create mode 100644 interface-definitions/include/vpp_host_resources.xml.i (limited to 'interface-definitions/include') 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 @@ + + + + Kernel interface name + + vpptapN + Kernel interface name + + + vpptap\d+ + + Kernel interface must start with vpptapN + + + 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 @@ + + + + Kernel interface name + + vpptunN + Kernel interface name + + + vpptun\d+ + + Kernel interface must start with vpptunN + + + 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..240fd5bb8 --- /dev/null +++ b/interface-definitions/include/unformat_log2_page_size.xml.i @@ -0,0 +1,29 @@ + + + default default-hugepage + + + default + Default + + + default-hugepage + Default huge-page + + + <number>K + Kilobyte + + + <number>M + Megabyte + + + <number>G + Gigabyte + + + + (default|default-hugepage|\d+K|\d+M|\d+G) + + 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 @@ + + + <number> + byte + + + <number>K + Kilobyte + + + <number>M + Megabyte + + + <number>G + Gigabyte + + + + (\d+|\d+K|\d+M|\d+G) + + 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 @@ + + + \ No newline at end of file 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 @@ + + + + Receive packet processing mode + + polling interrupt adaptive + + + polling + Constantly check for new data + + + interrupt + Interrupt mode + + + adaptive + Adaptive mode + + + (polling|interrupt|adaptive) + + + + 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 @@ + + + + Virtual Local Area Network (VLAN) ID + + u32:0-4094 + Virtual Local Area Network (VLAN) ID + + + + + VLAN ID must be between 0 and 4094 + + + #include + #include + #include + #include + + + 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..7f988ce44 --- /dev/null +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -0,0 +1,48 @@ + + + + Host resources control + + + + + Number of pre-allocated huge pages of the default size + + u32:0-4294967295 + Pages count + + + + + + 1024 + + + + Maximum number of memory map areas a process may have + + u32:0-65535 + Areas count + + + + + + 3096 + + + + Maximum shared memory segment size that can be created + + u32:0-18446744073709551612 + Size in bytes + + + + + + 2147483648 + + + + -- cgit v1.2.3 From cedfce8899cb3dc406922c68afc7e31b8edc01ae Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Wed, 8 Jan 2025 15:56:07 +0000 Subject: Move component version to conflict-components or dynamic --- conflict-components/version/vpp-version.xml.i | 3 +++ conflict-components/xml-component-version.xml.in | 4 ++++ interface-definitions/include/version/vpp-version.xml.i | 3 --- interface-definitions/xml-component-version.xml.in | 4 ---- 4 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 conflict-components/version/vpp-version.xml.i create mode 100644 conflict-components/xml-component-version.xml.in delete mode 100644 interface-definitions/include/version/vpp-version.xml.i delete mode 100644 interface-definitions/xml-component-version.xml.in (limited to 'interface-definitions/include') diff --git a/conflict-components/version/vpp-version.xml.i b/conflict-components/version/vpp-version.xml.i new file mode 100644 index 000000000..96a69c92f --- /dev/null +++ b/conflict-components/version/vpp-version.xml.i @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/conflict-components/xml-component-version.xml.in b/conflict-components/xml-component-version.xml.in new file mode 100644 index 000000000..ae137d435 --- /dev/null +++ b/conflict-components/xml-component-version.xml.in @@ -0,0 +1,4 @@ + + + #include + diff --git a/interface-definitions/include/version/vpp-version.xml.i b/interface-definitions/include/version/vpp-version.xml.i deleted file mode 100644 index 96a69c92f..000000000 --- a/interface-definitions/include/version/vpp-version.xml.i +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in deleted file mode 100644 index ae137d435..000000000 --- a/interface-definitions/xml-component-version.xml.in +++ /dev/null @@ -1,4 +0,0 @@ - - - #include - -- cgit v1.2.3 From d1d170c20fbec8f10f0eec440edae7655789e923 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Thu, 9 Jan 2025 17:06:19 +0000 Subject: Remove conflict-components --- conflict-components/version/vpp-version.xml.i | 3 --- conflict-components/xml-component-version.xml.in | 4 ---- interface-definitions/include/version/vpp-version.xml.i | 3 +++ 3 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 conflict-components/version/vpp-version.xml.i delete mode 100644 conflict-components/xml-component-version.xml.in create mode 100644 interface-definitions/include/version/vpp-version.xml.i (limited to 'interface-definitions/include') diff --git a/conflict-components/version/vpp-version.xml.i b/conflict-components/version/vpp-version.xml.i deleted file mode 100644 index 96a69c92f..000000000 --- a/conflict-components/version/vpp-version.xml.i +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/conflict-components/xml-component-version.xml.in b/conflict-components/xml-component-version.xml.in deleted file mode 100644 index ae137d435..000000000 --- a/conflict-components/xml-component-version.xml.in +++ /dev/null @@ -1,4 +0,0 @@ - - - #include - 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 @@ + + + \ No newline at end of file -- cgit v1.2.3 From 7897f8d216295b32626dc4d5604349812c592921 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Fri, 14 Feb 2025 15:59:59 +0000 Subject: T7168: Add IPsec XFRM netlink plugin and CLI Add linux-xfrm-nl plugin Add configuration commands for VPP IPsec ``` set vpp settings ipsec interface-type 'ipsec' set vpp settings ipsec netlink batch-delay-ms '250' set vpp settings ipsec netlink batch-size '150' set vpp settings ipsec netlink rx-buffer-size '1024' ``` --- data/templates/vpp/startup.conf.j2 | 26 ++++++++- interface-definitions/include/vpp/netlink.xml.i | 45 ++++++++++++++++ interface-definitions/vpp.xml.in | 72 ++++++++++--------------- src/conf_mode/vpp.py | 6 ++- 4 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 interface-definitions/include/vpp/netlink.xml.i (limited to 'interface-definitions/include') diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index 5cab34a97..cf10b6ce5 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -104,7 +104,15 @@ plugins { plugin geneve_plugin.so { enable } plugin gre_plugin.so { enable } plugin vxlan_plugin.so { enable } - plugin wireguard_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 } } linux-cp { @@ -179,3 +187,19 @@ dpdk { {% 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/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 @@ + + + + Netlink options + + + + + Receive buffer size + + u32:0-4294967295 + Receive buffer size + + + + + + + + + Batch size + + u32:0-4294967295 + Batch size + + + + + + + + + Batch delay + + u32:0-4294967295 + Batch delay (in ms) + + + + + + + + + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 77a461242..2160d0715 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -638,6 +638,34 @@ + + + IPsec settings + + + + + IPsec interface type + + ipsec ipip + + + ipsec + IPsec interface type + + + ipip + IPIP tunnel interface type + + + (ipsec|ipip) + + + ipsec + + #include + + Level 2 MAC address learning settings @@ -663,49 +691,7 @@ Linux control plane setting - - - Netlink options - - - - - Receive buffer size - - u32:0-4294967295 - Receive buffer size - - - - - - - - - Batch size - - u32:0-4294967295 - Batch size - - - - - - - - - Batch delay - - u32:0-4294967295 - Batch delay (in ms) - - - - - - - - + #include Ignore kernel routes diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 5126b7c79..ac95f8cb4 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS Inc. +# 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 @@ -139,6 +139,10 @@ def get_config(config=None): 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 -- cgit v1.2.3 From 4abb970491dfc83234b817808d18ae6bfeeeffe8 Mon Sep 17 00:00:00 2001 From: khramshinr Date: Thu, 20 Feb 2025 14:08:54 +0700 Subject: T7073: Verify VPP buffers page size T7077: Verify VPP memory default-hugepage-size T7079: Verify VPP memory main-heap-page-size T7080: Verify VPP statseg page-size get available hugepage sizes align memory main-heap-size by page size validate host_resources max_map_count --- .../include/unformat_log2_page_size.xml.i | 16 +--- interface-definitions/vpp.xml.in | 4 +- python/vyos/vpp/utils.py | 94 ++++++++++++++++++++++ smoketest/scripts/cli/test_vpp.py | 76 +++++++++++++++-- src/completion/list_mem_page_size.py | 69 ++++++++++++++++ src/conf_mode/vpp.py | 47 +++++++---- 6 files changed, 269 insertions(+), 37 deletions(-) create mode 100644 src/completion/list_mem_page_size.py (limited to 'interface-definitions/include') diff --git a/interface-definitions/include/unformat_log2_page_size.xml.i b/interface-definitions/include/unformat_log2_page_size.xml.i index 240fd5bb8..ce6f70dc8 100644 --- a/interface-definitions/include/unformat_log2_page_size.xml.i +++ b/interface-definitions/include/unformat_log2_page_size.xml.i @@ -1,6 +1,7 @@ default default-hugepage + default @@ -10,20 +11,7 @@ default-hugepage Default huge-page - - <number>K - Kilobyte - - - <number>M - Megabyte - - - <number>G - Gigabyte - - - (default|default-hugepage|\d+K|\d+M|\d+G) + (default|default-hugepage|4K|8K|1024K|64K|256K|2048K|4096K|16384K|262144K|1048576K|16777216K|1M|2M|4M|16M|256M|1024M|16384M|1G|16G) diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 1f1cb44cc..e0784fc43 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -774,7 +774,9 @@ Default hugepage size - #include + + + diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 08d69bba2..681d25578 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -16,12 +16,16 @@ # 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` @@ -211,6 +215,96 @@ def cli_ifaces_lcp_kernel_list( 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""" diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index 86b00da44..9f28c970f 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -19,7 +19,9 @@ import os import re +import sys import unittest +from collections import defaultdict from json import loads @@ -30,6 +32,9 @@ from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.utils.process import rc_cmd +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'] @@ -37,6 +42,38 @@ 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: @@ -60,12 +97,13 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10']) def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - # delete test config - self.cli_delete(base_path) - self.cli_commit() + 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() self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) @@ -1064,6 +1102,32 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): for config_entry in config_entries: self.assertIn(config_entry, config) + def test_13_mem_page_size(self): + sizes = ['default', 'default-hugepage'] + list_mem_page_size() + for size in sizes: + self.cli_set(base_path + ['settings', 'buffers', 'page-size', size]) + self.cli_set(base_path + ['settings', 'statseg', 'page-size', size]) + self.cli_set( + base_path + ['settings', 'memory', 'main-heap-page-size', size] + ) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['buffers']['page-size'], size) + self.assertEqual(conf['statseg']['page-size'], size) + 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: + 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) + 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 . +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/conf_mode/vpp.py b/src/conf_mode/vpp.py index ec6873e0d..aea0c1754 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -39,7 +39,11 @@ from vyos.vpp import control_host from vyos.vpp.config_deps import deps_xconnect_dict from vyos.vpp.config_verify import verify_dev_driver from vyos.vpp.config_filter import iface_filter_eth -from vyos.vpp.utils import EthtoolGDrvinfo +from vyos.vpp.utils import ( + EthtoolGDrvinfo, + human_page_memory_to_bytes, + human_memory_to_bytes, +) from vyos.vpp.configdb import JSONStorage airbag.enable() @@ -252,18 +256,6 @@ def get_config(config=None): return config -def convert_to_int(val): - rates = { - 'K': 1024, - 'M': 1024**2, - 'G': 1024**3, - } - try: - return int(val) - except ValueError: - return int(val[:-1]) * rates[val[-1]] - - def verify_memory(settings): memory_available: int = virtual_memory().available cpus: int = get_core_count() @@ -283,12 +275,24 @@ def verify_memory(settings): ) memory_required += netlink_buffer_size - memory_main_heap = convert_to_int( + memory_main_heap = human_memory_to_bytes( settings.get('memory', {}).get('main_heap_size', '1G') ) - memory_required += memory_main_heap - statseg_size = convert_to_int(settings.get('statseg', {}).get('size', '96M')) + memory_main_heap_page_size = settings.get('memory', {}).get( + 'main_heap_page_size', 0 + ) + if memory_main_heap_page_size: + memory_main_heap_page_size = human_page_memory_to_bytes( + memory_main_heap_page_size + ) + memory_required += (memory_main_heap + memory_main_heap_page_size - 1) & ~( + memory_main_heap_page_size - 1 + ) + else: + memory_required += memory_main_heap + + statseg_size = human_memory_to_bytes(settings.get('statseg', {}).get('size', '96M')) memory_required += statseg_size if memory_available < memory_required: @@ -452,6 +456,17 @@ def verify(config): raise ConfigError('"cpu corelist-workers" is not correct') verify_memory(config['settings']) + if 'host_resources' in config['settings']: + if ( + 'nr_hugepages' in config['settings']['host_resources'] + and 'max_map_count' in config['settings']['host_resources'] + ): + if int(config['settings']['host_resources']['max_map_count']) < 2 * int( + config['settings']['host_resources']['nr_hugepages'] + ): + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' + ) # Check if deleted interfaces are not xconnect memebrs for iface_config in config.get('removed_ifaces', []): -- cgit v1.2.3 From fa293316e679645e6397697428cee96e32e346fc Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Wed, 23 Apr 2025 15:11:16 +0300 Subject: T7181: VPP Static and dynamic NAT New CLI for static and dynamic NAT: ``` set vpp nat44 interface outside # multi set vpp nat44 interface inside # multi set vpp nat44 address-pool translation interface # multi set vpp nat44 address-pool translation address
# multi set vpp nat44 address-pool twice-nat interface # multi set vpp nat44 address-pool twice-nat address
# multi set vpp nat44 static rule external address
set vpp nat44 static rule external port set vpp nat44 static rule local address
set vpp nat44 static rule local port set vpp nat44 static rule protocol set vpp nat44 static rule options twice-nat set vpp nat44 static rule options self-twice-nat set vpp nat44 static rule options out-to-in-only set vpp nat44 static rule options twice-nat-address
set vpp nat44 exclude rule protocol set vpp nat44 exclude rule local-port set vpp nat44 exclude rule local-address
set vpp nat44 exclude rule external-interface ``` Settings: ``` set vpp settings nat44 session-limit # default 64512 set vpp settings nat44 timeout udp # default 300 set vpp settings nat44 timeout tcp-established # default 7440 set vpp settings nat44 timeout tcp-transitory # default 240 set vpp settings nat44 timeout icmp # default 60 set vpp settings nat44 workers set vpp settings nat44 no-forwarding ``` --- data/config-mode-dependencies/vyos-vpp.json | 17 +- .../include/vpp/nat_address_range.xml.i | 20 + .../include/vpp/nat_interface.xml.i | 11 + .../include/vpp/nat_protocol.xml.i | 30 ++ interface-definitions/vpp.xml.in | 293 +++++++++---- op-mode-definitions/show_vpp_nat44.xml.in | 12 + python/vyos/vpp/control_vpp.py | 50 ++- python/vyos/vpp/nat/nat44.py | 210 +++++----- src/conf_mode/vpp.py | 48 ++- src/conf_mode/vpp_nat.py | 458 +++++++++++++++++++++ src/conf_mode/vpp_nat_source.py | 119 ------ src/conf_mode/vpp_nat_static.py | 200 --------- src/op_mode/show_vpp_nat44.py | 91 +++- 13 files changed, 1043 insertions(+), 516 deletions(-) create mode 100644 interface-definitions/include/vpp/nat_address_range.xml.i create mode 100644 interface-definitions/include/vpp/nat_interface.xml.i create mode 100644 interface-definitions/include/vpp/nat_protocol.xml.i create mode 100644 src/conf_mode/vpp_nat.py delete mode 100644 src/conf_mode/vpp_nat_source.py delete mode 100644 src/conf_mode/vpp_nat_static.py (limited to 'interface-definitions/include') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 307e763b9..46c1ee9f2 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -10,43 +10,42 @@ "vpp_interfaces_loopback": ["vpp_interfaces_loopback"], "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], - "vpp_nat_static": ["vpp_nat_static"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_bonding": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ethernet": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_geneve": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_gre": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_ipip": { "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_loopback": { - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] }, "vpp_interfaces_vxlan": { "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], - "vpp_nat_source": ["vpp_nat_source"], + "vpp_nat": ["vpp_nat"], "vpp_kernel_interface": ["vpp_kernel-interfaces"] } } 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 @@ + + + + IP address or range + + ipv4 + IPv4 address + + + ipv4range + IPv4 address range + + + + + + + + + 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 @@ + + + + Add IP address from an interface + + + + + + + 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 @@ + + + + Protocol + + tcp udp icmp all + + + all + All protocols (TCP, UDP, and ICMP) + + + icmp + Internet Control Message Protocol (ICMP) + + + tcp + Transmission Control Protocol (TCP) + + + udp + User Datagram Protocol (UDP) + + + (tcp|udp|icmp|all) + + + all + + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 263f9cb91..b1ae348da 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -788,6 +788,98 @@ + + + NAT settings + + + + + NAT44 session timeouts + + + + + ICMP timeout + + u32 + Timeout in seconds (default: 60) + + + 60 + + + + TCP established timeout + + u32 + TCP established timeout in seconds (default: 7440) + + + 7440 + + + + TCP transitory timeout + + u32 + Timeout in seconds (default: 240) + + + 240 + + + + UDP timeout + + u32 + Timeout in seconds (default: 60) + + + 60 + + + + + + Maximum number of sessions per thread + + u32 + Number of sessions + + + + + Number of sessions must be between 1 and 4294967295 + + 64512 + + + + List of NAT workers + + <id> + Worker id + + + <idN>-<idM> + Worker id range (use '-' as delimiter) + + + + + Not a valid value or range + + + + + + Do not forward packets which do not match existing NAT translations (static or dynamic) + + + + + Physical memory settings @@ -842,73 +934,65 @@ - + NAT44 + 320 - + - Source NAT setting - 320 + NAT interface setting - + NAT inside interface - any + - + NAT outside interface - any + + + + + + NAT address pool + + - Outside NAT IP (source NAT only) + NAT translation pool - - - IP address, subnet, or range - - masquerade - - - ipv4 - IPv4 address to match - - - ipv4range - IPv4 address range to match - - - masquerade - NAT to the primary address of outbound-interface - - - - - (masquerade) - - - + #include + #include + + + + + NAT twice-nat pool + + + #include + #include - + - Static NAT settings - 321 + Static NAT rules @@ -920,6 +1004,7 @@ + #include NAT external parameters @@ -940,50 +1025,6 @@ #include - - - Protocol to NAT - - tcp udp icmp all - - - all - All protocols (TCP, UDP, and ICMP) - - - icmp - Internet control message protocol - - - tcp - Transmission control protocol - - - udp - user datagram protocol - - - (tcp|udp|icmp|all) - - - all - - - - NAT outside interface - - - - - - - - NAT inside interface - - - - - NAT local parameters @@ -1004,6 +1045,96 @@ #include + + + NAT static mapping options + + + + + Rewrite source IP addresses on packets sent from outside to inside + + + + + + Rewrite source IP addresses on packets sent only from a local address to an external address + + + + + + Only apply rule for traffic from outside to inside interfaces + + + + + + Force use of specific IP address from twice-nat address pool + + ipv4 + IPv4 address + + + + + + + + + #include + + + + + + + Exclude packets matching these rules from NAT + + + + + Rule number + + u32 + Number of rule + + + + + + IP address of the internal (local) device + + ipv4 + IPv4 address + + + + + + + + + Port number used by connection on internal device + + u32:1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + #include + + + External interface + + + + + #include diff --git a/op-mode-definitions/show_vpp_nat44.xml.in b/op-mode-definitions/show_vpp_nat44.xml.in index f2e77267a..96d53fba9 100644 --- a/op-mode-definitions/show_vpp_nat44.xml.in +++ b/op-mode-definitions/show_vpp_nat44.xml.in @@ -27,6 +27,18 @@ sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_summary + + + Show VPP NAT44 pool addresses + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_addresses + + + + Show VPP NAT44 interfaces + + sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_interfaces + diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 9d45bbd91..5db0ea560 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2023 VyOS Inc. +# 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 @@ -439,6 +439,54 @@ class VPPControl: 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_nat_timeouts( + self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int + ) -> None: + """Set NAT 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_client.api.nat_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) + + @_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 diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py index 97c4518e6..e01e05153 100644 --- a/python/vyos/vpp/nat/nat44.py +++ b/python/vyos/vpp/nat/nat44.py @@ -18,16 +18,20 @@ 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, - interface_in: str, - interface_out: str, - translation_pool: str, - ): - self.interface_in = interface_in - self.interface_out = interface_out - self.translation_pool = translation_pool + def __init__(self): self.vpp = VPPControl() def enable_nat44_ed(self): @@ -40,6 +44,10 @@ class Nat44: """ 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: @@ -49,148 +57,164 @@ class Nat44: """ self.vpp.api.nat44_ei_plugin_enable_disable(enable=True) - def enable_nat44_forwarding(self): - """Enable NAT44 forwarding - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44() - nat44.enable_nat44_forwarding() - """ - self.vpp.api.nat44_forwarding_enable_disable(enable=True) - - def disable_nat44_forwarding(self): - """Disable NAT44 forwarding - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44() - nat44.disable_nat44_forwarding() - """ - self.vpp.api.nat44_forwarding_enable_disable(enable=False) - - def add_nat44_out_interface(self): - """Add NAT44 output interface - Example: - from vyos.vpp.nat import Nat44 - nat44 = Nat44('eth0') - nat44.add_nat44_out_interface() - """ - self.vpp.api.nat44_ed_add_del_output_interface( - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), - is_add=True, - ) - - def delete_nat44_out_interface(self): - """Delete NAT44 output interface""" - self.vpp.api.nat44_ed_add_del_output_interface( - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), - is_add=False, - ) - - def add_nat44_interface_inside(self): + def add_nat44_interface_inside(self, interface_in): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x20, - sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), is_add=True, ) - def delete_nat44_interface_inside(self): + def delete_nat44_interface_inside(self, interface_in): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x20, - sw_if_index=self.vpp.get_sw_if_index(self.interface_in), + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), is_add=False, ) - def add_nat44_interface_outside(self): + def add_nat44_interface_outside(self, interface_out): """Add NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x10, - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), is_add=True, ) - def delete_nat44_interface_outside(self): + def delete_nat44_interface_outside(self, interface_out): """Delete NAT44 interface""" self.vpp.api.nat44_interface_add_del_feature( - flags=0x10, - sw_if_index=self.vpp.get_sw_if_index(self.interface_out), + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), is_add=False, ) - def add_nat44_address_range(self): + def add_nat44_address_range(self, addresses, twice_nat): """Add NAT44 address range""" - if '-' not in self.translation_pool and self.translation_pool != 'masquerade': - first_ip_address = last_ip_address = self.translation_pool + if '-' not in addresses: + first_ip_address = last_ip_address = addresses else: - first_ip_address, last_ip_address = self.translation_pool.split('-') + 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): + def delete_nat44_address_range(self, addresses, twice_nat): """Delete NAT44 address range""" - if '-' not in self.translation_pool and self.translation_pool != 'masquerade': - first_ip_address = last_ip_address = self.translation_pool + if '-' not in addresses: + first_ip_address = last_ip_address = addresses else: - first_ip_address, last_ip_address = self.translation_pool.split('-') + 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 enable_ipfix(self): - """Enable NAT44 IPFIX logging""" - self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) - - -class Nat44Static(Nat44): - def __init__(self): - self.vpp = VPPControl() - - def add_inside_interface(self, interface_in): - self.interface_in = interface_in - self.add_nat44_interface_inside() - - def delete_inside_interface(self, interface_in): - self.interface_in = interface_in - self.delete_nat44_interface_inside() - - def add_outside_interface(self, interface_out): - self.interface_out = interface_out - self.add_nat44_interface_outside() + 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_outside_interface(self, interface_out): - self.interface_out = interface_out - self.delete_nat44_interface_outside() + 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 + 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, - flags=0x08 if not (protocol or local_port) else 0x00, + 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 + 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, - flags=0x08 if not (protocol or local_port) else 0x00, + 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 enable_ipfix(self): + """Enable NAT44 IPFIX logging""" + self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index ca7d19160..7f9adc68b 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -136,10 +136,8 @@ def get_config(config=None): set_dependents('ethernet', conf, removed_iface) # NAT dependency - if conf.exists(['vpp', 'nat44', 'source']): - set_dependents('vpp_nat_source', conf) - if conf.exists(['vpp', 'nat44', 'static']): - set_dependents('vpp_nat_static', conf) + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) if not conf.exists(base): return { @@ -424,6 +422,7 @@ def verify(config): 'Only one multipoint GRE tunnel is allowed from the same source address' ) + workers = 0 if 'cpu' in config['settings']: if ( 'corelist_workers' in config['settings']['cpu'] @@ -496,6 +495,22 @@ def verify(config): if not all(el in cpus_available for el in all_core_numbers): raise ConfigError('"cpu corelist-workers" is not correct') + workers = len(all_core_numbers) + + if 'workers' in config['settings']['nat44']: + nat_workers = [] + for worker_range in config['settings']['nat44']['workers']: + worker_numbers = worker_range.split('-') + if int(worker_numbers[0]) > int(worker_numbers[-1]): + raise ConfigError( + f'Range for "nat44 workers {worker_range}" is not correct' + ) + nat_workers.extend( + range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1) + ) + if not all(el in list(range(workers)) for el in nat_workers): + raise ConfigError('"nat44 workers" is not correct') + verify_memory(config['settings']) if 'host_resources' in config['settings']: if ( @@ -716,6 +731,31 @@ def apply(config): # 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_nat_timeouts( + icmp=int(nat44_settings.get('timeout').get('icmp')), + udp=int(nat44_settings.get('timeout').get('udp')), + tcp_established=int(nat44_settings.get('timeout').get('tcp_established')), + tcp_transitory=int(nat44_settings.get('timeout').get('tcp_transitory')), + ) + + 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) + # Save persistent config if 'persist_config' in config and config['persist_config']: persist_config.write('eth_ifaces', config['persist_config']) diff --git a/src/conf_mode/vpp_nat.py b/src/conf_mode/vpp_nat.py new file mode 100644 index 000000000..d38599388 --- /dev/null +++ b/src/conf_mode/vpp_nat.py @@ -0,0 +1,458 @@ +#!/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, + ) + + # 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 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: + 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): + 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 __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_source.py b/src/conf_mode/vpp_nat_source.py deleted file mode 100644 index 40b16a3c8..000000000 --- a/src/conf_mode/vpp_nat_source.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/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.config import Config -from vyos import ConfigError -from vyos.vpp.nat.nat44 import Nat44 - - -def get_config(config=None) -> dict: - if config: - conf = config - else: - conf = Config() - - base = ['vpp', 'nat44', 'source'] - - # 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 dicitonary per interface 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 - - if effective_config: - config.update({'effective': effective_config}) - - return config - - -def verify(config): - if 'remove' in config: - return None - - required_keys = {'inside_interface', 'outside_interface'} - 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('_', '-')}" - ) - - if not config.get('translation', {}).get('address'): - raise ConfigError('Translation requires address') - - if config.get('translation', {}).get('address') == 'masquerade': - raise ConfigError('Masquerade is not implemented') - - -def generate(config): - pass - - -def apply(config): - # Delete NAT source - if 'effective' in config: - remove_config = config.get('effective') - interface_in = remove_config.get('inside_interface') - interface_out = remove_config.get('outside_interface') - translation_address = remove_config.get('translation', {}).get('address') - - n = Nat44(interface_in, interface_out, translation_address) - n.delete_nat44_out_interface() - n.delete_nat44_interface_inside() - n.delete_nat44_address_range() - - if 'remove' in config: - return None - - # Add NAT44 - interface_in = config.get('inside_interface') - interface_out = config.get('outside_interface') - translation_address = config.get('translation', {}).get('address') - - n = Nat44(interface_in, interface_out, translation_address) - n.enable_nat44_ed() - n.enable_nat44_forwarding() - n.add_nat44_out_interface() - # n.add_nat44_interface_inside() - n.add_nat44_address_range() - - -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_static.py b/src/conf_mode/vpp_nat_static.py deleted file mode 100644 index b890ea150..000000000 --- a/src/conf_mode/vpp_nat_static.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/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.configdiff import Diff -from vyos.configdiff import get_config_diff -from vyos.configdict import node_changed -from vyos.config import Config -from vyos import ConfigError -from vyos.vpp.nat.nat44 import Nat44Static - - -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', 'static'] - - # 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 per interface 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 - - in_iface_add = [] - in_iface_del = [] - out_iface_add = [] - out_iface_del = [] - - changed_rules = node_changed( - conf, - base + ['rule'], - key_mangling=('-', '_'), - recursive=True, - expand_nodes=Diff.DELETE | Diff.ADD, - ) - diff = get_config_diff(conf) - - for rule in changed_rules: - base_rule = base + ['rule', rule] - tmp = node_changed( - conf, - base_rule, - key_mangling=('-', '_'), - recursive=True, - expand_nodes=Diff.DELETE | Diff.ADD, - ) - - if 'inside_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['inside-interface']) - in_iface_add.append(new) if new else None - in_iface_del.append(old) if old else None - if 'outside_interface' in tmp: - new, old = diff.get_value_diff(base_rule + ['outside-interface']) - out_iface_add.append(new) if new else None - out_iface_del.append(old) if old else None - - final_in_iface_add = list(set(in_iface_add) - set(in_iface_del)) - final_in_iface_del = list(set(in_iface_del) - set(in_iface_add)) - final_out_iface_add = list(set(out_iface_add) - set(out_iface_del)) - final_out_iface_del = list(set(out_iface_del) - set(out_iface_add)) - - config.update( - { - 'in_iface_add': final_in_iface_add, - 'in_iface_del': final_in_iface_del, - 'out_iface_add': final_out_iface_add, - 'out_iface_del': final_out_iface_del, - 'changed_rules': changed_rules, - } - ) - - if effective_config: - config.update({'effective': effective_config}) - - return config - - -def verify(config): - if 'remove' in config: - return None - - required_keys = {'inside_interface', 'outside_interface'} - for rule, rule_config in config['rule'].items(): - missing_keys = required_keys - rule_config.keys() - if missing_keys: - raise ConfigError( - f"Required options are missing: {', '.join(missing_keys).replace('_', '-')} in rule {rule}" - ) - - if not rule_config.get('local', {}).get('address'): - raise ConfigError(f'Local settings require address in rule {rule}') - - if not rule_config.get('external', {}).get('address'): - raise ConfigError(f'External settings require address in rule {rule}') - - 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( - 'Source and destination ports must either both be specified, or neither must be specified' - ) - - -def generate(config): - pass - - -def apply(config): - n = Nat44Static() - - # Delete inside interfaces - for interface in config['in_iface_del']: - n.delete_inside_interface(interface) - # Delete outside interfaces - for interface in config['out_iface_del']: - n.delete_outside_interface(interface) - # Delete NAT static mapping rules - for rule in config['changed_rules']: - if rule in config.get('effective', {}).get('rule', {}): - rule_config = config['effective']['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')], - ) - - if 'remove' in config: - return None - - # Add NAT44 static mapping rules - n.enable_nat44_ed() - for interface in config['in_iface_add']: - n.add_inside_interface(interface) - for interface in config['out_iface_add']: - n.add_outside_interface(interface) - for rule in config['changed_rules']: - if rule in config.get('rule', {}): - rule_config = config['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')], - ) - - -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_nat44.py b/src/op_mode/show_vpp_nat44.py index fcf28f76a..d0569ac49 100644 --- a/src/op_mode/show_vpp_nat44.py +++ b/src/op_mode/show_vpp_nat44.py @@ -33,6 +33,15 @@ protocol_map = { 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""" @@ -50,6 +59,16 @@ def _verify(func): 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] = [] @@ -108,12 +127,30 @@ def _get_formatted_output_sessions(sessions_list): print('\n') -def _get_raw_output_static_rules(vpp_api): - nat_static_dump = vpp_api.nat44_static_mapping_dump() - rules_list = [ - json.loads(json.dumps(rule._asdict(), default=str)) for rule in nat_static_dump - ] - return rules_list +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): @@ -124,8 +161,16 @@ def _get_formatted_output_rules(rules_list): local_address = rule.get('local_ip_address') local_port = rule.get('local_port') or '' protocol = protocol_map[rule.get('protocol', 0)] - - values = [external_address, external_port, local_address, local_port, protocol] + 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', @@ -133,6 +178,7 @@ def _get_formatted_output_rules(rules_list): 'Local address', 'Local port', 'Protocol', + 'Options', ] out = sorted(data_entries, key=lambda x: x[2]) return tabulate(out, headers=headers, tablefmt='simple') @@ -159,7 +205,8 @@ def show_summary(raw: bool): @_verify def show_static(raw: bool): vpp = VPPControl() - rules_list: list[dict] = _get_raw_output_static_rules(vpp.api) + nat_static_dump = vpp.api.nat44_static_mapping_dump() + rules_list: list[dict] = _get_raw_output(nat_static_dump) if raw: return rules_list @@ -168,6 +215,32 @@ def show_static(raw: bool): 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__]) -- cgit v1.2.3 From 151afffe15ce89755f3c3f81a9d2c647e487f647 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 13 May 2025 17:33:35 +0300 Subject: T7419: VPP ACL implementation (#31) CLI: ``` set vpp acl ip tag-name rule action set vpp acl ip tag-name rule source prefix set vpp acl ip tag-name rule source port set vpp acl ip tag-name rule destination prefix set vpp acl ip tag-name rule destination port set vpp acl ip tag-name rule protocol set vpp acl ip tag-name rule tcp-flags set vpp acl ip tag-name rule tcp-flags not set vpp acl ip interface input acl-tag tag-name set vpp acl ip interface output acl-tag tag-name set vpp acl macip tag-name rule prefix set vpp acl macip tag-name rule mac-address set vpp acl macip tag-name rule mac-mask set vpp acl macip tag-name rule action set vpp acl macip interface tag-name ``` OP mode ``` show vpp acl ip tag-name show vpp acl ip interface show vpp acl macip tag-name show vpp acl macip interface ``` --- data/config-mode-dependencies/vyos-vpp.json | 8 + data/templates/vpp/startup.conf.j2 | 2 + .../include/vpp/acl_common_interface_ip_rule.xml.i | 25 ++ .../include/vpp/acl_port_range.xml.i | 18 + interface-definitions/include/vpp/acl_prefix.xml.i | 20 ++ .../include/vpp/acl_tcp_flags.xml.i | 50 +++ interface-definitions/vpp.xml.in | 254 ++++++++++++- op-mode-definitions/vpp_acl.xml.in | 65 ++++ python/vyos/vpp/acl/__init__.py | 3 + python/vyos/vpp/acl/acl.py | 106 ++++++ src/conf_mode/vpp.py | 4 + src/conf_mode/vpp_acl.py | 393 +++++++++++++++++++++ src/conf_mode/vpp_interfaces_bonding.py | 10 + src/conf_mode/vpp_interfaces_gre.py | 10 + src/conf_mode/vpp_interfaces_ipip.py | 10 + src/conf_mode/vpp_interfaces_loopback.py | 10 + src/conf_mode/vpp_interfaces_vxlan.py | 10 + src/op_mode/vpp_acl.py | 342 ++++++++++++++++++ 18 files changed, 1338 insertions(+), 2 deletions(-) create mode 100644 interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i create mode 100644 interface-definitions/include/vpp/acl_port_range.xml.i create mode 100644 interface-definitions/include/vpp/acl_prefix.xml.i create mode 100644 interface-definitions/include/vpp/acl_tcp_flags.xml.i create mode 100644 op-mode-definitions/vpp_acl.xml.in create mode 100644 python/vyos/vpp/acl/__init__.py create mode 100644 python/vyos/vpp/acl/acl.py create mode 100644 src/conf_mode/vpp_acl.py create mode 100644 src/op_mode/vpp_acl.py (limited to 'interface-definitions/include') diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json index 0f1d8af34..2a5613559 100644 --- a/data/config-mode-dependencies/vyos-vpp.json +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -10,41 +10,48 @@ "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_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"] @@ -52,6 +59,7 @@ "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/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 index 589afa67b..79a551338 100644 --- a/data/templates/vpp/startup.conf.j2 +++ b/data/templates/vpp/startup.conf.j2 @@ -114,6 +114,8 @@ plugins { plugin crypto_openssl_plugin.so { enable } {% endif %} # plugin wireguard_plugin.so { enable } + # ACL + plugin acl_plugin.so { enable } } linux-cp { 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 @@ + + + + ACL rule (tag) number + + u32 + Number + + + + + Number must be between 1 and 4294967295 + + + + + ACL tag name + + vpp acl ip tag-name + + + + + + 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 @@ + + + + Port number or range + + u32:1-65535 + Numeric IP port + + + range + Numbered port range (e.g., 1001-1005) + + + + + + + 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 @@ + + + + IP prefix + + ipv4net + IPv4 prefix + + + ipv6net + IPv6 prefix + + + + + + + + + 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 @@ + + + + Synchronise flag + + + + + + Acknowledge flag + + + + + + Finish flag + + + + + + Reset flag + + + + + + Urgent flag + + + + + + Push flag + + + + + + Explicit Congestion Notification flag + + + + + + Congestion Window Reduced flag + + + + diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index 368405a02..dab0ea308 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -942,7 +942,7 @@ Carrier-grade NAT (CGNAT) - 321 + 331 @@ -1060,7 +1060,7 @@ NAT44 - 320 + 330 @@ -1265,6 +1265,256 @@ + + + Access Control Lists (ACLs) + 332 + + + + + Access Control List by IPv4/IPv6 + + + + + ACL tag name + + txt + Tag name + + + + #include + + + Rule number for Access control element (ACE) + + u32 + Number of ACE + + + + + Rule number must be between 1 and 4294967295 + + + #include + + + Rule action + + permit deny permit-reflect + + + permit + Permit matching traffic + + + deny + Deny matching traffic + + + permit-reflect + Permit the matching outbound traffic and allow the reverse traffic + + + (permit|deny|permit-reflect) + + + + + + Source parameters + + + #include + #include + + + + + Destination parameters + + + #include + #include + + + + + Protocol + + + all + + + all + All IP protocols + + + <protocol> + IP protocol name + + + + + + all + + + + TCP flags + + + #include + + + Match flags not set + + + #include + + + + + + + + + + + Apply an ACL to an interface + + + + + + + + Input direction + + + #include + + + + + Output direction + + + #include + + + + + + + + + Access Control List by mac address + + + + + ACL tag name + + txt + ACL name + + + + #include + + + Rule number for Access control element (ACE) + + u32 + Number of ACE + + + + + Rule number must be between 1 and 4294967295 + + + #include + + + Rule action + + permit deny + + + permit + Permit matching traffic + + + deny + Deny matching traffic + + + (permit|deny) + + + + #include + + + Source IP prefix + + + + + Source MAC address + + macaddr + MAC address + + + + + + + + + Source MAC mask (default ff:ff:ff:ff:ff:ff) + + macaddr + MAC mask + + + + + + ff:ff:ff:ff:ff:ff + + + + + + + + Apply an ACL to an input interface + + + + + + + + ACL tag name + + vpp acl macip tag-name + + + + + + + + + VPP kernel interface settings 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 @@ + + + + + + + + + Show VPP ACL information + + + + + Show VPP ACL by IPv4/IPv6 + + + + + Show specified VPP ACL + + vpp acl ip tag-name + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls --tag-name="$6" + + + + Show VPP ACL interfaces + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_interfaces + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls + + + + Show VPP ACL by macip + + + + + Show specified VPP ACL + + vpp acl macip tag-name + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls --tag-name="$6" + + + + Show VPP ACL interfaces + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_interfaces + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls + + + sudo ${vyos_op_scripts_dir}/vpp_acl.py show_all_acls + + + + + + 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/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 3718f3897..ed0e80c2c 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -141,6 +141,10 @@ def get_config(config=None): if conf.exists(['vpp', 'nat', 'cgnat']): set_dependents('vpp_nat_cgnat', conf) + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + if not conf.exists(base): return { 'removed_ifaces': removed_ifaces, diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py new file mode 100644 index 000000000..28eae485b --- /dev/null +++ b/src/conf_mode/vpp_acl.py @@ -0,0 +1,393 @@ +#!/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, + ) + + # 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: + 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): + 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 index 7ab1abb24..2b17acb24 100644 --- a/src/conf_mode/vpp_interfaces_bonding.py +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -139,6 +139,16 @@ def get_config(config=None) -> dict: 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 diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py index 823f3ff4d..f6da2d55d 100644 --- a/src/conf_mode/vpp_interfaces_gre.py +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -110,6 +110,16 @@ def get_config(config=None) -> dict: 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 diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py index c52e7c997..f40a4e243 100644 --- a/src/conf_mode/vpp_interfaces_ipip.py +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -109,6 +109,16 @@ def get_config(config=None) -> dict: 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 diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py index f0bcdfb18..2b37b5e1c 100644 --- a/src/conf_mode/vpp_interfaces_loopback.py +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -99,6 +99,16 @@ def get_config(config=None) -> dict: 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 diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py index 963cd90ec..065a8eafb 100644 --- a/src/conf_mode/vpp_interfaces_vxlan.py +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -116,6 +116,16 @@ def get_config(config=None) -> dict: 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 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) -- cgit v1.2.3 From 6eb93578a33f27007c4ecda8b71efbd880a97653 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:36:18 +0300 Subject: T7424: Refactor resource validation and broaden cases (#38) * T7424: Refactor and extend resource usage verification on commit for VPP CLI T7424: Fix ruff errors * T7424: Implement check for smoke tests runtime; reduce resource requirements for test environments T7424: Fix errors in calculating the skipped and reserved CPU cores; Adjust default main heap size value. * T7424: Refactor the CPU checks logic; Add total CPU usage check T7424: Fix CPU reserve and skip cores calculations; Add total CPU usage check T7424: Refactor smoketests to reflect new logic * T7424: Refactor the CPU and memory checks logic --------- Co-authored-by: oniko94 --- .../include/vpp_host_resources.xml.i | 4 +- interface-definitions/vpp.xml.in | 7 +- python/vyos/vpp/config_resource_checks/__init__.py | 0 python/vyos/vpp/config_resource_checks/cpu.py | 76 ++++++ python/vyos/vpp/config_resource_checks/memory.py | 152 ++++++++++++ .../config_resource_checks/resource_defaults.py | 62 +++++ python/vyos/vpp/config_verify.py | 228 +++++++++++++++++ smoketest/scripts/cli/test_vpp.py | 28 ++- src/conf_mode/vpp.py | 271 +++++++-------------- 9 files changed, 639 insertions(+), 189 deletions(-) create mode 100644 python/vyos/vpp/config_resource_checks/__init__.py create mode 100644 python/vyos/vpp/config_resource_checks/cpu.py create mode 100644 python/vyos/vpp/config_resource_checks/memory.py create mode 100644 python/vyos/vpp/config_resource_checks/resource_defaults.py (limited to 'interface-definitions/include') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 7f988ce44..109982bef 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -15,7 +15,7 @@ - 1024 + 2048 @@ -28,7 +28,7 @@ - 3096 + 4096 diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in index dab0ea308..fa66d6126 100644 --- a/interface-definitions/vpp.xml.in +++ b/interface-definitions/vpp.xml.in @@ -427,11 +427,11 @@ Skip cores - u32:0-512 + u32:1-512 Skip cores - + @@ -771,12 +771,14 @@ Main heap size #include + 3G Main heap page size #include + 2M @@ -785,6 +787,7 @@ + 2M 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 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..6f360ae74 --- /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 get_resource_defaults + + +# Get default value for reserved cpu cores +reserved_cpus = get_resource_defaults().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..44f0c9358 --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -0,0 +1,152 @@ +# 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 get_resource_defaults + + +# Get default values for resource checks +defaults = get_resource_defaults() + + +def get_total_hugepages_free_memory() -> int: + """ + Returns the total amount of hugepage-backed free memory (in bytes) + as reported by /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) + + hugepages_free = info.get('HugePages_Free') + hugepage_size = info.get('Hugepagesize') * 1024 + + return hugepage_size * hugepages_free + + +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 get_memory_from_kernel_settings(settings: dict) -> int: + hugepage_settings = settings.get('hugepage_size', {}) + + total_bytes = 0 + for size_str, info in hugepage_settings.items(): + count = int(info.get('hugepage_count', 0)) + page_bytes = human_memory_to_bytes(size_str) + total_bytes += count * page_bytes + + return total_bytes + + +def buffer_size(settings: dict) -> int: + numa_count = get_numa_count() + buffers_per_numa = int( + settings.get('buffers', {}).get( + 'buffers_per_numa', defaults.get('buffers_per_numa') + ) + ) + data_size = int( + settings.get('buffers', {}).get('data_size', defaults.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', defaults.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', defaults.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', defaults.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', defaults.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', defaults.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..3c618accd --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py @@ -0,0 +1,62 @@ +# 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. + +import copy +import os + + +__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', +} + + +def get_resource_defaults() -> dict: + resource_map = copy.deepcopy(__default_resource_map) + # Check if current runtime is smoke tests + # Since CI/CD runners are limited in resources, reduce the checks for tests + if is_smoketest(): + resource_map.update(min_memory='6G', min_cpus=2, reserved_cpu_cores=0) + + return resource_map + + +def is_smoketest(): + if os.path.exists('/tmp/vyos.smoketests.hint'): + return True + return False diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 830d4af73..9cfbfb7cd 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -16,9 +16,19 @@ # 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.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 import resource_defaults +from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory + + +# Get default values for resource checks +defaults = resource_defaults.get_resource_defaults() def verify_vpp_remove_kernel_interface(config: dict): @@ -171,3 +181,221 @@ def verify_dev_driver(iface_name: str, driver_type: str) -> bool: 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 = defaults.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 = defaults.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})' + ) + + # Get available HupePage memory to compare with required memory for VPP + # (if it's smketests environment get system kernel settings for HugePages) + if not resource_defaults.is_smoketest(): + available_memory = mem_checks.get_total_hugepages_free_memory() + else: + available_memory = mem_checks.get_memory_from_kernel_settings( + config['kernel_memory_settings'] + ) + + memory_required = mem_checks.total_memory_required(config['settings']) + + # Check if there is a config currently active + # If yes, calculate how much memory it consumes + # and exclude it from required memory + if config.get('effective'): + memory_used = mem_checks.total_memory_required(config['effective']['settings']) + # If we want to reduce memory configs then there is nothing to check + if memory_used > memory_required: + return + memory_required -= memory_used + + if memory_required > available_memory: + raise ConfigError( + 'Not enough free memory to start VPP: ' + f'available: {round(available_memory / 1024 ** 3, 1)} GB, ' + f'required: {round(memory_required / 1024 ** 3, 1)} GB. ' + 'Please add kernel memory options for HugePages 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}' + ) diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py index d8f7f3473..3dfbb4cc8 100755 --- a/smoketest/scripts/cli/test_vpp.py +++ b/smoketest/scripts/cli/test_vpp.py @@ -41,6 +41,7 @@ VPP_CONF = '/run/vpp/vpp.conf' base_path = ['vpp'] driver = 'dpdk' interface = 'eth1' +system_memory_path = ['system', 'option', 'kernel', 'memory'] def get_vpp_config(): @@ -96,6 +97,9 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def setUp(self): self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver]) self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10']) + self.cli_set( + system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] + ) def tearDown(self): try: @@ -110,6 +114,12 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'ethernet', interface, 'address']) self.cli_commit() + # delete kernel memory settings + self.cli_delete( + system_memory_path + ['hugepage-size', '2M', 'hugepage-count', '2048'] + ) + self.cli_commit() + self.assertFalse(os.path.exists(VPP_CONF)) self.assertFalse(process_named_running(PROCESS_NAME)) @@ -1084,15 +1094,25 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): dpdk_options = { 'num-rx-desc': '512', 'num-tx-desc': '512', - 'num-rx-queues': '3', - 'num-tx-queues': '3', + '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']) @@ -1112,7 +1132,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def test_11_vpp_cpu_settings(self): main_core = '2' - workers = '2' + workers = '1' skip_cores = '1' self.cli_set(base_path + ['settings', 'cpu', 'workers', workers]) @@ -1149,7 +1169,7 @@ class TestVPP(VyOSUnitTestSHIM.TestCase): def test_12_vpp_cpu_corelist_workers(self): main_core = '0' - corelist_workers = ['1', '2-3'] + corelist_workers = ['3'] for worker in corelist_workers: self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', worker]) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index d5778c778..757904dd5 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -18,7 +18,6 @@ from pathlib import Path -from psutil import virtual_memory from pyroute2 import IPRoute from vpp_papi import VPPIOError, VPPValueError @@ -28,7 +27,6 @@ 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.utils.cpu import get_core_count, get_available_cpus from vyos.ifconfig import Section from vyos.template import render from vyos.utils.boot import boot_configuration_complete @@ -38,14 +36,22 @@ 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 -from vyos.vpp.config_filter import iface_filter_eth -from vyos.vpp.utils import ( - EthtoolGDrvinfo, - human_page_memory_to_bytes, - human_memory_to_bytes, - bytes_to_human_memory, +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, ) +from vyos.vpp.config_filter import iface_filter_eth +from vyos.vpp.utils import EthtoolGDrvinfo from vyos.vpp.configdb import JSONStorage airbag.enable() @@ -164,7 +170,7 @@ def get_config(config=None): # dictionary retrieved. default_values = conf.get_config_defaults(**config.kwargs, recursive=True) - # delete "xdp-options" from defaults if driver is DPDK + # 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'] @@ -278,63 +284,21 @@ def get_config(config=None): 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_memory(settings): - memory_available: int = virtual_memory().available - cpus: int = get_core_count() - - nr_hugepages = int(settings['host_resources']['nr_hugepages']) - hugepages_memory = nr_hugepages * 2 * 1024**2 - memory_required = hugepages_memory - - buffers_per_numa = int(settings.get('buffers', {}).get('buffers_per_numa', 16384)) - data_size = int(settings.get('buffers', {}).get('data_size', 2048)) - buffers_memory = buffers_per_numa * data_size * cpus - - memory_required += buffers_memory - - netlink_buffer_size = int( - settings.get('lcp', {}).get('netlink', {}).get('rx_buffer_size', 212992) - ) - memory_required += netlink_buffer_size - - memory_main_heap = human_memory_to_bytes( - settings.get('memory', {}).get('main_heap_size', '1G') - ) - - if memory_main_heap < 51 << 20: - # vpp is aborted when we try to use a smaller heap size - raise ConfigError('The main heap size must be greater than or equal to 51M') - - memory_main_heap_page_size = human_page_memory_to_bytes( - settings.get('memory', {}).get('main_heap_page_size', 'default') - ) - - if memory_main_heap_page_size > memory_main_heap: - raise ConfigError( - f'The main heap size must be greater than or equal to page-size({bytes_to_human_memory(memory_main_heap_page_size, "K")})' - ) - - memory_required += (memory_main_heap + memory_main_heap_page_size - 1) & ~( - memory_main_heap_page_size - 1 - ) - - statseg_size = human_memory_to_bytes(settings.get('statseg', {}).get('size', '96M')) - memory_required += statseg_size - - if memory_available < memory_required: - raise ConfigError( - 'Not enough free memory to start VPP:\n' - f'available: {round(memory_available / 1024 ** 3, 1)}GB\n' - f'required: {round(memory_required / 1024 ** 3, 1)}GB' - ) - - 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): @@ -346,12 +310,64 @@ def verify(config): 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 'host_resources' in config['settings']: + if ( + 'nr_hugepages' in config['settings']['host_resources'] + and 'max_map_count' in config['settings']['host_resources'] + ): + if int(config['settings']['host_resources']['max_map_count']) < 2 * int( + config['settings']['host_resources']['nr_hugepages'] + ): + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' + ) + + 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 @@ -372,6 +388,19 @@ def verify(config): 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': @@ -429,126 +458,6 @@ def verify(config): 'Only one multipoint GRE tunnel is allowed from the same source address' ) - workers = 0 - if 'cpu' in config['settings']: - if ( - 'corelist_workers' in config['settings']['cpu'] - or 'workers' in config['settings']['cpu'] - ) and 'main_core' not in config['settings']['cpu']: - raise ConfigError('"cpu main-core" is required but not set!') - - if ( - 'corelist_workers' in config['settings']['cpu'] - and 'workers' in config['settings']['cpu'] - ): - raise ConfigError( - '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!' - ) - - cpus = int(get_core_count()) - skip_cores = 0 - - if 'skip_cores' in config['settings']['cpu']: - skip_cores = int(config['settings']['cpu']['skip_cores']) - # the number of skipped cores should not be more than all CPUs - 1 (for main core) - if skip_cores > cpus - 1: - raise ConfigError( - f'The system does not have enough available CPUs to skip ' - f'(reduce "cpu skip-cores" to {cpus - 1} or less)' - ) - - if 'workers' in config['settings']['cpu']: - # number of worker threads must be not more than - # available CPUs in the system - 1 for main thread - number of skipped cores - # or - 1 (at least) for system processes - workers = int(config['settings']['cpu']['workers']) - available_workers = cpus - 1 - (skip_cores or 1) - if workers > available_workers: - raise ConfigError( - f'The system does not have enough CPUs for {workers} VPP workers ' - f'(reduce to {available_workers} or less)' - ) - - cpus_available = list(map(lambda el: el['cpu'], get_available_cpus())) - # available CPUs are all CPUs without first N skipped cores that will not be used - cpus_available = cpus_available[skip_cores:] - - if 'main_core' in config['settings']['cpu']: - main_core = int(config['settings']['cpu']['main_core']) - - if main_core not in cpus_available: - raise ConfigError(f'"cpu main-core {main_core}" is not available!') - - # CPU main-core must be not included to corelist-workers - if config.get('settings').get('cpu', {}).get('corelist_workers'): - corelist_workers = config['settings']['cpu']['corelist_workers'] - - all_core_numbers = [] - for worker_range in corelist_workers: - core_numbers = worker_range.split('-') - if int(core_numbers[0]) > int(core_numbers[-1]): - raise ConfigError( - f'Range for "cpu corelist-workers {worker_range}" is not correct' - ) - all_core_numbers.extend( - range(int(core_numbers[0]), int(core_numbers[-1]) + 1) - ) - - if main_core in all_core_numbers: - raise ConfigError( - f'"cpu main-core {main_core}" must not be included in the corelist-workers!' - ) - - if not all(el in cpus_available for el in all_core_numbers): - raise ConfigError('"cpu corelist-workers" is not correct') - - workers = len(all_core_numbers) - - if 'workers' in config['settings']['nat44']: - nat_workers = [] - for worker_range in config['settings']['nat44']['workers']: - worker_numbers = worker_range.split('-') - if int(worker_numbers[0]) > int(worker_numbers[-1]): - raise ConfigError( - f'Range for "nat44 workers {worker_range}" is not correct' - ) - nat_workers.extend( - range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1) - ) - if not all(el in list(range(workers)) for el in nat_workers): - raise ConfigError('"nat44 workers" is not correct') - - verify_memory(config['settings']) - if 'host_resources' in config['settings']: - if ( - 'nr_hugepages' in config['settings']['host_resources'] - and 'max_map_count' in config['settings']['host_resources'] - ): - if int(config['settings']['host_resources']['max_map_count']) < 2 * int( - config['settings']['host_resources']['nr_hugepages'] - ): - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' - ) - - if 'statseg' in config['settings']: - _size = human_memory_to_bytes(config['settings']['statseg'].get('size', '96M')) - - if 'size' in config['settings']['statseg']: - if _size < 1 << 20: - raise ConfigError( - 'The statseg size must be greater than or equal to 1M' - ) - - if 'page_size' in config['settings']['statseg']: - _page_size = human_page_memory_to_bytes( - config['settings']['statseg']['page_size'] - ) - if _page_size > _size: - raise ConfigError( - f'The statseg size must be greater than or equal to page-size({bytes_to_human_memory(_page_size, "K")})' - ) - # 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', {}): -- cgit v1.2.3 From 5d0f6aad265f2fa10c7d822e1d135072a6feaf4f Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:44:42 +0200 Subject: T7607: Remove "set vpp settings host-resources nr_hugepages " setting (#43) --- .../include/vpp_host_resources.xml.i | 13 ------------- python/vyos/vpp/config_resource_checks/memory.py | 21 ++++++++++++++++++--- python/vyos/vpp/config_verify.py | 12 ++++++++++++ src/conf_mode/vpp.py | 17 +++++------------ 4 files changed, 35 insertions(+), 28 deletions(-) (limited to 'interface-definitions/include') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 109982bef..6fee0f2f3 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -4,19 +4,6 @@ Host resources control - - - Number of pre-allocated huge pages of the default size - - u32:0-4294967295 - Pages count - - - - - - 2048 - Maximum number of memory map areas a process may have diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index f5c7286bc..64fdf9abe 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -26,10 +26,10 @@ from vyos.vpp.utils import ( from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map -def get_total_hugepages_free_memory() -> int: +def get_hugepages_info() -> dict: """ - Returns the total amount of hugepage-backed free memory (in bytes) - as reported by /proc/meminfo + Returns the information about HugePages + retrieved from /proc/meminfo """ info = {} with open('/proc/meminfo', 'r') as meminfo: @@ -37,13 +37,28 @@ def get_total_hugepages_free_memory() -> int: if line.startswith('Huge'): key, value, *_ = line.strip().split() info[key.rstrip(':')] = int(value) + return info + +def get_total_hugepages_free_memory() -> int: + """ + Returns the total amount of hugepage-backed free memory (in bytes) + """ + info = get_hugepages_info() hugepages_free = info.get('HugePages_Free') hugepage_size = info.get('Hugepagesize') * 1024 return hugepage_size * hugepages_free +def get_hugepages_total() -> 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. diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index cda5cf012..65dfac91a 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -389,3 +389,15 @@ def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: 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_hugepages_total() + + if max_map_count < 2 * hugepages: + raise ConfigError( + 'The max_map_count must be greater than or equal to (2 * HugePages_Total)' + ) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 757904dd5..9b9a3466b 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -49,6 +49,7 @@ from vyos.vpp.config_verify import ( 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 @@ -353,17 +354,8 @@ def verify(config): # Check if available memory is enough for current VPP config verify_vpp_memory(config) - if 'host_resources' in config['settings']: - if ( - 'nr_hugepages' in config['settings']['host_resources'] - and 'max_map_count' in config['settings']['host_resources'] - ): - if int(config['settings']['host_resources']['max_map_count']) < 2 * int( - config['settings']['host_resources']['nr_hugepages'] - ): - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * nr_hugepages)' - ) + 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']) @@ -477,8 +469,9 @@ def generate(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 ' sysctl_config: dict[str, str] = { - 'vm.nr_hugepages': config['settings']['host_resources']['nr_hugepages'], 'vm.max_map_count': config['settings']['host_resources']['max_map_count'], 'vm.hugetlb_shm_group': '0', 'kernel.shmmax': config['settings']['host_resources']['shmmax'], -- cgit v1.2.3 From d37cca0672b71691614da0a0f770eaf0fe117fa4 Mon Sep 17 00:00:00 2001 From: "Nataliia S." <81954790+natali-rs1985@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:33:11 +0200 Subject: T7658: T7656: Changes for max-map-count parameter (#53) * T7658: Increase max-map-count interval and default value to linux default Also input a warning instead of ConfigError in case of "host-resources max-map-count" didn't pass the verification * T7656: Clarify warning message for max-map-count option verification --- interface-definitions/include/vpp_host_resources.xml.i | 6 +++--- python/vyos/vpp/config_verify.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'interface-definitions/include') diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i index 6fee0f2f3..1706c8c87 100644 --- a/interface-definitions/include/vpp_host_resources.xml.i +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -8,14 +8,14 @@ Maximum number of memory map areas a process may have - u32:0-65535 + u32:65535-2147483647 Areas count - + - 4096 + 65535 diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index 65dfac91a..f6c47d0c0 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -19,6 +19,7 @@ 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 @@ -398,6 +399,8 @@ def verify_vpp_host_resources(config: dict): hugepages = mem_checks.get_hugepages_total() if max_map_count < 2 * hugepages: - raise ConfigError( - 'The max_map_count must be greater than or equal to (2 * HugePages_Total)' + 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' ) -- cgit v1.2.3