一、项目缘起

因网上看到一个项目需求:在Linux C语言条件下实现一个抓包工具。具体技术与功能明细如下:
* 1、开发技术基于C和DPDK框架 
* 2、抓包方式:支持端口镜像抓包、分光抓包 
* 3、网络协议:全面兼容HTTP、HTTPS 
* 4、部署模式:支持串联、并联两种组网方式 
* 5、基础能力:抓取终端域名访问的全量流量数据 
* 6、检查规则:检测流量内违法关键词、异常IP、违规域名、非法URL,并识别未完成ICP备案的访问域名 
* 7、拦截处置:检测到违规内容或未备案域名时,重置访问链接,强制跳转403提示页面 

觉得有些兴趣,遂开始了解一下DPDK。

二、从DPDK demo开始

先着手于 使用两台个人PC打通基于DPDK通信的demo,结果虽然差强人意,但过程中收获不少(多亏了Gemini和ChatGPT)。
  • 环境:windows(发包)、Ubuntu25.04(部署DPDK)、网卡Intel I219-V(兼容不足,实验败北的根源,Gemini说DPDK支持很好,ChatGPT说不推荐,还是GPT更胜一筹)、windows网线直连ubuntu
  • 软件:科来数据包播放器、

1、DPDK安装部署(DPDK23.11版本会有内核头文件与DPDK类型定义冲突的问题,最后选择的是DPDK24.11)


apt update
apt install -y build-essential meson ninja-build libnuma-dev python3-pyelftools pkg-config
wget https://fast.dpdk.org/rel/dpdk-24.11.1.tar.xz
tar xf dpdk-24.11.1.tar.xz
cd dpdk-24.11.1
# 编译安装
meson setup build
cd build
ninja
ninja install
ldconfig

2、配置大页内存


# 分配 1GB 的大页内存,挂载文件系统类型hugetlbfs并禁止设备文件(nodev,类似于 nosuid 禁止特殊权限、noexec 禁止执行程序)
echo 512 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir -p /mnt/huge
mount -t hugetlbfs nodev /mnt/huge

3、绑定有线网卡


lspci | grep -i ether // 查找有线网卡的PCI地址,如00:1f.6
ip link set dev enp0s31f6 down
笔者绑定 ** vfio-pci ** 驱动后会有VFIO group is not viable!的问题,因为Intel I219-V 是 PCH(南桥)集成设备,通常和很多设备在同一个 IOMMU Group,例如:SATA、SMBus、LPC、SPI、MEI、USB
VFIO 要求:同一个 IOMMU Group 的所有 PCI 设备都必须绑定 vfio-pci 或者解绑。
所以最终绑定的是通用的UIO驱动 ** uio_pci_generic **
这里仍然记录一下,物理机使用vfio-pci时需要再BIOS和系统内核同时开启IOMMU的方法:
1、BISO开启:进入BISO,找到 Intel VT-d设置为Enabled即可
2、Ubuntu内核开启:/etc/default/grub文件中GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"改成GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on"
开启后,更新引导并重启: update-grub && reboot

# 加载通用的 UIO 驱动
sudo modprobe uio_pci_generic
# 用 dpdk-devbind 强行绑定到这个驱动
sudo dpdk-devbind.py --bind=uio_pci_generic 00:1f.6
# 查看状态,确认进入了 "Network devices using DPDK-compatible driver"
sudo dpdk-devbind.py --status

uio

4、自制发包文件,用在科来数据包播放器中

0000  8c 16 45 30 7e b2 00 11 22 33 44 55 08 00 45 00
0010  00 5f 12 34 40 00 40 06 6d a2 c0 a8 64 0a c0 a8
0020  64 14 1f 90 00 50 00 00 00 01 00 00 00 01 80 18
0030  fa f0 31 a0 00 00 01 01 08 0a 00 00 00 00 00 00
0040  00 00 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31
0050  0d 0a 48 6f 73 74 3a 20 77 77 77 2e 62 61 69 64
0060  75 2e 63 6f 6d 0d 0a 55 73 65 72 2d 41 67 65 6e
0070  74 3a 20 74 65 73 74 0d 0a 0d 0a
保存上面的十六进制文本为http_test.txt(Ubuntu 网卡的真实 MAC:8C:16:45:30:7E:B2,源 IP 是 192.168.100.10,目的 IP 是 192.168.100.20,访问的是 www.baidu.com)
用Wireshark自带工具** text2pcap.exe ** 转为pcap文件:
`text2pcap.exe http_test.txt http_test.pcap`

dpdk_testpmd

