MTU/MSS 分片陷阱:为什么你的 WireGuard/隧道时快时慢

用通俗比喻讲清 MTU、MSS 与 TCP 分片的关系,解释为什么加了 WireGuard/Cloudflare Tunnel/内网穿透后网页打不开、下载卡一半,并给出用 ping -M do 测路径 MTU 与设置 clamp-mss-to-pmtu 的修复步骤。

结论先说在前面:你给服务器套上 WireGuard、Cloudflare Tunnel 或者 frp 之后,网页有时候能开、有时候卡在半截,大文件下到一半就断,SSH 偶尔僵死——这九成不是带宽问题,也不是被封锁,而是 MTU/MSS 在捣鬼。一句话:隧道给每个数据包套了一层"快递箱外壳",箱子变大了,超过了中间链路允许的最大尺寸,于是大包被拆成碎片;偏偏碎片又被防火墙误杀,对方收不齐,只能整包重传,于是你看到的就"时快时慢、时通时断"。本文用最直白的比喻讲清原理,并给你一套拿来即用的诊断与修复命令。

延伸阅读

更多相关攻略推荐:Ollama AI系列(2):VPS上的AI推理与API应用Ollama AI系列(2):VPS上的AI推理与API应用【CPU选型 01】搭载 AMD Ryzen 9950X 的 VPSARM / Ampere 席卷 VPS:性价比真香还是兼容陷阱?Ollama AI系列(2):VPS上的AI推理与API应用

一、MTU 和 MSS 到底是什么:快递箱与"被拆的包裹"

先把两个名词拆开看。MTU(Maximum Transmission Unit,最大传输单元)指的是一条链路上能一次性搬运的"最大包裹"尺寸,单位是字节,包含包裹里的货物和包装箱本身(IP 头、协议头等)。常见的以太网 MTU 是 1500 字节,这是个沿用多年的行业默认值。

MSS(Maximum Segment Size,最大报文段长度)则是 TCP 这一层能塞进"货物"的最大尺寸,只算数据、不算头部。它的算法很简单:MSS = MTU − 20(IPv4 头部)− 20(TCP 头部)。在 1500 字节 MTU 的标准以太网上,MSS 就是 1460 字节。两个数字的关系就这么直白:MTU 是"算上箱子的总尺寸",MSS 是"箱子里能装多少货"。

为什么这很重要?TCP 在三次握手时,双方会各自报出自己能接受的 MSS,取较小值作为这条连接的真实上限。这个上限一旦定好,后续每个 TCP 报文段都不会超过它,理论上就不会触发分片。分片是什么?当一个包超过了路径上某一段链路允许的最大尺寸,路由器会把它拆成几片分别转发,到了终点再拼回去。

拆包听起来没问题,但代价很大。第一,所有碎片必须全部到达终点才能重组,只要丢了一片,整个原始包就作废、必须整包重传;第二,分片重组要消耗路由器/终端的 CPU;第三,很多防火墙和安全设备会直接丢弃分片,因为分片头部信息不全、难以做深度检测。所以工程上的共识是:能不分片就不分片,而 MSS 协商正是 TCP 用来"提前把货装小"的机制。

打个比方:MTU 是快递柜每个格子的尺寸,MSS 是你能塞进格子的东西大小。你发货前和收件人约好"每箱不超过 1460",这样格子的门就不会卡住。一旦你非要用 1500 的大箱去撞 1492 的小格子,箱子进不去,只能被拆开——这就是分片。

二、为什么加了隧道就触发分片:头部开销层层叠加

裸奔在公网上的普通 TCP 连接,路径 MTU 基本就是 1500,MSS 1460,相安无事。但当你套上隧道,事情就变了:隧道协议会给每个原始数据包再包一层"外壳",外壳本身要占字节,于是留给真正数据的空间被压缩,等效 MTU 变小。

各主流隧道的外壳厚度(开销)大致如下,数值会因加密套件和配置浮动,给的是参考区间:

隧道/链路类型额外头部开销建议内层 MTU
标准以太网01500
PPPoE(多数 DSL/部分光纤)约 8 字节1492
WireGuard(IPv4 传输)约 60 字节1440
WireGuard(IPv4+IPv6 通用保守值)按 80 字节预算1420
OpenVPN(UDP)约 28–70 字节1410 左右
IPsec(隧道模式)约 70–90 字节1400 左右
Cloudflare Tunnel封装 + TLS,建议 MSS 钳到 1360视边缘而定

