Honour hosts routeback for same-interface intra-zone pairs
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

This commit is contained in:
2026-10-09 23:15:50 +11:00
parent 0b68110220
commit 3174eabd94
2 changed files with 32 additions and 5 deletions
+6 -5
View File
@@ -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
}
+26
View File
@@ -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)
}
}