summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/firewall/nftables-static-nat.j2115
-rw-r--r--data/templates/firewall/nftables.j220
-rw-r--r--interface-definitions/include/inbound-interface.xml.i11
-rw-r--r--interface-definitions/include/ipv4-address-prefix.xml.i19
-rw-r--r--interface-definitions/nat.xml.in53
-rwxr-xr-xsrc/conf_mode/nat.py18
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