bigywater.tech
网络协议 · 15

ICMP 差错报文:目的不可达、超时、需要分片

路由器和主机怎么告诉你“送不到”?ICMP 差错报文里放了什么?为什么 UDP 套接字有时收不到错误,TCP 被拒绝和被丢弃的体验差别这么大?这篇向 12 个不同的目的地发探测,抓下回来的每一个 ICMP 差错报文,逐字节核对它们引用的原包。

28 / 40 字节
不同设备在 ICMP 差错里引用的原包长度:最少是 IP 头加 8 字节
1005 ms 对 3001 ms
connect 被网关 RST 拒绝与被防火墙丢弃(无任何回应)各用的时间
4 / 12
往 12 个目的地发 UDP 探测,实际收到 ICMP 差错报文的目的地个数

端口不可达

实测 · 本机执行输出
  抓到 2 个 UDP 包和 2 个 ICMP 差错报文
  ICMP 类型 3(目的不可达)代码 3(端口不可达),IP 总长 56 = 20 + 8 + 引用 28 字节;来自 127.0.0.1,引用的原包:目的 127.0.0.1 协议 17(17 = UDP)源端口 53211 目的端口 53932
  ICMP 类型 3(目的不可达)代码 3(端口不可达),IP 总长 56 = 20 + 8 + 引用 28 字节;来自 127.0.0.1,引用的原包:目的 127.0.0.1 协议 17(17 = UDP)源端口 49222 目的端口 53932
  引用的 28 字节(IP 头 20 字节 + UDP 头 8 字节)与发出的原包前 28 字节比较(原包共 45 字节,载荷 17 字节没有被引用):前 26 字节逐字节相同:True;最后 2 个字节是 UDP 校验和字段,原包里抓到 0xfe2c,引用里是 0x0000
  应用这一侧:未 connect 的 UDP 套接字 sendto 之后 recvfrom → 没有任何错误(TimeoutError:超时);已 connect 的 UDP 套接字 send 之后 recv → ConnectionRefusedError: [Errno 61] Connection refused
  所以 UDP 想知道对方端口是否关闭,必须 connect 后读错误;ICMP 差错里引用了原包头,内核靠引用的 四元组 找到是哪个套接字

UDP 发到没人监听的端口,对方回 ICMP 类型 3 代码 3,里面引用原包的 IP 头和 UDP 头。只有 connect 过的 UDP 套接字能把它变成 ECONNREFUSED;没 connect 的套接字什么也不知道。

拒绝与丢弃

实测 · 本机执行输出
  lo0 上 connect 一个没人监听的 TCP 端口:ConnectionRefusedError: [Errno 61] Connection refused,用时 0.27 ms;抓到 2 个包:
  +   0.000 ms  IP 127.0.0.1.C1 > 127.0.0.1.C2: Flags [S], seq 2373677387, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val
  +   0.073 ms  IP 127.0.0.1.C2 > 127.0.0.1.C1: Flags [R.], seq 0, ack 2373677388, win 0, length 0
  其中 ICMP 包 0 个——TCP 的拒绝是 RST,不是 ICMP
  默认网关 81 端口的 connect 在线上是这样的(en0,地址已遮盖):
  +   0.000 ms  IP <本机局域网IP>.49279 > <本机局域网IP>.81: Flags [SEW], seq 2125921335, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 661024073 ecr 0
  +   3.023 ms  IP <本机局域网IP>.81 > <本机局域网IP>.49279: Flags [R.], seq 0, ack 2125921336, win 0, length 0
  +1001.255 ms  IP <本机局域网IP>.49279 > <本机局域网IP>.81: Flags [S], seq 2125921335, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 661025074 ecr 0,s
  +1003.819 ms  IP <本机局域网IP>.81 > <本机局域网IP>.49279: Flags [R.], seq 0, ack 1, win 0, length 0
  第一个 SYN 带 ECN 标志(SEW),网关用 RST 拒绝;1 秒后 macOS 又发一个 SYN,这次 ECN 标志去掉了(S),再被 RST 拒绝,connect 才报 Connection refused——所以被拒绝用了约 1 秒;重发是不是 ECN 协商失败后的回退,没有进一步验证
  公网主机 81 端口:3 秒内共发出 3 个 SYN(+ / +1000.982 / +2002.020 ms),收到该主机的包 0 个——没有任何回应(被防火墙丢弃),connect 一直等到超时
  默认网关的 81 端口:3 次 connect 结果「ConnectionRefusedError: [Errno 61] Connection refused」,用时 1005 / 1005 / 1005 ms
  公网某主机的 81 端口:3 次 connect 结果「TimeoutError」,用时 3001 / 3001 / 3001 ms
  公网某主机的 80 端口(对照:有服务):3 次 connect 结果「连上了」,用时 237 / 192 / 180 ms

TCP 的拒绝是 RST,不用 ICMP。lo0 上 0.27 ms 就失败;网关的 RST 配合一次 SYN 重发共 1005 ms;防火墙丢弃则一直等到 3001 ms 超时,3 个 SYN 没有任何回应。

