diff --git a/internal/nftables/compiler.go b/internal/nftables/compiler.go index af9b5d6..53d5b55 100644 --- a/internal/nftables/compiler.go +++ b/internal/nftables/compiler.go @@ -953,10 +953,10 @@ func (c *Compiler) compileImplicitIntraZone(state *FirewallState, overridden map return nil } -// intraZoneSkip drops intra-zone pairs on one interface (routeback's job) and pairs whose address -// families can never both match. +// intraZoneSkip drops intra-zone pairs on one interface unless both are routeback hosts entries +// (interface routeback is compileIntraZone's job), and pairs whose address families can never both match. func intraZoneSkip(si, di zoneMatch) bool { - if si.iface != "" && si.iface == di.iface { + if si.iface != "" && si.iface == di.iface && !(si.routeback && di.routeback) { return true } a, b := matchFamily(si), matchFamily(di) @@ -1278,11 +1278,12 @@ func (c *Compiler) selectChain(srcZone, dstZone, fwZone string) string { // zoneMatch classifies a packet into a zone: an interface (empty: any) and, for a hosts entry, one host // address; excl carves out hosts exclusions and the hosts of sub-zones, which shorewall matches first. // fam (an NFPROTO; addr's family when set, else 0: any) guards addr and excl so IPv4 offsets are -// never compared against IPv6 bytes. +// never compared against IPv6 bytes. routeback marks a hosts entry with the routeback option. type zoneMatch struct { iface, addr string excl []string fam byte + routeback bool } // resolveZone returns nil (fail closed) for an unknown zone, or one with neither interfaces nor hosts @@ -1326,7 +1327,7 @@ func (c *Compiler) resolveZone(zone, addr string) []zoneMatch { v4, v6 := splitFamily(slices.Concat(h.Exclusions, sub)) for _, a := range h.Addresses { if addrsOverlap(a, addr) { - m := zoneMatch{iface: h.Interface, addr: a, excl: v6} + m := zoneMatch{iface: h.Interface, addr: a, excl: v6, routeback: h.Options.RouteBack} if p, err := parsePrefix(a); err == nil && p.Addr().Is4() { m.excl = v4 } diff --git a/internal/nftables/compiler_test.go b/internal/nftables/compiler_test.go index 519f1ea..dccbc3a 100644 --- a/internal/nftables/compiler_test.go +++ b/internal/nftables/compiler_test.go @@ -3637,3 +3637,29 @@ func TestCompile_HostsIntraZone(t *testing.T) { t.Errorf("lan lan policy = %q, want %q", got, want) } } + +func TestCompile_HostsRouteBack(t *testing.T) { + sameIface := func(routeback bool) func(*config.Config) { + return func(c *config.Config) { + c.Hosts = []config.Host{ + {Zone: "lan", Interface: "wlo1", Addresses: []string{"192.0.2.0/24"}, Options: config.HostOptions{RouteBack: routeback}}, + {Zone: "lan", Interface: "wlo1", Addresses: []string{"198.51.100.0/24"}}, + } + } + } + if got := describeTagged(mustCompile(t, hostsCfg(sameIface(false))), "forward", "intra:lan"); len(got) != 0 { + t.Errorf("same-interface hosts without routeback = %q, want none", got) + } + want := []string{"iif=wlo1 ip4 saddr=192.0.2.0/24 oif=wlo1 ip4 daddr=192.0.2.0/24"} + if got := describeTagged(mustCompile(t, hostsCfg(sameIface(true))), "forward", "intra:lan"); !reflect.DeepEqual(got, want) { + t.Errorf("routeback hosts intra = %q, want %q", got, want) + } + + state := mustCompile(t, hostsCfg(func(c *config.Config) { + sameIface(true)(c) + c.Policy = append([]config.Policy{{Source: "lan", Dest: "lan", Action: config.PolicyDrop}}, c.Policy...) + })) + if got := describeTagged(state, "forward", "policy:0"); !reflect.DeepEqual(got, want) { + t.Errorf("routeback hosts lan lan policy = %q, want %q", got, want) + } +}