From 9207897983a3bfafa0ec3e436c1ad67790f09f06 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 19 Jan 2021 21:01:20 +0100 Subject: nat: T2947: add many-many translation Support a 1:1 or 1:n prefix translation. The following configuration will NAT source addresses from the 10.2.0.0/16 range to an address from 192.0.2.0/29. For this feature to work a Linux Kernel 5.8 or higher is required! vyos@vyos# show nat source { rule 100 { outbound-interface eth1 source { address 10.2.0.0/16 } translation { address 192.0.2.0/29 } } } This results in the nftables configuration: chain POSTROUTING { type nat hook postrouting priority srcnat; policy accept; oifname "eth1" counter packets 0 bytes 0 snat ip prefix to ip saddr map { 10.2.0.0/16 : 192.0.2.0/29 } comment "SRC-NAT-100" } --- data/templates/firewall/nftables-nat.tmpl | 31 ++++++++++++++++------ python/vyos/template.py | 43 ++++++++++++++++++++++++++++++- src/conf_mode/nat.py | 9 ++++--- src/tests/test_template.py | 19 ++++++++++++++ 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 770a24a95..5480447f2 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -21,18 +21,34 @@ {% set comment = 'DST-NAT-' + rule %} {% set base_log = '[NAT-DST-' + rule %} {% set interface = ' iifname "' + config.inbound_interface + '"' if config.inbound_interface is defined and config.inbound_interface != 'any' else '' %} -{% set trns_addr = 'dnat to ' + config.translation.address if config.translation is defined and config.translation.address is defined and config.translation.address is not none %} +{% if config.translation is defined and config.translation.address is defined and config.translation.address is not none %} +{# support 1:1 network translation #} +{% if config.translation.address | is_ip_network %} +{% set trns_addr = 'dnat ip prefix to ip daddr map { ' + config.source.address + ' : ' + config.translation.address + ' }' %} +{# we can now clear out the src_addr part as it's already covered in aboves map #} +{% set src_addr = '' %} +{% else %} +{% set trns_addr = 'dnat to ' + config.translation.address %} +{% endif %} +{% endif %} {% elif chain == 'POSTROUTING' %} {% set comment = 'SRC-NAT-' + rule %} {% set base_log = '[NAT-SRC-' + rule %} {% set interface = ' oifname "' + config.outbound_interface + '"' if config.outbound_interface is defined and config.outbound_interface != 'any' else '' %} -{% if config.translation is defined and config.translation.address is defined and config.translation.address == 'masquerade' %} -{% set trns_addr = config.translation.address %} -{% if config.translation.port is defined and config.translation.port is not none %} -{% set trns_addr = trns_addr + ' to ' %} +{% if config.translation is defined and config.translation.address is defined and config.translation.address is not none %} +{% if config.translation.address == 'masquerade' %} +{% set trns_addr = config.translation.address %} +{% if config.translation.port is defined and config.translation.port is not none %} +{% set trns_addr = trns_addr + ' to ' %} +{% endif %} +{# support 1:1 network translation #} +{% elif config.translation.address | is_ip_network %} +{% set trns_addr = 'snat ip prefix to ip saddr map { ' + config.source.address + ' : ' + config.translation.address + ' }' %} +{# we can now clear out the src_addr part as it's already covered in aboves map #} +{% set src_addr = '' %} +{% else %} +{% set trns_addr = 'snat to ' + config.translation.address %} {% endif %} -{% else %} -{% set trns_addr = 'snat to ' + config.translation.address if config.translation is defined and config.translation.address is defined and config.translation.address is not none %} {% endif %} {% endif %} {% set trns_port = ':' + config.translation.port if config.translation is defined and config.translation.port is defined and config.translation.port is not none %} @@ -132,7 +148,6 @@ add rule ip raw NAT_CONNTRACK counter accept {{ nat_rule(rule, config, 'PREROUTING') }} {% endfor %} {% endif %} - # # Source NAT rules build up here # diff --git a/python/vyos/template.py b/python/vyos/template.py index bf087c223..527384d0b 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -149,7 +149,9 @@ def netmask_from_ipv4(address): Example: - 172.18.201.10 -> 255.255.255.128 """ - from netifaces import interfaces, ifaddresses, AF_INET + from netifaces import interfaces + from netifaces import ifaddresses + from netifaces import AF_INET for interface in interfaces(): tmp = ifaddresses(interface) if AF_INET in tmp: @@ -160,6 +162,30 @@ def netmask_from_ipv4(address): raise ValueError +@register_filter('is_ip_network') +def is_ip_network(addr): + """ Take IP(v4/v6) address and validate if the passed argument is a network + or a host address. + + Example: + - 192.0.2.0 -> False + - 192.0.2.10/24 -> False + - 192.0.2.0/24 -> True + - 2001:db8:: -> False + - 2001:db8::100 -> False + - 2001:db8::/48 -> True + - 2001:db8:1000::/64 -> True + """ + try: + from ipaddress import ip_network + # input variables must contain a / to indicate its CIDR notation + if len(addr.split('/')) != 2: + raise ValueError() + ip_network(addr) + return True + except: + return False + @register_filter('network_from_ipv4') def network_from_ipv4(address): """ Take IP address and search all attached interface IP addresses for the @@ -248,6 +274,21 @@ def dec_ip(address, decrement): from ipaddress import ip_interface return str(ip_interface(address).ip - int(decrement)) +@register_filter('compare_netmask') +def compare_netmask(netmask1, netmask2): + """ + Compare two IP netmask if they have the exact same size. + + compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True + compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False + """ + from ipaddress import ip_network + try: + return ip_network(netmask1).netmask == ip_network(netmask2).netmask + except: + return False + + @register_filter('isc_static_route') def isc_static_route(subnet, router): # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 2d98cb11b..dae958774 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -26,6 +26,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render +from vyos.template import is_ip_network from vyos.util import cmd from vyos.util import check_kmod from vyos.util import dict_search @@ -68,9 +69,9 @@ def verify_rule(config, err_msg): 'ports can only be specified when protocol is '\ 'either tcp, udp or tcp_udp!') - if '/' in (dict_search('translation.address', config) or []): + if is_ip_network(dict_search('translation.address', config)): raise ConfigError(f'{err_msg}\n' \ - 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'Cannot use ports with an IPv4 network as translation address as it\n' \ 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') @@ -147,7 +148,7 @@ def verify(nat): addr = dict_search('translation.address', config) if addr != None: - if addr != 'masquerade': + if addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): if not is_addr_assigned(ip): print(f'WARNING: IP address {ip} does not exist on the system!') diff --git a/src/tests/test_template.py b/src/tests/test_template.py index 544755692..7800d007f 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -93,3 +93,22 @@ class TestVyOSTemplate(TestCase): self.assertEqual(vyos.template.dec_ip('2001:db8::b/64', '10'), '2001:db8::1') self.assertEqual(vyos.template.dec_ip('2001:db8::f', '5'), '2001:db8::a') + def test_is_network(self): + self.assertFalse(vyos.template.is_ip_network('192.0.2.0')) + self.assertFalse(vyos.template.is_ip_network('192.0.2.1/24')) + self.assertTrue(vyos.template.is_ip_network('192.0.2.0/24')) + + self.assertFalse(vyos.template.is_ip_network('2001:db8::')) + self.assertFalse(vyos.template.is_ip_network('2001:db8::ffff')) + self.assertTrue(vyos.template.is_ip_network('2001:db8::/48')) + self.assertTrue(vyos.template.is_ip_network('2001:db8:1000::/64')) + + def test_is_network(self): + self.assertTrue(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/8')) + self.assertTrue(vyos.template.compare_netmask('10.0.0.0/16', '20.0.0.0/16')) + self.assertFalse(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/16')) + self.assertFalse(vyos.template.compare_netmask('10.0.0.1', '20.0.0.0/16')) + + self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/48')) + self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/64', '2001:db8:2000::/64')) + self.assertFalse(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/64')) -- cgit v1.2.3