在 TIME_WAIT 狀態的 TCP 連線,收到 SYN 後會發生什麼?

小林coding發表於2022-03-02

週末跟朋友討論了一些 TCP 的問題,在查閱《Linux 伺服器高效能程式設計》這本書的時候,發現書上寫了這麼一句話:

圖片

書上說,處於 TIME_WAIT 狀態的連線,在收到相同四元組的 SYN 後,會回 RST 報文,對方收到後就會斷開連線。

書中作者只是提了這麼一句話,沒有給予原始碼或者抓包圖的證據。

起初,我看到也覺得這個邏輯也挺符合常理的,但是當我自己去啃了 TCP 原始碼後,發現並不是這樣的。

所以,今天就來討論下這個問題,「在 TCP 正常揮手過程中,處於 TIME_WAIT 狀態的連線,收到相同四元組的 SYN 後會發生什麼?

問題現象如下圖,左邊是服務端,右邊是客戶端:

圖片

先說結論

在跟大家分析 TCP 原始碼前,我先跟大家直接說下結論。

針對這個問題,關鍵是要看 SYN 的「序列號和時間戳」是否合法,因為處於 TIME_WAIT 狀態的連線收到 SYN 後,會判斷 SYN 的「序列號和時間戳」是否合法,然後根據判斷結果的不同做不同的處理。

先跟大家說明下, 什麼是「合法」的 SYN?

  • 合法 SYN:客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要並且 SYN 的「時間戳」比服務端「最後收到的報文的時間戳」要
  • 非法 SYN:客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要或者 SYN 的「時間戳」比服務端「最後收到的報文的時間戳」要

上面 SYN 合法判斷是基於雙方都開啟了 TCP 時間戳機制的場景,如果雙方都沒有開啟 TCP 時間戳機制,則 SYN 合法判斷如下:

  • 合法 SYN:客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要
  • 非法 SYN:客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要

收到合法 SYN

如果處於 TIME_WAIT 狀態的連線收到「合法的 SYN 」後,就會重用此四元組連線,跳過 2MSL 而轉變為 SYN_RECV 狀態,接著就能進行建立連線過程

用下圖作為例子,雙方都啟用了 TCP 時間戳機制,TSval 是傳送報文時的時間戳:

圖片

上圖中,在收到第三次揮手的 FIN 報文時,會記錄該報文的 TSval (21),用 ts_recent 變數儲存。然後會計算下一次期望收到的序列號,本次例子下一次期望收到的序列號就是 301,用 rcv_nxt 變數儲存。

處於 TIME_WAIT 狀態的連線收到 SYN 後,因為 SYN 的 seq(400) 大於 rcv_nxt(301),並且 SYN 的 TSval(30) 大於 ts_recent(21),所以是一個「合法的 SYN」,於是就會重用此四元組連線,跳過 2MSL 而轉變為 SYN_RECV 狀態,接著就能進行建立連線過程。

收到非法的 SYN

如果處於 TIME_WAIT 狀態的連線收到「非法的 SYN 」後,就會再回復一個第四次揮手的 ACK 報文,客戶端收到後,發現並不是自己期望收到確認號(ack num),就回 RST 報文給服務端

用下圖作為例子,雙方都啟用了 TCP 時間戳機制,TSval 是傳送報文時的時間戳:

圖片

上圖中,在收到第三次揮手的 FIN 報文時,會記錄該報文的 TSval (21),用 ts_recent 變數儲存。然後會計算下一次期望收到的序列號,本次例子下一次期望收到的序列號就是 301,用 rcv_nxt 變數儲存。

處於 TIME_WAIT 狀態的連線收到 SYN 後,因為 SYN 的 seq(200) 小於 rcv_nxt(301),所以是一個「非法的 SYN」,就會再回復一個與第四次揮手一樣的 ACK 報文,客戶端收到後,發現並不是自己期望收到確認號,就回 RST 報文給服務端

客戶端等待一段時間還是沒收到 SYN + ACK 後,就會超時重傳 SYN 報文,重傳次數達到最大值後,就會斷開連線。

PS:這裡先埋一個疑問,處於 TIME_WAIT 狀態的連線,收到 RST 會斷開連線嗎?

原始碼分析

下面原始碼分析是基於 Linux 4.2 版本的核心程式碼。

