alg 序列号调整

  在之前文章ftp&nat写到调整seq问题,现在专门来看下:

tcp负载长度发生变化

在ftp传输PORT命令或者PASV的应答时会进行alg处理,如果使能了nat则会修改PORT命令或者PASV的应答的内容,导致tcp负载发生变化。

/* Generic function for mangling variable-length address changes inside
 * NATed TCP connections (like the PORT XXX,XXX,XXX,XXX,XXX,XXX
 * command in FTP).
 *
 * Takes care about all the nasty sequence number changes, checksumming,
 * skb enlargement, ...
 *
 * */
int __nf_nat_mangle_tcp_packet(struct sk_buff *skb,struct nf_conn *ct,enum ip_conntrack_info ctinfo,
                   unsigned int protoff,unsigned int match_offset,
                   unsigned int match_len,const char *rep_buffer,unsigned int rep_len, bool adjust)
{
    -----------------------------

   /* 长度发生改变,需要调整序列号,match_len为原始的PORT命令内容长度,rep_len为修改后的长度 */
    /* 记住这里还没有变更tcp头的序列号,所以tcph->seq还是原始的值 */
if (adjust && rep_len != match_len)
        nf_ct_seqadj_set(ct, ctinfo, tcph->seq,
                 (int)rep_len - (int)match_len);//变化长度,变长为正,变短为负

    return 1;
}

 

其中:rep_len 表示替换后长度, match_len表示替换前的长度。

 

/**
 * struct nf_ct_seqadj - sequence number adjustment information
 *
 * @correction_pos: position of the last TCP sequence number modification上次序列号修改的位置
 * @offset_before: sequence number offset before last modification序列号偏移,在上一次修改之后
 * @offset_after: sequence number offset after last modification序列号偏移,在最后一次修改之后
 */
struct nf_ct_seqadj {
    u32        correction_pos;/* 最后一次修改tcp负载长度的报文原始的序列号 */
    s32        offset_before;/* 最后一次的上一次修改报文的tcp负载累积长度 */
    s32        offset_after;/* 最后一次修改tcp报文后的累积长度 */
};
/*
调整参数设置
偏移量off为零时不需要进行处理。首先,将此连接的状态设置IPS_SEQ_ADJUST_BIT标志,表明此连接需要进行序号调整。
对于此连接的第一次序号调整,三个相关参数的值都为零。记录下连接当前的序号correction_pos,调整之后的序号偏移量offset_after,
对于首次调整offset_before为零。
对于非首次的序号调整,如果现在的报文序号在上一次调整序号(correction_pos)位置之后,更新调整序号correction_pos的值,
并且offset_beforce记录的为上一次序号调整的偏移量,而offset_after记录的为最后一次调整的总的偏移量
*/
int nf_ct_seqadj_set(struct nf_conn *ct, enum ip_conntrack_info ctinfo,
             __be32 seq, s32 off)
{
    struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_ct_seqadj *this_way;

    if (off == 0)
        return 0;

    if (unlikely(!seqadj)) {
        WARN_ONCE(1, "Missing nfct_seqadj_ext_add() setup call\n");
        return 0;
    }

    set_bit(IPS_SEQ_ADJUST_BIT, &ct->status);

    spin_lock_bh(&ct->lock);
    this_way = &seqadj->seq[dir];/* 获取本方向的序列号信息,刚开始的时候是三者都为0 */
    if (this_way->offset_before == this_way->offset_after ||//只有初始化的时候会相等,说明是tcp负载长度第一次发生变化
        before(this_way->correction_pos, ntohl(seq))) {/* 新的序列号大于上一次的调整,后续调整 */
        this_way->correction_pos = ntohl(seq); //记录下连接当前的序号correction_pos
        this_way->offset_before     = this_way->offset_after;//记录的为上一次序号调整的偏移量
        this_way->offset_after    += off;//录的为最后一次调整的总的偏移量
    }
    spin_unlock_bh(&ct->lock);
    return 0;
}

 

 

TCP序号调整

