目录

网络-21 抓包实战:tcpdump 与 Wireshark

前置阅读:网络-07 TCP 连接管理网络-08 TCP 可靠传输

前面所有篇章讲的都是"协议应该怎么工作"。这一篇讲怎么亲眼看到它在工作——以及出问题时怎么从包里找出真相。

分工很明确:服务器上用 tcpdump 抓,本地用 Wireshark 分析。别指望在服务器上用 Wireshark(通常没有图形界面),也别用 tcpdump 做复杂分析(命令行看 TCP 流太痛苦)。

1. tcpdump 基础

# 最小可用命令:抓指定网卡的指定端口
tcpdump -i eth0 port 80

# 常用组合(推荐记住这一条)
tcpdump -i eth0 -nn -s0 -w cap.pcap port 80

关键参数:

参数 作用
-i eth0 指定网卡。-i any 抓所有网卡,-D 列出可用网卡
-nn 不解析域名和端口名。极其重要——不加会因反向 DNS 查询卡住,而且抓包本身又会产生 DNS 流量污染结果
-w file.pcap 写入文件,之后用 Wireshark 分析。不加则直接打印
-r file.pcap 读取已有文件
-s0 抓完整数据包(老版本默认只抓前 96 字节,会截断)
-c 100 抓够 100 个包就退出
-A 以 ASCII 打印内容,看 HTTP 明文时方便
-X 十六进制 + ASCII 并排显示
-v / -vv 更详细的输出(显示 TTL、IP ID 等)
-e 显示链路层信息(MAC 地址)
-tttt 打印可读的绝对时间戳

-nn-w 是两个最该形成肌肉记忆的参数:前者避免抓包动作自身干扰结果,后者让你能带回本地慢慢看。

1.1 生产环境的安全用法

在线上抓包要防止把磁盘写满:

# 单文件最大 100MB,最多保留 5 个循环覆盖
tcpdump -i eth0 -nn -s0 -w cap.pcap -C 100 -W 5 port 443

# 每 60 秒切换一个新文件,文件名带时间戳
tcpdump -i eth0 -nn -s0 -w 'cap-%Y%m%d-%H%M%S.pcap' -G 60 -W 10 port 443

另外两个注意事项

  • 抓包有性能开销。高流量机器上无过滤地抓包可能影响业务,务必用过滤表达式收窄范围。
  • 抓包内容可能含敏感信息(明文密码、token、用户数据)。pcap 文件要当作敏感数据对待,分析完及时删除。

2. 过滤表达式

这是 tcpdump 的核心。表达式在内核层面过滤(BPF),效率很高,越精确开销越小。

2.1 三类基本条件

# ── 按主机 ──
tcpdump host 1.2.3.4              # 源或目的是它
tcpdump src host 1.2.3.4          # 只看源
tcpdump dst host 1.2.3.4          # 只看目的
tcpdump net 192.168.1.0/24        # 整个网段

# ── 按端口 ──
tcpdump port 80
tcpdump src port 80
tcpdump portrange 8000-8100

# ── 按协议 ──
tcpdump tcp
tcpdump udp port 53               # DNS
tcpdump icmp                      # ping
tcpdump arp

2.2 逻辑组合

# and / or / not(也可写作 && || !)
tcpdump -nn 'host 1.2.3.4 and port 443'
tcpdump -nn 'port 80 or port 443'
tcpdump -nn 'tcp and not port 22'          # ★ 排除 SSH,否则会抓到自己

not port 22 几乎是必加的——你通过 SSH 登录服务器执行 tcpdump,抓到的包会包含你自己敲的每一个字符的回显,形成噪音甚至反馈循环。

复杂表达式要用单引号包起来,否则 shell 会解释掉 ()| 等字符:

tcpdump -nn '(host 1.2.3.4 or host 5.6.7.8) and (port 80 or port 443)'

2.3 按 TCP 标志位过滤

这是排查连接问题的利器:

# 只看 SYN 包 —— 看谁在发起连接
tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0'

# 只看握手的第一个包(SYN 且非 ACK)
tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'

# ★ 只看 RST —— 排查"连接被重置"最有用的一条
tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'

# 只看 FIN —— 看谁先关闭连接
tcpdump -nn 'tcp[tcpflags] & tcp-fin != 0'

# SYN 或 FIN 或 RST,即所有"连接状态变化"的包,不看数据
tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'

最后一条特别实用:它过滤掉所有数据传输,只留下连接的建立与关闭事件,在高流量环境下排查连接问题时能把噪音降到极低。

2.4 抓 HTTP 明文

# 抓 HTTP 请求行(匹配 GET / POST 开头的 TCP 载荷)
tcpdump -nn -A -s0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

# 更简单的近似写法:匹配 "GET " 的十六进制
tcpdump -nn -A 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'

第一条那个长表达式的含义是"TCP 载荷长度不为 0",即只要带数据的包,排除纯 ACK。看起来复杂,原理是 IP总长 - IP首部长 - TCP首部长 > 0

注意 HTTPS 抓不到明文,只能看到 TLS 握手和加密后的 Application Data。解密方法见后文。

3. 常见排查场景

3.1 场景一:连接被重置(Connection reset by peer)

tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0'

看 RST 是谁发的,这直接决定排查方向:

  • 服务端发的 RST → 端口没监听、应用崩了、backlog 满(见 网络-07)、或应用主动 close() 了还有未读数据的连接;
  • 客户端发的 RST → 客户端超时后放弃、连接池丢弃了连接、或收到了它认为不合法的包;
  • 中间设备发的 RST → 防火墙拦截、负载均衡器空闲超时。这种情况常见特征是双方都说"不是我发的",需要在两端同时抓包对比。

两端同时抓包是定位中间设备问题的唯一可靠方法:如果客户端抓到了 RST 但服务端没发出过,那 RST 一定来自中间。

3.2 场景二:连接超时,怀疑握手没完成

tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and host <对端IP>'

判断依据:

现象 结论
只看到 SYN,没有任何回应 包没到对端,或对端没回。查防火墙、安全组、路由
SYN 重复发送多次(间隔 1s、2s、4s…) 典型的 SYN 重传,对端不可达
SYN → SYN+ACK → 但客户端不回 ACK 少见,通常是客户端已放弃或有中间设备干扰
握手完成但随后立刻 RST 连上了但被拒绝,查 backlog 队列和应用状态

SYN 重传的时间间隔呈指数增长(1s、2s、4s、8s…),这是识别它的特征。看到这个模式就说明 SYN 根本没得到回应。

3.3 场景三:怀疑丢包和重传

# 先用统计确认有没有重传
netstat -s | grep -iE "retrans|lost"

# 抓包后在 Wireshark 里用过滤器:tcp.analysis.retransmission
tcpdump -nn -w cap.pcap host <对端IP>

Wireshark 会自动标记出重传包(详见下文)。判断依据:

  • 偶发重传:正常,互联网上不可避免;
  • 重传率超过 1%:链路质量有问题,或对端过载;
  • 同一个包反复重传:严重丢包,或 MTU 问题(见场景四)。

3.4 场景四:MTU 黑洞(小请求正常,大请求卡死)

这是一类特征极其鲜明、但很容易被误判为"应用 bug"的故障。

症状:小请求完全正常,一旦请求或响应体变大就卡住不动,最后超时。常见于 VPN、隧道、跨云专线环境。

原因:链路上某段的 MTU 比两端小,大包需要分片。发送方设置了 DF(Don’t Fragment)标志,路由器无法分片就丢弃并回一个 ICMP “需要分片” 报文——但如果防火墙把这个 ICMP 拦了,发送方永远收不到通知,只会一直重传同样大小的包。这就是 MTU 黑洞(原理见 网络-04)。

验证方法——用 ping 发送不同大小的、禁止分片的包:

# Linux:-M do 表示禁止分片,-s 指定载荷大小
ping -M do -s 1472 8.8.8.8     # 1472 + 28(IP+ICMP头) = 1500,标准以太网
ping -M do -s 1420 8.8.8.8     # 逐步减小,找到能通过的临界值

# macOS
ping -D -s 1472 8.8.8.8

如果 1472 失败而 1420 成功,说明路径 MTU 小于 1500。抓包会看到大包被反复重传且没有对应的 ACK。

处置:调小网卡 MTU,或在网关上做 MSS clamping(改写 TCP 握手时通告的 MSS 值):

# 让 TCP 自己协商出合适的 MSS
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
         -j TCPMSS --clamp-mss-to-pmtu

3.5 场景五:确认 TLS 版本与证书

tcpdump -nn -s0 -w tls.pcap 'tcp port 443 and host <目标>'

即使不解密,也能从握手包里看到:TLS 版本、密码套件列表、SNI(要访问的域名)、服务器证书。这些在 TLS 1.2 下都是明文的——TLS 1.3 把证书加密了(见 网络-17),但 SNI 仍是明文。

不抓包的快捷方式:

openssl s_client -connect example.com:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"

4. Wireshark 分析

把 pcap 拉回本地打开。几个最该掌握的功能:

4.1 显示过滤器

注意区分两种过滤器,这是新手最容易混的地方:

抓包过滤器(Capture Filter) 显示过滤器(Display Filter)
语法 BPF,和 tcpdump 相同 Wireshark 自有语法
时机 抓的时候就丢弃 抓完之后筛选显示
例子 port 80 tcp.port == 80

语法完全不同,写错了要么报错要么不生效。

常用显示过滤器:

ip.addr == 1.2.3.4                    # 主机
tcp.port == 443                       # 端口
tcp.flags.reset == 1                  # RST 包
tcp.flags.syn == 1 && tcp.flags.ack == 0    # 握手第一个包

tcp.analysis.retransmission           # ★ 重传
tcp.analysis.duplicate_ack            # ★ 重复 ACK(快重传的前兆)
tcp.analysis.zero_window              # ★ 零窗口(接收方处理不过来)
tcp.analysis.flags                    # 所有被判定为异常的包

http                                  # 所有 HTTP
http.request.method == "POST"
http.response.code >= 400             # 只看出错的响应
tls.handshake.type == 1               # ClientHello
dns                                   # DNS 查询
frame.len > 1400                      # 大包,查 MTU 问题

tcp.analysis.flags 是排障起点——它一次列出所有 Wireshark 认为异常的包(重传、乱序、零窗口等)。先看这个,再顺着线索深挖。

4.2 三个必用功能

① Follow TCP Stream(追踪流)

右键任意包 → Follow → TCP Stream。它把一条连接的所有包按顺序拼成完整的对话,直接以文本形式呈现请求和响应。看 HTTP 交互时这是最快的方式,不用逐包点开。

② Statistics 菜单

菜单项 用途
Conversations 按连接聚合,看谁传了多少数据、持续多久。找出异常连接的最快方式
Protocol Hierarchy 各协议流量占比,快速了解这份包里都有什么
TCP Stream Graphs 可视化时序图、吞吐、窗口大小变化
Flow Graph 时序图形式展示包的往来,看握手和挥手过程非常直观
Service Response Time 请求-响应延迟统计

Statistics → Conversations 按字节数排序,往往一眼就能看出"是哪条连接在异常传输"。

③ 时间参考与相对时间

Ctrl+T 切换时间显示方式;选中某个包按 Ctrl+Alt+T 设为时间参考点,后续包显示相对于它的时间差。测量"从请求发出到响应返回花了多久"时非常直观。

4.3 从包里认出常见事件

对照 网络-07网络-08 的原理,实际包里长这样:

① 三次握手
   [SYN]              Seq=0
   [SYN, ACK]         Seq=0 Ack=1
   [ACK]              Seq=1 Ack=1

② 数据传输(注意 Seq/Ack 的推进)
   [PSH, ACK]         Seq=1   Ack=1  Len=100
   [ACK]              Seq=1   Ack=101          ← 确认收到 100 字节

③ 四次挥手
   [FIN, ACK]         主动关闭方发起
   [ACK]
   [FIN, ACK]         被动关闭方也关闭
   [ACK]              之后主动方进入 TIME_WAIT

