WinPcap UDP发包程序详解:从以太网帧构造到协议栈绕过
简介这是一份基于WinPcap实现的UDP发包示例工程适合学习网络编程、理解UDP协议以及上手WinPcap库的开发者参考。压缩包共349个文件大小约5.94MB核心为C/C源码.h、.cpp与Visual Studio工程文件.sln、.vcxproj、.filters另附静态库文件.lib、.a以及若干js/css/html辅助页面和图片素材解压后结构完整便于直接打开编译调试。已有188人学习浏览。源码围绕winpcap-udp-test示例展开演示如何初始化WinPcap、获取本机网卡并构造UDP数据报进行发送可帮助读者理解操作系统底层网络封包捕获与发送机制UDP无连接、低延迟的特点使其适用于音视频传输、在线游戏等场景读者也能在此基础上修改报文内容或重写发送逻辑开展自己的网络实验与二次开发。1. 基于WinPcap的UDP发包程序为什么还要自己拼数据包如果你用普通Socket发UDP系统会帮你把IP头和UDP头都封装好Wireshark抓到的包规规矩矩但这也就意味着你看不到底层到底发生了什么。而这个基于WinPcap实现的UDP发包程序最大的价值在于它绕过了操作系统协议栈直接从数据链路层把一帧完整的以太网报文发出去——从目的MAC、源MAC到IP头、UDP头再到你填的负载数据全都在自己手里握着。适合两类人一类是刚入门网络协议、想知道“UDP包在线上到底是什么形态”的初学者另一类是做网络测试、需要伪造源地址或构造特定报文的开发。这个项目是个很小的示例但麻雀虽小WinPcap拿网卡、开适配器、发原始帧这条链路是完整的。2. 工程结构与编译环境拿到源码先别急着跑2.1 压缩包里都有什么这个包里的文件结构和常见的VC工程不太一样它不是一堆.cpp摆在明面上而是带着编译产物一起给的。看文件清单能发现几个关键东西libwpcap.a和libpacket.a是WinPcap的静态库前者是对外的主要API库后者是底层Packet API库.aps文件是VC工程资源的二进制备份说明这个项目在Visual Studio里被打开过。时间戳集中在2014年2月底的一天内说明这是一个短时间内写出来的功能验证程序不是那种上万行的工业级工具。典型的文件构成和使用思路是这样的文件作用libwpcap.aWinPcap核心API的静态库提供pcap_open_live、pcap_sendpacket等接口libpacket.a底层Packet.dll的导入库普通开发一般不直接调用WinPcap_UDP_Test.apsVC工程资源的附加产物可以忽略若干源码文件发行包里一般有main.cpp、udp_packet.h之类的核心代码注意一个坑libwpcap.a这个后缀在Windows下一般是给MinGW或Cygwin用的。如果你打算用Visual Studio直接打开工程VC默认认的是wpcap.lib所以要确认编译工具链是MinGW还是MSVC这决定了后面链接库时写的是.a还是.lib。2.2 环境准备与基础库安装跑这个项目之前WinPcap的运行环境必须先装好。WinPcap由两部分组成一个驱动服务负责抓包和发包和一个用户态库wpcap.dll。2014年的项目现在装的话建议装WinPcap 4.1.3最终版或者换成NpcapWinPcap的继任者兼容模式能跑大部分老代码。装完之后检查一下C:\Windows\System32\wpcap.dll是否出现没有的话程序运行时会直接报“找不到wpcap.dll”。如果你要在MinGW下自己编译目录结构建议这样摆winpcap_udp_test/ ├── src/ # 源码目录main.cpp、packet_builder.cpp等 ├── include/ # WinPcap的头文件pcap.h、pcap-stdinc.h等 ├── lib/ # libwpcap.a、libpacket.a放在这里 └── Makefile编译命令大致是这样g -o udp_send.exe src/main.cpp \ -I./include \ -L./lib \ -lwpcap -lpacket \ -lws2_32这里链接ws2_32是因为WinPcap在Windows上和Winsock有交互比如pcap_lookupnet里用到了socket地址转换。如果你不链这个库编译会报一堆inet_ntoa之类的未定义引用这是很多人第一次编译时就翻车的点。3. 核心模块拆解从设备列表到链路层帧的完整组装3.1 设备和网卡选择逻辑UDP发包程序的第一步是拿到一张能发包的网卡。WinPcap负责这个的是pcap_findalldevs它会枚举机器上的所有网络接口返回一个pcap_if_t链表。每个节点里有两个字段必须注意name是供pcap_open_live使用的设备名description是给用户看的描述。如果程序里没有网卡选择这一步直接硬编码\Device\NPF_{GUID}换个机器就废了。常见的枚举实现长这样pcap_if_t *alldevs; char errBuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(alldevs, errBuf) -1) { fprintf(stderr, 枚举网卡失败: %s\n, errBuf); return -1; } for (pcap_if_t *dev alldevs; dev ! NULL; dev dev-next) { printf(设备名: %s\n, dev-name); if (dev-description) printf(描述: %s\n, dev-description); }pcap_findalldevs会同时返回回环接口和物理网卡如果你在虚拟机里跑经常能看见一堆“Adapter for loopback traffic capture”。选设备的原则是要发出去的包最终走哪块物理网卡就选哪个设备的name。比如要发给局域网里另一台机器就选连着交换机的以太网卡别选回环接口因为回环接口不走真实的以太网线。3.2 打开设备与发送通道的初始化选定设备之后下一步是打开它。WinPcap打开设备的函数是pcap_open_live这个函数会设置网卡为混杂模式如果传入PCAP_OPENFLAG_PROMISCUOUS并把驱动切换到抓包状态。但这个程序里有个区别发包其实不需要混杂模式混杂模式是为了抓别人的包才开的所以这里传0非混杂在纯发包场景下就够了。pcap_t *handle pcap_open_live(devName, 65536, 0, 1000, errBuf); if (handle NULL) { fprintf(stderr, 无法打开设备 %s: %s\n, devName, errBuf); return -1; }参数说明devName上一步pcap_findalldevs拿到的设备名字符串。65536抓包快照长度发包场景用不到抓包但这是函数签名必填项填个MTU最大值最保险。0promisc标志这里关闭表示不抓别人流量。1000读取超时毫秒数发包不依赖这个但传0在某些老驱动下会有问题。errBuf失败时错误信息会写到这里固定大小PCAP_ERRBUF_SIZE256字节。打开之后有个隐蔽问题这块网卡如果正在被Wireshark占用开启抓包时pcap_open_live会返回失败因为NPF驱动同一时间只允许一个进程独占打开。这是WinPcap二十多年没变过的限制程序里没有做重试的话把Wireshark的抓包停掉就能解决。3.3 UDP报文构造从MAC头到负载数据的逐字节拼装这一步是整个项目最核心的代码也是用WinPcap发包和用Socket发包的本质区别所在。Socket发包时操作系统帮你填充MAC地址通过ARP缓存、IP头校验和、UDP长度你只给数据就行。但现在我们要自己写一个字节数组把一帧完整的以太网包拼出来。#pragma pack(push, 1) typedef struct eth_header { uint8_t dest_mac[6]; uint8_t src_mac[6]; uint16_t ether_type; // 0x0800 表示IPv4 } eth_header_t; typedef struct ip_header { uint8_t version_ihl; // 版本4位 首部长度4位 uint8_t tos; uint16_t total_len; uint16_t ident; uint16_t flags_frag; uint8_t ttl; uint8_t protocol; // 17表示UDP uint16_t checksum; uint32_t src_addr; uint32_t dst_addr; } ip_header_t; typedef struct udp_header { uint16_t src_port; uint16_t dst_port; uint16_t length; uint16_t checksum; } udp_header_t; #pragma pack(pop)#pragma pack(push, 1)非常关键它让编译器按1字节对齐结构体不加的话编译器会按4字节对齐在结构体字段之间塞填充字节报文就全乱了。以太网帧头14字节662IP头20字节UDP头8字节——这三个数加起来42字节正好是UDP包在以太网上的最小头长度如果你拆包后看到帧头长度不是42说明对齐没设对。3.4 组装完整帧并调用pcap_sendpacket结构体定义好之后要把它们填到一块内存里。这个项目里可以按地址偏移直接操作也可以把结构体外层套个总帧结构体我一般用后一种代码更容易读uint8_t frame[14 20 8 payload_len]; eth_header_t *eth (eth_header_t *)frame; ip_header_t *ip (ip_header_t *)(frame 14); udp_header_t *udp (udp_header_t *)(frame 14 20); uint8_t *payload frame 14 20 8; // 填充MAC地址 memcpy(eth-dest_mac, dest_mac, 6); memcpy(eth-src_mac, src_mac, 6); eth-ether_type htons(0x0800); // 填充IP头 ip-version_ihl 0x45; // IPv420字节首部 ip-total_len htons(20 8 payload_len); ip-protocol 17; // UDP ip-src_addr htonl(src_ip); ip-dst_addr htonl(dst_ip); // 填充UDP头 udp-src_port htons(src_port); udp-dst_port htons(dst_port); udp-length htons(8 payload_len); udp-checksum 0; // 按照标准可以直接填0但建议算 // 发送 if (pcap_sendpacket(handle, frame, sizeof(frame)) ! 0) { fprintf(stderr, 发包失败: %s\n, pcap_geterr(handle)); }这段有三个容易写错的字段挨个说明一下ip-total_len是整个IP包的长度包含IP头本身所以是208payload_len写错的话接收端会认为包不完整直接丢。udp-length是UDP头数据的总长即8payload_len这个值和IP头的total_len不要混淆。udp-checksum严格来说在IPv4下是可选的接收端可以忽略但很多网卡和系统默认开了UDP校验和验证如果你填0Wireshark会显示“UDP checksum offload”或直接标红数据包仍然能发出去且对端能收到但抓包分析时很不干净。pcap_sendpacket内部会直接把这个帧交给NPF驱动驱动从网卡队列里发出去。注意这个函数返回0才是发送成功跟socket编程的习惯相反send返回字节数0才算成功很多人刚上手时判断条件写反了包发不出去还一脸懵。3.5 校验和计算的正确姿势上面的代码里校验和填0能跑但作为一个认真做网络协议的测试工具最好把IP校验和和UDP校验和都算出来。校验和算法很简单把所有16位对齐的值相加超出16位的部分回卷相加最后取反。IP头校验和只覆盖IP头本身UDP校验和覆盖UDP头、数据和一个伪IP头伪头包含源/目的IP和协议字段用于防止包被错误路由。uint16_t checksum(uint16_t *addr, int len) { uint32_t sum 0; while (len 1) { sum *addr; len - 2; } if (len 1) sum *(uint8_t *)addr; while (sum 16) sum (sum 0xffff) (sum 16); return (uint16_t)(~sum); }这个函数是经典的RFC 1071实现只依赖32位累加器不依赖平台字节序因为最终取反时用ntohs转换由调用方负责。UDP校验和需要构造伪头伪头长这样12字节源IP4字节目的IP4字节0字节协议1字节UDP长度2字节和UDP头、数据一起参与校验计算。伪头不发送到线上只参与计算这是包构造里最容易忘的一块。4. 常见问题排查与避坑指南WinPcap发包的四大翻车现场4.1 “发送成功”但Wireshark里什么都抓不到现象程序在管理员权限下运行pcap_sendpacket返回0但Wireshark里就是看不到自己发的UDP包。原因分两种。第一种是Wireshark抓包时机不对——WinPcap发包时网卡是直接发送的Wireshark如果是在另一块网卡上抓包比如你发到以太网卡但Wireshark监听在WiFi卡上自然看不到。第二种是驱动层面丢包——很多USB外置网卡和老版本WinPcap驱动不兼容包进了驱动队列但没真正上线路。解决先确认Wireshark监听的接口和pcap_open_live选的是同一个name再关掉Wireshark的抓包改用这个发包程序自收自发发完立刻用pcap_next_ex去读同一块网卡如果自己发自己收也看不到基本就是驱动问题换Npcap的WinPcap兼容模式重装驱动。4.2 编译时出现大量“未定义引用”错误现象MinGW编译时链接阶段报pcap_open_live、pcap_sendpacket未定义但头文件明明包含了。原因库链接顺序不对。GNU工具链的静态库有依赖顺序libpacket.a依赖libwpcap.a而libwpcap.a又依赖libws2_32你如果写成-lpacket -lwpcap -lws2_32就没事写反了GCC就会丢符号。解决把库顺序固定成从底层到高层依赖关系排列g -o udp_send.exe main.c -lwpcap -lpacket -lws2_32如果还报错就在命令末尾加-Wl,--start-group -lwpcap -lpacket -Wl,--end-group强制循环解析。4.3 程序一打开就是“找不到网卡设备”现象在Windows 10/11上运行pcap_findalldevs返回的设备列表为空或者只有loopback接口。原因WinPcap的老驱动4.1.3之前的版本不支持新系统的网络栈。另一个隐藏原因是你装了Npcap但没有勾选“WinPcap API兼容模式”这个选项默认不勾。解决卸载WinPcap安装Npcap时勾上“Install in WinPcap API-compatible Mode”。装完重新跑程序设备列表就正常了。4.4 构造的包在Wireshark里显示“malformed”或“bad checksum”现象包发出去了Wireshark也能看到但解析不出来报IP长度错误或校验和错误。原因绝大多数是htons/htonl用错了地方。以太网和IP头里的多字节字段在网络字节序是大端而x86机器是小端你要发的数字必须转成大端再写入。忘转的典型症状是UDP目标端口填的5000抓包里显示12880之类的怪数字。解决给所有写到结构体里的多字节字段加上htons或htonl包括ether_type、total_len、udp-port、src_addr。注意version_ihl是单字节的0x45不用转。4.5 本机能收到但目标机器收不到现象发包程序跑通Wireshark本机也验证了包在线上的格式正常但接收方的应用说没收到。原因目的MAC地址填错了或目的IP不是本网段。如果你把目的MAC填成了网关的MAC但目的IP填的是同网段另一台机器的IP数据包会到达网关后被网关丢弃因为网关发现这个IP不在它的路由表内。反过来如果目的IP是跨网段地址但MAC填的是本网段主机的MAC那包就只在那台主机上转一圈上不了外网。解决写死MAC之前用ARP先拿到目标IP对应的真实MAC。最简单的方法是在发包程序里加一个步骤调用系统arp -a命令查询或者直接假设目标在同网段把MAC填成广播地址FF:FF:FF:FF:FF:FF让交换机帮你广播出去目标网卡收到广播帧再解包认IP。5. 运行验证与效果观察拿Wireshark当照妖镜5.1 启动程序前的准备工作这个程序没有图形界面是个控制台工具。运行之前需要把三件事准备好第一用管理员身份打开命令提示符因为WinPcap的NPF驱动要求程序提权才能访问网卡第二关闭正在跑Wireshark或其他占用网卡的抓包程序第三确定接收方——可以是本机的另一个IP也可以是局域网里随便一台机器但发跨网段包时要确认路由器允许转发。启动后程序一般会打印出找到的网卡列表让你输入序号选择然后询问源IP、目的IP、端口、负载内容、发包数量。有的版本是硬编码的参数在源码里需要重新编译才能改看你拿到的源码里main函数怎么写的。典型运行效果如下举例[1] 以太网 - Realtek PCIe GBE [2] 本地连接 - VirtualBox Host-Only 请选择网卡编号: 1 请输入源IP地址: 192.168.1.100 请输入目的IP地址: 192.168.1.200 请输入源端口: 12345 请输入目的端口: 6789 请输入发送次数: 10 开始发送... 已发送 10 个UDP数据包5.2 Wireshark里验证包格式发包的同时打开Wireshark监听同一块网卡过滤规则直接写udp.port 6789或者ip.src 192.168.1.100。双击抓到的包展开Ethernet II、Internet Protocol Version 4、User Datagram Protocol三层手动核对几个关键字段Ethernet层Source显示的是你填的源MACType是IPv40x0800。IP层Protocol是UDP17Header checksum没有标红。UDP层Source Port和Destination Port和你填的一致Length是8负载长度。UDP Payload里能看到你输入的ASCII内容如果有的话。这个验证过程本身就是WinPcap和Socket编程最大的区别体验用Socket发UDP的人永远不会亲眼看到MAC头和IP头长什么样而这个程序让这些细节裸奔在眼前。5.3 数据包抓不到时的排查路径如果Wireshark开了但一个包都没抓到按这个顺序查先看过滤条件是不是写反了比如监听的是回环接口但包发在物理网卡上再确认发包程序选的设备编号和Wireshark的接口名对应的是同一个适配器然后看Wireshark左下角有没有显示“已捕获”的计数在涨——如果涨了但过滤栏空白说明过滤写错了如果计数也没涨回去查驱动兼容性换个网卡接口再试。6. 二次开发思路把这个小工具改造成自己的协议测试台这套代码的骨架足够简洁适合在上面做各种协议实验。我建议的改造路径是从“发固定包”到“发可控包”逐步把它变成你自己的底层网络工具箱。第一步把硬编码的参数抽出来放到配置文件里。用Windows自带的INI文件读取就行不用引第三方库。这样改的好处是你不用为每次测试都重新编译源码输入不同的IP、端口、负载改成改INI文件。INI的格式很简单[target] src_ip192.168.1.100 dst_ip192.168.1.200 src_port12345 dst_port6789 dest_macFF-FF-FF-FF-FF-FF payloadhello_world_from_winpcap count100 interval_ms10count和interval_ms是控制发包压力和频率的加这两个字段能让工具做简单的性能测试——往目标打1000个包测它的接收丢包率。第二步加上自定义负载构造函数。现在的程序负载是用户输入的一段ASCII字符串但实际做网络测试时经常要发二进制数据。改造办法是在main里加一个模式判断如果配置文件里payload_hex1就把payload字段按十六进制解析AABBCCDD转成四个字节否则按ASCII原样传输。if (cfg.hex_mode) { for (int i 0; i len / 2; i) { sscanf(cfg.payload[i * 2], %2hhx, payload[i]); } }这个%2hhx格式符在C99里才能用MSVC从VS2013开始支持老编译器如果你在用就换成手动解析两个字符拼字节。第三步给程序加一个简单的统计输出。每次发包后计算一下平均发包时延用clock()或GetTickCount64打印总共发了多少个字节。这一步看起来简单但对排查网络性能问题很有用——如果你往千兆网卡上发小包永远只会看到“发送成功”有了时延统计就能发现驱动层面的拥塞。第四步进阶把这次学的WinPcap发包能力做成一个DLL供其他项目调用。接口可以这么设计int udp_send_init(const char *dev_name); int udp_send_frame(uint8_t *payload, int len, uint32_t dst_ip, uint16_t dst_port); void udp_send_cleanup(void);这三件套封装了“打开设备、构造包、发出去”的全过程。DLL的好处是以后你做其他项目比如模拟器、安全工具、网络测量程序就能直接引这个库发底层包不用把抓网卡的逻辑在各处重复。封装时记得把校验和计算写在内部对上层调用者隐藏htons之类的细节避免别人二次开发时踩第3.5节里说的坑。还有个值得提的改造点把捕获功能加回来。现在项目只有发包但WinPcap最擅长的是抓包。你可以在同一个pcap_t句柄上先pcap_sendpacket发一帧然后立刻调用pcap_next_ex读回来实现“自收自发”的回环测试。用这个模式做验证时连第二台电脑都不需要了。这套代码我前后拆过好几遍每次给新人讲WinPcap都拿它当起点。从小处说它是UDP发包从大处看它是底层协议栈的练习板。这个工具跑通的那一瞬间你才真正意识到平时那些Socket API替你干了多少活。从那以后我每次做网络编程都强制自己在Wireshark里看一遍实际线上的包长什么样再往下写业务逻辑这个习惯一直保留到现在。希望帮到你。本文还有配套的精品资源点击获取