大家好,我是小林。

昨晚有位讀者問了我這麼個問題:

大概意思是,一個已經建立的 TCP 連線,客戶端中途宕機了,而服務端此時也沒有資料要傳送,一直處於 establish 狀態,客戶端恢復後,向服務端建立連線,此時服務端會怎麼處理?

看過我的圖解網路的讀者都知道,TCP 連線是由「四元組」唯一確認的。

然後這個場景中,客戶端的IP、服務端IP、目的埠並沒有變化,所以這個問題關鍵要看客戶端傳送的 SYN 報文中的源埠是否和上一次連線的源埠相同。

1. 客戶端的 SYN 報文裡的埠號與歷史連線不相同

如果客戶端恢復後傳送的 SYN 報文中的源埠號跟上一次連線的源埠號不一樣,此時服務端會認為是新的連線要建立,於是就會通過三次握手來建立新的連線。

那舊連線裡處於 establish 狀態的服務端最後會怎麼樣呢?

如果服務端傳送了資料包給客戶端,由於客戶端的連線已經被關閉了,此時客戶的核心就會回 RST 報文,服務端收到後就會釋放連線。

如果服務端一直沒有傳送資料包給客戶端,在超過一段時間後, TCP 保活機制就會啟動,檢測到客戶端沒有存活後,接著服務端就會釋放掉該連線。

2. 客戶端的 SYN 報文裡的埠號與歷史連線相同

如果客戶端恢復後,傳送的 SYN 報文中的源埠號跟上一次連線的源埠號一樣,也就是處於 establish 狀態的服務端收到了這個 SYN 報文。

大家覺得服務端此時會做什麼處理呢?

  • 丟掉 SYN 報文?
  • 回覆 RST 報文?
  • 回覆 ACK 報文?

剛開始我看到這個問題的時候,也是沒有思路的,因為之前沒關注過,然後這個問題不能靠猜,所以我就看了 RFC 規範和看了 Linux 核心原始碼,最終知道了答案。

我不賣關子,先直接說答案。

處於 establish 狀態的服務端如果收到了客戶端的 SYN 報文(注意此時的 SYN 報文其實是亂序的,因為 SYN 報文的初始化序列號其實是一個隨機數),會回覆一個攜帶了正確序列號和確認號的 ACK 報文,這個 ACK 被稱之為 Challenge ACK。

接著,客戶端收到這個 Challenge ACK,發現序列號並不是自己期望收到的,於是就會回 RST 報文,服務端收到後,就會釋放掉該連線。

RFC 文件解釋

rfc793 文件裡的第 34 頁裡,有說到這個例子。


原文的解釋我也貼出來給大家看看。

  • When the SYN arrives at line 3, TCP B, being in a synchronized state,
    and the incoming segment outside the window, responds with an
    acknowledgment indicating what sequence it next expects to hear (ACK
    100).
  • TCP A sees that this segment does not acknowledge anything it
    sent and, being unsynchronized, sends a reset (RST) because it has
    detected a half-open connection.
  • TCP B aborts at line 5.
  • TCP A willcontinue to try to establish the connection;

我就不瞎翻譯了,意思和我在前面用中文說的解釋差不多。

原始碼分析

處於 establish 狀態的服務端如果收到了客戶端的 SYN 報文時,核心會呼叫這些函式:

tcp_v4_rcv
  -> tcp_v4_do_rcv
    -> tcp_rcv_established
      -> tcp_validate_incoming
        -> tcp_send_ack

我們只關注 tcp_validate_incoming 函式是怎麼處理 SYN 報文的,精簡後的程式碼如下:


從上面的程式碼實現可以看到,處於 establish 狀態的服務端,在收到報文後,首先會判斷序列號是否在視窗內,如果不在,則看看 RST 標記有沒有被設定,如果有就會丟掉。然後如果沒有 RST 標誌,就會判斷是否有 SYN 標記,如果有 SYN 標記就會跳轉到 syn_challenge 標籤,然後執行 tcp_send_challenge_ack 函式。

