summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Fort <nicolasfort1988@gmail.com>2023-10-10 17:35:30 +0000
committerNicolas Fort <nicolasfort1988@gmail.com>2023-10-11 09:44:28 +0000
commit2f2c3fa22478c7ba2e116486d655e07df878cdf4 (patch)
treef0c400df61a910651e4b890a583b88a3746cd8ab
parent9ceba9ede21f4f89bd0b4ee614178b82cddb7d41 (diff)
downloadvyos-1x-2f2c3fa22478c7ba2e116486d655e07df878cdf4.tar.gz
vyos-1x-2f2c3fa22478c7ba2e116486d655e07df878cdf4.zip
T5643: nat: add interface-groups to nat. Use same cli structure for interface-name|interface-group as in firewall.
-rw-r--r--interface-definitions/nat.xml.in4
-rw-r--r--python/vyos/nat.py32
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py28
-rwxr-xr-xsrc/conf_mode/nat.py14
-rwxr-xr-xsrc/migration-scripts/nat/5-to-662
5 files changed, 115 insertions, 25 deletions
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/nat-rule.xml.i>
<tagNode name="rule">
<children>
- #include <include/inbound-interface.xml.i>
+ #include <include/firewall/inbound-interface.xml.i>
<node name="translation">
<properties>
<help>Inside NAT IP (destination NAT only)</help>
@@ -77,7 +77,7 @@
<constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage>
</properties>
<children>
- #include <include/nat-interface.xml.i>
+ #include <include/firewall/outbound-interface.xml.i>
<node name="translation">
<properties>
<help>Outside NAT IP (source NAT only)</help>
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 <http://www.gnu.org/licenses/>.
+
+# T5643: move from 'set nat [source|destination] rule X [inbound-interface|outbound interface] <iface>'
+# to
+# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-name <iface>'
+
+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)