qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

五条路由都能到这个地址,电脑为什么偏要走最细的那一条?

同一个 IPv4 地址可以同时属于 /0、/8、/16、/24 和 /32。写一个不会改动真实网络的查询器,看最长前缀怎样选择下一站。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 斜杠后面的数字,表示前面固定了多少位
  4. 先判断能否匹配,再比较谁更具体
  5. 第一条能匹配就返回,为什么会选错?
  6. 完整程序检查五层匹配、回退和输入边界
  7. 它解释的是一次表内查询,不是整台路由器
  8. 理解它之后,排障时该看什么?
  9. 记住这三点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 1 个解释
网络前缀长度
地址后面的斜杠数字说明,开头有多少个二进制位固定为网络部分。IPv4 的 /24 固定前 24 位;/32 固定全部 32 位,表示一个具体地址。
打开完整术语小抄 ↗

假设目的地址是 10.20.30.7,路由表里既有整个园区的 10.0.0.0/8,又有实验室的 10.20.30.0/24,还有这台机器自己的 /32 条目。它们都包含目的地址,仅仅找到第一条能匹配的路由还不够。

在本文选定的单张 IPv4 转发表里,我们先找出所有包含目标的网段,再保留前缀最长的那个。实验只处理内存中的虚构路由表,不读系统配置,也不会改变你的网络。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • longest_prefix.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B longest_prefix.py

解压到独立练习文件夹,在含 longest_prefix.py 的目录打开终端运行。静态 IPv4 单表的最长前缀教学模型,不发送网络包,不读取或改变系统路由;拒绝重复网段,未实现等长路由、metric、ECMP、策略路由、协议选路或硬件转发。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

同一个地址,究竟选哪条路?

先查哪些网段包含目标地址,再在这些候选中选前缀最长的一条。取消更具体的路由,观察它会退回哪一层。

启用哪些路由

选择结果:10.20.30.7/32 → single-host(指定主机)

匹配的候选中,32 是最长的前缀长度。

先看是否包含地址,再比较前缀长度
网段是否启用包含目标决定
0.0.0.0/0是是匹配,但前缀较短
10.0.0.0/8是是匹配,但前缀较短
10.20.0.0/16是是匹配,但前缀较短
10.20.30.0/24是是匹配,但前缀较短
10.20.30.7/32是是选中

这是固定 IPv4 路由表的线性查找模型,不读取或修改系统路由。这里只比较一组已可用路由的最长前缀,不处理策略路由、路由学习、同长候选的取舍或多路径负载分担,也不会测试目标能否连通。

斜杠后面的数字,表示前面固定了多少位

IPv4 地址有 32 位,点号分成四组,每组占 8 位,写成十进制时取值为 0 到 255。10.20.30.0/24 表示前 24 位固定,后 8 位可以变化;因此它涵盖 10.20.30.0 到 10.20.30.255。

10.20.0.0/16 固定得更少,范围更大。/32 固定全部 32 位,只覆盖一个地址;/0 不固定任何位,可作为默认匹配。前缀也可以切在一组内部:例如 192.0.2.64/26 固定前三组的 24 位和最后一组的前 2 位。

64 的 8 位写法是 01000000,固定开头的 01,余下 6 位从 000000 变到 111111,所以最后一组的范围是 64 到 127。

这里的长短是二进制前缀位数,不是字符串有多少个字符,也不是地址看起来有几段相同。本文用标准库 IPv4Network 判断归属,避免手写字符串 startswith 把相似文本误认成同一网段。

先判断能否匹配,再比较谁更具体

实验表依次为 /0→default、10.0.0.0/8→campus、10.20.0.0/16→building、10.20.30.0/24→lab、10.20.30.7/32→single-host。这些名字只是下一站标签,并非真的网关IP。目标 10.20.30.7 五条都匹配,最长的是 /32;10.20.30.8 不匹配那条 /32,所以选 /24。

这让广泛规则与局部例外能够同时存在:默认路由处理没有更具体条目的地址,实验室网段处理自己的范围,单机条目还能覆盖其中一个地址。最长前缀规则并不表示道路更短或时延更低,前缀长度与路径的跳数是两回事。