tcp_send_challenge_ack 函式裡就會呼叫 tcp_send_ack 函式來回復一個攜帶了正確序列號和確認號的 ACK 報文。

如何關閉一個 TCP 連線?

這裡問題大家這麼一個問題,如何關閉一個 TCP 連線?

可能大家第一反應是「殺掉程序」不就行了嗎?

是的,這個是最粗暴的方式,殺掉客戶端程序和服務端程序影響的範圍會有所不同:

  • 在客戶端殺掉程序的話,就會發送 FIN 報文,來斷開這個客戶端程序與服務端建立的所有 TCP 連線,這種方式影響範圍只有這個客戶端程序所建立的連線,而其他客戶端或程序不會受影響。
  • 而在服務端殺掉程序影響就大了,此時所有的 TCP 連線都會被關閉,服務端無法繼續提供訪問服務。

所以,關閉程序的方式並不可取,最好的方式要精細到關閉某一條 TCP 連線。

有的小夥伴可能會說,偽造一個四元組相同的 RST 報文不就行了?

這個思路很好,但是不要忘了還有個序列號的問題,你偽造的 RST 報文的序列號一定能被對方接受嗎?

如果 RST 報文的序列號不能落在對方的滑動視窗內,這個 RST 報文會被對方丟棄的,就達不到關閉的連線的效果。

所以,要偽造一個能關閉 TCP 連線的 RST 報文,必須同時滿足「四元組相同」和「序列號正好落在對方的滑動視窗內」這兩個條件。

直接偽造符合預期的序列號是比較困難,因為如果一個正在傳輸資料的 TCP 連線,滑動視窗時刻都在變化,因此很難剛好偽造一個剛好落在對方滑動視窗內的序列號的 RST 報文。

辦法還是有的,我們可以偽造一個四元組相同的 SYN 報文,來拿到“合法”的序列號!

正如我們最開始學到的,如果處於 establish 狀態的服務端,收到四元組相同的 SYN 報文後,會回覆一個 Challenge ACK,這個 ACK 報文裡的「確認號」,正好是服務端下一次想要接收的序列號,說白了,就是可以通過這一步拿到服務端下一次預期接收的序列號。

然後用這個確認號作為 RST 報文的序列號,傳送給服務端,此時服務端會認為這個 RST 報文裡的序列號是合法的,於是就會釋放連線!

在 Linux 上有個叫 killcx 的工具,就是基於上面這樣的方式實現的,它會主動傳送 SYN 包獲取 SEQ/ACK 號,然後利用 SEQ/ACK 號偽造兩個 RST 報文分別發給客戶端和服務端,這樣雙方的 TCP 連線都會被釋放,這種方式活躍和非活躍的 TCP 連線都可以殺掉。

使用方式也很簡單,只需指明客戶端的 IP 和埠號。

./killcx <IP地址>:<埠號>

killcx 工具的工作原理,如下圖。

它偽造客戶端傳送 SYN 報文,服務端收到後就會回覆一個攜帶了正確「序列號和確認號」的 ACK 報文(Challenge ACK),然後就可以利用這個 ACK 報文裡面的資訊,偽造兩個 RST 報文:

  • 用 Challenge ACK 裡的確認號偽造 RST 報文傳送給服務端,服務端收到 RST 報文後就會釋放連線。
  • 用 Challenge ACK 裡的序列號偽造 RST 報文傳送給客戶端,客戶端收到 RST 也會釋放連線。

正是通過這樣的方式,成功將一個 TCP 連線關閉了!

這裡給大家貼一個使用 killcx 工具關閉連線的抓包圖,大家多看看序列號和確認號的變化。


所以,以後抓包中,如果莫名奇妙出現一個 SYN 包,有可能對方接下來想要對你發起的 RST 攻擊,直接將你的 TCP 連線斷開!

怎麼樣,很巧妙吧!