登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go netip.Prefix 怎么判断地址是否属于网段

来源:17golang原创

时间:2026-10-06 02:25:06 175浏览 收藏

判断一个 IP 地址是否属于某个 CIDR 网段,Go 里最直接的写法是:用 netip.ParsePrefix 解析网段,用 netip.ParseAddr 解析地址,然后调用 prefix.Contains(addr)。返回 true 表示地址在网段内,返回 false 表示不在;解析失败应单独作为错误处理,不能和“不属于网段”混在一起。

官方文档:https://pkg.go.dev/net/netip

我通常在访问控制、代理来源识别和内网地址分流里使用这套组合。它看起来只有三行,但最容易让我停下来检查的不是 Contains 本身,而是输入有没有解析成功、前缀是否需要规范化,以及 IPv4 和 IPv6 是否被混在了一起。

先把 Prefix.Contains 的判断关系说清楚

netip.Prefix 同时保存一个地址和一个前缀长度。例如 192.168.10.0/24 的前 24 位是网络位,剩余位是主机位。Contains 接收一个 netip.Addr,在地址族一致的前提下比较前缀覆盖的网络位。

package main

import (
    "fmt"
    "net/netip"
)

func main() {
    // 固定配置可以用 MustParsePrefix;输入错误时它会 panic。
    prefix := netip.MustParsePrefix("192.168.10.0/24")

    // 待判断地址同样解析为 netip.Addr。
    inside := netip.MustParseAddr("192.168.10.42")
    outside := netip.MustParseAddr("192.168.11.42")

    // Contains 只返回归属结果,不负责报告字符串解析错误。
    fmt.Println(prefix.Contains(inside))  // true
    fmt.Println(prefix.Contains(outside)) // false
}

这段写法适合代码中写死且已经确认正确的常量。若网段来自配置文件、环境变量、接口参数或数据库,不要使用 MustParsePrefix,因为一个拼写错误就会触发 panic。生产输入应使用返回 error 的解析函数。

netip Prefix、候选 Addr、地址族和 Contains 布尔结果的静态关系
图1:Prefix 保存网段地址与前缀长度,Contains 在地址族一致时比较网络位并返回布尔结果。这是静态结构说明图,不是运行截图。

配置输入要把错误和未命中分开

我第一次把这个判断放进配置驱动的访问规则时,最不适应的是不能只返回一个 bool。false 既可能表示地址确实不在网段,也可能是调用方把地址写成了 192.168.1.999。这两种情况的处理完全不同:前者是正常未命中,后者应该暴露配置或请求错误。

package cidrmatch

import (
    "fmt"
    "net/netip"
)

// InPrefix 判断 addrText 是否属于 prefixText,并保留解析错误。
func InPrefix(prefixText, addrText string) (bool, error) {
    prefix, err := netip.ParsePrefix(prefixText)
    if err != nil {
        // 给错误补充输入角色,便于定位是哪一项配置有问题。
        return false, fmt.Errorf("解析网段 %q: %w", prefixText, err)
    }

    addr, err := netip.ParseAddr(addrText)
    if err != nil {
        // 地址格式错误不是一次正常的“未命中”。
        return false, fmt.Errorf("解析地址 %q: %w", addrText, err)
    }

    return prefix.Contains(addr), nil
}

调用方可以据此建立明确的处理分支:err != nil 记录错误并拒绝继续判断;err == nil && matched 执行命中规则;err == nil && !matched 再尝试下一条网段或走默认策略。

Masked 让网段表示更稳定

ParsePrefix 会保留输入中的地址部分,所以 192.168.10.123/24 可以被解析为一个前缀。对于 Contains 来说,主机位不会改变前 24 位的归属判断;但日志、配置比较和去重时,保留主机位会让同一个网段出现多种写法。

我更习惯在配置进入系统时调用 Masked,把主机位清零,得到规范的 192.168.10.0/24。这样不是为了“修复 Contains”,而是让后续存储和观察更一致。

package cidrmatch

import "net/netip"

// CanonicalPrefix 把可解析的 CIDR 统一成网络地址形式。
func CanonicalPrefix(text string) (netip.Prefix, error) {
    prefix, err := netip.ParsePrefix(text)
    if err != nil {
        // 保留零值 Prefix,并把解析错误交给调用方。
        return netip.Prefix{}, err
    }

    // 例如 192.168.10.123/24 会规范为 192.168.10.0/24。
    return prefix.Masked(), nil
}

需要注意,Masked 返回一个新的 Prefix,不会原地修改原变量。若你后面要用规范值做键或输出日志,要记得接住返回值。

最容易忽略的是地址族不一致

IPv4 前缀和 IPv6 地址不属于同一地址族,即使 IPv6 地址采用了 IPv4 映射形式,直接判断也不会自动把它当成普通 IPv4。典型例子是前缀 192.168.10.0/24 与地址 ::ffff:192.168.10.42:后者是 IPv4 映射 IPv6 地址,直接比较会得到未命中。

