From ff4c9b63e88a915fce09594e3dc42329de3bcb91 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 4 Oct 2026 15:06:24 +1100 Subject: [PATCH] Include firewall zone in all/any rule expansion --- internal/nftables/compiler.go | 28 ++++++++++++++++-- internal/nftables/compiler_test.go | 46 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/internal/nftables/compiler.go b/internal/nftables/compiler.go index a86c5b5..cfdbff5 100644 --- a/internal/nftables/compiler.go +++ b/internal/nftables/compiler.go @@ -467,7 +467,11 @@ func (c *Compiler) compileOneRule(state *FirewallState, tag, srcSpec, dstSpec, p dports, sports config.PortSpec, action config.RuleAction, logLevel string, dnatDest, origDest string, fwZone string, section config.RuleSection) error { - for _, src := range c.zoneSpecs(srcSpec) { + srcs := c.zoneSpecs(srcSpec) + if action != config.RuleDNAT && action != config.RuleRedirect { + srcs = withFirewall(srcs, fwZone) + } + for _, src := range srcs { for _, srcAddr := range splitAddrs(src.Addr) { for _, od := range splitAddrs(origDest) { if action == config.RuleDNAT || action == config.RuleRedirect { @@ -476,7 +480,10 @@ func (c *Compiler) compileOneRule(state *FirewallState, tag, srcSpec, dstSpec, p } continue } - for _, dst := range c.zoneSpecs(dstSpec) { + for _, dst := range withFirewall(c.zoneSpecs(dstSpec), fwZone) { + if src.Zone == fwZone && dst.Zone == fwZone && (isGlobalZone(srcSpec) || isGlobalZone(dstSpec)) { + continue + } // 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, "+!") && !strings.Contains(dstSpec, "+!")) { @@ -526,6 +533,23 @@ func (c *Compiler) zoneSpecs(spec string) []config.ZoneSpec { return out } +// withFirewall adds the firewall zone beside a global all/any spec, which otherwise only reaches forward. +func withFirewall(specs []config.ZoneSpec, fwZone string) []config.ZoneSpec { + out := specs + for _, s := range specs { + if fwZone != "" && isGlobalZone(s.Zone) { + out = append(out, config.ZoneSpec{Zone: fwZone, Addr: s.Addr}) + } + } + return out +} + +func isGlobalZone(spec string) bool { + zone, _ := splitZoneSpec(spec) + base := strings.TrimSuffix(zone, "+") + return base == "all" || base == "any" +} + func isZoneExclusion(spec string) bool { base, _, ok := strings.Cut(spec, "!") base = strings.TrimSuffix(base, "+") diff --git a/internal/nftables/compiler_test.go b/internal/nftables/compiler_test.go index 7be75de..7fdbc39 100644 --- a/internal/nftables/compiler_test.go +++ b/internal/nftables/compiler_test.go @@ -2799,3 +2799,49 @@ func TestCompile_ConntrackHelperZones(t *testing.T) { }) } } + +func TestCompile_AllIncludesFirewall(t *testing.T) { + cases := []struct { + src, dst string + want map[string]int + }{ + {"all", "all", map[string]int{"input": 1, "output": 1, "forward": 1}}, + {"net", "all", map[string]int{"input": 1, "output": 0, "forward": 1}}, + {"all", "net", map[string]int{"input": 0, "output": 1, "forward": 1}}, + {"all", "fw", map[string]int{"input": 1, "output": 0, "forward": 0}}, + {"all:192.0.2.0/24", "fw", map[string]int{"input": 1, "output": 0, "forward": 0}}, + {"all!fw", "all!fw", map[string]int{"input": 0, "output": 0, "forward": 2}}, + } + for _, tc := range cases { + t.Run(tc.src+"->"+tc.dst, func(t *testing.T) { + cfg := &config.Config{ + Settings: config.Settings{TableName: "test", AddressFamily: config.FamilyINET}, + Zones: map[string]config.Zone{ + "fw": {Type: config.ZoneFirewall}, + "net": {Type: config.ZoneIP}, + "loc": {Type: config.ZoneIP}, + }, + Interfaces: []config.Interface{{Zone: "net", Interface: "eth0"}, {Zone: "loc", Interface: "eth1"}}, + Rules: []config.Rule{ + {Action: config.RuleAccept, Source: tc.src, Dest: tc.dst, Proto: "icmp", DPort: config.PortSpec{"8"}}, + }, + PortGroups: map[string]config.PortGroup{}, + } + state, err := NewCompiler(cfg).Compile() + if err != nil { + t.Fatalf("Compile() error: %v", err) + } + for chain, want := range tc.want { + got := 0 + for _, r := range state.Rules[chain] { + if r.Tag == "rule:0" { + got++ + } + } + if got != want { + t.Errorf("%s: got %d rule:0 entries, want %d", chain, got, want) + } + } + }) + } +}