
简介这份华中科技大学2018年计算机网络安全实验源码面向学习网络协议分析、WinPcap抓包与ARP欺骗中间人攻击的高校学生及安全初学者可用于复现嗅探器实验并理解数据包捕获、解析与攻击演示的完整流程。压缩包共27个文件以C源文件和头文件为主体配有Qt工程、界面文件及少量图标资源整体约293KB结构紧凑便于在Qt环境中阅读和二次调试。已有2815人学习说明其实验设计具有一定参考价值。源码覆盖抓包线程、协议解析、ARP欺骗对话框与发包线程、主窗口交互等模块读者可据此梳理嗅探器从捕获到分析、从ARP攻击到中间人演示的实现脉络并对照实验要求完成环境搭建、代码阅读与排错。1. 从一次抓不到包的实验说起Winpcap 嗅探器到底在解决什么做计算机网络安全实验时最容易被低估的一步是数据包捕获。基于 Winpcap 的数据分析器嗅探源码核心不是调用一个抓包函数而是把网卡收到的原始字节按协议栈逐层拆开再决定哪些字段值得记录。它适合刚接触网络协议分析、需要一套能编译运行的实验源码的人也适合想对照检查自己解析逻辑的熟手。这份源码把抓包、链路层解析、IP 头拆分、TCP/UDP 会话字段提取和过滤规则串成一条线。很多人第一次跑会卡在驱动、权限或字节序上抓到的包要么为空要么字段全是乱码。下面按工程骨架、协议解析、过滤与落盘、排错、回放验证的顺序拆开讲尽量让每一步都能对着源码复现。2. 环境与工程骨架Winpcap 开发包、编译链和最小抓包循环2.1 为什么先选 Winpcap 而不是原始套接字在 Windows 平台做链路层嗅探原始套接字能拿到的信息有限权限和驱动限制也多。Winpcap 的路径更直接网卡收到帧后由内核态捕获驱动把数据放进环形缓冲区用户态通过wpcap.dll提供的 API 读出来。常见组成是捕获驱动、Packet.dll适配层和wpcap.dll接口层三层各管一段实验源码只面向wpcap.dll编程移植和调试都省事。选它还有一个现实原因实验源码里的pcap_findalldevs、pcap_open_live、pcap_loop、pcap_compile这些接口很稳定能直接对应到“选网卡、开混杂、收包、过滤”四个动作。对课程实验来说先把抓包链路跑通再往协议解析里填字段比一上来手写驱动靠谱得多。需要留意的是Winpcap 的开发包要同时配头文件和导入库只装运行库不够如果本机装的是兼容 Winpcap API 的抓包驱动安装时要把开发头文件和库勾上否则编译阶段就会缺pcap.h或链接不到wpcap.lib。提示抓包程序要以管理员权限运行。普通权限下pcap_findalldevs可能返回空列表或者只能看到回环设备这不是代码问题。2.2 工程骨架与依赖配置这套实验源码按职责拆成几个模块读的时候不要从main一路平铺看到底先看数据流main负责参数和设备选择capture负责打开网卡和回调注册decode_eth剥以太网头decode_ip判断 IPv4/ARPdecode_tcpudp取端口和标志位filter编译 BPF 表达式log负责格式化输出。模块之间只传“帧起始指针 caplen 时间戳”避免把pcap_pkthdr到处透传。模块关键函数输入输出mainparse_args、select_device命令行参数设备名、过滤表达式capturepcap_open_live、pcap_loop设备名、snaplen回调帧数据decode_ethdecode_eth帧指针、caplen以太类型、上层偏移decode_ipdecode_ipv4IP 起始指针五元组、分片信息decode_tcpudpdecode_tcp、decode_udp传输层指针端口、标志位、载荷长度filterpcap_compile、pcap_setfilter表达式字符串内核过滤器loglog_packet解析结果结构体CSV 或控制台行依赖配置走三步把头文件目录加到编译器的 include 路径把wpcap.lib加到链接器输入确认目标平台位数和库位数一致。下面是一段命令行编译示例路径按本机开发包位置替换不要照抄绝对路径。cl /nologo /W3 /IC:\pcap-sdk\Include ^ main.c capture.c decode_eth.c decode_ip.c decode_tcpudp.c filter.c log.c ^ /link /LIBPATH:C:\pcap-sdk\Lib\x64 wpcap.lib这段命令的逻辑很直白/I告诉编译器去哪找pcap.h/LIBPATH告诉链接器去哪找wpcap.lib源文件按模块列出最后链接抓包库。/W3打开常用警告结构体对齐和指针截断这类问题在抓包代码里很常见警告不要关。参数里唯一需要按机器改的是 SDK 路径和 x64/x86 目录32 位工程链 32 位库混用会在链接阶段报符号重定义或找不到入口。如果工程用 IDE 管理对应做法是“附加包含目录”和“附加库目录”各加一条“附加依赖项”填wpcap.lib。编译前把运行权限设为管理员或者编译后右键以管理员身份启动。很多“编译通过但抓不到包”的现场最后都落在权限和库位数上跟解析代码无关。2.3 最小抓包循环先跑通再谈解析在动协议解析之前先写一个只打印帧长度的最小循环。这个循环能把设备枚举、打开网卡、回调、资源释放四件事跑通后面所有解析函数都挂在这个回调上。#include pcap.h #include stdio.h static void packet_handler(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes) { // 这里只做长度打印先确认回调真的被触发 printf(ts%ld.%06ld caplen%u len%u\n, (long)h-ts.tv_sec, (long)h-ts.tv_usec, h-caplen, h-len); } int main(void) { pcap_if_t *alldevs NULL; pcap_if_t *d NULL; pcap_t *handle NULL; char errbuf[PCAP_ERRBUF_SIZE]; // 1. 枚举网卡errbuf 用于接错误文本 if (pcap_findalldevs(alldevs, errbuf) -1) { fprintf(stderr, findalldevs: %s\n, errbuf); return 1; } for (d alldevs; d ! NULL; d d-next) { printf(dev: %s\n, d-name); } if (alldevs NULL) { fprintf(stderr, no device, run as administrator\n); return 1; } // 2. 打开第一个设备snaplen 取 65535混杂模式 1超时 1000ms handle pcap_open_live(alldevs-name, 65535, 1, 1000, errbuf); if (handle NULL) { fprintf(stderr, open_live: %s\n, errbuf); pcap_freealldevs(alldevs); return 1; } // 3. cnt-1 表示一直抓直到 CtrlC 或出错 pcap_loop(handle, -1, packet_handler, NULL); pcap_close(handle); pcap_freealldevs(alldevs); return 0; }逻辑说明pcap_findalldevs拿到设备链表链表节点里的name才是pcap_open_live要的标识pcap_open_live返回的pcap_t是后续编译过滤器、读包、关闭句柄的入口pcap_loop每收到一个包就回调一次回调里的bytes只在本次回调期间有效不要把它存到全局后异步使用。参数说明snaplen65535保证常见帧不被截断promisc1打开混杂模式to_ms1000是读超时影响pcap_loop的返回节奏不是抓包间隔。errbuf大小用PCAP_ERRBUF_SIZE别自己写 256。这一步跑通的标准是控制台能持续打印caplen并且caplen len。如果只打印设备列表没有包先换一个活跃网卡再检查权限和过滤器。最小循环不要加解析解析一进来字段错位和抓不到包会混在一起排错成本翻倍。3. 从链路层往上剥以太网帧、ARP 与 IP 头的解析实现3.1 以太网帧头与类型字段以太网帧头固定 14 字节目的 MAC 6 字节、源 MAC 6 字节、类型 2 字节。类型字段决定上层走 IPv4、ARP 还是带 VLAN 标签的帧。写解析函数时第一件事是判断caplen是否大于等于 14第二件事是把类型字段从网络序转成主机序。结构体定义要加#pragma pack否则编译器可能插入填充字节字段偏移全乱。#include stdint.h #include arpa/inet.h #pragma pack(push, 1) struct eth_hdr { uint8_t dst[6]; uint8_t src[6]; uint16_t type; // 网络字节序 }; #pragma pack(pop) int decode_eth(const uint8_t *frame, uint32_t caplen, uint16_t *eth_type, uint32_t *l3_offset) { if (frame NULL || caplen sizeof(struct eth_hdr)) { return -1; // 截断帧直接丢弃 } const struct eth_hdr *eth (const struct eth_hdr *)frame; *eth_type ntohs(eth-type); *l3_offset sizeof(struct eth_hdr); // VLAN 标签类型 0x8100 时后面还有 2 字节 TCI 和 2 字节内层类型 if (*eth_type 0x8100) { if (caplen sizeof(struct eth_hdr) 4) { return -1; } const uint8_t *vlan frame sizeof(struct eth_hdr); *eth_type ntohs(*(const uint16_t *)(vlan 2)); *l3_offset sizeof(struct eth_hdr) 4; } return 0; }逻辑说明先做长度检查再把type转主机序遇到0x8100不急着解析内层先把偏移往后推 4 字节再读内层类型。参数说明caplen是实际抓到的字节数len是原始帧长度解析一律用caplen否则越界读会把回调打崩。l3_offset是上层协议起始偏移IPv4 解析从这里开始不要在函数里写死 14。常见类型值可以对照下表实验里最常碰到的是0x0800和0x0806。看到0x86DD说明是 IPv6这份源码如果只做 IPv4就在分支里记一条“未支持”后返回不要硬套 IPv4 头。类型值含义处理方式0x0800IPv4进入 IP 头解析0x0806ARP进入 ARP 解析0x8100VLAN 标签偏移加 4 后重读类型0x86DDIPv6记录后跳过避免误判3.2 IP 头字段与分片标志IPv4 头最短 20 字节首部长度字段ihl以 4 字节为单位所以实际头长是ihl * 4。总长度、标识、标志位加片偏移、校验和这些字段是网络序读出来要转换源和目的 IP 是 4 字节数组用inet_ntop转字符串。传输层偏移等于l3_offset ihl * 4不要写死l3_offset 20带选项的 IP 头会把 TCP 头读错。#pragma pack(push, 1) struct ipv4_hdr { uint8_t ver_ihl; uint8_t tos; uint16_t tot_len; uint16_t id; uint16_t flags_off; uint8_t ttl; uint8_t proto; uint16_t checksum; uint8_t src[4]; uint8_t dst[4]; }; #pragma pack(pop) int decode_ipv4(const uint8_t *ip, uint32_t caplen, uint32_t *l4_offset, uint8_t *proto, uint16_t *frag_off) { if (caplen sizeof(struct ipv4_hdr)) { return -1; } const struct ipv4_hdr *h (const struct ipv4_hdr *)ip; uint8_t ihl h-ver_ihl 0x0f; if (ihl 5) { return -1; // 首部长度非法 } uint32_t hdr_len (uint32_t)ihl * 4; if (caplen hdr_len) { return -1; // 选项被截断 } uint16_t flags_off ntohs(h-flags_off); *frag_off flags_off 0x1fff; // 片偏移单位 8 字节 *proto h-proto; *l4_offset hdr_len; // 非首片不带传输层头不能继续解析 TCP/UDP if (*frag_off ! 0) { return 1; } return 0; }逻辑说明ver_ihl高 4 位是版本低 4 位是首部长度flags_off的低 13 位是片偏移0x2000是 MF 标志0x4000是 DF 标志。返回 1 表示“这是后续分片停止上层解析”调用方不要把它当错误。参数说明caplen是 IP 起始位置之后还能读的字节数不是整帧长度l4_offset返回的是相对 IP 起始的偏移调用方要再加上l3_offset才是整帧偏移。IP 校验和可以验证但实验里不必每个包都算先把字段取对更重要。需要验证时把首部按 16 位累加取反注意首部长度和字节序。分片重组不在基础源码范围内遇到frag_off ! 0记录“分片”后跳过比强行解析出错误端口更可信。3.3 ARP 与 ICMP 的判定分支ARP 不是 IP不能走 IPv4 解析。ARP 头固定 28 字节包含硬件类型、协议类型、操作码、发送方 MAC/IP、目标 MAC/IP。实验里常看操作码 1 和 2分别对应请求和应答。ICMP 则挂在 IP 的proto1下面类型和代码在传输层起始位置的前两个字节。#pragma pack(push, 1) struct arp_hdr { uint16_t htype; uint16_t ptype; uint8_t hlen; uint8_t plen; uint16_t oper; uint8_t sha[6]; uint8_t spa[4]; uint8_t tha[6]; uint8_t tpa[4]; }; #pragma pack(pop) void decode_arp(const uint8_t *p, uint32_t caplen) { if (caplen sizeof(struct arp_hdr)) { return; } const struct arp_hdr *a (const struct arp_hdr *)p; printf(ARP oper%u spa%u.%u.%u.%u tpa%u.%u.%u.%u\n, ntohs(a-oper), a-spa[0], a-spa[1], a-spa[2], a-spa[3], a-tpa[0], a-tpa[1], a-tpa[2], a-tpa[3]); } void decode_icmp(const uint8_t *p, uint32_t caplen) { if (caplen 8) { return; // ICMP 头至少 8 字节 } uint8_t type p[0]; uint8_t code p[1]; printf(ICMP type%u code%u\n, type, code); }逻辑说明ARP 的oper要ntohsIP 字段直接按字节打印不需要转序。ICMP 只取类型和代码差错报文里还会带原始 IP 头不要递归调用 IP 解析否则遇到畸形包会无限展开。参数说明caplen是从 ARP/ICMP 起始位置算起的可读长度调用前已经扣掉以太网头和 IP 头。分支顺序建议先判断以太类型再判断 IP 协议号0x0806直接进 ARP0x0800进 IPv4IPv4 里proto1进 ICMPproto6进 TCPproto17进 UDP。每个分支入口都做一次长度检查别指望上层已经检查过。血泪经验是解析函数返回错误码比打印“解析失败”更有用调用方可以根据返回码决定是跳过、记录还是计数。4. 传输层会话还原TCP/UDP 解析与 BPF 过滤表达式4.1 TCP 头、端口与标志位TCP 头最短 20 字节数据偏移字段高 4 位表示首部长度单位是 4 字节。源端口、目的端口、序号、确认号、窗口这些字段是网络序标志位在偏移 13 的位置。解析 TCP 时先读数据偏移再算载荷起始位置不要把选项区当载荷。#pragma pack(push, 1) struct tcp_hdr { uint16_t sport; uint16_t dport; uint32_t seq; uint32_t ack; uint8_t off_res; uint8_t flags; uint16_t window; uint16_t checksum; uint16_t urg; }; #pragma pack(pop) int decode_tcp(const uint8_t *tcp, uint32_t caplen, uint16_t *sport, uint16_t *dport, uint8_t *flags, uint32_t *payload_len) { if (caplen sizeof(struct tcp_hdr)) { return -1; } const struct tcp_hdr *t (const struct tcp_hdr *)tcp; uint8_t data_off (t-off_res 4) 0x0f; uint32_t hdr_len (uint32_t)data_off * 4; if (hdr_len 20 || caplen hdr_len) { return -1; } *sport ntohs(t-sport); *dport ntohs(t-dport); *flags t-flags; *payload_len caplen - hdr_len; return 0; }逻辑说明off_res的高 4 位是数据偏移低 4 位保留flags里0x02是 SYN0x10是 ACK0x01是 FIN0x08是 PSH。参数说明caplen是从 TCP 头起始位置算起的可读字节数payload_len只表示抓到的载荷长度不等于原始 TCP 载荷长度截断帧里会偏小。需要还原会话时用四元组加序号做键别只按端口配对。4.2 UDP 头与伪首部校验UDP 头固定 8 字节源端口、目的端口、长度、校验和。长度字段是 UDP 头加载荷的总长度网络序。抓包截断时UDP 长度可能大于caplen解析要以caplen为准不能拿长度字段直接做循环上界。#pragma pack(push, 1) struct udp_hdr { uint16_t sport; uint16_t dport; uint16_t len; uint16_t checksum; }; #pragma pack(pop) int decode_udp(const uint8_t *udp, uint32_t caplen, uint16_t *sport, uint16_t *dport, uint16_t *payload_len) { if (caplen sizeof(struct udp_hdr)) { return -1; } const struct udp_hdr *u (const struct udp_hdr *)udp; uint16_t udp_len ntohs(u-len); if (udp_len sizeof(struct udp_hdr)) { return -1; } *sport ntohs(u-sport); *dport ntohs(u-dport); uint16_t captured (uint16_t)(caplen - sizeof(struct udp_hdr)); *payload_len (udp_len - sizeof(struct udp_hdr) captured) ? (udp_len - sizeof(struct udp_hdr)) : captured; return 0; }逻辑说明先校验 UDP 长度字段合法再取“长度字段声明的载荷”和“实际抓到的载荷”中较小的一个。参数说明payload_len是可用于后续应用层解析的安全长度校验和验证需要构造伪首部实验里可以先跳过但要在日志里标记“未校验”避免把校验失败当成协议错误。4.3 BPF 过滤规则别在应用层做全量过滤抓包器最怕“先全抓再 if 过滤”。包一多用户态拷贝和解析会把 CPU 吃满真正有用的包反而被丢。Winpcap 支持 BPF 过滤表达式在驱动层编译执行只有命中的包才进回调。常用做法是打开网卡后立刻编译并设置过滤器再进pcap_loop。#include pcap.h int install_filter(pcap_t *handle, const char *expr) { struct bpf_program fp; // optimize1 让 BPF 做优化netmask 未知时用 PCAP_NETMASK_UNKNOWN if (pcap_compile(handle, fp, expr, 1, PCAP_NETMASK_UNKNOWN) -1) { fprintf(stderr, compile: %s\n, pcap_geterr(handle)); return -1; } if (pcap_setfilter(handle, fp) -1) { fprintf(stderr, setfilter: %s\n, pcap_geterr(handle)); pcap_freecode(fp); return -1; } pcap_freecode(fp); // 设置成功后释放编译结果 return 0; }逻辑说明pcap_compile把字符串编译成 BPF 程序pcap_setfilter把程序挂到句柄上pcap_freecode释放编译产物。参数说明optimize1打开优化netmask在表达式里出现broadcast或net时才有意义不知道就传PCAP_NETMASK_UNKNOWN。常见表达式可以对照下表先窄后宽别一上来写ip全收。表达式用途注意点tcp port 80只看 HTTP端口是主机序写法BPF 自己处理udp port 53只看 DNS不区分方向时源/目的都命中host 192.0.2.10指定主机实验里用文档地址别用真实地址tcp[tcpflags] tcp-syn ! 0只看 SYN需要位移语法支持vlan and ip带 VLAN 的 IP先匹配vlan再匹配内层4.4 输出格式与日志落盘解析结果要落盘格式越稳定越好排错。实验源码常用 CSV一行一个包字段固定时间戳、源 IP、目的 IP、协议、源端口、目的端口、帧长、载荷长、标志位。写文件时不要在回调里反复fopen/fclose在main里打开一次回调里fprintf退出前fflush加fclose。多线程抓包时给日志加锁单线程回调不需要。void log_packet(FILE *fp, const struct pcap_pkthdr *h, const char *src, const char *dst, const char *proto, uint16_t sport, uint16_t dport, uint32_t payload_len, uint8_t flags) { // 时间戳拆成秒和微秒IP 字符串由调用方填好 fprintf(fp, %ld.%06ld,%s,%s,%s,%u,%u,%u,%u,0x%02x\n, (long)h-ts.tv_sec, (long)h-ts.tv_usec, src, dst, proto, sport, dport, h-caplen, payload_len, flags); fflush(fp); // 实验阶段保留避免程序中断丢最后几行 }逻辑说明CSV 字段顺序固定后面用脚本切分不会因为协议不同而错列。参数说明h-caplen是抓到的帧长payload_len是传输层载荷长度两者含义不同flags只对 TCP 有意义UDP 和 ICMP 填 0。fflush每个包都调用会拉低吞吐实验里可以接受正式跑量时改成按行缓冲或定时刷新。5. 避坑与排查抓不到包、字段错位和回调崩溃的五个现场5.1 现象pcap_findalldevs返回空列表现象很直接程序启动后设备列表为空或者只打印一个回环设备换网卡名也打不开。原因通常有三类驱动没装好、程序不是管理员权限、抓包服务没启动。Winpcap 的捕获驱动是内核态组件普通用户枚举设备时会被拦住有些机器装了多个抓包驱动头文件和运行库版本不匹配也会出现枚举成功但打开失败。解决顺序是先用管理员权限启动程序再看pcap_geterr返回的具体文本然后确认开发包的头文件、导入库和运行库来自同一套安装最后检查设备名是否带\Device\NPF_前缀pcap_open_live要的是设备标识不是“以太网”这种显示名。如果仍然为空换一个活跃网卡测试禁用再启用网卡后重试。不要在没有设备时继续调用pcap_open_live空指针解引用会直接崩。5.2 现象能抓到包但 IP 字段全错现象是回调有数据打印出来的源 IP、目的 IP、协议号却是乱码或固定值。原因多数在三个地方结构体没有#pragma pack编译器插入填充字节字段没有做ntohs/ntohl以太网头偏移没算 VLAN直接把 14 当 IP 起始。还有一种情况是caplen小于结构体大小代码仍然强转读取越界读到的是缓冲区外的脏数据。解决方法是先把帧的前 32 字节按十六进制打印出来对照以太类型和 IP 版本号人工核对。结构体一律加pack网络序字段显式转换IP 起始偏移由decode_eth返回不在解析函数里写死。每次强转前加长度检查caplen sizeof(struct ipv4_hdr)直接返回。带 VLAN 的帧先用0x8100分支把偏移推到 18再进 IP 解析。字段错位不是玄学按字节打印一轮基本都能定位。5.3 现象回调里打印乱码或程序崩溃现象是回调里printf出乱码或者抓几十个包后程序崩溃。原因通常是拿bytes当持久缓冲区用回调返回后继续访问或者把len当可读长度实际caplen更小再或者用%s打印没有终止符的载荷。回调里的bytes生命周期只到本次回调结束跨回调保存要自己memcpy一份。解决方法是所有解析以caplen为上界字符串打印用%.*s加长度不依赖\0。需要异步处理时把帧数据复制到自己的队列队列里存caplen和副本指针消费完再释放。回调里不要做耗时文件操作和递归解析ICMP 差错报文里带原始 IP 头递归调用容易把栈打穿。抓包程序崩在回调里先注释掉解析只留长度打印能复现就说明是解析越界。5.4 现象过滤表达式生效但漏包现象是加了tcp port 80后一个包都没有去掉过滤器就能抓到。原因可能是表达式写错、过滤器挂载位置不对或者链路层带 VLAN 导致 BPF 匹配不到内层 IP。BPF 在链路层执行tcp port 80默认按无 VLAN 的以太网帧偏移匹配带标签的帧需要先写vlan。另一种情况是pcap_setfilter返回值没检查编译失败但程序继续跑看起来像“过滤器没生效”。解决方法是先不过滤抓一小段确认流量确实存在再把表达式拆窄从ip到tcp到port逐级加带 VLAN 的环境用vlan and tcp port 80。每次pcap_compile和pcap_setfilter都检查返回值用pcap_geterr打印错误。netmask不确定就传PCAP_NETMASK_UNKNOWN别随手填一个错误掩码。过滤器漏包时先怀疑表达式再怀疑网卡和链路类型最后才怀疑库。5.5 现象Debug 能跑Release 崩现象是调试配置下抓包正常切到发布配置后启动就崩或者解析结果随机变化。原因常见于未初始化指针、缓冲区大小按调试堆布局“碰巧够用”、结构体对齐在优化后暴露、日志函数里用了只在 Debug 下存在的断言。抓包代码里还有一类坑pcap_t关闭后回调仍在跑Release 下时序变化更容易触发。解决方法是打开编译警告并逐条清理指针初始化为NULL缓冲区大小用sizeof和常量不写魔数。关闭顺序固定为先停止循环再pcap_close最后释放自己的队列。结构体强转前做长度检查Release 下也不要关掉运行时检查。日志里记录caplen、偏移和返回码崩一次就能定位到具体解析分支。后悔药是编译期多看一眼警告比运行期抓崩溃便宜得多。6. 验证与进阶用构造流量回放核对解析结果6.1 用构造流量回放核对解析结果实时网卡抓包受环境影响今天有流量、明天没有排错不可重复。更稳的做法是先抓一段固定流量存成 pcap 文件再用离线接口回放。用ping生成 ICMP用curl访问一个文档地址生成 TCP抓包时只留目标主机的流量保存后把解析器切到离线模式。下面这段代码把pcap_open_live换成pcap_open_offline回调不变。#include pcap.h int replay(const char *path) { char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *handle pcap_open_offline(path, errbuf); if (handle NULL) { fprintf(stderr, open_offline: %s\n, errbuf); return -1; } struct pcap_pkthdr *h NULL; const u_char *bytes NULL; int rc; // pcap_next_ex 返回 1 表示读到包0 表示超时-2 表示文件结束 while ((rc pcap_next_ex(handle, h, bytes)) 1) { packet_handler(NULL, h, bytes); } pcap_close(handle); return (rc -2) ? 0 : -1; }逻辑说明离线接口和实时接口共用同一套回调解析函数不用改验证成本最低。参数说明pcap_next_ex的返回值要区分 0、1、-1、-2文件结束是 -2不是错误h和bytes由库内部维护下一次调用会覆盖跨包保存要复制。回放时把过滤器关掉先看全量再在命令行里加表达式做对比。6.2 字段对照表与验收标准回放时拿同一份 pcap 用图形化抓包工具打开逐字段对照。重点看五元组、IP 总长度、TCP 数据偏移、UDP 长度和标志位。验收标准可以定三条同一包的源/目的 IP 和端口一致caplen与帧长关系合理分片包不输出传输层端口。字段对不上时先查结构体对齐再查字节序最后查偏移计算。字段预期常见偏差以太类型0x0800 / 0x0806未转主机序显示 0x0008IP 首部长度ihl * 4写死 20带选项时错位TCP 数据偏移off_res 4乘 4直接读off_res载荷起点偏UDP 载荷长度长度字段与 caplen 取小用长度字段做循环上界越界从那以后我每次改解析函数都先用固定 pcap 回放一遍再上实时网卡实时抓包只用来验证过滤器和权限不用来定位字段错误。这套实验源码适合当骨架按标题里的实验名去资源页取完整工程先跑最小循环再逐层加解析能少走不少弯路。希望帮到你。本文还有配套的精品资源点击获取