TCP接收方对于重叠报文的处理

一、接受方有效负载的判断
在rfc793中说明了对于判断接收到的报文是否有负载的判断在Page 24和Page 25之间,其中的原文说明为
 A segment is judged to occupy a portion of valid receive sequence
  space if
 
    RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
 
  or
 
    RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
 
  The first part of this test checks to see if the beginning of the
  segment falls in the window, the second part of the test checks to see
  if the end of the segment falls in the window; if the segment passes
  either part of the test it contains data in the window.
通俗的说,就是说接收到的报文和接受方的滑动窗口有相交就可以,在内核中,这个检测通过函数
static inline int tcp_sequence(struct tcp_sock *tp, u32 seq, u32 end_seq)
{
return !before(end_seq, tp->rcv_wup) &&
!after(seq, tp->rcv_nxt + tcp_receive_window(tp));
}
完成,这个函数看起来有些复杂,事实上它的确很复杂,但是现在我们只关心其中最为基本的判断,就是判断这个报文是否包含了接受方“可接受的有效负载”这个方面的判断。由于这个函数看起来比较复杂,我们随便从内核中搜索下判断两个区间是否有重叠的函数,模糊搜索overlap,可以看到最为简洁的一个函数linux-2.6.21\net\netfilter\nf_sockopt.c:
static inline int overlap(int min1, int max1, int min2, int max2)
{
return max1 > min2 && min1 < max2;
}
本质上判断两个区间是否相交的实现就是这么判断的,就是每个区间的最大值大于另一个区间的最小值。对于这里的TCP环境,我们就不考虑边界值的情况了,这个边界值有很多细节和讲究,所以这里就暂时不讨论了。
二、为什么会这么规定
这个地方乍一看是比较奇怪的,比方说,如果我先收到了一个序列号为[30,50]的报文,然后再收到一个[40,60]的报文,这个时候两个报文都是合法的,也意味着接受方需要同时接受并处理这两个报文,这个看起来比较奇怪,在什么情况下会出现这种情况呢?最简单的办法就是在内核中修改报文的序列号,强制发送两个这样序列号的报文,这也是我最早验证这个问题使用的方法,但是这么暴力的修改并没有实际意义,既然rfc这么规定,应该是在真实环境中可能出现的才这么规定,如果不能出现,就应该直接禁止掉这种包的接受和处理。所以下面是借助iptables来模拟下真实环境中可能出现的报文重叠现象。
 
tsecer@harry: cat rxcollapse.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <arpa/inet.h> //inet_pton
#include <errno.h> //errno
#include <unistd.h> //sleep
#include <stdlib.h> //exit
#include <netinet/tcp.h>//TCP_NODELAY
 
#define QUIT(err, fmt, msg...) do {fprintf(stderr, "LINE:%d, err %d "fmt"\n", __LINE__, err, ##msg); exit(err); }while(0)
int main(int argc, char *argv[])
{
        in_addr_t stloopaddr;
        inet_pton(AF_INET, "127.0.0.1", (void*)&stloopaddr);
        sockaddr_in stlocal = 
                {AF_INET, htons(7777), {INADDR_ANY}};
        sockaddr_in stremote = 
                {AF_INET, htons(22), {stloopaddr}};
        int sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sfd == -1)
        {
                QUIT(errno, "socket");
        }
        if (bind(sfd, (struct sockaddr *)&stlocal, sizeof(stlocal)) == -1)
        {
               QUIT(errno, "bind");
        }
        if (connect(sfd, (struct sockaddr*)&stremote, sizeof(stremote)))
        {
             QUIT(errno, "connect");
        }
 
int iYes=1;
if (setsockopt(sfd, SOL_TCP, TCP_NODELAY, (void*)&iYes, sizeof(iYes)))
{
QUIT(errno, "setsockopt");
}
sleep(3);
char buff1[]= "aaaaaa";
char buff2[]= "bbbbbb";
printf("%d\n", write(sfd, (void*)buff1, sizeof(buff1) - 1));
printf("%d\n",write(sfd, (void*)buff2, sizeof(buff2) - 1));
        sleep(10000);
}
 
tsecer@harry: g++ rxcollapse.cpp -o rxcollapse
tsecer@harry: cat rxcollapse.sh 
./rxcollapse &
 
sleep 2
 
iptables -I INPUT -p tcp --tcp-flags ACK ACK -j DROP
netstat -anp | grep -w "22\|7777"
sleep 20
 
iptables -D INPUT -p tcp --tcp-flags ACK ACK -j DROP
 
