Add IPv4 port forwards and masquerade to host_firewall in an ip host_nat table

This commit is contained in:
2026-10-07 21:10:35 +00:00
parent c9d2334906
commit 9e2e6cbb99
3 changed files with 31 additions and 2 deletions
+5
View File
@@ -26,6 +26,11 @@ firewall:
ifaces: {} ifaces: {}
v4: {} v4: {}
v6: {} v6: {}
# IPv4 port forwards: name -> {daddr, to, tcp: ports, udp: ports}, ports as in
# the maps above. Forwarded traffic passes the forward hook, not input.
dnat: {}
# IPv4 source NAT: name -> nft match, e.g. 'oifname "vmbr0"'.
masquerade: {}
ssh_rate_limit: "10/minute burst 5 packets" ssh_rate_limit: "10/minute burst 5 packets"
# EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo # EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo
# ports to the admin set; a load peaks at about 60 MB of kernel memory, too much for a 1 GB host. # ports to the admin set; a load peaks at about 60 MB of kernel memory, too much for a 1 GB host.
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Host input firewall (nftables table inet host) Description=Host input firewall and NAT (nftables tables inet host and ip host_nat)
# Same early-boot placement as nftables.service. Its "flush ruleset" on start, # Same early-boot placement as nftables.service. Its "flush ruleset" on start,
# reload and stop would wipe this table, so load after it and follow all three. # reload and stop would wipe this table, so load after it and follow all three.
DefaultDependencies=no DefaultDependencies=no
+25 -1
View File
@@ -16,11 +16,15 @@ set {{ name }} {
{% set internal_ifaces = firewall.internal_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %} {% set internal_ifaces = firewall.internal_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
{% set guest_ifaces = firewall.guest_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %} {% set guest_ifaces = firewall.guest_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
{% set guest_match = 'iifname { "' ~ (guest_ifaces | join('", "')) ~ '" }' %} {% set guest_match = 'iifname { "' ~ (guest_ifaces | join('", "')) ~ '" }' %}
{% set dnat = firewall.dnat | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | list %}
{% set masquerade = firewall.masquerade | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
# Declare-then-delete makes every load one atomic transaction that replaces # Declare-then-delete makes every load one atomic transaction that replaces
# only this table; Cilium, Docker, NetBird and CrowdSec tables stay untouched. # only these tables; Cilium, Docker, NetBird and CrowdSec tables stay untouched.
table inet host {} table inet host {}
delete table inet host delete table inet host
table ip host_nat {}
delete table ip host_nat
{% if firewall.geo_countries %} {% if firewall.geo_countries %}
include "/etc/nftables.d/host-geo.nft" include "/etc/nftables.d/host-geo.nft"
@@ -116,3 +120,23 @@ table inet host {
{% endif %} {% endif %}
} }
} }
{% if dnat or masquerade %}
table ip host_nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
{% for fwd in dnat %}
{% for proto in ['tcp', 'udp'] if list(fwd[proto]) %}
ip daddr {{ fwd.daddr }} {{ proto }} dport { {{ list(fwd[proto]) }} } dnat to {{ fwd.to }}
{% endfor %}
{% endfor %}
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
{% for match in masquerade %}
{{ match }} masquerade
{% endfor %}
}
}
{% endif %}