抓包分析RST報文

2023-06-14 12:06:28

大家好,我是藍胖子,今天我們來分析下網路連線中經常出現的RST訊號,連線中出現RST訊號意味著這條連結將會斷開,來看下什麼時候會觸發RST訊號,這在分析連線斷開的原因時十分有幫助。

本文的講解視訊已經上傳 抓包分析RST報文

在開始分析觸發RST的場景之前,我們先來準備下需要的使用者端和伺服器端程式碼,以方便我們進行測試。

伺服器端程式碼目前先是在8080埠監聽,然後將接收到的訊息列印出來。

func main() {  
   listen, err := net.Listen("tcp", ":8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   go func() {  
      for {  
         conn, err := listen.Accept()  
         if err != nil {  
            log.Fatal(err)  
         }  
         buf := make([]byte, 1024)  
         n, err := conn.Read(buf)  
         if err != nil {  
            log.Fatal(err)  
         }  
         fmt.Println(string(buf[:n]))  
       
   }()  
   ch := make(chan int)  
   <-ch  
}

使用者端程式碼,連線8080埠然後列印hello world

func main() {  
   conn, err := net.Dial("tcp", "192.168.2.3:8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   _, err = conn.Write([]byte("hello world"))  
   if err != nil {  
      log.Fatal(err)  
   }  
}

現在,來讓我們測試下觸發RST的各種場景。

什麼時候會觸發RST

對端沒有監聽埠時

這個場景比較容器,不啟動伺服器端,然後對8080埠進行抓包,接著直接執行使用者端程式,看看此時使用者端收到的封包是怎樣的。

(base) ➜  ~ sudo tcpdump -i lo0 port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes


18:58:14.745651 IP xiongchongdembp.63558 > xiongchongdembp.http-alt: Flags [S], seq 1854765658, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 98239951 ecr 0,sackOK,eol], length 0
18:58:14.745699 IP xiongchongdembp.http-alt > xiongchongdembp.63558: Flags [R.], seq 0, ack 1854765659, win 0, length 0

從tcpdump的抓包結果可以看出,使用者端程式發出了握手訊號[S],直接被回覆了[R.]RST訊號,可見,伺服器端沒有監聽埠時,系統核心會對想要連線該埠的使用者端回覆RST訊號。

一端關閉了連線,另一端還在傳送資料

再來看看使用者端關閉後,對端繼續傳送訊息的場景,這樣的場景分為兩種情況,一種事伺服器端傳送keepalive訊息,一種是伺服器端傳送業務位元組資料。

使用者端關閉,伺服器端傳送keepalive

先來看看傳送keepalive訊息的場景,這次同樣用tcpdump監聽8080埠,不過為了更清晰的分析這次抓包檔案,我將tcpdump的抓包檔案存到了本地,之後wireshark再去開啟,tcpdump抓包命名如下:

sudo tcpdump -i lo0 port 8080 -w lo.pcap

接著,用文章開頭準備的程式碼段啟動伺服器端,使用者端,注意,此時伺服器端僅僅是列印了收到的訊息,並沒有對使用者端進行迴應,而使用者端程序也是在傳送訊息後就被銷燬了。來看看此時的抓包檔案

當用戶端程序關閉時,即使沒有顯示的呼叫close方法,核心也會幫助我們關閉連線,傳送fin訊號,此時使用者端連線會進入fin wait1狀態,在這個狀態下,使用者端還是可以正常回應keep alive訊息,不過超過fin wait1狀態的超時時間時,則會被系統核心自動回收掉,此時再傳送keepalive訊息就會回覆RST 這個超時時間在linux核心上可以通過下面這個檔案進行修改,預設是1min。

root@ecs-295280:~# cat /proc/sys/net/ipv4/tcp_fin_timeout
60

使用者端關閉,伺服器端傳送訊息

接著來看下,伺服器端在使用者端關閉(無論是主動呼叫close方法還是程序結束連線被核心關閉都一樣)的場景下主動傳送訊息觸發RST的場景。

此時需要修改下目前伺服器端的程式碼了。

func main() {  
   listen, err := net.Listen("tcp", ":8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   go func() {  
      for {  
         conn, err := listen.Accept()  
         if err != nil {  
            log.Fatal(err)  
         }  
         buf := make([]byte, 1024)  
         n, err := conn.Read(buf)  
         if err != nil {  
            log.Fatal(err)  
         }  
         fmt.Println(string(buf[:n]))  
		time.Sleep(time.Second)
         _, err = conn.Write([]byte("receive msg"))  
         if err != nil {  
            fmt.Println(err)  
         }  
  
   }()  
   ch := make(chan int)  
   <-ch  
}

這次的伺服器端不僅列印了收到的訊息,還將訊息傳送給了使用者端,為了確保伺服器端傳送訊息時,使用者端已經關閉了,我還在伺服器端收到訊息時故意停留了1s再傳送訊息。

此時用tcpdump抓包如下:

可以看到在連線關閉後,還往連線傳送訊息是會觸發RST訊號的。

當伺服器端緩衝區還有資料時,伺服器端關閉連結

伺服器端讀緩衝區還有資料

接著來看下伺服器端讀緩衝區有資料的情況下,伺服器端關閉連線的場景,這個場景伺服器端會直接傳送RST訊號,我們對使用者端程式碼進行修改,讓它傳送完訊息程序等待狀態,防止程序結束。

func main() {  
   conn, err := net.Dial("tcp", "192.168.2.3:8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   _, err = conn.Write([]byte("hello world"))  
   if err != nil {  
      log.Fatal(err)  
   }  
   time.Sleep(time.Hour)  
}

然後對伺服器端程式碼進行修改,握手成功後等待2s來確保使用者端傳送的訊息到達,然後關閉連線。

func main() {  
   listen, err := net.Listen("tcp", ":8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   go func() {  
      for {  
         conn, err := listen.Accept()  
         if err != nil {  
            log.Fatal(err)  
         }  
         time.Sleep(2 * time.Second)  
         conn.Close()  
      }  
  
   }()  
   ch := make(chan int)  
   <-ch  
}

對這個場景的抓包如下:

可見,伺服器端在關閉連線時直接傳送了RST訊號。

伺服器端寫緩衝區還有資料

再來看下最後一個RST訊號觸發的場景,預設情況下,當寫緩衝區還有資料時,如果呼叫close方法,會將寫緩衝區的傳送到對端然後再傳送fin訊號,但是如果設定了linger屬性,那麼情況會變得不同。

// SetLinger sets the behavior of Close on a connection which still// has data waiting to be sent or to be acknowledged.  
//  
// If sec < 0 (the default), the operating system finishes sending the  
// data in the background.  
//  
// If sec == 0, the operating system discards any unsent or  
// unacknowledged data.  
//  
// If sec > 0, the data is sent in the background as with sec < 0. On  
// some operating systems after sec seconds have elapsed any remaining  
// unsent data may be discarded.
func (c *TCPConn) SetLinger(sec int) error 

如果寫緩衝區還有資料或者傳送了資料但是沒有被ack,當設定linger為0時,進行close,會直接將寫緩衝區資料丟棄並且往對端傳送RST訊號。

為了驗證這種場景,我們將伺服器端的程式碼再改動下,將連線linger屬性設定為0,並且在寫入一段資料後馬上關閉。

func main() {  
   listen, err := net.Listen("tcp", ":8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   go func() {  
      for {  
         conn, err := listen.Accept()  
         if err != nil {  
            log.Fatal(err)  
         }  
         buf := make([]byte, 1024)  
         n, err := conn.Read(buf)  
         if err != nil {  
            log.Fatal(err)  
         }  
         conn.(*net.TCPConn).SetLinger(0)  
         fmt.Println(string(buf[:n]))  
         _, err = conn.Write([]byte("receive msg"))  
         if err != nil {  
            fmt.Println(err)  
         }  
         conn.Close()  
  
   }()  
   ch := make(chan int)  
   <-ch  
}

使用者端程式仍然保持在傳送訊息後,睡眠1小時的狀態,防止程序結束

func main() {  
   conn, err := net.Dial("tcp", "192.168.2.3:8080")  
   if err != nil {  
      log.Fatal(err)  
   }  
   _, err = conn.Write([]byte("hello world"))  
   if err != nil {  
      log.Fatal(err)  
   }  
   time.Sleep(time.Hour)  
}

對這種場景的抓包如下: