真实世界中的 PMTUD

上回书说到被 MTU 问题小小坑了一下,问题最后解决了,但是留了一个疑问点没有证实:为什么在 MSS 协商失败的情况下,curl https://x.com 可以,但是 curl https://accounts.google.com 不可以?

本文的实验代码都是在虚拟机中做的,所以没有隐藏 IP,直接粘贴的 tcpdump 结果。代码太宽,可以通过代码块右上角的工具栏配合阅读,比如点击 <-> 按钮来展开,或者在新窗口浏览。读本文之前,最好先读一下这篇介绍 MTU 介绍的比较好的博客:有关 MTU 和 MSS 的一切 (即本博客)。

上文中的猜想是这些网站实现了 PMTUD,这一点比较容易证明。

PMTUD 测试

TCP 握手的时候双方协商 MSS,是根据本地的网卡信息协商的。比如网卡的 MTU 是 1500,那么 MS S 就会是 1460,如果网卡 MTU 是 1450,那么 MSS 就是 1410. 这个过程,TCP 的双方都对中间网络设备的 MTU 没有概念,中间设备能转发的 MTU 很可能比两边都小(尤其是在有 VPN 或者有隧道的情况)。PMTUD 就是处理这种情况的:它的原理很简单,当有丢包的时候,我尝试发送小包,看能不能收到 ACK,如果能,说明链路 path 的 MTU 比我想的要小,等用小一点的包发送。PMTUD 的全称是 Path MTU Discovery。

验证方法很简单,我们只要创造一个环境,假设这个环境能接受的 MTU 最大是 800,超过 800 bytes 的都会直接丢包,并且不会发回去 ICMP 消息。

我们用 iptables 直接 DROP 掉超过 800 bytes 的包。实验环境我习惯将 DROP 打印出来。

然后,我们还要将 Generic Receive Offload 关闭(以及其他的 offload 也一起关了吧,方便查看)。如果不关的话,即使对方发过来小包,网卡也会帮我们合并成大包,导致被 iptables 丢弃。

最后,我们打开 tcpump,并且发送请求:curl -v https://accounts.google.com。抓包结果如下:

果然,对方一直尝试发给我们大小是 1400 的包,不断被我们丢弃,不断重发,非常锲而不舍,可惜是无用功。

还记得我们当时 MTU 设置错误,还是可以访问通 x.com,我们再拿它来试一下。

以下是 curl https://x.com 的抓包结果:

可以看到,在 server 端发送了 4 个长度为 1448 的包之后,就开始发送了一个长度为 512 的包,发现能够收到 ACK,就加大到 936 尝试扩大 MTU 发送,然后失败了,就退回到 512,可以看到后面还有大包的尝试,同样也失败了。不过最终的结果是发送成功的。

具体 PMTUD 的行为不太一样,比如 facebook.com 的第一次尝试是 1024,然后退到 512.

ICMP type 3 code 4 测试

ICMP 专门有一种消息是处理这种不可达的错误的。ICMP 的 type 3 意思是 Destination Unreachable,但是 Destination Unreachable 的原因有很多,对于每一种原因都有一种 Code,Code 4 意思就是 Fragmentation Needed and Don’t Fragment was Set。(即,包太大,需要拆成多个 IP 包,但是你有设置了不要拆包,所以我只能丢弃,并且用此 ICMP 来告知你。)

在上面的测试中,我们并没有发送任何的 ICMP 消息,而只是丢包。现在,我们添加一步,在丢包的时候,发回去一个 ICMP 消息。我们用 scapy 来做这个。代码非常简单,它抓所有超过 800 bytes 的包,对这些包的来源都发送一个 ICMP。还是 iptables 负责丢包,scapy 脚本只负责发 ICMP。

为什么 filter 是 greater 815 呢?因为 libpcapgreater 是 Ethernet 层的大小,Ethernet 的 header 是 14 bytes,所以我们要的条件是 >= 815 bytesgreater 是大于等于。(是,我也觉得很奇怪)

