跳到正文

IP、子网与路由

环境:Windows 11 x64,GCC 13.1.0(MinGW-w64),gcc -O2 -lws2_32,程序入口 WSAStartup。抓包沿用同一大类的自写 mini-tcpdump(LoadLibraryEx 加载 C:\Windows\System32\Npcap\wpcap.dll)。压测与抓包全部在 127.0.0.1;对外只对本机 DNS 白名单里的 dns.google/8.8.8.8 发了 24 个只读 ICMP 探测包(ping -n 4 × 2、tracert -h 5/-h 3 各一轮)。路由表、接口表、邻居表、注册表全部只读,没有增删任何一条路由、没有改任何配置。所有”内核默认值”都取自 netsh int ipv4 show global / netsh int tcp show global 的本机输出。

一、前缀长度是本机”愿意直接负责”的地址数量

Get-NetIPAddress -AddressFamily IPv4 给出 11 个地址,Get-NetIPInterface 给出各自的 NlMtu。把自己的地址和掩码喂给一段 30 行的 C 代码(p1.exe subnet),算出边界:

Loopback       127.0.0.1/8   mask=255.0.0.0     net=127.0.0.0     bcast=127.255.255.255  总地址=16777216 可用主机=16777214
cherry/Meta    198.18.0.1/30 mask=255.255.255.252 net=198.18.0.0  bcast=198.18.0.3      总地址=4       可用主机=2
以太网         192.168.103.24/24 net=192.168.103.0 bcast=192.168.103.255 总地址=256 可用主机=254
VMnet1         192.168.239.1/24  net=192.168.239.0 bcast=192.168.239.255 总地址=256 可用主机=254
APIPA x6       169.254.74.99/16  net=169.254.0.0   bcast=169.254.255.255 总地址=65536 可用主机=65534

这些算出来的广播地址不是我单方面宣布的——Windows 自己给每个已挂载的前缀插了一条”广播地址/32”主机路由。Get-NetRoute -AddressFamily IPv4 全表统计:

按前缀长度统计路由条数(合计 40)
/0   3      /4   11     /8   1      /24  3      /30  1      /32  21
198.18.0.3/32         -> cherry
127.255.255.255/32    -> Loopback Pseudo-Interface 1
另有 192.168.103.255/32、192.168.137.255/32、192.168.239.255/32
255.255.255.255/32    -> 11 条(每个接口一条受限广播)

对上账了:21 条 /32 = 11 条 255.255.255.255 + 10 条(5 个前缀 × “本机地址 + 广播地址”)。我用掩码算出的 5 个广播地址,内核路由里 5 个全在,一个不差;配了地址但介质断开的 cfw-tap(10.0.0.1/24)没有广播路由,因为它连 /24 前缀路由都没装。

CIDR 的算术只有一条规则要记牢:网络号 = IP & 掩码,广播 = 网络号 | ~掩码,可用主机 = 2^(32-前缀) − 2(/31、/32 例外)。上面每一行都是这条规则的直接代入,没有查表。

二、掩码真正的代价:决定谁需要一次 ARP

把本机网段的掩码改一格试试(同一份 p1.exe subnet):

192.168.103.24/23  net=192.168.102.0  bcast=192.168.103.255  总地址=512
192.168.103.24/24  net=192.168.103.0  bcast=192.168.103.255  总地址=256
192.168.103.24/25  net=192.168.103.0  bcast=192.168.103.127  总地址=128
掩码 /24: .1[on-link] .127[on-link] .128[on-link] .254[on-link]
掩码 /25: .1[on-link] .127[on-link] .128[需网关] .254[需网关]

.128 这条线就是”发广播 ARP 问 MAC”和”交给网关转发”的分界。本机邻居表里能看见这条线的实际负担——Get-NetNeighbor -AddressFamily IPv4 里 192.168.103.x 共 109 条:

低半段 .1-.127 = 52 条    高半段 .128-.254 = 57 条
状态分布:Permanent 1  Reachable 2  Stale 50  Unreachable 56
高半段样本:192.168.103.255/Permanent  .254/Reachable  .252/Stale  .250/Stale  .239/Stale

57 条落在 .128 以上——/24 让这台机器对 254 个地址”人人有责”,其中 56 条现在是 Unreachable(解析过、失败了、还留在表里)。若掩码是 /25,这 57 个地址的解析责任全部转给网关,本机邻居表里不该出现它们。netsh int ipv4 show global 给的对应上限是”邻居缓存限制:每接口 256 项”,109/256 已经用掉 43%。