如果系统的输入边界明确允许映射地址与 IPv4 规则匹配,可以在 IPv4 前缀场景中调用 Addr.Unmap。它会把 IPv4 映射 IPv6 地址还原成 IPv4;对其他地址则保持原值。

package cidrmatch

import "net/netip"

// ContainsNormalized 只在 IPv4 前缀场景下归一化映射地址。
func ContainsNormalized(prefix netip.Prefix, addr netip.Addr) bool {
    if prefix.Addr().Is4() {
        // ::ffff:192.168.10.42 会被还原成普通 IPv4 地址。
        addr = addr.Unmap()
    }

    // 原生 IPv6 前缀仍与 IPv6 地址比较,不做跨族猜测。
    return prefix.Contains(addr)
}
IPv4 Prefix、IPv6 Prefix、映射地址与 Addr Unmap 的边界关系
图2:IPv4 与 IPv6 是不同地址族;IPv4 映射 IPv6 地址要先根据输入边界决定是否 Unmap,再与 IPv4 网段比较。这是静态边界说明图。

这里不要为了“让它命中”而无条件转换所有地址。是否把映射地址等同于 IPv4,应由代理协议、上游地址格式和安全策略共同决定。若规则明确区分 IPv4 与 IPv6,就应保留原始地址族并让不一致自然返回 false。

多网段规则要返回命中的那一条

实际配置通常不止一个 CIDR。只返回 true 会让排查变得困难:你知道地址被放行,却不知道是哪条规则命中。我倾向于返回命中的规范前缀和一个布尔值,日志里就能直接记录规则来源。

package cidrmatch

import "net/netip"

// FirstMatch 返回第一条命中的规范前缀。
func FirstMatch(prefixes []netip.Prefix, addr netip.Addr) (netip.Prefix, bool) {
    for _, prefix := range prefixes {
        normalized := prefix.Masked()

        // 根据当前规则的地址族处理 IPv4 映射地址。
        candidate := addr
        if normalized.Addr().Is4() {
            candidate = candidate.Unmap()
        }

        if normalized.Contains(candidate) {
            return normalized, true
        }
    }

    // 零值 Prefix 与 false 共同表示没有规则命中。
    return netip.Prefix{}, false
}

如果规则有优先级,应在传入函数前完成排序,并清楚定义“第一条命中”的含义。若不同规则可能重叠,除了保存匹配结果,还要在配置加载阶段检查重复和覆盖关系,避免一条宽网段无意中遮住更具体的规则。

异常时的回退路径

当新规则上线后出现大量未命中,我不会先改 Contains 调用,而是按输入边界回退:

  • 先保留原始网段字符串和地址字符串,确认解析错误是否突然增加。
  • 记录 prefix.Addr().Is4()、addr.Is4() 与 addr.Is4In6(),判断是否是地址族变化。
  • 把规范化前缀写入结构化日志,确认配置中的主机位没有制造重复规则。
  • 若此前没有定义映射地址策略,先回退到旧规则,再明确是否允许 Unmap。
  • 不要把解析错误降级成普通 false,否则错误配置会被悄悄吞掉。

对我来说,最有价值的不是再包一层辅助函数,而是把“无效输入”“地址族不一致”和“正常未命中”拆成三类信号。这样告警里看到错误率上升时,能马上知道应该修配置、修地址提取逻辑,还是调整网段规则。

发布前的判断清单

检查项正确做法异常信号
网段解析外部输入使用 ParsePrefix 并处理 error错误被当作未命中
地址解析使用 ParseAddr 保留格式错误无效地址进入默认规则
前缀表示存储和日志使用 Masked 结果同一网段出现多种主机位写法
地址族明确 IPv4、IPv6 和映射地址策略看似相同的地址始终未命中
多规则返回具体命中前缀并定义优先级只能看到 true,找不到来源规则
回退保留旧策略和原始输入日志上线后无法区分规则错误与输入变化

相关问题

192.168.1.255 属于 192.168.1.0/24 吗?

属于。/24 覆盖从 192.168.1.0 到 192.168.1.255 的全部地址;Prefix.Contains 判断的是地址是否在前缀范围内,不排除传统语境里的网络地址或广播地址。

Prefix.Contains 能同时处理 IPv4 和 IPv6 吗?

能处理两种地址族,但一次判断要求前缀与地址属于兼容的同一地址族。IPv4 前缀不会自动包含 IPv4 映射 IPv6 地址。

为什么 ParsePrefix 后还要调用 Masked?

Contains 并不依赖主机位清零才能工作;调用 Masked 的主要目的,是让配置存储、比较、去重和日志输出使用统一的网络地址形式。

怎么判断一个子网是否完全落在另一个网段内?

这是前缀与前缀的包含关系,不应只检查一个任意地址。可先规范化两个前缀,再确认地址族一致、外层前缀长度不大于内层前缀长度,并检查外层前缀包含内层前缀的网络地址。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>