谁会回

实测 · 本机执行输出
  默认网关                    → 没有收到任何 ICMP 差错
  远端私有地址 10.x             → 类型 3 代码 1(主机不可达),来自本机默认网关,IP 总长 68(引用 40 字节)
  私有地址 172.31.x           → 没有收到任何 ICMP 差错
  运营商级 NAT 地址段 100.64.x   → 没有收到任何 ICMP 差错
  链路本地 169.254.x          → 没有收到任何 ICMP 差错
  保留地址 240.0.0.1          → 没有收到任何 ICMP 差错
  文档地址 198.51.100.7       → 没有收到任何 ICMP 差错
  文档地址 203.0.113.9        → 没有收到任何 ICMP 差错
  文档地址 192.0.2.1          → 没有收到任何 ICMP 差错
  公网 DNS 服务器 A            → 类型 3 代码 3(端口不可达),来自目的地自己,IP 总长 56(引用 28 字节)
  公网 DNS 服务器 B            → 类型 3 代码 3(端口不可达),来自目的地自己,IP 总长 68(引用 40 字节)
  公网 Web 主机               → 类型 3 代码 3(端口不可达),来自目的地自己,IP 总长 68(引用 40 字节)
  默认网关再单独试:间隔 1 s 连发 10 个到 33435 端口,收到 ICMP 差错 7 个(类型/代码:[(3, 3)])——网关不是每次都回
  12 个目的地里 4 个收到了差错报文;出现过的 (类型, 代码):[(3, 1), (3, 3)]

12 个目的地里只有 4 个回了差错:远端私有地址由网关回主机不可达,公网主机自己回端口不可达。其余的什么也没回,发送方无法区分原因;网关自己也只回了 10 个里的 7 个。

引用多少

实测 · 本机执行输出
  TTL=2  的探测:Time Exceeded 的 IP 总长 68,引用 40 字节;引用的 IP 头(版本/长度/标识/协议/源/目的)与原包相同:True;引用的 UDP 头(端口、长度)和载荷与原包相同:True
  TTL=3  的探测:Time Exceeded 的 IP 总长 68,引用 40 字节;引用的 IP 头(版本/长度/标识/协议/源/目的)与原包相同:True;引用的 UDP 头(端口、长度)和载荷与原包相同:True
  TTL=4  的探测:Time Exceeded 的 IP 总长 56,引用 28 字节;引用的 IP 头(版本/长度/标识/协议/源/目的)与原包相同:True;引用的 UDP 头(端口、长度)与原包相同:True
  TTL=10 的探测:Time Exceeded 的 IP 总长 56,引用 28 字节;引用的 IP 头(版本/长度/标识/协议/源/目的)与原包相同:True;引用的 UDP 头(端口、长度)与原包相同:True
  TTL=11 的探测:Time Exceeded 的 IP 总长 56,引用 28 字节;引用的 IP 头(版本/长度/标识/协议/源/目的)与原包相同:True;引用的 UDP 头(端口、长度)与原包相同:True
  各种 (类型, 引用字节数) 出现次数:{(11, 40): 2, (11, 28): 3}
  规范要求至少引用原包的 IP 头 + 8 字节(= 28 字节);更多的话接收方能拿到更多信息
  对照第一节:本机内核回端口不可达引用了 28 字节;第三节里三台公网主机引用的分别是 [28, 40, 40] 字节

规范只要求引用 IP 头加 8 字节。实测路由器有的引用 28 字节,有的引用整个 40 字节原包;引用里的端口让发送方(如 traceroute、内核)对应到具体探测或套接字。

不对广播回

实测 · 本机执行输出
  发 5 个到子网广播地址的 33435 端口:收到 ICMP 差错 0 个,来自 0 台设备
  发 5 个到默认网关(单播)的 33435 端口:收到 ICMP 差错 3 个,来自 1 台设备
  规范(RFC 1122 / 1812):不对发给广播 / 组播地址的数据报回 ICMP 差错,否则一个广播会引来整网的回复(广播风暴);也不对 ICMP 差错本身再回差错

发往子网广播地址的包没有任何设备回差错,发往单个主机的则会回。这避免一个广播引来整网回复。

分步演示

ICMP 差错
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一、二节的 lo0 部分在本机回环上抓包,其余在 en0 上抓包(dpkt 解析 ICMP 差错与引用的原包);默认网关与 MAC 做了遮盖,公网主机的地址不写出;网关 / 路由器是否回差错、回哪种每次运行并不完全一样(同一目的地不同轮次结果不同),文中为最后一次运行
    • 没有验证:协议不可达(代码 2)、网络不可达(代码 0)、被管理禁止(代码 9 / 10 / 13)这些代码(本机网络没有触发);ICMP 重定向与源抑制;IPv6 的 ICMPv6 差错(见 IPv6 部分);ICMP 差错限速的具体阈值(见 ICMP 限速一篇);网关重发 SYN 是否因为 ECN 回退
    京ICP备2026067502号-1京公网安备11010502063487号