• Selective Acknowledgment 选项 浅析 2


    来自:http://abcdxyzk.github.io/blog/2013/09/06/kernel-net-sack/

    static int
    tcp_sacktag_write_queue(struct sock *sk, const struct sk_buff *ack_skb,
                u32 prior_snd_una, struct tcp_sacktag_state *state)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        //ptr指向的就是SACK选项的起始位置
        const unsigned char *ptr = (skb_transport_header(ack_skb) +
                        TCP_SKB_CB(ack_skb)->sacked);
        //sp_wire在ptr的基础上前移两个字节,跳过选项的kind和len,指向SACK信息块的开始
        struct tcp_sack_block_wire *sp_wire = (struct tcp_sack_block_wire *)(ptr+2);
        //用于保存最后解析出来的SACK信息块,因为一个skb中最多可以携带4个,所以数组长度为4
        struct tcp_sack_block sp[TCP_NUM_SACKS];
        struct tcp_sack_block *cache;
        struct sk_buff *skb;
        //每个SACK信息块为8字节,所以num_sacks记录的是该skb中携带的SACK信息块的个数
        int num_sacks = min(TCP_NUM_SACKS, (ptr[1] - TCPOLEN_SACK_BASE) >> 3);
        int used_sacks;
        bool found_dup_sack = false;
        int i, j;
        int first_sack_index;
    
        state->flag = 0;
        state->reord = tp->packets_out;
    //如果之前还没有SACK确认过的段,那么复位highest_sack域为发送队列的头部,即当前发送队列中
        //最大的序号(该数据可能还尚未发送),
        if (!tp->sacked_out) {
            if (WARN_ON(tp->fackets_out))
                tp->fackets_out = 0;
            tcp_highest_sack_reset(sk);//因为当sacked_out为0,则说明没有通过sack确认的段,此时highest_sack自然就指向写队列的头。
            //--->tcp_sk(sk)->highest_sack = tcp_write_queue_head(sk);
    }
    
        }
    
    /*
    D-SACK的判断是通过RFC2883中所描述的进行的。如果是下面两种情况,则说明收到了一个D-SACK。
    1 如果SACK的第一个段所ack的区域被当前skb的ack所确认的段覆盖了一部分,则说明我们收到了一个d-sack,
    而代码中也就是sack第一个段的起始序列号小于snd_una
    2、如果sack的第二个段完全包含了第二个段,则说明我们收到了重复的sack
    */
        found_dup_sack = tcp_check_dsack(sk, ack_skb, sp_wire,
                         num_sacks, prior_snd_una);
        if (found_dup_sack)//检查是否有DSACK信息块,如果有,设置FLAG_DSACKING_ACK标记,表示输入段中有DSACK
            state->flag |= FLAG_DSACKING_ACK;
    
        /* Eliminate too old ACKs, but take into
         * account more or less fresh ones, they can
         * contain valid SACK info.为接收方通告过的最大接收窗口。
         * 如果SACK信息是很早以前的,直接丢弃
         */
        if (before(TCP_SKB_CB(ack_skb)->ack_seq, prior_snd_una - tp->max_window))
            return 0;
    //当前根本就没有需要确认的段,所以也没有必要继续处理
    
        if (!tp->packets_out)
            goto out;
    //后面会对sp[]数组按照SACK信息块中seq的大小做升序排列,first_sack_index
        //记录了排序后原本输入段携带的第一个SACK信息块在sp[]中的位置,如果第一个
        //SACK块无效,那么最终first_sack_index为-1
        used_sacks = 0;
        first_sack_index = 0;
        for (i = 0; i < num_sacks; i++) {
            //如果有DSACK,那么它一定在第一个位置。综合前面的判断设置dup_sack
            bool dup_sack = !i && found_dup_sack;
    
            sp[used_sacks].start_seq = get_unaligned_be32(&sp_wire[i].start_seq);
            sp[used_sacks].end_seq = get_unaligned_be32(&sp_wire[i].end_seq);
    //如果SACK信息块无效,分别进行相关统计
            if (!tcp_is_sackblock_valid(tp, dup_sack,
                            sp[used_sacks].start_seq,
                            sp[used_sacks].end_seq)) {
                int mib_idx;
    
                if (dup_sack) {
                    if (!tp->undo_marker)
                        mib_idx = LINUX_MIB_TCPDSACKIGNOREDNOUNDO;
                    else
                        mib_idx = LINUX_MIB_TCPDSACKIGNOREDOLD;
                } else {
                    /* Don't count olds caused by ACK reordering */
                    if ((TCP_SKB_CB(ack_skb)->ack_seq != tp->snd_una) &&
                        !after(sp[used_sacks].end_seq, tp->snd_una))
                        continue;
                    mib_idx = LINUX_MIB_TCPSACKDISCARD;
                }
    
                NET_INC_STATS(sock_net(sk), mib_idx);
                if (i == 0)//第一个SACK是无效的SACK,所以设置first_sack_index为-1
                    first_sack_index = -1;
                continue;
            }
    
            /* Ignore very old stuff early */
            if (!after(sp[used_sacks].end_seq, prior_snd_una))
                continue;
    
            used_sacks++;
        }
    
        /* order SACK blocks to allow in order walk of the retrans queue 
        对实际使用的SACK块,按起始序列号,从小到大进行冒泡排序。*/
        for (i = used_sacks - 1; i > 0; i--) {
            for (j = 0; j < i; j++) {
                if (after(sp[j].start_seq, sp[j + 1].start_seq)) {
                    swap(sp[j], sp[j + 1]);
    
                    /* Track where the first SACK �lock goes to */
                    if (j == first_sack_index)///注意first_sack_index会跟踪原始的第一个SACK信息块所在位置
                        first_sack_index = j + 1;
                }
            }
        }
    
        skb = tcp_write_queue_head(sk);
        state->fack_count = 0;
        i = 0;
    /*一旦收到对端的SACK信息,那么说明发生了丢包或者乱序,
    而且这种状况可能往往无法    立即恢复,这意味着发送方会连续多次收到SACK信息,
    而且这些SACK信息很可能是重复的。为了减少对发送队列的遍历次数,
    这里发送方在用SACK信息块更新发送队列时采    用了cache机制。本质上也很简单,
    就是将上一次的SACK信息块保存下来,在本次处理过程中,如果发现上一次已
    经处理过了该范围的SACK,那么就可以跳过不处理。
    cache    //信息块就保存在tp->recv_sack_cache[]中
    */处理重传队列中的skb

    重传队列中的skb有三种类型,分别是SACKED(S), RETRANS® 和LOST(L),而每种类型所处理的数据包的个数分别保存在sacked_out, retrans_out 和lost_out中。

    而处于重传队列的skb也就是会处于下面6中状态:

    Valid combinations are: b
     * Tag  InFlight    Description
     * 0    1        - orig segment is in flight.inFlight也就是表示还在网络中的段的个数;没有ack的包
     * S    0        - nothing flies, orig reached receiver.
     * L    0        - nothing flies, orig lost by net.
     * R    2        - both orig and retransmit are in flight.
     * L|R    1        - orig is lost, retransmit is in flight.
     * S|R  1        - orig reached receiver, retrans is still in flight.
     * (L|S|R is logically valid, it could occur when L|R is sacked,
     *  but it is equivalent to plain S and code short-curcuits it to S.
     *  L|S is logically invalid, it would mean -1 packet in flight 8))
     *
    重传队列中的skb的状态变迁是通过下面这几种事件来触发的:
    These 6 states form finite state machine, controlled by the following events:
     * 1. New ACK (+SACK) arrives. (tcp_sacktag_write_queue())
     * 2. Retransmission. (tcp_retransmit_skb(), tcp_xmit_retransmit_queue())
     * 3. Loss detection event of two flavors:
     *    A. Scoreboard estimator decided the packet is lost.
     *       A'. Reno "three dupacks" marks head of queue lost.
     *       A''. Its FACK modification, head until snd.fack is lost.
     *    B. SACK arrives sacking SND.NXT at the moment, when the
     *       segment was retransmitted.
     * 4. D-SACK added new rule: D-SACK changes any tag to S.
     *
    tcp socket的high_seq域,这个域是我们进入拥塞控制的时候最大的发送序列号,也就是snd_nxt.
    if (!tp->sacked_out) {/* 如果之前没有SACK块 */ /* It's already past, so skip checking against it */ cache = tp->recv_sack_cache + ARRAY_SIZE(tp->recv_sack_cache); } else { cache = tp->recv_sack_cache; /* Skip empty blocks in at head of the cache 上次的SACK 信息块保存在了数组的末尾n 个位置, 所以这里跳过开头的无效SACK信息*/ while (tcp_sack_cache_ok(tp, cache) && !cache->start_seq && !cache->end_seq) cache++; } //下面的逻辑首先是检查cache和SACK信息块是否有交集,如果有,跳过当前SACK块,提高效率 while (i < used_sacks) { u32 start_seq = sp[i].start_seq; u32 end_seq = sp[i].end_seq; bool dup_sack = (found_dup_sack && (i == first_sack_index)); struct tcp_sack_block *next_dup = NULL; if (found_dup_sack && ((i + 1) == first_sack_index)) next_dup = &sp[i + 1]; /* Skip too early cached blocks blocks如果cache的区间为[100, 200), 而当前SACK信息块的区间为[300, 400), 直接跳过这些cache块 */ while (tcp_sack_cache_ok(tp, cache) && !before(start_seq, cache->end_seq)) cache++; /* Can skip some work by looking recv_sack_cache? cache和SACK块一定是有交集的*/ if (tcp_sack_cache_ok(tp, cache) && !dup_sack && after(end_seq, cache->start_seq)) { //1. cache[100, 300), SACK[40, 440) /* Head todo? */ if (before(start_seq, cache->start_seq)) { skb = tcp_sacktag_skip(skb, sk, state, start_seq);//前移发送队列,使得遍历指针知道seq为100的的地方 skb = tcp_sacktag_walk(skb, sk, next_dup, state,//用[40, 100)即[start_seq, cache->start_seq)标记发送队列 start_seq, cache->start_seq, dup_sack); } /* Rest of the block already fully processed? cache[100, 300), SACK[50, 200)SACK信息块的后半段已经全部被cache包含, 这部分已经标记过了,没必要重新标记,所以继续处理下一个SACK信息块 */ if (!after(end_seq, cache->end_seq)) goto advance_sp; /*next_dup不为NULL的唯一一种情况就是输入段的DSACK信息块表示的确认范围被 后面的SACK信息块完全包裹,比如DSACK为[100, 200), 后面SACK为[50, 300), 只有这种情况,排序后,DSACK块才能排在SACK之后,这样next_dup才不为NULL 有可能DSACK和cache也有一部分重合 *///果start_seq < next_dup->start_seq < cache->start_seq,那么next_dup落在 //* (start_seq, cache->start_seq) 内的部分已经被上面的处理过了:)现在处理的next_dup的剩余部分 skb = tcp_maybe_skipping_dsack(skb, sk, next_dup, state, cache->end_seq); /* ...tail remains todo... */ if (tcp_highest_sack_seq(tp) == cache->end_seq) { /* ...but better entrypoint exists! */ skb = tcp_highest_sack(sk); if (!skb)/* 如果已经到了snd_nxt了,那么直接退出SACK块的遍历 */ break; state->fack_count = tp->fackets_out; cache++; goto walk; } ///跳过发送队列中那些序号小于cache->end_seq的skb,它们已经被标记过了 skb = tcp_sacktag_skip(skb, sk, state, cache->end_seq); /* Check overlap against next cached too (past this one already) */ cache++; continue; } //这个SACK信息块和cache块没有重叠,并且其start_seq一定大于所有的cache块的end_seq if (!before(start_seq, tcp_highest_sack_seq(tp))) { skb = tcp_highest_sack(sk); if (!skb) break; state->fack_count = tp->fackets_out; } //跳过发送队列中那些序号小于start_seq的段 skb = tcp_sacktag_skip(skb, sk, state, start_seq); walk://更新序号位于[start_seq,end_seq)之间的skb的记分牌 skb = tcp_sacktag_walk(skb, sk, next_dup, state, start_seq, end_seq, dup_sack); advance_sp: i++; }
    、、

    用上次缓存的tp->recv_sack_cache块来避免重复工作,提高处理效率。

    主要思想就是,处理sack块时,和cache块作比较,如果它们有交集,说明交集部分已经处理过了,

    不用再重复处理。

    //更新cache,cache的大小同样是4个,并且每次收到SACK,上一次的cache内容都会清除,
        //然后将本次接收的SACK块排序后的结果保存在cache中
        /* Clear the head of the cache sack blocks so we can skip it next time */
        for (i = 0; i < ARRAY_SIZE(tp->recv_sack_cache) - used_sacks; i++) {
            tp->recv_sack_cache[i].start_seq = 0;
            tp->recv_sack_cache[i].end_seq = 0;
        }
        for (j = 0; j < used_sacks; j++)
            tp->recv_sack_cache[i++] = sp[j];
    //更新乱序信息
        if ((state->reord < tp->fackets_out) &&
            ((inet_csk(sk)->icsk_ca_state != TCP_CA_Loss) || tp->undo_marker))
            tcp_update_reordering(sk, tp->fackets_out - state->reord, 0);
    //更新计数器
        tcp_verify_left_out(tp);
    out:
    
    #if FASTRETRANS_DEBUG > 0
        WARN_ON((int)tp->sacked_out < 0);
        WARN_ON((int)tp->lost_out < 0);
        WARN_ON((int)tp->retrans_out < 0);
        WARN_ON((int)tcp_packets_in_flight(tp) < 0);
    #endif
        return state->flag;
    }

    tcp_sacktag_walk是遍历重传队列,找到对应需要设置的段,然后设置tcp_cb的sacked域为TCPCB_SACKED_ACKED,这里要注意,还有一种情况就是sack确认了多个skb,这个时候我们就需要合并这些skb,然后再处理

    /*从参数skb指向的位置开始向后遍历发送队列,
    更新序号位于[start_seq,end_seq)之间的skb的记分牌*/
    static struct sk_buff *tcp_sacktag_walk(struct sk_buff *skb, struct sock *sk,
                        struct tcp_sack_block *next_dup,
                        struct tcp_sacktag_state *state,
                        u32 start_seq, u32 end_seq,
                        bool dup_sack_in)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *tmp;
    
        tcp_for_write_queue_from(skb, sk) {
            int in_sack = 0;
            bool dup_sack = dup_sack_in;
    //已经遍历到了tp->sk_send_head,后面的skb还没有发送,结束处理
            if (skb == tcp_send_head(sk))
                break;
    //因为发送队列中skb的序号是递增的,而当前skb的seq大于等于
            //SACK块的末尾序号,说明该skb和SACK信息块已经没有交集了,
            //而且发送队列后续skb也不会和该SACK信息块有交集了,结束处理
            /* queue is in-order => we can short-circuit the walk early */
            if (!before(TCP_SKB_CB(skb)->seq, end_seq))
                break;
    /*//假设用序号dseq和dend表示DSACK信息块的序号,
    DSACK有两种情形:        
    1)dend<=snd_una,即DSACK是由已经ACK的数据段触发的;    
    2)dend>sud_una并且DSACK信息块的序号被后面的SACK信息块包含;        
    //情形1根本就无需更新记分牌,所以如果next_dup不为NULL,那么一定        
    //是情形2。此时,由于DSACK被SACK包围,所以一定是先处理SACK        
    //然后再处理DSACK,此时为了避免重复遍历发送队列,所以需要同时处理这    两个SACK信息块。
    */
    /*下一个是DSACK块,需要先处理DSACK块。如果dend>=skb->seq,
            //说明DSACK块和当前skb可能有交集*/
            if (next_dup  &&
                before(TCP_SKB_CB(skb)->seq, next_dup->end_seq)) {
                  //传入的序号范围是DSACK的序号范围
                in_sack = tcp_match_skb_to_sack(sk, skb,
                                next_dup->start_seq,
                                next_dup->end_seq);
                  //1)in_sack==0:压根就没有DSACK;
            //2)DSACK无法确认skb;
            //3)in_sack<0:DSACK导致了skb分片,但是分片失败了
                if (in_sack > 0)
                    dup_sack = true;
            }
    
            /* skb reference here is a bit tricky to get right, since
             * shifting can eat and free both this skb and the next,
             * so not even _safe variant of the loop is enough.
             */
            if (in_sack <= 0) {
                //对于情形1,没得说,这里用SACK检测。情形2和3也一样是因为SACK是DSACK的超集
                tmp = tcp_shift_skb_data(sk, skb, state,
                             start_seq, end_seq, dup_sack);
                if (tmp) {
                    if (tmp != skb) {
                        skb = tmp;
                        continue;
                    }
    
                    in_sack = 0;
                } else {
                    in_sack = tcp_match_skb_to_sack(sk, skb,
                                    start_seq,
                                    end_seq);
                }
            }
    //结果小于0,说明skb分割失败,处理结束
            if (unlikely(in_sack < 0))
                break;
    //如果最终检测到skb数据可以被确认,则标记该skb
            if (in_sack) {
                TCP_SKB_CB(skb)->sacked =
    /* Mark the given newly-SACKed range as such, adjusting counters and hints.
    用来设置对应的tag,这里所要设置的也就是tcp_cb的sacked域
    值 为:
    #define TCPCB_SACKED_ACKED      0x01 /* SKB ACK'd by a SACK block    
    #define TCPCB_SACKED_RETRANS    0x02  /* SKB retransmitted       
    #define TCPCB_LOST              0x04  /* SKB is lost         
    #define TCPCB_TAGBITS           0x07  /* All tag bits         
    #define TCPCB_EVER_RETRANS      0x80  /* Ever retransmitted frame
    #define TCPCB_RETRANS     (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)
    */ tcp_sacktag_one(sk, state, TCP_SKB_CB(skb)->sacked, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq, dup_sack, tcp_skb_pcount(skb), &skb->skb_mstamp); if (!before(TCP_SKB_CB(skb)->seq, tcp_highest_sack_seq(tp))) tcp_advance_highest_sack(sk, skb); } state->fack_count += tcp_skb_pcount(skb); } return skb; } /* Avoid all extra work that is being done by sacktag while walking in * a normal way 参数skb指向发送队列中的某个skb,该函数从skb开始向后遍历发送队列, 直到遍历到sk->sk_send_head(即下一个要发送的skb, 可能为NULL)或者skb的末尾序号 (假设序号范围为[seq1, end1))end1大于等于参数skip_to_seq为止, 返回终止时的skb指针。 该函数的作用就是跳过那些序号小于SACK信息块确认范围的skb。 */ static struct sk_buff *tcp_sacktag_skip(struct sk_buff *skb, struct sock *sk, struct tcp_sacktag_state *state, u32 skip_to_seq) { tcp_for_write_queue_from(skb, sk) { if (skb == tcp_send_head(sk)) break; if (after(TCP_SKB_CB(skb)->end_seq, skip_to_seq)) break; state->fack_count += tcp_skb_pcount(skb); } return skb; }

    note:SACK 选项的接收方在收到一个包后可能会被要求做大量处理。这种 N:1 的比例使 SACK 发送方可以打击非常强大的平台上的接收方。

    然后攻击者发送一个充满 SACK 选项的包,目的是使另一方主机扫描整个队列以处理每个选项。一个指向队列末尾包的选项可能会迫使接收方的 TCP 协议栈遍历整个队列以判断此选项指向哪一个包。显然,攻击客户机可以根据自己的喜好发送任何 SACK 选项,但不太容易看出的一个问题是,它可以轻易控制被攻击方的重传队列的长度。SACK 选项的创建者可以控制接收者对接收到的每个选项执行的工作量,因为它也可以控制队列的大小。

  • 相关阅读:
    Educational Codeforces Round 74 (Rated for Div. 2)
    Codeforces Round #591 (Div. 2, based on Technocup 2020 Elimination Round 1) 题解
    D
    Card Hand Sorting 二进制枚举暴力
    20172018-acmicpc-southeastern-european-regional-programming-contest-seerc-2017-en A
    Round #590 (Div. 3)
    A
    P2634 [国家集训队]聪聪可可
    HDU-4807-Lunch Time(二分+费用流,思维)
    易错分析
  • 原文地址:https://www.cnblogs.com/codestack/p/12905483.html
Copyright © 2020-2023  润新知