Openwrt的GRETap6隧道的MTU问题

TLDR:如何使用GRE Tap隧道不会遇到MTU问题?使用IPv4连接(gretap4),同时(在第一次创建接口时!!)关闭GRE接口配置中的“设置Don't Fragment”选项。

背景

两个发射wifi的路由器用GRETap通过IPv6在链路层组网,我在这边wifi通过SSH连接那边路由器有线连接的台式机,诡异地连不上去。经过调研发现应该是MTU的问题。

IPv4和IPv6的MTU处理

MTU是指最大传输单元。当数据包在互联网上通过路由经过各种各样的链路的时候,不同链路能传的最大包大小不同,包太大就需要被分片或者丢弃。分包都会造成性能开销,所以最合适的包大小当然是整个链路里面最小的那个包大小。

IPv4同时允许分片和Path MTU Discovery(PMTUD)。

  • PMTUD就是指路由器直接把包丢了,向发送端发个icmp消息说包太大了,你重新按这个大小发包吧。
  • 分片就是指中间的路由器可以通过设置Fragment标记进行分片,将一个包直接拆成两个包。

IPv6在设计时直接完全禁止了分片的存在,没有分片标志位了。包如果太大只能被丢弃,然后使用PMTUD通知发送端。

局域网直接访问的问题

GRE隧道技术,通过在IP层里封装链路层的包,实现了二层组网。经过封装的包会出现这样的层次结构。

以太网 --(封装)--> IP --(封装)--> GRE --(封装)--> 内层以太网 --(封装)--> 内层IP --(封装)--> ...

同时GRE隧道不支持分片,因此原本能传的大包,经过隧道之后因为多封装了外侧的以太网+IP+GRE头,包会变大,导致超出MTU。

而且如果在隧道中间丢包,则PMTUD也无法正常工作。关键的问题在于,GRETap不在IP层。也就是说,现在在链路层L2组网。如果我要ssh连接隧道另一头的机子,连接时直接通过链路层,中间没有任何的路由。虽然实际上我们包被中间的路由器用GRE隧道封装了,但是路由器即使知道你笔记本这个包太大了传不了,想和你笔记本说一下,说你包太大了,如果通过ICMP的packet too big消息说的话,这属于L3层了。在笔记本看来,我是直接和那边机子沟通啊,你怎么突然过来插一脚,根本不会理会。

推荐阅读:RFC 7588 GRE隧道的分片方案。这篇RFC介绍了各大企业在使用GRE隧道时遇到分片问题时,由于默认的协议不支持分片,从而设计自己的方案,并对协议做出拓展的事情。

IPv4时的GRETap解决方案

Linux内核的GRE隧道,最近支持了ignore-df nopmtudisc选项。(如果是IPv4封装的GRE)在最外层的IP包处强制允许分片。这样即使单个包太长,中间的路由器也可以直接分片传输。

然而,因为IPv6完全不支持包分片,IPv6的GRE隧道就不能这么做了。

跨交换机GRETap6组网

上面说的都是使用GRE通过互联网创建隧道。但是实际上我们想做的是通过IPv6,跨学校交换机在两个房间组网。如果经过交换机的这个链路支持很大的MTU,那我们隧道就应该没问题。经过测试,交换机能传输大的以太网包。但是我们发现,怎么改接口的MTU都不管用。

不断抓包,使用nping命令直接发送以太网包发现,IPv6和IPv4的MTU居然不一样。先把网卡MTU配置为2000。首先使用nping -c 1 --icmp --dest-mac 00:0C:29:72:2D:59 1.1.1.1 --data-length 1954 可以发出总大小为2000的以太网包。但是如果转而想发送IPv6,使用 ping -s 11472 -Mdo 2001:250:4000:511d:114:514:1919:810 提示ping: local error: message too long, mtu: 1500

进一步搜索找出了问题的根源:IPv6的RA路由通告会通告当前网段的MTU大小。通过设置静态地址,忽略RA,可以使得MTU正确地变成网卡配置的MTU。

MSS Clamping

这个视频很好地介绍了MSS Clamping的原理。

如果链路中MTU出现了问题,则每次都得发送一个PMTUD的消息,减小传输大小,然后才能正常通信。这在每次通信时都增加了一个round trip的延迟开销

MSS是TCP的一个协商出来的参数,也是最大的传输大小。路由器防火墙(在Masquerade的同时?),如果发现链路的MTU很小,可以在协商的时候把TCP的MSS直接弄小一点。这样就直接省去了这个round trip步骤。

其他

诊断

  • 这个Reddit帖子提到这种SSH连接停住的现象很可能是MTU问题。确实很可能是MTU的问题。通过直接把笔记本接到那边路由器,或者连那边的wifi,问题都不再出现。
  • 这个人也有IPv6 GRETap的MTU问题

Path MTU Discovery (PMTUD)原理:虽然本地机子的MTU不会变,但是发包后路由器就会返回icmp消息说,你太长了,然后本地机子就会用mtu1300.

这个帖子里和我遇到了相同的问题。想想GRE包在被包裹之后,传递给那边的时候,还是通过IP层传的。这个帖子也尝试详细描述这个问题。

内核那边也有相关的讨论: - 最底下提到了ignore-df nopmtudisc这两个选项。这个回答里也有相关的例子 - 中间看到一些,bridge按照标准会丢大于自己MTU的包,这个好像无关,bridge取的是最小的MTU。

OpenWRT的支持

Package GRE:先看看GRE包的源码吧,稍微看看就可以发现,这里调的是proto_send_update,是netifd-proto.sh里面的函数。引用到了另外一个仓库:netifd。而netifd并不是调的ip link命令,而是直接C语言底层实现相关操作。

ip link命令又是怎么实现这个功能的呢?ip这个命令行工具来自iproute2包,搜索相关源码可以找到link_gre.c这里。可以发现在linux/if_tunnel.h这个内核头文件里有个flag叫IFLA_GRE_PMTUDISC。这里操纵的是struct nlmsghdr结构体,

那边netifd也是一样,但是使用了libnl库。内核文档libnl文档都描述了netlink消息的细节。

然而,这里可以看到,仅对IPv4的隧道支持了ignore-df选项。在ip命令的帮助里可以发现,ignore-df选项仅忽略IPv4的Dont Fragment包。

这里这里提到了,在IPv6里根本没有分包,必须由中间节点发icmp6消息提醒发送端减少MTU。很可能这个问题是无解的,只能改自己本地MTU?

这个RFC文档提到了一些实现,有些厂商为了解决这个问题而手动实现了分包。