文章总结: 本文档利用Packetdrill深入解析TCP零窗口探测机制,阐述了探测包的触发条件与指数回退策略。实验揭示了Linux发送0字节探测包的实现细节及其对Wireshark识别的影响。文章验证了tcp_retries2对探测重传次数的限制,并分析了tcp_invalid_ratelimit参数对回复无效探测包频率的约束,为TCP流量控制与故障排查提供了关键依据。 综合评分: 95 文章分类: 网络安全,安全工具,安全运营
Wireshark & Packetdrill | TCP ZeroWindow Probe
原创
7ACE
Echo Reply
2025年12月29日 08:08 江苏
知难而进
实验目的
基于 packetdrill TCP 三次握手脚本,通过构造模拟服务器端场景,研究测试 TCP ZeroWindow Probe 现象。
基础脚本
# cat tcp_zerowindow_probe_000.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 10000 <mss 1000>+0 > S. 0:0(0) ack 1 <...>+0.01 < . 1:1(0) ack 1 win 10000+0 accept(3, ..., ...) = 4#
TCP ZeroWindow Probe
回顾一下 TCP 零窗口的概念,在 Wireshark TCP 分析中和 TCP 零窗口相关的实际上有三种信息,分别是:TCP ZeroWindow 、TCP ZeroWindowProbe 、TCP ZeroWindowProbeAck 。实际运行环境中,有时是单独出现的,譬如 TCP ZeroWindow ,有时是一起出现的,也就是出现了零窗口,之后就会出现需要为恢复窗口而进行的零窗口探测和零窗口探测确认。
TCP ZeroWindow Probe(零窗口探测)是 TCP 协议的一种恢复机制,当接收方因缓冲区满而通告“零窗口”导致发送方停止传输后,为防止后续的“窗口已更新”通知报文丢失造成连接永久死锁,发送方会周期性地向接收方发送一个仅包含 1 字节数据的探测包,以此主动查询对方窗口状态,一旦收到非零窗口的回复,即可恢复正常数据传输,从而保障了流量控制的可靠性。
在之前写了不少篇关于 TCP 零窗口的文章,包括《TCP Analysis Flags 之 TCP ZeroWindow》、《TCP Analysis Flags 之 TCP ZeroWindowProbe》和《TCP 零窗口探测时间》,也探讨过在 Linux 上的实现,实际发送的是 0 字节的探测数据包,而由于这个的不同,在 Wireshark 中会识别为 TCP Keep-Alive 数据包,而不是 TCP ZeroWindowProbe 数据包。
基础测试
初始脚本如下,在对端接收窗口为 0 的情况下,尝试写入数据。
# cat tcp_zerowindow_001.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
+0.01 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 100`#
通过 tcpdump 捕获数据包如下,可以看到在客户端发送 Win 为 0 的 ACK 数据包后,间隔 313ms 后服务器端也就是发送端发送了零窗口探测包,因为没有模拟零窗口探测确认包,所以不断超时不断重传。
为什么是间隔 313ms,是因为第一次发送 TCP ZeroWindowProbe 数据包,实际上是发送端有需要发送的数据,但由于接收端零窗口所以发送失败,之后启动零窗口探测定时器,在超时后发送零窗口探测包,这个间隔时间就是应用写入数据的间隔时间 100ms 再加上零窗口探测定时器第一次超时 212ms 的时间。
# packetdrill tcp_zerowindow_001.pkt #
# tcpdump -i any -nn port 8080tcpdump: data link type LINUX_SLL2tcpdump: verbose output suppressed, use -v[v]... for full protocol decodelistening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes21:45:07.445809 tun0 In IP 192.0.2.1.55325 > 192.168.248.94.8080: Flags [S], seq 0, win 1000, options [mss 1460], length 021:45:07.445838 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [S.], seq 4160233482, ack 1, win 64240, options [mss 1460], length 021:45:07.455914 tun0 In IP 192.0.2.1.55325 > 192.168.248.94.8080: Flags [.], ack 1, win 1000, length 021:45:07.466087 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], seq 1:501, ack 1, win 64240, length 500: HTTP21:45:07.466097 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [P.], seq 501:1001, ack 1, win 64240, length 500: HTTP21:45:07.476332 tun0 In IP 192.0.2.1.55325 > 192.168.248.94.8080: Flags [.], ack 1001, win 0, length 021:45:07.789532 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:08.221484 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:09.085488 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:10.813508 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:14.397514 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:21.309490 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:45:35.133975 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:46:04.061482 tun0 Out IP 192.168.248.94.8080 > 192.0.2.1.55325: Flags [.], ack 1, win 64240, length 021:46:47.590577 ? In IP 192.0.2.1.55325 > 192.168.248.94.8080: Flags [R.], seq 1, ack 1001, win 0, length 0#
通过上述实验,也可以观察到 ZeroWindow Probe 的定时器初始时间是 RTO,随后的过程进行了指数回退,最大指数回退次数为 tcp_retries2 ,如果此时还没收到对端的响应,则会停止进行 ZeroWindow Probe 过程,但不会释放连接,这与在数据传输过程中发生 RTO 超时一直得不到响应之后会最终释放连接的过程不一样。
通过修改 net.ipv4.tcp_retries2 值为 3,调整 tcp_retries2 超时重传次数。
# sysctl -a|grep retries2net.ipv4.tcp_retries2 = 15# sysctl -q net.ipv4.tcp_retries2=3#
仍然执行上述脚本,可以看到在尝试 3 次 ZeroWindow Probe 发送而得不到响应,也再没有后续的动作了,且连接一直存在,直至 packetdrill 程序执行退出才 Reset 连接。
# packetdrill tcp_zerowindow_001.pkt #
# tcpdump -i any -nn port 8080tcpdump: data link type LINUX_SLL2tcpdump: verbose output suppressed, use -v[v]... for full protocol decodelistening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes22:08:52.517783 tun0 In IP 192.0.2.1.49137 > 192.168.51.195.8080: Flags [S], seq 0, win 1000, options [mss 1460], length 022:08:52.517812 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [S.], seq 2403889289, ack 1, win 64240, options [mss 1460], length 022:08:52.527891 tun0 In IP 192.0.2.1.49137 > 192.168.51.195.8080: Flags [.], ack 1, win 1000, length 022:08:52.538086 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [.], seq 1:501, ack 1, win 64240, length 500: HTTP22:08:52.538095 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [P.], seq 501:1001, ack 1, win 64240, length 500: HTTP22:08:52.548564 tun0 In IP 192.0.2.1.49137 > 192.168.51.195.8080: Flags [.], ack 1001, win 0, length 022:08:52.861511 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [.], ack 1, win 64240, length 022:08:53.309483 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [.], ack 1, win 64240, length 022:08:54.173486 tun0 Out IP 192.168.51.195.8080 > 192.0.2.1.49137: Flags [.], ack 1, win 64240, length 022:10:32.673003 ? In IP 192.0.2.1.49137 > 192.168.51.195.8080: Flags [R.], seq 1, ack 1001, win 0, length 0#
通过 tcpi 打印 tcpi_probes 和 tcpi_backoff 信息,脚本如下。
# cat tcp_zerowindow_002.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
+0.01 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 %{print (tcpi_probes, tcpi_backoff)}%
+0.25 %{print (tcpi_probes, tcpi_backoff)}%
+0.5 %{print (tcpi_probes, tcpi_backoff)}%
+1 %{print (tcpi_probes, tcpi_backoff)}%
+2 %{print (tcpi_probes, tcpi_backoff)}%
+0 `sleep 1`#
当 net.ipv4.tcp_retries2 为默认值为 15 的情况下,如下。
# packetdrill tcp_zerowindow_002.pkt 0 01 12 23 34 4#
而当 net.ipv4.tcp_retries2 修改值为 3 的情况下,如下,可以自行理解下 tcpi_probes 和 tcpi_backoff 值的不同之处。
# packetdrill tcp_zerowindow_002.pkt 0 01 12 23 33 0#
另外在 Linux 上发出的 ZeroWindow Probe 数据包的 Len 为 0,且 Seq Num 位于 snd_nxt 的前面,因此 Linux 在接收到这种数据包的时候会认定为无效的 Seq Num。对于这种类型的数据包回复 ACK 的时候会受到参数 tcp_invalid_ratelimit控制,这个参数控制了 TCP 对于这类无效数据包的 ACK 回复速率,默认值为 500ms ,也就是说对于这类无效数据包的 ACK 回复时间最小为 500ms,只要间隔大于 500ms,就会立即回复一个 ACK 数据包。
# sysctl -a|grep invalidnet.ipv4.tcp_invalid_ratelimit = 500#
首先模拟不断重传的 ZeroWindow Probe 数据包,指数回退过程。
# cat tcp_zerowindow_003.pkt 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0+0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [3000],4) = 0+0 bind(3, ..., ...) = 0+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460>+0 > S. 0:0(0) ack 1 <...>+0.01 < . 1:1(0) ack 1 win 10000+0 accept(3, ..., ...) = 4
+0.01 < P. 1:1461(1460) ack 1 win 10000+0 < P. 1461:2921(1460) ack 1 win 10000
+0.312 < . 2920:2920(0) ack 1 win 10000+0.424 < . 2920:2920(0) ack 1 win 10000+0.848 < . 2920:2920(0) ack 1 win 10000+1.696 < . 2920:2920(0) ack 1 win 10000
+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,可以看到在服务器端发送 Win 为 0 的 ACK 数据包后,模拟客户端发送的第一个 ZeroWindow Probe 数据包,服务器端响应了 ACK,但第二个 ZeroWindow Probe 数据包,服务器并未响应 ACK,因为间隔上一次回复 ACK 的时间间隔小于 500ms,之后的第三个和第四个 ZeroWindow Probe 数据包,因间隔时间明显大于 500ms ,因此服务器端均是直接响应 ACK 。
# packetdrill tcp_zerowindow_003.pkt#
# tcpdump -i any -nn port 8080tcpdump: data link type LINUX_SLL2tcpdump: verbose output suppressed, use -v[v]... for full protocol decodelistening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes22:38:49.705784 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 022:38:49.705827 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [S.], seq 1874600778, ack 1, win 2920, options [mss 1460], length 022:38:49.715913 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [.], ack 1, win 10000, length 022:38:49.726089 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP22:38:49.726134 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [.], ack 1461, win 1460, length 022:38:49.726147 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP22:38:49.773483 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [.], ack 2921, win 0, length 022:38:50.038094 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [.], ack 1, win 10000, length 022:38:50.038168 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [.], ack 2921, win 0, length 022:38:50.461999 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [.], ack 1, win 10000, length 022:38:51.310004 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [.], ack 1, win 10000, length 022:38:51.310026 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [.], ack 2921, win 0, length 022:38:53.006030 tun0 In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [.], ack 1, win 10000, length 022:38:53.006077 tun0 Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [.], ack 2921, win 0, length 022:38:54.008856 ? Out IP 192.168.161.125.8080 > 192.0.2.1.60617: Flags [R.], seq 1, ack 2921, win 2920, length 022:38:54.008928 ? In IP 192.0.2.1.60617 > 192.168.161.125.8080: Flags [R.], seq 2920, ack 1, win 10000, length 0#
上述实验,模拟客户端 ZeroWindow Probe 数据包,基本遵循了指数回退的过程,初步观察了 tcp_invalid_ratelimit 参数的限制。
以下再进一步增加这类无效数据包的数量,以及任意修改间隔时间,再次验证下 tcp_invalid_ratelimit 参数的限制。
# cat tcp_zerowindow_004.pkt 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0+0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [3000],4) = 0+0 bind(3, ..., ...) = 0+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460>+0 > S. 0:0(0) ack 1 <...>+0.01 < . 1:1(0) ack 1 win 10000+0 accept(3, ..., ...) = 4
+0.01 < P. 1:1461(1460) ack 1 win 10000+0 < P. 1461:2921(1460) ack 1 win 10000
+0.312 < . 2920:2920(0) ack 1 win 10000+0 < . 2920:2920(0) ack 1 win 10000+0.2 < . 2920:2920(0) ack 1 win 10000+0.32 < . 2920:2920(0) ack 1 win 10000+0.48 < . 2920:2920(0) ack 1 win 10000+0.6 < . 2920:2920(0) ack 1 win 10000
+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,可以对照着脚本中的间隔时间,理解下 tcp_invalid_ratelimit 500ms 的限制下的 ACK 响应数据包。
# packetdrill tcp_zerowindow_004.pkt#
# tcpdump -i any -nn port 8080tcpdump: data link type LINUX_SLL2tcpdump: verbose output suppressed, use -v[v]... for full protocol decodelistening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes22:48:21.725944 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 022:48:21.726019 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [S.], seq 2336095198, ack 1, win 2920, options [mss 1460], length 022:48:21.736151 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:21.746311 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP22:48:21.746574 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [.], ack 1461, win 1460, length 022:48:21.746628 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP22:48:21.789482 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [.], ack 2921, win 0, length 022:48:22.058231 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:22.058258 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [.], ack 2921, win 0, length 022:48:22.058269 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:22.258256 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:22.578231 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:22.578254 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [.], ack 2921, win 0, length 022:48:23.058227 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:23.658245 tun0 In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [.], ack 1, win 10000, length 022:48:23.658270 tun0 Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [.], ack 2921, win 0, length 022:48:24.661032 ? Out IP 192.168.179.251.8080 > 192.0.2.1.34889: Flags [R.], seq 1, ack 2921, win 2920, length 022:48:24.661063 ? In IP 192.0.2.1.34889 > 192.168.179.251.8080: Flags [R.], seq 2920, ack 1, win 10000, length 0#
往期推荐
1. Wireshark 提示和技巧 | 捕获点之 TCP 三次握手
2. Wireshark 提示和技巧 | a == ${a} 显示过滤宏
3. Wireshark TS | 防火墙空闲会话超时问题
4. Wireshark TS | 超时重传时间不翻倍
5. 网络设备 MTU MSS Jumboframe 全解
后台回复「TT」获取 Wireshark 提示和技巧系列 合集
后台回复「TS」获取 Wireshark Troubleshooting 系列 合集
如需交流或加技术群,可后台直接留言,我会在第一时间回复,谢谢!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Echo Reply 7ACE《Wireshark & Packetdrill | TCP ZeroWindow Probe》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论