④ 快重传的信号(见 网络-08)
   [ACK] Ack=1001  Dup ACK #1     ← 连续三个重复 ACK
   [ACK] Ack=1001  Dup ACK #2
   [ACK] Ack=1001  Dup ACK #3
   [TCP Retransmission]           ← 发送方立即重传,不等超时

⑤ 零窗口(接收方缓冲区满了,见 网络-09)
   [ACK] Win=0                    ← "别发了,我处理不过来"
   [TCP ZeroWindow]
   ...
   [TCP Window Update] Win=65535  ← 缓过来了,可以继续发

Win=0 零窗口是一个强信号:它说明接收方应用读取数据的速度跟不上,问题在对端的处理能力,不在网络。看到这个就不用查链路了。

5. TLS 解密

抓到的 HTTPS 是密文。有两种方法解密。

5.1 方法一:SSLKEYLOGFILE(推荐)

浏览器和多数使用 NSS/OpenSSL 的程序支持把会话密钥导出到文件:

# 启动前设置环境变量
export SSLKEYLOGFILE=~/tls_keys.log
google-chrome    # 或 curl、firefox

# Wireshark 中配置:
# Preferences → Protocols → TLS → (Pre)-Master-Secret log filename
# 指向 ~/tls_keys.log

配好后 Wireshark 会自动解密,密文变成可读的 HTTP。

Go 程序也可以导出

w, _ := os.OpenFile("tls_keys.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            KeyLogWriter: w,      // ★ 只在调试时启用,绝不能上生产
        },
    },
}

这个方法对 TLS 1.3 同样有效,因为导出的是会话密钥而非服务器私钥。

5.2 方法二:服务器私钥(局限大)

在 Wireshark 里配置服务器的 RSA 私钥。但这个方法对现代 TLS 基本失效——它只在 RSA 密钥交换时有用,而 ECDHE(TLS 1.2 的主流套件)和 TLS 1.3 都具备前向保密,会话密钥从未在网络上传输过,有私钥也解不出来(原理见 网络-16网络-17)。

所以前向保密的代价之一就是抓包调试变难。这也从另一个角度说明了它的安全价值。

5.3 中间人代理

调试 App 或第三方客户端时,更常用 Charles / mitmproxy / Fiddler 这类工具——原理是让客户端信任代理的 CA 证书,形成受控的中间人(见 网络-16 的抓包原理一节)。

mitmproxy -p 8080          # 客户端配置 HTTP 代理指向它,并信任其 CA

注意做了 SSL Pinning 的 App 会拒绝这种代理(见 网络-16 的反抓包策略)。

6. 容器与 K8s 里抓包

容器镜像通常不含 tcpdump,且没有 NET_ADMIN 权限。三种做法:

# ① 在宿主机上抓,用容器 veth 网卡(需要先找到对应网卡)
nsenter -t $(docker inspect -f '{{.State.Pid}}' <容器>) -n tcpdump -nn -i eth0

# ② K8s:临时挂一个共享网络命名空间的调试容器(推荐)
kubectl debug -it <pod> --image=nicolaka/netshoot --target=<容器名>
# 进去后直接 tcpdump,看到的就是目标容器的流量

# ③ 直接在 Pod 里执行(需镜像自带 tcpdump)
kubectl exec -it <pod> -- tcpdump -nn -i eth0 -w /tmp/cap.pcap
kubectl cp <pod>:/tmp/cap.pcap ./cap.pcap

kubectl debug + netshoot 是最方便的组合——netshoot 镜像预装了 tcpdump、dig、curl、ss 等全套网络工具,而 --target 让它共享目标容器的网络命名空间。

抓 K8s 的 DNS 问题时(对应 网络-11 的 ndots 陷阱):

# 在 Pod 里抓 DNS 查询,能直接看到 search 后缀导致的多次无效查询
tcpdump -nn -i any udp port 53

会清楚看到 api.github.com.default.svc.cluster.local 这样的拼接查询连续返回 NXDOMAIN——这就是 ndots 问题的直接证据

