限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。


tcpdump 通过 AF_PACKET 类型套接字、在 数据链路层(图中 NIC 处) 抓取进出数据包。在数据从网卡(NIC) 进入时,首先送入到抓包 socket 缓冲,然后再送入到网络协议栈。送入到抓包 socket 缓冲的数据再经 BPF 钩子过滤后(如果 tcpdump 设置了过滤器),最终传递到 tcpdump。在数据从应用层往外发送 时,数据首先经过网络协议栈(假定数据在经过协议栈的过程中没被过滤掉),然后先送入抓包 socket 缓冲,再传递给网卡发送出去。
从上述说明,我们了解到 tcpdump 是从 数据链路层 抓取数据,因此 tcpdump 能抓取所有进入网卡的数据,即使目的 MAC 地址不是网卡的数据。对于支持混杂模式的网卡,也可以开启混杂模式来抓取(在不开启混杂模式的情形下,网卡硬件电路会过滤掉目标 MAC 不是自身的包,广播包除外)。而对于发送出去的数据,如果还没有到达数据链路层之前就被过滤掉了(譬如被防火墙过滤的包等情形),tcpdump 将抓取不到它们。
有时候我们发现,从 tcpdump 抓包开始到有内容输出,会存在一段时间的延迟。如果没有指定 -n 选项的话,可能 tcpdump 可能在查询 DNS 服务器将 IP 地址转换为主机名,这可能消耗很长的时间,导致输出的延迟。在不关心主机名的情形,加上 -n 选项,可以避免这种情形。相信大家使用 route 命令也会有相同的体验,直接使用 route 有时候会有很长的延迟,如果是使用 route -n 就可以避免这种延迟。
可以用 -S 选项显示 绝对(absolute)序列号,而不是 相对(relative)序列号。
如果我们不想抓每个包的所有内容,可以用 -s 选项抓包的部分内容,譬如我们只抓取 以太网帧头 + IP 头,以减少抓包时间。默认抓包的长度为 262144 字节。
更多选项请参考 tcpdump 官方链接:https://www.tcpdump.org/manpages/tcpdump.1.html

