网络-21 抓包实战:tcpdump 与 Wireshark
前面所有篇章讲的都是"协议应该怎么工作"。这一篇讲怎么亲眼看到它在工作——以及出问题时怎么从包里找出真相。
分工很明确:服务器上用 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 从包里认出常见事件
① 三次握手
[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 出来。
xingliuhua