From f60a2c7e1f874c405ace6692c3d604a1b2cedc51 Mon Sep 17 00:00:00 2001 From: Ruakij Date: Tue, 3 Nov 2026 21:58:27 +0000 Subject: [PATCH] host_firewall: pass LAPI client flows from wt0 to the pod port with their own address --- host_firewall/defaults/main.yml | 5 +++++ host_firewall/templates/host.nft.j2 | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/host_firewall/defaults/main.yml b/host_firewall/defaults/main.yml index b702b97..5ec50df 100644 --- a/host_firewall/defaults/main.yml +++ b/host_firewall/defaults/main.yml @@ -34,6 +34,11 @@ firewall: ifaces: {} v4: {} v6: {} + # Hosts outside the ring that reach a NodePort service (CrowdSec LAPI) over wt0, and the port of its pods: Cilium + # translates the NodePort before netfilter, so the flows arrive as client -> pod:port. + lapi: + port: null + clients: {} # 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: {} diff --git a/host_firewall/templates/host.nft.j2 b/host_firewall/templates/host.nft.j2 index 04ba25e..3614e44 100644 --- a/host_firewall/templates/host.nft.j2 +++ b/host_firewall/templates/host.nft.j2 @@ -30,6 +30,8 @@ set {{ name }} { {% 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 %} +{% set lapi = cluster.lapi | default({}, true) %} +{% set lapi_rule = list(lapi.clients) and lapi.port %} {% set scrapers = firewall.scrapers | default({}, true) %} {% set forward_guard = firewall.forward_guard | default({}, true) | dict2items | rejectattr('value', 'none') | list %} {% set blocklists = firewall.spamhaus or firewall.firehol %} @@ -91,6 +93,9 @@ table inet host { {{ addr_set('admin_v6', 'ipv6_addr', firewall.admin.v6) }} {{ addr_set('cluster_v4', 'ipv4_addr', cluster.v4) }} {{ addr_set('cluster_v6', 'ipv6_addr', cluster.v6) }} +{% if lapi_rule %} + {{ addr_set('lapi_clients_v4', 'ipv4_addr', lapi.clients) }} +{% endif %} {{ addr_set('scrapers_v4', 'ipv4_addr', scrapers.v4) }} {{ addr_set('scrapers_v6', 'ipv6_addr', scrapers.v6) }} {{ addr_list_set('rl_exempt_v4', 'ipv4_addr', exempt_v4) }} @@ -132,10 +137,15 @@ table inet host { # the host stack (veth with legacy host routing). This chain runs right after. Flows from the cluster ring (pods, # node NetBird addresses) into the pod CIDR arriving on wt0 get mark 0x1bd20: not masqueraded, and accepted by the # NetBird forward filter, which drops forwarded wt0 traffic that no route group allows. Other pod to pod flows - # lose their marks. + # lose their marks. The same mark lets the LAPI clients (@lapi_clients_v4) reach the pod port of the NodePort service + # with their own address: Cilium has translated the NodePort before this point, so NetBird would masquerade the flow + # to the cilium_host address and the network policy would see world. chain pod_identity { type filter hook prerouting priority -140; policy accept; iifname "wt0" ct state new ip saddr @cluster_v4 ip daddr {{ cluster.v4.pods }} meta mark set 0x0001bd20 +{% if lapi_rule %} + iifname "wt0" ct state new ip saddr @lapi_clients_v4 ip daddr {{ cluster.v4.pods }} tcp dport {{ lapi.port }} meta mark set 0x0001bd20 +{% endif %} ct state new ip saddr {{ cluster.v4.pods }} ip daddr {{ cluster.v4.pods }} meta mark 0x0001bd21 meta mark set 0 ct state new ip saddr {{ cluster.v4.pods }} ip daddr {{ cluster.v4.pods }} meta mark 0x0001bd22 meta mark set 0 }