81、libpcap实战:实现一个简单的抓包程序、协议分析
网络抓包,说白了就是“偷听”网线上跑的数据。你想想看,数据包在网络上飞来飞去,我们能不能截获它们?能。libpcap 就是干这个的。我最早接触 libpcap 是在做网络监控项目的时候,那时候需要分析公司内网的流量异常。嗯,这玩意儿确实好用,但坑也不少。今天我们就手撸一个简单的抓包程序,顺带做点协议分析。
libpcap 是什么?
libpcap 是一个跨平台的网络数据包捕获库。它在 Linux 上叫 libpcap,Windows 上叫 WinPcap 或 Npcap。核心功能就两个:
- 从网卡上抓取原始数据包
- 按过滤规则筛选数据包(比如只抓 TCP 包)
我个人习惯把 libpcap 比作“网络显微镜”。没有它,你看到的只是应用层的数据;有了它,你能看到链路层、网络层、传输层的每一个比特。
核心概念:libpcap 工作在数据链路层,所以你能拿到完整的以太网帧头。这是它和 socket 编程最大的区别——socket 拿到的已经是剥掉链路层头部的数据了。
抓包程序的基本流程
写一个抓包程序,其实就四步:
- 查找设备:找到可用的网络接口(比如 eth0、wlan0)
- 打开设备:设置混杂模式,开始捕获
- 设置过滤:用 BPF(伯克利包过滤器)语法指定要抓的包
- 循环抓取:不断从网卡读取数据包,然后分析
我刚开始写的时候,总想着一步到位。结果呢?设备没打开就调过滤,直接段错误。所以啊,每一步都要检查返回值,别偷懒。
代码实战:一个简单的抓包程序
下面这个程序会抓取 10 个 TCP 数据包,并打印出源 IP、目的 IP、源端口、目的端口。
#include <pcap.h>
#include <stdio.h>
#include <stdlib.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;
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);
// 只处理 IPv4 + TCP
if (ntohs(eth->ether_type) != ETHERTYPE_IP) return;
if (ip_hdr->ip_p != IPPROTO_TCP) return;
printf("源IP: %s -> 目的IP: %s\n", inet_ntoa(ip_hdr->ip_src), inet_ntoa(ip_hdr->ip_dst));
printf("源端口: %d -> 目的端口: %d\n", ntohs(tcp_hdr->th_sport), ntohs(tcp_hdr->th_dport));
printf("数据包长度: %d 字节\n\n", pkthdr->len);
}
int main() {
char errbuf[PCAP_ERRBUF_SIZE];
pcap_t *handle;
struct bpf_program fp;
char filter_exp[] = "tcp"; // 只抓 TCP 包
bpf_u_int32 net;
// 1. 查找设备
char *dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "找不到设备: %s\n", errbuf);
return 1;
}
printf("使用设备: %s\n", dev);
// 2. 打开设备(混杂模式)
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "打开设备失败: %s\n", errbuf);
return 1;
}
// 3. 编译并设置过滤规则
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "编译过滤规则失败\n");
return 1;
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "设置过滤规则失败\n");
return 1;
}
// 4. 循环抓取 10 个包
printf("开始抓包...\n");
pcap_loop(handle, 10, packet_handler, NULL);
// 清理
pcap_freecode(&fp);
pcap_close(handle);
return 0;
}
提示:编译时需要链接 libpcap 库:gcc -o sniffer sniffer.c -lpcap。如果你在 Linux 上跑,记得用 root 权限,否则打不开设备。
协议分析:从原始字节到结构化数据
抓到的数据包是一堆字节流。我们需要按协议格式一层层解析。以太网帧的结构是这样的:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 目的 MAC | 6 | 接收方 MAC 地址 |
| 源 MAC | 6 | 发送方 MAC 地址 |
| 类型 | 2 | 0x0800 表示 IPv4,0x0806 表示 ARP |
| 数据 | 46-1500 | 上层协议数据 |
解析的时候,我们直接从 packet 指针开始偏移。以太网头固定 14 字节,然后根据类型决定下一步怎么走。如果是 IPv4,就继续解析 IP 头。IP 头长度不固定(因为有选项字段),所以要用 ip_hl * 4 来算。
我曾经在解析 IP 头时犯过一个低级错误:直接用了 sizeof(struct ip) 来偏移。结果呢?有些包带了 IP 选项,偏移量算错了,后面解析 TCP 端口全是乱码。嗯,从那以后我再也不敢忽略 ip_hl 了。
BPF 过滤规则:精准抓包
BPF 是 libpcap 的灵魂。你可以用一套简洁的语法告诉内核:我只想要哪些包。常见的规则有:
tcp:只抓 TCP 包udp port 53:只抓 DNS 查询host 192.168.1.1:只抓与某个 IP 通信的包tcp and src port 80:源端口是 80 的 TCP 包
BPF 规则在编译后会生成一段字节码,内核会用它来快速过滤。效率非常高,比在用户态自己判断快得多。
注意:BPF 规则是大小写敏感的。写 TCP 和写 tcp 结果不一样。我刚开始就踩过这个坑,写了个 TCP,结果一个包都没抓到,还以为是网卡坏了。
抓包程序的完整流程
下面这张图展示了从设备查找到数据包解析的完整流程:
避坑指南
写抓包程序,有几个坑我不得不提:
- 权限问题:libpcap 需要 root 权限才能打开设备。如果你在普通用户下跑,会返回权限错误。我曾经在调试时忘了切 root,折腾了半小时才发现。
- 字节序:网络字节序是大端,而 x86 机器是小端。记得用
ntohs()和ntohl()转换。我见过有人直接打印端口号,结果 80 变成了 20480。 - IP 头长度:别用
sizeof(struct ip)来偏移,要用ip_hdr->ip_hl * 4。因为 IP 头可能有选项,长度不固定。 - 内存对齐:从 packet 指针强制转换结构体指针时,有些架构要求对齐访问。x86 上没问题,但 ARM 上可能会段错误。稳妥的做法是用
memcpy拷贝到对齐的结构体中。
调试技巧:如果你抓不到包,先用 tcpdump -i eth0 确认网卡上确实有流量。如果 tcpdump 能抓到,你的程序抓不到,那多半是过滤规则写错了。
总结
libpcap 是个强大的工具,但用起来也不复杂。记住四个步骤:找设备、开设备、设过滤、循环抓。解析协议时,一层层剥开,从以太网头到 IP 头再到 TCP/UDP 头。嗯,说白了就是“剥洋葱”。
我个人的经验是:先写一个最简单的版本,只打印原始字节。确认能抓到包之后,再慢慢加解析逻辑。别一上来就想搞个完整的协议分析器,那样容易把自己绕晕。