TCP三次握手超时处理

发布于:2023-01-22 ⋅ 阅读:(410) ⋅ 点赞:(0)

以TCP服务端为例,在接收到客户端的第一个SYN报文之后,负责处理的tcp_conn_request函数,判断如果不使用syn cookie处理机制,将正常相应SYN+ACK报文,并且在此之前,启用TCP的定时器,负责SYN+ACK的超时重传。

int tcp_conn_request(struct request_sock_ops *rsk_ops, ...)
{
    if (fastopen_sk) {
    } else {
        tcp_rsk(req)->tfo_listener = false;
        if (!want_cookie)
            inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));
        af_ops->send_synack(sk, dst, &fl, req, &foc, !want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
    }
}
超时时间由函数tcp_timeout_init计算得到。默认情况下设置为宏TCP_TIMEOUT_INIT的值,1秒钟(1*HZ),即RFC6298规定的初始RTO值(重传超时时间)。

#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value    */
定时器的超时处理函数为reqsk_timer_handler,启动之前,将重传次数和超时次数清零。

static void reqsk_queue_hash_req(struct request_sock *req, unsigned long timeout)
{
    req->num_retrans = 0;
    req->num_timeout = 0;
    req->sk = NULL;
    
    timer_setup(&req->rsk_timer, reqsk_timer_handler, TIMER_PINNED);
    mod_timer(&req->rsk_timer, jiffies + timeout);
}
SYN+ACK重传次数默认初始化为宏TCP_SYNACK_RETRIES(5),可通过proc文件修改此值。按照RTO为1秒计算,重传5此需要耗时31秒钟,最后一次超时经发生在63秒之后。

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_synack_retries = TCP_SYNACK_RETRIES;
}
 
$ cat /proc/sys/net/ipv4/tcp_synack_retries
5

另外,用户层可通过setsockopt系统调用指定重传次数。最大可设定的重传次数由宏MAX_TCP_SYNCNT定义,为127次。

static int do_tcp_setsockopt(struct sock *sk, int level, int optname, ...)

    switch (optname) {
    case TCP_SYNCNT:
        if (val < 1 || val > MAX_TCP_SYNCNT)
            err = -EINVAL;
        else
            icsk->icsk_syn_retries = val;
        break;
    }
}
一旦超时发生,执行reqsk_timer_handler函数。出现超时情况可能会是由于服务端发送SYN+ACK响应报文丢失;客户端回复的ACK报文丢失;RTT(Round Trip Timeout)时间太长超过定时器的1秒钟;又或者是客户端根本就没有进行相应(如SYN泛洪攻击)。
 
在进行处理之前,需要确定重传次数的阈值。默认情况下,如果用户未通过setsockopt设置icsk_syn_retries次数,重传次数阈值就等于sysctl_tcp_synack_retries的值(默认为5次)。如果当前的请求套接口队列的长度小于规定的最大长度的一半(最大长度取值为8与sk_listener->sk_max_ack_backlog两者之中的较大值),重传次数阈值不变。反之,就要对thresh值再做处理,当然要保证thresh的值大于2(至少重传一次),如果队列中还在等待客户端的第三个ACK报文并且没有超时的请求套接口(young状态)的数量大于当前队列长度的一半,说明队列尚在健康状态,不对thresh做修改,否则,递减thresh的值,直到达到上述的健康状态。队列中不在young状态的请求套接口,意指已经重传过SYN+ACK的套接口。

static void reqsk_timer_handler(struct timer_list *t)
{
    max_retries = icsk->icsk_syn_retries ? : net->ipv4.sysctl_tcp_synack_retries;
    thresh = max_retries;
 
    qlen = reqsk_queue_len(queue);
    if ((qlen << 1) > max(8U, sk_listener->sk_max_ack_backlog)) {
        int young = reqsk_queue_len_young(queue) << 1;
 
        while (thresh > 2) {
            if (qlen < young)
                break;
            thresh--;
            young <<= 1;
        }
    }
    syn_ack_recalc(req, thresh, max_retries, defer_accept, &expire, &resend);
    req->rsk_ops->syn_ack_timeout(req);
}

函数syn_ack_recalc设置两个参数,分别是总的超时时间是否已超过(expire)和是否重传SYN+ACK数据包(resend)。不考虑延时accept的情况下,实现逻辑比较简单,超时次数已经大于限定的阈值,说明已经超时,需要销毁此请求套接口;另外,设置resend变量,重传SYN+ACK数据包。

