Exclude sub-zone hosts from wildcard parent interfaces
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

This commit is contained in:
2026-10-09 23:35:32 +11:00
parent b8ad59b053
commit 190ff72643
4 changed files with 91 additions and 4 deletions
+14
View File
@@ -473,6 +473,20 @@ func TestValidateHosts(t *testing.T) {
},
wantErr: "interface \"eth99\" not defined in interfaces",
},
{
name: "host interface matched by wildcard",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"lan": {Type: ZoneIP, Parents: []string{"net"}},
},
interfaces: []Interface{
{Zone: "net", Interface: "enp+"},
},
hosts: []Host{
{Zone: "lan", Interface: "enp2s0", Addresses: []string{"192.0.2.0/24"}},
},
},
{
name: "zone not defined",
zones: map[string]Zone{
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net/netip"
"slices"
"strings"
)
type Host struct {
@@ -44,7 +45,8 @@ func (c *Config) validateHosts() error {
ifaceFound := false
for _, iface := range c.Interfaces {
if iface.Interface == h.Interface || iface.PhysicalName() == h.Interface {
prefix, wild := strings.CutSuffix(iface.PhysicalName(), "+")
if iface.Interface == h.Interface || iface.PhysicalName() == h.Interface || (wild && strings.HasPrefix(h.Interface, prefix)) {
ifaceFound = true
break
}
+18 -3
View File
@@ -1351,11 +1351,11 @@ func (c *Compiler) resolveZone(zone, addr string) []zoneMatch {
return nil
}
// subZoneHosts lists the host addresses on iface that belong to sub-zones of zone, and those
// sub-zone hosts' exclusions, which fall back to zone.
// subZoneHosts lists the host addresses on interfaces overlapping iface that belong to sub-zones of
// zone, and those sub-zone hosts' exclusions, which fall back to zone.
func (c *Compiler) subZoneHosts(zone, iface string) (sub, back []string) {
for _, h := range c.cfg.Hosts {
if h.Interface == iface && c.cfg.IsSubZone(h.Zone, zone) {
if ifacesOverlap(h.Interface, iface) && c.cfg.IsSubZone(h.Zone, zone) {
sub = append(sub, h.Addresses...)
back = append(back, h.Exclusions...)
}
@@ -1363,6 +1363,21 @@ func (c *Compiler) subZoneHosts(zone, iface string) (sub, back []string) {
return sub, back
}
// ifacesOverlap reports whether two interface names (a "+" suffix being a prefix wildcard) can match the same interface.
func ifacesOverlap(a, b string) bool {
pa, wa := strings.CutSuffix(a, "+")
pb, wb := strings.CutSuffix(b, "+")
switch {
case wa && wb:
return strings.HasPrefix(pa, pb) || strings.HasPrefix(pb, pa)
case wa:
return strings.HasPrefix(pb, pa)
case wb:
return strings.HasPrefix(pa, pb)
}
return a == b
}
// addrsOverlap reports whether a host address can match a rule address; unparsable or negated rule addresses keep the host.
func addrsOverlap(host, rule string) bool {
if rule == "" || strings.HasPrefix(rule, "!") {
+56
View File
@@ -3663,3 +3663,59 @@ func TestCompile_HostsRouteBack(t *testing.T) {
t.Errorf("routeback hosts lan lan policy = %q, want %q", got, want)
}
}
func TestCompile_WildcardParentExcludesSubZoneHosts(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},
"lan": {Type: config.ZoneIP, Parents: []string{"net"}}, "lxd": {Type: config.ZoneIP},
},
Interfaces: []config.Interface{
{Zone: "net", Interface: "enp+"}, {Zone: "net", Interface: "wlo1"}, {Zone: "lxd", Interface: "lxdbr0"},
},
Hosts: []config.Host{
{Zone: "lan", Interface: "enp2s0", Addresses: []string{"192.0.2.0/24"}},
{Zone: "lan", Interface: "wlo1", Addresses: []string{"198.51.100.0/24"}},
},
Policy: []config.Policy{
{Source: "lxd", Dest: "net", Action: config.PolicyAccept},
{Source: "net", Dest: "all", Action: config.PolicyDrop},
{Source: "all", Dest: "all", Action: config.PolicyReject},
},
PortGroups: make(map[string]config.PortGroup),
}
state := mustCompile(t, cfg)
want := []string{
"iif=lxdbr0 oif=enp ip4 !daddr=192.0.2.0/24", "iif=lxdbr0 oif=enp ip6",
"iif=lxdbr0 oif=wlo1 ip4 !daddr=198.51.100.0/24", "iif=lxdbr0 oif=wlo1 ip6",
}
if got := describeTagged(state, "forward", "policy:0"); !reflect.DeepEqual(got, want) {
t.Errorf("lxd->net accept = %q, want %q", got, want)
}
want = []string{
"iif=enp ip4 !saddr=192.0.2.0/24", "iif=enp ip6",
"iif=wlo1 ip4 !saddr=198.51.100.0/24", "iif=wlo1 ip6",
}
if got := describeTagged(state, "input", "policy:1"); !reflect.DeepEqual(got, want) {
t.Errorf("net->fw drop = %q, want %q", got, want)
}
if got := describeTagged(state, "input", "policy:2"); len(got) == 0 || got[0] != "iif=enp2s0 ip4 saddr=192.0.2.0/24" {
t.Errorf("lan->fw reject = %q, want lan on enp2s0 first", got)
}
}
func TestIfacesOverlap(t *testing.T) {
for _, tt := range []struct {
a, b string
want bool
}{
{"enp2s0", "enp2s0", true}, {"enp2s0", "enp3s0", false},
{"enp+", "enp2s0", true}, {"enp2s0", "enp+", true}, {"enp+", "wlo1", false},
{"en+", "enp+", true}, {"enp+", "eno+", false}, {"enp", "enp+", true},
} {
if got := ifacesOverlap(tt.a, tt.b); got != tt.want {
t.Errorf("ifacesOverlap(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
}
}
}