From 40e8938667b06615e0a1a26271a30e00f8cff2c6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 1 May 2020 13:23:20 +0200 Subject: nat: T2198: initial XML and Python representation --- src/conf_mode/nat.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 src/conf_mode/nat.py (limited to 'src/conf_mode') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py new file mode 100755 index 000000000..188445214 --- /dev/null +++ b/src/conf_mode/nat.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 . + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'source': [], + 'destination': [] +} + +def get_config(): + nat = deepcopy(default_config_data) + conf = Config() + base = ['nat'] + if not conf.exists(base): + return None + else: + conf.set_level(base) + + return nat + +def verify(nat): + if not nat: + return None + + return None + +def generate(nat): + if not nat: + return None + + return None + +def apply(nat): + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From 7caf1568bbb6be59e5f13693c31f23ade9349daa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 1 May 2020 15:40:18 +0200 Subject: nat: T2198: destination nat template for iptables-restore --- data/templates/nat/nat-destination.tmpl | 13 +++++ data/templates/nat/nat-source.tmpl | 4 ++ src/conf_mode/nat.py | 91 +++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 data/templates/nat/nat-destination.tmpl create mode 100644 data/templates/nat/nat-source.tmpl (limited to 'src/conf_mode') diff --git a/data/templates/nat/nat-destination.tmpl b/data/templates/nat/nat-destination.tmpl new file mode 100644 index 000000000..ccd585264 --- /dev/null +++ b/data/templates/nat/nat-destination.tmpl @@ -0,0 +1,13 @@ +### Autogenerated by nat.py ### + +*nat +-A PREROUTING -j VYATTA_PRE_DNAT_HOOK +{% for r in destination -%} +{% if (',' in r.dest_port) or ('-' in r.dest_port) %} +-A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} {{ r.protocol }}" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{% else %} +-A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m tcp --dport {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} {{ r.protocol }}" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{% endif %} +{% endfor %} +-A VYATTA_PRE_DNAT_HOOK -j RETURN +COMMIT diff --git a/data/templates/nat/nat-source.tmpl b/data/templates/nat/nat-source.tmpl new file mode 100644 index 000000000..41179ae9c --- /dev/null +++ b/data/templates/nat/nat-source.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by nat.py ### +{% for r in source -%} +# {{ r.description }} +{% endfor %} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 188445214..538999f9a 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -16,8 +16,10 @@ from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.config import Config +from vyos.template import render from vyos import ConfigError default_config_data = { @@ -25,14 +27,89 @@ default_config_data = { 'destination': [] } +nat_source_config = '/tmp/nat_source' +nat_destination_config = '/tmp/nat_destination' + +def parse_source_destination(conf, source_dest): + """ Common wrapper to read in both NAT source and destination CLI """ + tmp = [] + base_level = ['nat', source_dest] + conf.set_level(base_level) + for number in conf.list_nodes(['rule']): + rule = { + 'description': '', + 'dest_address': '', + 'dest_port': '', + 'disable': False, + 'exclude': False, + 'interface_in': '', + 'interface_out': '', + 'log': False, + 'protocol': '', + 'number': number, + 'source_address': '', + 'source_port': '', + 'translation_address': '', + 'translation_port': '' + } + conf.set_level(base_level + ['rule', number]) + + if conf.exists(['description']): + rule['description'] = conf.return_value(['description']) + + if conf.exists(['destination', 'address']): + rule['dest_address'] = conf.return_value(['destination', 'address']) + + if conf.exists(['destination', 'port']): + rule['dest_port'] = conf.return_value(['destination', 'port']) + + if conf.exists(['disable']): + rule['disable'] = True + + if conf.exists(['exclude']): + rule['exclude'] = True + + if conf.exists(['inbound-interface']): + rule['interface_in'] = conf.return_value(['inbound-interface']) + + if conf.exists(['outbound-interface']): + rule['interface_out'] = conf.return_value(['outbound-interface']) + + if conf.exists(['log']): + rule['log'] = True + + if conf.exists(['protocol']): + rule['protocol'] = conf.return_value(['protocol']) + + if conf.exists(['source', 'address']): + rule['source_address'] = conf.return_value(['source', 'address']) + + if conf.exists(['source', 'port']): + rule['source_port'] = conf.return_value(['source', 'port']) + + if conf.exists(['translation', 'address']): + rule['translation_address'] = conf.return_value(['translation', 'address']) + + if conf.exists(['translation', 'port']): + rule['translation_port'] = conf.return_value(['translation', 'port']) + + tmp.append(rule) + + return tmp + def get_config(): nat = deepcopy(default_config_data) conf = Config() - base = ['nat'] - if not conf.exists(base): + if not conf.exists(['nat']): return None else: - conf.set_level(base) + conf.set_level(['nat']) + + # use a common wrapper function to read in the source / destination + # tree from the config - thus we do not need to replicate almost the + # same code :-) + for tgt in ['source', 'destination']: + nat[tgt] = parse_source_destination(conf, tgt) return nat @@ -40,12 +117,20 @@ def verify(nat): if not nat: return None + for rule in nat['source']: + interface = rule['interface_out'] + if interface and interface not in interfaces(): + print(f'NAT configuration warning: interface {interface} does not exist on this system') + return None def generate(nat): if not nat: return None + render(nat_source_config, 'nat/nat-source.tmpl', nat, trim_blocks=True) + render(nat_destination_config, 'nat/nat-destination.tmpl', nat, trim_blocks=True) + return None def apply(nat): -- cgit v1.2.3 From a5650abb6d575de2f696a934d52468992ac9f1e9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 1 May 2020 16:46:06 +0200 Subject: nat: T2198: migrate to common template for source/destination NAT --- data/templates/nat/iptables-restore.tmpl | 38 ++++++++++++++++++++++++++++++++ data/templates/nat/nat-destination.tmpl | 13 ----------- data/templates/nat/nat-source.tmpl | 4 ---- src/conf_mode/nat.py | 12 +++++----- 4 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 data/templates/nat/iptables-restore.tmpl delete mode 100644 data/templates/nat/nat-destination.tmpl delete mode 100644 data/templates/nat/nat-source.tmpl (limited to 'src/conf_mode') diff --git a/data/templates/nat/iptables-restore.tmpl b/data/templates/nat/iptables-restore.tmpl new file mode 100644 index 000000000..f20a05719 --- /dev/null +++ b/data/templates/nat/iptables-restore.tmpl @@ -0,0 +1,38 @@ +### Autogenerated by nat.py ### + +*nat +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +:VYATTA_PRE_DNAT_HOOK - [0:0] +:VYATTA_PRE_SNAT_HOOK - [0:0] +-A PREROUTING -j VYATTA_PRE_DNAT_HOOK +{% for r in destination -%} +{% if (',' in r.dest_port) or ('-' in r.dest_port) %} + +{% if r.protocol == 'tcp_udp' %} +# protocol has been tcp_udp - create two distinct rules +-A PREROUTING -i {{ r.interface_in }} -p tcp -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +-A PREROUTING -i {{ r.interface_in }} -p udp -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{% else %} +-A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment DST-NAT-{{ r.number }} -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{%- endif %} + +{% else %} + +{% if r.protocol == 'tcp_udp' %} +# protocol has been tcp_udp - create two distinct rules +-A PREROUTING -i {{ r.interface_in }} -p tcp -m {{ r.protocol }} --dports {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +-A PREROUTING -i {{ r.interface_in }} -p udp -m {{ r.protocol }} --dports {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{% else %} +-A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m {{ r.protocol }} --dport {{ r.dest_port }} -m comment --comment DST-NAT-{{ r.number }} -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} +{% endif %} + +{%- endif %} + +{% endfor %} +-A POSTROUTING -j VYATTA_PRE_SNAT_HOOK +-A VYATTA_PRE_DNAT_HOOK -j RETURN +-A VYATTA_PRE_SNAT_HOOK -j RETURN +COMMIT diff --git a/data/templates/nat/nat-destination.tmpl b/data/templates/nat/nat-destination.tmpl deleted file mode 100644 index ccd585264..000000000 --- a/data/templates/nat/nat-destination.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -### Autogenerated by nat.py ### - -*nat --A PREROUTING -j VYATTA_PRE_DNAT_HOOK -{% for r in destination -%} -{% if (',' in r.dest_port) or ('-' in r.dest_port) %} --A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} {{ r.protocol }}" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{% else %} --A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m tcp --dport {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} {{ r.protocol }}" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{% endif %} -{% endfor %} --A VYATTA_PRE_DNAT_HOOK -j RETURN -COMMIT diff --git a/data/templates/nat/nat-source.tmpl b/data/templates/nat/nat-source.tmpl deleted file mode 100644 index 41179ae9c..000000000 --- a/data/templates/nat/nat-source.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -### Autogenerated by nat.py ### -{% for r in source -%} -# {{ r.description }} -{% endfor %} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 538999f9a..b4e8c2053 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -19,6 +19,7 @@ from sys import exit from netifaces import interfaces from vyos.config import Config +from vyos.util import call from vyos.template import render from vyos import ConfigError @@ -27,8 +28,7 @@ default_config_data = { 'destination': [] } -nat_source_config = '/tmp/nat_source' -nat_destination_config = '/tmp/nat_destination' +iptables_nat_config = '/tmp/iptables_nat_config' def parse_source_destination(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ @@ -128,12 +128,14 @@ def generate(nat): if not nat: return None - render(nat_source_config, 'nat/nat-source.tmpl', nat, trim_blocks=True) - render(nat_destination_config, 'nat/nat-destination.tmpl', nat, trim_blocks=True) - + render(iptables_nat_config, 'nat/iptables-restore.tmpl', nat, trim_blocks=True) return None def apply(nat): + if not nat: + return None + + call(f'iptables-restore --test < {iptables_nat_config}') return None -- cgit v1.2.3 From a927192af24079e6d392e5cae0340441490c0091 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 1 May 2020 19:25:36 +0200 Subject: nat: T2198: move from iptables to nftables --- data/templates/firewall/nftables-nat.tmpl | 43 +++++++++++++++++++++++++++++++ data/templates/nat/iptables-restore.tmpl | 38 --------------------------- debian/control | 1 - src/conf_mode/nat.py | 17 +++++++++--- 4 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 data/templates/firewall/nftables-nat.tmpl delete mode 100644 data/templates/nat/iptables-restore.tmpl (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl new file mode 100644 index 000000000..340ab3678 --- /dev/null +++ b/data/templates/firewall/nftables-nat.tmpl @@ -0,0 +1,43 @@ +#!/usr/sbin/nft -f + +# Start with a "clean" NAT table +flush table nat + +add chain ip raw NAT_CONNTRACK +add rule ip raw PREROUTING position 25 counter jump VYATTA_CT_HELPER +add rule ip raw PREROUTING position 17 counter jump NAT_CONNTRACK +add rule ip raw OUTPUT position 26 counter jump VYATTA_CT_HELPER +add rule ip raw OUTPUT position 21 counter jump NAT_CONNTRACK +add rule ip raw NAT_CONNTRACK counter accept + + +{% for r in destination -%} +{% if r.protocol == 'tcp_udp' %} +{# Special handling for protocol tcp_udp which is represented as two individual rules #} +add rule ip nat PREROUTING iifname "{{ r.interface_in }}" tcp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" +add rule ip nat PREROUTING iifname "{{ r.interface_in }}" udp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" +{% else %} +add rule ip nat PREROUTING iifname "{{ r.interface_in }}" {{ r.protocol }} dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }}" +{% endif %} +{% endfor %} + + +{% for r in source -%} +{% if r.log %} +{% if r.exclude %} +{% set value = 'EXCL' %} +{% elif r.translation_address == 'masquerade' %} +{% set value = 'MASQ' %} +{% endif %} +add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter log prefix "[NAT-SRC-{{ r.number }}-{{ value }}]" comment "SRC-NAT-{{ r.number }}" +{% endif %} + +{% if r.exclude %} +{% set value = 'return' %} +{% elif r.translation_address == 'masquerade' %} +{% set value = 'masquerade' %} +{% else %} +{% set value = 'snat to ' + r.translation_address %} +{% endif %} +add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter {{ value }} comment "SRC-NAT-{{ r.number }}" +{% endfor %} diff --git a/data/templates/nat/iptables-restore.tmpl b/data/templates/nat/iptables-restore.tmpl deleted file mode 100644 index f20a05719..000000000 --- a/data/templates/nat/iptables-restore.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -### Autogenerated by nat.py ### - -*nat -:PREROUTING ACCEPT [0:0] -:INPUT ACCEPT [0:0] -:OUTPUT ACCEPT [0:0] -:POSTROUTING ACCEPT [0:0] -:VYATTA_PRE_DNAT_HOOK - [0:0] -:VYATTA_PRE_SNAT_HOOK - [0:0] --A PREROUTING -j VYATTA_PRE_DNAT_HOOK -{% for r in destination -%} -{% if (',' in r.dest_port) or ('-' in r.dest_port) %} - -{% if r.protocol == 'tcp_udp' %} -# protocol has been tcp_udp - create two distinct rules --A PREROUTING -i {{ r.interface_in }} -p tcp -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} --A PREROUTING -i {{ r.interface_in }} -p udp -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{% else %} --A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m multiport --dports {{ r.dest_port | replace('-', ':') }} -m comment --comment DST-NAT-{{ r.number }} -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{%- endif %} - -{% else %} - -{% if r.protocol == 'tcp_udp' %} -# protocol has been tcp_udp - create two distinct rules --A PREROUTING -i {{ r.interface_in }} -p tcp -m {{ r.protocol }} --dports {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} --A PREROUTING -i {{ r.interface_in }} -p udp -m {{ r.protocol }} --dports {{ r.dest_port }} -m comment --comment "DST-NAT-{{ r.number }} tcp_udp" -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{% else %} --A PREROUTING -i {{ r.interface_in }} -p {{ r.protocol }} -m {{ r.protocol }} --dport {{ r.dest_port }} -m comment --comment DST-NAT-{{ r.number }} -j DNAT --to-destination {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} -{% endif %} - -{%- endif %} - -{% endfor %} --A POSTROUTING -j VYATTA_PRE_SNAT_HOOK --A VYATTA_PRE_DNAT_HOOK -j RETURN --A VYATTA_PRE_SNAT_HOOK -j RETURN -COMMIT diff --git a/debian/control b/debian/control index c8fa8ca63..2aaca13ba 100644 --- a/debian/control +++ b/debian/control @@ -92,7 +92,6 @@ Depends: python3, pppoe, salt-minion, vyos-utils, - iptables, nftables, conntrack, ${shlibs:Depends}, diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index b4e8c2053..2e866fdf4 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os + from copy import deepcopy from sys import exit from netifaces import interfaces @@ -28,7 +30,14 @@ default_config_data = { 'destination': [] } -iptables_nat_config = '/tmp/iptables_nat_config' +iptables_nat_config = '/tmp/vyos-nat-rules.nft' + +def _check_kmod(): + modules = ['nft_nat', 'nft_chain_nat_ipv4'] + for module in modules: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') def parse_source_destination(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ @@ -128,19 +137,19 @@ def generate(nat): if not nat: return None - render(iptables_nat_config, 'nat/iptables-restore.tmpl', nat, trim_blocks=True) + render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) return None def apply(nat): if not nat: return None - call(f'iptables-restore --test < {iptables_nat_config}') - + call(f'{iptables_nat_config}') return None if __name__ == '__main__': try: + _check_kmod() c = get_config() verify(c) generate(c) -- cgit v1.2.3 From 1c6ae6f7e7cf30d9598d2886bb3d2c34685a2c8c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 11 May 2020 18:58:05 +0200 Subject: nat: T2198: automatically determine handler numbers When instantiating NAT it is required to isntall some nftable jump targets. The targets need to be added after a specific other target thus we need to dynamically query the handler number. This is done by get_handler() which could be moved to vyos.util at a later point in time so it can be reused for a firewall rewrite. --- data/templates/firewall/nftables-nat.tmpl | 19 ++++++++++--- debian/control | 2 +- src/conf_mode/nat.py | 45 ++++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 340ab3678..343807e79 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -3,11 +3,22 @@ # Start with a "clean" NAT table flush table nat +{% for rule in init_deinit -%} +# Add or remove conntrack helper rules for NAT operation- +{{ rule }} +{% endfor %} + add chain ip raw NAT_CONNTRACK -add rule ip raw PREROUTING position 25 counter jump VYATTA_CT_HELPER -add rule ip raw PREROUTING position 17 counter jump NAT_CONNTRACK -add rule ip raw OUTPUT position 26 counter jump VYATTA_CT_HELPER -add rule ip raw OUTPUT position 21 counter jump NAT_CONNTRACK + +# insert rule after VYATTA_CT_IGNORE +add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER +# insert rule after VYATTA_CT_PREROUTING_HOOK +add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK +# insert rule after VYATTA_CT_IGNORE +add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER +# insert rule after VYATTA_CT_PREROUTING_HOOK +add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK + add rule ip raw NAT_CONNTRACK counter accept diff --git a/debian/control b/debian/control index 2aaca13ba..609f46e4d 100644 --- a/debian/control +++ b/debian/control @@ -92,7 +92,7 @@ Depends: python3, pppoe, salt-minion, vyos-utils, - nftables, + nftables (>= 0.9.3), conntrack, ${shlibs:Depends}, ${misc:Depends} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 2e866fdf4..128e2469c 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json import os from copy import deepcopy @@ -21,24 +22,53 @@ from sys import exit from netifaces import interfaces from vyos.config import Config -from vyos.util import call from vyos.template import render +from vyos.util import call, cmd from vyos import ConfigError default_config_data = { - 'source': [], - 'destination': [] + 'prerouting_ct_helper': '', + 'prerouting_ct_conntrack': '', + 'output_ct_helper': '', + 'output_ct_conntrack': '', + 'destination': [], + 'source': [] } iptables_nat_config = '/tmp/vyos-nat-rules.nft' def _check_kmod(): + """ load required Kernel modules """ modules = ['nft_nat', 'nft_chain_nat_ipv4'] for module in modules: if not os.path.exists(f'/sys/module/{module}'): if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') + +def get_handler(chain, target): + """ Get handler number of given chain/target combination. Handler is + required when adding NAT/Conntrack helper targets """ + tmp = json.loads(cmd('nft -j list table raw')) + for rule in tmp.get('nftables'): + # We're only interested in rules - not chains + if not 'rule' in rule.keys(): + continue + + # Search for chain of interest + if rule['rule']['chain'] == chain: + for expr in rule['rule']['expr']: + # We're only interested in jump targets + if not 'jump' in expr.keys(): + continue + + # Search for target of interest + if expr['jump']['target'] == target: + return rule['rule']['handle'] + + return None + + def parse_source_destination(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ tmp = [] @@ -114,6 +144,11 @@ def get_config(): else: conf.set_level(['nat']) + nat['pre_ct_ignore'] = get_handler('PREROUTING', 'VYATTA_CT_IGNORE') + nat['pre_ct_conntrack'] = get_handler('PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') + nat['out_ct_ignore'] = get_handler('OUTPUT', 'VYATTA_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler('OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + # use a common wrapper function to read in the source / destination # tree from the config - thus we do not need to replicate almost the # same code :-) @@ -126,6 +161,9 @@ def verify(nat): if not nat: return None + if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): + raise Exception('could not determine nftable ruleset handlers') + for rule in nat['source']: interface = rule['interface_out'] if interface and interface not in interfaces(): @@ -138,6 +176,7 @@ def generate(nat): return None render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) + return None def apply(nat): -- cgit v1.2.3 From fda762065c03d55c05682bf9834354c0edca3e97 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 11 May 2020 19:32:32 +0200 Subject: nat: T2198: implement deletion of NAT subsystem --- data/templates/firewall/nftables-nat.tmpl | 20 ++++++++++++++------ src/conf_mode/nat.py | 31 +++++++++++++++++++------------ 2 files changed, 33 insertions(+), 18 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 343807e79..671cd0920 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -8,18 +8,26 @@ flush table nat {{ rule }} {% endfor %} + +{% if deleted %} +# NAT if going to be disabled - remove rules and targets from nftables +delete rule ip raw PREROUTING handle {{ pre_ct_ignore }} +delete rule ip raw PREROUTING handle {{ pre_ct_conntrack }} +delete rule ip raw OUTPUT handle {{ out_ct_ignore }} +delete rule ip raw OUTPUT handle {{ out_ct_conntrack }} + +delete chain ip raw NAT_CONNTRACK + +{% else %} +# NAT if enabled - add targets to nftables add chain ip raw NAT_CONNTRACK +add rule ip raw NAT_CONNTRACK counter accept -# insert rule after VYATTA_CT_IGNORE add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER -# insert rule after VYATTA_CT_PREROUTING_HOOK add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK -# insert rule after VYATTA_CT_IGNORE add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER -# insert rule after VYATTA_CT_PREROUTING_HOOK add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK - -add rule ip raw NAT_CONNTRACK counter accept +{% endif %} {% for r in destination -%} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 128e2469c..916f63f09 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -27,11 +27,12 @@ from vyos.util import call, cmd from vyos import ConfigError default_config_data = { - 'prerouting_ct_helper': '', - 'prerouting_ct_conntrack': '', - 'output_ct_helper': '', - 'output_ct_conntrack': '', + 'deleted': False, 'destination': [], + 'pre_ct_helper': '', + 'pre_ct_conntrack': '', + 'out_ct_helper': '', + 'out_ct_conntrack': '', 'source': [] } @@ -139,11 +140,21 @@ def parse_source_destination(conf, source_dest): def get_config(): nat = deepcopy(default_config_data) conf = Config() + if not conf.exists(['nat']): - return None + # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler('PREROUTING', 'VYATTA_CT_HELPER') + nat['pre_ct_conntrack'] = get_handler('PREROUTING', 'NAT_CONNTRACK') + nat['out_ct_ignore'] = get_handler('OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_conntrack'] = get_handler('OUTPUT', 'NAT_CONNTRACK') + + nat['deleted'] = True + + return nat else: conf.set_level(['nat']) + # Retrieve current table handler positions nat['pre_ct_ignore'] = get_handler('PREROUTING', 'VYATTA_CT_IGNORE') nat['pre_ct_conntrack'] = get_handler('PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') nat['out_ct_ignore'] = get_handler('OUTPUT', 'VYATTA_CT_IGNORE') @@ -158,7 +169,8 @@ def get_config(): return nat def verify(nat): - if not nat: + if nat['deleted']: + # no need to verify the CLI as NAT is going to be deactivated return None if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): @@ -172,18 +184,13 @@ def verify(nat): return None def generate(nat): - if not nat: - return None - render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) return None def apply(nat): - if not nat: - return None + cmd(f'{iptables_nat_config}') - call(f'{iptables_nat_config}') return None if __name__ == '__main__': -- cgit v1.2.3 From cc2ad34ce61e205454c4676a5bde77629d463964 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 12 May 2020 17:43:03 +0200 Subject: nat: T2198: make use of jmespath when walking nftables JSON output --- data/templates/firewall/nftables-nat.tmpl | 91 ++++++++++++++----------------- debian/control | 1 + src/conf_mode/nat.py | 71 +++++++++++++----------- 3 files changed, 83 insertions(+), 80 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 671cd0920..161ef27fb 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -1,62 +1,55 @@ #!/usr/sbin/nft -f -# Start with a "clean" NAT table +# Start with clean NAT table flush table nat -{% for rule in init_deinit -%} -# Add or remove conntrack helper rules for NAT operation- -{{ rule }} -{% endfor %} - - -{% if deleted %} -# NAT if going to be disabled - remove rules and targets from nftables -delete rule ip raw PREROUTING handle {{ pre_ct_ignore }} -delete rule ip raw PREROUTING handle {{ pre_ct_conntrack }} -delete rule ip raw OUTPUT handle {{ out_ct_ignore }} -delete rule ip raw OUTPUT handle {{ out_ct_conntrack }} - -delete chain ip raw NAT_CONNTRACK - -{% else %} -# NAT if enabled - add targets to nftables -add chain ip raw NAT_CONNTRACK -add rule ip raw NAT_CONNTRACK counter accept - -add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER -add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK -add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER -add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK +{% if helper_functions == 'remove' %} + # NAT if going to be disabled - remove rules and targets from nftables + delete rule ip raw PREROUTING handle {{ pre_ct_ignore }} + delete rule ip raw PREROUTING handle {{ pre_ct_conntrack }} + delete rule ip raw OUTPUT handle {{ out_ct_ignore }} + delete rule ip raw OUTPUT handle {{ out_ct_conntrack }} + + delete chain ip raw NAT_CONNTRACK +{% elif helper_functions == 'add' %} + # NAT if enabled - add targets to nftables + add chain ip raw NAT_CONNTRACK + add rule ip raw NAT_CONNTRACK counter accept + + add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER + add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK + add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER + add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK {% endif %} {% for r in destination -%} -{% if r.protocol == 'tcp_udp' %} -{# Special handling for protocol tcp_udp which is represented as two individual rules #} -add rule ip nat PREROUTING iifname "{{ r.interface_in }}" tcp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" -add rule ip nat PREROUTING iifname "{{ r.interface_in }}" udp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" -{% else %} -add rule ip nat PREROUTING iifname "{{ r.interface_in }}" {{ r.protocol }} dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }}" -{% endif %} +{% if r.protocol == 'tcp_udp' %} + {# Special handling for protocol tcp_udp which is represented as two individual rules #} + add rule ip nat PREROUTING iifname "{{ r.interface_in }}" tcp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" + add rule ip nat PREROUTING iifname "{{ r.interface_in }}" udp dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }} tcp_udp" +{% else %} + add rule ip nat PREROUTING iifname "{{ r.interface_in }}" {{ r.protocol }} dport { {{ r.dest_port }} } counter dnat to {{ r.translation_address }}{{ ":" + r.translation_port if r.translation_port }} comment "DST-NAT-{{ r.number }}" +{% endif %} {% endfor %} {% for r in source -%} -{% if r.log %} -{% if r.exclude %} -{% set value = 'EXCL' %} -{% elif r.translation_address == 'masquerade' %} -{% set value = 'MASQ' %} -{% endif %} -add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter log prefix "[NAT-SRC-{{ r.number }}-{{ value }}]" comment "SRC-NAT-{{ r.number }}" -{% endif %} - -{% if r.exclude %} -{% set value = 'return' %} -{% elif r.translation_address == 'masquerade' %} -{% set value = 'masquerade' %} -{% else %} -{% set value = 'snat to ' + r.translation_address %} -{% endif %} -add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter {{ value }} comment "SRC-NAT-{{ r.number }}" +{% if r.log %} +{% if r.exclude %} +{% set value = 'EXCL' %} +{% elif r.translation_address == 'masquerade' %} +{% set value = 'MASQ' %} +{% endif %} + add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter log prefix "[NAT-SRC-{{ r.number }}-{{ value }}]" comment "SRC-NAT-{{ r.number }}" +{% endif %} + +{% if r.exclude %} +{% set value = 'return' %} +{% elif r.translation_address == 'masquerade' %} +{% set value = 'masquerade' %} +{% else %} +{% set value = 'snat to ' + r.translation_address %} +{% endif %} + add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter {{ value }} comment "SRC-NAT-{{ r.number }}" {% endfor %} diff --git a/debian/control b/debian/control index 609f46e4d..323b8130f 100644 --- a/debian/control +++ b/debian/control @@ -32,6 +32,7 @@ Depends: python3, python3-waitress, python3-netaddr, python3-zmq, + python3-jmespath, cron, easy-rsa, ipaddrcheck, diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 916f63f09..580a06136 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import jmespath import json import os @@ -29,6 +30,7 @@ from vyos import ConfigError default_config_data = { 'deleted': False, 'destination': [], + 'helper_functions': None, 'pre_ct_helper': '', 'pre_ct_conntrack': '', 'out_ct_helper': '', @@ -47,25 +49,15 @@ def _check_kmod(): raise ConfigError(f'Loading Kernel module {module} failed') -def get_handler(chain, target): - """ Get handler number of given chain/target combination. Handler is - required when adding NAT/Conntrack helper targets """ - tmp = json.loads(cmd('nft -j list table raw')) - for rule in tmp.get('nftables'): - # We're only interested in rules - not chains - if not 'rule' in rule.keys(): +def get_handler(json, chain, target): + """ Get nftable rule handler number of given chain/target combination. + Handler is required when adding NAT/Conntrack helper targets """ + for x in json: + if x['chain'] != chain: continue - - # Search for chain of interest - if rule['rule']['chain'] == chain: - for expr in rule['rule']['expr']: - # We're only interested in jump targets - if not 'jump' in expr.keys(): - continue - - # Search for target of interest - if expr['jump']['target'] == target: - return rule['rule']['handle'] + if x['target'] != target: + continue + return x['handle'] return None @@ -141,24 +133,40 @@ def get_config(): nat = deepcopy(default_config_data) conf = Config() + # read in current nftable (once) for further processing + tmp = cmd('nft -j list table raw') + nftable_json = json.loads(tmp) + + # condense the full JSON table into a list with only relevand informations + pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' + condensed_json = jmespath.search(pattern, nftable_json) + if not conf.exists(['nat']): + nat['helper_functions'] = 'remove' + # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler('PREROUTING', 'VYATTA_CT_HELPER') - nat['pre_ct_conntrack'] = get_handler('PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler('OUTPUT', 'VYATTA_CT_HELPER') - nat['out_ct_conntrack'] = get_handler('OUTPUT', 'NAT_CONNTRACK') + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = True return nat - else: - conf.set_level(['nat']) - # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler('PREROUTING', 'VYATTA_CT_IGNORE') - nat['pre_ct_conntrack'] = get_handler('PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') - nat['out_ct_ignore'] = get_handler('OUTPUT', 'VYATTA_CT_IGNORE') - nat['out_ct_conntrack'] = get_handler('OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + # check if NAT connection tracking helpers need to be set up - this has to + # be done only once + if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): + nat['helper_functions'] = 'add' + + # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + + # set config level for parsing in NAT configuration + conf.set_level(['nat']) # use a common wrapper function to read in the source / destination # tree from the config - thus we do not need to replicate almost the @@ -173,8 +181,9 @@ def verify(nat): # no need to verify the CLI as NAT is going to be deactivated return None - if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): - raise Exception('could not determine nftable ruleset handlers') + if nat['helper_functions']: + if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): + raise Exception('could not determine nftable ruleset handlers') for rule in nat['source']: interface = rule['interface_out'] -- cgit v1.2.3 From 1e7d01e5b5a12c5bfaa8989ae6073679f6b647b0 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 12 May 2020 20:58:34 +0200 Subject: nat: T2198: add some basic verify() rules --- src/conf_mode/nat.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 580a06136..bde6841cc 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -25,6 +25,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.template import render from vyos.util import call, cmd +from vyos.validate import is_addr_assigned from vyos import ConfigError default_config_data = { @@ -176,6 +177,18 @@ def get_config(): return nat +def verify_rule(rule): + if rule['translation_port']: + if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + proto = rule['protocol'] + raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') + + if '/' in rule['translation_address']: + raise ConfigError(f'{err_msg}\n' \ + 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'statically maps a whole network of addresses onto another\n' \ + 'network of addresses') + def verify(nat): if nat['deleted']: # no need to verify the CLI as NAT is going to be deactivated @@ -190,6 +203,32 @@ def verify(nat): if interface and interface not in interfaces(): print(f'NAT configuration warning: interface {interface} does not exist on this system') + err_msg = f"Source NAT configuration error in rule {rule['number']}:" + + if not rule['interface_out']: + raise ConfigError(f'{err_msg} outbound-interface not specified') + + if not rule['translation_address']: + raise ConfigError(f'{err_msg} translation address not specified') + else: + addr = rule['translation_address'] + if addr != 'masquerade' and not is_addr_assigned(addr): + printf(f'Warning: IP address {addr} does not exist on the system!') + + # common rule verification + verify_rule(rule) + + for rule in nat['destination']: + interface = rule['interface_in'] + if interface and interface not in interfaces(): + print(f'NAT configuration warning: interface {interface} does not exist on this system') + + if not rule['interface_in']: + raise ConfigError(f'{err_msg} inbound-interface not specified') + + # common rule verification + verify_rule(rule) + return None def generate(nat): -- cgit v1.2.3 From 5abe2db17a6e085441e674f8c2d92277014a7189 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 19:09:43 +0200 Subject: nat: T2198: extend verify() for destination ports Destination NAT configuration: destination ports can only be specified when protocol is tcp, udp or tcp_udp. --- src/conf_mode/nat.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index bde6841cc..8fd8272d2 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -63,6 +63,19 @@ def get_handler(json, chain, target): return None +def verify_rule(rule, err_msg): + if rule['translation_port'] or rule['dest_port']: + if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + proto = rule['protocol'] + raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') + + if '/' in rule['translation_address']: + raise ConfigError(f'{err_msg}\n' \ + 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'statically maps a whole network of addresses onto another\n' \ + 'network of addresses') + + def parse_source_destination(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ tmp = [] @@ -177,18 +190,6 @@ def get_config(): return nat -def verify_rule(rule): - if rule['translation_port']: - if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: - proto = rule['protocol'] - raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') - - if '/' in rule['translation_address']: - raise ConfigError(f'{err_msg}\n' \ - 'Cannot use ports with an IPv4net type translation address as it\n' \ - 'statically maps a whole network of addresses onto another\n' \ - 'network of addresses') - def verify(nat): if nat['deleted']: # no need to verify the CLI as NAT is going to be deactivated @@ -200,11 +201,11 @@ def verify(nat): for rule in nat['source']: interface = rule['interface_out'] + err_msg = f"Source NAT configuration error in rule {rule['number']}:" + if interface and interface not in interfaces(): print(f'NAT configuration warning: interface {interface} does not exist on this system') - err_msg = f"Source NAT configuration error in rule {rule['number']}:" - if not rule['interface_out']: raise ConfigError(f'{err_msg} outbound-interface not specified') @@ -216,10 +217,12 @@ def verify(nat): printf(f'Warning: IP address {addr} does not exist on the system!') # common rule verification - verify_rule(rule) + verify_rule(rule, err_msg) for rule in nat['destination']: interface = rule['interface_in'] + err_msg = f"Destination NAT configuration error in rule {rule['number']}:" + if interface and interface not in interfaces(): print(f'NAT configuration warning: interface {interface} does not exist on this system') @@ -227,7 +230,7 @@ def verify(nat): raise ConfigError(f'{err_msg} inbound-interface not specified') # common rule verification - verify_rule(rule) + verify_rule(rule, err_msg) return None @@ -238,6 +241,8 @@ def generate(nat): def apply(nat): cmd(f'{iptables_nat_config}') + if os.path.isfile(iptables_nat_config): + os.unlink(iptables_nat_config) return None -- cgit v1.2.3 From b2ead2d037b860f0a6a12b177e70e5d698fd00e8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 21:19:18 +0200 Subject: nat: T2198: verify translation address for SNAT and DNAT --- src/conf_mode/nat.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 8fd8272d2..4d739068f 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -64,6 +64,7 @@ def get_handler(json, chain, target): def verify_rule(rule, err_msg): + """ Common verify steps used for both source and destination NAT """ if rule['translation_port'] or rule['dest_port']: if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: proto = rule['protocol'] @@ -75,6 +76,13 @@ def verify_rule(rule, err_msg): 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') + if not rule['translation_address']: + raise ConfigError(f'{err_msg} translation address not specified') + else: + addr = rule['translation_address'] + if addr != 'masquerade' and not is_addr_assigned(addr): + print(f'Warning: IP address {addr} does not exist on the system!') + def parse_source_destination(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ @@ -209,13 +217,6 @@ def verify(nat): if not rule['interface_out']: raise ConfigError(f'{err_msg} outbound-interface not specified') - if not rule['translation_address']: - raise ConfigError(f'{err_msg} translation address not specified') - else: - addr = rule['translation_address'] - if addr != 'masquerade' and not is_addr_assigned(addr): - printf(f'Warning: IP address {addr} does not exist on the system!') - # common rule verification verify_rule(rule, err_msg) -- cgit v1.2.3 From 9cec8471dae531072946daf5dcb74a0a9fe1e86c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 23:01:27 +0200 Subject: nat: T2198: sync generated DNAT rules with VyOS 1.2 The generated NAT rules in VyOS 1.2 are compared to the generated nftables ruleset in VyOS 1.3 this was done by converting the 1.2 iptables ruleset to nftables and then do the diff. To convert from iptables to nftables use the following command: $ iptables-save -t nat > /tmp/tmp.iptables $ iptables-restore-translate -f /tmp/tmp.iptables The following CLI options have been used for testing: set nat destination rule 10 description 'foo-10' set nat destination rule 10 destination address '1.1.1.1' set nat destination rule 10 destination port '1111' set nat destination rule 10 exclude set nat destination rule 10 inbound-interface 'eth0.202' set nat destination rule 10 log set nat destination rule 10 protocol 'tcp_udp' set nat destination rule 10 translation address '192.0.2.10' set nat destination rule 15 description 'foo-10' set nat destination rule 15 destination address '1.1.1.1' set nat destination rule 15 exclude set nat destination rule 15 inbound-interface 'eth0.202' set nat destination rule 15 log set nat destination rule 15 protocol 'tcp_udp' set nat destination rule 15 translation address '192.0.2.10' set nat destination rule 20 description 'foo-20' set nat destination rule 20 destination address '2.2.2.2' set nat destination rule 20 inbound-interface 'eth0.201' set nat destination rule 20 log set nat destination rule 20 protocol 'tcp' set nat destination rule 20 translation address '192.0.2.10' --- data/templates/firewall/nftables-nat.tmpl | 60 +++++++++++++++++-------------- src/conf_mode/nat.py | 4 +-- 2 files changed, 35 insertions(+), 29 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 528c4d82a..929cae563 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -4,34 +4,33 @@ flush table nat {% if helper_functions == 'remove' %} - # NAT if going to be disabled - remove rules and targets from nftables - delete rule ip raw PREROUTING handle {{ pre_ct_ignore }} - delete rule ip raw PREROUTING handle {{ pre_ct_conntrack }} - delete rule ip raw OUTPUT handle {{ out_ct_ignore }} - delete rule ip raw OUTPUT handle {{ out_ct_conntrack }} +{# NAT if going to be disabled - remove rules and targets from nftables #} +delete rule ip raw PREROUTING handle {{ pre_ct_ignore }} +delete rule ip raw PREROUTING handle {{ pre_ct_conntrack }} +delete rule ip raw OUTPUT handle {{ out_ct_ignore }} +delete rule ip raw OUTPUT handle {{ out_ct_conntrack }} + +delete chain ip raw NAT_CONNTRACK - delete chain ip raw NAT_CONNTRACK {% elif helper_functions == 'add' %} - # NAT if enabled - add targets to nftables - add chain ip raw NAT_CONNTRACK - add rule ip raw NAT_CONNTRACK counter accept - - add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER - add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK - add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER - add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK -{% endif %} +{# NAT if enabled - add targets to nftables #} +add chain ip raw NAT_CONNTRACK +add rule ip raw NAT_CONNTRACK counter accept +add rule ip raw PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER +add rule ip raw PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK +add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER +add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK +{% endif %} {% for r in destination if not r.disabled -%} {% set chain = "PREROUTING" %} {% set dst_addr = "ip daddr " + r.dest_address if r.dest_address %} -{% set dst_port = "dport { " + r.dest_port +" }" %} -{% set trns_addr = r.translation_address %} +{% set dst_port = "dport { " + r.dest_port +" }" if r.dest_port %} +{% set trns_addr = "dnat to " + r.translation_address %} {% set trns_port = ":" + r.translation_port if r.translation_port %} -{% set trns = "dnat to " + trns_addr + trns_port if trns_port %} {% set comment = "DST-NAT-" + r.number %} -{% set iface = "iifname " + r.interface_in %} +{% set iface = r.interface_in %} {% if r.log %} {% if r.exclude %} @@ -45,25 +44,32 @@ flush table nat {% if r.exclude %} {# rule has been marked as "exclude" thus we simply return here #} -{% set trns = "return" %} +{% set trns_addr = "return" %} +{% set trns_port = "" %} {% endif %} {% if r.protocol == 'tcp_udp' %} {# Special handling for protocol tcp_udp which is represented as two individual rules #} +{% set comment = comment + " tcp_udp" %} {% if log %} -add rule ip nat {{ chain }} {{ iface }} tcp {{ dst_port }} counter log prefix "{{ log }}" comment "{{ comment }} tcp_udp" + +{% set tcp_dst_port = "tcp " + dst_port if dst_port else "ip protocol tcp" %} +{% set udp_dst_port = "udp " + dst_port if dst_port else "ip protocol udp" %} + +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} {{ iface }} tcp {{ dst_port }} counter {{ trns }} comment {{ comment }} +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% if log %} -add rule ip nat {{ chain }} {{ iface }} udp {{ dst_port }} counter log prefix "{{ log }}" comment "{{ comment }} tcp_udp" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} {{ iface }} udp {{ dst_port }} counter {{ trns }} comment {{ comment }} -{% else %} +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +{% else %} +{% set proto_dst_port = dst_port if dst_port else "ip protocol " + r.protocol %} {% if log %} -add rule ip nat {{ chain }} {{ iface }} {{ r.protocol }} counter log prefix "{{ log }}" comment {{ comment }} +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} {{ iface }} {{ dst_addr }} {{ r.protocol }} {{ dst_port }} counter {{ trns }} comment {{ comment }} +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% endif %} {% endfor %} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 4d739068f..13edca846 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -94,7 +94,7 @@ def parse_source_destination(conf, source_dest): 'description': '', 'dest_address': '', 'dest_port': '', - 'disable': False, + 'disabled': False, 'exclude': False, 'interface_in': '', 'interface_out': '', @@ -118,7 +118,7 @@ def parse_source_destination(conf, source_dest): rule['dest_port'] = conf.return_value(['destination', 'port']) if conf.exists(['disable']): - rule['disable'] = True + rule['disabled'] = True if conf.exists(['exclude']): rule['exclude'] = True -- cgit v1.2.3 From e89f19c5bb0f7aa611cb4a8ac435b99127eee6db Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 23:41:29 +0200 Subject: nat: T2198: set default protocol to all to be backwards compatible --- src/conf_mode/nat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 13edca846..ebac6bfc0 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -99,7 +99,7 @@ def parse_source_destination(conf, source_dest): 'interface_in': '', 'interface_out': '', 'log': False, - 'protocol': '', + 'protocol': 'all', 'number': number, 'source_address': '', 'source_port': '', -- cgit v1.2.3 From cf6dcb61e1f102f3a9b9edb86eeecac92f944d0d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 00:16:40 +0200 Subject: nat: T2198: add support for SNAT based on source addresses CLI commands used for ruleset generation: set nat source rule 100 outbound-interface 'eth0.202' set nat source rule 100 protocol 'all' set nat source rule 100 source address '192.0.2.0/26' set nat source rule 100 translation address 'masquerade' set nat source rule 110 outbound-interface 'eth0.202' set nat source rule 110 protocol 'tcp' set nat source rule 110 source address '192.0.2.0/26' set nat source rule 110 source port '5556' set nat source rule 110 translation address 'masquerade' --- data/templates/firewall/nftables-nat.tmpl | 38 ++++++++++++++++++++----------- src/conf_mode/nat.py | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) (limited to 'src/conf_mode') diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 928f4ecfe..9bab8b363 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -23,8 +23,11 @@ add rule ip raw OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPE add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK {% endif %} + {% for r in destination if not r.disabled -%} {% set chain = "PREROUTING" %} +{% set src_addr = "ip saddr " + r.source_address if r.source_address %} +{% set src_port = "sport { " + r.source_port +" }" if r.source_port %} {% set dst_addr = "ip daddr " + r.dest_address if r.dest_address %} {% set dst_port = "dport { " + r.dest_port +" }" if r.dest_port %} {% set trns_addr = "dnat to " + r.translation_address %} @@ -56,29 +59,33 @@ add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRAC {% set tcp_dst_port = "tcp " + dst_port if dst_port else "ip protocol tcp" %} {% set udp_dst_port = "udp " + dst_port if dst_port else "ip protocol udp" %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ tcp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% if log %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% else %} {% set proto_dst_port = dst_port if dst_port else "ip protocol " + r.protocol %} +{% set proto_dst_port = "" if r.protocol == "all" %} + {% if log %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% endif %} {% endfor %} {% for r in source if not r.disabled -%} {% set chain = "POSTROUTING" %} +{% set src_addr = "ip saddr " + r.source_address if r.source_address %} +{% set src_port = "sport { " + r.source_port +" }" if r.source_port %} {% set dst_addr = "ip daddr " + r.dest_address if r.dest_address %} {% set dst_port = "dport { " + r.dest_port +" }" if r.dest_port %} -{% set trns_addr = "snat to " + r.translation_address %} +{% set trns_addr = "snat to " + r.translation_address if r.translation_address != "masquerade" else "masquerade" %} {% set trns_port = ":" + r.translation_port if r.translation_port %} {% set comment = "SRC-NAT-" + r.number %} {% set iface = r.interface_out %} @@ -106,20 +113,25 @@ add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_ad {% set tcp_dst_port = "tcp " + dst_port if dst_port else "ip protocol tcp" %} {% set udp_dst_port = "udp " + dst_port if dst_port else "ip protocol udp" %} +{% set tcp_src_port = "tcp " + src_port if src_port %} +{% set udp_src_port = "udp " + src_port if src_port %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ tcp_src_port }} {{ src_port }} {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ tcp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ tcp_src_port }} {{ src_port }} {{ tcp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% if log %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ udp_src_port }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ udp_src_port }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% else %} {% set proto_dst_port = dst_port if dst_port else "ip protocol " + r.protocol %} +{% set proto_dst_port = proto_dst_port if r.protocol != "all" %} +{% set proto_src_port = r.protocol + " " + src_port if r.protocol != "all" else src_port %} + {% if log %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ src_addr }} {{ proto_src_port }} {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" {% endif %} -add rule ip nat {{ chain }} oifname "{{ iface }}" {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ src_addr }} {{ proto_src_port }} {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" {% endif %} {% endfor %} diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index ebac6bfc0..5cb1af1f1 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -65,7 +65,7 @@ def get_handler(json, chain, target): def verify_rule(rule, err_msg): """ Common verify steps used for both source and destination NAT """ - if rule['translation_port'] or rule['dest_port']: + if rule['translation_port'] or rule['dest_port'] or rule['source_port']: if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: proto = rule['protocol'] raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') -- cgit v1.2.3