保存上文件为 a.py。运行方式是 python3 a.py

然后使用这台服务器进行测试。发现…… 结果和上文完全一样,我都告诉他们 next hop mtu 是 800 了,但是他们有自己的想法,从 512 开始尝试之类的。仿佛 ICMP 从来没发送到他们的服务器上。不知道是我构造包的问题,还是他们的服务器没有处理好 ICMP 的问题。比如之前看过 cloudflare 的这篇文章,就是说因为 ECMP 的问题,ICMP 消息会被路由到错误的负载均衡器上去,导致 PMTUD 失败。解决办法是将 ICMP type 3 code 4 广播到所有的负载均衡器上去。

ICMP type 3 code 4 虚拟机测试

为了试试看是不是我的脚本有问题,我在本地搭建了一个非常简单的网络环境。

抓包结构如下,可以看到,8000 端口尝试发送 1448 bytes 的包一直被忽略。当收到 ICMP 消息,server 端就立即改用 800 bytes (MSS 是 748 bytes)来发送了。所以,感觉还是公网发送 ICMP 黑洞的问题。

而且这个 path MTU 信息会在 route 的 cache 中,后续的发送会默认这个 path 的 MTU 就是 800,不会使用更高的尝试。

相关链接:

  1. nmap 有 Path MTU 探测功能:https://nmap.org/nsedoc/scripts/path-mtu.html
  2. Path MTU discovery in practice
  3. Iptables Tutorial 1.2.2 by Oskar Andreasson 一份不错的 iptables 教程
  4. RFC 5508 NAT Behavioral Requirements for ICMP
  5. Resolve IPv4 Fragmentation, MTU, MSS, and PMTUD Issues with GRE and IPsec


真实世界中的 PMTUD”已经有6条评论

  1. 有个问题:如果启用pmtud,收到icmp后,会直接干扰已建立的tcp flow接下来的包大小吗?也就是优先级高于syn协商里的mss么?
    之前以为的是收到icmp后,kernel更新route里的mtu,后续下一个tcp协商出来小的mss。
    谢谢xintao

    • Hi, 我觉得会干扰已经建立的 tcp flow。优先级高于 mss 协商。

      理由是,如果 mss 协商是正确的,那么就没有 pmtud 存在的必要了,pmtud 就是在 mss 协商不正确的情况才发挥作用。

      kernel 收到 icmp 更新 route 的 mtu,这个我看代码[1]觉得这个参数应该是在 3 层上的吧,所以更新了目标 ip 对应的 mtu ,应该在 ip 层生效,ip 层没有 tcp 连接的概念,所以会立即生效的。

      wiki 中[2] 也说,tcp 连接的第一个大于 MTU 的包会造成 ICMP code 3 type 4 发到 source,这时候就应该更新 PMTU 了,而不是等到下一次连接才行。这个功能对于 tcp 用户来说是透明的。(如果不透明的话,就需要用户去重建连接了)。

      1. https://github.com/torvalds/linux/blob/v3.15/net/ipv4/route.c#L951
      2. https://en.wikipedia.org/wiki/Path_MTU_Discovery#cite_ref-6

  2. > 而且这个 path MTU 信息会在 route 的 cache 中,后续的发送会默认这个 path 的 MTU 就是 800,不会使用更高的尝试。

    请教xintao,这个结论的依据有出处么?我看到的信息是 Starting with Linux kernel version 3.6, there is no routing cache for IPv4 anymore.

    • 我说的这个 cache 和你说的 routing cache 可能不是同一个东西。

      我的意思是,这个 pmtu 一经探测出来,后续就会一直使用(至少在一段时间内),而不是每一次发送一个包都会先发 1500 bytes 然后收到 ICMP,然后降低为 800 bytes 再重新发送。

      就是说,会把这个 route(到目标地址的路线)对应的 mtu 放到某个 cache 中。这样也是合理的。但是不是说放在了 routing cache 中。

      依据就是我上面的实验,可以看到 mtu 在一段时间内一直是使用的探测出来的值,而不是每次都探测。

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注