libpcap 抓包:从打开网卡到解析数据包

文章目录

    • [一、libpcap 是什么?为什么它能"抓到包"](#一、libpcap 是什么?为什么它能“抓到包”)
    • [二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据](#二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据)
    • 三、第一步:打开网卡(`pcap_open_live`)
    • 四、第二步:设置过滤规则(只抓想要的包)
      • [🔧 编译 + 应用过滤规则](#🔧 编译 + 应用过滤规则)
      • [🧠 理解关键点](#🧠 理解关键点)
    • [五、第三步:捕获并处理数据包(`pcap_loop`/ `pcap_dispatch`)](#五、第三步:捕获并处理数据包(pcap_loop/ pcap_dispatch))
      • [🔹 pcap_loop:自动循环式抓包(常用)](#🔹 pcap_loop:自动循环式抓包(常用))
        • [🔹 回调函数参数详解](#🔹 回调函数参数详解)
        • [🔹 回调函数示例:](#🔹 回调函数示例:)
          • [1️⃣ 数据包结构](#1️⃣ 数据包结构)
          • [2️⃣ IP 头](#2️⃣ IP 头)
          • [3️⃣ TCP 头](#3️⃣ TCP 头)
          • [✅ 总结](#✅ 总结)
      • [🔹 `pcap_dispatch`:轮询式抓包](#🔹 pcap_dispatch:轮询式抓包)
          • [🔹 示例:基于 `pcap_dispatch` 的轮询式抓包](#🔹 示例:基于 pcap_dispatch 的轮询式抓包)
        • [✅ 特点对比](#✅ 特点对比)
    • 六、释放资源:`pcap_close`
    • 七、完整实战:写一个可运行的抓包程序
    • 八、编译与运行
      • [1️⃣ 安装依赖](#1️⃣ 安装依赖)
      • [2️⃣ 编译程序](#2️⃣ 编译程序)
      • [3️⃣ 运行示例](#3️⃣ 运行示例)
    • 九、总结:抓包的完整逻辑图
    • [🧩 最后小结](#🧩 最后小结)

对于刚接触网络编程的同学来说,"抓包"听起来很高深。
其实, 只要学会用 libpcap ,你也能像 Wireshark 一样实现自己的抓包工具。


一、libpcap 是什么?为什么它能"抓到包"

libpcap 全称 Packet Capture Library ,是一个跨平台的网络数据包捕获库

它就像是一个"监听器",能直接从网卡驱动层获取经过的每一个数据包。

💡 你知道吗?

像 Wireshark、tcpdump 这些工具的底层,都是 libpcap


它能做的事情:

能力 说明
捕获数据包 从指定网卡中抓取原始网络数据
过滤数据包 只保留特定协议、端口或 IP 的包
分析包内容 读取协议头、源/目标 IP、端口等信息

简单来说:

👉 Wireshark 是图形化的,libpcap 是底层的

我们可以用它来实现各种自定义网络分析工具。


二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据

所有基于 libpcap 的程序,本质上都包含 3 步:

  1. 打开网卡(告诉系统我要抓哪个接口的数据)
  2. 设置过滤规则(只抓我关心的包,比如 TCP 或端口 80)
  3. 捕获处理数据(逐个分析抓到的包)

就像听音乐:

插耳机 🎧 → 选歌单 🎵 → 开始听 👂。

下面我们一步步拆开看。


三、第一步:打开网卡(pcap_open_live

pcap_open_live 是抓包的第一步,它会打开指定的网络接口(比如 eth0wlan0),并返回一个 pcap_t* 类型的句柄,后续所有操作都通过它完成。

示例代码

c 复制代码
// 初始化 libpcap:打开网卡并返回句柄
    char errbuf[PCAP_ERRBUF_SIZE];

    pcap_t* handle = pcap_open_live(
        device,     // 要打开的网卡名,例如 "eth0"
        65535,      // 最大包长度(以太网最大MTU)
        1,          // 开启混杂模式(1=是)
        1000,       // 超时时间:1000ms
        errbuf      // 存储错误信息
    );

    if (!handle) {
        fprintf(stderr, "打开网卡失败:%s\n", errbuf);
        return NULL;
    }

    printf("✅ 成功打开网卡:%s\n", device);
    

💬 参数说明

参数名 含义与类型说明
device 要打开的网卡名(字符串),例如 wlan0eth0
snaplen 抓包时每个数据包的最大抓取长度(整数),用于限制单个包读取的字节数。
promisc 是否开启混杂模式(整数,1/0 或 true/false),开启后可接收非本机目标的包。
to_ms 读取超时时间(毫秒,整数),控制捕获函数的阻塞返回行为。
errbuf 错误信息缓冲区(字符数组),用于接收 libpcap 返回的错误描述。

📘 混杂模式(promiscuous mode)

通常网卡只接收发给本机的数据包。

开启混杂模式后,可以监听同一局域网中所有设备的包(前提:网卡支持)。


四、第二步:设置过滤规则(只抓想要的包)

抓取所有包的信息量太大,因此我们通常会用过滤器来"筛选"感兴趣的数据。

libpcap 使用一种叫 BPF(Berkeley Packet Filter) 的语法。

像这样写规则:

示例规则 意义
tcp 抓取所有 TCP 包
udp port 53 抓取 DNS 包
dst port 80 or 443 抓取访问网站的包
src host 192.168.1.5 抓取来自特定主机的包

🔧 编译 + 应用过滤规则

我们不能直接把字符串规则交给驱动,要先"编译"成机器能执行的过滤程序。

c 复制代码
    struct bpf_program fp;   // 存储编译后的规则

    // 1️⃣ 编译过滤规则
    if (pcap_compile(handle, &fp, filter_rule, 0, PCAP_NETMASK_UNKNOWN) == -1) {
        fprintf(stderr, "编译过滤规则失败:%s\n", pcap_geterr(handle));
        return -1;
    }

    // 2️⃣ 应用过滤规则
    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "应用过滤规则失败:%s\n", pcap_geterr(handle));
        pcap_freecode(&fp);
        return -1;
    }

    // 3️⃣ 释放编译资源
    pcap_freecode(&fp);
    printf("✅ 成功设置过滤规则:%s\n", filter_rule);
    

🧠 理解关键点

  • pcap_compile:把人类可读的字符串(如 "tcp and port 80") 翻译成机器码。
  • pcap_setfilter:把机器码绑定到网卡,这样系统在内核层就能过滤数据,效率更高。
  • pcap_freecode:释放临时编译结果。

五、第三步:捕获并处理数据包(pcap_loop/ pcap_dispatch

完成过滤器后,就可以开始抓包了!

抓包的核心是注册一个回调函数 ,每次有符合规则的数据包时,libpcap 会自动调用它。


🔹 pcap_loop:自动循环式抓包(常用)

c 复制代码
pcap_loop(handle, count, packet_handler, user_data);

各参数含义:

参数 类型 作用 示例
handle pcap_t* 打开的网卡句柄,由 pcap_open_live 返回 handle
count int 要抓取的数据包数量;0 表示无限抓包 5 → 抓 5 个包;0 → 一直抓到用户按 Ctrl+C 停止
packet_handler 回调函数 每抓到一个符合规则的数据包,libpcap 会自动调用该函数 必须是 void (*)(u_char*, const struct pcap_pkthdr*, const u_char*) 类型
user_data u_char* 用户自定义数据,会传给回调函数,可用于传递上下文或统计信息 可以传 NULL 或传入结构体指针

💡 小提示
user_data 可以用来传递额外信息,比如包计数器、日志文件指针或程序状态。在简单示例中可以传 NULL


🔹 回调函数参数详解

libpcap 规定了回调函数的固定格式,参数不能随便改,否则无法正常调用:

c 复制代码
void packet_handler(
    u_char* user_data,                    // 用户自定义数据(传给 pcap_loop 的最后一个参数)
    const struct pcap_pkthdr* header,     // 包的基本信息(长度、时间戳等)
    const u_char* packet                  // 包的原始数据(二进制字节流)
);

各参数详解:

  • user_data:由你在调用 pcap_loop 时传入(最后一个参数),用于给回调函数传递额外信息(比如上下文结构体、配置参数等)。示例中没用到,所以可以忽略。

  • header:指向 struct pcap_pkthdr 结构体,包含包的元数据:

    • header->len:包的实际长度(字节);
    • header->ts:抓包的时间戳(tv_sec 是秒,tv_usec 是微秒)。
  • packet:指向包的原始二进制数据(从链路层开始的字节流),需要你手动解析(比如跳过以太网头、解析 IP 头、TCP 头等)。

🔹 回调函数示例:
c 复制代码
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>

// 每抓到一个包,就执行这个函数
void packet_handler(u_char* user_data, const struct pcap_pkthdr* header, const u_char* packet) {
    static int count = 0;
    count++;

    printf("\n===== 第 %d 个包 =====\n", count);
    printf("包长度:%d 字节\n", header->len);

    // 解析 IP 头(跳过以太网头 14 字节)
    const struct ip* ip_header = (struct ip*)(packet + 14);
    printf("源 IP:%s\n", inet_ntoa(ip_header->ip_src));
    printf("目标 IP:%s\n", inet_ntoa(ip_header->ip_dst));

    // 解析 TCP 头
    int ip_len = ip_header->ip_hl * 4;
    const struct tcphdr* tcp_header = (struct tcphdr*)(packet + 14 + ip_len);
    printf("源端口:%d -> 目标端口:%d\n",
           ntohs(tcp_header->th_sport), ntohs(tcp_header->th_dport));
}
1️⃣ 数据包结构

一个以太网帧经过 libpcap 捕获后,packet 指向的就是原始字节流,从链路层(以太网层)开始。典型的网络协议层次如下:

复制代码
[以太网头 Ethernet] 14 字节
[IP 头 IP]           可变长度,最小 20 字节
[TCP 头 TCP]         可变长度,最小 20 字节
[应用数据 Data]      剩下的 payload
  • 以太网头通常固定 14 字节:

    • 6 字节目标 MAC
    • 6 字节源 MAC
    • 2 字节类型字段(比如 IPv4 = 0x0800)

因此,如果你想解析 IP 头,需要跳过以太网头

c 复制代码
const struct ip* ip_header = (struct ip*)(packet + 14);

这里 packet + 14 的意思就是指针偏移 14 字节,直接指向 IP 头的起始位置。


2️⃣ IP 头

IP 头长度不是固定 20 字节,它的实际长度由 ip_hl 字段决定:

c 复制代码
int ip_len = ip_header->ip_hl * 4;
  • ip_hl 单位是 32-bit 字(4 字节),所以乘 4 得到实际字节数。
  • IP 头可能包含可选字段,所以不能直接固定 20 字节。

3️⃣ TCP 头

TCP 头紧跟在 IP 头后面,因此它的起始位置是:

复制代码
packet + 以太网头长度 + IP 头长度

用代码表示就是:

c 复制代码
const struct tcphdr* tcp_header = (struct tcphdr*)(packet + 14 + ip_len);
  • 这样就确保无论 IP 头有没有可选字段,都能正确指向 TCP 头。
  • TCP 头长度也是可变的(th_off 字段),后续如果解析应用层数据,还要用 TCP 头长度偏移。

✅ 总结
  1. 跳过以太网头(固定 14 字节) → 到达 IP 头。
  2. IP 头长度可变 → 用 ip_hl * 4 获取实际长度。
  3. TCP 头紧随 IP 头 → TCP 头指针 = packet + 14 + IP 头长度

📌 总结
pcap_loop 是抓包主循环,它只负责抓包和调用回调函数。回调函数 packet_handler 承载了你自定义的"处理逻辑",可以打印信息、分析数据、统计流量或写入文件等。


🔹 pcap_dispatch:轮询式抓包

除了 pcap_looplibpcap 还提供了一个非常常用的函数 ------ pcap_dispatch

它的作用类似,但在运行机制上更灵活、可控 ,尤其适合在需要定期检查退出条件与其他事件循环结合的场景。

c 复制代码
int pcap_dispatch(pcap_t* handle, int cnt,
                  pcap_handler callback, u_char* user);

返回值是一个 整数,表示它实际 "处理的数据包数量"

参数名 含义与说明
handle 打开的捕获会话句柄。
cnt 指定要处理的最大包数(≤0 表示处理所有已到达的包)。
callback 回调函数(和 pcap_loop 相同类型)。
user 用户自定义数据(传递给回调)。
🔹 示例:基于 pcap_dispatch 的轮询式抓包
c 复制代码
#include <signal.h>
volatile int stop_capture = 0;

void signal_handler(int sig) {
    (void)sig;
    stop_capture = 1;
    printf("\n🛑 收到中断信号,准备停止抓包...\n");
}

void pcap_start_capture(pcap_t* handle) {
    printf("开始抓包(按 Ctrl+C 停止)...\n");
    signal(SIGINT, signal_handler);

    while (!stop_capture) {
        // 每次只处理 1 个包,超时则返回 0
        int ret = pcap_dispatch(handle, 1, packet_handler, NULL);

        if (ret == -1) {
            fprintf(stderr, "❌ 抓包出错:%s\n", pcap_geterr(handle));
            break;
        }
        // ret == 0 表示这次没有抓到包,可继续轮询
    }

    printf("✅ 抓包结束。\n");
}

🧠 理解要点

  • pcap_dispatch 每调用一次,仅处理一次捕获批次(或指定个数的包)。
  • 返回后可立即检查退出标志、更新 UI 或执行其他任务。
  • 这种模式更适合做成"非阻塞"的实时程序(如入侵检测、流量监控)。
✅ 特点对比
对比项 pcap_loop pcap_dispatch
调用方式 阻塞式循环 ,直到抓完 count 个包或收到中断 单次处理调用,抓到指定数量包后立即返回
返回值 自动循环内部,不会主动返回 返回实际处理的包数,可在外层循环中判断
适用场景 一次性抓包,直到用户手动中断(如 Wireshark 模式) 在程序主循环中"轮询"式抓包(如防火墙、实时监控)
可中断性 需发送信号(如 Ctrl+C)才能中断 每次调用都可判断退出条件,更灵活

开始抓包

c 复制代码
void pcap_start_capture(pcap_t* handle, int count) {
    printf("开始抓包(按 Ctrl+C 停止)...\n");

    int ret = pcap_loop(
        handle,          // 打开的网卡句柄
        count,           // 要抓的包数量(0 表示无限抓)
        packet_handler,  // 回调函数
        NULL             // 可选参数,这里不用
    );

    if (ret == PCAP_ERROR_BREAK)
        printf("✅ 抓包结束。\n");
    else if (ret == PCAP_ERROR)
        fprintf(stderr, "❌ 抓包出错:%s\n", pcap_geterr(handle));
}

六、释放资源:pcap_close

程序退出前一定要释放资源,否则可能造成网卡占用或内存泄漏。

c 复制代码
void pcap_cleanup(pcap_t* handle) {
    if (handle) {
        pcap_close(handle);
        printf("🧹 已关闭网卡并释放资源\n");
    }
}

七、完整实战:写一个可运行的抓包程序

下面是完整示例(可以直接复制运行👇)

c 复制代码
/*
 * pcap_demo.c - 简易抓包示例(基于 libpcap)
 *
 * 用法:
 *   gcc pcap_demo.c -o pcap_demo -lpcap
 *   sudo ./pcap_demo <网卡名> "<过滤规则>"
 *
 * 示例:
 *   sudo ./pcap_demo wlan0 "tcp and port 80"
 */

#include <pcap.h>
#include <stdio.h>
#include <signal.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <string.h>

int stop_capture = 0; // 标记 Ctrl+C 停止

void signal_handler(int sig) {
    (void)sig;
    stop_capture = 1;
    printf("\n🛑 收到中断信号,准备停止抓包...\n");
}

/* ---------- 核心步骤:打开网卡(pcap_open_live) ---------- */
pcap_t* pcap_init(const char* device) {
    char errbuf[PCAP_ERRBUF_SIZE];
    pcap_t* handle = pcap_open_live(
        device,     // 网卡名,例如 "eth0" 或 "wlan0"
        65535,      // 最大抓包长度
        1,          // 混杂模式(1=开启)
        1000,       // 超时时间(ms)
        errbuf
    );
    if (!handle) {
        fprintf(stderr, "打开网卡失败:%s\n", errbuf);
    }
    return handle;
}

/* ---------- 编译并应用过滤器(pcap_compile + pcap_setfilter) ---------- */
int pcap_set_filter(pcap_t* handle, const char* filter_rule) {
    struct bpf_program fp;
    if (pcap_compile(handle, &fp, filter_rule, 0, PCAP_NETMASK_UNKNOWN) == -1) {
        fprintf(stderr, "编译过滤规则失败:%s\n", pcap_geterr(handle));
        return -1;
    }
    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "应用过滤规则失败:%s\n", pcap_geterr(handle));
        pcap_freecode(&fp);
        return -1;
    }
    pcap_freecode(&fp);
    printf("✅ 成功设置过滤规则:%s\n", filter_rule);
    return 0;
}

/* ---------- 回调函数:处理每个抓到的包(packet_handler) ---------- */
void packet_handler(u_char* user_data, const struct pcap_pkthdr* header, const u_char* packet) {
    (void)user_data;
    static int count = 0;
    count++;

    printf("\n===== 第 %d 个包 =====\n", count);
    printf("包长度:%u 字节 | 抓包时间:%ld.%06ld\n",
           header->len,
           (long)header->ts.tv_sec,
           (long)header->ts.tv_usec);

    /* 基本长度检查(以太网头 14 字节 + 最小 IP 头) */
    if (header->caplen < 14 + sizeof(struct ip)) {
        printf("包太短,无法解析 IP 头\n");
        return;
    }

    /* 解析 IPv4(假设以太网链路层) */
    const u_char* ip_ptr = packet + 14;
    const struct ip* ip_header = (const struct ip*)(ip_ptr);

    if (ip_header->ip_v != 4) {
        printf("非 IPv4 包(版本 %d),跳过解析\n", ip_header->ip_v);
        return;
    }
    //定义两个缓冲区,用于存储转换后的IP地址字符串
    char src_buf[INET_ADDRSTRLEN], dst_buf[INET_ADDRSTRLEN];
    // 将二进制的目标/源IP地址转换为字符串
    inet_ntop(AF_INET, &ip_header->ip_src, src_buf, sizeof(src_buf));
    inet_ntop(AF_INET, &ip_header->ip_dst, dst_buf, sizeof(dst_buf));
    printf("IP:%s -> %s\n", src_buf, dst_buf);

    int ip_header_len = ip_header->ip_hl * 4;
    if (ip_header_len < 20) {
        printf("IP 头长度异常:%d,跳过\n", ip_header_len);
        return;
    }

    /* 确保包含 TCP 头时长度足够 */
    if (ip_header->ip_p == IPPROTO_TCP) {
        const u_char* tcp_ptr = ip_ptr + ip_header_len;
        if (header->caplen < 14 + ip_header_len + sizeof(struct tcphdr)) {
            printf("包太短,无法解析 TCP 头\n");
            return;
        }
        const struct tcphdr* tcp_header = (const struct tcphdr*)(tcp_ptr);
        printf("TCP 端口:%u -> %u\n", ntohs(tcp_header->th_sport), ntohs(tcp_header->th_dport));
    } else {
        printf("非 TCP 协议(协议号 %d),跳过 TCP 解析\n", ip_header->ip_p);
    }
}

/* ---------- 抓包主循环:用 pcap_dispatch 短轮询以便响应中断 ---------- */
void pcap_start_capture(pcap_t* handle) {
    printf("开始抓包(按 Ctrl+C 停止)...\n");
    while (!stop_capture) {
        int ret = pcap_dispatch(handle, 1, packet_handler, NULL);
        if (ret == -1) {
            fprintf(stderr, "抓包出错:%s\n", pcap_geterr(handle));
            break;
        }
        /* ret == 0 表示这次超时未捕获到包,继续循环 */
    }
}

/* ---------- 释放资源 ---------- */
void pcap_cleanup(pcap_t* handle) {
    if (handle) {
        pcap_close(handle);
        printf("🧹 已关闭网卡并释放资源\n");
    }
}

/* ---------- 程序入口 ---------- */
int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "用法:%s <网卡名> <过滤规则>\n", argv[0]);
        fprintf(stderr, "示例:%s wlan0 \"tcp and port 80\"\n", argv[0]);
        return 1;
    }

    signal(SIGINT, signal_handler);

    const char* device = argv[1];
    const char* filter = argv[2];

    pcap_t* handle = pcap_init(device);
    if (!handle) return 1;

    if (pcap_set_filter(handle, filter) != 0) {
        pcap_cleanup(handle);
        return 1;
    }

    pcap_start_capture(handle);
    pcap_cleanup(handle);
    return 0;
}

八、编译与运行

1️⃣ 安装依赖

bash 复制代码
sudo apt install libpcap-dev

2️⃣ 编译程序

bash 复制代码
gcc pcap_demo.c -o pcap_demo -lpcap

3️⃣ 运行示例

bash 复制代码
sudo ./pcap_demo wlan0 "tcp and port 80"

你会看到:

复制代码
===== 第 1 个包 =====
包长度:74 字节 | 抓包时间:1698xxxxxxx.123456
IP:192.168.1.2 -> 172.217.160.110
TCP 端口:54321 -> 80

九、总结:抓包的完整逻辑图

复制代码
┌────────────┐
│ 打开网卡   │ ← pcap_open_live()
└─────┬──────┘
      │
      ▼
┌────────────┐
│ 编译过滤器 │ ← pcap_compile()
│ 应用过滤器 │ ← pcap_setfilter()
└─────┬──────┘
      │
      ▼
┌────────────┐
│ 捕获数据包 │ ← pcap_loop()
│ 解析内容   │ ← 自定义回调函数
└─────┬──────┘
      │
      ▼
┌────────────┐
│ 释放资源   │ ← pcap_close()
└────────────┘

🧩 最后小结

步骤 核心函数 作用
打开网卡 pcap_open_live 连接网络接口
编译过滤 pcap_compile 把规则转成机器码
应用过滤 pcap_setfilter 绑定过滤器
抓取数据 pcap_loop / pcap_dispatch 抓包主逻辑
清理资源 pcap_close 释放句柄
相关推荐
YuMiao5 小时前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
Sinclair3 天前
简单几步,安卓手机秒变服务器,安装 CMS 程序
android·服务器
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
Rockbean4 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
茶杯梦轩4 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试