嵌套 VPN 隧道:大包时内层为何卡住
每条隧道都会在下一层开始前花掉路径数据包预算的一部分。把头部加起来,内层会话就会在大包上失败。
DNS 能解析,SSH 会话保持连接,然后一次大文件上传卡住了,而 ping 同一主机仍然有回应。这种分裂指向的是数据包大小,而不是坏掉的隧道。嵌套隧道会在两种隧道单独都不会遇到的问题尺寸上出现它。内层会话本身没问题;交给它的数据包已经装不下了。
路径 MTU 究竟去了哪里
每次封装都会在下一层轮到自己之前先花掉字节。一条以太网路径给外层数据包 1500 字节,外层隧道在内部任何东西被寻址之前就先取走自己那一份。一个凭感觉猜的额外开销数值,错的程度恰好就是关键的那部分。
按顺序列出各层:接入路径、外层传输、外层隧道、内层传输、内层隧道、应用。每个头部只算一次,并把 UDP 与隧道自己的头部和标签分开。
两层 WireGuard,只算一次
取一条 IPv4 接入路径,其中一层 WireGuard 隧道套在另一层里面。外层花掉 IPv4 的 20 字节、UDP 的 8 字节,以及 WireGuard 数据头加认证标签的 32 字节。再往里,内层隧道需要自己的 20 字节 IPv4 头和 8 字节 UDP 头,然后才是第二份 32 字节的 WireGuard 开销。剩下的空间容纳 IPv4 头、TCP 头和数据。外层若是 IPv6 路径,则要花 40 而不是 20,剩 1320。
| Layer | Bytes |
|---|---|
| Path MTU | 1500 |
| Outer IPv4 header | 20 |
| Outer UDP header | 8 |
| Outer WireGuard header and tag | 32 |
| Inner transport IPv4 header | 20 |
| Inner UDP header | 8 |
| Inner WireGuard header and tag | 32 |
| Innermost IPv4 header | 20 |
| TCP header | 20 |
| Payload that fits | 1340 |
算下来是 1340 字节。同样的规则下只套一层,TCP 载荷能剩 1400,也就是 1500 减去 20、8、32、20 和 20,而 60 字节的差值正好就是第二条隧道加上的内层封装。
把 1400 套用到嵌套路径上,内层隧道就少 60 字节,直到数据包大到需要它们才看得出来。填充和协议自带的分片选项又会改变数字,所以要相信这张表。
MTU、MSS 与分片作用在不同位置
接口 MTU 限制的是接口会发出什么,而 TCP MSS 钳制改变的是 TCP 对端愿意发送什么。 钳制能修好 TCP 会话 但对大的 UDP 数据报毫无用处,于是修复在一个应用上看起来成功,却悄悄漏掉了下一个。协议自带的分片选项是第三个杠杆,有它自己的失败方式。接口 MTU 是最常见的第一步,也是最粗的一步。
桥接与路由的隧道承载的帧不同,所以另一套部署里的数字很少能直接搬过来。路径 MTU 发现, RFC 8201中所描述的方法,只有在它所依赖的 ICMP 报文被放行时才有用。
找出不够的那一层
如果条件允许对比,就在嵌套之前先单独验证每条隧道。固定大小的探测能把这件事定下来: ping -M do -s 1472 -c 3 10.8.0.1 在该层内发送一个完整的 1500 字节 IPv4 数据包。加上第二层,再重复同样的探测,这样任何失败都归于某个已知的改动。
传输卡住时,一次只改一个有记录的参数,并记下失败时的尺寸。回程路径也要检查,因为嵌套路由递归足以把数据包大小问题模仿得很像,让人白花一下午。被过滤的 ICMP 回应会掩盖真正的上限,所以探测比推理更重要。
把预算写在下一个能找到它的地方
每层一行的表格比口口相传的经验活得更久。把它放在隧道示意图旁边,这样没人会在压力下重算一遍。
隧道越多,依赖越多,所以回滚方案值得占据预算旁边的一席之地。如果数字还是对不上,就去测量路径,而不是再猜一次。目标是你能为之辩护的数字,而不是能记住的数字。