Linux 核心在收到 TCP 報文後,會執行 tcp_v4_rcv 函式,在該函式和 TIME_WAIT 狀態相關的主要程式碼如下:

int tcp_v4_rcv(struct sk_buff *skb)
{
  struct sock *sk;
 ...
  //收到報文後,會呼叫此函式,查詢對應的 sock
 sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
          th->dest, sdif, &refcounted);
 if (!sk)
  goto no_tcp_socket;

process:
  //如果連線的狀態為 time_wait,會跳轉到 do_time_wait
 if (sk->sk_state == TCP_TIME_WAIT)
  goto do_time_wait;

...

do_time_wait:
  ...
  //由tcp_timewait_state_process函式處理在 time_wait 狀態收到的報文
 switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
    // 如果是TCP_TW_SYN,那麼允許此 SYN 重建連線
    // 即允許TIM_WAIT狀態躍遷到SYN_RECV
    case TCP_TW_SYN: {
      struct sock *sk2 = inet_lookup_listener(....);
      if (sk2) {
          ....
          goto process;
      }
    }
    // 如果是TCP_TW_ACK,那麼,返回記憶中的ACK
    case TCP_TW_ACK:
      tcp_v4_timewait_ack(sk, skb);
      break;
    // 如果是TCP_TW_RST直接傳送RESET包
    case TCP_TW_RST:
      tcp_v4_send_reset(sk, skb);
      inet_twsk_deschedule_put(inet_twsk(sk));
      goto discard_it;
     // 如果是TCP_TW_SUCCESS則直接丟棄此包,不做任何響應
    case TCP_TW_SUCCESS:;
 }
 goto discard_it;
}

該程式碼的過程:

  1. 接收到報文後,會呼叫 __inet_lookup_skb() 函式查詢對應的 sock 結構;
  2. 如果連線的狀態是 TIME_WAIT,會跳轉到 do_time_wait 處理;
  3. tcp_timewait_state_process() 函式來處理收到的報文,處理後根據返回值來做相應的處理。

先跟大家說下,如果收到的 SYN 是合法的,tcp_timewait_state_process() 函式就會返回 TCP_TW_SYN,然後重用此連線。如果收到的 SYN 是非法的,tcp_timewait_state_process() 函式就會返回 TCP_TW_ACK,然後會回上次發過的 ACK。

接下來,看 tcp_timewait_state_process() 函式是如何判斷 SYN 包的。

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
      const struct tcphdr *th)

{
 ...
  //paws_reject 為 false,表示沒有發生時間戳迴繞
  //paws_reject 為 true,表示發生了時間戳迴繞
 bool paws_reject = false;

 tmp_opt.saw_tstamp = 0;
  //TCP頭中有選項且舊連線開啟了時間戳選項
 if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) { 
  //解析選項
    tcp_parse_options(twsk_net(tw), skb, &tmp_opt, 0NULL);

  if (tmp_opt.saw_tstamp) {
   ...
      //檢查收到的報文的時間戳是否發生了時間戳迴繞
   paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
  }
 }

....

  //是SYN包、沒有RST、沒有ACK、時間戳沒有迴繞,並且序列號也沒有迴繞,
 if (th->syn && !th->rst && !th->ack && !paws_reject &&
     (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
      (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; //允許重用TIME_WAIT四元組重新建立連線
 }


 if (!th->rst) {
    // 如果時間戳迴繞,或者報文裡包含ack,則將 TIMEWAIT 狀態的持續時間重新延長
  if (paws_reject || th->ack)
    inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
        TCP_TIMEWAIT_LEN);

     // 返回TCP_TW_ACK, 傳送上一次的 ACK
    return TCP_TW_ACK;
 }
 inet_twsk_put(tw);
 return TCP_TW_SUCCESS;
}

如果雙方啟用了 TCP 時間戳機制,就會通過 tcp_paws_reject() 函式來判斷時間戳是否發生了迴繞,也就是「當前收到的報文的時間戳」是否大於「上一次收到的報文的時間戳」:

  • 如果大於,就說明沒有發生時間戳繞回,函式返回 false。
  • 如果小於,就說明發生了時間戳迴繞,函式返回 true。

