From 483f3890e53e51c2f5ca9b9c697753a1494d4533 Mon Sep 17 00:00:00 2001 From: Ruakij Date: Fri, 6 Nov 2026 08:48:29 +0000 Subject: [PATCH] host_firewall: uplinks drop new forwarded connections that were not DNATed, replaces no_hairpin_ifaces --- host_firewall/defaults/main.yml | 9 +++++---- host_firewall/templates/host.nft.j2 | 11 ++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/host_firewall/defaults/main.yml b/host_firewall/defaults/main.yml index 1423519..052d1d9 100644 --- a/host_firewall/defaults/main.yml +++ b/host_firewall/defaults/main.yml @@ -116,10 +116,11 @@ firewall: # of any host, and ones holding an address of the host are left out # (/etc/nft-host-scanners.allowlist). Refreshed daily by nft-host-scanners. scanners: true - # Bridges and uplinks whose forwarded traffic may not leave through the interface it came in on (iifname X oifname X - # drop). The datacenter gateway routes packets for foreign addresses of the subnet to the host; with ip_forward they - # would be forwarded back out and masqueraded with the host address. Not for a bridge that carries guests. - no_hairpin_ifaces: {} + # Uplink interfaces (name -> interface). New forwarded connections that arrive on one are dropped unless they were + # DNATed here or a forward_guard zone admits the destination. The datacenter gateway routes packets for foreign + # addresses of the subnet to the host; with ip_forward they would be forwarded and masqueraded with the host address. + # Not for hosts whose forwarded inbound traffic is not DNATed in netfilter (Cilium NodePorts are DNATed in BPF). + uplinks: {} # Reject outbound TCP 25; the mail group turns it off. block_smtp_out: true # EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo diff --git a/host_firewall/templates/host.nft.j2 b/host_firewall/templates/host.nft.j2 index 0c0d945..c7486d1 100644 --- a/host_firewall/templates/host.nft.j2 +++ b/host_firewall/templates/host.nft.j2 @@ -441,14 +441,10 @@ table inet host { ip6 saddr @geo_v6 accept {% endif %} } -{% if forward_guard or blocklists or firewall.scanners or rl_forward or firewall.block_smtp_out or firewall.no_hairpin_ifaces %} +{% if forward_guard or blocklists or firewall.scanners or rl_forward or firewall.block_smtp_out or firewall.uplinks %} chain forward { type filter hook forward priority filter; policy accept; -{% for name, iface in firewall.no_hairpin_ifaces.items() %} - - iifname "{{ iface }}" oifname "{{ iface }}" counter drop comment "{{ name }}: no forwarding back out of the uplink" -{% endfor %} {% if rl_forward %} # Docker-published ports and port forwards arrive here after DNAT; pod and guest egress is not DNATed. @@ -496,6 +492,11 @@ table inet host { {{ match }} jump drop_log {% endfor %} {% endfor %} +{% for name, iface in firewall.uplinks.items() if iface is not none %} + + # {{ name }}: from outside only what was DNATed here or admitted by a forward_guard zone above. + iifname "{{ iface }}" ct state new ct status & dnat == 0 counter drop +{% endfor %} {% if firewall.block_smtp_out %} # Pod, container and guest egress to tcp/25 is rejected like the host's own (chain output). Connections DNATed to