80、网络抓包分析:libpcap库使用、BPF过滤规则、数据包解析

网络抓包,说白了就是“偷听”网线上跑的数据。你想想看,数据包在网络上飞来飞去,我们能不能截下来看看里面到底装了啥?

我个人习惯用 libpcap 来做这件事。它是 Linux/Unix 下最经典的抓包库,tcpdump 和 Wireshark 的底层都是它。Windows 上对应的叫 WinPcap 或 Npcap,API 基本一样。

今天我们就来聊聊怎么用 libpcap 抓包、怎么用 BPF 过滤规则、以及怎么解析数据包。嗯,这里要注意,不是让你去搞黑客攻击,而是做网络诊断和协议分析。

libpcap 的核心流程

libpcap 的使用套路其实很固定,我总结为四步走:

  1. 打开网络设备 —— 找到你要监听的网卡
  2. 设置过滤规则 —— 只抓你关心的包(比如只抓 HTTP 流量)
  3. 开始抓包循环 —— 不断从网卡读取数据包
  4. 解析数据包 —— 从以太网帧头一直解到应用层

我在项目中遇到过一个问题:刚上手时直接抓所有包,结果一秒几万个包,程序直接卡死。后来才明白,过滤规则是抓包的灵魂

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

数据包解析:从比特到协议

数据包解析说白了就是“剥洋葱”。从最外层的以太网帧头开始,一层层往里剥:

  1. 以太网帧头(14 字节):包含源 MAC、目的 MAC、上层协议类型
  2. IP 头(20 字节起):包含源 IP、目的 IP、协议类型(TCP/UDP/ICMP)
  3. TCP/UDP 头:包含源端口、目的端口
  4. 应用层数据:比如 HTTP 请求、DNS 查询

我刚开始做网络编程时,总是搞不清 IP 头的长度字段。IP 头长度是 ip_hl,单位是 4 字节。所以实际长度是 ip_hl * 4。这个坑我踩过不止一次,解析出来的数据全是乱的。

下面这张图展示了数据包的封装结构:

数据包封装结构(从外到内) 以太网帧头(14 字节) 目的MAC(6) | 源MAC(6) | 类型(2) IP 头(20 字节起) 版本(4) | 头长(4) | 服务类型(8) | 总长度(16) | ... TCP 头(20 字节起) 源端口(16) | 目的端口(16) | 序号(32) | ... 应用层数据(HTTP/DNS/...) GET /index.html HTTP/1.1 ... 第1层 第2层 第3层 第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 才是正确的。

核心要点: 解析数据包时,每一步都要小心“偏移量”。以太网头固定 14 字节,IP 头长度看 ip_hl,TCP 头长度看 th_off。一步算错,后面全乱。

避坑指南

我曾经在抓包时遇到一个诡异的问题:程序跑得好好的,突然抓不到包了。查了半天,发现是网卡被其他程序占用了。libpcap 默认只允许一个程序打开同一个网卡。解决办法是用 pcap_open_live 的最后一个参数设置 pcap_set_immediate_mode(),或者用 pcap_create() + pcap_activate() 分步打开。

另一个常见坑是 字节序。网络字节序是大端,而 x86 机器是小端。所以端口号、IP 地址都要用 ntohs()ntohl() 转换。我刚开始写时忘了转换,打印出来的端口号都是 256 的倍数,一看就不对劲。

嗯,最后说一句:libpcap 抓包是网络编程的基本功。你掌握了它,就能自己写一个迷你版的 Wireshark。别怕代码多,多练几次就熟了。