大家好,我是藍胖子,今天我們來分析下網路連線中經常出現的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的各種場景。
這個場景比較容器,不啟動伺服器端,然後對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訊息的場景,這次同樣用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)
}
對這種場景的抓包如下: