zoukankan      html  css  js  c++  java
  • linux tcp 在timewait 状态下的报文处理

    最近处理一个问题,我们nginx服务器作为透明代理,将核心网过来的用户上网请求代理到我们的cache服务器,如果cache服务器没有命中内容,则需要我们

    作为客户端往源站请求内容,但用户对此一无所知,也就是我们使用透明代理的模式来给用户提供上网服务。

    问题出在:我们作为客户端,往服务器端请求数据。服务器端主动断链之后,我们使用相同的ip和端口去连接服务器端,发现syn 没有得到响应。

    从图中TCP Port numbers reused 开始这行可以看出:

    106.332208  我们服务器在收到源站的主动断链请求

    106.371754  我们服务器发送了针对源站主动fin的ack。

    107.597531 我们服务器收到用户的一个GET 请求,

    107.598388 我们服务器调用close(socket),触发内核发送了fin请求给源站。

    107.605880 我们服务器收到源站返回的针对我们fin的ack,在此,四次挥手结束。那么主动断链的源站,肯定处于time_wait状态。

    107.636754 我们服务器收到用户的一个ack,这个因为我们服务器使用用户的ip和端口跟源站交互,所以ip和端口是一样的,所以只能从Seq,Ack,或者mac地址来区分链路。

    倒数的四个报文:

    109.597985 我们服务器使用新的socket,但是ip和端口跟之前的链路一样,往源站进行connect,触发内核发送syn请求,

    110.600579 我们服务器的第一个syn未收到回复,重发该请求。1s超时

    112.604765 我们服务器退避发送syn请求。2s超时

    116.613191 我们服务器在退避之后,4s超时,达到tcp_syn_retries 设置的2次上限,无奈给用户回复502.

    报文分析完毕,我们在排除丢包的情况下,想想源站为什么会对我们的syn无动于衷。

    下面都是假设源站是linux 3.10下的实现。

    由于源站是主动断链,在回复给我们服务器的fin的ack之后,进入time_wait状态。

    int tcp_v4_rcv(struct sk_buff *skb)
    {
    。。。
        sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
        if (!sk)----这里搜出来的sk,其实是inet_timewait_sock
            goto no_tcp_socket;
    
    process:
        if (sk->sk_state == TCP_TIME_WAIT)----------大状态是time_wait,大状态下又分为两个子状态,如fin_wait2,time_wait
            goto do_time_wait;
    。。。
    do_time_wait:
        if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
            inet_twsk_put(inet_twsk(sk));
            goto discard_it;
        }
    
        if (skb->len < (th->doff << 2)) {
            inet_twsk_put(inet_twsk(sk));
            goto bad_packet;
        }
        if (tcp_checksum_complete(skb)) {
            inet_twsk_put(inet_twsk(sk));
            goto csum_error;
        }
        switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {----返回四种结果
        case TCP_TW_SYN: {----------合理的syn,处理建联请求
            struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),---------查找监听socket
                                &tcp_hashinfo,
                                iph->saddr, th->source,
                                iph->daddr, th->dest,
                                inet_iif(skb));
            if (sk2) {---找到对应listen的socket,则继续处理,注意这个sk已经是listen的sk了。
                inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
                inet_twsk_put(inet_twsk(sk));
                sk = sk2;
                goto process;
            }
            /* Fall through to ACK */---------没找到listen的socket的话,则没有break,会进入下面的TCP_TW_ACK,回复ack并丢弃skb
        }
        case TCP_TW_ACK:---------回ack
            tcp_v4_timewait_ack(sk, skb);
            break;
        case TCP_TW_RST:---------关闭链路
            tcp_v4_send_reset(sk, skb);---发送rst包给对端,
            inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
            inet_twsk_put(inet_twsk(sk));
            goto discard_it;
        case TCP_TW_SUCCESS:;----虽然叫success,但是什么都不做,空语句,最终会走到discrad_it
        }
        goto discard_it;
    }
    }

    为了减少一点内存占用,在tcp_time_wait 函数中,将处于timewait状态的sock 替换为了 inet_timewait_sock 。

    crash> p sizeof(struct tcp_sock)
    $5 = 1968
    crash> p sizeof(struct inet_timewait_sock)
    $6 = 152

    也就是处于time_wait状态的socket比处于正常状态的socket少占用了1.8k内存,对于很多服务器来说,timewait状态下的socket比较多,算起来也很可观了,所以,linux又设计了一个

    tcp_max_tw_buckets 来限制处于time_wait的数量。

    这个也是 tcp_timewait_state_process(inet_twsk(sk), skb, th) 中能够将sock直接转换为 inet_timewait_sock 的原因。

    从流程看,需要分析 tcp_timewait_state_process 的处理:

    enum tcp_tw_status
    tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
                   const struct tcphdr *th)
    {
        struct tcp_options_received tmp_opt;
        struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
        bool paws_reject = false;
    
        tmp_opt.saw_tstamp = 0;
        if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
            tcp_parse_options(skb, &tmp_opt, 0, NULL);
    
            if (tmp_opt.saw_tstamp) {
                tmp_opt.rcv_tsecr    -= tcptw->tw_ts_offset;
                tmp_opt.ts_recent    = tcptw->tw_ts_recent;
                tmp_opt.ts_recent_stamp    = tcptw->tw_ts_recent_stamp;
                paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
            }
        }--------------这个是时间戳的检查,我们自己作为请求方但是没有开启时间戳,所以paws_reject为0,saw_tstamp为0.
    
        if (tw->tw_substate == TCP_FIN_WAIT2) {-----根据挥手流程,处于fin_wait2状态的socket会在收到fin之后迁入time_wait状态,这个是指tw_substate也是time_wait状态
            /* Just repeat all the checks of tcp_rcv_state_process() */
    
            /* Out of window, send ACK */
            if (paws_reject ||--------如注释,超过接收包的tcp窗口。则走oow流程
                !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                       tcptw->tw_rcv_nxt,
                       tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
                return tcp_timewait_check_oow_rate_limit(
                    tw, skb, LINUX_MIB_TCPACKSKIPPEDFINWAIT2);
    
            if (th->rst)---收到rst包,直接kill,但是要注意的是,kill返回的其实是 TCP_TW_SUCCESS,也就是啥都不干。
                goto kill;
    
            if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))---在fin_wait2状态,收到syn,并且seq小于我们需要接收的nxt,则rst掉,认为是过期的syn
                return TCP_TW_RST;
    
            /* Dup ACK? */
            if (!th->ack ||---没有ack标志,则丢弃,说明走到这的肯定都带ack标志,因为就算是fin,ack标志也是设置的。
                !after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||-----有ack标志,但是end_seq在窗口左边,也就是oow,有可能是重复ack,丢弃
                TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {---是纯ack,我们是因为收到fin-ack才进入的fin-wait2,现在又来个纯ack,不是fin,也不是syn,丢弃
                inet_twsk_put(tw);
                return TCP_TW_SUCCESS;
            }
    
            /* New data or FIN. If new data arrive after half-duplex close,
             * reset.
             */
            if (!th->fin ||---不带fin标志,直接rst掉
                TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1)---是fin包,收到的seq有数据,rst掉,看这意思,不能fin带数据。
                return TCP_TW_RST;
    
            /* FIN arrived, enter true time-wait state. */
            tw->tw_substate      = TCP_TIME_WAIT;----------到这的,肯定是有fin标志的,否则前面就返回了,fin-wait2收到fin,迁入time_wait状态,此时子状态也是time_wait了
            tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
            if (tmp_opt.saw_tstamp) {
                tcptw->tw_ts_recent_stamp = get_seconds();
                tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
            }
    
            if (tcp_death_row.sysctl_tw_recycle &&-----开启了tw_recyle的情况下,
                tcptw->tw_ts_recent_stamp &&----------开启了时间戳的情况下下
                tcp_tw_remember_stamp(tw))
                inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,---设置超时为tw_timeout,这个跟链路相关,在tcp_time_wait 中设置为3.5*RTO。
                           TCP_TIMEWAIT_LEN);
            else
                inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,----没有设置时间戳和tw_recyle,则默认的60s,这个值是写死的,尼玛也不让改,只能编译内核
                           TCP_TIMEWAIT_LEN);
            return TCP_TW_ACK;
        }-------------如果子状态是fin-wait2,则在这个里面处理
    
        /*
         *    Now real TIME-WAIT state.---------------------本文syn发送的时候,服务器应该处于这个状态,下面就是服务器收到本syn该执行的代码
         *
         *    RFC 1122:
         *    "When a connection is [...] on TIME-WAIT state [...]
         *    [a TCP] MAY accept a new SYN from the remote TCP to
         *    reopen the connection directly, if it:-----------------在timewait状态下重新open的条件:
         *
         *    (1)  assigns its initial sequence number for the new----初始seq比之前老链路ack的序号大
         *    connection to be larger than the largest sequence
         *    number it used on the previous connection incarnation,
         *    and
         *
         *    (2)  returns to TIME-WAIT state if the SYN turns out
         *    to be an old duplicate".
         */
    
        if (!paws_reject &&------------防回绕校验失败
            (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&-------当前需要和预期的序号相同且纯fin或者纯rst,
             (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {----rst标志被置位
            /* In window segment, it may be only reset or bare ack. */
    
            if (th->rst) {------我们已经处于timewait状态,收到rst,
                /* This is TIME_WAIT assassination, in two flavors.
                 * Oh well... nobody has a sufficient solution to this
                 * protocol bug yet.
                 */
                if (sysctl_tcp_rfc1337 == 0) {
    kill:
                    inet_twsk_deschedule(tw, &tcp_death_row);
                    inet_twsk_put(tw);
                    return TCP_TW_SUCCESS;--------丢弃这个包
                }
            }
            inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                       TCP_TIMEWAIT_LEN);
    
            if (tmp_opt.saw_tstamp) {----有时间戳选项的话,更新时间戳
                tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
                tcptw->tw_ts_recent_stamp = get_seconds();
            }
    
            inet_twsk_put(tw);
            return TCP_TW_SUCCESS;--------丢弃这个包
        }--------------显然,我们的syn不满足这个if
    
        /* Out of window segment.
    
           All the segments are ACKed immediately.
    
           The only exception is new SYN. We accept it, if it is
           not old duplicate and we are not in danger to be killed
           by delayed old duplicates. RFC check is that it has
           newer sequence number works at rates <40Mbit/sec.
           However, if paws works, it is reliable AND even more,
           we even may relax silly seq space cutoff.
    
           RED-PEN: we violate main RFC requirement, if this SYN will appear
           old duplicate (i.e. we receive RST in reply to SYN-ACK),
           we must return socket to time-wait state. It is not good,
           but not fatal yet.
         */
    
        if (th->syn && !th->rst && !th->ack && !paws_reject &&-------我们的syn包不含rst标志,也没有ack标志,但没有开启时间戳选项,所以paws_reject为0.满足条件
            (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||-------我们的syn的序号是2972897916,而老的链路的tw_rcv_nxt为2674663925,满足条件,按道理就&&条件满足
             (tmp_opt.saw_tstamp &&----------------------------------有时间戳选项的话
              (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {---且时间戳条件满足
            u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
            if (isn == 0)
                isn++;
            TCP_SKB_CB(skb)->tcp_tw_isn = isn;
            return TCP_TW_SYN;
        }
    
        if (paws_reject)
            NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED);
    
        if (!th->rst) {-----其他情况处理,如不是有效的syn,比如序列号在window之前,ack包,但oow,
            /* In this case we must reset the TIMEWAIT timer.
             *
             * If it is ACKless SYN it may be both old duplicate
             * and new good SYN with random sequence number <rcv_nxt.
             * Do not reschedule in the last case.
             */
            if (paws_reject || th->ack)
                inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN);
    
            return tcp_timewait_check_oow_rate_limit(
                tw, skb, LINUX_MIB_TCPACKSKIPPEDTIMEWAIT);
        }
        inet_twsk_put(tw);
        return TCP_TW_SUCCESS;
    }

     为了防止回绕,一般我们通过开启 /proc/sys/net/ipv4/tcp_timestamps 来防止回绕,也就是PAWS(Protect Against Wrapped Sequence numbers) 。

    在本案例中,我们发送的syn,按道理是符合条件的,对方为啥一点反应都没有呢?为了弄清楚这个问题,我们发了一堆命令给源站,源站表示看不懂,

    后来才知道,因为他们是windows系统来提供网站服务的,因此不能继续分析了。当然也不是没有任何收获,毕竟对于大多数linux服务器的实现流程更

    清楚了,从代码看,如果是linux服务器,就算没有建联成功,好歹会回复一个ack,而不是像目前这样啥都不回,导致请求端重传并超时。

    状态问题:

    tcp        0      0 10.47.242.207:8000      10.47.242.118:7000      FIN_WAIT2   7344/tcp_server.o
    
    12: CFF22F0A:1F40 76F22F0A:1B58 05 00000000:00000000 00:00000000 00000000     0        0 5271486 1 ffff940408230f80 20 4 30 10 -1 

    根据/proc/net/tcp中的显示,当状态5,也就是 TCP_FIN_WAIT2,因为:

    static int tcp4_seq_show(struct seq_file *seq, void *v)
    {
    ...
    switch (st->state) {
        case TCP_SEQ_STATE_LISTENING:
        case TCP_SEQ_STATE_ESTABLISHED:
            if (sk->sk_state == TCP_TIME_WAIT)------------当状态为time-wait的时候,会显示子状态
                get_timewait4_sock(v, seq, st->num, &len);
            else
                get_tcp4_sock(v, seq, st->num, &len);
            break;
        case TCP_SEQ_STATE_OPENREQ:
            get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
            break;
        }
    ...}

    对参数理解的收获:

    net.ipv4.tcp_tw_recycle = 0 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭,开启时,回收的时间为3.5*RTO。

    net.ipv4.tcp_fin_timeout = 60 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间,如果发送fin的一端是使用shutdown方式来关闭写的一端,

    则这个状态可能会维持很长很长,而不是这个60s。

    我通过写的简单的tcp的一个简单例子来模拟源站,发现了只要没有将服务器端缓冲区的数据recv干净,调用close的话,会发rst,recv干净之后再调用close的话,会发fin。

    void tcp_close(struct sock *sk, long timeout)
    {
    。。。
    else if (data_was_unread) {
            /* Unread data was tossed, zap the connection. */
            NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
            tcp_set_state(sk, TCP_CLOSE);
            tcp_send_active_reset(sk, sk->sk_allocation);
    }
    。。。。
    else if (tcp_close_state(sk)) {
    tcp_send_fin(sk);
    }
    }

    Q:开启了tw_recycle,也就是快速回收,那么回收的速度是多快呢?

    tcp_time_wait函数中,该值为3.5倍的RTO。
    void tcp_time_wait(struct sock *sk, int state, int timeo)
    {
    。。。
        if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
            recycle_ok = tcp_remember_stamp(sk);
    
        if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
            tw = inet_twsk_alloc(sk, state);
    
        if (tw != NULL) {
            struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
            const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);//3.5*rto
    。。。
        if (recycle_ok) {//开启tw 快速回收,则超时时间很短
                tw->tw_timeout = rto;//这个rto其实是3.5倍的rtt
            } else {
                tw->tw_timeout = TCP_TIMEWAIT_LEN;
                if (state == TCP_TIME_WAIT)
                    timeo = TCP_TIMEWAIT_LEN;
            }
    。。。
    }

    也就是说,开启recycle,则回收tw的socket时间为3.5倍的rto。

    Q.timewait定时器到期后,怎么释放这些tw的资源

    inet_twdr_hangman 函数负责干这事。具体可以在设置timer的时候看到,tcp_death_row 是处理所有tw状态的一个结构,包括设置定时器,锁,清理tw等。它分为快慢的两种timer,一种是正常处理2MSL的timer,一种是快速回收的tw的timer。具体可以查看 inet_twsk_schedule ,两种timer分别调用inet_twdr_hangman,inet_twdr_twcal_tick最终调用的都是 __inet_twsk_kill来回收资源。

     
  • 相关阅读:
    Java环境变量的简记
    win10系统安装postgresql后无法连接
    VB.net Postgresql调用,NpgsqlDataReader,NpgsqlDataAdapter的调用例子。
    Postgresql服务器无法连接(10060错误信息)
    解决visual studio的No EditorOptionDefinition 错误
    从零开始Wordpress建站经验分享,内附耗时及成本
    Servlet端 接收不到4096,8192长度的JSON参数
    Wiser的Junit测试用法
    Oracle表生成JavaBean
    mysql表生成JavaBean
  • 原文地址:https://www.cnblogs.com/10087622blog/p/11017455.html
Copyright © 2011-2022 走看看