fix(netpolicy): normalize allowlist CIDR inputs

This commit is contained in:
Sakurapainting 2026-04-02 19:37:29 +08:00
parent bf89c4101f
commit a86705340f
3 changed files with 57 additions and 2 deletions

View file

@ -62,7 +62,7 @@ When `gateway.host` is a loopback address (`127.0.0.1`, `::1`, or `localhost`) a
CIDR sources in fallback mode: CIDR sources in fallback mode:
- If `gateway.allowed_cidrs` is configured, that list is used. - If `gateway.allowed_cidrs` is configured, that list is used.
- If `gateway.allowed_cidrs` is empty, discovered private CIDRs are used (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, `fc00::/7`). - If `gateway.allowed_cidrs` is empty, PicoClaw discovers CIDR networks from local interfaces and uses only those that fall within private address ranges (for example, within `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, `fc00::/7`).
- If no private non-loopback CIDR can be discovered, gateway startup fails. On public-only hosts, configure `gateway.allowed_cidrs` explicitly. - If no private non-loopback CIDR can be discovered, gateway startup fails. On public-only hosts, configure `gateway.allowed_cidrs` explicitly.
Loopback clients are always allowed for local administration. Loopback clients are always allowed for local administration.

View file

@ -19,15 +19,31 @@ func NewIPAllowlist(allowedCIDRs []string) (*IPAllowlist, error) {
return &IPAllowlist{}, nil return &IPAllowlist{}, nil
} }
seen := make(map[string]struct{}, len(allowedCIDRs))
nets := make([]*net.IPNet, 0, len(allowedCIDRs)) nets := make([]*net.IPNet, 0, len(allowedCIDRs))
for _, cidr := range allowedCIDRs { for _, rawCIDR := range allowedCIDRs {
cidr := strings.TrimSpace(rawCIDR)
if cidr == "" {
continue
}
_, ipNet, err := net.ParseCIDR(cidr) _, ipNet, err := net.ParseCIDR(cidr)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid CIDR %q: %w", cidr, err) return nil, fmt.Errorf("invalid CIDR %q: %w", cidr, err)
} }
canonical := ipNet.String()
if _, ok := seen[canonical]; ok {
continue
}
seen[canonical] = struct{}{}
nets = append(nets, ipNet) nets = append(nets, ipNet)
} }
if len(nets) == 0 {
return &IPAllowlist{}, nil
}
return &IPAllowlist{nets: nets}, nil return &IPAllowlist{nets: nets}, nil
} }

View file

@ -69,3 +69,42 @@ func TestIPAllowlistWithZoneAddressInCIDR(t *testing.T) {
t.Fatal("allowlist should accept IPv6 link-local with zone") t.Fatal("allowlist should accept IPv6 link-local with zone")
} }
} }
func TestNewIPAllowlistTrimsSkipsAndDedups(t *testing.T) {
allowlist, err := NewIPAllowlist([]string{
" 192.168.1.8/24 ",
"",
"192.168.1.0/24",
" ",
"10.0.0.0/8",
})
if err != nil {
t.Fatalf("NewIPAllowlist() error = %v", err)
}
if allowlist.IsOpen() {
t.Fatal("allowlist should not be open")
}
if len(allowlist.nets) != 2 {
t.Fatalf("len(allowlist.nets) = %d, want 2", len(allowlist.nets))
}
if !allowlist.AllowsRemoteAddr("192.168.1.22:1234") {
t.Fatal("allowlist should allow deduplicated 192.168.1.0/24 CIDR")
}
if !allowlist.AllowsRemoteAddr("10.9.8.7:1234") {
t.Fatal("allowlist should allow 10.0.0.0/8 CIDR")
}
if allowlist.AllowsRemoteAddr("203.0.113.7:1234") {
t.Fatal("allowlist should reject outside CIDR")
}
}
func TestNewIPAllowlistAllEmptyEntries(t *testing.T) {
allowlist, err := NewIPAllowlist([]string{"", " ", "\t"})
if err != nil {
t.Fatalf("NewIPAllowlist() error = %v", err)
}
if !allowlist.IsOpen() {
t.Fatal("allowlist should be open when all entries are empty")
}
}