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來回收資源。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM