当前位置:网站首页>TCP协议之《Out-Of-Window报文限速》

TCP协议之《Out-Of-Window报文限速》

2022-08-10 03:26:00 程序员扫地僧

当TCP接收到数据报文的序列号不在窗口之内,或者确认序列号不在窗口内,又或者PAWS(Protection Against Wrapped Sequence numbers)检查未通过,TCP将会使用正确的序列号和确认序号回复一个ACK报文,以纠正对端的序列号错误问题。如果对端一直发送以上三种类型的数据包(无论是由于错误或者恶意),将造成本端一直回复ACK报文,类似一种ACK的DoS攻击。 

PROC文件系统的/proc/sys/net/ipv4/tcp_invalid_ratelimit针对此情况,控制发送ACK确认报文的最大速率。其值为零表示禁用速率控制;默认情况下其值为500,单位时毫秒,即控制两次发送重复确认报文的间隔在500毫秒。

一、初始化
如函数tcp_sk_init所示,在内核初始化过程中,将命名空间的sysctl_tcp_invalid_ratelimit值设置为HZ的一半,HZ的值可在内核配置时指定,例如HZ为1000时,速率限制为500。static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_invalid_ratelimit = HZ/2;
}
 
$ cat /proc/sys/net/ipv4/tcp_invalid_ratelimit
500

二、检查判断
基础检查函数为__tcp_oow_rate_limited,如下。变量last_oow_ack_time实际上为TCP套接口结构的成员。函数逻辑比较简单,如果当前时间减去上一次的时间last_oow_ack_time小于限定值sysctl_tcp_invalid_ratelimit,表明对端发送的Out-Of-Window的ACK报文过快,返回真。static bool __tcp_oow_rate_limited(struct net *net, int mib_idx, u32 *last_oow_ack_time)
{
    if (*last_oow_ack_time) {
        s32 elapsed = (s32)(tcp_jiffies32 - *last_oow_ack_time);
 
        if (0 <= elapsed && elapsed < net->ipv4.sysctl_tcp_invalid_ratelimit) {
            NET_INC_STATS(net, mib_idx);
            return true;    /* rate-limited: don't send yet! */
        }
    }
    *last_oow_ack_time = tcp_jiffies32;
    return false;   /* not rate-limited: go ahead, send dupack now! */
}

三、OOW限速

tcp_oow_rate_limited封装了以上的检查函数,对于未设置SYN标志的数据报文不做OOW限速。仅对远端发送的不在窗口内的ACK或者SYN报文做限速处理,如果超出速率限制,本端将不再回应重复的ACK或者SYN+ACK报文。

bool tcp_oow_rate_limited(struct net *net, const struct sk_buff *skb, int mib_idx, u32 *last_oow_ack_time)
{
    /* Data packets without SYNs are not likely part of an ACK loop. */
    if ((TCP_SKB_CB(skb)->seq != TCP_SKB_CB(skb)->end_seq) && !tcp_hdr(skb)->syn)
        return false;
 
    return __tcp_oow_rate_limited(net, mib_idx, last_oow_ack_time);
}

速率判断的检查点在函数tcp_validate_incoming函数中。如果PAWS检查没有通过,tcp_paws_discard函数返回真,并且此报文的TCP头部没有设置reset标志,调用tcp_oow_rate_limited进行速率检查,如果未超限,回复ACK报文,否则,丢弃。

如果报文在(或者部分数据)接收窗口内,tcp_sequence函数将返回真,否则,其为Out-Of-Window的报文。检查如果未设置reset标志,也未设置SYN标志,检查其速率是否超限,处理与上相同。如果为SYN报文的话,将跳转到syn_challenge处理,稍后下节介绍。static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, int syn_inerr)
{
    /* RFC1323: H1. Apply PAWS check first. */
    if (tcp_fast_parse_options(sock_net(sk), skb, th, tp) && tp->rx_opt.saw_tstamp && tcp_paws_discard(sk, skb)) {
        if (!th->rst) {
            if (!tcp_oow_rate_limited(sock_net(sk), skb, LINUX_MIB_TCPACKSKIPPEDPAWS, &tp->last_oow_ack_time))
                tcp_send_dupack(sk, skb);
            goto discard;
        }
    }
 
    /* Step 1: check sequence number */
    if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
        if (!th->rst) {
            if (th->syn)
                goto syn_challenge;
            if (!tcp_oow_rate_limited(sock_net(sk), skb, LINUX_MIB_TCPACKSKIPPEDSEQ, &tp->last_oow_ack_time))
                tcp_send_dupack(sk, skb);
        }
        goto discard;
    }
}

乱序ACK判断函数tcp_disordered_ack如下。

1)首先判断报文设置了TCP头部的ACK标志位;其次报文的开始序号等于结束序号(ACK不占用序号);再者序号等于套接口下一个要接收的序号。

2)确认序号等于套接口待确认序号,表明其确认的是带确认序号的前一个序号的数据,其为重复ACK。

3)发送窗口不更新。

4)时间戳的差值小于等于近似RTO的值。