從原始碼可以看到,當收到 SYN 包後,如果該 SYN 包的時間戳沒有發生迴繞,也就是時間戳是遞增的,並且 SYN 包的序列號也沒有發生迴繞,也就是 SYN 的序列號「大於」下一次期望收到的序列號。就會初始化一個序列號,然後返回 TCP_TW_SYN,接著就重用該連線,也就跳過 2MSL 而轉變為 SYN_RECV 狀態,接著就能進行建立連線過程。

如果雙方都沒有啟用 TCP 時間戳機制,就只需要判斷 SYN 包的序列號有沒有發生迴繞,如果 SYN 的序列號大於下一次期望收到的序列號,就可以跳過 2MSL,重用該連線。

如果 SYN 包是非法的,就會返回 TCP_TW_ACK,接著就會傳送與上一次一樣的 ACK 給對方。

在 TIME_WAIT 狀態,收到 RST 會斷開連線嗎?

在前面我留了一個疑問,處於 TIME_WAIT 狀態的連線,收到 RST 會斷開連線嗎?

會不會斷開,關鍵看 net.ipv4.tcp_rfc1337 這個核心引數(預設情況是為 0):

  • 如果這個引數設定為 0, 收到 RST 報文會提前結束 TIME_WAIT 狀態,釋放連線。
  • 如果這個引數設定為 1, 就會丟掉 RST 報文。

原始碼處理如下:

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
      const struct tcphdr *th)

{
....
  //rst報文的時間戳沒有發生迴繞
 if (!paws_reject &&
     (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&
      (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {

      //處理rst報文
      if (th->rst) {
        //不開啟這個選項,當收到 RST 時會立即回收tw,但這樣做是有風險的
        if (twsk_net(tw)->ipv4.sysctl_tcp_rfc1337 == 0) {
          kill:
          //刪除tw定時器,並釋放tw
          inet_twsk_deschedule_put(tw);
          return TCP_TW_SUCCESS;
        }
      } else {
        //將 TIMEWAIT 狀態的持續時間重新延長
        inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
      }

      ...
      return TCP_TW_SUCCESS;
    }
}

TIME_WAIT 狀態收到 RST 報文而釋放連線,這樣等於跳過 2MSL 時間,這麼做還是有風險。

sysctl_tcp_rfc1337 這個引數是在 rfc 1337 文件提出來的,目的是避免因為 TIME_WAIT 狀態收到 RST 報文而跳過 2MSL 的時間,文件裡也給出跳過 2MSL 時間會有什麼潛在問題。

TIME_WAIT 狀態之所以要持續 2MSL 時間,主要有兩個目的:

  • 防止歷史連線中的資料,被後面相同四元組的連線錯誤的接收;
  • 保證「被動關閉連線」的一方,能被正確的關閉;

詳細的為什麼要設計 TIME_WAIT 狀態,我在這篇有詳細說明:如果 TIME_WAIT 狀態持續時間過短或者沒有,會有什麼問題?

雖然 TIME_WAIT 狀態持續的時間是有一點長,顯得很不友好,但是它被設計來就是用來避免發生亂七八糟的事情。

《UNIX網路程式設計》一書中卻說道:TIME_WAIT 是我們的朋友,它是有助於我們的,不要試圖避免這個狀態,而是應該弄清楚它

所以,我個人覺得將 net.ipv4.tcp_rfc1337 設定為 1 會比較安全。

總結

在 TCP 正常揮手過程中,處於 TIME_WAIT 狀態的連線,收到相同四元組的 SYN 後會發生什麼?

如果雙方開啟了時間戳機制:

  • 如果客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要並且SYN 的「時間戳」比服務端「最後收到的報文的時間戳」要。那麼就會重用該四元組連線,跳過 2MSL 而轉變為 SYN_RECV 狀態,接著就能進行建立連線過程。
  • 如果客戶端的 SYN 的「序列號」比服務端「期望下一個收到的序列號」要或者SYN 的「時間戳」比服務端「最後收到的報文的時間戳」要。那麼就會再回復一個第四次揮手的 ACK 報文,客戶端收到後,發現並不是自己期望收到確認號,就回 RST 報文給服務端

在 TIME_WAIT 狀態,收到 RST 會斷開連線嗎?

  • 如果 net.ipv4.tcp_rfc1337 引數為 0,則提前結束 TIME_WAIT 狀態,釋放連線。
  • 如果 net.ipv4.tcp_rfc1337 引數為 1,則會丟掉該 RST 報文。

完!

相關文章