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 释放句柄
相关推荐
wanhengidc6 小时前
服务器硬盘的作用都有哪些?
运维·服务器·安全·智能手机·云计算
爱奥尼欧6 小时前
【Linux笔记】网络部分——传输层协议TCP(1)
linux·运维·网络·笔记·tcp/ip·1024程序员节
二进制coder6 小时前
Linux I2C子系统全面详解:从理论到实战
linux·运维·服务器
菲橙6 小时前
5.2 MCP服务器
运维·服务器
lang201509287 小时前
WebSocket子协议STOMP
网络·websocket·网络协议
饺子大魔王的男人7 小时前
3秒传输GB级文件:FastSend让P2P共享告别云存储依赖
网络·网络协议·p2p
在坚持一下我可没意见7 小时前
Java 网络编程:TCP 与 UDP 的「通信江湖」(基于TCP回显服务器)
java·服务器·开发语言·笔记·tcp/ip·udp·java-ee
KevinLyu7 小时前
PHP内核详解· 内存管理篇(四)· 分配小块内存
php
一叶知秋yyds8 小时前
openwrt 系统下通过命令行设置允许wan口进行Luci页面的访问
网络·openwrt·luci 开启wan 口访问