bigywater.tech
网络协议 · 16

路径 MTU 发现:DF 位、ICMP 与“黑洞”

为什么 TCP 的包都带 DF?路上有链路比 1500 小,发送方怎么知道?ICMP 被防火墙拦掉会怎么样?这篇用 DF 置位的探测包在真实公网路径上找到一条 MTU 1492 的链路,再看内核有没有记住它,最后用推演说明“黑洞”。

1492
真实公网路径上第 3 跳路由器在 ICMP 3/4 里告知的下一跳 MTU(本机网口是 1500)
9 / 9
1500 字节的 DF 探测全部收到 3/4;1492 字节及以下的全部到达目的地
EMSGSIZE
本机对设了 DF 又超过接口 MTU 的 UDP 数据报直接拒绝,线上 0 个包

DF 的作用

实测 · 本机执行输出
  DF=置位 载荷 1472 字节(IP 总长 1500):发送成功;线上抓到 1 个 IP 包,IP 总长 [1500],DF 位 [1]
  DF=置位 载荷 1473 字节(IP 总长 1501):OSError: [Errno 40] Message too long;线上抓到 0 个 IP 包
  DF=置位 载荷 4000 字节(IP 总长 4028):OSError: [Errno 40] Message too long;线上抓到 0 个 IP 包
  DF=不置位 载荷 1473 字节(IP 总长 1501):发送成功;线上抓到 2 个 IP 包,IP 总长 [1500, 21],DF 位 [0, 0]
  DF=不置位 载荷 4000 字节(IP 总长 4028):发送成功;线上抓到 3 个 IP 包,IP 总长 [1500, 1500, 1068],DF 位 [0, 0, 0]
  lo0(MTU 16384)DF 置位发 20000 字节 UDP:OSError: [Errno 40] Message too long
  UDP(本机发出)                 DF 位 = [0]
  ICMP echo 请求(ping 默认)     DF 位 = [0]
  ICMP echo 请求(ping -D)     DF 位 = [1]
  TCP(本机发出)                 DF 位 = [1]
  net.inet.tcp.path_mtu_discovery: 1
  net.inet.tcp.pmtud_blackhole_detection: 1
  net.inet.tcp.pmtud_blackhole_mss: 1200
  net.inet.tcp.mssdflt: 512
  route get 默认路由:接口 en0,MTU = 1500
  route get 127.0.0.1:接口 lo0,MTU = 16384
  route get 公网某主机:接口 en0,MTU = 1500
  TCP 默认带 DF(靠 PMTUD 调整段长);UDP 和 ping 默认不带,放不下就被分片;ping -D 加上 DF

DF 置位的包放不下就不能分片,路由器丢掉它并回 ICMP 3/4,里面带着自己出口的 MTU。TCP 默认带 DF,UDP 与 ping 默认不带;设了 DF 又超过本机接口 MTU 的数据报在本机就报 Message too long。

在公网上探测

实测 · 本机执行输出
  共发出 72 个探测;抓到的发出包 DF 位全部置位:True
    IP 总长   主机 A          主机 B          主机 C        (格式:端口不可达个数 + 需要分片个数 / 发出个数)
      576     3+0/3       3+0/3       3+0/3
     1000     3+0/3       3+0/3       3+0/3
     1280     3+0/3       3+0/3       3+0/3
     1400     3+0/3       3+0/3       3+0/3
     1472     3+0/3       3+0/3       3+0/3
     1480     3+0/3       3+0/3       3+0/3
     1492     3+0/3       3+0/3       3+0/3
     1500     0+3/3       0+3/3       0+3/3
  收到的 3/4 报文里的“下一跳 MTU”字段:[1492];其中 IP 总长 > 1492 的探测(1500)收到了 3/4,≤ 1492 的收到的是端口不可达
  3/4 报文来自:['私有地址'];引用的原包总长 [1500],引用的原包 TTL [62];回复包自己的 TTL [62]

用不同大小的 DF 探测发往三个公网主机,≤1492 的全部到达目的地,1500 的全部收到 3/4,下一跳 MTU 字段是 1492。

窄链路在第几跳