WireGuard 的开销构成最透明:外层 IPv4 头 20 字节 + UDP 头 8 字节 + WireGuard 自身头与认证标签约 32 字节 = 60 字节。如果外层走 IPv6,头变成 40 字节,总开销升到 80 字节。所以 WireGuard 官方推荐的接口 MTU 是 1420(按 80 字节开销留余量,能同时兼容 IPv4/IPv6 传输),纯 IPv4 链路可放宽到 1440。移动网络这类 MTU 飘忽的环境,甚至建议直接降到 1280(IPv6 的最小保证 MTU)。

Cloudflare Tunnel 走的是 cloudflared 长连接 + TLS 封装,它本身默认按 1500 思考,但 Cloudflare 边缘在发包给客户侧时,如果包超过约 1476 字节就会做分片。于是很多"网页开一半卡死"的案例,本质是客户侧应用吐出的大 TCP 段,被隧道一封装就超了,触发分片。

frp 这类内网穿透更隐蔽:它把内网流量再套一层 TCP/UDP 隧道。常见排障帖里反复出现的剧情是——公网用户到 frps 那段 MSS 被正常钳制了,但 frps 到 frpc(内网客户端)这段没钳,内网吐出的大包封装后超过路径 MTU,于是"小文件能传、大文件必断"。

关键陷阱在于:这些隧道的 MTU 减少,并不会自动反向通知你上层应用的 TCP。应用层在握手时仍按 1500/1460 谈 MSS,完全不知道底下套了壳。于是大包照发,封装后超限,分片或丢弃就此发生。

三、典型症状:为什么偏偏是大包出事、小包正常

分片引发的故障有个鲜明特征:小流量一切正常,大流量就翻车。这恰好和"被封锁""带宽不够"的表现不同,是定位 MTU 问题最重要的线索。

症状一:网页能打开首页,但大页面或带图片的页面卡在半截。HTTP 请求小、响应头也小,但图片、JS、CSS 这类大响应要发大包。大包被分片或丢弃后,浏览器收不全,于是转圈圈。很多站长形容为"加了隧道后网站时好时坏"。

症状二:大文件下载/上传到一定比例就断。小文件整包能传完,大文件必然触发大段传输,一旦分片黑洞出现,连接就 stall(挂起)直到超时。你用 curl 或 wget 会看到进度条卡住然后失败。

症状三:SSH 登录正常,但一跑 top、vim 或 cat 大文件就卡死。SSH 建立连接时的交互数据很小,可一旦要回显大量输出,单屏数据超过 MSS,包就被拆,连接假死。很多人误以为是"服务器负载高"或"网络抖动"。

症状四:HTTPS/TLS 握手失败或极慢。TLS 握手阶段要交换证书链,证书往往几千字节,单包就可能超 MSS。握手卡住,浏览器报 ERR_SSL_PROTOCOL_ERROR 之类,但 ping 又通——典型的 ICMP 被放行、大 TCP 却被吞。

症状五:TCP 连接能建立,但数据传输几乎为零。这是最极端的"PMTU 黑洞":发送方设了 DF(不分片)位,中间某路由器发现包太大,按规矩该回一个 ICMP"需要分片"的消息(类型 3、代码 4),告诉发送方把包改小。可如果那台路由器/防火墙把 ICMP 拦了,发送方永远收不到通知,就一直发大包、一直被静默丢弃。结果:握手成功,数据不动。这跟"连不上"是两码事,恰恰最容易误导新手。

四、怎么诊断:用 ping -M do 测出真实路径 MTU

诊断的核心就一句话:找到这条路径上能通过、且不分片的最大包尺寸。Linux 上用 ping 的 -M do 参数(do = 禁止分片),从 1472 字节载荷开始往下试。

原理:ICMP 回显请求本身也要算头部,载荷 + 20(IP 头)+ 8(ICMP 头)= 总包大小。所以 -s 1472 恰好对应 1500 的总 MTU。公式:路径 MTU = 最大成功的 -s 载荷 + 28

