80、网络抓包分析:libpcap库使用、BPF过滤规则、数据包解析
网络抓包,说白了就是“偷听”网线上跑的数据。你想想看,数据包在网络上飞来飞去,我们能不能截下来看看里面到底装了啥?
我个人习惯用 libpcap 来做这件事。它是 Linux/Unix 下最经典的抓包库,tcpdump 和 Wireshark 的底层都是它。Windows 上对应的叫 WinPcap 或 Npcap,API 基本一样。
今天我们就来聊聊怎么用 libpcap 抓包、怎么用 BPF 过滤规则、以及怎么解析数据包。嗯,这里要注意,不是让你去搞黑客攻击,而是做网络诊断和协议分析。
libpcap 的核心流程
libpcap 的使用套路其实很固定,我总结为四步走:
- 打开网络设备 —— 找到你要监听的网卡
- 设置过滤规则 —— 只抓你关心的包(比如只抓 HTTP 流量)
- 开始抓包循环 —— 不断从网卡读取数据包
- 解析数据包 —— 从以太网帧头一直解到应用层
我在项目中遇到过一个问题:刚上手时直接抓所有包,结果一秒几万个包,程序直接卡死。后来才明白,过滤规则是抓包的灵魂。
BPF 过滤规则
BPF(Berkeley Packet Filter)是 libpcap 的过滤语言。你写的过滤表达式会被编译成字节码,在内核里直接执行,效率极高。
常用的 BPF 表达式:
| 表达式 | 含义 |
|---|---|
tcp |
只抓 TCP 包 |
udp |
只抓 UDP 包 |
port 80 |
源或目的端口为 80 |
host 192.168.1.1 |
源或目的 IP 为 192.168.1.1 |
tcp and port 443 |
TCP 且端口为 443(HTTPS) |
not arp |
排除 ARP 包 |
我曾经踩过一个坑:用 tcp port 80 过滤,结果发现抓不到包。排查了半天,原来是网卡开启了 混杂模式 但没设置对。libpcap 默认不开启混杂模式,你得手动调用 pcap_set_promisc()。
代码实战:用 libpcap 抓包
下面是一个完整的抓包示例。它抓取所有 TCP 包,并打印出源 IP、目的 IP 和端口。
#include <pcap.h>
#include <stdio.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <net/ethernet.h>
void packet_handler(u_char *user, const struct pcap_pkthdr *pkthdr, const u_char *packet) {
struct ether_header *eth = (struct ether_header *)packet;
if (ntohs(eth->ether_type) != ETHERTYPE_IP) return;
struct ip *ip_hdr = (struct ip *)(packet + sizeof(struct ether_header));
struct tcphdr *tcp_hdr = (struct tcphdr *)(packet + sizeof(struct ether_header) + ip_hdr->ip_hl * 4);
printf("Src: %s:%d -> Dst: %s:%d\n",
inet_ntoa(ip_hdr->ip_src), ntohs(tcp_hdr->th_sport),
inet_ntoa(ip_hdr->ip_dst), ntohs(tcp_hdr->th_dport));
}
int main() {
char errbuf[PCAP_ERRBUF_SIZE];
pcap_t *handle;
// 1. 打开网络设备
char *dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "找不到设备: %s\n", errbuf);
return 1;
}
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "打开设备失败: %s\n", errbuf);
return 1;
}
// 2. 编译并设置 BPF 过滤规则
struct bpf_program fp;
char filter_exp[] = "tcp";
if (pcap_compile(handle, &fp, filter_exp, 0, PCAP_NETMASK_UNKNOWN) == -1) {
fprintf(stderr, "编译过滤规则失败\n");
return 1;
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "设置过滤规则失败\n");
return 1;
}
printf("开始抓包,按 Ctrl+C 停止...\n");
// 3. 进入抓包循环
pcap_loop(handle, 10, packet_handler, NULL);
pcap_close(handle);
return 0;
}
这段代码我用了很多次,基本是抓包程序的“骨架”。你只需要改 packet_handler 里的解析逻辑,就能抓各种协议。
-lpcap 链接库。如果你在 Ubuntu 上,先 sudo apt install libpcap-dev。Windows 用户需要安装 WinPcap 开发包,然后链接 wpcap.lib。
数据包解析:从比特到协议
数据包解析说白了就是“剥洋葱”。从最外层的以太网帧头开始,一层层往里剥:
- 以太网帧头(14 字节):包含源 MAC、目的 MAC、上层协议类型
- IP 头(20 字节起):包含源 IP、目的 IP、协议类型(TCP/UDP/ICMP)
- TCP/UDP 头:包含源端口、目的端口
- 应用层数据:比如 HTTP 请求、DNS 查询
我刚开始做网络编程时,总是搞不清 IP 头的长度字段。IP 头长度是 ip_hl,单位是 4 字节。所以实际长度是 ip_hl * 4。这个坑我踩过不止一次,解析出来的数据全是乱的。
下面这张图展示了数据包的封装结构:
解析 HTTP 请求
光解析 TCP 头还不够,我们经常要看应用层数据。比如抓 HTTP 请求,数据就在 TCP 载荷里。
假设我们已经拿到了 TCP 头后面的数据,直接把它当字符串打印出来:
// 在 packet_handler 中,获取 TCP 载荷
u_char *payload = (u_char *)(tcp_hdr + 1);
int payload_len = pkthdr->len - (sizeof(struct ether_header) + ip_hdr->ip_hl * 4 + tcp_hdr->th_off * 4);
if (payload_len > 0) {
printf("HTTP 数据:\n%.*s\n", payload_len, payload);
}
这里有个细节:TCP 头的长度是 th_off,单位也是 4 字节。所以 TCP 头实际长度是 th_off * 4。我见过有人直接用 sizeof(struct tcphdr),但 TCP 头可能有选项字段,长度不固定。用 th_off 才是正确的。
避坑指南
我曾经在抓包时遇到一个诡异的问题:程序跑得好好的,突然抓不到包了。查了半天,发现是网卡被其他程序占用了。libpcap 默认只允许一个程序打开同一个网卡。解决办法是用 pcap_open_live 的最后一个参数设置 pcap_set_immediate_mode(),或者用 pcap_create() + pcap_activate() 分步打开。
另一个常见坑是 字节序。网络字节序是大端,而 x86 机器是小端。所以端口号、IP 地址都要用 ntohs()、ntohl() 转换。我刚开始写时忘了转换,打印出来的端口号都是 256 的倍数,一看就不对劲。
嗯,最后说一句:libpcap 抓包是网络编程的基本功。你掌握了它,就能自己写一个迷你版的 Wireshark。别怕代码多,多练几次就熟了。