简介:面向网络开发者的网络抓包工具源码工程,标签虽标注为Java,实际主体是Qt工程与npcap本地接口调用,界面交互模仿Wireshark,适合需要二次开发或系统学习数据包捕获、协议解析的开发者。npcap是Windows平台上libpcap的移植版本,提供底层抓包与过滤接口;Qt则负责跨平台界面与信号槽事件处理,二者结合可完成从网卡抓包、过滤器设置到数据包列表展示与详情解析的完整流程。压缩包共86个文件,主要包括cpp/h源码、ui界面文件、qrc资源、vcxproj/sln工程配置及构建日志,另附exe可执行文件,整体约2.48MB,工程结构清晰,便于直接运行对照和二次开发。已有377人学习下载。通过源码可掌握npcap抓包API在Qt工程中的集成方式,理解过滤器规则、包列表刷新、协议字段解析等核心模块,也能为网络故障排查、协议分析工具定制或相关课程设计提供直接参考。
1. 与其一直黑盒用 Wireshark,不如基于 npcap 和 Qt 手写一个 sniffer
用 Wireshark 抓包查问题,查得多了总有一个念头冒出来:这个黑匣子里到底发生了什么?列表、协议树、十六进制视图,数据是怎么从网卡流转到屏幕上的?与其一直黑盒使用,不如基于 npcap 和 Qt 自己写一个 sniffer。标题里那个 sniffer.zip 就是这类东西:一个在 Windows 上跑的、模仿 Wireshark 的网络抓包工具。
这个方向适合三类人。一是搞网络协议解析,要给内网私有协议做定制分析工具的工程师,Wireshark 太重,自己写能把承载网卡、过滤规则、协议摘要按需求裁剪。二是刚接触 libpcap API 的学生,想用 Qt 把抓包循环和界面串起来。三是有现成抓包需求但被商业分析工具的授权折腾过的人。做完之后最直观的回报是:你再打开 Wireshark 时,不再觉得它是玄学,而是能说出每一列数据是从哪一层、哪一个结构体里抠出来的。
2. 把 Wireshark 拆开看:npcap、Qt 与抓包三板斧的设计选型
2.1 为什么是 npcap:它在 Windows 网络栈里的位置与选型理由
普通应用程序想拿网络数据只能走 socket,而 socket 经过的是 TCP/IP 协议栈,你能看到的是“剥完壳”的应用层数据,永远看不到以太网帧头,也看不到交换机广播的 ARP 报文。抓包工具要的是链路层原始帧,必须在网卡驱动和协议栈之间装一个“分流器”,每个数据包经过时复制一份到内核缓冲区,用户态程序再把它取出来。
npcap 的 NPF 驱动干的就是这件事。它挂在 Windows 的 NDIS 6 过滤器驱动链上,数据包到达网卡驱动后先过 NPF,NPF 留一份副本放到内核缓冲区,再按原路径交给协议栈。用户态这边,npcap 提供了一套 libpcap 风格的接口,头文件是 pcap.h,导入库叫 wpcap.lib。你在 Windows 上开发抓包工具,选型理由其实只有一个:WinPcap 已经停止维护多年,新版 Windows 驱动签名过不了,而 npcap 是 Wireshark 在 Windows 上的默认安装项,维护活跃,有原生 64 位支持,接口又和旧 WinPcap 兼容,几乎不需要迁移成本。
安装完 npcap 后,建议先在命令行验证驱动状态,别一头扎进代码。开一个管理员权限的 cmd:
sc query npcap正常输出里 SERVICE_STATE 应该是 RUNNING,SERVICE_NAME 是 npcap。如果显示 STOPPED,去服务管理器找到 npcap,启动并设为自动。还要留意一点:装完 npcap 后系统里会多出虚拟网卡,VMware、VirtualBox 那些也都会注册 NPF 接口,后面写设备枚举时会看到很多名字奇怪的项目,按描述里的“Adapter for loopback traffic capture”或者虚拟机关键字过滤掉就行。
2.2 为什么是 Qt:信号槽让抓包线程和界面线程不用抢锁
选 Qt 而不是 MFC 或 WPF,不是因为界面控件数量多,而是抓包这个场景天然是“一个线程卡在阻塞读里,另一个线程要持续刷新表格”。MFC 里做这种事要自己开线程、自己封装消息、自己处理窗口句柄跨线程访问,一不留神就崩溃。Qt 的信号槽机制,配合跨线程的队列连接,能把这件事压缩成两行代码。
抓包线程负责在 pcap_next_ex 上阻塞等待,每拿到一个包就 emit 一个信号,信号带的参数就是原始字节。界面线程的槽函数接到信号后做解析和表格刷新。Qt 的 AutoConnection 在跨线程发射信号时自动走队列连接,也就是说 emit 方和槽函数执行方是不同的线程,但队列保证了顺序,槽函数不可能被两帧数据同时打进重入,所以大部分场景连 mutex 都可以省掉。
我做这个工具时踩过一个印象很深的坑:pcap 的官方回调方式里,你注册一个函数指针,回调发生在 pcap 内部线程上下文,千万不要在那个回调函数里直接操作 QTableWidget。当时图省事,回调里直接往列表插行,结果是事件错乱、界面时不时卡死。后来改成只 emit 信号,所有的 UI 操作全部放到槽函数里,问题彻底消失。记住一句话:C 回调里不做任何 Qt 界面操作,这是用 Qt 写抓包工具的第一条红线。
2.3 做到什么程度算“模仿 Wireshark”:MVP 功能拆分与实现顺序
Wireshark 的功能庞大到没人能全部复刻,但抓包工具的核心体验就三板斧:顶部一个报文列表、中间一个协议解析树、底部一个十六进制字节视图。抓住这三块,一个 sniffer 的骨架就立住了。
我之前定的 MVP 清单是这样的:
| 功能模块 | 必须项 | 增强项(后续版本) |
|---|---|---|
| 网卡枚举 | 列出所有 NPF 接口,显示描述 | 按虚拟/物理过滤,记住上次选择 |
| 抓包控制 | 启动、停止 | 暂停、清空列表 |
| 报文列表 | 时间、源、目的、协议、长度、信息摘要 | 着色规则、列排序 |
| 协议解析 | 以太网帧 + IPv4 + TCP/UDP/ARP/ICMP | IPv6、DNS、HTTP、TLS 指纹 |
| 字节视图 | 十六进制 + ASCII 双边显示 | 点击列表高亮对应协议字段 |
| 文件保存 | pcap 格式,Wireshark 能直接打开 | 导出 CSV、过滤后另存 |
实现顺序有一个原则:每一步都要能看到界面变化。先写网卡枚举,打开程序能看到下拉列表,这是一小步但很有成就感;然后写抓包循环,列表开始滚帧;再补协议摘要;最后才是协议树和保存文件。不要一上来就想复刻 Wireshark 的过滤器语法,那是把项目拖死的常见原因。显示过滤器这块,自己做一套解析器工作量极大,MVP 阶段完全可以用 BPF 内核过滤加简单的协议列筛选代替。
3. 搭出能抓包的最小工具:从 npcap 安装到报文上屏
3.1 环境准备:npcap 下载安装、Qt 5.15.2 与编译器配对
npcap 安装器的下载,直接去 npcap 官网拿最新版本安装包。给 Wireshark 装过的人会眼熟,因为 Wireshark 的安装向导里有一页就是“Install Npcap”,你可以单独装。安装时有两个勾选项要留意:一个是“Support raw 802.11 traffic”,普通 Wi-Fi 网卡抓不到原始 802.11 帧,这个选项对大多数场景无效,不建议勾;另一个是“Install Npcap in WinPcap API-compatible Mode”,建议勾上,这能让老程序也走 npcap 补丁过的兼容接口,后面调试少很多烦恼。
Qt 版本我推荐 5.15.2 或 6.x,编译器选 MSVC 2019 64 位。不要选 MinGW,因为 npcap SDK 的库是 MSVC 格式的,MinGW 经常出现符号对不上的诡异问题,翻车概率极高。Qt 5.15.2 比 6.x 的资料多,网上随便一搜就是大把现成例子,新项目如果没特殊要求,5.15.2 加 MSVC2019 64 位是当前最稳妥的组合。
再准备 npcap SDK,也就是开发用的头文件和导入库。装完安装器后,SDK 要单独下载解压到固定目录,比如 C:/npcap-sdk,里面能见到 Include/pcap.h 与 Lib/x64/wpcap.lib。还需要一套 Visual Studio 2019 或 2022 的 Build Tools,只用命令行编译也可以,但 Qt Creator 配置好 MSVC 套件后体验更好。最终环境参数可以按这个表核对:
| 组件 | 版本建议 | 用途 |
|---|---|---|
| npcap 安装器 | 最新稳定版,0.995 以上 | 提供内核驱动与用户态 dll |
| npcap SDK | 与安装器同版本 | 提供头文件和导入库 |
| Qt | 5.15.2 MSVC2019 64 位 | GUI 框架 |
| VS Build Tools | 2019 或 2022 | C++ 编译器与链接器 |
3.2 工程骨架:在 pro 文件里把 npcap SDK 接进来
新建一个 Qt Widgets Application,工程文件按下面这个模板改。路径按你自己的 SDK 位置调整:
QT += core gui widgets TARGET = Sniffer TEMPLATE = app CONFIG += c++11 INCLUDEPATH += C:/npcap-sdk/Include LIBS += -LC:/npcap-sdk/Lib/x64 -lwpcap -lws2_32这里链接的是 wpcap.lib,不是 npcap.lib。npcap 虽然改进了驱动,但用户态 API 还是沿用了 libpcap 命名,导入库也保留了老名字,这是为了兼容大量既有代码。加 ws2_32 是因为 pcap 的内部实现要调 Windows Socket 的辅助函数。如果目标平台是 32 位,把 Lib/x64 改成 Lib/x86 就行,但现在新机器基本都是 64 位,别两边混用。
工程建好后,先写网卡枚举,这是第一个能看到界面的功能。在 MainWindow 构造时调用这段代码,把 pcap 设备列表填进 QComboBox:
#include <pcap.h> QComboBox *combo = new QComboBox(this); pcap_if_t *alldevs = nullptr; char errbuf[PCAP_ERRBUF_SIZE] = {0}; if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, nullptr, &alldevs, errbuf) == -1) { qWarning() << "枚举网卡失败: " << errbuf; } else { for (pcap_if_t *dev = alldevs; dev != nullptr; dev = dev->next) { QString desc = dev->description ? QString::fromUtf8(dev->description) : QStringLiteral("无描述"); combo->addItem(QString("%1 (%2)").arg(desc).arg(dev->name), dev->name); } } pcap_freealldevs(alldevs);pcap_findalldevs_ex 的第一个参数传 PCAP_SRC_IF_STRING,是要求它枚举本地接口;拿回来的 pcap_if_t 链表里,name 字段是内核设备路径,形如 \Device\NPF_{GUID},description 是人类可读的网卡名称。把 name 存进 QVariant,后面打开设备时直接从下拉框取。注意 errbuf 在失败时才有意义,成功时可以不管它。这段代码跑通,说明 SDK 链接没问题,环境闭环了。
3.3 核心抓包循环:pcap_next_ex 与 Qt 信号槽上屏
抓包线程是整个工具的心脏。我推荐不用 pcap_loop 回调,而是用 pcap_next_ex 在一个 while 循环里轮询,原因后面避坑章会细说,这里只给结论:pcap_loop 回调在停止时特别麻烦,pcap_breakloop 在部分 npcap 版本上要等超时才能返回,关窗口会卡几秒;pcap_next_ex 配合 1000 毫秒超时,循环每秒至少醒一次,检查停止标志非常顺手。
第一步,打开网卡并编译 BPF 过滤器:
pcap_t *handle = pcap_open_live( devName.toStdString().c_str(), // 网卡内核路径 65535, // snaplen,每个包最多保留 65535 字节 1, // promisc,混杂模式 1000, // timeout,单位毫秒 errbuf); if (handle == nullptr) { qCritical() << "打开网卡失败: " << errbuf; return; }snaplen 设 65535 是 libpcap 的传统做法,保证任何包都不会因为截断而丢负载。如果只关心 TCP 头做连接分析,设 1536 也能省一点内存,但一旦想看应用层数据就得重新抓,所以我都是直接拉满。promisc 置 1 进入混杂模式,交换机上不是发给本机的包也能收到一部分,这是抓包工具的基础能力。timeout 设 1000,单位是毫秒,控制 pcap_next_ex 在没有包时的唤醒频率。
第二步,写抓包线程的工作对象。这里用 QObject 的子类加 moveToThread 的方式,信号槽可以正常跨线程投递:
class CaptureWorker : public QObject { Q_OBJECT public: explicit CaptureWorker(pcap_t *handle, QObject *parent = nullptr) : QObject(parent), m_handle(handle) {} void setRunning(bool running) { m_running = running; } public slots: void run() { while (m_running) { struct pcap_pkthdr *header = nullptr; const u_char *data = nullptr; int ret = pcap_next_ex(m_handle, &header, &data); if (ret == 1) { QByteArray bytes(reinterpret_cast<const char *>(data), static_cast<int>(header->caplen)); emit packetCaptured(header->ts.tv_sec, header->ts.tv_usec, header->caplen, bytes); } else if (ret == -1) { emit errorOccurred(QString::fromUtf8(pcap_geterr(m_handle))); break; } } emit finished(); } signals: void packetCaptured(quint32 tsSec, quint32 tsUsec, quint32 caplen, QByteArray bytes); void errorOccurred(QString message); void finished(); private: pcap_t *m_handle = nullptr; bool m_running = false; };pcap_next_ex 的返回值有三个分支:1 表示拿到一个包,0 表示超时没有包进来,-1 表示网卡出错。拿包以后必须马上把数据复制成 QByteArray,因为 pcap_next_ex 内部缓冲区在下一次调用时会被覆盖,绝不能把 data 指针直接发到信号里。caplen 是实际拷出来的字节数,正常情况下等于 len 字段,大小写敏感这里别写混。
第三步,在 MainWindow 里把信号接到界面槽:
m_thread = new QThread(this); m_worker = new CaptureWorker(handle); m_worker->moveToThread(m_thread); connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); connect(m_thread, &QThread::started, m_worker, &CaptureWorker::run); connect(m_worker, &CaptureWorker::packetCaptured, this, &MainWindow::appendPacket); connect(m_worker, &CaptureWorker::finished, this, &MainWindow::onCaptureFinished); m_worker->setRunning(true); m_thread->start();connect 的第五个参数在本例不用显式写,跨线程默认就是队列连接,packetCaptured 信号发射后,appendPacket 槽会切换到 UI 线程执行,这就是 2.2 节说的不需要自己加锁的关键原因。appendPacket 里做的事就是往 QTableWidget 插一行,先把时间、源 MAC、目的 MAC 和长度填进去,协议解析在下一节做。
3.4 把裸帧变成可读列表:以太网帧与 IPv4 报文解析
抓包抓上来的是完整以太网帧,第一字节就是目的 MAC。解析格式要养成一个好习惯:不要直接把缓冲区强转成结构体,因为编译器会在结构体成员间插填充字节,字节序也是主机序。用偏移量加手工组字,看着笨但永远不会因为对齐翻车。先写两个工具函数:
quint16 getBE16(const QByteArray &buf, int offset) { return (quint16(quint8(buf[offset])) << 8) | quint8(buf[offset + 1]); } quint32 getBE32(const QByteArray &buf, int offset) { return (quint32(getBE16(buf, offset)) << 16) | getBE16(buf, offset + 2); }这是把网络字节序转成主机序的标准写法。网络抓包文件里所有多字节字段都是大端,你要是直接用主机序去读,源端口和目的端口会完全颠倒。写完工具函数,解析以太网帧:
struct EthInfo { QString srcMac; QString dstMac; quint16 etherType; int payloadOffset; }; EthInfo parseEthernet(const QByteArray &buf) { EthInfo info; info.dstMac = macToString(buf.mid(0, 6)); info.srcMac = macToString(buf.mid(6, 6)); info.etherType = getBE16(buf, 12); info.payloadOffset = 14; // 以太网头固定 14 字节 return info; }macToString 把 QByteArray 的六个字节格式化成冒号分隔的字符串,这个工具函数不在这里展开,按 xx:xx:xx:xx:xx:xx 写就行。注意如果 etherType 是 0x8100,帧头后面还跟着 4 字节的 VLAN 标签,真正的 payload 从 18 字节开始,这是后面避坑章要处理的点。
IPv4 报文从 payloadOffset 开始,头部最少 20 字节。需要取版本、头长、总长度、协议号、源 IP、目的 IP 六个字段就能撑起列表显示:
QString parseIPv4(const QByteArray &buf, int offset, quint8 *protocol, QString *srcIp, QString *dstIp) { quint8 versionIhl = quint8(buf[offset]); int headerLen = (versionIhl & 0x0F) * 4; quint16 totalLen = getBE16(buf, offset + 2); quint8 ttl = quint8(buf[offset + 8]); *protocol = quint8(buf[offset + 9]); quint32 src = getBE32(buf, offset + 12); quint32 dst = getBE32(buf, offset + 16); *srcIp = QString("%1.%2.%3.%4") .arg((src >> 24) & 0xFF).arg((src >> 16) & 0xFF) .arg((src >> 8) & 0xFF).arg(src & 0xFF); *dstIp = QString("%1.%2.%3.%4") .arg((dst >> 24) & 0xFF).arg((dst >> 16) & 0xFF) .arg((dst >> 8) & 0xFF).arg(dst & 0xFF); return QString("TTL=%1 Len=%2").arg(ttl).arg(totalLen); }versionIhl 的高 4 位是 IP 版本,低 4 位乘以 4 是头长度。无选项的普通 IPv4 头长度是 20,也就是 versionIhl 等于 0x45,头部偏移从 offset 往后数 20 字节正好接 TCP 头。TCP 的源端口和目的端口就在那 20 字节之后,按同样的 getBE16 方式取。填列表时,协议号 1 显示 ICMP,6 显示 TCP,17 显示 UDP,这样列表的“协议”列就有内容了。
到这里,你已经有一个能枚举网卡、抓包、把以太网和 IP 摘要显示在表格里的工具,这就是 sniffer 的最简可用版。接下来真正折磨人的是部署和运行时的那堆坑。
4. 抓包工具开发避坑:蓝屏、Qt 库混用与抓不到 VLAN 标签
4.1 npcap 驱动触发蓝屏:现象、原因与止血方案
现象很吓人:装了自研工具后,在拨号上网或者笔记本合盖唤醒的瞬间,系统直接蓝屏,常见错误码 0xD1 或者 0x50,指向 npcap.sys。第一次遇到时我也以为是自己代码写崩了,后来复现几次发现只有特定网卡加特定链路状态下才会触发。
原因是 npcap 的 NPF 驱动在 NDIS 6 过滤器链上跟某些网卡的驱动存在时序竞争,PPP 拨号链路建立那一刻,网卡驱动状态切换和过滤器绑定动作撞在一起,触发了访问越界。这不是你程序的问题,是驱动层的兼容性缺陷,老版本 npcap 更常见,新版已经收敛了不少。
解决分三步。第一步升级 npcap 到最新稳定版,多数普通触发能直接消失。第二步,在网卡高级属性里关闭电源管理里的“允许计算机关闭此设备以节约电源”,唤醒蓝屏的基本都靠这个解决。第三步,如果确定某个网卡不用抓包,在网络适配器属性的“Npcap”选项卡里直接卸载对该网卡的绑定,把影响面收窄。这段血泪经验就一句话:蓝屏先赖驱动,别急着重构自己代码。
4.2 cannot mix incompatible qt library:Qt 库版本混用的真面目
现象是程序启动瞬间崩溃,弹窗写着“cannot mix incompatible qt library (version ex50601) with this librar”。这个提示很有迷惑性,看起来像 Qt 被人为篡改,实际上就是运行时加载的 Qt 库版本和编译时用的头文件对不上。
常见触发路径有两条。一条是系统 PATH 里有多个 Qt 版本,比如之前装过 Qt 5.6 的老项目残留了 bin 目录,程序启动时先于程序目录加载到了旧 QtCore.dll。另一条是 Debug 和 Release 混用,用 Release 编译的 exe 却把 debug 版的 dll 复制到了同目录。version ex50601 里的 5.06.01 就是 Qt 5.6.1 的版本号,如果编译时用的是 5.15.2,那就铁定是这个原因。
解决办法是让加载路径变得可预期。开发机上检查系统环境变量 PATH,把多余的 Qt bin 目录清掉;发布时用官方自带的 windeployqt 工具处理:
windeployqt.exe --release --no-translations Sniffer.exe这个命令会把 exe 依赖的 Qt dll 和 platforms、styles 等插件目录一次性复制到目标目录。如果还崩,用 Dependencies 工具看 exe 实际加载到的 QtCore.dll 路径,指向哪里就修哪里。后来我养成一个习惯:发布包 exe 目录下只放 windeployqt 生成的 Qt 文件,从来不手动拷 dll,这个坑再没踩过。
4.3 qt.qpa.plugin 找不到 platform plugin:部署期最容易翻车的一环
现象是双击 exe 没有任何窗口,弹一个错误框,内容是“qt.qpa.plugin: could not find the qt platform plugin "windows"”。这个报错在 Qt 5.15 和 Qt 6 都常见,原因单一且明确:程序在找 platform 插件 dll 时找不到 platforms/qwindows.dll。
qwindows.dll 是 Qt 在 Windows 上创建窗口的底层插件,它不在自定义的发布目录里,必须从 Qt 安装目录的 plugins/platforms 下复制过来。很多人拷贝发布文件时只带了 Qt5Core.dll、Qt5Gui.dll 那些主 dll,漏掉整个 plugins 目录,程序就会报这个错。
解决有两种。一是 windeployqt 自动处理,它会生成 platforms 目录并放好 qwindows.dll,这是最省心的。二是手动部署,在 exe 同级目录建 platforms 子目录,把 Qt 安装目录下 plugins/platforms/qwindows.dll 复制进去。需要提醒的是:网上有些 Linux 交叉编译教程会提到 linuxfb 平台插件,那是嵌入式 Linux 环境下的东西,Windows 上不需要也不应该去下载 linuxfb,出现这条字样通常意味着你把某个 Linux 构建产物拷错到了 Windows 发布包里。代码本身没有错,就不要去改代码,问题全在部署目录结构。
4.4 停不下来:pcap_breakloop、pcap_next_ex 与线程退出竞态
现象是关闭窗口后进程迟迟不退,任务管理器里能看到残留,或者退出瞬间偶发崩溃。这个问题几乎每个写抓包工具的人都会遇到,我一度以为是 Qt 析构顺序问题,后来定位到是抓包停止的流程不对。
原因有两层。第一层是 pcap_loop 配合 pcap_breakloop 的行为依赖超时时间,timeout 设得很大时,breakloop 并不能立刻让阻塞中的抓包调用返回,必须等内核超时或者下一个包到达。第二层是如果直接在线程的析构函数里调用 pcap_close,另一个线程还阻塞在 pcap_next_ex 上,两边同时访问同一个 pcap_t 结构,崩溃概率极高。
正确的停止顺序必须固定,我一般按这个来:
void MainWindow::stopCapture() { m_worker->setRunning(false); // 让循环尽快退出 if (m_handle) { pcap_breakloop(m_handle); // 主动打断阻塞读 } m_thread->quit(); // 结束事件循环 m_thread->wait(3000); // 最多等 3 秒 if (m_handle) { pcap_close(m_handle); // 线程退出后再关设备 m_handle = nullptr; } }顺序是:先置停止标志,再 breakloop,然后等线程事件循环结束,最后才 pcap_close。还差一个关键点:pcap_next_ex 的超时不要设 0,设 0 在某些驱动上会一直阻塞直到有包,停止标志就永远等不到执行;设 1000 毫秒,停止指令最长 1 秒内生效。关窗口卡上几秒不一定是死锁,可能只是 breakloop 在等超时,给它一点容错空间。
4.5 抓不到 VLAN 标签包:网卡 offload 与 BPF 互斥
现象是这样的:交换机配置了 trunk,抓包工具跑着,报文列表里却永远看不到带 802.1Q 标签的帧,即便打开 Wireshark 也很难看到 VLAN 字段。很多人第一反应是过滤器写错,但过滤器根本没写。
原因在网卡驱动。Intel、Realtek 的主流网卡默认开启 VLAN offload,硬件在收包时就把 VLAN 标签剥离掉,驱动再以无标签的帧交给上层。这就意味着你抓到的数据已经不是网线上的原始帧了。Wireshark 在 Windows 上对 VLAN 支持不彻底,根源也在这里。
解决需要两步配合。第一步去网卡高级属性里找“VLAN ID”“packet priority”或“VLAN offload”相关选项,把硬件解析关掉,让标签保留在帧里。第二步是在 BPF 过滤里用 vlan 关键字,libpcap 语法已经原生支持:
vlan 10这条过滤器只保留 VLAN ID 为 10 的帧。如果网卡 offload 没关,BPF 就算写了也匹配不到,因为内核拿到的包已经被剥了标签。所以排查 VLAN 问题的顺序永远是:先看网卡属性,再谈过滤器。做完这个设置,以太网帧的 etherType 才可能是 0x8100,3.4 节提到的 VLAN 头解析才有意义。
5. 收尾的硬功夫:把抓包结果落成 pcap 文件,让 Wireshark 认账
5.1 用 pcap_dump 系列函数落地文件
抓包工具只有屏幕上滚动的数字是不够的,必须能把流量存下来给 Wireshark 做离线分析。最简单可靠的方式是用 pcap_dump_open、pcap_dump、pcap_dump_close 这套 API,它替你处理了 pcap 文件头的 24 个字节和每一帧的记录头:
pcap_dumper_t *dumper = pcap_dump_open(handle, "capture.pcap"); if (dumper == nullptr) { qCritical() << "打开 dump 文件失败: " << pcap_geterr(handle); return; } // 在抓包循环里,每帧记录 pcap_dump(reinterpret_cast<u_char *>(dumper), header, data); // 停止抓包后关闭 pcap_dump_close(dumper);pcap_dump_open 的第一个参数是 pcap_t 指针,文件头里的链路类型就是从这个句柄读的,所以不用自己写 network 字段。pcap_dump 的参数三件套和 pcap_next_ex 拿到的 header、data 完全对得上,拷贝进去即可。这套接口写出来的文件就是标准 pcap,Wireshark 双击就能打开。
如果想自己写文件格式探个底也是很好的学习路径。pcap 文件头就 24 字节:4 字节魔数 0xa1b2c3d4、2 字节主版本、2 字节次版本、4 字节时区修正、4 字节时间戳精度、4 字节 snaplen、4 字节链路类型。抓 Ethernet 时链路类型填 1,snaplen 填 65535。每帧数据前再补 16 字节记录头:时间戳秒、时间戳微秒、本帧实际长度、原始长度。但日常开发我建议直接用 pcap_dump,少写一半的字节序处理代码。
5.2 用 Wireshark 验证:时间戳、链路类型与过滤器
写完保存功能,验证方法只有一个:把生成的文件丢进 Wireshark,看它的反应。第一次验证要盯三个细节。
第一是时间列。Wireshark 打开后默认显示相对时间或绝对时间,如果所有包的时间戳都是 1970 年 1 月 1 日,说明 dump 写入正确但时间戳字段填错了位置。第二是链路类型。打开文件后在“链路”标签看是否显示 Ethernet,如果显示 Null/Loopback,说明 pcap_dump_open 拿到的句柄链路类型不对。第三是过滤器能否正常工作。在 Wireshark 过滤器栏输入tcp.port == 443,能筛出 HTTPS 连接,说明包内容和文件格式都对。想验证 SYN 包可以用显示过滤器:
(tcp.flags.syn == 1) && (tcp.flags.ack == 0)这条能看到 TCP 三次握手里的第一个 SYN。对应到自研工具里,就是 3.4 节解析 IP 头后继续解析 TCP 头部 flags 字段,在“信息”列显示 [SYN]、[ACK] 这些标记。
我自己的习惯是工具每改一次解析逻辑,就存一份 pcap,然后用 Wireshark 对照同一段流量的显示差异。有一次发现我解析的 IP 总长度比 Wireshark 少 14,排查了半天才发现是拿 Ethernet 头偏移当成 IP 解析起点,少跳了一个帧头的长度。这种问题光看自己程序的输出永远发现不了,和 Wireshark 对齐一次就清楚了。抓包工具开发到最后拼的不是抓包能力,而是对协议各字段语义的理解深度,希望这篇文章能帮你把第一块基石立稳。
本文还有配套的精品资源,点击获取