实测 · 本机执行输出
  TTL= 1:收到 3/3 个回应:超时 11/0 × 3(来自默认网关)
  TTL= 2:收到 3/3 个回应:超时 11/0 × 3(来自私有地址的路由器)
  TTL= 3:收到 3/3 个回应:超时 11/0 × 3(来自私有地址的路由器)
  TTL= 4:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL= 5:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL= 6:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL= 7:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL= 8:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL= 9:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  TTL=10:收到 3/3 个回应:需要分片 3/4,下一跳 MTU 1492 × 3(来自私有地址的路由器)
  从 TTL=4 起一直收到 3/4:TTL=3 的包在第 3 跳的路由器处超时,TTL≥4 的包要由第 3 跳的路由器转发给第 4 跳,它的出口链路放不下 1500 字节,于是丢掉并回 ICMP 3/4(下一跳 MTU 1492)。验证:回 3/4 的设备与 TTL=3 回超时的是同一个地址:True;回复包 TTL 分别是 [62] 与 [62]

让 TTL 从 1 递增,TTL=4 起开始收到 3/4,而且回包设备就是 TTL=3 回超时的那一跳:窄链路在第 3 跳通往第 4 跳的出口。

内核的反应

实测 · 本机执行输出
  前面第二、三节的探测已经让内核收到过很多个 3/4。现在 route get 公网主机的 MTU = 1500(收到 3/4 之前是 1500)
  再对同一目的地发 DF=1、IP 总长 1500 的 UDP:发送成功
  再对同一目的地发 DF=1、IP 总长 1492 的 UDP:发送成功
  再对同一目的地发 DF=1、IP 总长 1480 的 UDP:发送成功
  netstat -rn 里该主机的路由条目:

实验里 UDP 套接字收到很多 3/4 以后,route get 里这台主机的 MTU 仍是 1500,再发 1500 字节的 DF 包仍然“发送成功”。TCP 的行为这里没有验证。

MSS 通告

实测 · 本机执行输出
  主机 A:本机 SYN 通告 MSS 1460;对方 SYN-ACK 通告 MSS = 1400,对应 MTU ≈ 1440
  主机 B:本机 SYN 通告 MSS 1460;对方 SYN-ACK 通告 MSS = 1400,对应 MTU ≈ 1440
  主机 C:本机 SYN 通告 MSS 1460;对方 SYN-ACK 通告 MSS = 1452,对应 MTU ≈ 1492
  主机 D:没有连通(没抓到 SYN-ACK)
  主机 E:本机 SYN 通告 MSS 1460;对方 SYN-ACK 通告 MSS = 1452,对应 MTU ≈ 1492
  MSS 是对方(或路径上的中间设备)按自己的接口 MTU 写进去的上限,不是整条路径的 MTU;路径上更窄的链路只能靠 DF + ICMP 3/4(或黑洞检测)发现

对方在 SYN-ACK 里通告的 MSS(1400 或 1452)反映的是对方一侧的限制,不是整条路径的最小 MTU。

黑洞

实测 · 本机执行输出
  ICMP 能回到发送方:1500 → 路由器回 ICMP 3/4(下一跳 MTU 1400);1400 → 送达 ⇒ 成功,发了 2 个包
  ICMP 被防火墙拦住(“黑洞”):1500 → 被静默丢弃;1500 → 被静默丢弃;1500 → 被静默丢弃;1500 → 被静默丢弃;1500 → 被静默丢弃 …共试 12 次 ⇒ 一直失败(12 次后放弃),发了 12 个包
  ICMP 被拦,但发送方在连续 3 次无应答后退到 1200:1500 → 被静默丢弃;1500 → 被静默丢弃;1500 → 被静默丢弃;1200 → 送达 ⇒ 成功,发了 4 个包
  上面是按规范写出来的纸面推演:macOS 的 pmtud_blackhole_detection 实际如何退避、阈值多少,这里没有实测

如果 ICMP 3/4 被拦,带 DF 的大包会被静默丢弃,而小包照常通。模拟了三种情形:ICMP 可达时 2 个包找到 1400;ICMP 被拦则一直失败;若发送方连续无应答后退到 1200 则第 4 个包送达(这一项是推演,不是 macOS 的实测)。

分步演示

路径 MTU 发现
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一节在 en0(MTU 1500)上抓包并读本机 sysctl;第二、三节是三个公网主机与一条真实公网路径上的 DF 探测(UDP 到没人监听的端口),路径上设备的地址不写出,只记类别;第四、五节是同一台机器上的观察;第六节是纸面模拟,不是实测
    • 没有验证:TCP 收到 3/4 后是否更新路由缓存与降低 MSS;macOS 的黑洞检测(pmtud_blackhole_detection)的触发条件、退避次数;PLPMTUD(RFC 4821)的探测机制;IPv6 的 PMTUD(路由器不分片,只有 Packet Too Big);ICMP 被拦时真实网络上的黑洞表现;路径上第 3 跳之后还有没有更窄的链路
    京ICP备2026067502号-1京公网安备11010502063487号