summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2025-04-25 14:52:02 +0300
committerGitHub <noreply@github.com>2025-04-25 14:52:02 +0300
commitf4e02fdff7a7fcb42a1c5fc313e317af63cd1dcd (patch)
tree046587e39d00c2959cd159583b6b4059de16de99 /src
parent4d32bcddda72c1df944470f06b75908b2dc31667 (diff)
parentfa293316e679645e6397697428cee96e32e346fc (diff)
downloadvyos-1x-f4e02fdff7a7fcb42a1c5fc313e317af63cd1dcd.tar.gz
vyos-1x-f4e02fdff7a7fcb42a1c5fc313e317af63cd1dcd.zip
Merge pull request #29 from natali-rs1985/T7181-stati+dynamic
T7181: VPP Static and dynamic NAT
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/vpp.py48
-rw-r--r--src/conf_mode/vpp_nat.py458
-rw-r--r--src/conf_mode/vpp_nat_source.py119
-rw-r--r--src/conf_mode/vpp_nat_static.py200
-rw-r--r--src/op_mode/show_vpp_nat44.py91
5 files changed, 584 insertions, 332 deletions
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__])