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 --- debian/control | 3 + .../include/nat-address-port.xml.i | 47 +++++++++ interface-definitions/include/nat-rule.xml.i | 86 ++++++++++++++++ .../include/nat-translation-port.xml.i | 13 +++ interface-definitions/nat.xml.in | 110 +++++++++++++++++++++ src/conf_mode/nat.py | 63 ++++++++++++ 6 files changed, 322 insertions(+) create mode 100644 interface-definitions/include/nat-address-port.xml.i create mode 100644 interface-definitions/include/nat-rule.xml.i create mode 100644 interface-definitions/include/nat-translation-port.xml.i create mode 100644 interface-definitions/nat.xml.in create mode 100755 src/conf_mode/nat.py diff --git a/debian/control b/debian/control index ab0fc0b29..c8fa8ca63 100644 --- a/debian/control +++ b/debian/control @@ -92,6 +92,9 @@ Depends: python3, pppoe, salt-minion, vyos-utils, + iptables, + nftables, + conntrack, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/interface-definitions/include/nat-address-port.xml.i b/interface-definitions/include/nat-address-port.xml.i new file mode 100644 index 000000000..0848364ff --- /dev/null +++ b/interface-definitions/include/nat-address-port.xml.i @@ -0,0 +1,47 @@ + + + IP address, subnet, or range + + ipv4 + IPv4 address to match + + + ipv4net + IPv4 prefix to match + + + ipv4range + IPv4 address range to match + + + !ipv4 + Match everything except the specified address + + + !ipv4net + Match everything except the specified prefix + + + !ipv4range + Match everything except the specified range + + + + + + + Port number + + 1-65535 + Numeric IP port + + + start-end + Numbered port range (e.g., 1001-1005) + + + + \n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005' + + + diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i new file mode 100644 index 000000000..fdba4b8bd --- /dev/null +++ b/interface-definitions/include/nat-rule.xml.i @@ -0,0 +1,86 @@ + + + Rule number for NAT + + 1-9999 + Number for this NAT rule + + + + + NAT rule number must be between 1 and 9999 + + + + + Rule description + + + + + NAT destination parameters + + + #include + + + + + Disable NAT rule + + + + + + Exclude packets matching this rule from NAT + + + + + + NAT rule logging + + + + + + Protocol to NAT + + tcp udp tcp_udp all + + + tcp + Transmission Control Protocol + + + udp + User Datagram Protocol + + + tcp_udp + Both TCP and UDP + + + all + All IP protocols + + + 0-255 + IP protocol number + + + !<protocol> + All IP protocols except for the specified name or number (negation) + + + + + + NAT source parameters + + + #include + + + + diff --git a/interface-definitions/include/nat-translation-port.xml.i b/interface-definitions/include/nat-translation-port.xml.i new file mode 100644 index 000000000..93de471e3 --- /dev/null +++ b/interface-definitions/include/nat-translation-port.xml.i @@ -0,0 +1,13 @@ + + + Port number + + 1-65535 + Numeric IP port + + + <start>-<end> + Numbered port range (e.g., 1001-1005) + + + diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in new file mode 100644 index 000000000..bcbdb37af --- /dev/null +++ b/interface-definitions/nat.xml.in @@ -0,0 +1,110 @@ + + + + + Network Address Translation (NAT) parameters + 220 + + + + + Destination NAT settings + + + #include + + + + + Inbound interface of NAT traffic + + + + + + + + Inside NAT IP (destination NAT only) + + + + + IP address, subnet, or range + + ipv4 + IPv4 address to match + + + ipv4net + IPv4 prefix to match + + + ipv4range + IPv4 address range to match + + + + + #include + + + + + + + + + Source NAT settings + + + #include + + + + + Outbound interface of NAT traffic + + + + + + + + Outside NAT IP (source NAT only) + + + + + IP address, subnet, or range + + masquerade + + + ipv4 + IPv4 address to match + + + ipv4net + IPv4 prefix to match + + + ipv4range + IPv4 address range to match + + + masquerade + NAT to the primary address of outbound-interface + + + + + #include + + + + + + + + + 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 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 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 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(-) 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(-) 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(-) 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 728e1c6073cb216d3cb8b66f519bd590458165e6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 12 May 2020 18:44:20 +0200 Subject: nat: T2198: add new ipv4-range validator --- interface-definitions/nat.xml.in | 7 ++++++- src/validators/ipv4-range | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100755 src/validators/ipv4-range diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index bcbdb37af..af9dd1eff 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -95,7 +95,12 @@ masquerade NAT to the primary address of outbound-interface - + + + + + (masquerade) + #include diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range new file mode 100755 index 000000000..0d707d6c5 --- /dev/null +++ b/src/validators/ipv4-range @@ -0,0 +1,30 @@ +#!/bin/bash + +# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter +ip2dec () { + local a b c d ip=$@ + IFS=. read -r a b c d <<< "$ip" + printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" +} + +# This only works with real bash (<<<) - split IP addresses into array with +# hyphen as delimiter +readarray -d - -t strarr <<< $1 + +ipaddrcheck --is-ipv4-single ${strarr[0]} +if [ $? -gt 0 ]; then + exit 1 +fi + +ipaddrcheck --is-ipv4-single ${strarr[1]} +if [ $? -gt 0 ]; then + exit 1 +fi + +start=$(ip2dec ${strarr[0]}) +stop=$(ip2dec ${strarr[1]}) +if [ $start -ge $stop ]; then + exit 1 +fi + +exit 0 -- cgit v1.2.3 From 1330898ed095b42b6aba7ba00f9a6932b241a230 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 12 May 2020 19:23:15 +0200 Subject: nat: T2198: add ipv4-{address,prefix,rage}-exclude validators Exclude validators are required to support the ! (not) operator on the CLI to exclude addresses from NAT. --- .../include/nat-address-port.xml.i | 9 +++++- src/validators/ipv4-address-exclude | 7 +++++ src/validators/ipv4-prefix-exclude | 7 +++++ src/validators/ipv4-range | 33 ++++++++++++---------- src/validators/ipv4-range-exclude | 7 +++++ 5 files changed, 47 insertions(+), 16 deletions(-) create mode 100755 src/validators/ipv4-address-exclude create mode 100755 src/validators/ipv4-prefix-exclude create mode 100755 src/validators/ipv4-range-exclude diff --git a/interface-definitions/include/nat-address-port.xml.i b/interface-definitions/include/nat-address-port.xml.i index 0848364ff..8705d31cb 100644 --- a/interface-definitions/include/nat-address-port.xml.i +++ b/interface-definitions/include/nat-address-port.xml.i @@ -25,7 +25,14 @@ !ipv4range Match everything except the specified range - + + + + + + + + diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude new file mode 100755 index 000000000..80ad17d45 --- /dev/null +++ b/src/validators/ipv4-address-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-address "${arg:1}" diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude new file mode 100755 index 000000000..4f7de400a --- /dev/null +++ b/src/validators/ipv4-prefix-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-prefix "${arg:1}" diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range index 0d707d6c5..ae3f3f163 100755 --- a/src/validators/ipv4-range +++ b/src/validators/ipv4-range @@ -7,24 +7,27 @@ ip2dec () { printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" } -# This only works with real bash (<<<) - split IP addresses into array with -# hyphen as delimiter -readarray -d - -t strarr <<< $1 +# Only run this if there is a hypen present in $1 +if [[ "$1" =~ "-" ]]; then + # This only works with real bash (<<<) - split IP addresses into array with + # hyphen as delimiter + readarray -d - -t strarr <<< $1 -ipaddrcheck --is-ipv4-single ${strarr[0]} -if [ $? -gt 0 ]; then - exit 1 -fi + ipaddrcheck --is-ipv4-single ${strarr[0]} + if [ $? -gt 0 ]; then + exit 1 + fi -ipaddrcheck --is-ipv4-single ${strarr[1]} -if [ $? -gt 0 ]; then - exit 1 -fi + ipaddrcheck --is-ipv4-single ${strarr[1]} + if [ $? -gt 0 ]; then + exit 1 + fi -start=$(ip2dec ${strarr[0]}) -stop=$(ip2dec ${strarr[1]}) -if [ $start -ge $stop ]; then - exit 1 + start=$(ip2dec ${strarr[0]}) + stop=$(ip2dec ${strarr[1]}) + if [ $start -ge $stop ]; then + exit 1 + fi fi exit 0 diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude new file mode 100755 index 000000000..3787b4dec --- /dev/null +++ b/src/validators/ipv4-range-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-range "${arg:1}" -- cgit v1.2.3 From ac4f99ac3b176f1804b17b32e6615e8b3701dfe8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 12 May 2020 19:28:06 +0200 Subject: nat: T2198: split nat-address-port include into individual files --- .../include/nat-address-port.xml.i | 54 ---------------------- interface-definitions/include/nat-address.xml.i | 37 +++++++++++++++ interface-definitions/include/nat-port.xml.i | 17 +++++++ interface-definitions/include/nat-rule.xml.i | 6 ++- 4 files changed, 58 insertions(+), 56 deletions(-) delete mode 100644 interface-definitions/include/nat-address-port.xml.i create mode 100644 interface-definitions/include/nat-address.xml.i create mode 100644 interface-definitions/include/nat-port.xml.i diff --git a/interface-definitions/include/nat-address-port.xml.i b/interface-definitions/include/nat-address-port.xml.i deleted file mode 100644 index 8705d31cb..000000000 --- a/interface-definitions/include/nat-address-port.xml.i +++ /dev/null @@ -1,54 +0,0 @@ - - - IP address, subnet, or range - - ipv4 - IPv4 address to match - - - ipv4net - IPv4 prefix to match - - - ipv4range - IPv4 address range to match - - - !ipv4 - Match everything except the specified address - - - !ipv4net - Match everything except the specified prefix - - - !ipv4range - Match everything except the specified range - - - - - - - - - - - - - - Port number - - 1-65535 - Numeric IP port - - - start-end - Numbered port range (e.g., 1001-1005) - - - - \n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005' - - - diff --git a/interface-definitions/include/nat-address.xml.i b/interface-definitions/include/nat-address.xml.i new file mode 100644 index 000000000..933dae07b --- /dev/null +++ b/interface-definitions/include/nat-address.xml.i @@ -0,0 +1,37 @@ + + + IP address, subnet, or range + + ipv4 + IPv4 address to match + + + ipv4net + IPv4 prefix to match + + + ipv4range + IPv4 address range to match + + + !ipv4 + Match everything except the specified address + + + !ipv4net + Match everything except the specified prefix + + + !ipv4range + Match everything except the specified range + + + + + + + + + + + diff --git a/interface-definitions/include/nat-port.xml.i b/interface-definitions/include/nat-port.xml.i new file mode 100644 index 000000000..24803ae05 --- /dev/null +++ b/interface-definitions/include/nat-port.xml.i @@ -0,0 +1,17 @@ + + + Port number + + 1-65535 + Numeric IP port + + + start-end + Numbered port range (e.g., 1001-1005) + + + + \n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005' + + + diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index fdba4b8bd..183692664 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -21,7 +21,8 @@ NAT destination parameters - #include + #include + #include @@ -79,7 +80,8 @@ NAT source parameters - #include + #include + #include -- 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(+) 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 f233a9e4073f3c5257923962a96af5833734e41e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 14 May 2020 18:37:39 +0200 Subject: nat: T2198: migrate "show nat" commands to XML and Python - "show nat source|destination statistics" is now implemented in Python - "show nat source|destination rules" needs a new implementation, see T2459 - "show nat source|destination translations" has been copied over from the old repo and is here until it is rewritten, this was not possible for "rules" as there would have been too much dependencies. This one only requires libxml-simple-perl --- debian/control | 1 + op-mode-definitions/nat.xml | 98 ++++++++ src/op_mode/show_nat_statistics.py | 63 +++++ .../to_be_migrated/vyatta-nat-translations.pl | 267 +++++++++++++++++++++ 4 files changed, 429 insertions(+) create mode 100644 op-mode-definitions/nat.xml create mode 100755 src/op_mode/show_nat_statistics.py create mode 100755 src/op_mode/to_be_migrated/vyatta-nat-translations.pl diff --git a/debian/control b/debian/control index 323b8130f..f7285941a 100644 --- a/debian/control +++ b/debian/control @@ -91,6 +91,7 @@ Depends: python3, pmacct (>= 1.6.0), python3-certbot-nginx, pppoe, + libxml-simple-perl, salt-minion, vyos-utils, nftables (>= 0.9.3), diff --git a/op-mode-definitions/nat.xml b/op-mode-definitions/nat.xml new file mode 100644 index 000000000..ffaa2cba3 --- /dev/null +++ b/op-mode-definitions/nat.xml @@ -0,0 +1,98 @@ + + + + + + + Show Network Address Translation (NAT) information + + + + + Show source Network Address Translation (NAT) information + + + + + Show configured source NAT rules + + echo To be migrated to Python - https://phabricator.vyos.net/T2459 + + + + Show statistics for configured source NAT rules + + ${vyos_op_scripts_dir}/show_nat_statistics.py --source + + + + Show active source NAT translations + + + + + Show active source NAT translations for an IP address + + <x.x.x.x> + + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source --verbose --ipaddr="$6" + + + + Show active source NAT translations detail + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source --verbose + + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source + + + + + + Show destination Network Address Translation (NAT) information + + + + + Show configured destination NAT rules + + echo To be migrated to Python - https://phabricator.vyos.net/T2459 + + + + Show statistics for configured destination NAT rules + + ${vyos_op_scripts_dir}/show_nat_statistics.py --destination + + + + Show active destination NAT translations + + + + + Show active NAT destination translations for an IP address + + <x.x.x.x> + + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination --verbose --ipaddr="$6" + + + + Show active destination NAT translations detail + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination --verbose + + + ${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination + + + + + + + + diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py new file mode 100755 index 000000000..b77693e19 --- /dev/null +++ b/src/op_mode/show_nat_statistics.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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 . + +import jmespath +import json + +from argparse import ArgumentParser +from jinja2 import Template +from sys import exit +from vyos.util import cmd + +OUT_TMPL_SRC=""" +rule pkts bytes interface +---- ---- ----- --------- +{% for r in output %} +{%- if r.comment -%} +{%- set packets = r.counter.packets -%} +{%- set bytes = r.counter.bytes -%} +{%- set interface = r.interface -%} +{# remove rule comment prefix #} +{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') -%} +{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} +{%- endif %} +{% endfor %} +""" + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") +group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") +args = parser.parse_args() + +if args.source or args.destination: + tmp = cmd('sudo nft -j list table 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] }" + destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" + data = { + 'output' : jmespath.search(source if args.source else destination, tmp), + 'direction' : 'source' if args.source else 'destination' + } + + tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True) + print(tmpl.render(data)) + exit(0) +else: + parser.print_help() + exit(1) + diff --git a/src/op_mode/to_be_migrated/vyatta-nat-translations.pl b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl new file mode 100755 index 000000000..94ed74bad --- /dev/null +++ b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl @@ -0,0 +1,267 @@ +#!/usr/bin/perl +# +# Module: vyatta-nat-translate.pl +# +# **** License **** +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. +# +# This code was originally developed by Vyatta, Inc. +# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc. +# All Rights Reserved. +# +# Author: Stig Thormodsrud +# Date: July 2008 +# Description: Script to display nat translations +# +# **** End License **** +# + +use Getopt::Long; +use XML::Simple; +use Data::Dumper; +use POSIX; + +use warnings; +use strict; + +my $dump = 0; +my ($xml_file, $verbose, $proto, $stats, $ipaddr, $pipe); +my $type; +my $verbose_format = "%-20s %-18s %-20s %-18s\n"; +my $format = "%-20s %-20s %-4s %-8s"; + +sub add_xml_root { + my $xml = shift; + + $xml = '' . $xml . ''; + return $xml; +} + + +sub read_xml_file { + my $file = shift; + + local($/, *FD); # slurp mode + open FD, "<", $file or die "Couldn't open $file\n"; + my $xml = ; + close FD; + return $xml; +} + +sub print_xml { + my $data = shift; + print Dumper($data); +} + +sub guess_snat_dnat { + my ($src, $dst) = @_; + + if ($src->{original} eq $dst->{reply}) { + return "dnat"; + } + if ($dst->{original} eq $src->{reply}) { + return "snat"; + } + return "unkn"; +} + +sub nat_print_xml { + my ($data, $type) = @_; + + my $flow = 0; + + my %flowh; + while (1) { + my $meta = 0; + last if ! defined $data->{flow}[$flow]; + my $flow_ref = $data->{flow}[$flow]; + my $flow_type = $flow_ref->{type}; + my (%src, %dst, %sport, %dport, %proto); + my (%packets, %bytes); + my $timeout = undef; + my $uses = undef; + while (1) { + my $meta_ref = $flow_ref->{meta}[$meta]; + last if ! defined $meta_ref; + my $dir = $meta_ref->{direction}; + if ($dir eq 'original' or $dir eq 'reply') { + my $l3_ref = $meta_ref->{layer3}[0]; + my $l4_ref = $meta_ref->{layer4}[0]; + my $count_ref = $meta_ref->{counters}[0]; + if (defined $l3_ref) { + $src{$dir} = $l3_ref->{src}[0]; + $dst{$dir} = $l3_ref->{dst}[0]; + if (defined $l4_ref) { + $sport{$dir} = $l4_ref->{sport}[0]; + $dport{$dir} = $l4_ref->{dport}[0]; + $proto{$dir} = $l4_ref->{protoname}; + } + } + if (defined $stats and defined $count_ref) { + $packets{$dir} = $count_ref->{packets}[0]; + $bytes{$dir} = $count_ref->{bytes}[0]; + } + } elsif ($dir eq 'independent') { + $timeout = $meta_ref->{timeout}[0]; + $uses = $meta_ref->{'use'}[0]; + } + $meta++; + } + my ($proto, $in_src, $in_dst, $out_src, $out_dst); + $proto = $proto{original}; + $in_src = "$src{original}"; + $in_src .= ":$sport{original}" if defined $sport{original}; + $in_dst = "$dst{original}"; + $in_dst .= ":$dport{original}" if defined $dport{original}; + $out_src = "$dst{reply}"; + $out_src .= ":$dport{reply}" if defined $dport{reply}; + $out_dst = "$src{reply}"; + $out_dst .= ":$sport{reply}" if defined $sport{reply}; + if (defined $verbose) { + printf($verbose_format, $in_src, $in_dst, $out_src, $out_dst); + } +# if (! defined $type) { +# $type = guess_snat_dnat(\%src, \%dst); +# } + if (defined $type) { + my ($from, $to); + if ($type eq 'source') { + $from = "$src{original}"; + $to = "$dst{reply}"; + if (defined $sport{original} and defined $dport{reply}) { + if ($sport{original} ne $dport{reply}) { + $from .= ":$sport{original}"; + $to .= ":$dport{reply}"; + } + } + } else { + $from = "$dst{original}"; + $to = "$src{reply}"; + if (defined $dport{original} and defined $sport{reply}) { + if ($dport{original} ne $sport{reply}) { + $from .= ":$dport{original}"; + $to .= ":$sport{reply}"; + } + } + } + if (defined $verbose) { + print " $proto: $from ==> $to"; + } else { + my $timeout2 = ""; + if (defined $timeout) { + $timeout2 = $timeout; + } + printf($format, $from, $to, $proto, $timeout2); + print " $flow_type" if defined $flow_type; + print "\n"; + } + } + if (defined $verbose) { + print " timeout: $timeout" if defined $timeout; + print " use: $uses " if defined $uses; + print " type: $flow_type" if defined $flow_type; + print "\n"; + } + if (defined $stats) { + foreach my $dir ('original', 'reply') { + if (defined $packets{$dir}) { + printf(" %-8s: packets %s, bytes %s\n", + $dir, $packets{$dir}, $bytes{$dir}); + } + } + } + $flow++; + } + return $flow; +} + + +# +# main +# +GetOptions("verbose" => \$verbose, + "proto=s" => \$proto, + "file=s" => \$xml_file, + "stats" => \$stats, + "type=s" => \$type, + "ipaddr=s" => \$ipaddr, + "pipe" => \$pipe, +); + +my $conntrack = '/usr/sbin/conntrack'; +if (! -f $conntrack) { + die "Package [conntrack] not installed"; +} + +die "Must specify NAT type!" if !defined($type); +die "Unknown NAT type!" if (($type ne 'source') && ($type ne 'destination')); + +my $xs = XML::Simple->new(ForceArray => 1, KeepRoot => 0); +my ($xml, $data); + +# flush stdout after every write for pipe mode +$| = 1 if defined $pipe; + +if (defined $verbose) { + printf($verbose_format, 'Pre-NAT src', 'Pre-NAT dst', + 'Post-NAT src', 'Post-NAT dst'); +} else { + printf($format, 'Pre-NAT', 'Post-NAT', 'Prot', 'Timeout'); + print " Type" if defined $pipe; + print "\n"; +} + +if (defined $xml_file) { + $xml = read_xml_file($xml_file); + $data = $xs->XMLin($xml); + if ($dump) { + print_xml($data); + exit; + } + nat_print_xml($data, 'snat'); + +} elsif (defined $pipe) { + while ($xml = ) { + $xml =~ s/\<\?xml version=\"1\.0\" encoding=\"utf-8\"\?\>//; + $xml =~ s/\//; + $xml = add_xml_root($xml); + $data = $xs->XMLin($xml); + nat_print_xml($data, $type); + } +} else { + if (defined $proto) { + $proto = "-p $proto" + } else { + $proto = ""; + } + if ($type eq 'source') { + my $ipopt = ""; + if (defined $ipaddr) { + $ipopt = "--orig-src $ipaddr"; + } + $xml = `sudo $conntrack -L -n $ipopt -o xml $proto 2>/dev/null`; + chomp $xml; + $data = undef; + $data = $xs->XMLin($xml) if ! $xml eq ''; + } + if ($type eq 'destination') { + my $ipopt = ""; + if (defined $ipaddr) { + $ipopt = "--orig-dst $ipaddr"; + } + $xml = `sudo $conntrack -L -g $ipopt -o xml $proto 2>/dev/null`; + chomp $xml; + $data = undef; + $data = $xs->XMLin($xml) if ! $xml eq ''; + } + nat_print_xml($data, $type) if defined $data; +} + +# end of file -- cgit v1.2.3 From 756e36da2cf41694981a43f6fed0d558d80eaac2 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 18:28:04 +0200 Subject: nat: T2198: add protocol completion helper and regex constraint --- interface-definitions/include/nat-rule.xml.i | 233 +++++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 9 deletions(-) diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index 183692664..b52eb86c3 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -47,32 +47,247 @@ Protocol to NAT - tcp udp tcp_udp all + all ip hopopt icmp igmp ggp ipencap st tcp egp igp pup udp tcp_udp hmp xns-idp rdp iso-tp4 dccp xtp ddp idpr-cmtp ipv6 ipv6-route ipv6-frag idrp rsvp gre esp ah skip ipv6-icmp ipv6-nonxt ipv6-opts rspf vmtp eigrp ospf ax.25 ipip etherip encap 99 pim ipcomp vrrp l2tp isis sctp fc mobility-header udplite mpls-in-ip manet hip shim6 wesp rohc + + all + All IP protocols + + + ip + Internet Protocol, pseudo protocol number + + + hopopt + IPv6 Hop-by-Hop Option [RFC1883] + + + icmp + internet control message protocol + + + igmp + Internet Group Management + + + ggp + gateway-gateway protocol + + + ipencap + IP encapsulated in IP (officially IP) + + + st + ST datagram mode + tcp - Transmission Control Protocol + transmission control protocol + + + egp + exterior gateway protocol + + + igp + any private interior gateway (Cisco) + + + pup + PARC universal packet protocol udp - User Datagram Protocol + user datagram protocol tcp_udp Both TCP and UDP - all - All IP protocols + hmp + host monitoring protocol - 0-255 - IP protocol number + xns-idp + Xerox NS IDP + + + rdp + "reliable datagram" protocol + + + iso-tp4 + ISO Transport Protocol class 4 [RFC905] + + + dccp + Datagram Congestion Control Prot. [RFC4340] + + + xtp + Xpress Transfer Protocol + + + ddp + Datagram Delivery Protocol + + + idpr-cmtp + IDPR Control Message Transport + + + Ipv6 + Internet Protocol, version 6 + + + ipv6-route + Routing Header for IPv6 + + + ipv6-frag + Fragment Header for IPv6 + + + idrp + Inter-Domain Routing Protocol + + + rsvp + Reservation Protocol + + + gre + General Routing Encapsulation + + + esp + Encap Security Payload [RFC2406] + + + ah + Authentication Header [RFC2402] + + + skip + SKIP + + + ipv6-icmp + ICMP for IPv6 + + + ipv6-nonxt + No Next Header for IPv6 + + + ipv6-opts + Destination Options for IPv6 + + + rspf + Radio Shortest Path First (officially CPHB) + + + vmtp + Versatile Message Transport + + + eigrp + Enhanced Interior Routing Protocol (Cisco) + + + ospf + Open Shortest Path First IGP + + + ax.25 + AX.25 frames + + + ipip + IP-within-IP Encapsulation Protocol + + + etherip + Ethernet-within-IP Encapsulation [RFC3378] + + + encap + Yet Another IP encapsulation [RFC1241] + + + 99 + Any private encryption scheme + + + pim + Protocol Independent Multicast + + + ipcomp + IP Payload Compression Protocol - !<protocol> - All IP protocols except for the specified name or number (negation) + vrrp + Virtual Router Redundancy Protocol [RFC5798] + + + l2tp + Layer Two Tunneling Protocol [RFC2661] + + + isis + IS-IS over IPv4 + + + sctp + Stream Control Transmission Protocol + + + fc + Fibre Channel + + + mobility-header + Mobility Support for IPv6 [RFC3775] + + + udplite + UDP-Lite [RFC3828] + + + mpls-in-ip + MPLS-in-IP [RFC4023] + + + manet + MANET Protocols [RFC5498] + + + hip + Host Identity Protocol + + + shim6 + Shim6 Protocol + + + wesp + Wrapped Encapsulating Security Payload + + + rohc + Robust Header Compression + + + 0-255 + IP protocol number + + !?(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc|[01]?[0-9][0-9]?) + -- cgit v1.2.3 From d0b24799d9001cb467fd36fe3757bcfee7b9abc1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 18:50:01 +0200 Subject: nat: T2198: migrate "log enable" node to only "log" --- src/migration-scripts/nat/4-to-5 | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100755 src/migration-scripts/nat/4-to-5 diff --git a/src/migration-scripts/nat/4-to-5 b/src/migration-scripts/nat/4-to-5 new file mode 100755 index 000000000..dda191719 --- /dev/null +++ b/src/migration-scripts/nat/4-to-5 @@ -0,0 +1,58 @@ +#!/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 . + +# Drop the enable/disable from the nat "log" node. If log node is specified +# it is "enabled" + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + 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) +else: + for direction in ['source', 'destination']: + if not config.exists(['nat', direction]): + continue + + for rule in config.list_nodes(['nat', direction, 'rule']): + base = ['nat', direction, 'rule', rule] + + # Check if the log node exists and if log is enabled, + # migrate it to the new valueless 'log' node + if config.exists(base + ['log']): + tmp = config.return_value(base + ['log']) + config.delete(base + ['log']) + if tmp == 'enable': + config.set(base + ['log']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3 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(-) 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(-) 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 682bfd2c869acbf9f7c6dc681e28ca703c290d7f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 21:43:39 +0200 Subject: nat: T2198: restructure DNAT template Make the entire template more maintainable --- data/templates/firewall/nftables-nat.tmpl | 146 +++++++++++++++++++----------- 1 file changed, 91 insertions(+), 55 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 161ef27fb..01dcec19f 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -1,55 +1,91 @@ -#!/usr/sbin/nft -f - -# Start with clean NAT table -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 }} - - 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 %} -{% 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 %} +#!/usr/sbin/nft -f + +# Start with clean NAT table +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 }} + + 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 -%} +{% 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 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 %} + +{% if r.log %} +{% if r.exclude %} +{% set log = "[" + comment + "-EXCL]" %} +{% elif r.translation_address == 'masquerade' %} +{% set log = "[" + comment + "-MASQ]" %} +{% else %} +{% set log = "[" + comment + "]" %} +{% endif %} +{% endif %} + +{% if r.exclude %} +{# rule has been marked as "exclude" thus we simply return here #} +{% set trns = "return" %} +{% endif %} + + +{% if r.protocol == 'tcp_udp' %} +{# Special handling for protocol tcp_udp which is represented as two individual rules #} +{% if log %} +add rule ip nat {{ chain }} {{ iface }} tcp {{ dst_port }} counter log prefix "{{ log }}" comment "{{ comment }} tcp_udp" +{% endif %} +add rule ip nat {{ chain }} {{ iface }} tcp {{ dst_port }} counter {{ trns }} comment {{ comment }} +{% if log %} +add rule ip nat {{ chain }} {{ iface }} udp {{ dst_port }} counter log prefix "{{ log }}" comment "{{ comment }} tcp_udp" +{% endif %} +add rule ip nat {{ chain }} {{ iface }} udp {{ dst_port }} counter {{ trns }} comment {{ comment }} +{% else %} + +{% if log %} +add rule ip nat {{ chain }} {{ iface }} {{ r.protocol }} counter log prefix "{{ log }}" comment {{ comment }} +{% endif %} +add rule ip nat {{ chain }} {{ iface }} {{ dst_addr }} {{ r.protocol }} {{ dst_port }} counter {{ trns }} comment {{ comment }} +{% 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 %} + -- cgit v1.2.3 From f75db67c495c0e9e251bebba46b75e9d085dece0 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 21:47:08 +0200 Subject: nat: T2198: do not run DNAT rule if rule is disabled --- data/templates/firewall/nftables-nat.tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 01dcec19f..528c4d82a 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -23,7 +23,7 @@ flush table nat {% endif %} -{% for r in destination -%} +{% 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 +" }" %} @@ -48,7 +48,6 @@ flush table nat {% set trns = "return" %} {% endif %} - {% if r.protocol == 'tcp_udp' %} {# Special handling for protocol tcp_udp which is represented as two individual rules #} {% if log %} -- 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(-) 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 8062afa8a5beb73464e911cf7c5ca66f58585d0b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 15 May 2020 23:22:22 +0200 Subject: nat: T2198: sync generated SNAT 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 source rule 10 description 'foo-10' set nat source rule 10 destination address '1.1.1.1' set nat source rule 10 destination port '1111' set nat source rule 10 exclude set nat source rule 10 log 'enable' set nat source rule 10 outbound-interface 'eth0.202' set nat source rule 10 protocol 'tcp_udp' set nat source rule 10 translation address '192.0.2.10' set nat source rule 15 description 'foo-10' set nat source rule 15 destination address '1.1.1.1' set nat source rule 15 exclude set nat source rule 15 log 'enable' set nat source rule 15 outbound-interface 'eth0.202' set nat source rule 15 protocol 'tcp_udp' set nat source rule 15 translation address '192.0.2.10' set nat source rule 20 description 'foo-20' set nat source rule 20 destination address '2.2.2.2' set nat source rule 20 log 'enable' set nat source rule 20 outbound-interface 'eth0.201' set nat source rule 20 protocol 'tcp' set nat source rule 20 translation address '192.0.2.10' 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' --- data/templates/firewall/nftables-nat.tmpl | 49 ++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 929cae563..928f4ecfe 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -74,23 +74,52 @@ add rule ip nat {{ chain }} iifname "{{ iface }}" {{ proto_dst_port }} {{ dst_ad {% endfor %} -{% for r in source -%} +{% for r in source if not r.disabled -%} +{% set chain = "POSTROUTING" %} +{% 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_port = ":" + r.translation_port if r.translation_port %} +{% set comment = "SRC-NAT-" + r.number %} +{% set iface = r.interface_out %} + {% if r.log %} {% if r.exclude %} -{% set value = 'EXCL' %} +{% set log = "[" + comment + "-EXCL]" %} {% elif r.translation_address == 'masquerade' %} -{% set value = 'MASQ' %} +{% set log = "[" + comment + "-MASQ]" %} +{% else %} +{% set log = "[" + comment + "]" %} {% 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' %} +{# rule has been marked as "exclude" thus we simply return here #} +{% 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 %} + +{% 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 }} oifname "{{ iface }}" {{ 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 }}" +{% if log %} +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ 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 }}" + {% else %} -{% set value = 'snat to ' + r.translation_address %} +{% set proto_dst_port = dst_port if dst_port else "ip protocol " + r.protocol %} +{% if log %} +add rule ip nat {{ chain }} oifname "{{ iface }}" {{ 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 }}" {% endif %} - add rule ip nat POSTROUTING oifname "{{ r.interface_out }}" ip saddr {{ r.source_address }} counter {{ value }} comment "SRC-NAT-{{ r.number }}" {% endfor %} - -- 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(-) 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(-) 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 From d7662ecfff558192a5b5009679108ca58c8518fa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 15:29:31 +0200 Subject: nat: T2198: restructure DNAT template part for less duplicated code Build up only one output rule string by appending the configuration part by part. --- data/templates/firewall/nftables-nat.tmpl | 104 +++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 9bab8b363..5ce110d82 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -5,10 +5,12 @@ 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 }} + +{% set base_command = "delete rule ip raw" %} +{{ base_command }} PREROUTING handle {{ pre_ct_ignore }} +{{ base_command }} OUTPUT handle {{ out_ct_ignore }} +{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }} +{{ base_command }} OUTPUT handle {{ out_ct_conntrack }} delete chain ip raw NAT_CONNTRACK @@ -17,13 +19,17 @@ delete chain ip raw NAT_CONNTRACK 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 %} +{% set base_command = "add rule ip raw" %} +{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER +{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER +{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK +{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK +{% endif %} +# +# Destination NAT rules build up here +# {% for r in destination if not r.disabled -%} {% set chain = "PREROUTING" %} {% set src_addr = "ip saddr " + r.source_address if r.source_address %} @@ -32,16 +38,24 @@ add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRAC {% 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 interface = " iifname \"" + r.interface_in + "\"" %} {% set comment = "DST-NAT-" + r.number %} -{% set iface = r.interface_in %} + +{% if r.protocol == "tcp_udp" %} +{% set protocol = "tcp" %} +{% set comment = comment + " tcp_udp" %} +{% else %} +{% set protocol = r.protocol %} +{% endif %} {% if r.log %} +{% set base_log = "[NAT-DST-" + r.number %} {% if r.exclude %} -{% set log = "[" + comment + "-EXCL]" %} +{% set log = base_log + "-EXCL]" %} {% elif r.translation_address == 'masquerade' %} -{% set log = "[" + comment + "-MASQ]" %} +{% set log = base_log + "-MASQ]" %} {% else %} -{% set log = "[" + comment + "]" %} +{% set log = base_log + "]" %} {% endif %} {% endif %} @@ -51,34 +65,60 @@ add rule ip raw OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRAC {% 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 %} +{% set output = "add rule ip nat " + chain + interface + " counter" %} +{% set output = output + " comment \"" + comment + "\"" %} -{% 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" %} +{% if src_addr %} +{% set output = output + " " + src_addr %} +{% endif %} -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 }}" {{ 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 }}" {{ src_addr }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" -{% endif %} -add rule ip nat {{ chain }} iifname "{{ iface }}" {{ src_addr }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +{% if src_port %} +{% set output = output + " " + src_port %} +{% endif %} +{% if dst_addr %} +{% set output = output + " " + dst_addr %} +{% endif %} + +{% if dst_port %} +{% set output = output + " " + protocol + " " + dst_port %} {% else %} -{% set proto_dst_port = dst_port if dst_port else "ip protocol " + r.protocol %} -{% set proto_dst_port = "" if r.protocol == "all" %} +{% set output = output + " ip protocol " + protocol %} +{% endif %} -{% if log %} -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 }}" {{ src_addr }} {{ src_port }} {{ proto_dst_port }} {{ dst_addr }} counter {{ trns_addr }}{{ trns_port }} comment "{{ comment }}" +{# Special handling of log option, we must repeat the entire rule before the #} +{# NAT translation options are added, this is essential #} +{% if log %} +{% set log_output = output + " log prefix \"" + log + "\"" %} +{% endif %} + +{% if trns_addr %} +{% set output = output + " " + trns_addr %} +{% endif %} + +{% if trns_port %} +{# Do not add a whitespace here, translation port must be directly added after IP address #} +{# e.g. 192.0.2.10:3389 #} +{% set output = output + trns_port %} +{% endif %} + +{{ log_output if log_output }} +{{ output }} + +{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #} +{% if r.protocol == "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 }} +{{ output | replace("tcp ", "udp ") }} {% endif %} {% endfor %} + + +# +# Source NAT rules build up here +# {% for r in source if not r.disabled -%} {% set chain = "POSTROUTING" %} {% set src_addr = "ip saddr " + r.source_address if r.source_address %} -- cgit v1.2.3 From 6f349ee3b4d3da731ca22a70db6650848a0c28d9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 16:16:09 +0200 Subject: nat: T2198: use Jinja2 macro for common ruleset for SNAT and DNAT By using a Jinja2 macro the same template code can be used to create both source and destination NAT rules with only minor changes introduced by e.g. the used chain (POSTROUTING vs PREROUTING). Used the following configuration for testing on two systems with VyOS 1.2 and the old implementation vs the new one here. set nat destination rule 15 description 'foo-10' set nat destination rule 15 destination address '1.1.1.1' set nat destination rule 15 inbound-interface 'eth0.202' set nat destination rule 15 protocol 'tcp_udp' set nat destination rule 15 translation address '192.0.2.10' set nat destination rule 15 translation port '3389' set nat destination rule 20 description 'foo-20' set nat destination rule 20 destination address '2.2.2.2' set nat destination rule 20 destination port '22' set nat destination rule 20 inbound-interface 'eth0.201' set nat destination rule 20 protocol 'tcp' set nat destination rule 20 translation address '192.0.2.10' 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' set nat source rule 120 outbound-interface 'eth0.202' set nat source rule 120 protocol 'tcp_udp' set nat source rule 120 source address '192.0.3.0/26' set nat source rule 120 translation address '2.2.2.2' --- data/templates/firewall/nftables-nat.tmpl | 138 +++++++++++------------------- 1 file changed, 50 insertions(+), 88 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 5ce110d82..abb32ddc6 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -27,69 +27,76 @@ add rule ip raw NAT_CONNTRACK counter accept {{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK {% endif %} -# -# Destination NAT rules build up here -# -{% 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 %} -{% set trns_port = ":" + r.translation_port if r.translation_port %} -{% set interface = " iifname \"" + r.interface_in + "\"" %} -{% set comment = "DST-NAT-" + r.number %} - -{% if r.protocol == "tcp_udp" %} +{% macro nat_rule(rule, chain) %} +{% set src_addr = "ip saddr " + rule.source_address if rule.source_address %} +{% set src_port = "sport { " + rule.source_port +" }" if rule.source_port %} +{% set dst_addr = "ip daddr " + rule.dest_address if rule.dest_address %} +{% set dst_port = "dport { " + rule.dest_port +" }" if rule.dest_port %} +{% set comment = "DST-NAT-" + rule.number %} + +{% if chain == "PREROUTING" %} +{% set interface = " iifname \"" + rule.interface_in + "\"" %} +{% set trns_addr = "dnat to " + rule.translation_address %} +{% elif chain == "POSTROUTING" %} +{% set interface = " oifname \"" + rule.interface_out + "\"" %} +{% set trns_addr = rule.translation_address %} +{% if rule.translation_address != 'masquerade' %} +{% set trns_addr = "snat to " + trns_addr %} +{% endif %} +{% endif %} +{% set trns_port = ":" + rule.translation_port if rule.translation_port %} + +{% if rule.protocol == "tcp_udp" %} {% set protocol = "tcp" %} {% set comment = comment + " tcp_udp" %} {% else %} -{% set protocol = r.protocol %} +{% set protocol = rule.protocol %} {% endif %} -{% if r.log %} -{% set base_log = "[NAT-DST-" + r.number %} -{% if r.exclude %} +{% if rule.log %} +{% set base_log = "[NAT-DST-" + rule.number %} +{% if rule.exclude %} {% set log = base_log + "-EXCL]" %} -{% elif r.translation_address == 'masquerade' %} +{% elif rule.translation_address == 'masquerade' %} {% set log = base_log + "-MASQ]" %} {% else %} {% set log = base_log + "]" %} {% endif %} {% endif %} -{% if r.exclude %} +{% if rule.exclude %} {# rule has been marked as "exclude" thus we simply return here #} {% set trns_addr = "return" %} {% set trns_port = "" %} {% endif %} -{% set output = "add rule ip nat " + chain + interface + " counter" %} -{% set output = output + " comment \"" + comment + "\"" %} +{% set output = "add rule ip nat " + chain + interface %} + +{% if protocol != "all" %} +{% set output = output + " ip protocol " + protocol %} +{% endif %} {% if src_addr %} {% set output = output + " " + src_addr %} {% endif %} - {% if src_port %} -{% set output = output + " " + src_port %} +{% set output = output + " " + protocol + " " + src_port %} {% endif %} {% if dst_addr %} {% set output = output + " " + dst_addr %} {% endif %} - {% if dst_port %} {% set output = output + " " + protocol + " " + dst_port %} -{% else %} -{% set output = output + " ip protocol " + protocol %} {% endif %} +{# 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 %} -{% set log_output = output + " log prefix \"" + log + "\"" %} +{% set log_output = output + " log prefix \"" + log + "\" comment \"" + comment + "\"" %} {% endif %} {% if trns_addr %} @@ -102,76 +109,31 @@ add rule ip raw NAT_CONNTRACK counter accept {% set output = output + trns_port %} {% endif %} +{% if comment %} +{% set output = output + " comment \"" + comment + "\"" %} +{% endif %} + {{ log_output if log_output }} {{ output }} {# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #} -{% if r.protocol == "tcp_udp" %} +{% if rule.protocol == "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 }} {{ output | replace("tcp ", "udp ") }} {% endif %} -{% endfor %} - - +{% endmacro %} +# +# Destination NAT rules build up here +# +{% for rule in destination if not rule.disabled -%} +{{ nat_rule(rule, 'PREROUTING') }} +{% endfor %} # # Source NAT rules build up here # -{% 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 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 %} - -{% if r.log %} -{% if r.exclude %} -{% set log = "[" + comment + "-EXCL]" %} -{% elif r.translation_address == 'masquerade' %} -{% set log = "[" + comment + "-MASQ]" %} -{% else %} -{% set log = "[" + comment + "]" %} -{% endif %} -{% endif %} - -{% if r.exclude %} -{# rule has been marked as "exclude" thus we simply return here #} -{% 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 %} - -{% 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_src_port }} {{ src_port }} {{ tcp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" -{% endif %} -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_src_port }} {{ src_port }} {{ udp_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" -{% endif %} -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 }}" {{ src_addr }} {{ proto_src_port }} {{ proto_dst_port }} {{ dst_addr }} counter log prefix "{{ log }}" comment "{{ comment }}" -{% endif %} -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 %} +{% for rule in source if not rule.disabled -%} +{{ nat_rule(rule, 'POSTROUTING') }} {% endfor %} -- cgit v1.2.3 From 2a0d1e77e650bd3e8cdff29ac62a3b23c41c85af Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 17:21:21 +0200 Subject: nat: T2198: add common ip-protocol validator It allows IP protocol numbers 0-255, protocol names e.g. tcp, ip, ipv6 and the negated form with a leading "!". --- interface-definitions/include/nat-rule.xml.i | 2 +- src/validators/ip-protocol | 41 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100755 src/validators/ip-protocol diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index b52eb86c3..f62a08987 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -286,7 +286,7 @@ IP protocol number - !?(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc|[01]?[0-9][0-9]?) + diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol new file mode 100755 index 000000000..078f8e319 --- /dev/null +++ b/src/validators/ip-protocol @@ -0,0 +1,41 @@ +#!/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 . + +import re +from sys import argv,exit + +if __name__ == '__main__': + if len(argv) != 2: + exit(1) + + input = argv[1] + try: + # IP protocol can be in the range 0 - 255, thus the range must end with 256 + if int(input) in range(0, 256): + exit(0) + except ValueError: + pass + + pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \ + "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \ + "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \ + "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \ + "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \ + "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b" + if re.match(pattern, input): + exit(0) + + exit(1) -- cgit v1.2.3 From d8891acde69ba17019b650656eaa6fa313a222b9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 17:52:07 +0200 Subject: Debian: add required dependency on systemd --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index f7285941a..bd6e445ee 100644 --- a/debian/control +++ b/debian/control @@ -34,6 +34,7 @@ Depends: python3, python3-zmq, python3-jmespath, cron, + systemd, easy-rsa, ipaddrcheck, tcpdump, -- cgit v1.2.3 From 2ba8c8499f8664b69bb9b48268f7c5cd5b06cd89 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 16 May 2020 18:32:22 +0200 Subject: nat: T2198: remove "tcp_udp" from "show nat dest stat"x --- src/op_mode/show_nat_statistics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py index b77693e19..0b53112f2 100755 --- a/src/op_mode/show_nat_statistics.py +++ b/src/op_mode/show_nat_statistics.py @@ -31,7 +31,7 @@ rule pkts bytes interface {%- set bytes = r.counter.bytes -%} {%- set interface = r.interface -%} {# remove rule comment prefix #} -{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') -%} +{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%} {{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} {%- endif %} {% endfor %} -- cgit v1.2.3