tcpdump 的 BPF 过滤语法:从入门到精通
前言
在网络排障、安全分析和日常运维中,tcpdump 是最常用的网络抓包工具之一。它的强大之处不仅在于能抓包,更在于它内置了一套极其灵活的过滤语言 —— BPF(Berkeley Packet Filter)语法。这套语法让你可以像编写查询语句一样,精确地告诉 tcpdump 你需要什么样的数据包。
本文将从零开始,由浅入深地系统讲解这套语法,让你读完就能上手操作,并能理解那些看起来很复杂的过滤表达式。
一、认识 BPF
1.1 什么是 BPF?
BPF 全称 Berkeley Packet Filter,直译为”伯克利包过滤器”。它最初由 Steven McCanne 和 Van Jacobson 在 1992 年提出,目的是为 Unix 系统提供一种高效的数据包捕获机制。
我们可以通过一个生活化的场景来理解它:
你站在一条高速公路上,每分钟有上万辆车通过。你只想记录红色的小轿车,但如果你把每辆车都拦下来看颜色,交通就瘫痪了。
BPF 就相当于在高速公路入口安装了一个智能摄像头:你告诉它规则,它在车辆通过时实时判断,只有符合条件的车辆才会被记录,不匹配的直接放过,不影响正常交通。
在 Linux 系统中,BPF 工作在网络协议栈的最底层。当数据包到达网卡时,内核会先执行你编写的 BPF 过滤规则,只有通过过滤的包才会被复制一份交给 tcpdump 等用户态程序。这种在内核层面就完成过滤的机制,极大地提高了抓包效率。
1.2 BPF 与 tcpdump 的关系
tcpdump 本身是一个用户态程序,它依赖 libpcap 库来实现抓包功能。当我们执行 tcpdump 并传入过滤表达式时,整个流程是这样的:
tcpdump将你输入的文本表达式交给libpcap库libpcap调用pcap_compile()函数,将文本表达式编译成 BPF 虚拟机的字节码指令- 这些字节码通过
pcap_setfilter()被注入到内核中的 BPF 虚拟机 - 内核在网络层对每个数据包执行这些指令
- 只有匹配成功的包才会被复制到用户态的
tcpdump
这套机制的核心优势在于:过滤逻辑在内核中执行,避免了大量无关数据从内核到用户态的拷贝开销。
二、BPF 语法基础
2.1 三个核心限定词
BPF 过滤表达式由一个个”原语”(Primitive)通过逻辑运算符组合而成。每个原语通常包含一个标识符(如 IP 地址、端口号)和若干个限定词(Qualifier)来精确描述这个标识符的含义。
限定词分为三类:
类型限定词(Type)
指明标识符的性质是什么。
| 关键词 | 含义 | 示例 |
|---|---|---|
host | 表示一个 IP 地址或主机名 | host 192.168.1.1 |
net | 表示一个网段 | net 192.168.0.0/16 |
port | 表示一个端口号 | port 80 |
portrange | 表示一个端口范围 | portrange 8000-9000 |
方向限定词(Dir)
指明数据包的传输方向。
| 关键词 | 含义 | 示例 |
|---|---|---|
src | 数据包来源 | src host 192.168.1.1 |
dst | 数据包目标 | dst port 80 |
| 不指定 | 默认为双向 | port 80 表示源或目标为 80 端口 |
协议限定词(Proto)
指明数据包的网络协议类型。
| 关键词 | 含义 |
|---|---|
tcp | TCP 协议 |
udp | UDP 协议 |
icmp | ICMP 协议(ping 命令使用) |
arp | ARP 协议(局域网地址解析) |
ip | IP 协议(所有 IP 包) |
ip6 | IPv6 协议 |
ether | 以太网帧 |
2.2 逻辑运算符
多个条件可以通过逻辑运算符组合成更复杂的表达式。
| 运算符 | 含义 | 示例 |
|---|---|---|
and 或 && | 逻辑与,所有条件都满足 | host 1.1.1.1 and port 80 |
or 或 || | 逻辑或,至少一个条件满足 | port 80 or port 443 |
not 或 ! | 逻辑非,排除某些条件 | not port 22 |
括号 () 可以用来明确运算的优先级:
tcpdump 'host 1.1.1.1 and (port 80 or port 443)'
⚠️ 重要提醒:在 shell 命令行中使用括号时,必须用单引号将整个表达式包起来,否则 shell 会误解括号的含义。
2.3 基础实战演练
下面从最简单的例子开始,逐步增加复杂度:
# 1. 最简单的抓包(无过滤)
tcpdump -i eth0
# 2. 只看特定主机的所有流量
tcpdump host 192.168.1.100
# 3. 只看特定端口的流量(HTTP)
tcpdump port 80
# 4. 只看从某主机发出的流量
tcpdump src host 192.168.1.100
# 5. 只看发给某个端口的流量
tcpdump dst port 443
# 6. 组合条件:某主机访问网页的流量
tcpdump 'host 192.168.1.100 and (port 80 or port 443)'
# 7. 排除干扰:不看自己的 SSH 连接
tcpdump not port 22
# 8. 只看 TCP 协议
tcpdump tcp
# 9. 只看网段内的所有流量
tcpdump net 192.168.0.0/16
这些例子涵盖了大部分日常运维场景。掌握了以上内容,你已经可以应对 80% 的抓包需求了。
三、高级过滤技术
3.1 理解数据包的结构
要使用高级过滤,首先需要理解数据包在内存中的存储方式。对计算机来说,网络数据包就是一串连续的字节,像这样:
45 00 00 3C 1A 2B 40 00 40 06 8A 9C C0 A8 01 64 ...
我们可以通过 协议[偏移量] 的方式来访问这串字节中的特定位置。这就像访问数组元素一样:packet[0]、packet[1]……每个字节都有唯一的偏移位置。
不同的网络协议规定了各自头部各字段的位置:
IP 头部结构(关键字段)
| 偏移(字节) | 字段含义 |
|---|---|
| 0 | 版本号 + 头部长度 |
| 8 | TTL(Time To Live,生存时间) |
| 9 | 上层协议类型(6=TCP,17=UDP,1=ICMP) |
| 12-15 | 源 IP 地址 |
| 16-19 | 目的 IP 地址 |
TCP 头部结构(关键字段)
| 偏移(字节) | 字段含义 |
|---|---|
| 0-1 | 源端口 |
| 2-3 | 目的端口 |
| 4-7 | 序列号 |
| 8-11 | 确认号 |
| 12 | 数据偏移 |
| 13 | 标志位(Flags) |
| 14-15 | 窗口大小 |
3.2 访问特定字节
基本的访问格式是:协议[偏移量]
# 检查 IP 头部的 TTL 字段(第 8 个字节)
tcpdump 'ip[8] < 10'
# 检查 IP 头部协议字段(第 9 个字节),6 表示 TCP
tcpdump 'ip[9] = 6'
# 检查 ICMP 头部类型字段(第 0 个字节),8 表示 ping 请求
tcpdump 'icmp[0] = 8'
如果需要访问连续多个字节,使用 协议[偏移量:长度] 格式:
# TCP 源端口是 2 个字节,偏移 0 开始
tcpdump 'tcp[0:2] = 80'
# TCP 目的端口是 2 个字节,偏移 2 开始
tcpdump 'tcp[2:2] = 443'
# HTTP 请求行开头 4 个字节等于 "GET "
tcpdump 'tcp[20:4] = 0x47455420'
3.3 TCP 标志位的深入理解
TCP 标志位是高级过滤中最常见的应用场景。TCP 头部第 13 个字节包含了 8 个标志位:
二进制位: 7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
| CWR | ECE | URG | ACK | PSH | RST | SYN | FIN |
+-----+-----+-----+-----+-----+-----+-----+-----+
每个标志位对应一个比特(bit)。tcpdump 预定义了以下常量来帮助我们检查这些标志位:
| 常量 | 十六进制 | 二进制 | 含义 |
|---|---|---|---|
tcp-fin | 0x01 | 00000001 | 关闭连接 |
tcp-syn | 0x02 | 00000010 | 建立连接 |
tcp-rst | 0x04 | 00000100 | 重置连接 |
tcp-push | 0x08 | 00001000 | 立即推送数据 |
tcp-ack | 0x10 | 00010000 | 确认数据 |
tcp-urg | 0x20 | 00100000 | 紧急数据 |
检查标志位使用按位与(&)操作:
# 抓所有 SYN 包(包括 SYN-ACK)
tcpdump 'tcp[tcpflags] & tcp-syn != 0'
# 抓纯 SYN 包(不包括 SYN-ACK):SYN=1 且 ACK=0
tcpdump 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
# 抓 RST 包(连接重置)
tcpdump 'tcp[tcpflags] & tcp-rst != 0'
# 抓 FIN 包(关闭连接)
tcpdump 'tcp[tcpflags] & tcp-fin != 0'
理解按位与:
tcp[tcpflags] & tcp-syn != 0的意思是”把标志位字节和 SYN 的二进制值做按位与运算,结果不为 0 说明 SYN 位是 1″。这就像一个灯泡开关,你要检查某个开关是否打开。
3.4 比较运算和数值匹配
除了检查标志位,你还可以用各种比较运算符来匹配数值:
| 运算符 | 含义 |
|---|---|
= 或 == | 等于 |
!= | 不等于 |
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
# 抓取包大小大于 1500 字节的包(可能涉及巨帧)
tcpdump greater 1500
# 抓取包大小小于 64 字节的包
tcpdump less 64
# 抓取 TTL 小于等于 10 的包
tcpdump 'ip[8] <= 10'
# 抓取 ICMP 类型为 8(ping 请求)或 0(ping 回复)的包
tcpdump 'icmp[0] = 8 or icmp[0] = 0'
3.5 十六进制值的使用技巧
在很多情况下,我们需要用十六进制来表示特定的字节模式。常用的 ASCII 字符与十六进制对应关系:
| 字符串 | 十六进制 |
|---|---|
GET(四个字符:G E T 空格) | 0x47455420 |
POST | 0x504f5354 |
HTTP | 0x48545450 |
200 | 0x323030 |
404 | 0x343034 |
# 抓 HTTP GET 请求(数据开头是 "GET ")
tcpdump -A 'tcp port 80 and tcp[20:4] = 0x47455420'
# 抓 HTTP POST 请求
tcpdump -A 'tcp port 80 and tcp[20:4] = 0x504f5354'
# 抓 HTTP 响应码为 200 的包(偏移量可能需要调整)
tcpdump -A 'tcp port 80 and tcp[20:4] = 0x323030'
3.6 网卡级别的过滤
你还可以在以太网帧层面进行过滤:
# 抓特定源 MAC 地址的包
tcpdump 'ether src 00:11:22:33:44:55'
# 抓特定目标 MAC 地址的包
tcpdump 'ether dst aa:bb:cc:dd:ee:ff'
# 抓特定 MAC 地址的双向流量
tcpdump 'ether host 00:11:22:33:44:55'
# 抓 VLAN 标签(802.1Q)为 100 的包
tcpdump 'vlan 100'
# 抓 MPLS 标签的包
tcpdump 'mpls'
3.7 协议栈嵌套场景
在实际网络中,数据包经常是多层协议嵌套的(例如以太网 → IP → TCP → HTTP)。BPF 允许你处理这种嵌套:
# 抓 IPv6 的 TCP 包
tcpdump 'ip6 proto 6'
# 抓 IPv6 的 ICMP 包
tcpdump 'ip6 proto 58'
# 抓非 IP 包(如 ARP)
tcpdump 'not ip'
# 抓 IP 但非 TCP 非 UDP 的包
tcpdump 'ip and not tcp and not udp'
第四部分:完整参考速查表
4.1 常用过滤器速查
| 需求 | 表达式 |
|---|---|
| 特定 IP | host 10.0.0.1 |
| 特定源 IP | src host 10.0.0.1 |
| 特定目标 IP | dst host 10.0.0.1 |
| 特定网段 | net 192.168.0.0/16 |
| 特定端口 | port 80 |
| 多个端口 | port 80 or port 443 |
| 端口范围 | portrange 8000-9000 |
| 特定协议 | tcp / udp / icmp |
| 排除端口 | not port 22 |
| 包大小 | greater 1000 / less 64 |
| MAC 地址 | ether host 00:11:22:33:44:55 |
| VLAN | vlan 100 |
| TCP SYN | tcp[tcpflags] & tcp-syn != 0 |
| TCP ACK | tcp[tcpflags] & tcp-ack != 0 |
| TCP RST | tcp[tcpflags] & tcp-rst != 0 |
| TCP FIN | tcp[tcpflags] & tcp-fin != 0 |
4.2 常用偏移量速查
| 表达式 | 含义 |
|---|---|
ip[8] | IP 头部的 TTL 字段 |
ip[9] | IP 头部的协议字段(6=TCP,17=UDP) |
ip[12:4] | 源 IP 地址 |
ip[16:4] | 目的 IP 地址 |
tcp[0:2] | TCP 源端口 |
tcp[2:2] | TCP 目的端口 |
tcp[tcpflags] 或 tcp[13] | TCP 标志位 |
udp[0:2] | UDP 源端口 |
udp[2:2] | UDP 目的端口 |
icmp[0] | ICMP 类型(8=请求,0=回复) |
五、tcpdump 常用选项
除了过滤表达式,tcpdump 本身还有大量实用选项,这里整理最常用的一部分:
| 选项 | 含义 | 示例 |
|---|---|---|
-i | 指定网络接口 | -i eth0 |
-c | 抓取指定数量的包后退出 | -c 100 |
-n | 不解析主机名(加速) | -n |
-nn | 不解析主机名和端口名 | -nn |
-v / -vv / -vvv | 输出详细信息 | -vv |
-s | 抓取每个包的前 N 个字节 | -s 0(抓完整包) |
-w | 写入文件(pcap 格式) | -w capture.pcap |
-r | 从文件读取 | -r capture.pcap |
-A | 以 ASCII 格式打印数据 | -A |
-X | 以十六进制和 ASCII 打印 | -X |
-e | 打印链路层头信息 | -e |
-q | 精简输出 | -q |
-D | 列出所有可用网卡 | -D |
六、实用案例合集
6.1 安全分析场景
# 1. 检测 SYN 洪水攻击(大量 SYN 包)
tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' | wc -l
# 2. 检测端口扫描(大量来自同一 IP 的 SYN)
tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0' -c 1000
# 3. 查看 DNS 请求(UDP 53 端口)
tcpdump -i eth0 'udp port 53'
# 4. 检查 ARP 欺骗(异常的 ARP 响应)
tcpdump -i eth0 'arp and arp[7] = 2'
6.2 应用排查场景
# 1. 详细查看 HTTP 交互
tcpdump -i eth0 -A 'tcp port 80 and (tcp[20:4] = 0x47455420 or tcp[20:4] = 0x504f5354)'
# 2. 查看 HTTPS 连接建立
tcpdump -i eth0 'tcp port 443 and tcp[tcpflags] & tcp-syn != 0'
# 3. 查看 MySQL 数据库查询(3306 端口)
tcpdump -i eth0 -A 'port 3306'
# 4. 跟踪某个请求的完整 TCP 会话
tcpdump -i eth0 'host 192.168.1.100 and port 80' -c 100
6.3 网络诊断场景
# 1. 查看 TTL 异常低的包
tcpdump -i eth0 'ip[8] < 5'
# 2. 查看重传包(重复的 ACK)
tcpdump -i eth0 'tcp[tcpflags] & tcp-ack != 0 and tcp[8:4] = tcp[4:4]'
# 3. 查看 ICMP 错误消息
tcpdump -i eth0 'icmp[0] = 3 or icmp[0] = 11'
# 4. 查看 DHCP 交互
tcpdump -i eth0 'udp port 67 or udp port 68'
七、常见问题与排障
7.1 抓不到包的排查步骤
- 确认网卡名称:
tcpdump -D列出所有接口 - 检查网卡是否启用:
ip link show - 检查权限:大多数情况下需要
sudo - 不加过滤器试试:
sudo tcpdump -i eth0 -c 10 - 确认流量确实存在:用
ping测试
7.2 命令报错的解决方法
| 错误信息 | 解决方案 |
|---|---|
Permission denied | 使用 sudo 运行 |
No such device exists | 网卡名写错了,用 -D 查看正确的名称 |
syntax error | 检查括号是否配对,是否用引号包住了表达式 |
pcap_compile: not supported | 某些 BPF 语法在旧内核中不支持 |
7.3 性能优化建议
- 尽量使用 -n 选项:禁止 DNS 反向解析,显著提升速度
- 在内核过滤:BPF 在内核执行,比
grep输出快得多 - 慎用 -A 和 -X:打印数据内容会增加 CPU 开销
- 限制抓包数量:使用
-c参数自动退出
结语
BPF 过滤语法虽然看起来复杂,但它的核心逻辑非常清晰:通过类型、方向、协议三个维度定位数据包,通过逻辑运算组合多个条件,通过字节偏移实现精细化控制。
在实际使用中,大部分场景只需要基础语法就够了。高级的字节偏移技巧可以作为”备用武器”,遇到复杂问题时才拿出来使用。建议从简单的过滤开始,逐步尝试更复杂的组合,最终你会发现自己已经能熟练地写出各种过滤表达式了。