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