前言
默认情况下,Wireshark 的 TCP 解析器会跟踪每个 TCP 会话的状态,并在检测到问题或潜在问题时提供额外的信息。在第一次打开捕获文件时,会对每个 TCP 数据包进行一次分析,数据包按照它们在数据包列表中出现的顺序进行处理。可以通过 “Analyze TCP sequence numbers” TCP 解析首选项启用或禁用此功能。
TCP 分析展示
在数据包文件中进行 TCP 分析时,关于 “TCP Window Full” 一般是如下显示的,包括:
- Packet List 窗口中的 Info 信息列,以 [TCP Window Full] 黑底红字进行标注;
- Packet Details 窗口中的 TCP 协议树下,在 [SEQ/ACK analysis] -> [TCP Analysis Flags] 中定义该 TCP 数据包的分析说明。
TCP Window Full 定义
实际在 TCP 分析中,关于 TCP Window Full 的定义非常简单,如下,为本端发送端所发的 TCP 段大小超过对端接收端的接收窗口大小,受到对端接收窗口大小的限制,需注意的是这个标记是在发送端标记,而不是在接收端标记。
Set when the segment size is non-zero, we know the window size in the reverse direction, and our segment size exceeds the window size in the reverse direction.
具体的代码如下,总的来说这段代码的作用是检测 TCP 数据流中的“窗口已满”情况,即当前数据段刚好达到接收端宣告的窗口边界,检测到这种情况时,会设置相应的标志以供后续处理使用。
/* WINDOW FULL* If we know the window scaling* and if this segment contains data and goes all the way to the* edge of the advertised window* then we mark it as WINDOW FULL* SYN/RST/FIN packets are never WINDOW FULL*/if( seglen>0&& tcpd->rev->win_scale!=-1&& (seq+seglen)==(tcpd->rev->tcp_analyze_seq_info->lastack+(tcpd->rev->window<<(tcpd->rev->is_first_ack?0:(tcpd->rev->win_scale==-2?0:tcpd->rev->win_scale))))&& (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_WINDOW_FULL;}
- lastack,定义为 Last seen ack for the reverse flow。
- 关于 tcpd->rev->is_first_ack,在之后实例中会展开说明。
Packetdrill 示例
在上述 TCP Window Full
定义和代码可知,TCP 分析的逻辑很简单,因此通过 packetdrill 比较容易模拟出相关现象。
# cat tcp_window_full.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, ..., 500) = 500
+0 > P. 1:501(500) ack 1
+0.1 < . 1:1(0) ack 501 win 1000+.1 write(4, ..., 1000) = 1000
+0 > . 501:1001(500) ack 1
+0 > P. 1001:1501(500) ack 1
+0.1 < . 1:1(0) ack 1501 win 1000
通过 tcpdump 捕获数据包后,经 Wireshark 展示如下,可以看到由于客户端 No.5 宣告的 Win 为 1000,所以服务器仅能发送 1000 字节大小的数据分段,也就是分别发送了 No.6 和 No.7 两个 Len 为 500 字节的数据包,这样在 No.7 数据包中会判断为 [TCP Window Full],因为发送字节大小已经达到了对端也就是客户端宣告的 Win 大小。
BIF 大小 1000,专家信息标识说明为 TCP window specified by the receiver is now completely full
,接收端窗口满。
实例
关于 TCP Window Full
的实例,正常来说考虑到接收端接收窗口的释放速度,再加上 Window Scale 的存在,TCP 窗口满现象相对来说并不是那么容易出现。而在不同的场景中,也会伴生着出现像是 TCP Window Update
、TCP ZeroWindow
、TCP ZeroWindowProbe
、TCP ZeroWindowProbeAck
等信息。
- 窗口满
可以看到客户端 No.277 宣告的 Win 为 8712,而服务器 No.278 标识为 [TCP Window Full],是由于在途字节数达到了 8712 字节。
而在之后的通讯过程中,由于客户端 Win 降为了 0,在 No.281 也就标识了 [TCP ZeroWindow],之后由于接收窗口释放变化,又发送了 No.282 Win 为 4356 的 ACK,之后服务器再次发送 No.283 数据分段时又一次标识了 [TCP Window Full],同样的原因是由于在途字节数达到了 4356 字节。
该案例中,客户端并不支持 Window Scale ,也就是在 TCP SYN 中未携带该选项。
- 窗口满的特例
当 TCP 通讯速率上不去,疑似某端接收窗口满或有问题时,但又没看到明显的 [TCP Window Full] 标识时,请相信自己的判断,真有可能是发生了 TCP 窗口满问题。
看不到 [TCP Window Full] 标识,仅仅是有可能数据包文件或者说这条 TCP 流不完整,并没有抓到 TCP 三次握手的信息,因此后续的数据通讯过程中是无法得知这条流是否支持 Window Scale 以及它的具体数值,没有这些信息的情况下,是没有办法根据数据包中携带的 Win 字段值计算出实际的窗口值,所以也就没法判断什么时候发生了窗口满事件。
如下,就像只会知道接收窗口 250,在不知道 Window Scale 为 4 的情况下,如何能得出计算后的 Win 大小为 1000 ?
这种情况下,看到的现象就是一端通告的 Win 只是一个小值,但是另一端发送的在途数据又能超过这个值,此时的情况你就可以判断出是因为缺少了 TCP 三次握手。
注意,仅仅是 Wireshark 因为数据包文件不完整无法判断,真实的数据流如果双方均支持 Window Scale,实际的通讯流均是按照各自的缩放因子来计算且发送数据的。
案例也很好模拟出,在上述 packetdrill 示例中把 No.1-3 TCP 三次握手数据包忽略掉即可,如下,No.7 此时已经不再标识 [TCP Window Full], 虽然对端 No.5 所通告的 Win 为 1000 ,但是 Window size scaling factor: -1(unknown),从 [TCP Window Full] 的代码来说,tcpd->rev->win_scale =-1 已经没有后续判断的依据了,因此 [TCP Window Full] 不再出现。
注意区别 Window size scaling factor: -2 (no window scaling used),-2 代表着说数据包文件完整,也就是捕获到了 TCP 三次握手,但双方不支持 Window Scale。
如何知道?有条件的话,复现一次 TCP 连接,重新捕获到 TCP 三次握手即可,不管是 -2 不支持,或者其它的值。然而就算知道了,再通过手动指定 Window Scale 值的方式,也无法让 [TCP Window Full] 再出现 。
- 再谈窗口满的特例
这里就要提到之前代码中的 tcpd->rev->is_first_ack
,如下,如果 is_frist_ack 为真,对于 Window 的偏移量也就是 Window Scale ,使用值 0,而不是用 TCP 三次握手中的 Window Scale 值。
tcpd->rev->window<<(tcpd->rev->is_first_ack?0:(tcpd->rev->win_scale==-2?0:tcpd->rev->win_scale))
is_first_ack 具体是在 TCP 三次握手阶段即设置为 True,不管是 SYN 或是 SYN/ACK 数据包,均会设置本方向的 is_first_ack 值为 True 。
if(tcph->th_flags & TH_SYN) {if(tcph->th_flags & TH_ACK) {expert_add_info_format(pinfo, tf_syn, &ei_tcp_connection_synack,"Connection establish acknowledge (SYN+ACK): server port %u", tcph->th_sport);/* Save the server port to help determine dissector used */tcpd->server_port = tcph->th_sport;}else {expert_add_info_format(pinfo, tf_syn, &ei_tcp_connection_syn,"Connection establish request (SYN): server port %u", tcph->th_dport);/* Save the server port to help determine dissector used */tcpd->server_port = tcph->th_dport;tcpd->ts_mru_syn = pinfo->abs_ts;}/* Remember where the next segment will start. */if (tcp_desegment && tcp_reassemble_out_of_order && tcpd && !PINFO_FD_VISITED(pinfo)) {if (tcpd->fwd->maxnextseq == 0) {tcpd->fwd->maxnextseq = tcph->th_seq + 1;}}/* Initiliaze the is_first_ack */tcpd->fwd->is_first_ack = TRUE;
在以下这个案例中,No.79 判定为 [TCP Window Full],光从表面现象来看会感觉很奇怪,因为 TCP 三次握手中明显双方均支持 Window Scale,且双方均为很大的一个值 WS 128,而客户端仅仅从 No.70-79 传了 10 个 MSS 1448 的分段后即出现了窗口满现象。No.79 显示为 BIF 14480 大小,和 SYN/ACK 通告的窗口 Win 14480 相等,但没有使用 Window Scale 来相乘计算,即对于 [TCP Window FUll] 的判断,使用的直接是 0 值,而不是 Window Scale 128。
这是因为对于客户端 No.79 来说,tcpd->rev->is_first_ack
实际就是服务器端 is_first_ack 的取值,而此时在 rev 方向仅有 No.68 SYN/ACK 数据包,此时 is_first_ack 的值为 True,因此对于 Window 的偏移量也就是 Window Scale ,使用值 0,所以最终判断为 [TCP Window Full] 。
- 继续谈窗口满的特例
再说一个案例,服务器端 No.5 判定为 [TCP Window Full],是因为客户端 No.3 通告的 Win 为 1000,也就是 Win 250 使用了 Window Scale 4 来相乘计算,因此服务器端能发送 No.4 和 No.5 两个 500 字节大小的数据分段,并在 No.5 上标记 [TCP Window Full] 。
也就是说服务器端在 No.5 数据包分析时,判断 rev 方向 Window Scale 的值是真实值 4 而不是 0 ,这也就意味着此时客户端的 is_first_ack 的值为 False 。而这是在分析客户端 No.3 ACK 数据包时,通过以下代码所实现:
/** Remember if we have already seen at least one ACK,* then we can neutralize the Window Scale side-effect at the beginning (issue 14690)*/if(tcp_analyze_seq&& (tcph->th_flags & (TH_SYN|TH_ACK)) == TH_ACK) {if(tcpd->fwd->is_first_ack) {tcpd->fwd->is_first_ack = FALSE;}}
总结
总结来说,关于 TCP Window Scale ,有两句话说得很到位:
- RFC7323 2.2 The window field in a segment where the SYN bit is set (i.e., a or <SYN,ACK>) MUST NOT be scaled.
- The Window Scale option has to be honored and interpreted adequately.