pkill  rxcollapse
tsecer@harry: sh -x rxcollapse.sh 
+ sleep 2
+ ./rxcollapse
+ iptables -I INPUT -p tcp --tcp-flags ACK ACK -j DROP
+ grep -w '22\|7777'
+ netstat -anp
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      721/sshd            
tcp        0      0 127.0.0.1:22            127.0.0.1:7777          ESTABLISHED 3630/sshd: [accepte 
tcp       21      0 127.0.0.1:7777          127.0.0.1:22            ESTABLISHED 3628/./rxcollapse   
tcp6       0      0 :::22                   :::*                    LISTEN      721/sshd            
+ sleep 20
6
6
+ iptables -D INPUT -p tcp --tcp-flags ACK ACK -j DROP
+ pkill rxcollapse
rxcollapse.sh: line 11:  3628 Terminated              ./rxcollapse
 
下面是在上面操作的同时通过tcpdump抓包获得的输出内容:
tsecer@harry: 
tsecer@harry: tcpdump -nnvvSi lo -As0  tcp and port 7777
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
07:54:28.302043 IP (tos 0x0, ttl 64, id 47766, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [S], cksum 0xfe30 (incorrect -> 0x3e3f), seq 183558512, win 43690, options [mss 65495,sackOK,TS val 3300326 ecr 0,nop,wscale 7], length 0
E..<..@.@..#.........a..
..p.........0.........
.2[.........
07:54:28.302148 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [S.], cksum 0xfe30 (incorrect -> 0xb691), seq 2846654934, ack 183558513, win 43690, options [mss 65495,sackOK,TS val 3300327 ecr 3300326,nop,wscale 7], length 0
E..<..@.@.<............a....
..q.....0.........
.2[..2[.....
07:54:28.302240 IP (tos 0x0, ttl 64, id 47767, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [.], cksum 0xfe28 (incorrect -> 0x88d5), seq 183558513, ack 2846654935, win 342, options [nop,nop,TS val 3300327 ecr 3300327], length 0
E..4..@.@..*.........a..
..q.......V.(.....
.2[..2[.
07:54:28.402057 IP (tos 0x0, ttl 64, id 55856, offset 0, flags [DF], proto TCP (6), length 73)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [P.], cksum 0xfe3d (incorrect -> 0xc6ac), seq 2846654935:2846654956, ack 183558513, win 342, options [nop,nop,TS val 3300426 ecr 3300327], length 21
E..I.0@.@.b|...........a....
..q...V.=.....
.2\J.2[.SSH-2.0-OpenSSH_6.3
 
07:54:28.402093 IP (tos 0x0, ttl 64, id 47768, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [.], cksum 0xfe28 (incorrect -> 0x87fa), seq 183558513, ack 2846654956, win 342, options [nop,nop,TS val 3300426 ecr 3300426], length 0
E..4..@.@..).........a..
..q.......V.(.....
.2\J.2\J
07:54:31.308578 IP (tos 0x0, ttl 64, id 47769, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x586d), seq 183558513:183558519, ack 2846654956, win 342, options [nop,nop,TS val 3303333 ecr 3300426], length 6
E..:..@.@..".........a..
..q.......V.......
.2g..2\Jaaaaaa
07:54:31.308775 IP (tos 0x0, ttl 64, id 47770, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x5564), seq 183558519:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303333 ecr 3300426], length 6
E..:..@.@..!.........a..
..w.......V.......
.2g..2\Jbbbbbb
07:54:31.318288 IP (tos 0x0, ttl 64, id 47771, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x555a), seq 183558519:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303343 ecr 3300426], length 6
E..:..@.@.. .........a..
..w.......V.......
.2g..2\Jbbbbbb
07:54:31.521290 IP (tos 0x0, ttl 64, id 47772, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x306b), seq183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303546 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2hz.2\Jaaaaaabbbbbb
07:54:31.928855 IP (tos 0x0, ttl 64, id 47773, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2ed4), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303953 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2j..2\Jaaaaaabbbbbb
07:54:32.743433 IP (tos 0x0, ttl 64, id 47774, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2ba5), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3304768 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2m@.2\Jaaaaaabbbbbb
07:54:34.372490 IP (tos 0x0, ttl 64, id 47775, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2548), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3306397 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2s..2\Jaaaaaabbbbbb
07:54:37.623209 IP (tos 0x0, ttl 64, id 47776, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x1895), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3309648 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2.P.2\Jaaaaaabbbbbb
07:54:44.135414 IP (tos 0x0, ttl 64, id 47777, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0xff24), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3316160 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2...2\Jaaaaaabbbbbb
07:54:50.739325 IP (tos 0x0, ttl 64, id 47778, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [R.], cksum 0xfe28 (incorrect -> 0x30a8), seq 183558525, ack 2846654956, win 342, options [nop,nop,TS val 3322764 ecr 3300426], length 0
E..4..@.@............a..
..}.......V.(.....
.2...2\J
07:54:50.739462 IP (tos 0x0, ttl 64, id 55857, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [.], cksum 0xfe28 (incorrect -> 0x30b8), seq 2846654956, ack 183558513, win 342, options [nop,nop,TS val 3322764 ecr 3300426], length 0
E..4.1@.@.b............a....
..q...V.(.....
.2...2\J
07:54:50.740026 IP (tos 0x0, ttl 64, id 30712, offset 0, flags [DF], proto TCP (6), length 40)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [R], cksum 0xa705 (correct), seq 183558513, win 0, length 0
E..(w.@.@............a..
..q....P.......
 
三、模拟的方法的说明
这里模拟的方法就是借助“丢包”来促使发送方在重传的过程中将两个相邻的重传包合并。这里为了简单,没有写自己的服务器端程序,看了下系统打开的侦听端口,发现22端口被sshd打开侦听,所以客户端直接绑定到7777来链接sshd的22端口,当然这个端口并不需要有什么特殊之处,所以不用关注。
在连接的时候,通过TCP_NODELAY禁止nagle算法,也就是要求两个小包"aaaaa"和"bbbbbb"在首次发送的时候是两个独立的包,不用等待第一个包的返回就可以发送第二个小包。
在connect连接到server之后,进程通过sleep暂停一段时间,这个时间用来等待iptable禁掉server的ack包,从而让客户端认为自己发送的两个小包都丢失了,进而进行重传。
两次write之间不要有(太长)间隔,如果第二个包的write调用发生在第一个包的重传之后,此时tcp会进入丢包状态,此时拥塞窗口调整为1,从而第二个包无法通过拥塞检测而无法发送。
 
丢包进入拥塞控制的代码
static void tcp_retransmit_timer(struct sock *sk)--->>>>tcp_enter_loss
tp->snd_cwnd    = 1;
tp->snd_cwnd_cnt   = 0;
tp->snd_cwnd_stamp = tcp_time_stamp;
在发送时的检测
tcp_write_xmit--->>>tcp_cwnd_test
cwnd = tp->snd_cwnd;
if (in_flight < cwnd)
return (cwnd - in_flight);
四、重传报文的合并
tcp_retransmit_skb--->>>tcp_retrans_try_collapse
/* Attempt to collapse two adjacent SKB's during retransmission. */
static void tcp_retrans_try_collapse(struct sock *sk, struct sk_buff *skb, int mss_now)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *next_skb = skb->next;
 
/* The first test we must make is that neither of these two
 * SKB's are still referenced by someone else.
 */
if (!skb_cloned(skb) && !skb_cloned(next_skb)) {
……
/* Update sequence range on original skb. */
TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(next_skb)->end_seq;
 
/* Merge over control information. */
flags |= TCP_SKB_CB(next_skb)->flags; /* This moves PSH/FIN etc. over */
TCP_SKB_CB(skb)->flags = flags;
                ……
}
五 、接受方的处理
从接受方来看,通过了tcp_sequence之后就是通过tcp_data_queue进入队列
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
……
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
SOCK_DEBUG(sk, "partial packet: rcv_next %X seq %X - %X\n",
   tp->rcv_nxt, TCP_SKB_CB(skb)->seq,
   TCP_SKB_CB(skb)->end_seq);
 
tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
 
/* If window is closed, drop tail of packet. But after
 * remembering D-SACK for its head made in previous line.
 */
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}                          
……
}
可以看到接收的时候也并没有特别歧视,只是在设置了SO_DEBUG选项的时候打印一个内核日志,然后就照样放到接受队列了。但是这么做总也不是个办法,岂不是接受方会收到莫名其妙的相同的一份拷贝?
六、API层的处理
这个不是常规的tcp socket read的执行流程,但是看起来更简洁,所以就以这个为例进行说明
int tcp_read_sock(struct sock *sk, read_descriptor_t *desc,sk_read_actor_t recv_actor) --->>>
static inline struct sk_buff *tcp_recv_skb(struct sock *sk, u32 seq, u32 *off)
{
struct sk_buff *skb;
u32 offset;
 
skb_queue_walk(&sk->sk_receive_queue, skb) {
offset = seq - TCP_SKB_CB(skb)->seq;
if (skb->h.th->syn)
offset--;
if (offset < skb->len || skb->h.th->fin) {
*off = offset;
return skb;
}
}
return NULL;
}
以刚才的例子[30,50],[40,60]为例,当从30开始读取20个字节之后,在调用tcp_recv_skb时,seq为50,此时返回的off就是第二个报文的50-40=10这个未知开始,或者更直观的说,返回的*off满足 offset = seq - TCP_SKB_CB(skb)->seq;,也就是TCP_SKB_CB(skb)->seq + offset = seq,只截取了前一个报文结束的地方,重叠的地方以起始未知更低的报文为准。

posted on 2019-03-07 09:52  tsecer  阅读(1125)  评论(0编辑  收藏  举报

导航