当前处理的报文的序号,在调整序号之后,新的序号seqoff使用offset_after,否则,使用offset_before的值,后者可能是重传/乱序报文。报文的新序号等于其自带序号与偏移量seqoff的和。

               skb1-seq    correction_pos
                  |           |
------------------|-----------|--------------------|-----
                              |                    |
                              |                 skb2-seq
                              |
             offset_before <--|-->offset_after

对于seq序号的调整,使用方向dir在结构seqadj->seq所取得的相关数据。

对于ACK序号。其方向与以上的序号处理相反,使用other_way结构。

       previous
     correction_pos
          |                      current
          | skb3-ack_seq      correction_pos
          |        |              |
----------|--------|--------------|--------------------|-----
          |        |              |                    |
          |        |              |              skb4-ack_seq
          |        |              |                    |
          offset_before           |--> offset_after <--|

 

/* TCP sequence number adjustment.  Returns 1 on success, 0 on failure */
int nf_ct_seq_adjust(struct sk_buff *skb,
             struct nf_conn *ct, enum ip_conntrack_info ctinfo,
             unsigned int protoff)
{
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct tcphdr *tcph;
    __be32 newseq, newack;
    s32 seqoff, ackoff;
    struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);
    struct nf_ct_seqadj *this_way, *other_way;
    int res;

    this_way  = &seqadj->seq[dir];
    other_way = &seqadj->seq[!dir];

    if (!skb_make_writable(skb, protoff + sizeof(*tcph)))
        return 0;

    tcph = (void *)skb->data + protoff;
    spin_lock_bh(&ct->lock);
    //这个判断非常关键,一共有7中情况,见下面详细分析
    //这里采用的是after,没有等于号,也就是说tcph->seq==this_way->correction_pos
    //使用seqoff = this_way->offset_before,这符合实际。
    /* 新报文序列号是在最新修改之后,所以使用after,否则使用before */
    if (after(ntohl(tcph->seq), this_way->correction_pos))
        seqoff = this_way->offset_after;//新的序号seqoff使用offset_after
    else
        seqoff = this_way->offset_before;

//对于ACK序号。其方向与以上的序号处理相反,使用other_way结构
    /* 调整确认序列号,确认序列号应该与反方向的发送序列号的偏移进行匹配,
         * 判断该报文是在上次修改之前的报文,还是之后的报文。
         * other_way->correction_pos记录的是发送方向发生改变的报文的原始序列号。而tcph->ack_seq是接收方对修改序列号
         * 后的报文的应答,这里减去other_way->offset_before后即为对原始发送序列号报文的应答序列号。
         */

    if (after(ntohl(tcph->ack_seq) - other_way->offset_before,
          other_way->correction_pos))
        ackoff = other_way->offset_after;
    else
        ackoff = other_way->offset_before;

    
    /* 新的序列号等于原始序列号加上偏移 */
    newseq = htonl(ntohl(tcph->seq) + seqoff); 
    newack = htonl(ntohl(tcph->ack_seq) - ackoff);

    inet_proto_csum_replace4(&tcph->check, skb, tcph->seq, newseq, false);
    inet_proto_csum_replace4(&tcph->check, skb, tcph->ack_seq, newack,
                 false);

    pr_debug("Adjusting sequence number from %u->%u, ack from %u->%u\n",
         ntohl(tcph->seq), ntohl(newseq), ntohl(tcph->ack_seq),
         ntohl(newack));

    tcph->seq = newseq;
    tcph->ack_seq = newack;

    res = nf_ct_sack_adjust(skb, protoff, tcph, ct, ctinfo);
    spin_unlock_bh(&ct->lock);

    return res;
}

 

 

 

 当前还没有发生过tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为0,不会实际修改tcph->seq。

 

 

 

当前已经发生过一次tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为10,修改tcph->seq=tcph->seq+10。 

 

 

 当前已经发生过一次tcp负载长度变化,但是当前的报文的序列号为6000,小于this_way->correction_pos。这种情况就是报文迷路了导致的,