反例是 APIPA:169.254.0.0/16 意味着 65536 个地址都在链路内,本机 6 张卡各配了一个这样的地址,但 Get-NetRoute -DestinationPrefix '169.254.0.0/16' 返回 0 条——前缀路由根本没装。于是对 169.254.1.1 发起 TCP 连接,栈里立刻给出:

bind=0.0.0.0   connect=169.254.1.1   SO_ERROR=10051 WSAENETUNREACH

三、127.0.0.0/8 整段都是本机:绑定地址矩阵

回环路由是 /8 一条 + 127.0.0.1/32、127.255.255.255/32 两条主机路由。实际能不能连?把监听套接字绑到不同地址,再往不同目的地址连(p1.exe lb,端口 19511,非阻塞 connect + 2s select):

  bind=127.0.0.1 connect=127.0.0.1       select=1 SO_ERROR=0 OK   本机源地址=127.0.0.1 用时=0.58ms
  bind=127.0.0.1 connect=127.1.2.3       select=0 SO_ERROR=0 OK   本机源地址=0.0.0.0   用时=2007.10ms
  bind=127.0.0.1 connect=127.255.255.255 select=0 SO_ERROR=10049 10049 WSAEADDRNOTAVAIL
  bind=0.0.0.0   connect=127.0.0.1       select=1 SO_ERROR=0 OK   本机源地址=127.0.0.1 用时=0.17ms
  bind=0.0.0.0   connect=127.1.2.3       select=1 SO_ERROR=0 OK   本机源地址=127.0.0.1 用时=0.36ms
  bind=0.0.0.0   connect=127.255.255.255 select=0 SO_ERROR=10049 10049 WSAEADDRNOTAVAIL

四件事一次看清:

  1. 整个 /8 都走本机:127.1.2.3 的包不出网卡,wildcard 绑定的监听 socket 照样接得住。
  2. 源地址由路由选,不由目的地址决定:连 127.1.2.3 时本机源地址是 127.0.0.1(接口的主地址),不是目的地址本身。
  3. 精确绑定只服务它自己那个地址:监听在 127.0.0.1 时,发往 127.1.2.3 的 SYN 既没被接受、也没吃到 RST,而是静默等到 2 秒超时(SO_ERROR 仍为 0)。这个”没监听就回 RST,地址不属于我就连理都不理”的差别,是排查”连 127.x.x.x 死活不通”的关键线索。
  4. 子网广播地址不能当连接目的:127.255.255.255 直接 WSAEADDRNOTAVAIL。

最长前缀匹配最硬的一条证据是设备级的:ping -n 20 -l 1472 127.0.0.1 期间,在回环设备和 Realtek PCIe 真网卡上同时开两个抓包(filter=icmp):

真网卡 \Device\NPF_{9A4AD1D4-...} (datalink=1):captured 0 packets
回环   \Device\NPF_Loopback        (datalink=0):12 条,全部 ttl=128 totlen=1500

目的地址 127.0.0.1 同时匹配 127.0.0.0/8 和三条 0.0.0.0/0,0 个字节的本机流量漏到了物理网卡上。(12 条而不是 40 条,是因为 ping -n 20 一秒才发一条,7 秒的抓包窗口只覆盖到 6 个往返;totlen=1500 = 1472 + 8(ICMP) + 20(IP),回环 NlMtu=4294967295 所以不碎片化。)

四、路由选择:三条默认路由 + 一个本地代理

Get-NetRoute 里的三条 0.0.0.0/0:

  0  0.0.0.0/0  10.0.0.0          ifMetric=1   cfw-tap      (介质断开)
  0  0.0.0.0/0  192.168.103.254   ifMetric=25  以太网
  0  0.0.0.0/0  198.18.0.2        ifMetric=    cherry

标准说法是”前缀最长者胜,同级再比度量”。本机的实测结果要多一层:同一个目的地址,走哪条路取决于它是被解析出来的还是被写死的。

ping -n 4 8.8.8.8    → 真网卡抓到:192.168.103.24 -> 8.8.8.8  ttl=128 type=8,回包 ttl=107
ping -n 4 dns.google → 解析到 198.18.2.156,RTT 0–2ms、回包 TTL=128
tracert dns.google   → 出在 cherry 设备上:198.18.0.1 -> 198.18.2.156(第六节细看)

8.8.8.8 这条走的是物理网关路径,实测 4 个包往返 171–174ms,回包 TTL=107(本机出方向默认 TTL=128,netsh 里”默认跃点限制 : 128 个跃点”;128−107 = 21 跳)。而 dns.google 被解析进 198.18.0.0/15——RFC 2544 的基准测试地址段,是 fake-IP 代理的专用空间:Resolve-DnsName 返回的这条记录 TTL=1(故意不让缓存),ping 它的 RTT 是 0–2ms、回包 TTL=128。同一台服务器,两个数字。

