pathvector-dev

最初发布于 https://blog.pathvector.dev/protocol-lab-mss-37/ —— 免费 Protocol Lab 系列的一部分。

本文属于 Protocol Lab,这是一个免费的动手系列,通过在容器实验室中构建和破坏协议来学习网络协议。所有实验材料——拓扑、配置和脚本——都存放在仓库中:github.com/pathvector-studio/protocol-lab

在 Lab #25 中,我们观察了 Path MTU Discovery 通过 ICMP 发现更小的链路——以及当该 ICMP 被过滤时它如何黑洞。本实验是操作性修复:MSS 钳制。路径上的路由器重写经过的 TCP SYN 中的 MSS,让两端事先协商发送能适配最窄链路的报文段——无需依赖 PMTUD 正常工作。

阅读指南: rfc-notes/mss-clamping.md

先修: Lab 25:MTU 和路径 MTU 发现

预计用时:35–50 分钟。

目标

客户端链路 MTU 为 1500;r–server 链路 MTU 为 1400

  • 不钳制时,客户端的 SYN 通告 MSS 1460(其本地 1500 − 40);完整报文段对 1400 链路而言过大,依赖 PMTUD。
  • 钳制时r 将 SYN 中的 MSS 下调至 1360(1400 − 40),让两端始终发送适配的报文段。

完成后,你应能解释下表:

服务器看到的 SYN MSS 原因
不钳制 1460 客户端本地 MTU 1500 − 40
钳制 1360 r 钳制到 1400-MTU 链路(− 40)

你将学到

  • TCP MSS 选项是什么,以及端点如何从本地 MTU 推导它。
  • 为什么 SYN 中的 MSS 对路径中更小的链路是盲的
  • 当 ICMP 被过滤时,PMTUD 为什么会黑洞(Lab 25 回顾)。
  • 路由器上的MSS 钳制如何重写 SYN 中的 MSS 以适配路径。
  • 为什么这对会缩小有效 MTU 的隧道(WireGuard/VXLAN/GRE)很重要。

本实验涵盖:

  • 完整的 PMTUD 内部机制(Lab 25 涵盖)。
  • IPv6 MSS 细节(使用更大最小值时 MSS = MTU − 60)。
  • 每路由 MTU 固定或 PLPMTUD(RFC 8899)。

RFC 中可阅读的位置

参考 重点
RFC 9293 §3.7.1 MSS 选项及报文段大小的决定方式
RFC 1191 PMTUD(钳制回退的机制)
RFC 2923 PMTUD 黑洞问题
RFC 5737 / RFC 1918 确认此处使用的地址仅限本地

全景

r 位于客户端与服务器之间,且只有 r–server 链路 MTU 为 1400。

 client ---- eth1/eth1 ---- r ---- eth2/eth1 ---- server
 MTU 1500                       MTU 1400 (narrow)   MTU 1400
   SYN: mss 1460  --->  r clamps --->  mss 1360 at server

Enter fullscreen mode Exit fullscreen mode

r 的 FORWARD 链(mangle 表)中,我们将 SYN 的 MSS 钳制到 PMTU。

flowchart LR
  C["client (MTU 1500)<br/>SYN mss 1460"] --> R["r<br/>TCPMSS --clamp-mss-to-pmtu"]
  R -->|"SYN rewritten<br/>mss 1360"| S["server (MTU 1400)"]

Enter fullscreen mode Exit fullscreen mode

10.0.0.0/8 是本地封闭范围。

注意: 本实验全部使用本地/文档地址空间(RFC 1918 / RFC 5737),因此不会触及真实互联网。

你需要的环境

推荐环境:

  • Linux / WSL2 / Linux 虚拟机
  • Docker
  • containerlab

使用的镜像:

  • nicolaka/netshoot:latest —— 包含 iptablestcpdumpcurlpython3

无需额外镜像。

运行实验

快速路径,脚本会为你完成部署、验证和清理:

./scripts/labctl.sh run mss-37

Enter fullscreen mode Exit fullscreen mode

或手动逐步执行:

1. 进入工作目录

cd protocol-lab/examples/mss-37

Enter fullscreen mode Exit fullscreen mode

2. 部署并启动 HTTP 服务器

sudo containerlab deploy -t mss-37.clab.yml
docker exec -d clab-mss-37-server python3 -m http.server 80
docker exec clab-mss-37-r ip -br link show eth2   # mtu 1400

Enter fullscreen mode Exit fullscreen mode

3. 查看不钳制时的 SYN MSS

docker exec -d clab-mss-37-server sh -c 'tcpdump -i eth1 -n -c1 "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0" > /tmp/syn.txt 2>&1'
docker exec clab-mss-37-client curl -s --max-time 4 http://10.0.8.2/ >/dev/null
docker exec clab-mss-37-server grep -oE 'mss [0-9]+' /tmp/syn.txt

Enter fullscreen mode Exit fullscreen mode

你应看到 mss 1460 —— 客户端本地 1500 − 40。

4. 在 r 上启用 MSS 钳制

docker exec clab-mss-37-r iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Enter fullscreen mode Exit fullscreen mode

5. 再次查看 SYN MSS

docker exec -d clab-mss-37-server sh -c 'tcpdump -i eth1 -n -c1 "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0" > /tmp/syn2.txt 2>&1'
docker exec clab-mss-37-client curl -s --max-time 4 http://10.0.8.2/ >/dev/null
docker exec clab-mss-37-server grep -oE 'mss [0-9]+' /tmp/syn2.txt

