81、libpcap实战:实现一个简单的抓包程序、协议分析

网络抓包,说白了就是“偷听”网线上跑的数据。你想想看,数据包在网络上飞来飞去,我们能不能截获它们?能。libpcap 就是干这个的。我最早接触 libpcap 是在做网络监控项目的时候,那时候需要分析公司内网的流量异常。嗯,这玩意儿确实好用,但坑也不少。今天我们就手撸一个简单的抓包程序,顺带做点协议分析。

libpcap 是什么?

libpcap 是一个跨平台的网络数据包捕获库。它在 Linux 上叫 libpcap,Windows 上叫 WinPcap 或 Npcap。核心功能就两个:

  • 从网卡上抓取原始数据包
  • 按过滤规则筛选数据包(比如只抓 TCP 包)

我个人习惯把 libpcap 比作“网络显微镜”。没有它,你看到的只是应用层的数据;有了它,你能看到链路层、网络层、传输层的每一个比特。

核心概念:libpcap 工作在数据链路层,所以你能拿到完整的以太网帧头。这是它和 socket 编程最大的区别——socket 拿到的已经是剥掉链路层头部的数据了。

抓包程序的基本流程

写一个抓包程序,其实就四步:

  1. 查找设备:找到可用的网络接口(比如 eth0、wlan0)
  2. 打开设备:设置混杂模式,开始捕获
  3. 设置过滤:用 BPF(伯克利包过滤器)语法指定要抓的包
  4. 循环抓取:不断从网卡读取数据包,然后分析

我刚开始写的时候,总想着一步到位。结果呢?设备没打开就调过滤,直接段错误。所以啊,每一步都要检查返回值,别偷懒。

代码实战:一个简单的抓包程序

下面这个程序会抓取 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 抓包流程 1. 查找设备 pcap_lookupdev() 2. 打开设备 pcap_open_live() 3. 设置过滤 pcap_compile() + pcap_setfilter() 4. 循环抓取数据包 pcap_loop() 或 pcap_next() 5. 协议解析 以太网头 → IP头 → TCP/UDP头 → 应用层数据

避坑指南

写抓包程序,有几个坑我不得不提:

  • 权限问题: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 头。嗯,说白了就是“剥洋葱”。

我个人的经验是:先写一个最简单的版本,只打印原始字节。确认能抓到包之后,再慢慢加解析逻辑。别一上来就想搞个完整的协议分析器,那样容易把自己绕晕。

公众号:蓝海资料掘金营,微信 deep3321