static int tcp_disordered_ack(const struct sock *sk, const struct sk_buff *skb)
{
    u32 seq = TCP_SKB_CB(skb)->seq;
    u32 ack = TCP_SKB_CB(skb)->ack_seq;
 
    return (/* 1. Pure ACK with correct sequence number. */
        (th->ack && seq == TCP_SKB_CB(skb)->end_seq && seq == tp->rcv_nxt) &&
        /* 2. ... and duplicate ACK. */
        ack == tp->snd_una &&
        /* 3. ... and does not update window. */
        !tcp_may_update_window(tp, ack, seq, ntohs(th->window) << tp->rx_opt.snd_wscale) &&
        /* 4. ... and sits in replay window. */
        (s32)(tp->rx_opt.ts_recent - tp->rx_opt.rcv_tsval) <= (inet_csk(sk)->icsk_rto * 1024) / HZ);
}
static inline bool tcp_paws_discard(const struct sock *sk, const struct sk_buff *skb)
{
    return !tcp_paws_check(&tp->rx_opt, TCP_PAWS_WINDOW) && !tcp_disordered_ack(sk, skb);
}

合法序列号判断函数tcp_sequence如下。当前报文的结束序号不能在上次接收的报文的结束序号(rcv_wup)之前,并且报文的开始序号不能在窗口右侧(下一个要接收的序号rcv_nxt加上接收窗口大小)。

static inline bool tcp_sequence(const 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));
}

四、挑战ACK限速

函数__tcp_oow_rate_limited的另外一个封装函数tcp_send_challenge_ack。

static void tcp_send_challenge_ack(struct sock *sk, const struct sk_buff *skb)
{
    /* First check our per-socket dupack rate limit. */
    if (__tcp_oow_rate_limited(net, LINUX_MIB_TCPACKSKIPPEDCHALLENGE, &tp->last_oow_ack_time))
        return;
}
如果接收到的ACK报文,其确认序号在套接口当前未确认序号之前,表明其在确认已经被确认过的发送数据。并且如果此ACK报文的确认序号位于当前未确认序号减去最大的发送窗口的结果之前,内核认为此ACK为非正常报文,即很可能并不是对端系统发出的,借此判断当前存在盲数据注入攻击Blind Data Injection Attack。如果没有禁止发送挑战ACK(未设置FLAG_NO_CHALLENGE_ACK),内核将调用tcp_send_challenge_ack。

如果此ACK报文的确认序号与当前的待确认序号相差不到一个发送窗口的距离,内核认为是一个正常的重复ACK报文。

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    u32 prior_snd_una = tp->snd_una;
 
    /* If the ack is older than previous acks then we can probably ignore it. */
    if (before(ack, prior_snd_una)) {
        /* RFC 5961 5.2 [Blind Data Injection Attack].[Mitigation] */
        if (before(ack, prior_snd_una - tp->max_window)) {
            if (!(flag & FLAG_NO_CHALLENGE_ACK))
                tcp_send_challenge_ack(sk, skb);
            return -1;
        }
        goto old_ack;
    }
    
old_ack:
    SOCK_DEBUG(sk, "Ack %u before %u:%u\n", ack, tp->snd_una, tp->snd_nxt);
    return 0;
}

参见如下的TCP服务端状态机tcp_rcv_state_process函数。如果以上的tcp_ack函数返回0,并且当前套接口的状态不是连接建立初期的TCP_SYN_RECV状态,依然焦勇函数tcp_send_challenge_ack处理,其中进行限速判断。注意,在调用tcp_ack时,传入了FLAG_NO_CHALLENGE_ACK标志,所以无论如何,tcp_ack内部都不会调用函数tcp_send_challenge_ack了。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    if (!th->ack && !th->rst && !th->syn)
        goto discard;,
    if (!tcp_validate_incoming(sk, skb, th, 0))
        return 0;
 
    /* step 5: check the ACK field */
    acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT | FLAG_NO_CHALLENGE_ACK) > 0;
    if (!acceptable) {
        if (sk->sk_state == TCP_SYN_RECV)
            return 1;   /* send one RST */
        tcp_send_challenge_ack(sk, skb);
        goto discard;
    }
}

以上已经提到tcp_validate_incoming函数。此处对于TCP头部标志设置了reset位的报文,如果此报文为乱序报文,内核将回复挑战ACK。由于函数tcp_validate_incoming不会在套接口的状态位于TCP_SYN_SENT或者TCP_LISTEN等时调用,所以如果在函数中判断报文设置了SYN标志,内核认为发生了错误,回复挑战ACK报文。

static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, int syn_inerr)
{
    /* Step 2: check RST bit */
    if (th->rst) {
        if (rst_seq_match)
            tcp_reset(sk);
        else {
            /* Disable TFO if RST is out-of-order and no data has been received for current active TFO socket */
            if (tp->syn_fastopen && !tp->data_segs_in && sk->sk_state == TCP_ESTABLISHED)
                tcp_fastopen_active_disable(sk);
            tcp_send_challenge_ack(sk, skb);
        }
        goto discard;
    }
    /* step 4: Check for a SYN. RFC 5961 4.2 : Send a challenge ack */
    if (th->syn) {
syn_challenge:
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSYNCHALLENGE);
        tcp_send_challenge_ack(sk, skb);
        goto discard;
    }
 

原网站

版权声明
本文为[程序员扫地僧]所创,转载请带上原文链接,感谢
https://blog.csdn.net/wuyongmao/article/details/126246459