static inline void syn_ack_recalc(struct request_sock *req, const int thresh,
                  const int max_retries, const u8 rskq_defer_accept, int *expire, int *resend)
{
    if (!rskq_defer_accept) {
        *expire = req->num_timeout >= thresh;
        *resend = 1;
        return;
    }

另外,超时处理函数会调用特定的面向连接的套接口的超时函数,对于TCP来说,其为tcp_syn_ack_timeout,其并没有实质性的处理逻辑,仅完成增加MIB的统计计数。对于DCCP而言,其为dccp_syn_ack_timeout函数。

struct request_sock_ops tcp_request_sock_ops __read_mostly = {
    .family     =   PF_INET,
    .rtx_syn_ack    =   tcp_rtx_synack,
    .syn_ack_timeout =  tcp_syn_ack_timeout,
};
void tcp_syn_ack_timeout(const struct request_sock *req)
{   
    struct net *net = read_pnet(&inet_rsk(req)->ireq_net);
    __NET_INC_STATS(net, LINUX_MIB_TCPTIMEOUTS);
}
接下来超时处理函数根据之前计算的条件变量,决定是否进行重传操作。注意此处,在重传之后要更新重传计数,如果是第一次重传,减少young的计数,表明队列中一个套接口不再处于young状态。

static void reqsk_timer_handler(struct timer_list *t)
{
    if (!expire &&
        (!resend ||
         !inet_rtx_syn_ack(sk_listener, req) ||
         inet_rsk(req)->acked)) {
        unsigned long timeo;
 
        if (req->num_timeout++ == 0)
            atomic_dec(&queue->young);
        timeo = min(TCP_TIMEOUT_INIT << req->num_timeout, TCP_RTO_MAX);
        mod_timer(&req->rsk_timer, jiffies + timeo);
        return;
    }
}

具体由tcp_rtx_synack函数完成TCP的SYN+ACK报文的重传。成功之后,增加重传次数的计数值(num_retrans)。

int inet_rtx_syn_ack(const struct sock *parent, struct request_sock *req)
{
    int err = req->rsk_ops->rtx_syn_ack(parent, req);
    if (!err)
        req->num_retrans++;
}
下次超时时间的计算,为初始时间TCP_TIMEOUT_INIT(1秒钟),左移num_timeout位,最大超时时间为TCP_RTO_MAX(120秒),可见超时时间以2的指数级增长。

    timeo = min(TCP_TIMEOUT_INIT << req->num_timeout, TCP_RTO_MAX);

一、RTT的更新
在接收到客户端的SYN报文后,记录下当前的时间。

static void tcp_openreq_init(struct request_sock *req, ...)
{
    struct inet_request_sock *ireq = inet_rsk(req);
 
    tcp_rsk(req)->snt_synack = tcp_clock_us();
}
在接收到TCP三次握手的第三个ACK报文之后,计算网络的RTT值。如果中间有重传发生(num_retrans不为零),不计算RTT值。否则使用当前时间由函数tcp_clock_us得到的值减去snt_sysack之前记录的值,结果为RTT,单位为微秒。

void tcp_synack_rtt_meas(struct sock *sk, struct request_sock *req)
{      
    struct rate_sample rs;
    long rtt_us = -1L;
   
    if (req && !req->num_retrans && tcp_rsk(req)->snt_synack)
        rtt_us = tcp_stamp_us_delta(tcp_clock_us(), tcp_rsk(req)->snt_synack);
 
    tcp_ack_update_rtt(sk, FLAG_SYN_ACKED, rtt_us, -1L, rtt_us, &rs);

二、延时ACCEPT功能
用户层可通过setsockopt设置延时accept功能。

static int do_tcp_setsockopt(struct sock *sk, int level,
        int optname, char __user *optval, unsigned int optlen)
{
    switch (optname) {
    case TCP_DEFER_ACCEPT:
        /* Translate value in seconds to number of retransmits */
        icsk->icsk_accept_queue.rskq_defer_accept = secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,  TCP_RTO_MAX / HZ);
        break;
    }
}
如果开启此功能,处理逻辑位于函数tcp_check_req中。如果仅仅是接收到客户端回复的第三个握手ACK报文,无数据,不进行处理,设置acked为1。反之如果接收到数据和ACK,进行正常处理,忽略延时accept功能。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    /* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
    if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
        TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
        inet_rsk(req)->acked = 1;
        return NULL;
    }
}
这将导致触发SYN+ACK报文的重传处理函数,defer_accept与max_retries设为了相同的值。

static void reqsk_timer_handler(struct timer_list *t)
{
    defer_accept = READ_ONCE(queue->rskq_defer_accept);
    if (defer_accept)
        max_retries = defer_accept;
    syn_ack_recalc(req, thresh, max_retries, defer_accept, &expire, &resend);
}
其中,expire和resend的计算逻辑变化如下,如果当前的超时次数大于阈值thresh,并且大于最大重传次数(即延时accept的次数),判定为超时;或者acked等于0(即未接收到单独的ACK报文)也判定为超时,其它情况下判定未超时。

对于重传resend,如果未接收到单独的ACK报文或者是已到延时accept的最后,设定resend为真,进行SYN+ACK的重传,保证还能接收到客户端的ACK或者数据,可将此请求套接口转换到established状态。

static inline void syn_ack_recalc(struct request_sock *req, const int thresh,
                  const int max_retries, const u8 rskq_defer_accept, int *expire, int *resend)
{
    *expire = req->num_timeout >= thresh &&
          (!inet_rsk(req)->acked || req->num_timeout >= max_retries);
    
    *resend = !inet_rsk(req)->acked || req->num_timeout >= rskq_defer_accept - 1;
}

三、超时定时器清除
在正常接收到客户端的ACK报文之后,在tcp_check_req函数中,调用inet_csk_complete_hashdance函数,最终由reqsk_queue_unlink删除SYN+ACK超时定时器。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    return inet_csk_complete_hashdance(sk, child, req, own_req);
}
struct sock *inet_csk_complete_hashdance(struct sock *sk, struct sock *child,
                     struct request_sock *req, bool own_req)
{
    if (own_req) {
        inet_csk_reqsk_queue_drop(sk, req);
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
    }
}
void inet_csk_reqsk_queue_drop(struct sock *sk, struct request_sock *req)
{
    if (reqsk_queue_unlink(&inet_csk(sk)->icsk_accept_queue, req)) {
 
    }
}
static bool reqsk_queue_unlink(struct request_sock_queue *queue, struct request_sock *req)
{
    if (timer_pending(&req->rsk_timer) && del_timer_sync(&req->rsk_timer))
        reqsk_put(req);
}

本文含有隐藏内容,请 开通VIP 后查看