ACK 要么Quick 要么Delay
基于 packetdrill TCP 三次握手脚本,通过构造模拟服务器端场景,继续研究测试 TCP Delayed ACK 现象。
基础脚本
# cat tcp_delayed_ack_000.pkt0 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 Delayed ACK
TCP Delayed ACK(延迟确认)是一种为了减少网络中小数据包数量的优化机制。其核心原理是:当接收方收到数据后,它不会立即对每个收到的数据包单独发送 ACK(确认)数据包,而是等待一小段时间,期望在此期间有顺路的数据(比如应用层的响应)可以携带这个 ACK 一起发送给对方。如果在超时前都没有数据要发送,则再单独发送 ACK 数据包。这样做的目的是提高网络带宽利用率,减少纯 ACK 数据包的数量。然而,它也可能在某些情况下增加延迟。Linux 在实现上有个延迟 ACK 定时器,它的定时时间保存在一个 ato 变量中,默认值是 40 ms,但是会动态调整,最大定时时间为 500ms。
扩展测试
扩展一些综合场景,包括快速路径与慢速路径的不同,测试验证 Delayed ACK 的处理,部分场景在之前文章有所提及。
初始脚本如下,模拟注入两个 MSS 大小的数据包。
# cat tcp_delayed_ack_013.pkt0 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 `sleep 1`#
通过 tcpdump 捕获数据包如下,在收到第一个数据段后,服务器端立马响应 ACK 数据包,即 Quick ACK,而在收到第二个数据段时,服务器所发送的 ACK 数据包变为了 Delayed ACK,间隔 40ms+。
同时可以看到因 SO_RCVBUF 3000 限制,服务器 Win 实际大小为 2920,在收到两个数据段后,实际接收窗口已经降为 0。
# packetdrill tcp_delayed_ack_013.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 bytes10:37:59.619864 tun0 In IP 192.0.2.1.44967 > 192.168.46.14.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 010:37:59.619891 tun0 Out IP 192.168.46.14.8080 > 192.0.2.1.44967: Flags [S.], seq 2007312500, ack 1, win 2920, options [mss 1460], length 010:37:59.629964 tun0 In IP 192.0.2.1.44967 > 192.168.46.14.8080: Flags [.], ack 1, win 10000, length 010:37:59.640057 tun0 In IP 192.0.2.1.44967 > 192.168.46.14.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP10:37:59.640080 tun0 Out IP 192.168.46.14.8080 > 192.0.2.1.44967: Flags [.], ack 1461, win 1460, length 010:37:59.640093 tun0 In IP 192.0.2.1.44967 > 192.168.46.14.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP10:37:59.683564 tun0 Out IP 192.168.46.14.8080 > 192.0.2.1.44967: Flags [.], ack 2921, win 0, length 010:38:00.663603 ? Out IP 192.168.46.14.8080 > 192.0.2.1.44967: Flags [R.], seq 1, ack 2921, win 2920, length 010:38:00.663629 ? In IP 192.0.2.1.44967 > 192.168.46.14.8080: Flags [R.], seq 2921, ack 1, win 10000, length 0#
模拟增加一个数据段,脚本如下。
# cat tcp_delayed_ack_014.pkt0 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 < P. 2921:4381(1460) ack 1 win 10000+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,在收到第一个数据段后,服务器端立马响应 ACK 数据包,即 Quick ACK,而在收到第二个数据段时,服务器所发送的 ACK 数据包变为了 Delayed ACK,ACK 延迟发出,之后在收到第三个数据段时,理论应该超过了 MSS 1460 字节的大小,但却没有立马发送 ACK,仍然是 Delayed ACK,间隔 40ms+ 发出 ACK,且 ACK Num 为 4381,也就是说在 Win 0 的情况下,TCP 仍然接收了数据。
首先是 Delayed ACK ,为什么在超出 MSS 大小的情况下,仍然没有立马发出 ACK,可以在 __tcp_ack_snd_check() 函数中找到答案,关于超出 MSS 大小的条件 (tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss 只是其中一个,之后还要通过 &&(逻辑与)与后面的条件 (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat || __tcp_select_window(sk) >= tp->rcv_wnd) 进行判断。而在这里的原因是 (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat || __tcp_select_window(sk) >= tp->rcv_wnd) 条件无法满足,为 0,也就是说未被读取的数据量大于应用的低水位标记,而且重新计算得到的接收窗口(此时为0)比当前已经通告的窗口小。
再就是在接收第三个数据段时,实际进入了快速路径下的数据包处理流程,在快速路径下减少了很多处理,也就是并不会执行很严格的检查,包括接收窗口大小是否为 0 ,也就是说落在接收窗口之外,但预留缓存有,TCP 仍能接收并确认。
# packetdrill tcp_delayed_ack_014.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 bytes15:30:38.640732 tun0 In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 015:30:38.640795 tun0 Out IP 192.168.238.33.8080 > 192.0.2.1.42771: Flags [S.], seq 1335463627, ack 1, win 2920, options [mss 1460], length 015:30:38.651160 tun0 In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [.], ack 1, win 10000, length 015:30:38.661273 tun0 In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP15:30:38.661322 tun0 Out IP 192.168.238.33.8080 > 192.0.2.1.42771: Flags [.], ack 1461, win 1460, length 015:30:38.661340 tun0 In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP15:30:38.661384 tun0 In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP15:30:38.705677 tun0 Out IP 192.168.238.33.8080 > 192.0.2.1.42771: Flags [.], ack 4381, win 0, length 015:30:39.662891 ? Out IP 192.168.238.33.8080 > 192.0.2.1.42771: Flags [R.], seq 1, ack 4381, win 2920, length 015:30:39.662930 ? In IP 192.0.2.1.42771 > 192.168.238.33.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0#
尝试修改数据包的时间间隔,脚本如下。
# cat tcp_delayed_ack_015.pkt0 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.05 < P. 2921:4381(1460) ack 1 win 10000+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,在收到第一个数据段后,服务器端立马响应 ACK 数据包,即 Quick ACK,而在收到第二个数据段时,服务器所发送的 ACK 数据包变为了 Delayed ACK,间隔 40ms+ 发出 ACK。
之后在收到第三个数据段时,服务器端立马响应 ACK 数据包,且 ACK Num 为 2921,也就是说在 Win 0 的情况下,此时 TCP 没有接收该数据。
原因是在处理接收到的数据时,由于超出窗口之外,进入了 Quick ACK 模式,通过 tcp_incr_quickack() 增加了 icsk_ack.quick 的值为 2,之后条件判断因处于 Quick ACK 模式,所以调用 tcp_send_ack() 发出了 ACK 数据包。
# packetdrill tcp_delayed_ack_015.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:24:23.210723 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 022:24:23.210776 tun0 Out IP 192.168.81.143.8080 > 192.0.2.1.57777: Flags [S.], seq 2661366222, ack 1, win 2920, options [mss 1460], length 022:24:23.221001 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [.], ack 1, win 10000, length 022:24:23.231148 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP22:24:23.231227 tun0 Out IP 192.168.81.143.8080 > 192.0.2.1.57777: Flags [.], ack 1461, win 1460, length 022:24:23.231250 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP22:24:23.274138 tun0 Out IP 192.168.81.143.8080 > 192.0.2.1.57777: Flags [.], ack 2921, win 0, length 022:24:23.281080 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP22:24:23.281103 tun0 Out IP 192.168.81.143.8080 > 192.0.2.1.57777: Flags [.], ack 2921, win 0, length 022:24:24.283549 tun0 Out IP 192.168.81.143.8080 > 192.0.2.1.57777: Flags [R.], seq 1, ack 2921, win 2920, length 022:24:24.283578 tun0 In IP 192.0.2.1.57777 > 192.168.81.143.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0#
综合上述脚本基础,调整 RCVBUF,并增加 read(),脚本如下。
# cat tcp_delayed_ack_016.pkt0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0+0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [6000],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 read (4,...,2920) = 2920+0.01 < P. 2921:4381(1460) ack 1 win 10000+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,在收到前两个数据段后,服务器端均是立马响应 ACK 数据包,即 Quick ACK,因为通过减少 RCVBUF ,降低初始 Quick ACK 的次数,quickacks = tcp_sk(sk)->rcv_wnd / (2 * icsk->icsk_ack.rcv_mss),其中 rcv_wnd 5840 / 2*MSS 1460 = 2 ,Quick ACK 次数为 2 ,再之后的 read() 并没有再触发发送 ACK。
之后在收到第三个数据段时,经 __tcp_ack_snd_check() 判断需要进行 tcp_send_delayed_ack() ,ACK 延迟发送,间隔 40ms+。
# packetdrill tcp_delayed_ack_016.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 bytes11:42:27.939884 tun0 In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 011:42:27.939920 tun0 Out IP 192.168.206.54.8080 > 192.0.2.1.46035: Flags [S.], seq 4150009852, ack 1, win 5840, options [mss 1460], length 011:42:27.950030 tun0 In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [.], ack 1, win 10000, length 011:42:27.960102 tun0 In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP11:42:27.960116 tun0 Out IP 192.168.206.54.8080 > 192.0.2.1.46035: Flags [.], ack 1461, win 4380, length 011:42:27.960124 tun0 In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP11:42:27.960128 tun0 Out IP 192.168.206.54.8080 > 192.0.2.1.46035: Flags [.], ack 2921, win 2920, length 011:42:27.970158 tun0 In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP11:42:28.011564 tun0 Out IP 192.168.206.54.8080 > 192.0.2.1.46035: Flags [.], ack 4381, win 4380, length 011:42:29.972270 ? Out IP 192.168.206.54.8080 > 192.0.2.1.46035: Flags [R.], seq 1, ack 4381, win 4380, length 011:42:29.972307 ? In IP 192.0.2.1.46035 > 192.168.206.54.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0#
继续增加一个 MSS,脚本如下。
# cat tcp_delayed_ack_017.pkt0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0+0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [6000],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 read (4,...,2920) = 2920+0.01 < P. 2921:4381(1460) ack 1 win 10000+0 < P. 4381:5841(1460) ack 1 win 10000+0 `sleep 1`#
通过 tcpdump 捕获数据包如下,相较上个测试,在收到前三个数据段后,服务器的处理没有变化,之后在收到第四个数据段时,服务器端立马发送了 ACK,但如之前的案例分析,这里并不只是超出 MSS 大小一个条件所决定的。
依然是在 __tcp_ack_snd_check() 函数中找答案,关于超出 MSS 大小的条件 (tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss 为 1 只是其中一个,之后还要通过 &&(逻辑与)与后面的条件 (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat || __tcp_select_window(sk) >= tp->rcv_wnd) 进行判断。而此时 (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat || __tcp_select_window(sk) >= tp->rcv_wnd) 条件同样满足为 1,其中前者未被读取的数据量大于应用的低水位标记为 0 ,而后者重新计算得到的接收窗口为 2920 等于当前已经通告的窗口 2920,为 1,最终执行 tcp_send_ack() 发出 ACK 数据包。
# cat tcp_delayed_ack_013.pkt0 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 `sleep 1`#0
往期推荐
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……




还没有评论,来说两句吧...