after(ntohl(tcph->seq), this_way->correction_pos)为假,seqoff = this_way->offset_before;也就是为0,不会实际修改tcph->seq。

 

 

 当前已经发生过两次tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为5,修改tcph->seq=tcph->seq+5。

 

 

 前已经发生过两次tcp负载长度变化,但是当前的报文的序列号为16000,小于this_way->correction_pos。这种情况就是报文迷路了导致的,

after(ntohl(tcph->seq), this_way->correction_pos)为假,seqoff = this_way->offset_before;也就是为10,会实际修改tcph->seq=tcph->seq+10。

 

当前已经发生过两次tcp负载长度变化,但是当前的报文的序列号为6000,小于this_way->correction_pos。这种情况就是报文严重迷路了导致的,after(ntohl(tcph->seq), this_way->correction_pos)为假,

seqoff = this_way->offset_before;也就是为10,会实际修改tcph->seq=tcph->seq+10。

这种修改是错误的,因为序列号6000的报文不需要修改序列号。也就是说netfilter不能正确处理两次tcp负载长度变化之前的迷路报文。

应答序列号的调整

  应答序列号表示接受方期望收到的下一个字节序列号。它等于接收方收到的报文的发送序列号加上报文的长度。

接收方还可以合并应答,也就是说应答报文不一定和接收的报文对应起来。 而且发送方在接收应答序列号时,只关注比当前已经应答过的序列号大的序列号即可。

正常情况下一对一应答报文

情况1:

 

 

当前发送方向还没有发生过tcp负载长度变化,接收方收到的tcph->seq=6000,假设报文长度为100,那么应答序列号为tcph->ack_seq=6100。after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为0,不会修改tcph->ack_seq。

情况2:

 

 

当前发送方向已经发生过一次tcp负载长度变化,接收方收到的tcph->seq=10000,假设报文长度为100。假设本次应答报文就是应答了序列号为10000的报文,由于长度边长了10,所以应答序列号为tcph->ack_seq=10110。tcph->ack_seq-other_way->offset_before=10110-0=10110大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=10100。这个值与发送方的正常的期望应答序列号一致,没有问题。

情况3:

 

 

当前发送方向已经发生过一次tcp负载长度变化,接收方收到的tcph->seq=16010(netfilter给增加了10个字节),假设报文长度为100。假设本次应答报文就是应答了序列号为16000的报文,应答序列号为tcph->ack_seq=16110。tcph->ack_seq-other_way->offset_before=16110-0=16110大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=16100。这个值与发送方的正常的期望应答序列号一致,没有问题。

情况4:

 

 

当前发送方向已经发生过两次tcp负载长度变化,假设本次应答序列号为20000的报文,假设报文长度为100。那么接收方收到的报文的序列号为20010(netfilter给增加一个10个字节),由于本次报文的长度在netfilter中减少了5个字节,所以实际接收方收到的报文长度为95个字节,应答序列号为tcph->ack_seq=20105。tcph->ack_seq-other_way->offset_before=20105 - 5=20100大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为5,修改tcph->ack_seq=tcph->ack_seq-5=26105-5=26100。这个值与发送方的正常的期望应答序列号一致,没有问题。

情况5:

 

 

当前发送方向已经发生过两次tcp负载长度变化,接收方收到的tcph->seq=26005(netfilter给增加了5个字节序列号),假设报文长度为100。假设本次应答报文就是应答了序列号为26000的报文,应答序列号为tcph->ack_seq=26105。tcph->ack_seq-other_way->offset_before=26105 - 10=26095大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为5,修改tcph->ack_seq=tcph->ack_seq-5=26105-5=26100。这个值与发送方的正常的期望应答序列号一致,没有问题。

从前面五种情况来说,netfilter不会导致应答序列号超过发送方发送的最后一个字节+1。这是符合我们的预期的。前面没有说明合并应答的情况,实际也是满足要求的。

正常情况下非一对一应答,应答相比于发送有延迟

情况1:

 

 

当前发送方向已经发生过一次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设本次应答接下来1000个字节。那么应答序列号tcph->ack_seq=7000。tcph->ack_seq-other_way->offset_before=7000 - 0=7000小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为0,修改tcph->ack_seq不会发生变化,没有问题。