Enter fullscreen mode Exit fullscreen mode

现在是 mss 1360 —— r 已将其钳制到 1400 − 40。

预期输出

  • r 的 eth2: mtu 1400
  • 不钳制时:服务器看到 SYN 携带 mss 1460
  • 钳制后:服务器看到 mss 1360
  • mangle FORWARD 链显示 TCPMSS clamp to PMTU 规则。

工作原理

MSS 钳制即“在开始时告知两端一个安全的报文段大小,而不是依赖 PMTUD”。

  • MSS。 TCP 在一个报文段中能接受的最大净荷。仅在 SYN 中通告,值为 本地 MTU − 40(IPv4):MTU 1500 → 1460。发送方将报文段大小限制为对端通告的 MSS。
  • 盲区。 每个端点只知道自己接口的 MTU。即使路径中存在更窄的链路(此处为 1400-MTU 的 r–server 链路),SYN 中的 MSS 也不会反映它。
  • PMTUD 与黑洞(Lab 25)。当设置 DF 的大报文段遇到窄链路时,路由器返回 ICMP “需要分片”,发送方缩小尺寸。但若该 ICMP 被过滤,发送方永远不知道;大报文段持续被丢弃,连接挂起——这就是黑洞
  • 钳制。 路径上的路由器将经过 SYN 中的 MSS 选项 重写为出接口 MTU − 40(若通告值更大)。本实验中,r 将 1460 降为 1360。两端随后以 1360 或更小发送,因此报文段始终适配 1400 链路——即使 PMTUD 失效也不会黑洞。仅重写 SYN 就足够,因为 MSS 仅在 SYN 中协商。

核心洞见:路径上的路由器将端点不可见的路径最小 MTU 注入 SYN 的 MSS,使两端从第一个字节起就发送适配的报文段。 这是隧道场景下的常用手段。

常见陷阱

  • 混淆 MSS 与 MTU。 MTU 是整个 IP 包;MSS 是 TCP 净荷。IPv4 中,MSS = MTU − 40。
  • 认为 MSS 可重新协商。 它仅在 SYN 中协商,不会随数据报文段改变——这正是重写 SYN 的 MSS 选项就足够的原因。
  • 认为 PMTUD 就够了。 ICMP 过滤会导致黑洞。钳制是保险。
  • 认为只需降低端点的 MTU。 路径中深处的窄链路对端点不可见。必须由路径上的路由器执行重写。
  • 期望它对 UDP 有帮助。 MSS 是 TCP 概念;UDP 是另一回事。
  • 钳制值。 --clamp-mss-to-pmtu 基于出接口 MTU 推导;固定值请用 --set-mss

清理

sudo containerlab destroy -t mss-37.clab.yml --cleanup

Enter fullscreen mode Exit fullscreen mode

若你使用 labctl.sh run mss-37,脚本会在结束时自动执行 destroy

理解自测

  1. 什么是 TCP MSS?它与 MTU 有什么区别(IPv4 关系)?
  2. MSS 何时协商?能否在 SYN 之外改变?
  3. 为什么 SYN 中的 MSS 无法反映路径中更窄的链路?
  4. PMTUD 何时会黑洞?
  5. MSS 钳制重写什么、在何处重写?本实验中 1460 为什么变成 1360?
  6. 为什么隧道(WireGuard/VXLAN/GRE)特别需要钳制?

参考

已验证运行日志(2026-07-07)

本实验已在真实硬件上确认可复现。

环境:

  • Ubuntu 26.04 LTS(内核 7.0.0-27-generic,x86_64)
  • Docker 29.1.3
  • containerlab 0.77.0
  • client / r / server:nicolaka/netshoot:latestiptablestcpdumpcurlpython3

运行 PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run mss-37 执行 deploy → verify → destroy,verification.json 返回 "status": "verified"

不钳制 → SYN 携带 MSS 1460

在服务器的 eth1 上捕获客户端的 SYN:

Flags [S], seq ..., options [mss 1460, ...]

Enter fullscreen mode Exit fullscreen mode

这就是 MSS 1460,来自客户端链路 MTU 1500 —— 对 r–server 链路(MTU 1400)而言过大,连接依赖 PMTUD。

在 r 上钳制后 → SYN 携带 MSS 1360

mangle FORWARD: TCPMSS  tcp  flags:0x06/0x02  TCPMSS clamp to PMTU

Enter fullscreen mode Exit fullscreen mode

添加 iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 后,服务器捕获到的同一客户端 SYN 显示:

mss 1360

Enter fullscreen mode Exit fullscreen mode

r 将 SYN 的 MSS 选项从 1460 → 1360(1400 − 40)重写,以匹配其出接口(eth2,MTU 1400)。此后两端发送 1360 字节或更小的报文段,因此一切都适配窄链路——即使 ICMP 被过滤也不会黑洞。

服务器看到的 SYN MSS
不钳制 1460
钳制 1360

清理

containerlab destroy -t mss-37.clab.yml --cleanup

Enter fullscreen mode Exit fullscreen mode


这就是 MSS 钳制:路径路由器上的一条 iptables 规则,让每个 TCP 连接在发送第一个字节前就协商好安全的报文段大小——无需 PMTUD 正常工作。

探索完整的 Protocol Lab 系列:github.com/pathvector-studio/protocol-lab。如果这些实验对你有帮助,请在 GitHub 上 ⭐ 为仓库点星——这确实能帮助他人发现该项目。

接下来,我们将继续探讨当教科书机制失效时,真实网络如何保持可靠。