具体步骤:

  • 先测标准值:ping -M do -s 1472 你的目标IP。能通说明路径 MTU 至少 1500。
  • 如果不通,每次减 10 或减 8 再试,比如 ping -M do -s 1464、ping -M do -s 1452,直到能通。
  • 假设 -s 1412 能通、-s 1420 不通,那么路径 MTU ≈ 1412 + 28 = 1440。
  • macOS 用 ping -D -s 1472;Windows 用 ping -f -l 1472,含义一样。

为什么强调 -M do?因为它强制设置 DF 位,模拟真实 TCP 大包"不允许分片"的行为。如果连这种被禁止分片的包都过不去,那真实业务的大包更过不去。反过来,如果 ping 本身就不通,先确认 ICMP 是否被放行——很多云厂商安全组默认不放 ICMP,你得临时放行才能测,否则测出来全是假阴性。

还有一个验证手段:用 tcpdump 抓分片。命令形如:tcpdump -i 网卡名 "ip[6:2] & 0x1fff != 0"。如果隧道接口(比如 wg0)上不断冒出分片包,说明 MTU 确实没配对。再查内核分片统计:grep "^Ip:" /proc/net/snmp,看 IpFragFails、IpReasmFails 是否在涨。

顺带提醒:有些新版 Linux 的 ping 还支持 -M probe,它会自动在"探测"和"允许分片"间切换,更适合二分查找 MTU,但 -M do 是最通用、最该记住的那条。

五、怎么修复:设对 WireGuard MTU,再用 MSS 钳制兜底

修复有两条路,建议双管齐下。

第一招:把隧道接口 MTU 设对。WireGuard 在 /etc/wireguard/wg0.conf 的 [Interface] 段加一行:

MTU = 1420

纯 IPv4 链路可写 1440;套了双层隧道(比如 WireGuard 里再跑 VPN)降到 1332 甚至 1280。改完重启:wg-quick down wg0 && wg-quick up wg0。也可以运行时直接改:ip link set dev wg0 mtu 1420。Cloudflare Tunnel 这边,cloudflared 支持 --mss-clamp 参数,比如 cloudflared tunnel --mss-clamp 1360 run --token 你的令牌,把内层 TCP 的 MSS 直接钳到 1360(对应 1500 MTU 减 40 头部,再留点余量)。frp 可在服务端 frps.toml 设 tls_mtu = 1400 限制隧道层 MTU。

第二招:MSS 钳制(clamp-mss-to-pmtu),这才是根治手段。前面说过,应用层的 TCP 握手不会自动知道隧道把 MTU 变小了。MSS 钳制的思路是:让网关/路由器在转发 TCP 的 SYN 包时,顺手把里面的 MSS 字段改小,从源头杜绝大包产生。它对非 TCP 流量(比如 UDP、ICMP)无影响,只对 TCP 生效,比直接改 MTU 更精准。

Linux 上用 iptables 一行搞定:

iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

这条规则的意思是:对所有转发的、带 SYN 或 RST 标志的 TCP 包,按出口网卡的 MTU 自动把 MSS 钳到"MTU − 40"(IPv4)。注意 --clamp-mss-to-pmtu 只能用在 FORWARD、OUTPUT、POSTROUTING 链。如果你只想针对 WireGuard 接口生效,加 -o wg0:

iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

本机自己作为客户端发起的连接(SYN-ACK 出本地),要补一条 OUTPUT 链:

iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

如果你确切知道路径 MTU(比如测得是 1420),也可以直接写死:iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1380(1420 − 40 = 1380)。写死的好处是确定性高,坏处是路径变了不会自适应。

用 nftables 的现代写法等价:在 inet mangle 的 forward 链里写 "tcp flags syn tcp option maxseg size set rt mtu"。

规则要让它重启还在,记得保存:iptables-save > /etc/iptables/rules.v4,或装 iptables-persistent。验证是否生效:tcpdump 抓握手看 MSS 值,钳制前是 1460,钳制后应变成你设定的值(比如 1380)。再用 wget 拉个大文件,应当能顺畅下完不再卡死。

最后给一句定心丸:绝大多数"加了隧道就时快时慢"的工单,按"测路径 MTU → 设隧道 MTU → 加 MSS 钳制"三步走,基本都能收口。它不是玄学,是协议栈封装逻辑没对齐而已。

#MTU #MSS #WireGuard #隧道排障 #分片