三次握手里的序号、确认号到底怎么算?初始序号是怎么选的?SYN 没人回时多久重发一次?connect() 和 accept() 谁先返回?这篇抓真实的握手包,连续建立 2000 个连接看 ISN 的分布,并用 netlab 延迟量出两端“完成握手”的时刻差。
lo0 SYN:客户端→服务端,标志 SYN,序号 1721712522,确认号 0 lo0 SYN-ACK:服务端→客户端,标志 ACK+SYN,序号 2719271520,确认号 1721712523 lo0 ACK:客户端→服务端,标志 ACK,序号 1721712523,确认号 2719271521 关系:服务端的确认号 1721712523 = 客户端 ISN 1721712522 + 1:True;客户端最后的确认号 2719271521 = 服务端 ISN 2719271520 + 1:True;第三个包的序号 1721712523 = 客户端 ISN + 1:True lo0 上 connect() 用时 4758 µs en0 到 example.com:connect() 用时 176.7 ms;抓包里 SYN → SYN-ACK 间隔 175.2 ms(就是一个往返),connect 返回时刻与收到 SYN-ACK 基本同时
SYN 占一个序号。SYN-ACK 的确认号是客户端 ISN + 1,最后一个 ACK 的确认号是服务端 ISN + 1,序号是客户端 ISN + 1。
抓到 2000 个 SYN,不同的 ISN 2000 个;范围 561029 ~ 4289965881(32 位空间 0 ~ 4294967295) 相邻两个连接的 ISN 之差(模 2^32)中,小于 2^20 的只有 0 个(若是按时钟递增会几乎全部很小);均值 0.493×2^32(均匀随机应接近 0.5);前 8 个 ISN:[1391584131, 2407812438, 2883483316, 2297013526, 237829798, 3735851509, 169266384, 3701917642] 把 32 位空间分 16 份,每份的个数:[132, 119, 134, 160, 123, 117, 111, 132, 118, 117, 117, 125, 115, 114, 148, 118](均匀随机时每份约 125) 老系统用 4 微秒一次的时钟递增 ISN,容易被猜到后伪造连接(RFC 6528 要求加哈希随机化);这里看到的是随机分布
2000 个连接的 ISN 随机分布在整个 32 位空间,不是按时钟递增。这是对抗伪造连接的第一道防线。
net.inet.tcp.keepinit: 75000 net.inet.tcp.always_keepalive: 0 (net.inet.tcp.syn_retries 在本机不存在) 25 秒的超时里 connect 结果 TimeoutError,用时 25.0 秒;抓到 SYN 9 个,发送时刻(秒):[0.0, 1.0, 2.0, 3.01, 4.01, 5.01, 7.01, 11.01, 19.01] 相邻 SYN 的间隔:[1.0, 1.0, 1.01, 1.0, 1.0, 2.0, 4.0, 8.0](固定 1 秒还是指数退避?逐步变长);所有 SYN 的序号相同:True;带 ECN 的 SYN 1 个、不带的 8 个
没有回应时本机前 6 个 SYN 间隔 1 秒,之后 2、4、8 秒退避;ECN 只有第一个 SYN 带。总超时由 keepinit(75 秒)决定。
本机 lo0:我的 SYN 选项 {'MSS': 16344, 'WScale': 6, 'TS': True, 'SACK': True};对方 SYN-ACK 选项 {'MSS': 16344, 'WScale': 6, 'TS': True, 'SACK': True},窗口字段 65535,标志 ACK+SYN
example.com:我的 SYN 选项 {'MSS': 1460, 'WScale': 6, 'TS': True, 'SACK': True};对方 SYN-ACK 选项 {'MSS': 1400, 'SACK': True, 'TS': True, 'WScale': 13},窗口字段 65535,标志 ACK+SYN+ECE
另一个公网主机:我的 SYN 选项 {'MSS': 1460, 'WScale': 6, 'TS': True, 'SACK': True};对方 SYN-ACK 选项 {'MSS': 1452, 'SACK': True, 'TS': True, 'WScale': 13},窗口字段 65535,标志 ACK+SYN+ECE
双方各自在 SYN / SYN-ACK 里通告自己的 MSS、窗口缩放、是否支持时间戳和 SACK;只有双方都带了某个选项,这个功能才会启用(窗口缩放要求两边都带,否则都不启用)MSS、窗口缩放、时间戳、SACK 在握手里一次协商;窗口缩放要求双方的 SYN 都带才启用。
往返 100 ms:客户端 connect() 在 101 ms 时返回(1 个往返:SYN → SYN-ACK),服务端 accept() 在 151 ms 时返回(再过半个往返,等到客户端的第三个 ACK 到达) 所以客户端先认为连接建立,服务端后认为;如果第三个 ACK 丢了,客户端会以为已连接并开始发数据,服务端还在 SYN_RCVD(下一篇状态机再看)
客户端 connect() 在收到 SYN-ACK 时返回,服务端 accept() 要等第三个 ACK。差半个往返。