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 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