bigywater.tech
网络协议 · 65

MSS 协商与钳制:隧道和 PPPoE 里的 MSS

MSS 是 TCP 双方在握手时说好的最大段大小,取决于接口的 MTU,也常被路径上的设备“钳制”得更小。这篇看 SYN 里的 MSS 从哪来、example.com 把我们的发送大小压到了多少、macOS 上手动设置 TCP_MAXSEG 能做到什么,最后列出 PPPoE、GRE、WireGuard 这些隧道该用的 MSS。

1460 → 1400
本机在 SYN 里通告的 MSS 与 example.com 在 SYN+ACK 里通告的 MSS
1388
实测发给 example.com 的最大数据段载荷 = 1400 − 12 字节时间戳
EINVAL
TCP_MAXSEG 在 connect 之前或 listen 之前设置的结果

MSS 的来源

实测 · 本机执行输出
  接口 MTU:lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 | en0 mtu 1500
  net.inet.tcp.mssdflt = 512(没有路由信息时的默认 MSS);net.inet.tcp.minmss = 216
  lo0:本机 SYN 的 MSS = 16344(= 16384 − 40 = 16344),对端 SYN+ACK 的 MSS = 16344
  en0 → example.com:本机 SYN 的 MSS = 1460(= 1500 − 40 = 1460),example.com SYN+ACK 的 MSS = 1400;内核 tcp_info 里的 maxseg = 1388

SYN 里的 MSS = 出口接口 MTU − 40:lo0 16344,en0 1460。

取小者再减时间戳

实测 · 本机执行输出
  请求体发出 142964 / 200000 字节(BrokenPipeError [Errno 32] Broken pipe)
  本机发出的数据段 30 个,载荷的分布(字节 → 个数,前几名):[(1388, 28), (81, 2)]
  本机通告 1460、对方通告 1400 → 取小者 1400;减去 12 字节时间戳 = 1388;实测最大载荷 1388
  对照:若对方没有把 MSS 压到 1400,本机可以发 1460 − 12 = 1448 字节的段

双方通告取小者(1460 对 1400 → 1400),再扣 12 字节时间戳,实测最大载荷 1388。

手动设置

实测 · 本机执行输出
  设在客户端 connect 之前:setsockopt EINVAL;SYN 的 MSS 16344,SYN+ACK 的 MSS 16344;getsockopt(TCP_MAXSEG) 读回 客户端 16332 / 服务端 16332;服务端→客户端 实测最大数据段载荷 16332 字节
  设在监听套接字 listen 之前:setsockopt EINVAL;SYN 的 MSS 16344,SYN+ACK 的 MSS 16344;getsockopt(TCP_MAXSEG) 读回 客户端 16332 / 服务端 16332;服务端→客户端 实测最大数据段载荷 16332 字节
  设在发送方(accept 出来的)连上之后:setsockopt 成功;SYN 的 MSS 16344,SYN+ACK 的 MSS 16344;getsockopt(TCP_MAXSEG) 读回 客户端 16332 / 服务端 1000;服务端→客户端 实测最大数据段载荷 1000 字节

TCP_MAXSEG 在 connect / listen 之前是 EINVAL;连上之后设置能把本端的段限制到 1000,但握手里的 MSS 已定,不再变化。

隧道与钳制

实测 · 本机执行输出
  标准以太网                              MTU 1500 → IPv4 MSS 1460 / IPv6 MSS 1440
  PPPoE(8 字节封装)                      MTU 1492 → IPv4 MSS 1452 / IPv6 MSS 1432
  GRE 隧道(24 字节)                      MTU 1476 → IPv4 MSS 1436 / IPv6 MSS 1416
  本机 utun1(接口 MTU 实测 1380)           MTU 1380 → IPv4 MSS 1340 / IPv6 MSS 1320
  WireGuard 常见默认                     MTU 1420 → IPv4 MSS 1380 / IPv6 MSS 1360
  IPv6 最小 MTU                        MTU 1280 → IPv4 MSS 1240 / IPv6 MSS 1220
  MSS 钳制只改 SYN 与 SYN+ACK 里的选项(路由器重写),不碰数据段;和 PMTUD(靠 ICMP 的 3/4 反馈)互补——ICMP 被防火墙丢弃时 PMTUD 失效,钳制仍然有效

PPPoE 1452、GRE 1436、WireGuard 1380 等是计算值;钳制由路由器改写 SYN 里的 MSS,与 PMTUD 互补。

分步演示

MSS 协商与钳制
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一节 lo0 与 en0 → example.com:80 抓包;第二节的 POST 请求体只发出 142964 字节,服务器就关了连接;第三节在 lo0 上;第四节全是计算值
    • 没有验证:任何一台真正做 MSS 钳制的路由器(本机不是路由器,也没有隧道设备可用);example.com 把 MSS 压到 1400 的原因(服务器自己的设置还是路径上的设备,没有区分);IPv6 的 MSS 协商;路由表里按目的地缓存的 MTU / MSS 对新连接的影响;TCP_MAXSEG 在 en0 真实连接上的行为
    京ICP备2026067502号-1京公网安备11010502063487号