常用命令:
`dpdk-testpmd -l 4-5 -n 4 -- -i --rxq=1 --txq=1 --forward-mode=rxonly --verbose=1 --rxd=1024 ` // 使用核心Core4 Core5,收发队列各1个,纯收包模式,日志等级1、收包descriptor 1024
`testpmd> start`
`testpmd> stop`
`testpmd> set fwd rxonly`
`testpmd> set verbose 1`
`testpmd> show port stats 0`

rx-drop

rx-miss

dpdk_sniffer程序

因为收包这块RX_packets、RX-missed、RX-dropped数量的问题,手撕代码测试,源码如下:

main.c


#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>

#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32

// 极简网卡初始化函数
static inline int port_init(uint16_t port, struct rte_mempool *mbuf_pool) {
    struct rte_eth_dev_info dev_info;
    struct rte_eth_rxconf rx_conf;

    // struct rte_eth_conf port_conf = {
    //     .rxmode = {
    //         .max_lro_pkt_size = RTE_ETHER_MAX_LEN,
    //         // 显式关闭可能导致板载网卡丢包的硬件卸载功能
    //         .offloads = 0, 
    //     },
    //     .txmode = {
    //         .offloads = 0,
    //     }
    // };
    struct rte_eth_conf port_conf = {
        .rxmode = {
            .max_lro_pkt_size = RTE_ETHER_MAX_LEN,
            .mq_mode = RTE_ETH_MQ_RX_NONE, // 关键:彻底关闭多队列模式 (No Multi-Queue)
            .offloads = 0,
        },
        .txmode = {
            .mq_mode = RTE_ETH_MQ_TX_NONE, // 关键:关闭发送多队列
            .offloads = 0,
        },
        // 显式将 rx_adv_conf.rss_conf 置空,彻底废除哈希分流
        .rx_adv_conf = {
            .rss_conf = {
                .rss_key = NULL,
                .rss_hf = 0, // 核心:哈希函数设为0,不参与任何分流
            },
        },
    };
    const uint16_t rx_rings = 1, tx_rings = 1;
    int retval;
    uint16_t q;

    if (!rte_eth_dev_is_valid_port(port)) return -1;

    // 配置网卡,1个收包队列,1个发包队列
    retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
    if (retval != 0) return retval;

    retval = rte_eth_dev_info_get(port, &dev_info);
    if (retval != 0) return retval;

    rx_conf = dev_info.default_rxconf; // 获取网卡硬件默认的队列配置
    rx_conf.offloads = port_conf.rxmode.offloads; // 保持offload一致

    // 分配并初始化收包队列
    for (q = 0; q < rx_rings; q++) {
        retval = rte_eth_rx_queue_setup(port, q, RX_RING_SIZE,
                rte_eth_dev_socket_id(port), &rx_conf, mbuf_pool);
        if (retval < 0) return retval;
    }

    // 分配并初始化发包队列(虽然单向抓包用不上,但必须初始化)
    for (q = 0; q < tx_rings; q++) {
        retval = rte_eth_tx_queue_setup(port, q, TX_RING_SIZE,
                rte_eth_dev_socket_id(port), NULL);
        if (retval < 0) return retval;
    }

    // 启动物理网卡
    retval = rte_eth_dev_start(port);
    if (retval < 0) return retval;

    // 开启混杂模式(重要:确保能抓到非本地MAC的镜像包)
    rte_eth_promiscuous_enable(port);

    struct rte_eth_link link;
    printf("正在等待网卡链路建立...\n");
    do {
        int ret = rte_eth_link_get_nowait(port, &link);
        if (ret < 0) {
            // 推荐的做法:在这里处理错误日志或返回
            printf("Error getting link status for port %u\n", port);
        }
        rte_delay_ms(500);
    } while (link.link_status == RTE_ETH_LINK_DOWN);
    
    printf("网卡已成功 Up!速率: %u Mbps, 模式: %s\n", 
           link.link_speed, (link.link_duplex == RTE_ETH_LINK_FULL_DUPLEX) ? "全双工" : "半双工");
    return 0;
}

