赞
踩
对于日常工作的开发工作中,大部分同学都没有涉及到网络的抓包工作,同时对于大部分开发的同学来说对于网络抓包工具也是不熟悉。那本文将介绍计算机网络中用于抓包的两个常用工具tcpdump和Wireshark,通过使用tcpdump/Wireshark工具来对TCP的三次握手和四次挥手进行抓包实战,来帮助大家更好的理解计算机网络中的连接与断开的原理,同时也是帮助大家学会使用网络抓包工具来解决项目中的网络问题。
这两者实际上是搭配使用的,先用Tcpdump命令在 Linux 服务器上抓包,接着把抓包的文件拖出到 Windows电脑后,用Wireshark可视化分析。如果你是在Windows上抓包,只需要用Wireshark工具就可以。
Tcpdump使用说明 | ||
选项 | 示例 | 说明 |
-i | tcpdump -i eth0 | 指定网络接口 默认是0 号接口,any 表示所有接口 |
-nn | tcpdump -nn | 不解析IP地址和端口名称 |
-c | tcpdump -c 5 | 限制要抓取的网络包的个数 |
-w | tcpdump -w file.pcap | 将信息保存到文件呢中文件以。pcap为后缀 |
host src host dst host | tcpdump -nn host 192.168.1.100 | 主机过滤 |
port src port dst port | tcpdump --nn port 80 | 端口过滤 |
ip ip6 arp tcp udp | tcpdump -nn tcp | 协议过滤 |
and or not | tcpdump -nn host 192.168.1.100 and port 80 | 逻辑表达式过滤 |
tcp[tcoflages] | tcpdump -nn “tcp[tcpflags] & tcp-syn! =0” | 特定状态的TCP包 |
tcpdump提供了大量的选项以及各式各样的过滤表达式,来帮助你抓取指定的数据包,不过不要担心,只需要掌握一些常用选项和过滤表达式,就可以满足大部分场景的需要了。假设我们要抓取下面的 ping 的数据包:
- # -I ens192 表示从ens192 网卡出发
- # -c 3 表示发出3个icmp数据
-
- (base) mystic@k8s-master:~$ ping -I ens192 -c 3 20.12.151.195
- PING 20.12.151.195 (20.12.151.195) from 20.1.206.131 ens192: 56(84) bytes of data.
- 64 bytes from 20.12.151.195: icmp_seq=1 ttl=62 time=1.11 ms
- 64 bytes from 20.12.151.195: icmp_seq=2 ttl=62 time=0.421 ms
- 64 bytes from 20.12.151.195: icmp_seq=3 ttl=62 time=0.347 ms
-
- --- 20.12.151.195 ping statistics ---
- 3 packets transmitted, 3 received, 0% packet loss, time 2021ms
- rtt min/avg/max/mdev = 0.347/0.626/1.111/0.344 ms
要抓取上面的 ping 命令数据包,首先我们要知道 ping 的数据包是 icmp 协议,接着在使用 tcpdump抓包的时候,就可以指定只抓 icmp 协议的数据包:
- tcpdump -i ens192 icmp and host 20.12.151.195 -nn
-
- # -i ens192
- # icmp
- # host
- # -nn
- ------------------------------------------------------------------
- 返回的结果:
-
- 时间戳协议源地址.源端口 > 目的地址.目的端口网络包详细信息
-
从tcpdump 抓取的 icmp 数据包,我们很清楚的看到 icmp echo的交互过程了,首先发送方发起了ICMP echo request 请求报文,接收方收到后回了一个ICMP echo reply 响应报文,之后 seq是递增的。
tcpdump -i ens192 icmp and host 20.12.151.195 -w ping.pcap
接着把 ping.pcap 文件拖到电脑,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:
在Wireshark 的页面里,可以更加直观的分析数据包,不仅展示各个网络包的头部信息,还会用不同的颜色来区分不同的协议,由于这次抓包只有ICMP 协议,所以只有紫色的条目。接着,在网络包列表中选择某一个网络包后,在其下面的网络包详情中,可以更清楚的看到,这个网络包在协议栈各层的详细信息。
从ping 的例子中,我们可以看到网络分层就像有序的分工,每一层都有自己的责任范围和信息,上层协议完成工作后就交给下一层,最终形成一个完整的网络包。
本次例子,我们将要访问的 http://192.168.3.200 服务端。在终端一用 tcpdump 命令抓取数据包:
tcpdump -i ens192 icmp and host 20.12.151.195 and port 80 -w test.pcap
curl http://192.168.3.200
使用 Wireshark 打开 http.pcap 后,你就可以在 Wireshark 中,看到如下的界面:
Wireshark 可以用时序图的方式显示数据包交互的过程,从菜单栏中,点击 统计 (Statistics) -> 流量图(Flow Graph),然后,在弹出的界面中的「流量类型」选择 「TCP Flows」,你可以更清晰的看到,整个过程中 TCP 流的执行过程:
为什么三次握手连接过程的 Seq 是0?实际上是Wireshark工具帮我们做了优化,它默认显示的是序列号 seq 是相对值,而不是真实值。如果你想看到实际的序列号的值,可以右键菜单, 然后找到协议首选项,接着找到RelativeSeq后,把它给取消,操作如下:
为什么抓到的TCP挥手是三次,而不是书上说的四次?因为服务器端收到客户端的 FIN 后,服务器端同时也要关闭连接,这样就可以把 ACK 和 FIN 合并到一起发送,节省了一个包,变成了“三次挥手”。而通常情况下,服务器端收到客户端的 FIN 后,很可能还没发送完数据,所以就会先回复客户端一个ACK 包,稍等一会儿,完成所有数据包的发送后,才会发送 FIN 包,这也就是四次挥手了。如下图,就是四次挥手的过程:
本次实验用了两台虚拟机,一台作为服务端,一台作为客户端,它们的关系如下:
为了模拟 TCP 第一次握手 SYN 丢包的情况,我是在拔掉服务器的网线后,立刻在客户端执行 curl 命令:
- # 客户端发起请求
-
- $ date ; curl http://192.168.25.16; date
-
- # 请求阻塞中…………
其间 tcpdump 抓包的命令如下:
- # 客户端执行tcpdump 抓取到服务端的HTTP的数据包
-
- $ tcpdump -i ens192 tcp and host 192.168.12.36 and port -w tcp_sys_timeout.pcap
过了一会, curl 返回了超时连接的错误:
- # 客户端发起请求
-
- $ date ; curl http://192.168.25.16; date
-
- # 请求阻塞中…………
-
- # 显示 connect timeout
从date 返回的时间,可以发现在超时接近 1 分钟的时间后,curl 返回了错误。接着,把 tcp_sys_timeout.pcap 文件用 Wireshark 打开分析,显示如下图:
从上图可以发现, 客户端发起了 SYN 包后,一直没有收到服务端的 ACK ,所以一直超时重传了次,并且每次 RTO 超时时间是不同的:
可以发现,每次超时时间 RTO 是指数(翻倍)上涨的,当超过最大重传次数后,客户端不再发送 SYN包。在 Linux 中,第一次握手的 SYN 超时重传次数,是如下内核参数指定的:
- $ cat /proc/sys/net/ipv4/tcp_syn_retries
- 5
-
- tcp_syn_retries 默认值为 5,也就是 SYN 最大重传次数是 5 次。
接下来,我们继续做实验,把 tcp_syn_retries 设置为 2 次:
$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
重传抓包后,用 Wireshark 打开分析,显示如下图:
实验一总结:通过实验一的实验结果,我们可以得知,当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达 tcp_syn_retries 值后,客户端不再发送 SYN 包。
为了模拟客户端收不到服务端第二次握手 SYN、ACK 包,我的做法是在客户端加上防火墙限制,直接粗暴的把来自服务端的数据都丢弃,防火墙的配置如下:
- # 客户端配置防火墙
- iptable -I input -s 192.168.25.16 -j DROP
接着,在客户端执行 curl 命令:
- # 客户端发起请求
-
- $ date ; curl http://192.168.25.16; date
-
- # 请求阻塞中…………
-
- # 显示 connect timeout
从 date 返回的时间前后,可以算出大概 1 分钟后,curl 报错退出了。客户端在这其间抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:
从图中可以发现:
所以,我们可以发现,当第二次握手的 SYN、ACK 丢包时,客户端会超时重发 SYN 包,服务端也会超时重传 SYN、ACK 包。客户端设置了防火墙,屏蔽了服务端的网络包,为什么 tcpdump 还能抓到服务端的网络包?添加 iptables 限制后, tcpdump 是否能抓到包 ,这要看添加的 iptables 限制条件:
网络包进入主机后的顺序如下:
tcp_syn_retries 是限制 SYN 重传次数,那第二次握手 SYN、ACK 限制最大重传次数是多少?TCP 第二次握手 SYN、ACK 包的最大重传次数是通过 tcp_synack_retries 内核参数限制的,其默认值如下:
- $ cat /proc/sys/net/ipv4/tcp_synack_retries
- 5
-
- 是的,TCP 第二次握手 SYN、ACK 包的最大重传次数默认值是 5 次。
- 为了验证 SYN、ACK 包最大重传次数是 5 次,我们继续做下实验,我们先把客户端的 tcp_syn_retries
- 设置为 1,表示客户端 SYN 最大超时次数是 1 次,目的是为了防止多次重传 SYN,把服务端 SYN、
- ACK 超时定时器重置。
接着,还是如上面的步骤:
把抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:
从上图,我们可以分析出:
接着,我把 tcp_synack_retries 设置为 2, tcp_syn_retries 依然设置为 1:
- $ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
- $ echo 1 > /proc/sys/net/ipv4/tcp_syn_retries
依然保持一样的实验步骤进行操作,接着把抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:
由上图可见:
实验二总结: 通过实验二的实验结果,我们可以得知,当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。
为了模拟 TCP 第三次握手 ACK 包丢,我的实验方法是在服务端配置防火墙,屏蔽客户端 TCP 报文中标志位是 ACK 的包,也就是当服务端收到客户端的 TCP ACK 的报文时就会丢弃,iptables 配置命令如下:
接着,在客户端执行如下 tcpdump 命令:
然后,客户端向服务端发起 telnet,因为 telnet 命令是会发起 TCP 连接,所以用此命令做测试:
此时,由于服务端收不到第三次握手的 ACK 包,所以一直处于 SYN_RECV 状态:
而客户端是已完成 TCP 连接建立,处于 ESTABLISHED 状态:
过了 1 分钟后,观察发现服务端的 TCP 连接不见了:
过了 30 分别,客户端依然还是处于 ESTABLISHED 状态:
接着,在刚才客户端建立的 telnet 会话,输入 123456 字符,进行发送:
持续「好长」一段时间,客户端的 telnet 才断开连接:
以上就是本次的实现三的现象,这里存在两个疑点:
原因:服务端在重传 SYN、ACK 包时,超过了最大重传次数 tcp_synack_retries ,于是服务端的 TCP连接主动断开了。
原因:客户端向服务端发送数据包时,由于服务端的 TCP 连接已经退出了,所以数据包一直在超时重传,共重传了 15 次, telnet 就断开了连接。
上图的流程:
客户端在向服务端发起 HTTP GET 请求时,一个完整的交互过程,需要 2.5 个 RTT 的时延。由于第三次握手是可以携带数据的,这时如果在第三次握手发起 HTTP GET 请求,需要2个RTT的时延。但是在下一次(不是同个 TCP 连接的下一次)发起 HTTP GET 请求时,经历的 RTT 也是一样,如下图:
在 Linux 3.7 内核版本中,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。
注:客户端在请求并存储了Fast Open Cookie 之后,可以不断重复 TCP Fast Open 直至服务器认为Cookie 无效(通常为过期)。
在 Linux 上如何打开 Fast Open 功能?
可以通过设置 net.ipv4.tcp_fastopn 内核参数,来打开 Fast Open 功能。net.ipv4.tcp_fastopn 各个值的意义:
在下图,数据包 7 号,客户端发起了第二次 TCP 连接时,SYN 包会携带 Cooike,并且长度为 5 的数据。服务端收到后,校验 Cooike 合法,于是就回了 SYN、ACK 包,并且确认应答收到了客户端的数据包,ACK = 5 + 1 = 6。
当接收方收到乱序数据包时,会发送重复的 ACK,以便告知发送方要重发该数据包,当发送方收到 3个重复 ACK 时,就会触发快速重传,立刻重发丢失数据包。
TCP 重复确认和快速重传的一个案例,用 Wireshark 分析,显示如下:
注意:快速重传和重复 ACK 标记信息是 Wireshark 的功能,非数据包本身的信息。
以上案例在 TCP 三次握手时协商开启了选择性确认 SACK,因此一旦数据包丢失并收到重复 ACK ,即使在丢失数据包之后还成功接收了其他数据包,也只需要重传丢失的数据包。如果不启用 SACK,就必须重传丢失包之后的每个数据包。如果要支持 SACK ,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。
TCP 为了防止发送方无脑的发送数据,导致接收方缓冲区被填满,所以就有了滑动窗口的机制,它可利用接收方的接收窗口来控制发送方要发送的数据量,也就是流量控制。
接收窗口是由接收方指定的值,存储在 TCP 头部中,它可以告诉发送方自己的 TCP 缓冲空间区大小,这个缓冲区是给应用程序读取数据的空间:
接收窗口的大小,是在 TCP 三次握手中协商好的,后续数据传输时,接收方发送确认应答 ACK 报文时,会携带当前的接收窗口的大小,以此来告知发送方。
假设接收方接收到数据后,应用层能很快的从缓冲区里读取数据,那么窗口大小会一直保持不变,过程如下:
但是现实中服务器会出现繁忙的情况,当应用程序读取速度慢,那么缓存空间会慢慢被占满,于是为了保证发送方发送的数据不会超过缓冲区大小,服务器则会调整窗口大小的值,接着通过 ACK 报文通知给对方,告知现在的接收窗口大小,从而控制发送方发送的数据大小。
假设接收方处理数据的速度跟不上接收数据的速度,缓存就会被占满,从而导致接收窗口为 0,当发送方接收到零窗口通知时,就会停止发送数据。如下图,可以看到接收方的窗口大小在不断的收缩至 0:接着,发送方会定时发送窗口大小探测报文,以便及时知道接收方窗口大小的变化。
以下图 Wireshark 分析图作为例子说明:
可以发现,这些窗口探测报文以 3.4s、6.5s、13.5s 的间隔出现,说明超时时间会翻倍递增。
当我们 TCP 报文的承载的数据非常小的时候,例如几个字节,那么整个网络的效率是很低的,因为每个 TCP 报文中都会有 20 个字节的 TCP 头部,也会有 20 个字节的 IP 头部,而数据只有几个字节,所以在整个报文中有效数据占有的比重就会非常低。
这就好像快递员开着大货车送一个小包裹一样浪费。那么就出现了常见的两种策略,来减少小报文的传输,分别是:
Nagle 算法做了一些策略来避免过多的小数据报文发送,这可提高传输效率。Nagle 算法的策略:
只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
可以看出,Nagle 算法一定会有一个小报文,也就是在最开始的时候。另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh这样的交互性比较强的程序,则需要关闭 Nagle 算法。可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)。
事实上当没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP
头,但却没有携带数据报文。为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。
TCP 延迟确认的策略:
延迟等待的时间是在 Linux 内核中定义:关键就需要 HZ 这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样。
当 TCP 延迟确认 和 Nagle 算法混合使用时,会导致时耗增长,如下图:
发送方使用了 Nagle 算法,接收方使用了 TCP 延迟确认会发生如下的过程:
很明显,这两个同时使用会造成额外的时延,这就会使得网络"很慢"的感觉。要解决这个问题,只有两个办法:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。