前言
默认情况下,Wireshark 的 TCP 解析器会跟踪每个 TCP 会话的状态,并在检测到问题或潜在问题时提供额外的信息。在第一次打开捕获文件时,会对每个 TCP 数据包进行一次分析,数据包按照它们在数据包列表中出现的顺序进行处理。可以通过 “Analyze TCP sequence numbers” TCP 解析首选项启用或禁用此功能。
TCP 分析展示
在 TCP 分析中和 TCP 零窗口相关的实际上有三种信息,分别是:TCP ZeroWindow
、TCP ZeroWindowProbe
、TCP ZeroWindowProbeAck
。实际运行环境中,有时是单独出现的,譬如 TCP ZeroWindow
,有时是一起出现的,也就是出现了零窗口,之后就会出现需要为恢复窗口而进行的零窗口探测和零窗口探测确认。
在数据包文件中进行 TCP 分析时,关于 “TCP ZeroWindowProbe”、“TCP ZeroWindowProbeAck” 一般是如下显示的,包括:
- Packet List 窗口中的 Info 信息列,以 [TCP ZeroWindowProbe]、[TCP ZeroWindowProbeAck] 黑底红字进行标注;
- Packet Details 窗口中的 TCP 协议树下,在 [SEQ/ACK analysis] -> [TCP Analysis Flags] 中定义该 TCP 数据包的分析说明。
TCP ZeroWindowProbe 和 ZeroWindowProbeAck 定义
实际在 TCP 分析中,关于 TCP ZeroWindowProbe 和 ZeroWindowProbeAck 相关的定义也相对简单,主要说明如下:
- TCP ZeroWindowProbe
当满足以下多个条件时设置,包括:
- 当 Seq Num 等于之前下一个期望的 Seq Num
- TCP 段大小为 1
- 反方向最后一次看到的接收窗口大小为零时设置
影响 Fast Retransmission
、Out-Of-Order
或 Retransmission
。
Set when the sequence number is equal to the next expected sequence number, the segment size is one, and last-seen window size in the reverse direction was zero.
If the single data byte from a Zero Window Probe is dropped by the receiver (not ACKed), then a subsequent segment should not be flagged as retransmission if all of the following conditions are true for that segment: * The segment size is larger than one. * The next expected sequence number is one less than the current sequence number.
This affects “Fast Retransmission”, “Out-Of-Order”, or “Retransmission”.
具体的代码如下,总的来说这段代码的作用是检测零窗口探测包,并对其做出适当的标记,以便 Wireshark 进行后续的分析和显示。 主要检测以下三个条件,如果都满足,则认为是一个零窗口探测数据包。
- 检查 TCP 段长度是否为 1 字节;
- 检查 Seq Num 是否等于之前下一个期望的 Seq Num;
- 检查反方向之前的接收窗口大小是否为 0。
/* ZERO WINDOW PROBE
* it is a zero window probe if
* the sequence number is the next expected one
* the window in the other direction is 0
* the segment is exactly 1 byte
*/
if( seglen==1
&& seq==tcpd->fwd->tcp_analyze_seq_info->nextseq
&& tcpd->rev->window==0 ) {
if(!tcpd->ta) {
tcp_analyze_get_acked_struct(pinfo->num, seq, ack, TRUE, tcpd);
}
tcpd->ta->flags|=TCP_A_ZERO_WINDOW_PROBE;
goto finished_fwd;
}
- next expected sequence number,为 nextseq,定义为 highest seen nextseq。
- 关于零窗口探测数据包的定义和说明,段大小为 1 字节,但在 linux 下实测是 0 字节,之后展开说明。
- TCP ZeroWindowProbeAck
当满足以下多个条件时设置,包括:
- TCP 段大小为 0
- 接收窗口大小为 0
- Seq Num 是否等于之前下一个期望的 Seq Num
- Ack Num 等于之前的 LastAck Num
- 反方向之前数据包是一个零窗口探针
替代 TCP Dup ACK
。
Set when the all of the following are true:
The segment size is zero.
The window size is zero.
The sequence number is equal to the next expected sequence number.
The acknowledgment number is equal to the last-seen acknowledgment number.
The last-seen packet in the reverse direction was a zero window probe.
Supersedes “TCP Dup ACK”.
具体的代码如下,总的来说这段代码的作用是用于检测零窗口探测的 ACK 包,并对其进行适当的标记和处理,以便 Wireshark 能够正确分析和显示 TCP 流量控制过程中的重要信息。代码逻辑如下,如果所有条件都满足,则认为该 ACK 数据包是对零窗口探测包的响应,包括:
- 检查 TCP 段长度是否为 0;
- 检查接收窗口大小是否为 0;
- 检查接收窗口大小与同方向之前接收窗口大小是否相同;
- 检查 Seq Num 是否等于同方向之前下一个期望的 Seq Num;
- 检查 ACK Num 是否等于同方向之前的 LastACK Num,或者是同方向之前的 LastACK Num+1;
- 检查反方向之前数据包是否是零窗口探测包;
- 检查当前数据包是否不是 SYN/FIN/RST 数据包。
/* ZERO WINDOW PROBE ACK
* It is a zerowindowprobe ack if it repeats the previous ACK and if
* the last segment in the reverse direction was a zerowindowprobe
* It also repeats the previous zero window indication
*/
if( seglen==0
&& window==0
&& window==tcpd->fwd->window
&& seq==tcpd->fwd->tcp_analyze_seq_info->nextseq
&& (ack==tcpd->fwd->tcp_analyze_seq_info->lastack || EQ_SEQ(ack,tcpd->fwd->tcp_analyze_seq_info->lastack+1))
&& (tcpd->rev->lastsegmentflags&TCP_A_ZERO_WINDOW_PROBE)
&& (flags&(TH_SYN|TH_FIN|TH_RST))==0 ) {
if(!tcpd->ta) {
tcp_analyze_get_acked_struct(pinfo->num, seq, ack, TRUE, tcpd);
}
tcpd->ta->flags|=TCP_A_ZERO_WINDOW_PROBE_ACK;
/* Some receivers consume that extra byte brought in the PROBE,
* but it was too early to know that during the WINDOW PROBE analysis.
* Do it now by moving the rev nextseq & maxseqtobeacked.
* See issue 10745.
*/
if(EQ_SEQ(ack,tcpd->fwd->tcp_analyze_seq_info->lastack+1)) {
tcpd->rev->tcp_analyze_seq_info->nextseq=ack;
tcpd->rev->tcp_analyze_seq_info->maxseqtobeacked=ack;
}
goto finished_fwd;
}
- next expected sequence number,为 nextseq,定义为 highest seen nextseq。
- lastack,定义为 Last seen ack for the reverse flow。
- 相对于文档介绍,代码中多了一些判断条件,包括像是与之前接收窗口的比较(是否都为 0),Ack Num 等于之前 Ack Num+1的场景。
Packetdrill 示例
根据上述 TCP ZeroWindowProbe
、TCP ZeroWindowProbeAck
定义和代码说明,起初我认为 packetdrill 是比较容易模拟出来的,只要满足当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,则启动零窗口探测定时器,在超时后发出零窗口探测包,段大小为 1 字节,但是实际按以下 packetdrill 代码模拟后,测试出来的零窗口探测包,段大小为 0 字节,这样在 Wireshark 中会识别为 TCP Keep-Alive 数据包,而不是 TCP ZeroWindowProbe 数据包。
# cat tcp_zero_window_probe.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 1000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 1000
+0 accept(3, ..., ...) = 4
+.1 write(4, ..., 1000) = 1000
+0 > P. 1:1001(1000) ack 1
+0.01 < . 1:1(0) ack 1001 win 0
+.1 write(4, ..., 1000) = 1000
+0 `sleep 100000`
#
以上产生了一个疑问,对于 TCP 零窗口探测数据包,它的数据段是 1 字节还是 0 字节?还是两者都可以?
至少说绝大多数的资料上,都写的是 1 字节,包括 RFC9293 中关于 Zero-Window Probing 的一段说明,截取如下:
The sending TCP peer must regularly transmit at least one octet of new data (if available), or retransmit to the receiving TCP peer even if the send window is zero, in order to "probe" the window.
而且 Wireshark TCP 分析标志定义和源码中也是如此:
Set when the sequence number is equal to the next expected sequence number, the segment size is one, and last-seen window size in the reverse direction was zero.
但是在 Linux (5.15.0) 通过 packetdrill 测试的结果却是 0 字节,因此会被 Wirehshark 识别为 TCP Keep-Alive 数据包,简单翻了下源码,感觉确实是发送序号为 snd_una - 1、长度为 0 的 ACK 包作为探测包,以下少数资料也提到这一点:
https://blog.csdn.net/sinat_20184565/article/details/105691191
https://blog.csdn.net/zhangskd/article/details/44571323
https://www.cnblogs.com/lshs/p/6038654.html
https://blog.csdn.net/weixin_45537413/article/details/114069445
http://www.tcpipguide.com/free/t_TCPWindowManagementIssues-3.htm
其中 TCPIPGUIDE 中的说明:The probe segment can contain either zero or one byte of data, even when the window is closed.
现阶段我的答案是 Windows 系统是 1 字节,Linux 系统(可能是所有版本又或是某个版本之后)是 0 字节。
至少我觉得可以在某个时间提交一个 Issue,和 Wireshark 开发者探讨下,是否能改进相关的判断逻辑。
实例
关于 TCP ZeroWindowProbe
、TCP ZeroWindowProbeAck
的实例,依赖于 TCP ZeroWindow
的出现,实际日常抓包中并不是经常会看到,但如果看到了,则代表着本端接收窗口为 0 ,意味着本端因性能或容量等问题而无法接收数据,因此通知发送方暂停发送数据,而发送方满足一定条件,启动零窗口探测定时器,在超时后发出零窗口探测包 TCP ZeroWindowProbe
,接收端根据窗口是否恢复的情况回复 TCP ZeroWindowProbeAck
或者 TCP Window Update
。
- TCP ZeroWindow + TCP ZeroWindowProbe + TCP Window Update
一种零窗口情景,接收端出现 Win 为 0 的情形,发送 TCP ZeroWindow
通知,发送端在经过一段时间后发出 TCP ZeroWindowProbe
数据包,但接收端收到探测后,由于已经打开窗口,因此直接回复 TCP Window Update
数据包。
首先服务器端 No.10 Win 为 0 且未设置 SYN、FIN、RST 的情况下,标识为 [TCP ZeroWindow]
,之后 286ms 客户端发送了 No.11 [TCP ZeroWindowProbe]
用于确认服务器端接收窗口是否恢复,服务器紧接着回复确认 No.12,表示窗口已恢复 Win 1420,标识为 [TCP Window Update]
数据包。
- TCP Window Full + TCP ZeroWindow + TCP ZeroWindowProbe + TCP ZeroWindowProbeAck + TCP Window Update
大满贯场景,覆盖了 5 种 TCP 分析标志。首先客户端发送数据,发现服务器接收窗口满了,则在 No.4 和 No.6 上标识 [TCP Window Full]
,此时服务器端 No.7 因为 Win 为 0 且未设置 SYN、FIN、RST 的情况下,标识为 [TCP ZeroWindow]
,之后陷入等待,大概 2 秒+后,客户端发送了 No.8 [TCP ZeroWindowProbe]
用于确认服务器端接收窗口是否恢复,服务器紧接着回复确认 No.9,表示仍处于零窗口未恢复,标识为 [TCP ZeroWindowProbeAck]
+ [TCP ZeroWindow]
,又再过了 300ms 后,服务器端发送 No.10 Win 此时更新为 14600,表示接收窗口已恢复,标识成 [TCP Window Update]
,至此完成一次完整的零窗口出现、探测及恢复过程。
类似的场景同样如下
- Keep-Alive 零窗口探测特例
在上述 packetdrill 示例中已经提及,TCP 零窗口探测包 0 字节,在 Wireshark 会识别成 TCP Keep-Alive
数据包的特殊示例。
首先服务器端发送数据,发现客户端接收窗口满了,则在 No.5 上标识 [TCP Window Full]
,此时客户端 No.6 因为 Win 为 0 且未设置 SYN、FIN、RST 的情况下,标识为 [TCP ZeroWindow]
,之后因为当网络中没有发送且未确认的数据包,且服务器端有待发送的数据包时,启动了零窗口探测定时器,在超时后发出零窗口探测包,Seq Num 回退 1 为 1000,且段大小为 0 字节,标识成 [TCP Keep-Alive]
。之后因为未模拟客户端回复 ACK 数据包,所以服务器端在未收到确认的情况下,不断超时重传零窗口探测数据包。
总结
套用以前总结的一句话,不要迷信 Wireshark,虽是神器,但不是万能的,只有学到手的东西,才是真正属于自己的。