功能说明
这段代码把一组 CIDR 前缀编译成 IP 白名单,并提供“给定地址是否允许”的查询。它适合管理后台、Webhook 来源校验、内部 API 网关或运维工具的访问控制。
使用 net/netip 而不是字符串前缀匹配,可以正确处理网段边界、IPv6 和主机位。例如 10.0.0.0/8 会匹配 10.2.3.4,但不会错误匹配 110.2.3.4 或 10.0.0.0.evil.com。
关键代码
NewAllowlist 逐个调用 netip.ParsePrefix,解析失败立即返回带原始输入的包装错误;prefix.Masked() 把主机位清零,避免 10.1.2.3/8 这类写法造成意外范围。
Allows 先用 netip.ParseAddr 解析地址,失败时默认拒绝(fail closed),然后遍历前缀调用 Contains。IPv4 前缀只匹配 IPv4 地址,IPv6 前缀只匹配 IPv6 地址,二者不会混淆。
使用注意
如果客户端地址来自 X-Forwarded-For,必须只信任由自己代理追加的那一段,否则攻击者可以伪造白名单地址。IPv4-mapped IPv6(如 ::ffff:10.2.3.4)不会自动按 IPv4 前缀匹配,必要时先调用 Unmap()。
白名单是只读结构,查询并发安全;构建后不要再修改切片。代码不做 DNS 解析,域名应提前解析并固定,避免 DNS 重绑定。空白名单表示拒绝所有地址,这是安全的默认值。
验证方式
示例中 10.2.3.4、192.168.1.200 和 2001:db8::1 返回 true,192.168.2.1、203.0.113.9 返回 false,每个结果都有断言。传入 10.0.0.0/33 会因为前缀长度非法而返回错误。可以再加入 ::ffff:10.2.3.4,观察未做 Unmap 时的实际行为。
代码示例
package main
import (
"fmt"
"net/netip"
)
type Allowlist struct {
prefixes []netip.Prefix
}
func NewAllowlist(cidrs []string) (*Allowlist, error) {
list := &Allowlist{}
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
return nil, fmt.Errorf("parse %q: %w", cidr, err)
}
list.prefixes = append(list.prefixes, prefix.Masked())
}
return list, nil
}
func (a *Allowlist) Allows(raw string) bool {
address, err := netip.ParseAddr(raw)
if err != nil {
return false
}
for _, prefix := range a.prefixes {
if prefix.Contains(address) {
return true
}
}
return false
}
func main() {
list, err := NewAllowlist([]string{"10.0.0.0/8", "192.168.1.0/24", "2001:db8::/32"})
if err != nil {
panic(err)
}
cases := map[string]bool{
"10.2.3.4": true,
"192.168.1.200": true,
"192.168.2.1": false,
"2001:db8::1": true,
"203.0.113.9": false,
}
for address, want := range cases {
got := list.Allows(address)
fmt.Printf("%-15s allowed=%v\n", address, got)
if got != want {
panic("unexpected result for " + address)
}
}
if _, err := NewAllowlist([]string{"10.0.0.0/33"}); err == nil {
panic("invalid prefix should fail")
}
}