情况2:

当前发送方向已经发生过一次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了15000了。本次应答接下来5000个字节。那么应答序列号tcph->ack_seq=11000。该应答序列号已经包含了对发生长度变化的10000序列号报文的应答。因为该报文在netfilter中增加了10个字节,所以实际应答应该要比序列号小10个字节,即真实到发送端的应答序列号为11000-10=10090。tcph->ack_seq-other_way->offset_before=11000 - 0=11000大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=10090,与预期相符,没有问题。

情况3:

 

 

当前发送方向已经发生过两次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了25000了。本次应答接下来2000个字节。那么应答序列号tcph->ack_seq=8000。该应答序列号没有包含了对发生长度变化的10000序列号报文的应答,所以实际应答应该要比序列号不变,即真实到发送端的应答序列号为8000。tcph->ack_seq-other_way->offset_before=8000- 5=7095小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为10,修改tcph->ack_seq=tcph->ack_seq-10=7095,与预期不相符,有点问题,发生了报文的部分应答问题

情况4:

 

 

当前发送方向已经发生过两次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了25000了。本次应答接下来5000个字节。那么应答序列号tcph->ack_seq=11000。该应答序列号已经包含了对发生长度变化的10000序列号报文的应答。因为该报文在netfilter中增加了10个字节,所以实际应答应该要比序列号小10个字节,即真实到发送到的应答序列号为11000-10=10090。tcph->ack_seq-other_way->offset_before=11000 - 0=11000小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为10,修改tcph->ack_seq=tcph->ack_seq-10=10090,与预期相符,没有问题。

迷路的应答报文

迷路应答报文与延迟应答类似,只不过发送方收到应答报文时,对应的字节已经被应答过了,属于重复应答。

 

 

 

 sack选项的序列号调整

kind=4是选择性确认(Selective Acknowledgment,SACK)选项。
TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,
它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。我们可以通过修改
/proc/sys/net/ipv4/tcp_sack内核变量来启用或关闭选择性确认选项。 kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。
其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。
因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。

 

sack序列号调整与应答序列号调整类似:

/* Adjust one found SACK option including checksum correction */
static void nf_ct_sack_block_adjust(struct sk_buff *skb,
                    struct tcphdr *tcph,
                    unsigned int sackoff,
                    unsigned int sackend,
                    struct nf_ct_seqadj *seq)
{
    while (sackoff < sackend) {
        struct tcp_sack_block_wire *sack;
        __be32 new_start_seq, new_end_seq;

        sack = (void *)skb->data + sackoff;//获取sack块
        /* 起始序列号 */
        if (after(ntohl(sack->start_seq) - seq->offset_before,
              seq->correction_pos))
            new_start_seq = htonl(ntohl(sack->start_seq) -
                    seq->offset_after);
        else
            new_start_seq = htonl(ntohl(sack->start_seq) -
                    seq->offset_before);
        //结束序列号
        if (after(ntohl(sack->end_seq) - seq->offset_before,
              seq->correction_pos))
            new_end_seq = htonl(ntohl(sack->end_seq) -
                      seq->offset_after);
        else
            new_end_seq = htonl(ntohl(sack->end_seq) -
                      seq->offset_before);

        pr_debug("sack_adjust: start_seq: %u->%u, end_seq: %u->%u\n",
             ntohl(sack->start_seq), ntohl(new_start_seq),
             ntohl(sack->end_seq), ntohl(new_end_seq));

        inet_proto_csum_replace4(&tcph->check, skb,
                     sack->start_seq, new_start_seq, false);
        inet_proto_csum_replace4(&tcph->check, skb,
                     sack->end_seq, new_end_seq, false);
         /* 修改 */
        sack->start_seq = new_start_seq;
        sack->end_seq = new_end_seq;
        /* 调到下一个sack选项 */
        sackoff += sizeof(*sack);
    }
}

 

posted @ 2023-03-14 22:30  codestacklinuxer  阅读(98)  评论(0编辑  收藏  举报