文章总结: 本文通过packetdrill脚本构造应用受限与空闲恢复场景,实验验证TCP拥塞窗口校验(cwv)机制,展示cwnd在慢启动阶段从10增长至20的过程,并对比RFC2861与RFC7661的cwv方案差异,强调cwnd不代表当前网络容量,需通过校验防止突发流量。 综合评分: 80 文章分类: 其他
Wireshark & Packetdrill | TCP 拥塞窗口校验
原创
7ACE 7ACE
Echo Reply
2026年9月25日 11:04 江苏
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
不要直接相信别人的结论
实验目的
基于 packetdrill TCP 脚本,通过构造应用受限(Application-Limited)和空闲恢复场景,研究测试 TCP 拥塞窗口校验(CWV,Congestion Window Validation)行为与机制。
基础脚本
# cat tcp_cwv_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 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
#
CWV
TCP 拥塞窗口(cwnd)用于控制发送方在网络中未确认的数据量,是拥塞控制的核心变量。在持续有数据发送的场景下,cwnd 通过慢启动和拥塞避免算法逐步增长,反映的是网络路径的可用带宽。但现实中存在大量应用受限(Application-Limited)场景:应用层并不是时刻都有数据要发送,发送方实际使用的窗口远小于 cwnd。此时,cwnd 并不反映当前网络的真实容量——它可能是在过去某个时刻被”撑大”的,早已过时。
RFC 2861 首次提出了 CWV 机制,核心思想是:当发送方长期处于应用受限状态时,应当对 cwnd 进行”校验”,防止基于过时的窗口值向网络注入突发流量。RFC 7661 废弃了 RFC 2861(将其重新分类为 Historic),提出了 New CWV 方案:不再区分 application-limited 和 idle,统一按 rate-limited 处理,允许应用在受限间隔后更快恢复发送速率。CWV 要解决的核心问题可以用一句话概括:一个被历史行为撑大的 cwnd,不代表当前网络还能承载这个窗口的流量。
在 Linux 实现中,CWV 主要通过 tcp_is_cwnd_limited() 判断发送方是否受 cwnd 限制,以及 tcp_cwnd_validate() 在发送路径上校验窗口大小。相关标志位 tp->is_cwnd_limited 记录着发送方是否处于 cwnd 受限状态。
基础测试
首先构造一个满窗口基线场景:发送方写入 20 MSS 数据,超过初始 cwnd=10,确保 10 个立即发送、10 个排队,观察 cwnd 在慢启动阶段的持续增长。
# cat tcp_cwv_001.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
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,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0 %{print ("init cwnd:", tcpi_snd_cwnd, "ssthresh:", tcpi_snd_ssthresh)}%
// 写入 20 MSS,cwnd=10 时 10 个立即发送、10 个排队
+0.01 write(4, ..., 20000) = 20000
+0 %{print ("after writes cwnd:", tcpi_snd_cwnd)}%
// 每次确认 2 段,间隔 0.01s
+0.01 < . 1:1(0) ack 2001 win 10000
+0 %{print ("ack 2 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 4001 win 10000
+0 %{print ("ack 4 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 6001 win 10000
+0 %{print ("ack 6 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 8001 win 10000
+0 %{print ("ack 8 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 10001 win 10000
+0 %{print ("ack 10 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
#
# packetdrill tcp_cwv_001.pkt
init cwnd: 10 ssthresh: 2147483647
after writes cwnd: 10
ack 2 cwnd: 12 ca_state: 0
ack 4 cwnd: 14 ca_state: 0
ack 6 cwnd: 16 ca_state: 0
ack 8 cwnd: 18 ca_state: 0
ack 10 cwnd: 20 ca_state: 0
#
初始 cwnd=10,写入 20 MSS 后 10 个立即发送、10 个排队,发送方受 cwnd 限制。每次 ACK 确认 2 段,cwnd 在慢启动阶段从 10 持续增长到 20。
通过 tcpdump 捕获数据包展示如下:
# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:42:04.088291 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
22:42:04.088392 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [S.], seq 3170983219, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:42:04.099642 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 1, win 10000, length 0
22:42:04.109887 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
22:42:04.109893 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 1001:2001, ack 1, win 64240, length 1000: HTTP
22:42:04.109900 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 2001:3001, ack 1, win 64240, length 1000: HTTP
22:42:04.109901 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 3001:4001, ack 1, win 64240, length 1000: HTTP
22:42:04.109905 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 4001:5001, ack 1, win 64240, length 1000: HTTP
22:42:04.109906 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 5001:6001, ack 1, win 64240, length 1000: HTTP
22:42:04.109908 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 6001:7001, ack 1, win 64240, length 1000: HTTP
22:42:04.109913 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 7001:8001, ack 1, win 64240, length 1000: HTTP
22:42:04.109914 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 8001:9001, ack 1, win 64240, length 1000: HTTP
22:42:04.109919 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 9001:10001, ack 1, win 64240, length 1000: HTTP
22:42:04.119968 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 2001, win 10000, length 0
22:42:04.119999 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 10001:11001, ack 1, win 64240, length 1000: HTTP
22:42:04.120000 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 11001:12001, ack 1, win 64240, length 1000: HTTP
22:42:04.130213 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 4001, win 10000, length 0
22:42:04.130251 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 12001:13001, ack 1, win 64240, length 1000: HTTP
22:42:04.130253 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 13001:14001, ack 1, win 64240, length 1000: HTTP
22:42:04.140326 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 6001, win 10000, length 0
22:42:04.140347 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 14001:15001, ack 1, win 64240, length 1000: HTTP
22:42:04.150454 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 8001, win 10000, length 0
22:42:04.150491 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 15001:16001, ack 1, win 64240, length 1000: HTTP
22:42:04.150493 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 16001:17001, ack 1, win 64240, length 1000: HTTP
22:42:04.160532 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [.], ack 10001, win 10000, length 0
22:42:04.160569 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [.], seq 17001:18001, ack 1, win 64240, length 1000: HTTP
22:42:04.160571 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 18001:19001, ack 1, win 64240, length 1000: HTTP
22:42:04.160575 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 19001:20001, ack 1, win 64240, length 1000: HTTP
22:42:04.306769 tun0 Out IP 192.168.85.245.8080 > 192.0.2.1.57929: Flags [P.], seq 19001:20001, ack 1, win 64240, length 1000: HTTP
22:42:04.357981 tun0 In IP 192.0.2.1.57929 > 192.168.85.245.8080: Flags [R.], seq 1, ack 10001, win 10000, length 0
#
但发送端并没持续发包,所以随着发送段被逐步 ACK 确认,实际之后并非全程保持 cwnd 增长。
修改脚本,继续 ACK 之后的数据可以观察到 cwnd 的后续变化。
# cat tcp_cwv_002.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
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,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0 %{print ("init cwnd:", tcpi_snd_cwnd, "ssthresh:", tcpi_snd_ssthresh)}%
// 写入 20 MSS,cwnd=10 时 10 个立即发送、10 个排队
+0.01 write(4, ..., 20000) = 20000
+0 %{print ("after writes cwnd:", tcpi_snd_cwnd)}%
// 每次确认 2 段,间隔 0.01s
+0.01 < . 1:1(0) ack 2001 win 10000
+0 %{print ("ack 2 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 4001 win 10000
+0 %{print ("ack 4 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 6001 win 10000
+0 %{print ("ack 6 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 8001 win 10000
+0 %{print ("ack 8 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 10001 win 10000
+0 %{print ("ack 10 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 12001 win 10000
+0 %{print ("ack 12 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 14001 win 10000
+0 %{print ("ack 14 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 16001 win 10000
+0 %{print ("ack 16 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 18001 win 10000
+0 %{print ("ack 18 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
+0.01 < . 1:1(0) ack 20001 win 10000
+0 %{print ("ack 20 cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
#
# packetdrill tcp_cwv_002.pkt
init cwnd: 10 ssthresh: 2147483647
after writes cwnd: 10
ack 2 cwnd: 12 ca_state: 0
ack 4 cwnd: 14 ca_state: 0
ack 6 cwnd: 16 ca_state: 0
ack 8 cwnd: 18 ca_state: 0
ack 10 cwnd: 20 ca_state: 0
ack 12 cwnd: 20 ca_state: 0
ack 14 cwnd: 20 ca_state: 0
ack 16 cwnd: 20 ca_state: 0
ack 18 cwnd: 20 ca_state: 0
ack 20 cwnd: 20 ca_state: 0
#
通过 tcpdump 捕获数据包展示如下:
# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:45:10.067308 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
22:45:10.067366 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [S.], seq 2531793480, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:45:10.077592 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 1, win 10000, length 0
22:45:10.087835 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
22:45:10.087842 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 1001:2001, ack 1, win 64240, length 1000: HTTP
22:45:10.087853 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 2001:3001, ack 1, win 64240, length 1000: HTTP
22:45:10.087854 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 3001:4001, ack 1, win 64240, length 1000: HTTP
22:45:10.087859 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 4001:5001, ack 1, win 64240, length 1000: HTTP
22:45:10.087860 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 5001:6001, ack 1, win 64240, length 1000: HTTP
22:45:10.087863 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 6001:7001, ack 1, win 64240, length 1000: HTTP
22:45:10.087871 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 7001:8001, ack 1, win 64240, length 1000: HTTP
22:45:10.087872 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 8001:9001, ack 1, win 64240, length 1000: HTTP
22:45:10.087875 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 9001:10001, ack 1, win 64240, length 1000: HTTP
22:45:10.097920 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 2001, win 10000, length 0
22:45:10.097954 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 10001:11001, ack 1, win 64240, length 1000: HTTP
22:45:10.097956 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 11001:12001, ack 1, win 64240, length 1000: HTTP
22:45:10.107993 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 4001, win 10000, length 0
22:45:10.108037 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 12001:13001, ack 1, win 64240, length 1000: HTTP
22:45:10.108039 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 13001:14001, ack 1, win 64240, length 1000: HTTP
22:45:10.118087 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 6001, win 10000, length 0
22:45:10.118113 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 14001:15001, ack 1, win 64240, length 1000: HTTP
22:45:10.128152 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 8001, win 10000, length 0
22:45:10.128184 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 15001:16001, ack 1, win 64240, length 1000: HTTP
22:45:10.128186 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 16001:17001, ack 1, win 64240, length 1000: HTTP
22:45:10.138246 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 10001, win 10000, length 0
22:45:10.138287 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [.], seq 17001:18001, ack 1, win 64240, length 1000: HTTP
22:45:10.138290 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 18001:19001, ack 1, win 64240, length 1000: HTTP
22:45:10.138294 tun0 Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [P.], seq 19001:20001, ack 1, win 64240, length 1000: HTTP
22:45:10.148332 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 12001, win 10000, length 0
22:45:10.158386 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 14001, win 10000, length 0
22:45:10.168480 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 16001, win 10000, length 0
22:45:10.178587 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 18001, win 10000, length 0
22:45:10.188648 tun0 In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [.], ack 20001, win 10000, length 0
22:45:10.265176 ? Out IP 192.168.174.216.8080 > 192.0.2.1.59901: Flags [F.], seq 20001, ack 1, win 64240, length 0
22:45:10.265204 ? In IP 192.0.2.1.59901 > 192.168.174.216.8080: Flags [R.], seq 1, ack 20001, win 10000, length 0
#
应用受限场景
接下来构造应用受限场景:先满窗口发送让 cwnd 增长,排空管道后仅发送 2 MSS,观察 cwnd 是否冻结。
# cat tcp_cwv_003.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
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,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
// 阶段一:发送 20 MSS
+0.01 write(4, ..., 20000) = 20000
+0 %{print ("phase1 after writes cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 4001 win 10000
+0 %{print ("phase1 ack4 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 8001 win 10000
+0 %{print ("phase1 ack8 cwnd:", tcpi_snd_cwnd)}%
// 排空管道
+0.01 < . 1:1(0) ack 20001 win 10000
+0 %{print ("phase1 drained cwnd:", tcpi_snd_cwnd)}%
// 阶段二:仅发 2 MSS
+0.01 write(4, ..., 1000) = 1000
+0 write(4, ..., 1000) = 1000
+0 %{print ("phase2 after write cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 22001 win 10000
+0 %{print ("phase2 ack2 cwnd:", tcpi_snd_cwnd)}%
#
# packetdrill tcp_cwv_003.pkt
phase1 after writes cwnd: 10
phase1 ack4 cwnd: 14
phase1 ack8 cwnd: 18
phase1 drained cwnd: 18
phase2 after write cwnd: 18
phase2 ack2 cwnd: 18
#
阶段一:满窗口发送,cwnd 从 10 增长到 18,而排空管道后 cwnd 仍保持 18。
阶段二:仅发送 2 MSS,is_cwnd_limited 为 false,综合判断发送方当前不满足 cwnd-limited 条件,属于应用受限,cwnd 并不增长。
空闲重置测试
构造场景:先满窗口发送让 cwnd 增长到较大值,再空闲 300ms(超过 RTO),恢复发送时观察 cwnd 被 tcp_slow_start_after_idle 重置。
# cat tcp_cwv_004.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
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,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
// 阶段一:发送 20 MSS
+0.01 write(4, ..., 20000) = 20000
+0 %{print ("phase1 after writes cwnd:", tcpi_snd_cwnd)}%
// 每次 ACK 4 段,间隔 0.01s
+0.01 < . 1:1(0) ack 4001 win 10000
+0 %{print ("phase1 ack4 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 8001 win 10000
+0 %{print ("phase1 ack8 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 12001 win 10000
+0 %{print ("phase1 ack12 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 16001 win 10000
+0 %{print ("phase1 ack16 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 20001 win 10000
+0 %{print ("phase1 ack20 cwnd:", tcpi_snd_cwnd)}%
// 阶段二:空闲 300ms(> RTO),恢复发送
+0.3 write(4, ..., 1000) = 1000
+0 %{print ("after idle cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
#
# packetdrill tcp_cwv_004.pkt
phase1 after writes cwnd: 10
phase1 ack4 cwnd: 14
phase1 ack8 cwnd: 18
phase1 ack12 cwnd: 22
phase1 ack16 cwnd: 22
phase1 ack20 cwnd: 22
after idle cwnd: 11 ca_state: 0
#
阶段一:cwnd 从 10 增长到 22 后停止增长。
阶段二:空闲 300ms(超过 RTO)后恢复发送,cwnd 从 22 重置为 11。重置逻辑由 tcp_cwnd_restart() 处理:cwnd 22 减半为 11,max(11,10)=11。
若空闲时间更长(超过 2 个 RTO),cwnd 会进一步减半:22→11→5,此时 max(5, 10)=10,才会回到初始值。
# cat tcp_cwv_005.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
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,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
// 阶段一:发送 20 MSS
+0.01 write(4, ..., 20000) = 20000
+0 %{print ("phase1 after writes cwnd:", tcpi_snd_cwnd)}%
// 每次 ACK 4 段,间隔 0.01s
+0.01 < . 1:1(0) ack 4001 win 10000
+0 %{print ("phase1 ack4 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 8001 win 10000
+0 %{print ("phase1 ack8 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 12001 win 10000
+0 %{print ("phase1 ack12 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 16001 win 10000
+0 %{print ("phase1 ack16 cwnd:", tcpi_snd_cwnd)}%
+0.01 < . 1:1(0) ack 20001 win 10000
+0 %{print ("phase1 ack20 cwnd:", tcpi_snd_cwnd)}%
// 阶段二:空闲 600ms,恢复发送
+0.6 write(4, ..., 1000) = 1000
+0 %{print ("after idle cwnd:", tcpi_snd_cwnd, "ca_state:", tcpi_ca_state)}%
#
# packetdrill tcp_cwv_005.pkt
phase1 after writes cwnd: 10
phase1 ack4 cwnd: 14
phase1 ack8 cwnd: 18
phase1 ack12 cwnd: 22
phase1 ack16 cwnd: 22
phase1 ack20 cwnd: 22
after idle cwnd: 10 ca_state: 0
#
tcp_slow_start_after_idle 的影响
上述行为由 tcp_slow_start_after_idle 参数控制,默认值为 1。
# sysctl -a | grep slow_start_after_idle
net.ipv4.tcp_slow_start_after_idle = 1
如果将该参数设置为 0,空闲后的 cwnd 重置将被禁用,cwnd 保持之前的值:
# sysctl -q net.ipv4.tcp_slow_start_after_idle=0
修改后重新运行 tcp_cwv_004.pkt,after idle cwnd 将保持 22 而非重置为 11。这在某些场景下(如数据中心内部、高频交互式应用)可以避免不必要的慢启动重启,但也意味着发送方可能基于一个过时的 cwnd 向网络注入突发流量。
# packetdrill tcp_cwv_004.pkt
phase1 after writes cwnd: 10
phase1 ack4 cwnd: 14
phase1 ack8 cwnd: 18
phase1 ack12 cwnd: 22
phase1 ack16 cwnd: 22
phase1 ack20 cwnd: 22
after idle cwnd: 22 ca_state: 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 7ACE《Wireshark & Packetdrill | TCP 拥塞窗口校验》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论