上图所示 TCP连接建立过程 如下:
1. 客户端 发送 序号为 x 的 SYN 请求给服务端;
2. 服务端 以 序号为 y 的 SYN 请求、确认号为 x+1 的 ACK 回应客户端的 SYN 请求;
3. 客户端 再回应 服务端 序号为 y 的 SYN 请求一个 ACK,ACK 的 序列号为 x+1, 确认号为 y+1 。应该了解的是:包的序列号 seq 是 服务端 和 客户端 独自维护的,而回应给对端的 ack 的编号,是对端发送过来包的 seq + 1 。看实际 TCP连接建立过程 抓包例子(只截取了握手信息):
# tcpdump -xx -vvv -A -S tcp and host 192.168.10.211
[ 162.969323] device eth0 entered promiscuous mode
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
192.168.10.211.56925 > 192.168.10.198.8000: Flags [S], cksum 0xe487 (correct), seq 2305192361, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
192.168.10.198.8000 > 192.168.10.211.56925: Flags [S.], cksum 0x738b (correct), seq 3370362890, ack 2305192362, win 64240, options [mss 1460,nop,nop,sackOK,nop,wscale 5], length 0
192.168.10.211.56925 > 192.168.10.198.8000: Flags [.], cksum 0x8f38 (correct), seq 2305192362, ack 3370362891, win 8212, length 0从上面看到,有两台机器,IP 分别为 192.168.10.211 和 192.168.10.198 ,192.168.10.198 为 服务端,192.168.10.211 为客户端。> 指示了数据通信方向,如 192.168.10.211.56925 > 192.168.10.198.8000 表示 192.168.10.211 向 192.168.10.198 发送数据。分析下抓包数据:
1. `Flags [S]` 标记 客户端 向服务端 发起连接 `SYN` 请求,即前面图中 TCP连接过程 中的 步骤1;
2. `[S.]` 标记 服务端 对 客户端 `SYN` 请求回应的 `ACK`,以及服务端发往客户端 SYN 请求,即前面
图中 TCP连接过程 中的 步骤2;
3. `[.]` 标记 客户端回应服务端的 SYN 请求一个ACK,即前面图中 TCP连接过程 中的 步骤3。
192.168.10.211.56925 > 192.168.10.198.8000: Flags [P.], cksum 0x7121 (correct), seq 2305192362:2305192531, ack 3370362891, win 8212, length 169
192.168.10.198.8000 > 192.168.10.211.56925: Flags [.], cksum 0xa6d0 (correct), seq 3370362891, ack 2305192531, win 2003, length 0
1. 客户端 调用 close() 主动关闭连接,向服务端发送序列号为 x 的 FIN 包;
2. 服务端 回应 客户端 序列号为 y、确认号为 x+1 的 ACK 。
3. 服务端 调用 close() 向 客户端 发送 序列号为 z、确认号为 x+1 的 FIN 包;
4. 客服端 回应 服务端 序列号为 x+1、确认号为 z+1 的 ACK,然后进入 TIME-WAIT 等待,
超时后进入 CLOSED 终态。数据抓包如下:
192.168.10.198.8000 > 192.168.10.211.57040: Flags [F.], cksum 0x941f (correct), seq 3223361884, ack 4164693292, win 2003, length 0
192.168.10.211.57040 > 192.168.10.198.8000: Flags [.], cksum 0x7bdf (correct), seq 4164693292, ack 3223361885, win 8211, length 0
192.168.10.211.57040 > 192.168.10.198.8000: Flags [F.], cksum 0x7bde (correct), seq 4164693292, ack 3223361885, win 8211, length 0
192.168.10.198.8000 > 192.168.10.211.57040: Flags [.], cksum 0x941e (correct), seq 3223361885, ack 4164693293, win 2003, length 0其中,[F.] 标记带确认序号的 FIN 包,而 [.] 标记对 FIN 包的回应。
如果想了解 TCP 握手和挥手的内部细节,可参考博文 Linux:TCP三握四挥简析 。
网络通信对端设备可能有着不同的硬件资源(CPU,内存,网卡,磁盘等)、不同的系统负载,这些都可能导致双方通信时发送和接收数据之间存在速度上的差异。TCP 协议中有一个 滑动窗口,其目的之一就是为了处理通信双发收发数据速度差异的问题,其工作原理简单来讲,就是通信双方的 socket 维护统计各自的收发缓冲(窗口)可用空间大小,收到数据的一方在回复对方 ACK 时带上自己当前可用缓冲(窗口)大小,发送方根据接收方回复的当前可用缓冲(窗口)大小,来调整数据发送的数量。这就是所谓的 滑动窗口,很形象,一个动态变化的窗口。
当接收方处理数据不及时,将会导致接收方的缓冲(窗口)变小直至为 0,这时侯接收方如果再收到发送方的数据,会回复发送方一个 零窗口(Zero Window) 信息。看一下 tcpdump 抓包信息:
$ sudo tcpdump -# -S -vvv -w client.cap 'tcp and host 192.168.1.89'
......
50 15:50:52.591981 IP (tos 0x0, ttl 64, id 31689, offset 0, flags [DF], proto TCP (6), length 2916)
192.168.1.201.35320 > 192.168.1.89.8888: Flags [.], cksum 0x8fc9 (incorrect -> 0xca38), seq 4072327897:4072330761, ack 2817502359, win 502, options [nop,nop,TS val 1683157335 ecr 147045879], length 2864
51 15:50:52.634926 IP (tos 0x0, ttl 64, id 31691, offset 0, flags [DF], proto TCP (6), length 1468)
192.168.1.201.35320 > 192.168.1.89.8888: Flags [.], cksum 0x8a21 (incorrect -> 0x0405), seq 4072329345:4072330761, ack 2817502359, win 502, options [nop,nop,TS val 1683157378 ecr 147045879], length 1416
52 15:50:52.635450 IP (tos 0x0, ttl 64, id 35240, offset 0, flags [DF], proto TCP (6), length 64)
192.168.1.89.8888 > 192.168.1.201.35320: Flags [.], cksum 0x49a5 (correct), seq 2817502359, ack 4072330761, win 0, options [nop,nop,TS val 147045922 ecr 1683157335,nop,nop,sack 1 {4072329345:4072330761}], length 0
......上面是 192.168.1.89.8888 作为数据接收方,192.168.1.201.35320 作为数据发送方,接收方一直不从 socket 缓冲 读取数据,导致了最后接收缓冲耗尽,再有数据来时,只能向发送方回复 零窗口 信息(看 52 开头这行的 win 0 信息)。从这个抓包数据看起来可能不那么清晰,我们用 WireShark 来解析一下,看起来就一目了然了:

[1] https://www.rfc-editor.org/rfc/rfc793
[2] https://blog.csdn.net/vnjohn/article/details/129245099