bigywater.tech
网络协议 · 63

TCP 时间戳选项:RTT 测量与 PAWS

TCP 时间戳选项一箭双雕:让发送方在重传存在时仍能放心测 RTT,又让接收方能挡掉绕了一圈回来的旧包(PAWS)。这篇在 macOS 上抓包看它的格式、开销、时钟,用 TSecr 配对测 RTT,在丢包里验证 ACK 回显的是哪一次发送。PAWS 的注入验证做不到,只给计算。

1000 Hz
本机与 example.com 服务器的时间戳钟:1 ms 一个刻度,49.7 天回绕
34 / 36
被重传的段里,有 ACK 回显了重传那次的 TSval 的个数
50 vs 100 ms
单个抓包点用 TSecr 配对读到的 RTT 与内核 rttcur 的差别

格式与开销

实测 · 本机执行输出
  lo0:SYN 里 TSval=True(TSecr = 0),SYN+ACK 里 TS=True;SYN 的 MSS = 16344;数据段里最大载荷 = 16332 字节;数据段 TCP 头长 32 字节(20 基本 + 12 时间戳选项)
  所以 lo0 上 MSS 16344 − 12 字节时间戳选项 = 16332 字节有效载荷
  example.com:本机 SYN 里 TS=True,服务器 SYN+ACK 里 TS=True(两边都有才会启用);SYN+ACK 的 MSS = 1400

kind 8、10 字节加两个 NOP 共 12 字节,两端 SYN 都带才启用;lo0 上有效载荷从 MSS 16344 变成 16332。

时钟

实测 · 本机执行输出
  net.inet.tcp.randomize_timestamps: 1(1 = 每条连接的 TSval 起点加一个随机偏移,所以 TSval 的绝对值没有意义,只有差值有意义)
  lo0:客户端发出的 keepalive 探测 9 个,其中带时间戳选项的 0 个(TCP 头长 [20] 字节);服务端回的 ACK 9 个,带时间戳选项的 9 个(头长 [32] 字节)
  服务端 ACK 里的 TSecr(回显客户端最近一个带时间戳的包的 TSval):[949424606]——只有一个值,因为探测没有带时间戳,服务端没有新的可回显
  服务端 TSval 的变化(相邻差值):[1001, 1000, 1000, 1000, 1001, 1001, 1001, 1001]
  本机的时间戳钟:1000.0 次/秒(Hz);每个刻度 1.00 ms;32 位 TSval 绕一圈要 49.7 天(计算)
  example.com:握手之后、关闭之前(第 1~7.5 秒)本机发出的纯 ACK(keepalive 探测)6 个,带时间戳的 0 个;服务器发来的带时间戳的包 9 个
  服务器 TSval 随时间变化的速率 1000.1 Hz(每个刻度 1.00 ms)

回 ACK 的一端 TSval 每秒增加约 1000;每条连接起点有随机偏移。keepalive 探测不带时间戳。

用 TSecr 量 RTT

实测 · 本机执行输出
  凭 TSecr 配对成功 20 个数据段(共发 20 条)。dummynet 让同一个包在 lo0 上被抓到不止一次(延迟前后各一份),所以“RTT”有两种读法:
  数据段第一次被抓到 → 对应 ACK 第一次被抓到:中位 50.0 ms(最小 49.5 / 最大 51.1)——只包含一个方向的延迟
  数据段第一次被抓到 → 对应 ACK 最后一次被抓到(= 真正送达):中位 50.0 ms(最小 49.5 / 最大 51.1)——两个方向都包含
  内核 tcp_info:rttcur = 100 ms,srtt = 93 ms,rttvar = 13 ms

TSecr 配对 20/20 成功,但单个抓包点读到的差值不是真正 RTT(50 对 100 ms)。

Karn 问题

实测 · 本机执行输出
  发了 150 个不同的数据段;其中被重传(同一序号出现不同 TSval)的 36 个
    序号 24194:发送过 2 次,TSval = [3326993437, 3326993503],间隔(ms)= [66]
    序号 24294:发送过 2 次,TSval = [3326993483, 3326993573],间隔(ms)= [90]
    序号 24344:发送过 2 次,TSval = [3326993508, 3326993573],间隔(ms)= [65]
  在被重传的 36 个段里:有 ACK 回显了某次重传的 TSval 的 34 个,有 ACK 回显了最初那次的 TSval 的 0 个

36 个被重传的段中,34 个 ACK 回显重传的 TSval,发送方因此分得清 ACK 对应哪次发送。

PAWS

实测 · 本机执行输出
     100 Mbit/s 的链路上,2^32 字节的序号空间     343.6 秒回绕一次(计算)
    1000 Mbit/s 的链路上,2^32 字节的序号空间      34.4 秒回绕一次(计算)
   10000 Mbit/s 的链路上,2^32 字节的序号空间       3.4 秒回绕一次(计算)
  100000 Mbit/s 的链路上,2^32 字节的序号空间       0.3 秒回绕一次(计算)
  时间戳钟 1000 Hz:TSval 32 位回绕要 49.7 天,PAWS 的比较窗口(2^31)是 24.9 天;规范要求时间戳钟的刻度在 1 ms~1 s 之间,保证一个包在网络里的寿命(MSL)远小于绕一圈的时间(计算)

序号 32 位回绕:10 Gbit/s 下 3.4 秒。TSval 49.7 天才回绕,可以挡掉旧包。没有实测。

分步演示

时间戳与 PAWS
本步的对象

    本文用到的工具与边界

    • 本机 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 抓包;第三、四节用 netlab(每方向 50 ms 与 20 ms,丢包 0% 与 20%);抓包点读到的 RTT 只有一半的解释是推断;重传数量随机,每次运行不同
    • 没有验证:PAWS 的实际丢包行为(需要注入旧报文,没有原始套接字);关闭时间戳(本机没有找到对应的 sysctl)后的行为;TSval 起点随机偏移的范围;kernel 内部如何用 TSecr 量 RTT(只从抓包侧配对);时间戳泄露开机时间的隐私问题(randomize_timestamps=1 正是对它的缓解,但没有测试其它系统)
    京ICP备2026067502号-1京公网安备11010502063487号