开启 TCP 时间戳以后(Linux 下默认开启),每个 TCP 头里都会带两个 32 位字段:TSval 和 TSecr
TSval:发送方当前的时间戳计数值
TSecr:对端最近一次收到的 TSval 的回显
注:TSecr 只有在 ACK 位置 1 时才有意义;如果 ACK 没置位,发送方应把 TSecr 设为 0,接收方也必须忽略它
工作模式
协商
TCP 时间戳需要在三次握手里协商,双方都支持才会启用。
客户端可以在初始 SYN 里带 Timestamp;
服务端只有在看见对方 SYN 里带了 Timestamp 时,才可以在 SYN-ACK 里带;
只有当 SYN 和 SYN-ACK 都带了 Timestamp,才算真正协商成功;
行为:
一旦协商成功,此后这个连接上的每个非 RST 段都应该带 Timestamp
RFC 1323 建议 RST 不带 Timestamp,且即使带了也应该被对端忽略
如果某个段本来“应该有”但却没带,接收方通常应静默丢弃这个段,但不能因为这一个段没带就直接中止整个连接;
如果一个连接压根没在三次握手里协商 Timestamp,那后续突然冒出来的 Timestamp 选项要忽略。
值
TCP 时间戳不要求发送端和接收端时钟同步。对接收端来说,这个时间戳只是一个单调不减的序号,本质上更像“逻辑序号”,不是墙上时钟。
RFC 对 Timestamp clock 建议时间戳时钟频率大概在:1ms / tick 到 1s / tick 这个区间。
太慢的话,RTT 测量太粗糙。
太快的话,容易出现时间戳回绕
用处
tcp_timestamps 参数是默认开启的。开启了 tcp_timestamps 参数,TCP 头部就会使用时间戳选项,它有两个好处:
便于精确计算 RTT (RTTM)
能防止序列号回绕(PAWS)
防止序列号回绕(PAWS)
序列号是一个 32 位的无符号整型,上限值是 4GB,超过 4GB 后就需要将序列号回绕进行重用
这在以前网速慢的年代不会造成什么问题,但在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。为了解决这个问题,就需要有 TCP 时间戳。
防回绕序列号算法要求连接双方通过最近一次收到的数据包的时间戳来维护一个「最近可信的时间戳参考值 TS.Recent」,每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中它的 TSval 比这个参考值“老很多”,那它就可能是旧包,于是被拒绝。
注意:对于 RST 报文,其时间戳即使过期了,只要 RST 报文的序列号在对方的接收窗口内,也是能被接受的。
RFC 1323:It is recommended that RST segments NOT carry timestamps, and that RST segments be acceptable regardless of their timestamp. Old duplicate RST segments should be exceedingly unlikely, and their cleanup function should take precedence over timestamps.
便于精准计算 RTT(RTTM)
传统 TCP 也能估 RTT,但它容易遇到一个经典麻烦:重传歧义。
例如:
你发了一个数据段 A;
超时了,你又重传了 A;
后面 ACK 回来了。
问题来了:
这个 ACK 到底是在确认第一次发的 A,还是重传后的 A?如果分不清,RTT 样本就不准。
有 Timestamp 后,你发出去的每个段都带自己的 TSval。对方 ACK 时,把它最近收到的那个 TSval 回显到 TSecr 里。
于是你一看 ACK 里的 TSecr,就知道它到底是在回应哪一次发送。
RFC 也明确说,收到 ACK 后如果启用了 Timestamp,可以用 当前本地时间戳时钟 - 对端回显回来的 TSecr 来得到 RTT 样本。
另外,RFC 说:收到 TSecr 后,只有当这个 ACK 真的推进了发送窗口左边界(也就是确认了新的数据,SND.UNA 增加),这个 RTT 样本才适合用来更新平均 RTT。
否则可能会被发送间隙、延迟 ACK 等因素“灌水”。
举个直观例子:
A 发了一段数据,B 回了 ACK;
然后 A 很久没再发数据;
过了一阵子,A 又发新数据,里面带着对 B 之前时间戳的回显;
这时 B 从 TSecr 里反推出的“往返时间”,会夹杂这段空闲时间,不是真实网络 RTT。
RFC 专门写了这个场景,并给出规则:别把这种没推进 SND.UNA 的样本拿去更新平滑 RTT。
Linux
Linux 里对应的内核参数是:net.ipv4.tcp_timestamps
这个配置挂在 ipv4 下,但也同样会对 ipv6 生效
0:关闭
1:开启,且每个连接使用随机偏移(默认)
2:开启,但不使用随机偏移
为什么默认 “+ 随机偏移”
因为如果不加随机偏移,远端可能从 TSval 大致推断出主机 uptime,进而做指纹识别或辅助攻击(参考)
值
曾经,Linux 以本地时钟计数(jiffies)作为时间戳的值(Linux 4.13 之前),但现在已经不是了。
目前,默认情况下,Linux 的 TCP 时间戳实际是 1 ms / tick。
可选: 1 us / tick
如果需要,可以为路由开启 tcp_usec_ts 选项(目前没有全局 sysctl 配置)
忽略的特殊处理
Linux 在 PAWS 检查做了一个特殊处理,如果一个 TCP 连接连续 24 天不收发数据则在接收第一个包时基于时间戳的 PAWS 会失效,也就是可以 PAWS 函数会放过这个特殊的情况,认为是合法的,可以接收该数据包。
24 天这个原因:RFC 建议最小的是 1ms + 1,也就是 24.8 天回绕一次,这也是 Linux 默认的行为,因此用 24 天重置一次来解决长时间空闲连接的问题
问题
时间戳回绕
时间戳的大小是 32 bit,所以理论上也是有回绕的可能性的。
如果时钟计数加 1 需要1ms,则需要约 24.8 天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错。
如果时钟计数提高到 1us 加1,则回绕需要约71.58分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过70分钟,只是如果70分钟没有报文收发则会有一个包越过PAWS(这种情况会比较多见,相比之下 24 天没有数据传输的TCP连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题;
如果时钟计数提高到 0.1 us 加 1,回绕需要 7 分钟多一点,这时就可能会有问题了,连接如果 7 分钟没有数据收发就会有一个报文越过 PAWS,对于TCP连接而言这么短的时间内没有数据交互太常见了吧!这样的话会频繁有包越过 PAWS 检查,从而使得旧包混入数据中的概率大大增加;
现代 Linux 下因为最快只会到 1us/tick,所以通常不会出现时间戳回绕的问题。
要解决低版本 Linux 时间戳回绕的问题,可以考虑以下解决方案:
1) 升级内核版本到 4.13 以上
2) 修改 Linux 源码,将一个与时钟频率无关的值作为时间戳,时钟频率可以增加但时间戳的增速不变
随着时钟频率的提高,TCP在相同时间内能够收发的包也会越来越多。如果时间戳的增速不变,则会有越来越多的报文使用相同的时间戳。这种趋势到达一定程度则时间戳就会失去意义,除非在可预见的未来这种情况不会发生。