我宣布现在没人比我更懂 TCP。

见邮件列表 Two different places between TCP socket behavior and RFC documents

虽然没有肉眼可见的 vulunerability / exposure 但多少还是被承认为 BUG 了…对方锐意修复中。

问题1. 关闭时有待处理数据,但未能发送 RST

根据 RFC 2525 第 2.17 节,当 close() 一个套接字时,如果其接收缓冲区中仍有待读取的数据,应该发送 RST(复位)。

根据 RFC 1122:主机可以实现一种“半双工”TCP 关闭序列,…不能继续读取数据…如果接收缓冲区中仍有待处理数据时进行 CLOSE 调用,或者在调用 CLOSE 后接收到新数据,TCP 应该发送一个 RST 以表明数据已丢失。

然而,FreeBSD 的 TCP 套接字并非如此。以下是 TCPDUMP 的输出,显示在套接字有待处理数据时调用 close() 发出的是 FIN(结束)而非 RST:

1
2
3
4
5
6
7
A > B: Flags [S], seq 2636678338, win 65535, length 0
B > A: Flags [S.], seq 1969223298, ack 2636678339, win 65535, length 0
A > B: Flags [.], ack 1, win 1277, length 0
A > B: Flags [P.], seq 1:6, ack 1, win 1277, length 5
B > A: Flags [.], ack 6, win 1277, length 0
B > A: Flags [F.], seq 1, ack 6, win 1277, length 0
A > B: Flags [.], ack 2, win 1277, length 0

无论是 close()shutdown(SHUT_RDWR)shutdown(SHUT_RD),还是 SO_LINGER 设置为开启或关闭,都得到相同的结果。而在 Linux 上,同样的操作会得到以下结果:

1
2
3
4
5
6
A > B: Flags [S], seq 2879877684, win 65495, length 0
B > A: Flags [S.], seq 1538598692, ack 2879877685, win 65483, length 0
A > B: Flags [.], ack 1, win 512, length 0
A > B: Flags [P.], seq 1:6, ack 1, win 512, length 5
B > A: Flags [.], ack 6, win 512, length 0
B > A: Flags [R.], seq 1, ack 6, win 512, length 0

当然,虽然没有发送 RST,但是这里不会像预期的那样导致连接一直保持。因为它的 FIN segment 实际上重新打开了窗口。

1
2
3
4
5
6
7
A > B: Flags [.], ack 1, win 510, length 0
B > A: Flags [.], ack 66137, win 0, length 0
A > B: Flags [.], ack 1, win 510, length 0
B > A: Flags [.], ack 66137, win 0, length 0
A > B: Flags [.], ack 1, win 510, length 0
B > A: Flags [.], ack 66137, win 0, length 0
B > A: Flags [F.], seq 1, ack 66137, win 1027, length 0

我们通过发送大量数据,使得对面最终通告了 0 窗口。但在对面进行close() 后,它又打开了窗口。此时发送端就又可以发送数据,包括发送 FIN 了。

如果发送数据,而对面已经做了接受方向的关闭,则会得到一个 RST;如果发送 FIN,也就能完成四次挥手。看起来都能够使连接关闭。但这里的问题在于,打开接收窗口与关闭接受方向显然是矛盾的。尤其是,打开窗口邀请对面发送数据,而这数据是必定会被自己所拒绝的,这造成了一些不必要的网络流量以及处理资源的浪费。

另一方面,FIN 标志着优雅地关闭了连接。但这里显然并不优雅。我们需要用清晰的报文来标志异常。

数据发送/强制重置比起主动重置除了少量资源消耗以外,使得关闭这件事整体变得复杂了….

问题2. 在 SYN-RECEIVED 状态下对旧序列 segment 回复 RST 而非确认

根据 RFC 793 第 69 页:如果一个传入的 segment 不可接受,应该回复一个确认(这里“应该”没有大写)。

这应该适用于包括 SYN-RECEIVED 在内的所有状态,但 FreeBSD 的 TCP 套接字并非如此。我用手动构造的 TCP 分段发现了这个问题:

1
2
3
4
A > B: Flags [S], seq 1, win 8192, length 0
B > A: Flags [S.], seq 4054810353, ack 2, win 65535, length 0
A > B: Flags [.], ack 1, win 8192, length 0
B > A: Flags [R], seq 4054810354, win 0, length 0

预期的行为是发送一个空的确认:

1
2
3
4
A > B: Flags [S], seq 1, win 8192, length 0
B > A: Flags [S.], seq 3620804602, ack 2, win 65495, length 0
A > B: Flags [.], ack 1, win 8192, length 0
B > A: Flags [.], ack 1, win 65495, length 0

这与 Linux 的行为是一致的。

上文发送后,维护者似乎不是很明白,于是我作了如下解释:

考虑以下 TCP 握手序列:

  1. 套接字 A 向处于 TCP_LISTEN 状态的套接字 B 发送一个 SYN(同步)分段 <CTL=SYN><SEQ=x>
  2. 套接字 B 转换为 TCP_SYN_RECV 状态,并以 <CTL=SYN,ACK><SEQ=y><ACK=x+1> 进行响应。
  3. 套接字 A 没有发送预期的 <CTL=ACK><SEQ=x+1><ACK=y+1> 来完成三次握手,而是错误地发送了 <CTL=ACK><SEQ=x><ACK=y+1>

根据 RFC 文档,对这种窗口外 ACK 的恰当响应应是一个空的 ACK 分段 <CTL=ACK><SEQ=y+1><ACK=x+1>。之后,套接字 B 应等待一个有效的 ACK,或者在必要时重新传输 SYN-ACK。

然而,在 FreeBSD 当前的实现中,它发送了一个 RST(复位)分段,格式为 <CTL=RST><SEQ=y+1>,这会 prematurely 终止连接。

这种行为似乎偏离了 RFC 的指导,并可能导致不必要的连接重置。

对于窗口不同程度的偏离都观察到了这一现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. expect seq=2, actual seq=0x80000001
2. expect seq=2, actual seq=0x80000002
3. expect seq=2, actual seq=0x80000003
4. expect seq=2, actual seq=0x90000000
5. expect seq=2000001 (0x1e8481), actual seq=1
6. expect seq=1, actual seq=2000001 (0x1e8481)
All of them, under FreeBSD, give a RST reply. Here is the tcpdump of the first
case.

A > B: Flags [S], seq 1, win 8192, length 0
0x0000: 4500 0028 0000 4000 4006 0000 7f00 0001
0x0010: 7f00 0001 22b9 22b8 0000 0001 0000 0000
0x0020: 5002 2000 4c6e 0000
B > A: Flags [S.], seq 1643153760, ack 2, win 65535, options [mss
16344], length 0
0x0000: 4500 002c 0000 4000 4006 0000 7f00 0001
0x0010: 7f00 0001 22b8 22b9 61f0 8960 0000 0002
0x0020: 6012 ffff fe20 0000 0204 3fd8
A > B: Flags [.], seq 2147483648, ack 1, win 8192, length 0
0x0000: 4500 0028 0000 4000 4006 0000 7f00 0001
0x0010: 7f00 0001 22b9 22b8 8000 0001 61f0 8961
0x0020: 5010 2000 e10d 0000
B > A: Flags [R], seq 1643153761, win 0, length 0
0x0000: 4500 0028 0000 4000 4006 0000 7f00 0001
0x0010: 7f00 0001 22b8 22b9 61f0 8961 0000 0000
0x0020: 5004 0000 fe1c 0000