// 核心收包与实时打印线程
static __rte_noreturn void lcore_main(void) {
    uint16_t port = 0; // 我们的板载网卡是 port 0
    uint64_t loop_count = 0; // 增加计数器
    printf("\n[DPDK Sniffer] 核心 %u 正在实时监听 Port %u 流量...\n", rte_lcore_id(), port);

    while (1) {
        struct rte_mbuf *bufs[BURST_SIZE];
        // 核心API:无阻塞批量收包
        const uint16_t nb_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);

        // 每循环 1 亿次打印一行提示,证明主循环没卡死,且确实在尝试收包
        if (unlikely(loop_count++ % 100000000 == 0)) {
            printf("[Debug] 正在死守网卡收包... 当前收包返回值: %u\n", nb_rx);
        }

        if (unlikely(nb_rx == 0)) {
            continue; // 没有包时空转,死守网卡线速
        }

        // 实时处理收到的每一个报文
        for (uint16_t i = 0; i < nb_rx; i++) {
            struct rte_mbuf *m = bufs[i];
            
            // 转换为以太网头部结构体指针(指针零拷贝移位)
            struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(m, struct rte_ether_hdr *);
            
            // 实时打印报文基础信息(你可以自由扩展解析 IP/TCP/HTTP)
            printf("实时捕获 -> 源MAC: %02X:%02X:%02X:%02X:%02X:%02X "
                   "| 目的MAC: %02X:%02X:%02X:%02X:%02X:%02X "
                   "| 长度: %u 字节\n",
                   eth_hdr->src_addr.addr_bytes[0], eth_hdr->src_addr.addr_bytes[1],
                   eth_hdr->src_addr.addr_bytes[2], eth_hdr->src_addr.addr_bytes[3],
                   eth_hdr->src_addr.addr_bytes[4], eth_hdr->src_addr.addr_bytes[5],
                   eth_hdr->dst_addr.addr_bytes[0], eth_hdr->dst_addr.addr_bytes[1],
                   eth_hdr->dst_addr.addr_bytes[2], eth_hdr->dst_addr.addr_bytes[3],
                   eth_hdr->dst_addr.addr_bytes[4], eth_hdr->dst_addr.addr_bytes[5],
                   rte_pktmbuf_pkt_len(m));
            
            // 如果你想看全量十六进制,甚至可以解开下面这行(高吞吐流量下慎用,会刷屏卡死终端)
            // rte_pktmbuf_dump(stdout, m, 64); 

            // 旁路监听模式下,处理完必须手动释放 mbuf,防止内存泄漏
            rte_pktmbuf_free(m);
        }
    }
}

int main(int argc, char *argv[]) {
    struct rte_mempool *mbuf_pool;
    uint16_t portid = 0;

    // 1. 初始化基础环境(EAL),解析 -l, -n, --iova-mode 等参数
    int ret = rte_eal_init(argc, argv);
    if (ret < 0) rte_exit(EXIT_FAILURE, "EAL 初始化失败\n");

    // 2. 创建内存池(大页内存分配)
    mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS,
        MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
    if (mbuf_pool == NULL) rte_exit(EXIT_FAILURE, "无法创建内存池\n");

    // 3. 初始化我们的 0 号物理网卡
    if (port_init(portid, mbuf_pool) != 0)
        rte_exit(EXIT_FAILURE, "网卡 Port %u 初始化失败\n", portid);

    // 4. 在当前核心上直接启动主循环(Demo单核运行即可)
    lcore_main();

    rte_eal_cleanup();
    return 0;
}

meson.build

main.c同目录下创建 meson.build
project('dpdk_sniffer', 'c',
  version: '1.0',
  default_options: ['werror=true', 'c_std=gnu99']
)

# 自动寻找系统里通过 ninja install 安装好的 DPDK 依赖
dpdk = dependency('libdpdk')

sources = files(
  'main.c'
)

executable('dpdk_sniffer',
  sources,
  dependencies: dpdk
)

编译与实战运行


# 让 meson 扫描工程并生成 build 构建目录
meson setup build

# 进入构建目录并用 ninja 编译
cd build
ninja

# 如需重编,彻底删除之前的旧 build 缓存
rm -rf build

# -l 4:强行把主收包循环焊死在 4 号 CPU 核心上。
# --iova-mode=pa:指定物理地址模式,完美适配 UIO 驱动。
./dpdk_sniffer -l 4 -n 4 --iova-mode=pa
可惜最终结果上,仍然没有有效输出

snifferlog

回归testpmd工具

现象说明
testpmd 能识别网卡EAL、PCI、PMD 都正常
Link UpPHY 正常
testpmd 有 RX-packetsPMD 偶尔能收到包
RX-missed 持续增加RX Ring/PMD 工作异常
自己程序 rte_eth_rx_burst() 始终返回 0不是 testpmd 的问题
Windows 发包确认正常排除发送端
只有 Intel I219-V唯一的特殊变量

三、总结

I219-V 并不是一个适合 DPDK 的网卡,
Intel 官方这些年推荐的 DPDK 网卡基本都是:

网卡	DPDK 支持
I350	⭐⭐⭐⭐⭐
82599ES	⭐⭐⭐⭐⭐
X540	⭐⭐⭐⭐⭐
X550	⭐⭐⭐⭐⭐
X710	⭐⭐⭐⭐⭐
E810	⭐⭐⭐⭐⭐
I219-V	⭐⭐