bigywater.tech
网络协议 · 11

路由表与最长前缀匹配:看懂 netstat -rn 与默认路由

数据包要往哪发,是路由表说了算。netstat -rn 里那一串 flags 和 link#14 是什么意思?为什么发往公网的包走默认路由、发往邻居的包不经网关?“最长前缀匹配”到底怎么匹配?路由表很大的时候怎么查才快?这篇用本机的真实路由表和 route get 一项项验证。

11 / 12
自己写的最长前缀匹配与内核 route get 选出的接口一致的个数(唯一例外是本机自己的地址)
1281 µs → 0.9 µs
2 万条路由时逐条扫描与分桶哈希每次查表的耗时
link#14
Gateway 栏里表示直连:下一跳就是目的地本身

路由表的列

实测 · 本机执行输出
  Destination        Gateway            Flags               Netif Expire
  default            <本机局域网IP>       UGScg                 en0       
  127                127.0.0.1          UCS                   lo0       
  127.0.0.1          127.0.0.1          UH                    lo0       
  169.254            link#14            UCS                   en0      !
  <本机局域网IP>/22       link#14            UCS                   en0      !
  224.0.0/4          link#14            UmCS                  en0      !
  255.255.255.255/32 link#14            UCS                   en0      !
  出现过的 flags 字母含义: U=路由可用;G=下一跳是网关;H=主机路由(/32);S=静态(手动 / 系统添加);C=可克隆:用到时为每个目的地克隆一条主机路由;c=该路由是克隆出来的;g=默认路由标志(网关路由);L=带链路层地址(ARP 邻居);W=由克隆产生;I=绑定到特定接口(ifscope);i=地址已解析完成;m=组播;b=广播

目的地、网关、标志、出口接口、寿命。网关栏写 link#N 表示直连,写地址表示经这个网关转发;U / G / H / S / C / L 等标志说明路由的性质。

内核怎么选

实测 · 本机执行输出
  回环        目的地 127.0.0.1      → 匹配的路由 127.0.0.1/?  网关 (直连)  接口 lo0  标志 <UP,HOST,DONE,LOCAL>
  本机自己的 en0 地址目的地 <本机局域网IP>      → 匹配的路由 <本机局域网IP>/?  网关 (直连)  接口 lo0  标志 <UP,HOST,DONE,LLINFO,WASCLONED,LOCAL,IFSCOPE,IFREF>
  同网段的邻居    目的地 <本机局域网IP>      → 匹配的路由 <本机局域网IP>/255.255.252.0  网关 (直连)  接口 en0  标志 <UP,DONE,CLONING,STATIC>
  公网地址      目的地 8.8.8.8        → 匹配的路由 default/default  网关 <本机局域网IP>  接口 en0  标志 <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
  组播        目的地 224.0.0.1      → 匹配的路由 224.0.0.0/240.0.0.0  网关 (直连)  接口 en0  标志 <UP,DONE,CLONING,STATIC,MULTICAST>
  链路本地      目的地 169.254.1.1    → 匹配的路由 169.254.0.0/255.255.0.0  网关 (直连)  接口 en0  标志 <UP,DONE,CLONING,STATIC>

route get 给出内核对某个目的地选中的那条路由:本机地址走 lo0,同网段直连,公网走 default 经网关,组播和链路本地走各自的网段路由。

最长前缀匹配

实测 · 本机执行输出
  127.0.0.1        我的 LPM: /32 接口 lo0   内核: 接口 lo0   一致
  <本机局域网IP>        我的 LPM: /32 接口 en0   内核: 接口 lo0   不一致
  <本机局域网IP>        我的 LPM: /32 接口 en0   内核: 接口 en0   一致
  <本机局域网IP>        我的 LPM: /32 接口 en0   内核: 接口 en0   一致
  8.8.8.8          我的 LPM: /0 接口 en0   内核: 接口 en0   一致
  1.1.1.1          我的 LPM: /0 接口 en0   内核: 接口 en0   一致
  224.0.0.251      我的 LPM: /32 接口 en0   内核: 接口 en0   一致
  255.255.255.255  我的 LPM: /32 接口 en0   内核: 接口 en0   一致
  169.254.100.1    我的 LPM: /16 接口 en0   内核: 接口 en0   一致
  10.0.0.1         我的 LPM: /0 接口 en0   内核: 接口 en0   一致
  172.16.0.1       我的 LPM: /0 接口 en0   内核: 接口 en0   一致
  203.0.113.9      我的 LPM: /0 接口 en0   内核: 接口 en0   一致
  netstat -rn 里有没有本机自己 en0 地址的那条路由: 有(route get 却能查到一条标志含 LOCAL、出口 lo0 的主机路由)
  12 个目的地里 11 个一致。多条路由同时匹配时,总是前缀最长的赢——例如本机自己 en0 的地址同时匹配 default(/0)、局域网(/22)、主机路由 /32,选的是 /32
  路由表里一共有 206 条路由,前缀分布 [0, 4, 8, 16, 22, 32]

所有匹配的路由里取前缀最长的;default 是 /0,是没有更具体路由时的兜底。实测我的实现与内核 11/12 一致,例外的是本机地址。

怎么查得快

实测 · 本机执行输出
  逐条扫描:5000 次查询 6406 ms(每次 1281 µs);按前缀长度分桶 + 哈希:同样 5000 次 4 ms(每次 0.9 µs),两种方法结果一致
  分桶哈希对 5 万次查询 43 ms;最多只要比较 8 个前缀长度,与路由条数无关——真实路由器用 trie 或硬件 TCAM,思路是一样的:不要逐条比

逐条扫描随表增大线性变慢;按前缀长度分桶加哈希,比较次数只取决于不同前缀长度的个数。

分步演示

路由表
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。路由表通过 sysd.py 读 netstat -rn 与 route -n get,局域网地址与 MAC 做了遮盖;第四节是 Python 单线程基准,表为随机生成的 2 万条路由,只看相对大小;内核 route get 与自己实现的比较仅用了 12 个目的地
    • 没有验证:多条默认路由 / 接口作用域(ifscope)路由的选择;策略路由与多路径(ECMP);路由缓存失效;IPv6 路由表(netstat -rn -f inet6);内核里路由表真正使用的数据结构(基数树)
    京ICP备2026067502号-1京公网安备11010502063487号