diff options
-rw-r--r-- | data/templates/firewall/nftables-static-nat.j2 | 115 | ||||
-rw-r--r-- | data/templates/firewall/nftables.j2 | 20 | ||||
-rw-r--r-- | interface-definitions/include/inbound-interface.xml.i | 11 | ||||
-rw-r--r-- | interface-definitions/include/ipv4-address-prefix.xml.i | 19 | ||||
-rw-r--r-- | interface-definitions/nat.xml.in | 53 | ||||
-rwxr-xr-x | src/conf_mode/nat.py | 18 |
6 files changed, 226 insertions, 10 deletions
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 @@ +<!-- include start from inbound-interface.xml.i --> +<leafNode name="inbound-interface"> + <properties> + <help>Inbound interface of NAT traffic</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> 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 @@ +<!-- include start from ipv4-address-prefix.xml.i --> +<leafNode name="address"> + <properties> + <help>IP address, prefix</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> 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 <include/nat-rule.xml.i> <tagNode name="rule"> <children> - <leafNode name="inbound-interface"> - <properties> - <help>Inbound interface of NAT traffic</help> - <completionHelp> - <list>any</list> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/inbound-interface.xml.i> <node name="translation"> <properties> <help>Inside NAT IP (destination NAT only)</help> @@ -65,6 +57,17 @@ <children> #include <include/nat-rule.xml.i> <tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of NAT rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> + </properties> <children> #include <include/nat-interface.xml.i> <node name="translation"> @@ -110,6 +113,38 @@ </tagNode> </children> </node> + <node name="static"> + <properties> + <help>Static NAT (one-to-one)</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + </properties> + <children> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>NAT destination parameters</help> + </properties> + <children> + #include <include/ipv4-address-prefix.xml.i> + </children> + </node> + #include <include/inbound-interface.xml.i> + <node name="translation"> + <properties> + <help>Translation address or prefix</help> + </properties> + <children> + #include <include/ipv4-address-prefix.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index a72e82a83..e75418ba5 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 = '/run/nftables_nat.conf' +nftables_static_nat_conf = '/run/nftables_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,20 +179,35 @@ 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}') if tmp > 0: 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}') return None |