写点什么

抓包分析 RST 信号

  • 2023-06-12
    广东
  • 本文字数:3472 字

    阅读完需:约 11 分钟

抓包分析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 8080tcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening 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 018: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_timeout60
复制代码

客户端关闭,服务端发送消息

接着来看下,服务端在客户端关闭(无论是主动调用 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)  }
复制代码


对这种场景的抓包如下:



发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020-09-17 加入

还未添加个人简介

评论

发布
暂无评论
抓包分析RST信号_TCP_蓝胖子的编程梦_InfoQ写作社区