技术重在实践,提升源于细节。
前言
继续上篇关于的文章,之前描述的是一个接收窗口满的特殊案例,现象是接收端在接收窗口为 0 的情况下,依然正常接收了数据,最后也说明了是快速路径处理的原因。其中涉及到快速路径和慢速路径的切换过程,发现了些有趣的现象,再次记录说明下。
问题说明
首先简单回顾一下上次接收端能收的问题,如下脚本以及数据包信息,通过 setsockopt 方式限制了服务器端的接收缓存。
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.02 < P. 1461:2921(1460) ack 1 win 10000
+0 < P. 2921:4381(1460) ack 1 win 10000
+0.02 read (4,...,4380) = 4380
+0 `sleep 1`
#
通过 tcpdump 抓包信息如下,可以看到服务器的接收窗口大小为 2920,客户端发送了第一个 1460 字节大小的数据,服务器端进行了 ACK 响应,由于应用未读取数据的缘故,接收窗口大小降为了 1460 大小,之后客户端继续发送了两个 1460 字节大小的数据,理论上由于接收窗口满的限制,服务器端仅会正常接收第二个数据段,而丢弃第三个数据段,但此时看到的现象是,服务器端响应的 ACK 数据包 ACK Num 为 4381 ,也就是说实际 TCP 层面是确认了第二个和第三个数据段,而且通过 read() 也证明可以读取 4380 长度大小的数据。
这都说明了接收端是能收到超出自身接收窗口的数据,并正常读取,这个现象是因为在接收第三个数据段时进入了快速路径下的数据包处理流程,在快速路径下减少了很多处理,也就是并不会执行很严格的检查,包括接收窗口大小是否为 0 。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... forfull protocol decode
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
19:31:58.237559 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
19:31:58.237584 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [S.], seq 346115781, ack 1, win 2920, options [mss 1460], length 0
19:31:58.247660 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [.], ack 1, win 10000, length 0
19:31:58.257728 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP
19:31:58.257746 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 1461, win 1460, length 0
19:31:58.277741 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
19:31:58.277764 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
19:31:58.297823 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 4381, win 2920, length 0
19:31:59.299615 ? Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [F.], seq 1, ack 4381, win 2920, length 0
19:31:59.299641 ? In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
#
相关的内核接收代码在 tcp_rcv_established() 中,这段代码主要是 Linux 内核中 TCP 快速路径(Fast Path)处理逻辑的一部分,用于判断一个接收到的 TCP 数据包是否可以快速处理,如下。
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
conststruct tcphdr *th = (conststruct tcphdr *)skb->data;
struct tcp_sock *tp = tcp_sk(sk);
unsigned intlen = skb->len;
...
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
...
if (len <= tcp_header_len) {
...
} else {
...
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
...
}
}
...
问题扩展
上面所提到的关于快速路径的判断逻辑,有一个地方是关于窗口大小变化,在此测试继续扩展下相关实验。
修改脚本,将 TCP 三次握手阶段的 Win 大小改为 11000,之后三个仍保持 10000 大小。
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 11000
+0 accept(3, ..., ...) = 4
+0.01 < P. 1:1461(1460) ack 1 win 10000
+0.02 < P. 1461:2921(1460) ack 1 win 10000
+0 < P. 2921:4381(1460) ack 1 win 10000
+0.02 read (4,...,4380) = 4380
+0 `sleep 1`
#
通过 tcpdump 抓包信息如下,可以看到现象基本一致,服务器端还是会正常接收第二个和第三个数据段,ACK Num 仍为 4381 。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:18:10.589575 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
22:18:10.589602 tun0 Out IP 192.168.16.143.8080>192.0.2.1.37351: Flags [S.], seq 2402739733, ack 1, win 2920, options [mss 1460], length 0
22:18:10.599687 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [.], ack 1, win 11000, length 0
22:18:10.609801 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP
22:18:10.609824 tun0 Out IP 192.168.16.143.8080>192.0.2.1.37351: Flags [.], ack 1461, win 1460, length 0
22:18:10.629777 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
22:18:10.629808 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
22:18:10.649860 tun0 Out IP 192.168.16.143.8080>192.0.2.1.37351: Flags [.], ack 4381, win 2920, length 0
22:18:11.665751 tun0 Out IP 192.168.16.143.8080>192.0.2.1.37351: Flags [F.], seq 1, ack 4381, win 2920, length 0
22:18:11.665782 tun0 In IP 192.0.2.1.37351>192.168.16.143.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
#
但实际于对于服务器端接收到的这几个数据包,处理流程和之前已经不太一样,如下。
...
+0.01 < . 1:1(0) ack 1 win 11000
// 由于 Win 变为了 10000,与之前 11000 不一致,所以 tp->pred_flags 值不一致,
因此这个数据包直接走的是慢速流程处理,且未更新tp->snd_wnd,同时也未更新 tp->pred_flags 值。
+0.01 < P. 1:1461(1460) ack 1 win 10000
// Win 10000 仍是与 Win 11000 时候的 tp->pred_flags 值比较,仍然不一致,依然是慢速流程处理,
但此时由于ack_seq变化,更新了tp->snd_wnd,同时以 Win 10000 计算更新了 tp->pred_flags 值。
+0.02 < P. 1461:2921(1460) ack 1 win 10000
// 此时 tp->pred_flags 值一致,先进入快速处理流程,再检查truesize=2304 小于 forward_alloc=3584,
最终走的快速处理流程,所以这个超出接收窗口大小的数据段仍然能接收。
+0 < P. 2921:4381(1460) ack 1 win 10000
#
继续修改脚本,将第一个数据段的 Win 也改为 11000 大小,如下。
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 11000
+0 accept(3, ..., ...) = 4
+0.01 < P. 1:1461(1460) ack 1 win 11000
+0.02 < P. 1461:2921(1460) ack 1 win 10000
+0 < P. 2921:4381(1460) ack 1 win 10000
+0.02 read (4,...,4380) = 4380
+0 `sleep 1`
#
通过 tcpdump 抓包信息如下,可以看到现象基本一致,服务器端还是会正常接收第二个和第三个数据段,ACK Num 仍为 4381 。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... forfull protocol decode
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:33:12.977559 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
22:33:12.977585 tun0 Out IP 192.168.208.53.8080>192.0.2.1.43083: Flags [S.], seq 1691412187, ack 1, win 2920, options [mss 1460], length 0
22:33:12.987650 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [.], ack 1, win 11000, length 0
22:33:12.997711 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [P.], seq 1:1461, ack 1, win 11000, length 1460: HTTP
22:33:12.997729 tun0 Out IP 192.168.208.53.8080>192.0.2.1.43083: Flags [.], ack 1461, win 1460, length 0
22:33:13.017717 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
22:33:13.017740 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
22:33:13.037769 tun0 Out IP 192.168.208.53.8080>192.0.2.1.43083: Flags [.], ack 4381, win 2920, length 0
22:33:14.039595 tun0 Out IP 192.168.208.53.8080>192.0.2.1.43083: Flags [F.], seq 1, ack 4381, win 2920, length 0
22:33:14.039620 tun0 In IP 192.0.2.1.43083>192.168.208.53.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
同样对于服务器端接收到的这几个数据包,处理流程如下。
...
+0.01 < . 1:1(0) ack 1 win 11000
// 此时 tp->pred_flags 值一致,先进入快速处理流程,再检查truesize=2304 大于 forward_alloc=0,
最终走的是慢速处理流程。
+0.01 < P. 1:1461(1460) ack 1 win 11000
// 由于 Win 变为了 10000,与之前 11000 不一致,所以 tp->pred_flags 值不一致,
因此这个数据包直接走的是慢速流程处理,但此时由于ack_seq变化,更新了tp->snd_wnd,同时更新 tp->pred_flags 值。
+0.02 < P. 1461:2921(1460) ack 1 win 10000
// 此时 tp->pred_flags 值一致,先进入快速处理流程,再检查truesize=2304 小于 forward_alloc=3584,
最终走的快速处理流程,所以这个超出接收窗口大小的数据段仍然能接收。
+0 < P. 2921:4381(1460) ack 1 win 10000
#
继续修改脚本,将第二个数据段的 Win 也改为 11000 大小,如下。
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 11000
+0 accept(3, ..., ...) = 4
+0.01 < P. 1:1461(1460) ack 1 win 11000
+0.02 < P. 1461:2921(1460) ack 1 win 11000
+0 < P. 2921:4381(1460) ack 1 win 10000
+0.02 read (4,...,4380) = 4380
+0 `sleep 1`
#
此时运行脚本直接报错,可以看到 read() 像之前一样尝试读取 4380 大小的数据,但实际上只读取到了 2920 大小,可见最后一个也就是第三个数据段并未正常接收。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... forfull protocol decode
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
19:31:58.237559 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
19:31:58.237584 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [S.], seq 346115781, ack 1, win 2920, options [mss 1460], length 0
19:31:58.247660 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [.], ack 1, win 10000, length 0
19:31:58.257728 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP
19:31:58.257746 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 1461, win 1460, length 0
19:31:58.277741 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
19:31:58.277764 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
19:31:58.297823 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 4381, win 2920, length 0
19:31:59.299615 ? Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [F.], seq 1, ack 4381, win 2920, length 0
19:31:59.299641 ? In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
#
0
通过 tcpdump 抓包信息如下,可以看到与之前的现象不一致了,服务器端只会正常接收第二个数据段,而丢弃了第三个数据段,ACK Num 为 2921 。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... forfull protocol decode
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
19:31:58.237559 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
19:31:58.237584 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [S.], seq 346115781, ack 1, win 2920, options [mss 1460], length 0
19:31:58.247660 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [.], ack 1, win 10000, length 0
19:31:58.257728 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP
19:31:58.257746 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 1461, win 1460, length 0
19:31:58.277741 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
19:31:58.277764 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
19:31:58.297823 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 4381, win 2920, length 0
19:31:59.299615 ? Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [F.], seq 1, ack 4381, win 2920, length 0
19:31:59.299641 ? In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
#
1
那么对于服务器端接收到的这几个数据包,实际处理流程如下。
# tcpdump -i any-nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... forfull protocol decode
listening onany, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
19:31:58.237559 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [S], seq 0, win 10000, options [mss 1460], length 0
19:31:58.237584 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [S.], seq 346115781, ack 1, win 2920, options [mss 1460], length 0
19:31:58.247660 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [.], ack 1, win 10000, length 0
19:31:58.257728 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1:1461, ack 1, win 10000, length 1460: HTTP
19:31:58.257746 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 1461, win 1460, length 0
19:31:58.277741 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 1461:2921, ack 1, win 10000, length 1460: HTTP
19:31:58.277764 tun0 In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [P.], seq 2921:4381, ack 1, win 10000, length 1460: HTTP
19:31:58.297823 tun0 Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [.], ack 4381, win 2920, length 0
19:31:59.299615 ? Out IP 192.168.25.166.8080>192.0.2.1.37193: Flags [F.], seq 1, ack 4381, win 2920, length 0
19:31:59.299641 ? In IP 192.0.2.1.37193>192.168.25.166.8080: Flags [R.], seq 4381, ack 1, win 10000, length 0
#
2
问题总结
TCP 弯弯绕绕的知识点真是多, 。
往期推荐
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...