顺带一个包长上限的对照:ping -l 65500 127.0.0.1 能通(65500+28=65528 ≤ 65535),-l 65508 被工具自己拒绝”有效范围从 0 到 65500”;而协议栈的真实上限要用 UDP 才量得准——sendto 发 65507 成功、65508 报 10040 WSAEMSGSIZE(65508+20+8 = 65536,正好越过 IPv4 总长度字段的天花板 65535)。工具限制和栈限制不是一回事。

五、ICMP 差错:本机生成它、消费它的完整数量关系

在 127.0.0.1 上找一个没人监听的端口发 UDP,然后抓 icmp。原包和差错包的字段一一对上(totlen 列取自同一份抓包):

载荷=8    UDP totlen=36  id=7f2e → ICMP type=3 code=3 totlen=64  id=7f2f   (+20µs)
载荷=20   ICMP totlen=76
载荷=40   UDP totlen=68  id=7f3b → ICMP totlen=96  id=7f3c   (+25µs)
载荷=100  UDP totlen=128 id=7f4f → ICMP totlen=156 id=7f50   (+21µs)
载荷=600  ICMP totlen=576
载荷=1400 ICMP totlen=576
载荷=4000 ICMP totlen=576

读出三条规律:

  • totlen = 56 + 载荷,到 576 封顶。56 = 20(外层 IP) + 8(ICMP 头) + 20(被回带的原 IP 头) + 8(原 UDP 头)。封顶在 576 说明内核只回带”装得下的部分”:576−56 = 520 字节原始载荷被抄进差错报文里。这个 576 和 netsh int ipv4 show global 里的”最小 Mtu : 576”是同一个数——标准建议差错报文不要超过最小重组 MTU,本机真的照做了。
  • 差错包和原包共用一个 IP ID 计数器(7f2e→7f2f、7f3b→7f3c、7f4f→7f50 全是 +1)。同一台机器同一接口上所有流量共享这条序列,所以 IP ID 是一个可被观察的”包序号泄漏”;注意 netsh 里”随机化标识符 : enabled”,但回环路径上实测是严格 +1,开关名和实际行为不要望文生义。
  • 生成延迟 20–25 微秒(回环,抓包时钟直接给出)。

消费侧(Windows 的 UDP 会把 ICMP 差错变成 socket 错误):

场景实测
未 connect 的 socketrecvfrom 返回 10054 WSAECONNRESET,0.1ms 内可见
connect 过的 socket同样 10054,两种绑定行为一致
连续 6 轮”发一个→收一个”6/6 轮都拿到 10054,没有”错误位粘住要两次成功收包才清”的现象
载荷 8–4000 七种长度七次全部拿到 10054,差错生成不受载荷长度影响

最有冲击力的是配额。同一台机器、同一个关闭端口,只改突发规模:

突发   30 个 40 字节 UDP 包(0.56ms 发完)-> ICMP type=3 code=3:30 条,socket 侧 10054:30 次
突发  100 个(2.22ms)-> 100 条 / 100 次
突发  200 个(3.28ms)-> 105 条 / 105 次
突发  400 个(4 个 socket 各 100,6.44ms)-> 每个 socket 恰好 25 条,合计 100 条
突发 1000 个(13.98ms)-> 194 条 / 194 次

100 个以内一条不少,200 个只回来一半,1000 个只回来 194 条(19%),而且四个 socket 是均分这份配额的(各 25 条)——限额是全局的,不是每 socket 一份。抓包计数和 socket 侧 10054 计数每次都完全相等,说明少的不是应用取走了,是内核压根没生成。这就是”丢包/不可达”信号在本机不可按比例采信的量化证据,也是为什么 ICMP 差错不能当拥塞信号用:超配额的那部分被静默吞掉了。

六、traceroute 的原理,以及它在本机为什么塌成 1 跳

原理是标准规定的两句话:TTL 减到 0 的路由器丢弃包并回 ICMP type=11 code=0;目的主机收到 echo 就回 type=0。tracert -d -h 5 8.8.8.8 在本机的输出和抓包放在一起看:

  1     *      170 ms   169 ms  8.8.8.8

