summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNataliia S. <81954790+natali-rs1985@users.noreply.github.com>2025-04-01 17:30:12 +0300
committerGitHub <noreply@github.com>2025-04-01 15:30:12 +0100
commit07a3b0f5ae87a2ab400390c9fa0ca632d7815e15 (patch)
treedd6c549c9694e14d31c4c47f0649f187d36863b3 /src
parentaf6751ecdf44bde0c6dfb397e5ad128561f93112 (diff)
downloadvyos-1x-07a3b0f5ae87a2ab400390c9fa0ca632d7815e15.tar.gz
vyos-1x-07a3b0f5ae87a2ab400390c9fa0ca632d7815e15.zip
T7283: VPP add static NAT support (#24)
* T7283: VPP add static NAT support Add static mapping NAT implementation ``` set vpp nat44 static rule 10 outbound-interface 'eth0' set vpp nat44 static rule 10 inbound-interface 'eth1' set vpp nat44 static rule 10 destination address 192.168.122.10 # optional, if not set outbound interface ip address is used set vpp nat44 static rule 10 destination port 6545 # optional set vpp nat44 static rule 10 protocol tcp|udp|icmp|all # optional, defaults to "all" set vpp nat44 static rule 10 translation address 100.64.0.10 set vpp nat44 static rule 10 translation port 64010 # optional ``` * Improve help strings (Daniil Baturin) --------- Co-authored-by: Daniil Baturin <daniil@baturin.org>
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/vpp.py2
-rw-r--r--src/conf_mode/vpp_nat_static.py209
-rw-r--r--src/op_mode/show_vpp_nat44.py183
3 files changed, 394 insertions, 0 deletions
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
index 1f4c6c0f0..ca7d19160 100755
--- a/src/conf_mode/vpp.py
+++ b/src/conf_mode/vpp.py
@@ -138,6 +138,8 @@ def get_config(config=None):
# 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 not conf.exists(base):
return {
diff --git a/src/conf_mode/vpp_nat_static.py b/src/conf_mode/vpp_nat_static.py
new file mode 100644
index 000000000..50384a00b
--- /dev/null
+++ b/src/conf_mode/vpp_nat_static.py
@@ -0,0 +1,209 @@
+#!/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 'inbound_interface' in tmp:
+ new, old = diff.get_value_diff(base_rule + ['inbound-interface'])
+ in_iface_add.append(new) if new else None
+ in_iface_del.append(old) if old else None
+ if 'outbound_interface' in tmp:
+ new, old = diff.get_value_diff(base_rule + ['outbound-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 = {'inbound_interface', 'outbound_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('translation', {}).get('address'):
+ raise ConfigError(f'Translation requires address in rule {rule}')
+
+ has_dest_port = 'port' in rule_config.get('destination', {})
+ has_trans_port = 'port' in rule_config.get('translation', {})
+
+ if not has_trans_port == has_dest_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 inbound interfaces
+ for interface in config['in_iface_del']:
+ n.delete_inbound_interface(interface)
+ # Delete outbound interfaces
+ for interface in config['out_iface_del']:
+ n.delete_outbound_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(
+ iface_out=rule_config.get('outbound_interface'),
+ local_ip=rule_config.get('translation').get('address'),
+ external_ip=rule_config.get('destination', {}).get('address', ''),
+ local_port=int(rule_config.get('translation', {}).get('port', 0)),
+ external_port=int(rule_config.get('destination', {}).get('port', 0)),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ use_iface=(
+ True
+ if not rule_config.get('destination', {}).get('address')
+ else False
+ ),
+ )
+
+ if 'remove' in config:
+ return None
+
+ # Add NAT44 static mapping rules
+ n.enable_nat44_ed()
+ for interface in config['in_iface_add']:
+ n.add_inbound_interface(interface)
+ for interface in config['out_iface_add']:
+ n.add_outbound_interface(interface)
+ for rule in config['changed_rules']:
+ if rule in config.get('rule', {}):
+ rule_config = config['rule'][rule]
+ n.add_nat44_static_mapping(
+ iface_out=rule_config.get('outbound_interface'),
+ local_ip=rule_config.get('translation').get('address'),
+ external_ip=rule_config.get('destination', {}).get('address', ''),
+ local_port=int(rule_config.get('translation', {}).get('port', 0)),
+ external_port=int(rule_config.get('destination', {}).get('port', 0)),
+ protocol=protocol_map[rule_config.get('protocol', 'all')],
+ use_iface=(
+ True
+ if not rule_config.get('destination', {}).get('address')
+ else False
+ ),
+ )
+
+
+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
new file mode 100644
index 000000000..1aae8164e
--- /dev/null
+++ b/src/op_mode/show_vpp_nat44.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+protocol_map = {
+ 0: 'all',
+ 1: 'icmp',
+ 6: 'tcp',
+ 17: 'udp',
+}
+
+
+def _verify(func):
+ """Decorator checks if config for VPP NAT44 exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ base = 'vpp nat44'
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured')
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+def _get_raw_output_sessions(vpp_api):
+ users: list[dict] = vpp_api.nat44_user_dump()
+ sessions_list: list[dict] = []
+ for user in users:
+ ip_address = str(user._asdict().get('ip_address'))
+ user_sessions_dump = vpp_api.nat44_user_session_v3_dump(ip_address=ip_address)
+ user_sessions = [
+ json.loads(json.dumps(session._asdict(), default=str))
+ for session in user_sessions_dump
+ ]
+ sessions_list.extend(user_sessions)
+ return sorted(sessions_list, key=lambda x: x["inside_ip_address"])
+
+
+def _get_formatted_output_sessions(sessions_list):
+ print('NAT44 ED sessions:')
+ print(f'--------------- {len(sessions_list)} sessions ---------------')
+ for session in sessions_list:
+ in_ip_addr = session.get('inside_ip_address')
+ in_port = session.get('inside_port')
+ out_ip_addr = session.get('outside_ip_address')
+ out_port = session.get('outside_port')
+ protocol = protocol_map[session.get('protocol')].upper()
+ last_heard = session.get('last_heard')
+ time_since_last_heard = session.get('time_since_last_heard')
+ total_bytes = session.get('total_bytes')
+ total_pkts = session.get('total_pkts')
+ ext_host_address = session.get('ext_host_address')
+ ext_host_port = session.get('ext_host_port')
+ is_timed_out = session.get('is_timed_out')
+
+ print(f' i2o {in_ip_addr} proto {protocol} port {in_port}')
+ print(f' o2i {out_ip_addr} proto {protocol} port {out_port}')
+ print(f' external host {ext_host_address}:{ext_host_port}')
+ print(
+ f' i2o flow: match: saddr {in_ip_addr} sport {in_port} daddr {ext_host_address} dport {ext_host_port} proto {protocol} rewrite: saddr {out_ip_addr}'
+ + (
+ f' sport {out_port}'
+ if protocol != 'ICMP'
+ else f' daddr {ext_host_address} icmp-id {ext_host_port}'
+ )
+ )
+ print(
+ f' o2i flow: match: saddr {ext_host_address} sport {ext_host_port} daddr {out_ip_addr} dport {out_port} proto {protocol} rewrite: '
+ + (
+ f'daddr {in_ip_addr} dport {in_port}'
+ if protocol != 'ICMP'
+ else f' saddr {ext_host_address} daddr {in_ip_addr} icmp-id {ext_host_port}'
+ )
+ )
+ print(f' last heard {last_heard}')
+ print(f' time since last heard {time_since_last_heard}')
+ print(f' total packets {total_pkts}, total bytes {total_bytes}')
+ if is_timed_out:
+ print(' session timed out')
+ print('\n')
+
+
+def _get_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_rules(vpp, rules_list):
+ data_entries = []
+ for rule in rules_list:
+ dest_address = rule.get('external_ip_address')
+ dest_port = rule.get('external_port') or ''
+ trans_address = rule.get('local_ip_address')
+ trans_port = rule.get('local_port') or ''
+ protocol = protocol_map[rule.get('protocol', 0)]
+ dest_sh_if_index = rule.get('external_sw_if_index')
+
+ vpp_if_name = vpp.get_interface_name(dest_sh_if_index)
+ if vpp_if_name:
+ dest_address = vpp_if_name
+
+ values = [dest_address, dest_port, trans_address, trans_port, protocol]
+ data_entries.append(values)
+ headers = [
+ 'Des_address/interface',
+ 'Dest_port',
+ 'Trans_address',
+ 'Trans_port',
+ 'Protocol',
+ ]
+ out = sorted(data_entries, key=lambda x: x[2])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def show_sessions(raw: bool):
+ vpp = VPPControl()
+ sessions_list: list[dict] = _get_raw_output_sessions(vpp.api)
+
+ if raw:
+ return sessions_list
+
+ else:
+ return _get_formatted_output_sessions(sessions_list)
+
+
+@_verify
+def show_summary(raw: bool):
+ vpp = VPPControl()
+ return vpp.cli_cmd('show nat44 summary').reply
+
+
+@_verify
+def show_static(raw: bool):
+ vpp = VPPControl()
+ rules_list: list[dict] = _get_raw_output_static_rules(vpp.api)
+
+ if raw:
+ return rules_list
+
+ else:
+ return _get_formatted_output_rules(vpp, rules_list)
+
+
+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)