7. 本章面试题

tcpdump 抓包时哪两个参数最重要?为什么?

-nn-w

  • -nn 不解析域名和端口名。不加会因反向 DNS 查询卡住,而且抓包动作自身产生的 DNS 流量会污染结果
  • -w file.pcap 写入文件,带回本地用 Wireshark 分析。命令行看 TCP 流非常痛苦。

另外 not port 22 几乎必加——否则会抓到自己 SSH 会话的回显,形成噪音甚至反馈循环。

生产环境还要用 -C/-W/-G 限制文件大小和数量,避免写满磁盘。

抓包过滤器和显示过滤器有什么区别?
抓包过滤器 显示过滤器
语法 BPF(同 tcpdump) Wireshark 自有语法
时机 抓的时候就丢弃,不可恢复 抓完后筛选显示
例子 port 80 tcp.port == 80

语法完全不同,写错了要么报错要么不生效。抓包过滤器在内核层面(BPF)执行,效率高但丢掉的包找不回来;显示过滤器可以随时改。

线上报 "Connection reset by peer",怎么定位?

抓 RST 包:tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'

关键是判断 RST 是谁发的

  • 服务端发的 → 端口未监听、应用崩溃、backlog 满、或 close() 时还有未读数据;
  • 客户端发的 → 超时放弃、连接池丢弃连接;
  • 中间设备发的 → 防火墙拦截、LB 空闲超时。特征是双方都说不是自己发的

两端同时抓包是定位中间设备问题的唯一可靠方法——客户端抓到 RST 但服务端没发过,就说明来自中间。

小请求正常、大请求卡死,可能是什么问题?怎么验证?

典型的 MTU 黑洞,常见于 VPN、隧道、跨云专线。

原因:路径上某段 MTU 偏小,大包带 DF 标志无法分片,路由器丢弃并回 ICMP “需要分片”——但防火墙把这个 ICMP 拦了,发送方永远收不到通知,只能一直重传同样大小的包。

验证:用禁止分片的 ping 逐步减小包大小找临界值:

ping -M do -s 1472 8.8.8.8    # Linux,1472+28=1500
ping -D  -s 1472 8.8.8.8      # macOS

1472 失败而 1420 成功即可确认。处置:调小 MTU,或网关上做 MSS clamping(--clamp-mss-to-pmtu)。

Wireshark 里看到 Win=0 说明什么?

零窗口——接收方的接收缓冲区满了,在告诉发送方"别发了,我处理不过来"。

这是一个强信号:说明接收方应用读取数据的速度跟不上,瓶颈在对端的处理能力,不在网络链路。看到它就不必再查网络了,去查对端应用为什么消费得慢。

之后会看到 TCP Window Update 表示缓过来了。原理见 网络-09

抓到的 HTTPS 流量怎么解密?为什么有服务器私钥也不一定行?

推荐方法:SSLKEYLOGFILE。设置环境变量让客户端导出会话密钥,Wireshark 里配置该文件路径即可自动解密。对 TLS 1.3 同样有效,因为导出的是会话密钥。Go 里用 tls.Config.KeyLogWriter仅限调试,绝不能上生产)。

为什么私钥不一定行:只有 RSA 密钥交换时私钥才能解出会话密钥。而 ECDHE 和 TLS 1.3 具备前向保密——会话密钥从未在网络上传输,有私钥也解不出来。

这正是前向保密的代价之一:抓包调试变难,反过来也印证了它的安全价值。

K8s 里怎么给 Pod 抓包?

容器镜像通常不含 tcpdump 且无 NET_ADMIN 权限。推荐用 kubectl debug

kubectl debug -it <pod> --image=nicolaka/netshoot --target=<容器名>

netshoot 预装 tcpdump、dig、curl、ss 全套工具,--target 让它共享目标容器的网络命名空间,所以看到的就是目标容器的流量。

其他方式:宿主机上用 nsenter 进入容器的 netns;或镜像自带 tcpdump 时直接 kubectl exec 抓完再 kubectl cp 出来。