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 @@ -1,43 +1,54 @@ #!/usr/sbin/nft -f # 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 {% 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/debian/control b/debian/control index 2aaca13ba..609f46e4d 100644 --- a/debian/control +++ b/debian/control @@ -1,111 +1,111 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers Build-Depends: debhelper (>= 9), quilt, python3, python3-setuptools, quilt, python3-lxml, python3-nose, python3-coverage, whois, fakeroot, libvyosconfig0 (>= 0.0.7) Standards-Version: 3.9.6 Package: vyos-1x Architecture: all Depends: python3, ${python3:Depends}, python3-netifaces, python3-jinja2, python3-pystache, python3-psutil, python3-tabulate, python3-six, python3-isc-dhcp-leases, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-flask, python3-waitress, python3-netaddr, python3-zmq, cron, easy-rsa, ipaddrcheck, tcpdump, tshark, isc-dhcp-client, bmon, hvinfo, file, lsscsi, pciutils, usbutils, procps, snmp, snmpd, openssh-server, ntp, ntpdate, iputils-arping, libvyosconfig0, beep, isc-dhcp-server, isc-dhcp-relay, keepalived (>=2.0.5), wireguard, tftpd-hpa, igmpproxy, accel-ppp, mdns-repeater, udp-broadcast-relay, pdns-recursor, lcdproc, lcdproc-extra-drivers, openvpn, openvpn-auth-ldap, openvpn-auth-radius, libpam-radius-auth (>= 1.5.0), mtr-tiny, telnet, traceroute, vyos-qat-kernel-modules, vyos-qat-utilities, ssl-cert, nginx-light, lldpd, iperf, iperf3, frr, radvd, dbus, usb-modeswitch, hostapd (>= 0.6.8), wpasupplicant (>= 0.6.7), iw, crda, wireless-regdb, pmacct (>= 1.6.0), python3-certbot-nginx, pppoe, salt-minion, vyos-utils, - nftables, + nftables (>= 0.9.3), conntrack, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware Architecture: amd64 i386 Depends: ${misc:Depends}, ${shlibs:Depends}, vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. 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 @@ -1,159 +1,198 @@ #!/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 json import os from copy import deepcopy 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 = [] 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() if not conf.exists(['nat']): return None 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 :-) for tgt in ['source', 'destination']: nat[tgt] = parse_source_destination(conf, tgt) return nat 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(): print(f'NAT configuration warning: interface {interface} does not exist on this system') 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 call(f'{iptables_nat_config}') return None if __name__ == '__main__': try: _check_kmod() c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)