summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsarthurdev <965089+sarthurdev@users.noreply.github.com>2022-09-20 17:16:43 +0200
committersarthurdev <965089+sarthurdev@users.noreply.github.com>2022-09-21 20:53:49 +0200
commite6ba98a85ca72abc7e7e2001d208bcd1806c2c13 (patch)
treedc36b04502efdab35b3ab9612e36c4fc008068f4
parent448d4f6db9cf6dfceffccf988301e5f4d04c9afa (diff)
downloadvyos-1x-e6ba98a85ca72abc7e7e2001d208bcd1806c2c13.tar.gz
vyos-1x-e6ba98a85ca72abc7e7e2001d208bcd1806c2c13.zip
nat66: T4605: Refactor NAT66 to use python module for parsing rules
* Rename table to vyos_nat * Refactor tests to use `verify_nftables` format
-rw-r--r--data/templates/firewall/nftables-nat66.j2158
-rw-r--r--data/vyos-firewall-init.conf20
-rw-r--r--python/vyos/nat.py27
-rwxr-xr-xsmoketest/scripts/cli/test_nat66.py63
-rwxr-xr-xsrc/conf_mode/nat66.py11
-rwxr-xr-xsrc/op_mode/show_nat66_statistics.py2
6 files changed, 85 insertions, 196 deletions
diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2
index 28714c7a7..27b3eec88 100644
--- a/data/templates/firewall/nftables-nat66.j2
+++ b/data/templates/firewall/nftables-nat66.j2
@@ -1,125 +1,5 @@
#!/usr/sbin/nft -f
-{% macro nptv6_rule(rule,config, chain) %}
-{% set comment = '' %}
-{% set base_log = '' %}
-{% set dst_prefix = 'ip6 daddr ' ~ config.destination.prefix.replace('!','!= ') if config.destination.prefix is vyos_defined %}
-{% set src_prefix = 'ip6 saddr ' ~ config.source.prefix.replace('!','!= ') if config.source.prefix is vyos_defined %}
-{% set source_address = 'ip6 saddr ' ~ config.source.address.replace('!','!= ') if config.source.address is vyos_defined %}
-{% set dest_address = 'ip6 daddr ' ~ config.destination.address.replace('!','!= ') if config.destination.address is vyos_defined %}
-{# Port #}
-{% if config.source.port is vyos_defined and config.source.port.startswith('!') %}
-{% set src_port = 'sport != { ' ~ config.source.port.replace('!','') ~ ' }' %}
-{% else %}
-{% set src_port = 'sport { ' ~ config.source.port ~ ' }' if config.source.port is vyos_defined %}
-{% endif %}
-{% if config.destination.port is vyos_defined and config.destination.port.startswith('!') %}
-{% set dst_port = 'dport != { ' ~ config.destination.port.replace('!','') ~ ' }' %}
-{% else %}
-{% set dst_port = 'dport { ' ~ config.destination.port ~ ' }' if config.destination.port is vyos_defined %}
-{% endif %}
-{% if chain is vyos_defined('PREROUTING') %}
-{% set comment = 'DST-NAT66-' ~ rule %}
-{% set base_log = '[NAT66-DST-' ~ rule %}
-{% set interface = ' iifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %}
-{% if config.translation.address | is_ip_network %}
-{# support 1:1 network translation #}
-{% set dnat_type = 'dnat prefix to ' %}
-{% else %}
-{% set dnat_type = 'dnat to ' %}
-{% endif %}
-{% set trns_address = dnat_type ~ config.translation.address if config.translation.address is vyos_defined %}
-{% elif chain is vyos_defined('POSTROUTING') %}
-{% set comment = 'SRC-NAT66-' ~ rule %}
-{% set base_log = '[NAT66-SRC-' ~ rule %}
-{% if config.translation.address is vyos_defined %}
-{% if config.translation.address is vyos_defined('masquerade') %}
-{% set trns_address = config.translation.address %}
-{% else %}
-{% if config.translation.address | is_ip_network %}
-{# support 1:1 network translation #}
-{% set snat_type = 'snat prefix to ' %}
-{% else %}
-{% set snat_type = 'snat to ' %}
-{% endif %}
-{% set trns_address = snat_type ~ config.translation.address %}
-{% endif %}
-{% endif %}
-{% set interface = ' oifname "' ~ config.outbound_interface ~ '"' if config.outbound_interface is vyos_defined else '' %}
-{% endif %}
-{% set trns_port = ':' ~ config.translation.port if config.translation.port is vyos_defined %}
-{# protocol has a default value thus it is always present #}
-{% if config.protocol is vyos_defined('tcp_udp') %}
-{% set protocol = 'tcp' %}
-{% set comment = comment ~ ' tcp_udp' %}
-{% else %}
-{% set protocol = config.protocol %}
-{% endif %}
-{% if config.log is vyos_defined %}
-{% if config.translation.address is vyos_defined('masquerade') %}
-{% set log = base_log ~ '-MASQ]' %}
-{% else %}
-{% set log = base_log ~ ']' %}
-{% endif %}
-{% endif %}
-{% if config.exclude is vyos_defined %}
-{# rule has been marked as 'exclude' thus we simply return here #}
-{% set trns_addr = 'return' %}
-{% set trns_port = '' %}
-{% endif %}
-{% set output = 'add rule ip6 nat ' ~ chain ~ interface %}
-{# Count packets #}
-{% set output = output ~ ' counter' %}
-{# Special handling of log option, we must repeat the entire rule before the #}
-{# NAT translation options are added, this is essential #}
-{% if log is vyos_defined %}
-{% set log_output = output ~ ' log prefix "' ~ log ~ '" comment "' ~ comment ~ '"' %}
-{% endif %}
-{% if src_prefix is vyos_defined %}
-{% set output = output ~ ' ' ~ src_prefix %}
-{% endif %}
-{% if dst_port is vyos_defined %}
-{% set output = output ~ ' ' ~ protocol ~ ' ' ~ dst_port %}
-{% endif %}
-{% if dst_prefix is vyos_defined %}
-{% set output = output ~ ' ' ~ dst_prefix %}
-{% endif %}
-{% if source_address is vyos_defined %}
-{% set output = output ~ ' ' ~ source_address %}
-{% endif %}
-{% if src_port is vyos_defined %}
-{% set output = output ~ ' ' ~ protocol ~ ' ' ~ src_port %}
-{% endif %}
-{% if dest_address is vyos_defined %}
-{% set output = output ~ ' ' ~ dest_address %}
-{% endif %}
-{% if config.exclude is vyos_defined %}
-{# rule has been marked as 'exclude' thus we simply return here #}
-{% set trns_address = 'return' %}
-{% endif %}
-{% if trns_address is vyos_defined %}
-{% set output = output ~ ' ' ~ trns_address %}
-{% endif %}
-{% if trns_port is vyos_defined %}
-{# Do not add a whitespace here, translation port must be directly added after IP address #}
-{# e.g. 2001:db8::1:3389 #}
-{% set output = output ~ trns_port %}
-{% endif %}
-{% if comment is vyos_defined %}
-{% set output = output ~ ' comment "' ~ comment ~ '"' %}
-{% endif %}
-{{ log_output if log_output is vyos_defined }}
-{{ output }}
-{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #}
-{% if config.protocol is vyos_defined('tcp_udp') %}
-{# Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #}
-{{ log_output | replace('tcp ', 'udp ') if log_output is vyos_defined }}
-{{ output | replace('tcp ', 'udp ') }}
-{% endif %}
-{% endmacro %}
-
-# Start with clean NAT table
-flush table ip6 nat
{% if helper_functions is vyos_defined('remove') %}
{# NAT if going to be disabled - remove rules and targets from nftables #}
{% set base_command = 'delete rule ip6 raw' %}
@@ -137,19 +17,41 @@ add rule ip6 raw NAT_CONNTRACK counter accept
{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK
{% endif %}
-#
-# Destination NAT66 rules build up here
-#
+{% if first_install is not vyos_defined %}
+delete table ip6 vyos_nat
+{% endif %}
+table ip6 vyos_nat {
+ #
+ # Destination NAT66 rules build up here
+ #
+ chain PREROUTING {
+ type nat hook prerouting priority -100; policy accept;
+ counter jump VYOS_DNPT_HOOK
{% if destination.rule is vyos_defined %}
{% for rule, config in destination.rule.items() if config.disable is not vyos_defined %}
-{{ nptv6_rule(rule, config, 'PREROUTING') }}
+ {{ config | nat_rule(rule, 'destination', ipv6=True) }}
{% endfor %}
{% endif %}
-#
-# Source NAT66 rules build up here
-#
+ }
+
+ #
+ # Source NAT66 rules build up here
+ #
+ chain POSTROUTING {
+ type nat hook postrouting priority 100; policy accept;
+ counter jump VYOS_SNPT_HOOK
{% if source.rule is vyos_defined %}
{% for rule, config in source.rule.items() if config.disable is not vyos_defined %}
-{{ nptv6_rule(rule, config, 'POSTROUTING') }}
+ {{ config | nat_rule(rule, 'source', ipv6=True) }}
{% endfor %}
{% endif %}
+ }
+
+ chain VYOS_DNPT_HOOK {
+ return
+ }
+
+ chain VYOS_SNPT_HOOK {
+ return
+ }
+}
diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf
index 348299462..3a0e1ee48 100644
--- a/data/vyos-firewall-init.conf
+++ b/data/vyos-firewall-init.conf
@@ -28,26 +28,6 @@ table ip nat {
}
}
-table ip6 nat {
- chain PREROUTING {
- type nat hook prerouting priority -100; policy accept;
- counter jump VYOS_DNPT_HOOK
- }
-
- chain POSTROUTING {
- type nat hook postrouting priority 100; policy accept;
- counter jump VYOS_SNPT_HOOK
- }
-
- chain VYOS_DNPT_HOOK {
- return
- }
-
- chain VYOS_SNPT_HOOK {
- return
- }
-}
-
table inet mangle {
chain FORWARD {
type filter hook forward priority -150; policy accept;
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 654afa424..44dd65372 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -17,11 +17,15 @@
from vyos.template import is_ip_network
from vyos.util import dict_search_args
-def parse_nat_rule(rule_conf, rule_id, nat_type):
+def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
output = []
+ ip_prefix = 'ip6' if ipv6 else 'ip'
log_prefix = ('DST' if nat_type == 'destination' else 'SRC') + f'-NAT-{rule_id}'
log_suffix = ''
+ if ipv6:
+ log_prefix = log_prefix.replace("NAT-", "NAT66-")
+
ignore_type_addr = False
translation_str = ''
@@ -39,7 +43,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type):
protocol = rule_conf['protocol']
if protocol == 'tcp_udp':
protocol = '{ tcp, udp }'
- output.append(f'ip protocol {protocol}')
+ output.append(f'meta l4proto {protocol}')
if 'exclude' in rule_conf:
translation_str = 'return'
@@ -51,9 +55,12 @@ def parse_nat_rule(rule_conf, rule_id, nat_type):
port = dict_search_args(rule_conf, 'translation', 'port')
if addr and is_ip_network(addr):
- map_addr = dict_search_args(rule_conf, nat_type, 'address')
- translation_output.append(f'ip prefix to ip {translation_prefix}addr map {{ {map_addr} : {addr} }}')
- ignore_type_addr = True
+ if not ipv6:
+ map_addr = dict_search_args(rule_conf, nat_type, 'address')
+ translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}')
+ ignore_type_addr = True
+ else:
+ translation_output.append(f'prefix to {addr}')
elif addr == 'masquerade':
if port:
addr = f'{addr} to '
@@ -85,7 +92,15 @@ def parse_nat_rule(rule_conf, rule_id, nat_type):
if addr[:1] == '!':
operator = '!='
addr = addr[1:]
- output.append(f'ip {prefix}addr {operator} {addr}')
+ output.append(f'{ip_prefix} {prefix}addr {operator} {addr}')
+
+ addr_prefix = dict_search_args(rule_conf, target, 'prefix')
+ if addr_prefix and ipv6:
+ operator = ''
+ if addr_prefix[:1] == '!':
+ operator = '!='
+ addr_prefix = addr[1:]
+ output.append(f'ip6 {prefix}addr {operator} {addr_prefix}')
port = dict_search_args(rule_conf, target, 'port')
if port:
diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py
index 537b094a4..6cf7ca0a1 100755
--- a/smoketest/scripts/cli/test_nat66.py
+++ b/smoketest/scripts/cli/test_nat66.py
@@ -71,12 +71,12 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
nftables_search = [
- ['oifname "eth1"', 'ip6 saddr fc00::/64', 'snat prefix to fc01::/64'],
- ['oifname "eth1"', 'ip6 saddr fc00::/64', 'masquerade'],
- ['oifname "eth1"', 'ip6 saddr fc00::/64', 'return']
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', f'snat prefix to {translation_prefix}'],
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', 'masquerade'],
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', 'return']
]
- self.verify_nftables(nftables_search, 'ip6 nat')
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_source_nat66_address(self):
source_prefix = 'fc00::/64'
@@ -88,25 +88,11 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
# check validate() - outbound-interface must be defined
self.cli_commit()
- tmp = cmd('sudo nft -j list table ip6 nat')
- data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp))
-
- for idx in range(0, len(data_json)):
- data = data_json[idx]
-
- self.assertEqual(data['chain'], 'POSTROUTING')
- self.assertEqual(data['family'], 'ip6')
- self.assertEqual(data['table'], 'nat')
-
- iface = dict_search('match.right', data['expr'][0])
- address = dict_search('match.right.prefix.addr', data['expr'][2])
- mask = dict_search('match.right.prefix.len', data['expr'][2])
- snat_address = dict_search('snat.addr', data['expr'][3])
+ nftables_search = [
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', f'snat to {translation_address}']
+ ]
- self.assertEqual(iface, 'eth1')
- # check for translation address
- self.assertEqual(snat_address, translation_address)
- self.assertEqual(f'{address}/{mask}', source_prefix)
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_destination_nat66(self):
destination_address = 'fc00::1'
@@ -129,7 +115,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
['iifname "eth1"', 'ip6 saddr fc02::1', 'ip6 daddr fc00::1', 'return']
]
- self.verify_nftables(nftables_search, 'ip6 nat')
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_destination_nat66_protocol(self):
translation_address = '2001:db8:1111::1'
@@ -153,7 +139,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
['iifname "eth1"', 'tcp dport 4545', 'ip6 saddr 2001:db8:2222::/64', 'tcp sport 8080', 'dnat to 2001:db8:1111::1:5555']
]
- self.verify_nftables(nftables_search, 'ip6 nat')
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_destination_nat66_prefix(self):
destination_prefix = 'fc00::/64'
@@ -165,22 +151,25 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
# check validate() - outbound-interface must be defined
self.cli_commit()
- tmp = cmd('sudo nft -j list table ip6 nat')
- data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp))
+ nftables_search = [
+ ['iifname "eth1"', f'ip6 daddr {destination_prefix}', f'dnat prefix to {translation_prefix}']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
- for idx in range(0, len(data_json)):
- data = data_json[idx]
+ def test_destination_nat66_without_translation_address(self):
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', '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', 'translation', 'port', '443'])
- self.assertEqual(data['chain'], 'PREROUTING')
- self.assertEqual(data['family'], 'ip6')
- self.assertEqual(data['table'], 'nat')
+ self.cli_commit()
- iface = dict_search('match.right', data['expr'][0])
- translation_address = dict_search('dnat.addr.prefix.addr', data['expr'][3])
- translation_mask = dict_search('dnat.addr.prefix.len', data['expr'][3])
+ nftables_search = [
+ ['iifname "eth1"', 'tcp dport 443', 'dnat to :443']
+ ]
- self.assertEqual(f'{translation_address}/{translation_mask}', translation_prefix)
- self.assertEqual(iface, 'eth1')
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_source_nat66_required_translation_prefix(self):
# T2813: Ensure translation address is specified
@@ -222,7 +211,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64', 'tcp dport 9999', 'tcp sport 8080', 'snat to 2001:db8:1111::1:80']
]
- self.verify_nftables(nftables_search, 'ip6 nat')
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
def test_nat66_no_rules(self):
# T3206: deleting all rules but keep the direction 'destination' or
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index f64102d88..d8f913b0c 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -36,7 +36,7 @@ airbag.enable()
k_mod = ['nft_nat', 'nft_chain_nat']
-nftables_nat66_config = '/tmp/vyos-nat66-rules.nft'
+nftables_nat66_config = '/run/nftables_nat66.nft'
ndppd_config = '/run/ndppd/ndppd.conf'
def get_handler(json, chain, target):
@@ -147,6 +147,9 @@ def verify(nat):
return None
def generate(nat):
+ if not os.path.exists(nftables_nat66_config):
+ nat['first_install'] = True
+
render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755)
render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755)
return None
@@ -154,15 +157,15 @@ def generate(nat):
def apply(nat):
if not nat:
return None
- cmd(f'{nftables_nat66_config}')
+
+ cmd(f'nft -f {nftables_nat66_config}')
+
if 'deleted' in nat or not dict_search('source.rule', nat):
cmd('systemctl stop ndppd')
if os.path.isfile(ndppd_config):
os.unlink(ndppd_config)
else:
cmd('systemctl restart ndppd')
- if os.path.isfile(nftables_nat66_config):
- os.unlink(nftables_nat66_config)
return None
diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py
index bc81692ae..cb10aed9f 100755
--- a/src/op_mode/show_nat66_statistics.py
+++ b/src/op_mode/show_nat66_statistics.py
@@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina
args = parser.parse_args()
if args.source or args.destination:
- tmp = cmd('sudo nft -j list table ip6 nat')
+ tmp = cmd('sudo nft -j list table ip6 vyos_nat')
tmp = json.loads(tmp)
source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"