Emit nft-decodable family guards #40

Merged
benvin merged 5 commits from benvin/nft-decodable into main 2026-10-11 10:19:14 +11:00
Member

Why: nft list cannot decode rules carrying repeated or conflicting meta nfproto guards, and single-family guards on negated lists made DROPs fail open.

  • Expands every address match per family: positive lists one rule per address, negated lists one AND-ed rule per family (bare family when it has no negations)
  • Combines source/dest/zone/origdest/NAT target only within one family; a family with no possible match yields no rule
  • Leaves each inet rule one guard ahead of its first L3 payload; ip/ip6 tables strip guards and keep only their family
  • Fails the compile on conflicting guards instead of dropping the rule
  • Tests per-family output for rules, DNAT, SNAT, static NAT, tunnels, blrules; gated netns test (TOMSWALL_NETNS_TEST=1) lists all three families
Why: `nft list` cannot decode rules carrying repeated or conflicting `meta nfproto` guards, and single-family guards on negated lists made DROPs fail open. - Expands every address match per family: positive lists one rule per address, negated lists one AND-ed rule per family (bare family when it has no negations) - Combines source/dest/zone/origdest/NAT target only within one family; a family with no possible match yields no rule - Leaves each inet rule one guard ahead of its first L3 payload; ip/ip6 tables strip guards and keep only their family - Fails the compile on conflicting guards instead of dropping the rule - Tests per-family output for rules, DNAT, SNAT, static NAT, tunnels, blrules; gated netns test (`TOMSWALL_NETNS_TEST=1`) lists all three families
unkin-agent added 1 commit 2026-10-09 23:39:51 +11:00
Emit one family guard per rule, none in ip/ip6 tables
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
42a4dab6a3
Author
Member
  • internal/nftables/compiler.go:1934 — a negated single-family rule address (!192.0.2.1) is now guarded ip4, so the DROP/REJECT no longer matches IPv6 packets (before, the non-matching payload compare was true for them); Dest: "net:!2001:db8::5" with a v4 zone is dropped outright by the contradiction check (:98) so the rule vanishes → for negated lists emit the guarded compare plus an unguarded other-family variant (as zoneMatchExprs does per family), and do not treat a negated guard as a hard family requirement.
  • internal/nftables/compiler.go:1934 — mixed-family negated lists (!192.0.2.1,2001:db8::1) stay unguarded, so the v4 compare reads bytes 12-15 of an IPv6 header (attacker-chosen source bits) and the v6 compare reads past the IPv4 header; a v6 source with 2001:db8:c000:201:: bits skips the exclusion and the DROP does not fire → split by family into one guarded rule per family instead of leaving the list unguarded.
  • internal/nftables/compiler.go:92 — in ip/ip6 tables the unguarded mixed list keeps other-family payloads (ip6 saddr/ip saddr of the wrong length), contrary to the "other-family dropped" contract; the new TestCompile_FamilyGuardsDecodable fails for ip and ip6 as soon as guardCfg adds Source: "net:!192.0.2.1,2001:db8::1" → fix with the per-family split above and add that rule to guardCfg.
  • internal/nftables/guards_test.go:14 — guardCfg has no SNAT, tunnel, static-NAT, DNAT, blrules or negated rule-level addresses, so the netns/decode test does not exercise the paths this PR changed → add them to guardCfg.
- internal/nftables/compiler.go:1934 — a negated single-family rule address (`!192.0.2.1`) is now guarded `ip4`, so the DROP/REJECT no longer matches IPv6 packets (before, the non-matching payload compare was true for them); `Dest: "net:!2001:db8::5"` with a v4 zone is dropped outright by the contradiction check (:98) so the rule vanishes → for negated lists emit the guarded compare plus an unguarded other-family variant (as zoneMatchExprs does per family), and do not treat a negated guard as a hard family requirement. - internal/nftables/compiler.go:1934 — mixed-family negated lists (`!192.0.2.1,2001:db8::1`) stay unguarded, so the v4 compare reads bytes 12-15 of an IPv6 header (attacker-chosen source bits) and the v6 compare reads past the IPv4 header; a v6 source with 2001:db8:c000:201:: bits skips the exclusion and the DROP does not fire → split by family into one guarded rule per family instead of leaving the list unguarded. - internal/nftables/compiler.go:92 — in ip/ip6 tables the unguarded mixed list keeps other-family payloads (`ip6 saddr`/`ip saddr` of the wrong length), contrary to the "other-family dropped" contract; the new TestCompile_FamilyGuardsDecodable fails for ip and ip6 as soon as guardCfg adds `Source: "net:!192.0.2.1,2001:db8::1"` → fix with the per-family split above and add that rule to guardCfg. - internal/nftables/guards_test.go:14 — guardCfg has no SNAT, tunnel, static-NAT, DNAT, blrules or negated rule-level addresses, so the netns/decode test does not exercise the paths this PR changed → add them to guardCfg.
unkin-agent added 2 commits 2026-10-09 23:49:14 +11:00
Cover per-family expansion across rules, NAT, tunnels and blrules
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
bc65312647
Author
Member
  • internal/nftables/compiler.go:685 (specCount, used by the guard at :505) — counts via splitAddrs, which now returns 2 alternatives for any negated list, before table-family filtering. A single-source-address negated rule with ratelimit/connlimit (e.g. Source: net:!10.0.0.0/8) now fails to compile in ip and ip6 tables (and with a single-family negation in inet), where one rule was emitted before and only one is emitted now. Shorewall v4 configs on ip tables regress → count alternatives after dropping the other family (skip those whose addrFamily differs from the table family; for a negated single-family list in inet keep the guard only if both alternatives survive) and add an ip-table test with ratelimit + negated source.
- internal/nftables/compiler.go:685 (specCount, used by the guard at :505) — counts via splitAddrs, which now returns 2 alternatives for any negated list, before table-family filtering. A single-source-address negated rule with ratelimit/connlimit (e.g. `Source: net:!10.0.0.0/8`) now fails to compile in `ip` and `ip6` tables (and with a single-family negation in inet), where one rule was emitted before and only one is emitted now. Shorewall v4 configs on `ip` tables regress → count alternatives after dropping the other family (skip those whose addrFamily differs from the table family; for a negated single-family list in inet keep the guard only if both alternatives survive) and add an ip-table test with ratelimit + negated source.
unkin-agent added 1 commit 2026-10-09 23:52:04 +11:00
Count limiter expansion after table-family filtering
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
9bfdf292ad
Author
Member

No findings.

No findings.
unkin-agent added 1 commit 2026-10-10 01:10:41 +11:00
Merge remote-tracking branch 'origin/main' into benvin/nft-decodable
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
78dbb6ad18
# Conflicts:
#	internal/nftables/compiler_test.go
benvin merged commit 57d209794e into main 2026-10-11 10:19:14 +11:00
benvin deleted branch benvin/nft-decodable 2026-10-11 10:19:14 +11:00
Sign in to join this conversation.