← 中文资源

简体中文版本

用 CIDR 白名单校验客户端 IP

解析 CIDR 前缀构建白名单,安全判断 IPv4/IPv6 地址是否允许访问。

阅读英文原文

功能说明

这段代码把一组 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")
	}
}