第一条能匹配就返回,为什么会选错?

本例把默认路由放在第一条。若查询器遇到匹配就立即返回,所有地址都会被 /0 接走,再具体的条目都没有机会参与。这就是代码里 wrong_first_match 输出 0.0.0.0/0 的原因。

正确实现要比较全部匹配条目的 prefixlen。把路由列表重新排列不应改变结果,除非你另外定义了等长条目的选择策略。实验直接枚举五条路由的 120 种排列,对每种排列都查询五个目的地址。

完整程序检查五层匹配、回退和输入边界

保存为 longest_prefix.py,执行 python -B longest_prefix.py。输出中五个地址分别选中 /32、/24、/16、/8、/0。随后移除单机 /32 条目,再查 10.20.30.7,此时回退到仍然匹配的 /24。移除默认条目后查询 192.0.2.1,则返回 None,表示这张模型表没有路由。

程序还拒绝 10.20.30.7/24 这种带主机位的网段输入,以及同一网段被重复配置的情况。前者使用 strict=True 提醒你明确网络地址,后者明确把等长路由策略留在本篇范围之外。总计执行 605 项核对。

展开收起代码 · 小实验python · 67 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""IPv4 longest-prefix teaching lookup; never reads or writes OS routes."""
import itertools
import json
from ipaddress import IPv4Address, IPv4Network


def build_table(entries):
    table = []
    networks = set()
    for cidr, next_hop in entries:
        network = IPv4Network(cidr, strict=True)
        if network in networks:
            raise ValueError("duplicate prefix: equal-prefix route policy is outside this model")
        networks.add(network)
        table.append((network, next_hop))
    return table


def lookup(table, destination):
    address = IPv4Address(destination)
    matches = [(network, hop) for network, hop in table if address in network]
    if not matches:
        return None
    network, hop = max(matches, key=lambda route: route[0].prefixlen)
    return {"prefix": str(network), "next_hop": hop}


def main():
    entries = [("0.0.0.0/0", "default"), ("10.0.0.0/8", "campus"),
               ("10.20.0.0/16", "building"), ("10.20.30.0/24", "lab"),
               ("10.20.30.7/32", "single-host")]
    expected = {"10.20.30.7": "10.20.30.7/32", "10.20.30.8": "10.20.30.0/24",
                "10.20.9.8": "10.20.0.0/16", "10.9.8.7": "10.0.0.0/8",
                "192.0.2.1": "0.0.0.0/0"}
    table = build_table(entries)
    checks = 0
    for permutation in itertools.permutations(entries):
        reordered = build_table(permutation)
        for address, prefix in expected.items():
            assert lookup(reordered, address)["prefix"] == prefix
            checks += 1
    assert lookup(build_table(entries[1:]), "192.0.2.1") is None
    checks += 1
    for invalid in [[("10.20.30.7/24", "bad")],
                    [("10.0.0.0/8", "a"), ("10.0.0.0/8", "b")]]:
        try:
            build_table(invalid)
            raise AssertionError("invalid table accepted")
        except ValueError:
            checks += 1
    smaller = build_table([row for row in entries if row[0] != "10.20.30.7/32"])
    fallback = lookup(smaller, "10.20.30.7")
    assert fallback["prefix"] == "10.20.30.0/24"
    checks += 1
    assert lookup([], "10.1.2.3") is None
    checks += 1
    report = {"checks_passed": checks,
              "results": {address: lookup(table, address) for address in expected},
              "after_removing_host_route": fallback,
              "wrong_first_match": str(next(network for network, _ in table
                                            if IPv4Address("10.20.30.7") in network)),
              "boundary": "static IPv4 teaching model; no ECMP, policy routing, metrics or packets"}
    print(json.dumps(report, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B longest_prefix.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。静态 IPv4 单表的最长前缀教学模型,不发送网络包,不读取或改变系统路由;拒绝重复网段,未实现等长路由、metric、ECMP、策略路由、协议选路或硬件转发。

运行后对照结果
{
  "checks_passed": 605,
  "results": {
    "10.20.30.7": {
      "prefix": "10.20.30.7/32",
      "next_hop": "single-host"
    },
    "10.20.30.8": {
      "prefix": "10.20.30.0/24",
      "next_hop": "lab"
    },
    "10.20.9.8": {
      "prefix": "10.20.0.0/16",
      "next_hop": "building"
    },
    "10.9.8.7": {
      "prefix": "10.0.0.0/8",
      "next_hop": "campus"
    },
    "192.0.2.1": {
      "prefix": "0.0.0.0/0",
      "next_hop": "default"
    }
  },
  "after_removing_host_route": {
    "prefix": "10.20.30.0/24",
    "next_hop": "lab"
  },
  "wrong_first_match": "0.0.0.0/0",
  "boundary": "static IPv4 teaching model; no ECMP, policy routing, metrics or packets"
}

它解释的是一次表内查询,不是整台路由器

真实系统可能先根据策略决定查哪张表,也需要由路由协议和管理规则决定哪些候选进入转发表;同样具体的路由还可能比较其他属性或进行多路径转发。本例没有实现这些步骤,所以不能把这个几十行查询器称为完整系统路由算法。

实验也不检查下一站是否可达、接口是否工作、MTU 是否合适,更不测量时延。一次最长前缀匹配成功,只说明在给定表里选到了一个条目;它不能证明真实数据包已经送达。RFC 1812 是 IPv4 路由器的经典规则来源,本文只取其中基本匹配与最长匹配这一层。

理解它之后,排障时该看什么?

当某一小段地址总走不同出口,先找是否存在覆盖它的更具体路由;不要只盯着默认网关。更具体条目一旦撤掉,下一次查询可能落到较宽的条目,这能解释某些局部例外消失后的路径变化。

本篇线性遍历 R 条路由,每次查询要检查至多 R 次,适合把规则写清楚。生产设备可能使用不同数据结构和硬件;这次验证的是选择结果,不是其吞吐量或内部实现。

记住这三点

  • 最长前缀是在已匹配条目里选固定地址位数最多的那个。
  • 默认路由不会压过更具体的匹配,列表先后也不应代替这个规则。
  • 选到路由、真实可达和路径更快,是三件需要分别验证的事。

别把例子用过头

  • 把字符串前缀匹配当作IP网段匹配。
  • 一看见默认路由匹配就返回,漏掉更具体条目。
  • 把单表教学模型当成涵盖策略路由、多路径与协议选路的完整操作系统实现。

自己试一小步

在实验室 /24 里增加一条 /25 例外,会影响哪些目标?

  1. 新增 10.20.30.128/25,给它一个新的下一站标签。
  2. 先在纸上预测 10.20.30.127、10.20.30.128、10.20.30.200 和原有单机地址的查询结果。
  3. 核对边界两侧的选择,并测试只删除 /25 时如何回退。

做到这些,就算完成

  • 解释 /25 的边界来自二进制位,而非字符串长度。
  • 保留 /32 单机例外并考虑它与新条目的关系。
  • 保持本地模型,不执行系统路由修改;本节不提供结果答案。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • RFC 1812:IPv4 下一跳选择 (新标签页)↗IETF / RFC Editor · 英文

    5.2.4.3 中 Basic Match 与 Longest Match 的先后规则,以及其后仍可能有其他筛选。

    2026-10-17 实际打开核对:实际阅读 5.2.4.3 的基本匹配和最长匹配段落;未把经典规范的全部选路步骤等同于本地模型。

  • RFC 4632:CIDR 与最长匹配 (新标签页)↗IETF / RFC Editor · 英文

    CIDR 聚合背景和按最长匹配转发的规则。

    2026-10-17 实际打开核对:5.1 路由通告规则第 1 项,核对更具体地址块与聚合的关系。

  • Python 3.14:ipaddress 网络对象 (新标签页)↗Python Software Foundation · 英文

    IPv4前缀含义、strict主机位校验与地址是否属于网段的容器运算。

    2026-10-17 实际打开核对:Network definitions、IPv4Network(strict=True) 与 Networks as containers of addresses。