diff options
| author | Nataliia S. <81954790+natali-rs1985@users.noreply.github.com> | 2025-04-01 17:30:12 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-01 15:30:12 +0100 |
| commit | 07a3b0f5ae87a2ab400390c9fa0ca632d7815e15 (patch) | |
| tree | dd6c549c9694e14d31c4c47f0649f187d36863b3 /src | |
| parent | af6751ecdf44bde0c6dfb397e5ad128561f93112 (diff) | |
| download | vyos-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-x | src/conf_mode/vpp.py | 2 | ||||
| -rw-r--r-- | src/conf_mode/vpp_nat_static.py | 209 | ||||
| -rw-r--r-- | src/op_mode/show_vpp_nat44.py | 183 |
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) |
