summaryrefslogtreecommitdiff
path: root/src/conf_mode
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/conf_mode
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/conf_mode')
-rwxr-xr-xsrc/conf_mode/vpp.py2
-rw-r--r--src/conf_mode/vpp_nat_static.py209
2 files changed, 211 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)