From bf235291d0061f95702c4bf434df7543362bb356 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 3 Oct 2026 23:50:18 +1000 Subject: [PATCH] Fail closed on unknown/none conntrack DEST and pair all+ symmetrically --- internal/nftables/compiler.go | 5 ++++- internal/nftables/compiler_test.go | 31 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/nftables/compiler.go b/internal/nftables/compiler.go index d9327ea..66b36c2 100644 --- a/internal/nftables/compiler.go +++ b/internal/nftables/compiler.go @@ -268,6 +268,9 @@ func (c *Compiler) compileConntrackPair(state *FirewallState, tag, chain string, return fmt.Errorf("conntrack DEST zone %q needs an address in prerouting", dstZone) } srcIfaces, dstIfaces := c.resolveZoneInterfaces(srcZone, srcAddr), []string{""} + if chain == "raw_prerouting" && c.resolveZoneInterfaces(dstZone, dstAddr) == nil { + return nil + } if chain == "raw_output" { srcIfaces, dstIfaces = []string{""}, c.resolveZoneInterfaces(dstZone, dstAddr) } @@ -417,7 +420,7 @@ func (c *Compiler) compileOneRule(state *FirewallState, tag, srcSpec, dstSpec, p for _, dst := range c.zoneSpecs(dstSpec) { // Exclusion expansion never pairs fw with itself, and pairs a zone with itself only for "all+". if src.Zone == dst.Zone && (isZoneExclusion(srcSpec) || isZoneExclusion(dstSpec)) && - (src.Zone == fwZone || !strings.Contains(srcSpec, "+!")) { + (src.Zone == fwZone || !strings.Contains(srcSpec, "+!") && !strings.Contains(dstSpec, "+!")) { continue } for _, dstAddr := range splitAddrs(dst.Addr) { diff --git a/internal/nftables/compiler_test.go b/internal/nftables/compiler_test.go index 65d157c..1485ef5 100644 --- a/internal/nftables/compiler_test.go +++ b/internal/nftables/compiler_test.go @@ -2506,6 +2506,16 @@ func TestCompile_ConntrackZones(t *testing.T) { ct: config.ConntrackRule{Action: config.ConntrackDrop, Source: "nte", Dest: "fw"}, want: map[string][]string{}, }, + { + name: "unknown dest zone fails closed in prerouting", + ct: config.ConntrackRule{Action: config.ConntrackDrop, Source: "net", Dest: "typo"}, + want: map[string][]string{}, + }, + { + name: "none dest zone yields no rule", + ct: config.ConntrackRule{Action: config.ConntrackDrop, Source: "net", Dest: "none"}, + want: map[string][]string{}, + }, { name: "all!net expands to every other zone", ct: config.ConntrackRule{Action: config.ConntrackNoTrack, Source: "all!net", Dest: "fw:192.0.2.53"}, @@ -2576,3 +2586,24 @@ func TestCompile_RuleZoneExclusionExpands(t *testing.T) { t.Errorf("unknown zone compiled %d rules, want 0", len(r)) } } + +func TestCompile_RuleZoneExclusionIntraZoneSymmetric(t *testing.T) { + cfg := listCfg(func(cfg *config.Config) { + cfg.Zones["lan"] = config.Zone{Type: config.ZoneIP} + cfg.Interfaces = append(cfg.Interfaces, config.Interface{Zone: "lan", Interface: "eth1"}) + cfg.Rules = []config.Rule{ + {Source: "lan", Dest: "all+!net", Action: config.RuleAccept}, + {Source: "lan", Dest: "all!net", Action: config.RuleAccept}, + } + }) + state := mustCompile(t, cfg) + for i, want := range []bool{true, false} { + got := false + for _, r := range taggedRules(state, "forward", fmt.Sprintf("rule:%d", i)) { + got = got || describeRule(r) == "iif=eth1 oif=eth1" + } + if got != want { + t.Errorf("rule:%d lan->lan forward = %v, want %v", i, got, want) + } + } +}