From 69bcdb9a680b33422d041fd03e70c25094bfa6a2 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Sat, 13 Aug 2022 00:07:41 +0000 Subject: nat: T538: Add static NAT one-to-one Ability to set static NAT (one-to-one) in one rule set nat static rule 10 destination address '203.0.113.0/24' set nat static rule 10 inbound-interface 'eth0' set nat static rule 10 translation address '192.0.2.0/24' It will be enough for PREROUTING and POSTROUTING rules Use a separate table 'vyos_static_nat' as SRC/DST rules and STATIC rules can have the same rule number --- data/templates/firewall/nftables-static-nat.j2 | 115 +++++++++++++++++++++ data/templates/firewall/nftables.j2 | 20 ++++ .../include/inbound-interface.xml.i | 11 ++ .../include/ipv4-address-prefix.xml.i | 19 ++++ interface-definitions/nat.xml.in | 53 ++++++++-- src/conf_mode/nat.py | 18 +++- 6 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 data/templates/firewall/nftables-static-nat.j2 create mode 100644 interface-definitions/include/inbound-interface.xml.i create mode 100644 interface-definitions/include/ipv4-address-prefix.xml.i diff --git a/data/templates/firewall/nftables-static-nat.j2 b/data/templates/firewall/nftables-static-nat.j2 new file mode 100644 index 000000000..d3c43858f --- /dev/null +++ b/data/templates/firewall/nftables-static-nat.j2 @@ -0,0 +1,115 @@ +#!/usr/sbin/nft -f + +{% macro nat_rule(rule, config, chain) %} +{% set comment = '' %} +{% set base_log = '' %} + +{% if chain is vyos_defined('PREROUTING') %} +{% set comment = 'STATIC-NAT-' ~ rule %} +{% set base_log = '[NAT-DST-' ~ rule %} +{% set interface = ' iifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{% if config.translation.address is vyos_defined %} +{# support 1:1 network translation #} +{% if config.translation.address | is_ip_network %} +{% set trns_addr = 'dnat ip prefix to ip daddr map { ' ~ config.destination.address ~ ' : ' ~ config.translation.address ~ ' }' %} +{# we can now clear out the dst_addr part as it's already covered in aboves map #} +{% else %} +{% set dst_addr = 'ip daddr ' ~ config.destination.address if config.destination.address is vyos_defined %} +{% set trns_addr = 'dnat to ' ~ config.translation.address %} +{% endif %} +{% endif %} +{% elif chain is vyos_defined('POSTROUTING') %} +{% set comment = 'STATIC-NAT-' ~ rule %} +{% set base_log = '[NAT-SRC-' ~ rule %} +{% set interface = ' oifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{% if config.translation.address is vyos_defined %} +{# support 1:1 network translation #} +{% if config.translation.address | is_ip_network %} +{% set trns_addr = 'snat ip prefix to ip saddr map { ' ~ config.translation.address ~ ' : ' ~ config.destination.address ~ ' }' %} +{# we can now clear out the src_addr part as it's already covered in aboves map #} +{% else %} +{% set src_addr = 'ip saddr ' ~ config.translation.address if config.translation.address is vyos_defined %} +{% set trns_addr = 'snat to ' ~ config.destination.address %} +{% endif %} +{% endif %} +{% endif %} + +{% if config.exclude is vyos_defined %} +{# rule has been marked as 'exclude' thus we simply return here #} +{% set trns_addr = 'return' %} +{% set trns_port = '' %} +{% endif %} + +{% if config.translation.options is vyos_defined %} +{% if config.translation.options.address_mapping is vyos_defined('persistent') %} +{% set trns_opts_addr = 'persistent' %} +{% endif %} +{% if config.translation.options.port_mapping is vyos_defined('random') %} +{% set trns_opts_port = 'random' %} +{% elif config.translation.options.port_mapping is vyos_defined('fully-random') %} +{% set trns_opts_port = 'fully-random' %} +{% endif %} +{% endif %} + +{% if trns_opts_addr is vyos_defined and trns_opts_port is vyos_defined %} +{% set trns_opts = trns_opts_addr ~ ',' ~ trns_opts_port %} +{% elif trns_opts_addr is vyos_defined %} +{% set trns_opts = trns_opts_addr %} +{% elif trns_opts_port is vyos_defined %} +{% set trns_opts = trns_opts_port %} +{% endif %} + +{% set output = 'add rule ip vyos_static_nat ' ~ chain ~ interface %} + +{% if dst_addr is vyos_defined %} +{% set output = output ~ ' ' ~ dst_addr %} +{% endif %} +{% if src_addr is vyos_defined %} +{% set output = output ~ ' ' ~ src_addr %} +{% 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 is vyos_defined %} +{% set log_output = output ~ ' log prefix "' ~ log ~ '" comment "' ~ comment ~ '"' %} +{% endif %} +{% if trns_addr is vyos_defined %} +{% set output = output ~ ' ' ~ trns_addr %} +{% endif %} + +{% if trns_opts is vyos_defined %} +{% set output = output ~ ' ' ~ trns_opts %} +{% endif %} +{% if comment is vyos_defined %} +{% set output = output ~ ' comment "' ~ comment ~ '"' %} +{% endif %} +{{ log_output if log_output is vyos_defined }} +{{ output }} +{% endmacro %} + +# Start with clean STATIC NAT chains +flush chain ip vyos_static_nat PREROUTING +flush chain ip vyos_static_nat POSTROUTING + +{# NAT if enabled - add targets to nftables #} + +# +# Destination NAT rules build up here +# +add rule ip vyos_static_nat PREROUTING counter jump VYOS_PRE_DNAT_HOOK +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'PREROUTING') }} +{% endfor %} +{% endif %} +# +# Source NAT rules build up here +# +add rule ip vyos_static_nat POSTROUTING counter jump VYOS_PRE_SNAT_HOOK +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'POSTROUTING') }} +{% endfor %} +{% endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index b91fed615..5971e1bbc 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -181,6 +181,26 @@ table ip nat { } } +table ip vyos_static_nat { + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + counter jump VYOS_PRE_DNAT_HOOK + } + + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + counter jump VYOS_PRE_SNAT_HOOK + } + + chain VYOS_PRE_DNAT_HOOK { + return + } + + chain VYOS_PRE_SNAT_HOOK { + return + } +} + table ip6 nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; diff --git a/interface-definitions/include/inbound-interface.xml.i b/interface-definitions/include/inbound-interface.xml.i new file mode 100644 index 000000000..3289bbf8f --- /dev/null +++ b/interface-definitions/include/inbound-interface.xml.i @@ -0,0 +1,11 @@ + + + + Inbound interface of NAT traffic + + any + + + + + diff --git a/interface-definitions/include/ipv4-address-prefix.xml.i b/interface-definitions/include/ipv4-address-prefix.xml.i new file mode 100644 index 000000000..f5be6f1fe --- /dev/null +++ b/interface-definitions/include/ipv4-address-prefix.xml.i @@ -0,0 +1,19 @@ + + + + IP address, prefix + + ipv4 + IPv4 address to match + + + ipv4net + IPv4 prefix to match + + + + + + + + diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 9295b631f..501ff05d3 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -14,15 +14,7 @@ #include - - - Inbound interface of NAT traffic - - any - - - - + #include Inside NAT IP (destination NAT only) @@ -65,6 +57,17 @@ #include + + Rule number for NAT + + u32:1-999999 + Number of NAT rule + + + + + NAT rule number must be between 1 and 999999 + #include @@ -110,6 +113,38 @@ + + + Static NAT (one-to-one) + + + + + Rule number for NAT + + + #include + + + NAT destination parameters + + + #include + + + #include + + + Translation address or prefix + + + #include + + + + + + diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 85819a77e..b76ea9f9e 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -45,6 +45,7 @@ else: k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] nftables_nat_config = '/tmp/vyos-nat-rules.nft' +nftables_static_nat_conf = '/tmp/vyos-static-nat-rules.nft' def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. @@ -88,7 +89,7 @@ def get_config(config=None): # T2665: we must add the tagNode defaults individually until this is # moved to the base class - for direction in ['source', 'destination']: + for direction in ['source', 'destination', 'static']: if direction in nat: default_values = defaults(base + [direction, 'rule']) for rule in dict_search(f'{direction}.rule', nat) or []: @@ -178,10 +179,22 @@ def verify(nat): # common rule verification verify_rule(config, err_msg) + if dict_search('static.rule', nat): + for rule, config in dict_search('static.rule', nat).items(): + err_msg = f'Static NAT configuration error in rule {rule}:' + + if 'inbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'inbound-interface not specified') + + # common rule verification + verify_rule(config, err_msg) + return None def generate(nat): render(nftables_nat_config, 'firewall/nftables-nat.j2', nat) + render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat) # dry-run newly generated configuration tmp = run(f'nft -c -f {nftables_nat_config}') @@ -190,10 +203,13 @@ def generate(nat): os.unlink(nftables_nat_config) raise ConfigError('Configuration file errors encountered!') + tmp = run(f'nft -c -f {nftables_nat_config}') + return None def apply(nat): cmd(f'nft -f {nftables_nat_config}') + cmd(f'nft -f {nftables_static_nat_conf}') if os.path.isfile(nftables_nat_config): os.unlink(nftables_nat_config) -- cgit v1.2.3