隧道与代理环境下的 MTU 路径黑洞(PMTUD)、MSS Clamping 与分片丢包排查
隧道接口 ping 得通,SSH 能连上,curl https://example.com 却卡在 TLS 握手之后的第一个大响应包,几十秒后超时。这类故障在 WireGuard、TUN 代理、TLS 隧道、WebSocket 代理的部署现场反复出现,抓包看就是几十到几百字节的小包正常往返,一旦服务端开始推送 1400 字节以上的数据段,客户端收不到任何回应。链路本身没有断,是路径 MTU 发现(PMTUD)在中间某一跳被静默掐死了。
这篇文章把这条故障链从以太网帧头一直拆到应用层,推导每种封装下真实可用的 MSS,给出内核侧可落地的 TCPMSS clamping 规则,最后用 ConnProof 的 CLI 子命令做端到端验证。所有推导都基于 RFC 791、RFC 8200、RFC 879、RFC 1191、RFC 8899 以及 Linux 内核 net/ipv4/tcp_output.c、net/ipv4/ip_forward.c 的实际行为。
一、MTU 与 MSS 的物理边界从哪来
以太网 II 帧的 payload 上限是 1500 字节,这个数字由 IEEE 802.3 的帧格式和早期共享介质冲突检测窗口共同决定,现代全双工交换网络保留它纯粹是兼容性惯性。1500 字节里装的是 IP 数据报,IPv4 头部最小 20 字节、带选项可到 60 字节,IPv6 固定头 40 字节、扩展头另算。
TCP 的 MSS 是 TCP 头部里的一个 16 位选项字段(RFC 793、RFC 6691),它声明的是「对端可以发给我的单个 TCP 段的 payload 最大值」,不含 TCP 头和 IP 头。一个不封装、不拨号的 IPv4 以太网路径,理论 MSS 是:
1500 (以太网 payload)
- 20 (IPv4 头,无选项)
- 20 (TCP 头,无选项)
= 1460 字节2
3
4
IPv6 下同一物理链路:
1500
- 40 (IPv6 固定头)
- 20 (TCP 头)
= 1440 字节2
3
4
这是裸链路的理论上限。任何一层封装都会从这个数字里继续扣。问题在于,MSS 是端系统在 SYN 握手时协商的,它反映的是「本机接口 MTU」,而不是「整条路径的最小 MTU」。一旦路径中间某个隧道把可用 payload 压到 1400 以下,而两端还在按 1460 发送,中间设备就必须分片。分片本身不致命,致命的是分片之后其中一片被丢,或者根本不允许分片(DF=1)却又收不到 ICMP 差错报文。
二、封装头部开销的精确账本
先把常见封装的头开销算清楚,这是后面所有 MSS 推导的基础。注意区分「封装在 IP 之内」和「封装在 TCP/UDP 之内」两类,前者直接吃掉 IP 层 MTU,后者吃掉 TCP payload 空间。
2.1 WireGuard
WireGuard(RFC 草案、Linux 内核 5.6+ 原生支持)是 UDP 封装。一个承载 IPv4 内层包的 WireGuard 数据包结构:
[外层 IPv4 20][外层 UDP 8][WG 头 16][内层 IP 数据报]WG 头固定 16 字节:1 字节 type、3 字节 reserved、4 字节 receiver index、8 字节 counter。外层 IPv4 20、UDP 8,合计 44 字节。承载 IPv6 内层包时外层仍是 IPv4 或 IPv6,若外层是 IPv6 则开销 40+8+16=64。
WireGuard 接口的 MTU 应设为:
IPv4 外层 + IPv4 内层:1500 - 44 = 1456
IPv4 外层 + IPv6 内层:1500 - 44 = 1456(内层 IPv6 头 40 已在内层数据报里)
IPv6 外层 + IPv6 内层:1500 - 64 = 14362
3
WireGuard 官方文档建议接口 MTU 取 1420,这是给外层 IPv6、内层 IPv6、外加可能的 PPPoE(8 字节)留的余量。实际部署里 1420 是稳妥值,1420 对应的 TCP MSS(内层 IPv4):
1420 - 20 - 20 = 13802.2 TUN 设备(三层隧道)
TUN 是纯三层,内核在 TUN 接口上收发的是裸 IP 数据报,没有额外封装头。但 TUN 之上的用户态程序通常会把 IP 包再塞进某个传输协议(UDP、TCP、TLS)发出去。以最常见的 UDP 转发为例:
[外层 IPv4 20][外层 UDP 8][内层 IP 数据报]开销 28 字节,TUN 接口 MTU 可设 1472。若用户态程序用 TCP 承载,开销 20+20=40,MTU 设 1460。若再套一层 TLS(TLS 记录头 5 字节 + AEAD tag 16 字节,RFC 8446 中 TLS 1.3 的 record 结构),每包再吃 21 字节,MTU 落到 1439 附近。
2.3 TLS 隧道与 WebSocket 代理
TLS 记录层(RFC 5246、RFC 8446)本身不改变 IP 层 MTU,它作用在 TCP payload 之上。TLS 1.3 的 AEAD 加密记录结构:
[ContentType 1][Version 2][Length 2][Encrypted Payload][AEAD Tag 16]固定开销 5 + 16 = 21 字节。若应用层在 TLS 之上再跑 WebSocket(RFC 6455),每帧再加 2 到 14 字节帧头(取决于 payload 长度和是否 mask)。一个 WebSocket over TLS over TCP over IPv4 的隧道,单包有效载荷空间:
1460 (TCP payload)
- 21 (TLS 记录开销)
- 2~14 (WS 帧头)
= 1425~1437 字节2
3
4
这类隧道通常由用户态代理程序自己控制分片,不直接暴露 IP MTU,但如果代理程序把内层 IP 包原样塞进 WS 帧,内层 MTU 就必须按这个数字反推。很多代理实现默认按 1500 处理,于是内层包一超过 1425 就被 TCP 分段,接收端重组时若代理程序没实现重组逻辑,直接丢包。
2.4 常见封装开销汇总表
| 封装类型 | 外层协议 | 头开销 | 建议接口 MTU | IPv4 内层 TCP MSS |
|---|---|---|---|---|
| 裸以太网 | IPv4 | 0 | 1500 | 1460 |
| 裸以太网 | IPv6 | 0 | 1500 | 1440 |
| PPPoE | IPv4 | 8 | 1492 | 1452 |
| WireGuard | IPv4/UDP | 44 | 1420 | 1380 |
| WireGuard | IPv6/UDP | 64 | 1420 | 1380 |
| OpenVPN UDP | IPv4/UDP | 28+ | 1450 | 1410 |
| TUN + UDP | IPv4/UDP | 28 | 1472 | 1432 |
| TUN + TCP | IPv4/TCP | 40 | 1460 | 1420 |
| TUN + TLS | IPv4/TCP | 40+21 | 1439 | 1399 |
| TUN + WS + TLS | IPv4/TCP | 40+21+14 | 1425 | 1385 |
这张表是后面所有排查的基准。现场遇到「小包通、大包死」,第一件事就是拿实际部署的封装链去查这张表,看内层 MTU 设对了没有。
三、PMTUD 与路径黑洞的物理机理
PMTUD 是 RFC 1191(IPv4)和 RFC 8201(IPv6)定义的机制。发送方在 IP 头里置 DF(Don't Fragment)位,沿途路由器若发现下一跳 MTU 小于包长,且包不允许分片,就回一个 ICMP 差错报文告诉发送方「我这边 MTU 是 X」,发送方据此调小后续包长。
IPv4 的差错报文是 ICMP Type 3 Code 4(Destination Unreachable, Fragmentation Needed and DF set),报文里必须携带触发它的原始 IP 头加前 8 字节 payload(RFC 792)。IPv6 对应的是 ICMPv6 Type 2(Packet Too Big),携带触发包的 MTU 值。
机制本身没问题,问题出在中间设备对 ICMP 的处理策略上。
3.1 ICMP 被防火墙静默丢弃
大量企业边界防火墙、云厂商安全组、运营商 CGNAT 设备的默认策略是「拒绝所有入向 ICMP」,或者只放行 Echo Request/Reply,把 Type 3 Code 4 一并干掉。理由通常是防 ICMP 扫描、防隧道、防信息泄露。结果是:
- 发送方按 1500 发出 DF=1 的大包
- 中间某跳 MTU 只有 1400,路由器想回 ICMP Type 3 Code 4
- 防火墙把 ICMP 丢掉,发送方永远收不到
- 发送方 TCP 重传同样的 1500 字节包,再次被丢
- TCP 重传退避,RTO 指数增长,最终连接超时
这就是「路径 MTU 黑洞」(PMTU Black Hole,RFC 2923 描述的现象)。链路物理上通,但大包永远过不去,小包正常。
3.2 抓包特征
在发送端 tcpdump 能看到非常典型的模式:
## 发送端
14:22:01.001 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [S], seq 1000, win 65535, options [mss 1460,...], length 0
14:22:01.052 IP 203.0.113.10.443 > 10.0.0.5.54321: Flags [S.], seq 5000, ack 1001, options [mss 1460,...], length 0
14:22:01.053 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [.], ack 1, length 0
14:22:01.054 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [P.], seq 1:1461, ack 1, length 1460
14:22:01.054 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [P.], seq 1461:2921, ack 1, length 1460
14:22:01.300 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [P.], seq 1:1461, ack 1, length 1460 <- 重传
14:22:01.800 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [P.], seq 1:1461, ack 1, length 1460 <- 重传
14:22:02.800 IP 10.0.0.5.54321 > 203.0.113.10.443: Flags [P.], seq 1:1461, ack 1, length 1460 <- 重传2
3
4
5
6
7
8
9
特征非常清晰:SYN/SYN-ACK/ACK 三次握手正常,MSS 协商成 1460,紧接着 1460 字节的 PSH 包发出后没有任何 ACK 回来,发送方按 RTO 1s、2s、4s 退避重传,每次重传同样的 1460 字节。抓包时看不到任何 ICMP 报文,因为它在中间被丢了。
如果中间设备允许 ICMP 通过,会看到:
14:22:01.150 IP 192.0.2.1 > 10.0.0.5: ICMP 203.0.113.10 unreachable - need to frag (mtu 1400), length 556这条 ICMP 里的 mtu 1400 就是黑洞的出口。抓到它,问题基本定位。
3.3 Wireshark 过滤表达式
现场排查用这几个过滤器:
## 只看 DF 位置位的大包
ip.flags.df == 1 && ip.len > 1400
## 只看 ICMP 分片需要报文
icmp.type == 3 && icmp.code == 4
## 只看 TCP 重传
tcp.analysis.retransmission
## 组合:DF 大包 + 重传
ip.flags.df == 1 && ip.len > 1400 && tcp.analysis.retransmission
## IPv6 的 Packet Too Big
icmpv6.type == 22
3
4
5
6
7
8
9
10
11
12
13
14
如果 icmp.type == 3 && icmp.code == 4 一条都抓不到,而 ip.flags.df == 1 && ip.len > 1400 有大量重传,PMTU 黑洞基本坐实。
四、MSS Clamping 的两种实现路径
既然 PMTUD 不可靠,工程上的标准解法是在隧道入口做 MSS Clamping(RFC 879 的变通实践)。原理是:路由器或隧道端点拦截经过的 TCP SYN 包,把里面的 MSS 选项改写成一个更小的值,让两端从握手开始就按小 MSS 发送,从源头避免分片。
4.1 iptables 实现
## 在 FORWARD 链上对经过隧道的 TCP SYN 做 MSS 改写
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
## 或者指定固定值(例如 WireGuard 场景)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1380
## 只对特定出接口生效
iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu2
3
4
5
6
7
8
9
10
11
--clamp-mss-to-pmtu 会读取路由表里该目的地对应的 PMTU,减去 40(IPv4)或 60(IPv6)得到新 MSS。这个选项依赖路由表里的 PMTU 信息,如果路由是静态的且没设 MTU,它退化成接口 MTU。生产环境更常用 --set-mss 指定明确值,避免依赖 PMTU 缓存。
4.2 nftables 实现
nftables 的写法更清晰,现代发行版(Debian 10+、RHEL 8+)推荐用这套:
table inet mangle {
chain forward {
type filter hook forward priority mangle; policy accept;
# 对出 wg0 的 TCP SYN 改写 MSS
oifname "wg0" tcp flags & (syn | rst) == syn \
tcp option maxseg size set rt mtu
# 或固定值
oifname "wg0" tcp flags & (syn | rst) == syn \
tcp option maxseg size set 1380
# IPv6 场景
oifname "wg0" meta nfproto ipv6 tcp flags & (syn | rst) == syn \
tcp option maxseg size set 1360
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tcp option maxseg size set rt mtu 是 nftables 的动态写法,取路由 MTU 减去 IP 头和 TCP 头。固定值写法更可控,尤其当隧道对端 MTU 已知时。
4.3 内核 sysctl 层的配合
MSS Clamping 之外,还有几个 sysctl 参数影响分片和 PMTUD 行为:
## 启用 PMTU 发现(默认 1,别关)
net.ipv4.ip_no_pmtu_disc = 0
## PMTU 路由缓存的最小值,防止被恶意 ICMP 打到极小值
net.ipv4.route.min_pmtu = 552
## 允许接收 ICMP 差错报文里的 MTU 信息
net.ipv4.icmp_echo_ignore_all = 0
## TCP 层对 PMTU 变化的响应
net.ipv4.tcp_mtu_probing = 12
3
4
5
6
7
8
9
10
11
net.ipv4.tcp_mtu_probing 值得单独说。它有三个值:
- 0:关闭 MTU 探测
- 1:默认关闭,但检测到 PMTU 黑洞时启用(推荐)
- 2:始终启用
设为 1 时,内核在连续重传失败后会主动尝试更小的 MSS,绕过被丢的 ICMP。这是 PMTUD 失效场景的兜底机制,代价是探测期间有额外延迟。生产环境建议设 1。
net.ipv4.route.min_pmtu 默认 552,防止中间设备伪造极小 MTU 的 ICMP 把连接打到碎片化。如果隧道内层跑的是需要更大 MTU 的协议,可以适当调低这个值,但不要低于 576(IPv4 最小重组缓冲,RFC 791)。
4.4 TCPMSS 的失败边界
MSS Clamping 不是万能的,有几个明确的失败场景:
非 TCP 流量。UDP、QUIC、ICMP 本身没有 MSS 选项,clamping 管不了。QUIC(RFC 9000)自己做路径 MTU 发现,如果 ICMP 被丢,QUIC 会退到 1200 字节保守值,性能下降但不至于断。
SYN 包被加密。如果隧道本身加密了内层 TCP 头(例如某些全加密代理),中间设备看不到 TCP 选项,clamping 无从下手。这种情况只能在内层端点做,或者把隧道接口 MTU 直接设小。
MSS 选项不存在。极少数老实现不发 MSS 选项,内核按 RFC 879 默认值 536 处理,反而不会出问题。
PMTU 缓存污染。如果路由缓存里存了一个错误的 PMTU(例如被伪造的 ICMP 打小),clamping 会按这个错误值改写。需要
ip route flush cache清理。
五、分片丢包的排查流程
现场排查按这个顺序走,每一步都有明确的判据。
5.1 确认小包通、大包不通
## 小包 ping,应该通
ping -c 3 -s 100 203.0.113.10
## 大包 ping,DF 置位,不通则说明路径 MTU 小于 1500
ping -c 3 -s 1472 -M do 203.0.113.10
## 二分法找路径 MTU
for s in 1400 1450 1472; do
ping -c 1 -W 1 -s $s -M do 203.0.113.10 && echo "MTU >= $((s+28))"
done2
3
4
5
6
7
8
9
10
-M do 是 Linux ping 的 DF 置位选项,-s 指定 ICMP payload 大小,实际 IP 包长是 payload + 28(IPv4 头 20 + ICMP 头 8)。1472 + 28 = 1500,正好是标准以太网 MTU。
5.2 定位隧道接口 MTU
## 查看所有接口 MTU
ip link show
## 查看隧道接口详情
ip -d link show wg0
## 查看路由的 PMTU
ip route get 203.0.113.102
3
4
5
6
7
8
WireGuard 接口如果 MTU 显示 1420,内层 TCP MSS 应该是 1380。如果显示 1500,就是配置漏了,需要改:
ip link set dev wg0 mtu 1420WireGuard 的 MTU 也可以在配置文件里写:
[Interface]
PrivateKey = ...
Address = 10.0.0.1/24
MTU = 14202
3
4
5.3 抓包确认分片行为
在隧道两端同时抓包:
## 隧道入口(物理接口)
tcpdump -i eth0 -n -v 'udp port 51820' -w wg-in.pcap
## 隧道出口(wg0 接口)
tcpdump -i wg0 -n -v 'tcp port 443' -w wg-out.pcap
## 只看 DF 大包和 ICMP
tcpdump -i eth0 -n -v 'ip[6] & 0x40 != 0 and ip[2:2] > 1400'
tcpdump -i eth0 -n -v 'icmp[0] = 3 and icmp[1] = 4'2
3
4
5
6
7
8
9
如果 wg0 上看到 1460 字节的 TCP 包,而 eth0 上对应的 UDP 包被分片(IP 头里 MF 位置位或 offset 非 0),说明内层 MTU 设大了,WireGuard 在物理链路上做了 IP 分片。分片本身能过,但如果中途丢一片,整个包作废,表现为随机丢包。
5.4 用 ConnProof 做端到端验证
ConnProof 的 CLI 提供了直接的 MTU 探测能力。基本用法:
## 诊断到目标地址的端到端连通性,包含 MTU 探测
connproof diagnose 203.0.113.10 --port 443 --verbose
## 只测 TCP 层,指定 IPv4
connproof tcp 203.0.113.10 --port 443 --ipv4 --timeout 10
## 测 TLS 握手,确认大包能否通过
connproof tls 203.0.113.10 --port 443 --verbose
## 测 WebSocket 隧道
connproof websocket wss://tunnel.example.com/ws --verbose
## 输出 JSON 格式,便于脚本处理
connproof diagnose 203.0.113.10 --port 443 --format json2
3
4
5
6
7
8
9
10
11
12
13
14
connproof diagnose 会依次探测 ICMP、TCP 握手、TLS 握手、应用层数据,并在 verbose 模式下打印每一步的 MTU 探测结果。典型输出:
[diagnose] target=203.0.113.10 port=443
[icmp] echo reply, rtt=12.3ms
[mtu] probing 1500... timeout
[mtu] probing 1400... ok
[mtu] probing 1450... timeout
[mtu] path mtu = 1400 (binary search, 3 probes)
[tcp] syn-ack, mss=1460 (advertised), rtt=13.1ms
[tcp] warning: advertised mss 1460 > path mtu 1400, fragmentation likely
[tls] handshake ok, cert cn=example.com
[tls] application data 1380 bytes... ok
[verdict] path mtu black hole detected, apply mss clamping at 13602
3
4
5
6
7
8
9
10
11
这个输出直接给出了结论和修复建议。如果 connproof diagnose 报 path mtu 小于接口 MTU,就是黑洞。
5.5 验证修复效果
改完 MSS Clamping 后重新验证:
## 清空路由缓存,避免旧 PMTU 干扰
ip route flush cache
## 重新诊断
connproof diagnose 203.0.113.10 --port 443 --verbose
## 抓包确认 SYN 里的 MSS 被改写
tcpdump -i wg0 -n -v 'tcp[tcpflags] & tcp-syn != 0' -c 52
3
4
5
6
7
8
修复后 SYN 包里的 MSS 应该显示为 1380 或更小,而不是 1460。connproof diagnose 的 verdict 应该变成 path mtu consistent, no fragmentation detected。
六、IPv6 场景的特殊性
IPv6 强制不允许中间路由器分片(RFC 8200),端到端分片只能由源端做。这意味着 IPv6 下 PMTUD 失效的后果比 IPv4 更严重:路由器收到过大包只能丢,回 ICMPv6 Type 2,如果 ICMPv6 被丢,发送方完全不知道发生了什么。
IPv6 的最小 MTU 是 1280(RFC 8200),任何链路都必须支持。隧道接口的 IPv6 MTU 不能低于 1280,否则会破坏邻居发现(NDP)和 DHCPv6。WireGuard over IPv6 的内层 IPv6 MTU 建议值:
1500 - 64 (外层 IPv6 + UDP + WG 头) = 1436对应的 TCP MSS:
1436 - 40 (IPv6 头) - 20 (TCP 头) = 1376nftables 的 IPv6 clamping 规则:
oifname "wg0" meta nfproto ipv6 tcp flags & (syn | rst) == syn \
tcp option maxseg size set 13602
取 1360 而不是 1376,是给可能的扩展头留余量。
七、QUIC 与 UDP 流量的处理
QUIC(RFC 9000)跑在 UDP 上,没有 MSS 选项,MSS Clamping 对它无效。QUIC 自己实现了 DPLPMTUD(RFC 8899),会主动探测路径 MTU。如果 ICMP 被丢,QUIC 探测会失败,退到 1200 字节的保守值。这个值对大多数应用够用,但视频流和大文件传输会明显降速。
对 QUIC 流量的处理策略:
- 确保 ICMP 能通。这是最根本的,让 DPLPMTUD 正常工作。
- 隧道接口 MTU 设对。QUIC 内层包不会超过隧道 MTU,从源头避免分片。
- 限制 QUIC 包大小。某些代理实现支持配置 QUIC 初始包大小,可以设成隧道 MTU 减 28。
UDP 分片在隧道场景下尤其危险。一个 1400 字节的 UDP 包被 WireGuard 封装后变成 1444 字节,如果物理链路 MTU 是 1500 没问题,但如果中间还有 PPPoE(8 字节),就变成 1452,仍在 1500 内。真正的问题是内层 UDP 包本身超过隧道 MTU 时,WireGuard 会在物理链路上分片,分片丢了整包作废,UDP 没有重传,直接丢数据。
八、生产环境的配置清单
把前面所有内容整理成一份可执行的配置清单,按部署顺序排列。
8.1 隧道接口 MTU
## WireGuard
ip link set dev wg0 mtu 1420
## TUN + UDP
ip link set dev tun0 mtu 1472
## TUN + TCP
ip link set dev tun0 mtu 1460
## TUN + TLS
ip link set dev tun0 mtu 14392
3
4
5
6
7
8
9
10
11
8.2 nftables MSS Clamping
table inet mangle {
chain forward {
type filter hook forward priority mangle; policy accept;
# IPv4 出隧道
oifname "wg0" meta nfproto ipv4 tcp flags & (syn | rst) == syn \
tcp option maxseg size set 1380
# IPv6 出隧道
oifname "wg0" meta nfproto ipv6 tcp flags & (syn | rst) == syn \
tcp option maxseg size set 1360
}
}2
3
4
5
6
7
8
9
10
11
12
13
8.3 sysctl 持久化
写入 /etc/sysctl.d/99-mtu.conf:
net.ipv4.tcp_mtu_probing = 1
net.ipv4.ip_no_pmtu_disc = 0
net.ipv4.route.min_pmtu = 552
net.ipv4.icmp_echo_ignore_all = 02
3
4
应用:
sysctl -p /etc/sysctl.d/99-mtu.conf8.4 防火墙放行 ICMP
如果条件允许,在边界防火墙放行 ICMP Type 3 Code 4 和 ICMPv6 Type 2:
## iptables
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
iptables -A INPUT -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
## nftables
icmp type destination-unreachable icmp code frag-needed accept
icmpv6 type packet-too-big accept2
3
4
5
6
7
这是治本的做法。MSS Clamping 是治标,ICMP 放行才是让 PMTUD 正常工作。
九、故障案例复盘
一个真实的 WireGuard 部署案例。客户端在办公室,出口有企业防火墙;服务端在云上,安全组只放行了 UDP 51820。客户端能连上 WireGuard,能 ping 通服务端内网地址,但访问服务端背后的 HTTPS 服务时,TLS 握手完成后第一个大响应包就卡住。
排查过程:
ping -s 1472 -M do服务端内网地址,不通;ping -s 1400 -M do通。路径 MTU 在 1400 到 1472 之间。ip link show wg0显示 MTU 1500,没设过。这是根因。- 服务端抓包,看到客户端发来的 1460 字节 TCP 包,WireGuard 封装后 1504 字节,超过物理链路 1500,内核在物理接口上做了 IP 分片。
- 分片后第一片 1500 字节,第二片 4 字节。企业防火墙对碎片化 UDP 包的处理策略是丢弃,所以第二片丢失,整个包作废。
- 客户端收不到 ACK,重传,循环。
修复:
## 客户端和服务端都改
ip link set dev wg0 mtu 1420
## 服务端加 MSS Clamping
nft add rule inet mangle forward oifname "wg0" tcp flags syn tcp option maxseg size set 13802
3
4
5
改完后 connproof diagnose 显示 path mtu 一致,TLS 应用数据传输正常,问题解决。
这个案例的关键点是:WireGuard 接口 MTU 默认继承物理接口的 1500,但 WireGuard 封装需要 44 到 64 字节,实际可用只有 1436 到 1456。不手动改 MTU,内核就会在物理链路上分片,而分片包在很多网络里被特殊对待,丢包率远高于普通包。
十、验证与监控
修复之后需要持续监控,避免配置漂移。几个关键指标:
## 统计 IP 分片包数量
nstat -az | grep -i frag
## 统计 ICMP 分片需要报文
nstat -az | grep -i IcmpInDestUnreachs
## TCP 重传率
ss -s
## 路由 PMTU 缓存
ip route show cache2
3
4
5
6
7
8
9
10
11
nstat 里的 IpFragOKs、IpFragFails、IpReasmFails 是分片相关的计数器。如果 IpFragFails 持续增长,说明有包因为 DF 置位且超过 MTU 被丢。IpReasmFails 增长说明分片重组失败,通常是丢片。
用 ConnProof 做定期探测:
## 每天跑一次,输出 JSON 存日志
connproof diagnose 203.0.113.10 --port 443 --format json >> /var/log/mtu-probe.jsonl
## 对比历史,发现 MTU 变化
connproof doctor --verbose2
3
4
5
connproof doctor 会检查本机网络配置,包括接口 MTU、路由表、sysctl 参数,给出潜在问题列表。定期跑一次能提前发现配置漂移。
十一、相关排查入口
MTU 问题经常和其他网络故障混在一起,排查时容易走弯路。以下几个站内入口对应不同症状:
- 客户端显示连接成功但实际没有网络流量,先看 客户端连接成功但无网络,里面区分了 MTU 黑洞和 DNS 故障的不同表现。
- 系统代理和 TUN 模式的差异会导致完全不同的 MTU 行为,参考 系统代理环境与 TUN 模式 理解两种模式的封装层次。
- 需要向客服或上游反馈问题时,按 向客服反馈排错证据 的清单准备抓包文件和 ConnProof 输出,能大幅缩短定位时间。
MTU 路径黑洞的排查核心就三步:确认小包通大包不通、定位路径最小 MTU、在隧道入口做 MSS Clamping 或改接口 MTU。每一步都有明确的命令和判据,不需要猜测。真正花时间的是理解封装链的每一层开销,以及为什么中间设备会丢 ICMP。把这两件事搞清楚,剩下的就是执行。