From 2f2c3fa22478c7ba2e116486d655e07df878cdf4 Mon Sep 17 00:00:00 2001 From: Nicolas Fort Date: Tue, 10 Oct 2023 17:35:30 +0000 Subject: T5643: nat: add interface-groups to nat. Use same cli structure for interface-name|interface-group as in firewall. --- interface-definitions/nat.xml.in | 4 +-- python/vyos/nat.py | 32 ++++++++++++++++---- smoketest/scripts/cli/test_nat.py | 28 ++++++++++-------- src/conf_mode/nat.py | 14 ++++++--- src/migration-scripts/nat/5-to-6 | 62 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 25 deletions(-) create mode 100755 src/migration-scripts/nat/5-to-6 diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index a06ceefb6..0a639bd80 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -14,7 +14,7 @@ #include - #include + #include Inside NAT IP (destination NAT only) @@ -77,7 +77,7 @@ NAT rule number must be between 1 and 999999 - #include + #include Outside NAT IP (source NAT only) diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 9cbc2b96e..0887bfdf1 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -32,14 +32,34 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): translation_str = '' if 'inbound_interface' in rule_conf: - ifname = rule_conf['inbound_interface'] - if ifname != 'any': - output.append(f'iifname "{ifname}"') + operator = '' + if 'interface_name' in rule_conf['inbound_interface']: + iiface = rule_conf['inbound_interface']['interface_name'] + if iiface[0] == '!': + operator = '!=' + iiface = iiface[1:] + output.append(f'iifname {operator} {{{iiface}}}') + else: + iiface = rule_conf['inbound_interface']['interface_group'] + if iiface[0] == '!': + operator = '!=' + iiface = iiface[1:] + output.append(f'iifname {operator} @I_{iiface}') if 'outbound_interface' in rule_conf: - ifname = rule_conf['outbound_interface'] - if ifname != 'any': - output.append(f'oifname "{ifname}"') + operator = '' + if 'interface_name' in rule_conf['outbound_interface']: + oiface = rule_conf['outbound_interface']['interface_name'] + if oiface[0] == '!': + operator = '!=' + oiface = oiface[1:] + output.append(f'oifname {operator} {{{oiface}}}') + else: + oiface = rule_conf['outbound_interface']['interface_group'] + if oiface[0] == '!': + operator = '!=' + oiface = oiface[1:] + output.append(f'oifname {operator} @I_{oiface}') if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': protocol = rule_conf['protocol'] diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 703e5ab28..2f744a2f7 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -82,12 +82,12 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): # or configured destination address for NAT if int(rule) < 200: self.cli_set(src_path + ['rule', rule, 'source', 'address', network]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_100]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'interface-name', outbound_iface_100]) self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) nftables_search.append([f'saddr {network}', f'oifname "{outbound_iface_100}"', 'masquerade']) else: self.cli_set(src_path + ['rule', rule, 'destination', 'address', network]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_200]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'interface-name', outbound_iface_200]) self.cli_set(src_path + ['rule', rule, 'exclude']) nftables_search.append([f'daddr {network}', f'oifname "{outbound_iface_200}"', 'return']) @@ -98,13 +98,15 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): def test_snat_groups(self): address_group = 'smoketest_addr' address_group_member = '192.0.2.1' + interface_group = 'smoketest_ifaces' + interface_group_member = 'bond.99' rule = '100' - outbound_iface = 'eth0' self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member]) self.cli_set(src_path + ['rule', rule, 'source', 'group', 'address-group', address_group]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'interface-group', interface_group]) self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) self.cli_commit() @@ -112,7 +114,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): nftables_search = [ [f'set A_{address_group}'], [f'elements = {{ {address_group_member} }}'], - [f'ip saddr @A_{address_group}', f'oifname "{outbound_iface}"', 'masquerade'] + [f'ip saddr @A_{address_group}', f'oifname @I_{interface_group}', 'masquerade'] ] self.verify_nftables(nftables_search, 'ip vyos_nat') @@ -136,12 +138,12 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): rule_search = [f'dnat to 192.0.2.1:{port}'] if int(rule) < 200: self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_100]) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_100]) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'interface-name', inbound_iface_100]) rule_search.append(f'{inbound_proto_100} sport {port}') rule_search.append(f'iifname "{inbound_iface_100}"') else: self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_200]) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_200]) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'interface-name', inbound_iface_200]) rule_search.append(f'iifname "{inbound_iface_200}"') nftables_search.append(rule_search) @@ -167,7 +169,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): rule = '1000' self.cli_set(dst_path + ['rule', rule, 'destination', 'address', '!192.0.2.1']) self.cli_set(dst_path + ['rule', rule, 'destination', 'port', '53']) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'eth0']) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'interface-name', 'eth0']) self.cli_set(dst_path + ['rule', rule, 'protocol', 'tcp_udp']) self.cli_set(dst_path + ['rule', rule, 'source', 'address', '!192.0.2.1']) self.cli_set(dst_path + ['rule', rule, 'translation', 'address', '192.0.2.1']) @@ -186,7 +188,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_dnat_without_translation_address(self): - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'interface-name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443']) self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) self.cli_set(dst_path + ['rule', '1', 'packet-type', 'host']) @@ -236,13 +238,13 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_set(dst_path + ['rule', '10', 'destination', 'address', dst_addr_1]) self.cli_set(dst_path + ['rule', '10', 'destination', 'port', dest_port]) self.cli_set(dst_path + ['rule', '10', 'protocol', protocol]) - self.cli_set(dst_path + ['rule', '10', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '10', 'inbound-interface', 'interface-name', ifname]) self.cli_set(dst_path + ['rule', '10', 'translation', 'redirect', 'port', redirected_port]) self.cli_set(dst_path + ['rule', '20', 'destination', 'address', dst_addr_1]) self.cli_set(dst_path + ['rule', '20', 'destination', 'port', dest_port]) self.cli_set(dst_path + ['rule', '20', 'protocol', protocol]) - self.cli_set(dst_path + ['rule', '20', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '20', 'inbound-interface', 'interface-name', ifname]) self.cli_set(dst_path + ['rule', '20', 'translation', 'redirect']) self.cli_commit() @@ -266,7 +268,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): weight_4 = '65' dst_port = '443' - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'interface-name', ifname]) self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dst_port]) self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-address']) @@ -276,7 +278,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_1, 'weight', weight_1]) self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_2, 'weight', weight_2]) - self.cli_set(src_path + ['rule', '1', 'outbound-interface', ifname]) + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'interface-name', ifname]) self.cli_set(src_path + ['rule', '1', 'load-balance', 'hash', 'random']) self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_3, 'weight', weight_3]) self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_4, 'weight', weight_4]) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 52a7a71fd..cb97a8662 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -151,8 +151,11 @@ def verify(nat): err_msg = f'Source NAT configuration error in rule {rule}:' if 'outbound_interface' in config: - if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + if 'interface_name' in config['outbound_interface'] and 'interface_group' in config['outbound_interface']: + raise ConfigError(f'Cannot specify both interface-group and interface-name for nat source rule "{rule}"') + elif 'interface_name' in config['outbound_interface']: + if config['outbound_interface']['interface_name'] not in 'any' and config['outbound_interface']['interface_name'] not in interfaces(): + Warning(f'rule "{rule}" interface "{config["outbound_interface"]["interface_name"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config): if 'exclude' not in config and 'backend' not in config['load_balance']: @@ -172,8 +175,11 @@ def verify(nat): err_msg = f'Destination NAT configuration error in rule {rule}:' if 'inbound_interface' in config: - if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') + if 'interface_name' in config['inbound_interface'] and 'interface_group' in config['inbound_interface']: + raise ConfigError(f'Cannot specify both interface-group and interface-name for destination nat rule "{rule}"') + elif 'interface_name' in config['inbound_interface']: + if config['inbound_interface']['interface_name'] not in 'any' and config['inbound_interface']['interface_name'] not in interfaces(): + Warning(f'rule "{rule}" interface "{config["inbound_interface"]["interface_name"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']: if 'exclude' not in config and 'backend' not in config['load_balance']: diff --git a/src/migration-scripts/nat/5-to-6 b/src/migration-scripts/nat/5-to-6 new file mode 100755 index 000000000..de3830582 --- /dev/null +++ b/src/migration-scripts/nat/5-to-6 @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# 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, see . + +# T5643: move from 'set nat [source|destination] rule X [inbound-interface|outbound interface] ' +# to +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-name ' + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['nat']): + # Nothing to do + exit(0) + +for direction in ['source', 'destination']: + # If a node doesn't exist, we obviously have nothing to do. + if not config.exists(['nat', direction]): + continue + + # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, + # but there are no rules under it. + if not config.list_nodes(['nat', direction]): + continue + + for rule in config.list_nodes(['nat', direction, 'rule']): + base = ['nat', direction, 'rule', rule] + for iface in ['inbound-interface','outbound-interface']: + if config.exists(base + [iface]): + tmp = config.return_value(base + [iface]) + config.delete(base + [iface]) + config.set(base + [iface, 'interface-name'], value=tmp) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3