路由器和主机怎么告诉你“送不到”?ICMP 差错报文里放了什么?为什么 UDP 套接字有时收不到错误,TCP 被拒绝和被丢弃的体验差别这么大?这篇向 12 个不同的目的地发探测,抓下回来的每一个 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 差错本身再回差错
发往子网广播地址的包没有任何设备回差错,发往单个主机的则会回。这避免一个广播引来整网回复。