Go 入门:用 net/netip 处理 IP 地址更省心

介绍 net/netip 的 Addr 和 Prefix,讲解 IP 解析、私网判断、CIDR 匹配、HTTP 客户端 IP 的边界。

处理 IP 地址时,很多旧代码会使用 net.IP。它能用,但类型是 []byte,可变、比较不方便,也容易在 IPv4 和 IPv6 之间绕晕。Go 1.18 引入了 net/netip,提供了更现代的 AddrPrefix 类型。对新代码来说,优先学 netip 会更省心。

IP 地址在业务里很常见:后台白名单、登录风险判断、内网接口保护、限流维度、审计日志。它看起来只是字符串,实际有很多边界。

解析 IP

最基本的解析:

addr, err := netip.ParseAddr("192.168.1.20")
if err != nil {
	return err
}
fmt.Println(addr.Is4())

Addr 是值类型,可以直接比较:

a, _ := netip.ParseAddr("127.0.0.1")
b, _ := netip.ParseAddr("127.0.0.1")
fmt.Println(a == b)

这比 net.IP 的字节切片比较直观很多。值类型也减少了被意外修改的可能。

判断私网地址

netip.Addr 有一些实用方法:

func describe(addr netip.Addr) string {
	switch {
	case addr.IsLoopback():
		return "loopback"
	case addr.IsPrivate():
		return "private"
	case addr.IsGlobalUnicast():
		return "global"
	default:
		return "other"
	}
}

IsPrivate 会识别常见私网地址,比如 10.0.0.0/8172.16.0.0/12192.168.0.0/16,也包括 IPv6 的私有范围。业务上如果要阻止访问内网地址,用它比自己写字符串前缀可靠。

CIDR 匹配

白名单经常用 CIDR:

prefix, err := netip.ParsePrefix("192.168.1.0/24")
if err != nil {
	return err
}
addr, _ := netip.ParseAddr("192.168.1.42")
fmt.Println(prefix.Contains(addr))

可以把多条白名单预先解析好:

type IPAllowList []netip.Prefix

func (l IPAllowList) Contains(addr netip.Addr) bool {
	for _, p := range l {
		if p.Contains(addr) {
			return true
		}
	}
	return false
}

配置加载时解析 CIDR,运行时只做匹配。不要每个请求都重新解析白名单字符串,那既浪费,也会让配置错误在请求路径里才暴露。

从 RemoteAddr 取 IP

HTTP 请求的 RemoteAddr 通常是 ip:port

func remoteIP(r *http.Request) (netip.Addr, error) {
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return netip.Addr{}, err
	}
	return netip.ParseAddr(host)
}

IPv6 地址里本来就有冒号,所以不要自己用 strings.Split(r.RemoteAddr, ":")net.SplitHostPort 会正确处理 IPv4、IPv6 和端口。

X-Forwarded-For 不能随便信

服务放在反向代理后面时,真实客户端 IP 可能在 X-Forwarded-ForX-Real-IP 里。但这些头也是客户端可以伪造的,除非请求来自你信任的代理。

func clientIP(r *http.Request, trustedProxy IPAllowList) (netip.Addr, error) {
	remote, err := remoteIP(r)
	if err != nil {
		return netip.Addr{}, err
	}
	if !trustedProxy.Contains(remote) {
		return remote, nil
	}

	xff := r.Header.Get("X-Forwarded-For")
	if xff == "" {
		return remote, nil
	}
	first := strings.TrimSpace(strings.Split(xff, ",")[0])
	return netip.ParseAddr(first)
}

这个例子只在远端地址属于可信代理时才读取 XFF。真实项目还要根据公司网关规范决定取第一个还是最后一个 IP。重点是:不要无条件相信请求头。

拒绝访问内网地址

如果你的服务允许用户输入 URL 并由服务端去请求,就要防 SSRF。至少要在解析目标主机后拒绝内网 IP、回环地址和未指定地址。

func safeTargetIP(addr netip.Addr) bool {
	if addr.IsLoopback() || addr.IsPrivate() || addr.IsUnspecified() {
		return false
	}
	return addr.IsGlobalUnicast()
}

这只是基础检查。完整 SSRF 防护还要考虑 DNS 重绑定、重定向、IPv6、代理和云厂商元数据地址。入门阶段先知道“用户输入的 URL 不能直接让服务器访问内网”,已经非常重要。

存储格式

日志和数据库里可以存 addr.String()

logger.Info("login failed", "ip", addr.String())

如果要做范围查询或高性能匹配,可以再考虑二进制存储。普通后台、审计日志和白名单配置,字符串足够直观。不要过早把 IP 存成整数,IPv6 会让这个设计变复杂。

测试不同类型地址

IP 相关函数最好覆盖 IPv4、IPv6、私网、回环和非法输入:

func TestSafeTargetIP(t *testing.T) {
	cases := []struct {
		ip   string
		want bool
	}{
		{"127.0.0.1", false},
		{"192.168.1.1", false},
		{"8.8.8.8", true},
		{"::1", false},
	}
	for _, tc := range cases {
		addr, err := netip.ParseAddr(tc.ip)
		if err != nil {
			t.Fatal(err)
		}
		if got := safeTargetIP(addr); got != tc.want {
			t.Fatalf("%s got %v want %v", tc.ip, got, tc.want)
		}
	}
}

测试能提醒你不要只按 IPv4 思考。现在很多环境已经默认支持 IPv6,业务代码至少不要遇到 IPv6 就解析错误。

Prefix 的标准化

解析 CIDR 后,可以调用 Masked 得到规范化前缀。比如 192.168.1.42/24 实际代表的是整个 192.168.1.0/24 网段。

p, err := netip.ParsePrefix("192.168.1.42/24")
if err != nil {
	return err
}
fmt.Println(p.Masked()) // 192.168.1.0/24

配置白名单时建议保存规范化结果。这样展示、比较和日志都更一致。用户输入主机地址加掩码并不罕见,程序应该把它整理成明确的网段。

端口和地址分开处理

如果配置里允许 host:port,不要先解析 IP。先拆端口,再解析 host:

func parseAddrPort(s string) (netip.AddrPort, error) {
	ap, err := netip.ParseAddrPort(s)
	if err != nil {
		return netip.AddrPort{}, err
	}
	if !ap.Addr().IsValid() || ap.Port() == 0 {
		return netip.AddrPort{}, fmt.Errorf("invalid address port")
	}
	return ap, nil
}

AddrPort 对 IPv6 尤其有用,因为 [::1]:8080 这种格式自己拆很容易错。网络相关代码尽量交给标准库解析,不要靠字符串位置猜。

小结

net/netip 提供了更适合新代码的 IP 类型:可比较、不可变、方法清晰。入门时可以用它处理 IP 解析、私网判断、CIDR 匹配和日志字段。

真正的难点不在 API,而在信任边界。RemoteAddr 是直接连接地址,代理头只有在可信代理后面才可信;用户输入 URL 时要防止访问内网;白名单配置要启动时解析并验证。把这些边界想清楚,IP 处理就不会停留在字符串拼接层面。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页