抓包(Meta Tunnel,datalink=12,filter=icmp):
[        0] 198.18.0.1 -> 8.8.8.8 ttl=1  ICMP type=8 code=0 id=1 seq=54956 totlen=92
[   171606] 8.8.8.8 -> 198.18.0.1 ttl=107 ICMP type=0 code=0 id=1 seq=54956
[   119646] 198.18.0.1 -> 8.8.8.8 ttl=1  ICMP type=8 code=0 id=1 seq=54957
整轮抓包统计:type=8 共 6 条,type=0 共 6 条,type=11 共 0 条

三个 ttl=1 的请求全部拿到了 echo 应答(type=0),一条 Time Exceeded 都没有。真实 IPv4 路由上,TTL=1 的包必须在第一跳被丢掉——这里没有,说明第一跳不是在做逐跳转发,而是终结/重写了 TTL 的代理。tracert 因此认为”第 1 跳就是目的地”,直接停掉了(它只发了 TTL=1 的三组探测,没有 TTL=2..5)。

对 fake-IP 地址那组更露骨:

[   596502] 198.18.0.1 -> 198.18.2.156 ttl=1 ICMP type=8 id=de9e seq=54959
[   597707] 198.18.2.156 -> 198.18.0.1 ttl=1 ICMP type=0 id=de9e seq=54959   (+1.2ms)

回包的 TTL 和 IP ID 与请求完全相同(真回包是 ttl=107、id=0000),往返 1.2ms——这是本地合成应答的指纹,不是远端主机。(tracert 打给 8.8.8.8 的第一个探测显示 *,但抓包里那条 seq 的回包确实在 171.6ms 时回来了;工具判定和线上事实不一致,这也是”以抓包为准”的理由。)

结论可直接拿去用:看 TTL 只能判断”这一跳之后还剩几跳”,不能证明中间真的过了几跳。 判据是交叉的——回包 TTL(107)、RTT(171ms)、DNS 返回地址是否落在 198.18.0.0/15、以及 type=11 是否存在。本机这套判据一致指向:物理网关路径真实,fake-IP 路径由本地代理终结(《分层模型与抓包入门》第五节用 HTTPS 流量验过同一件事,这里用 ICMP 再验一次)。

七、小结

  • 掩码算出来的东西内核会替你确认一遍:本机 40 条 IPv4 路由 = 3×/0 + 11×/4 + 1×/8 + 3×/24 + 1×/30 + 21×/32,其中 5 个子网广播地址(127.255.255.255、192.168.103.255、192.168.137.255、192.168.239.255、198.18.0.3)与 IP & ~掩码 手算结果逐条吻合;11 条 255.255.255.255/32 对应 11 个接口。
  • /24 让本机对 254 个地址负责:邻居表里 192.168.103.x 有 109 条(低半段 52 / 高半段 57,Unreachable 56),上限是内核的每接口 256 项。掩码换成 /25,高半段那 57 条的解析责任就归网关。
  • 127.0.0.0/8 整段在本机内部:wildcard 监听能接住 127.1.2.3,源地址被选为 127.0.0.1;绑 127.0.0.1 的监听对发往 127.1.2.3 的 SYN 不回 RST 也不响应,静默到超时;127.255.255.255 报 10049、169.254.1.1 报 10051(169.254/16 无路由)。20 个回环 ping 在物理网卡上是 0 条,最长前缀匹配到此有硬证据。
  • ICMP 差错在本机是限量供应的:突发 30/100 个包一条不少,200 个 → 105 条,1000 个 → 194 条(19%),400 个分给四个 socket 时各得 25 条——配额全局共享;差错报文定长上限 576 字节(= 最小 MTU),回带 20+8+520 字节原报文;生成延迟 20–25µs;IP ID 与原包 +1 连续(同一计数器,“随机化标识符 enabled”并未生效于这条路径)。
  • 大小上限要分清工具和栈:ping -l 最大 65500(工具限制),UDP sendto 最大载荷 65507、65508 报 10040(栈限制,65535 总长)。
  • traceroute 在本机不可用:ttl=1 探测拿到 type=0、type=11 计数为 0,fake-IP 回包复制了请求的 IP ID 与 TTL。真实公网参照系是 8.8.8.8:RTT 171–174ms、回包 TTL=107(出方向默认 128 → 约 21 跳)。
  • 本节未在本机实测:多跳真实链路上的 TTL 逐级递减过程(本机唯一可用出口被本地代理终结,无法观测到任何 type=11);IP 碎片化与重组(回环 NlMtu=4294967295、以太网 9000,本机可发包上限 65507 触发不了碎片化,只有内核计数器”分段数 : 1”和”重组限制 535521728 字节”这两个只读值);IPv4 选项(record-route 等,未发过带选项的包)。