ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++手写ARP扫描器:报文构造、原始套接字与局域网主机发现实战

C++手写ARP扫描器:报文构造、原始套接字与局域网主机发现实战 简介面向计算机网络课程设计这份PDF以C实现ARP协议获取局域网活动主机物理地址为主线完整呈现广工课程设计报告。内容包括ARP工作原理、以太网与ARP帧结构、基于Winpcap库的数据帧构造与收发解析方法以及Visual C环境下的代码分析适合高校网络课程学生、毕业设计者和初学网络编程的开发者参考。资源为单个PDF文件大小约458KB集中了设计题目、已知参数、实施步骤和核心源码片段便于离线阅读与对照实践。目前已有169人学习属于轻量但内容紧凑的课程设计参考文档。文档后半部分详细展示了包含pcap.h、Packet32.h的代码涵盖获取本机MAC、发送ARP请求、捕获并解析响应数据包、输出IP-MAC映射等关键函数并解释了直接、广播、混杂等网卡模式能帮助读者深入理解网络层与数据链路层的地址解析交互快速套用到自己的抓包或扫描工具开发中。1. 用 C 手写 ARP 扫描器广工计网课设到底在考什么如果你是广工计算机专业的学生大概率会在计算机网络课程设计里碰到这个题目使用 ARP 协议获取局域网内部活动主机物理地址。第一次看到“活动主机”和“物理地址”这两个词很多人以为要写一个类似“局域网IP扫描器”的图形界面工具但拿到任务书才发现核心要求是让你自己构造 ARP 报文通过网卡发出去再解析返回的应答报文把局域网里在线主机的 IP 和 MAC 地址对应关系列出来。说白了就是让你用 C 从零实现一个最简版本的 ARP 扫描器——不依赖系统命令不调用现成的 ping 或 arp -a而是自己动手拼报文。这个课题放到课程设计里很巧妙它不高深却把以太网帧结构、ARP 协议格式、socket 编程、字节序转换、超时重传这几个计算机网络最核心的知识点全串起来了。你做完之后能彻底搞懂“IP 地址怎么翻译成 MAC 地址”这一底层机制也能对 raw socket 和抓包分析有直观的认识。适合的人群很明确正在做计网课设的本科生、想补网络编程基础的后端开发者以及准备网络方向面试但一直没碰过报文构造的求职者。我按常见的实战方案把这套程序的完整实现思路和避坑经验拆给你照着搭一个能跑的最小版本不难。2. ARP 报文结构拆解先弄懂 28 字节怎么拼再谈代码2.1 以太网帧头14 字节的开销不能算错否则收不到应答写 ARP 程序之前必须先清楚数据在局域网里传输的真实格式。ARP 报文不是单独在网络上跑的它外面要套一层以太网帧头。这 14 个字节长这样6 字节目的 MAC 地址、6 字节源 MAC 地址、2 字节上层协议类型。对 ARP 请求来说目的 MAC 地址填广播地址 FF:FF:FF:FF:FF:FF协议类型填 0x0806。很多人第一次写只构造了 28 字节的 ARP 报文就发出去结果网卡直接丢弃因为对方网卡根本不知道该把这个数据包交给谁。以太网帧头后面才是真正的 ARP 报文固定 28 字节。它的结构是2 字节硬件类型以太网填 1、2 字节协议类型IP 协议填 0x0800、1 字节硬件地址长度MAC 是 6、1 字节协议地址长度IP 是 4、2 字节操作码请求是 1应答是 2、6 字节发送方 MAC、4 字节发送方 IP、6 字节目标 MAC、4 字节目标 IP。整个以太网帧加 ARP 报文共 42 字节如果网卡要求最小帧长 60 字节还得补 18 字节填充但一般 socket 发送时系统会处理。写代码前有个关键点必须确认网卡发送时自动填充以太网帧头还是需要我们手动构造这取决于你用的 socket 类型。用 SOCK_RAW 且自己构造完整帧或者用 SOCK_PACKETLinux 老接口都需要自己拼帧头如果走标准 UDP socket你根本没机会控制 ARP 报文。常见做法是用 AF_PACKET 协议族的 SOCK_RAW socket自己把 42 字节的帧完整拼好再 sendto。Windows 下实现方式不同它需要用 WinPcap 或 Npcap 的库函数因为 Windows 对 raw socket 限制严格。广工课设一般在 Windows 环境跑常见方案就是装 Npcap 并链接 wpcap 库。2.2 字节序陷阱MAC 地址逐字节写IP 地址要转网络序字节序是 ARP 程序最容易翻车的地方。MAC 地址本身是一个字节数组直接按顺序填入就能得到正确的物理地址但 IP 地址是 32 位整数内存存储分大端和小端。ARP 报文里 IP 地址必须按网络字节序大端存放。你在代码里定义一个struct in_addr它内部已经是网络序可以直接用但如果你自己把 IP 字符串转成整数再塞进报文就一定要调用inet_addr()或inet_pton()。举个例子IP 地址 192.168.1.1在内存里如果是小端序的整数0x0101A8C0直接 memcpy 到报文里就会变成 1.1.168.192目标主机收到后根本不会应答。正确处理方式是用inet_addr(192.168.1.1)得到网络序整数或者干脆一个字节一个字节地填到字符数组里——粗暴但最不容易错。这段代码展示了完整的 42 字节帧构造流程struct EtherHeader { uint8_t dest_mac[6]; uint8_t src_mac[6]; uint16_t ether_type; }; struct ArpPacket { uint16_t hardware_type; // 1: Ethernet uint16_t protocol_type; // 0x0800: IPv4 uint8_t hardware_size; // 6 uint8_t protocol_size; // 4 uint16_t opcode; // 1: request, 2: reply uint8_t sender_mac[6]; uint32_t sender_ip; uint8_t target_mac[6]; uint32_t target_ip; }; // 使用 htons 转网络序MAC 地址逐字节填充 uint8_t frame[42]; memset(frame, 0xFF, 6); // 目的 MAC广播地址 memcpy(frame 6, local_mac, 6); // 源 MAC本机网卡地址 frame[12] 0x08; frame[13] 0x06; // 以太网类型ARP ArpPacket* arp (ArpPacket*)(frame 14); arp-hardware_type htons(1); arp-protocol_type htons(0x0800); arp-hardware_size 6; arp-protocol_size 4; arp-opcode htons(1); // ARP 请求 memcpy(arp-sender_mac, local_mac, 6); arp-sender_ip inet_addr(local_ip.c_str()); memset(arp-target_mac, 0, 6); // 请求时目标 MAC 置 0 arp-target_ip target_ip; // 已经是网络序不要用(ArpPacket*)(frame 14)强转后直接给成员赋值就完事。结构体存在内存对齐问题如果编译器自动填充了字节你填的字段位置就和协议格式对不上。要么给结构体加#pragma pack(push, 1)禁止对齐要么直接用memcpy一个字段一个字段地拼第二种方式更直观。我一般用一段整字节数组按偏移量手动填充这样每一步都看得见排查报文问题也方便。提示首包先抓下来看十六进制转储帧头和报文偏移对不上时八成是对齐问题而非字节序问题。2.3 获取本机 MAC 和 IP三种常见做法课设推荐用 GetAdaptersInfo构造 ARP 报文需要先知道自己的 MAC 地址和 IP 地址这个信息不能写死因为程序换一台机器就要能跑。Windows 下常见做法是调用GetAdaptersInfo这个 API它能枚举所有网卡并返回每块网卡的 MAC、IP、子网掩码等详细配置。遍历返回的链表找到状态为活动的、IP 地址属于局域网网段的那一项。Linux 下则简单很多用ioctl(sock, SIOCGIFHWADDR, ifreq)就能拿到硬件地址拿 IP 用SIOCGIFADDR。拿到 MAC 后要注意一个细节GetAdaptersInfo返回的 MAC 地址是 6 字节数组直接输出时要格式化建议每个字节补零大写比如A4:5E:45:8B:0F:2C。程序里保存时保持原始字节数组不要转成字符串又转回来减少出错环节。如果机器有虚拟网卡VMware 或 VirtualBox 的虚拟网卡枚举时要根据AdapterName或Description字段过滤常见过滤方式是跳过包含 VMware、VirtualBox、Loopback 的项。不滤掉虚拟网卡扫出来的结果里会混入虚拟机的 MAC 地址课设答辩时容易被老师追问。3. 用原始套接字发 ARP 请求从 sendto 到 recvfrom 的完整链路3.1 AF_PACKET 与 SOCK_RAW为什么普通的 UDP socket 干不了这活ARP 协议和 TCP/UDP 完全不是一个层级。TCP/UDP 是传输层你在应用层用socket(AF_INET, SOCK_DGRAM, 0)创建的 socket操作系统的协议栈会自动帮你处理 IP 头和以太网头的封装。但 ARP 是链路层协议它直接工作在以太网帧之上没有 IP 头也没有端口号。标准 socket 接口根本不暴露这一层的收发能力所以必须用原始套接字。Linux 下常见的组合是socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP))其中第三个参数指定只接收 ARP 协议类型的帧可以避免收到大量无关的广播包。发送时sendto的目标地址结构体是sockaddr_ll它有个关键字段sll_ifindex指定从哪块网卡发出去。不设这个字段或者设错了可能发不出去也可能从错误的网卡发出导致对方收不到请求。接收端用recvfrom收数据收到的是一个完整的以太网帧我们需要跳过 14 字节帧头然后检查opcode字段是 1 还是 2。如果是 2说明是 ARP 应答。收到应答后从报文的 sender_mac 字段提取物理地址从 sender_ip 字段提取 IP 地址这样就完成了一个活动主机的发现流程。int sock socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); if (sock 0) { perror(socket); exit(1); } struct sockaddr_ll addr; memset(addr, 0, sizeof(addr)); addr.sll_family AF_PACKET; addr.sll_protocol htons(ETH_P_ARP); addr.sll_ifindex if_nametoindex(eth0); // 换成你的网卡名 struct sockaddr_ll dest; memset(dest, 0, sizeof(dest)); dest.sll_family AF_PACKET; dest.sll_protocol htons(ETH_P_ARP); dest.sll_ifindex addr.sll_ifindex; dest.sll_halen 6; memset(dest.sll_addr, 0xFF, 6); // 目的 MAC 为广播 if (sendto(sock, frame, 42, 0, (struct sockaddr*)dest, sizeof(dest)) 0) { perror(sendto); } uint8_t buffer[1024]; while (true) { int len recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL); if (len 42) continue; EtherHeader* eth (EtherHeader*)buffer; if (ntohs(eth-ether_type) ! 0x0806) continue; ArpPacket* arp (ArpPacket*)(buffer 14); if (ntohs(arp-opcode) ! 2) continue; printf(IP: %s MAC: %02X:%02X:%02X:%02X:%02X:%02X\n, inet_ntoa(*(struct in_addr*)arp-sender_ip), arp-sender_mac[0], arp-sender_mac[1], arp-sender_mac[2], arp-sender_mac[3], arp-sender_mac[4], arp-sender_mac[5]); }用recvfrom会收到本机自己发出的包和其他主机的广播包一定要做三层过滤以太网类型必须是 0x0806、ARP 操作码必须是 2应答、发送方 IP 必须是你查询的目标 IP。有些 ARP 应答是其他主机主动广播的虽然能提供物理地址但如果不是你要扫的 IP直接忽略避免误报。提示recvfrom是阻塞调用需要设置超时或配合select使用否则程序会卡在循环里永远不退出。3.2 为什么要设置 SO_BROADCAST不设这个选项的后果有人写代码时用setsockopt设置了SO_BROADCAST选项也有人发现不设也能发出去。这个选项的作用是允许 socket 发送广播报文对于 AF_PACKET 的原始套接字内核不一定强制检查但如果你换用其他类型 socket不设置就可能返回 Permission denied。规范做法是发送前统一设置一次避免不同 Linux 版本内核行为不一致导致程序换个环境就翻车。广播帧的构造有个细节目的 MAC 是全 F但目标 IP 必须是具体的 IP 地址不能是广播地址 255.255.255.255。ARP 请求本身就是用来查询某个具体 IP 对应的 MAC 地址的报文里的 target_ip 字段填的就是你要扫描的 IP。如果填了广播 IP所有主机都会收到请求但按 RFC 826 的规定只有 IP 匹配的主机才会应答所以结果一样但不太规范。设置完 socket 后建议先向自己本机 IP 发一个 ARP 请求验证链路。本机收到后一定会应答因为协议栈对发往本机 IP 的 ARP 请求会立即处理。如果连本机都收不到应答说明 socket 设置或报文构造有问题这时抓包分析比盲改代码更高效。4. 扫描策略与超时重传扫一个 IP 容易扫整个网段才是课设难点4.1 顺序扫描还是并发扫描三种方案的开销对比课程设计要求扫描整个局域网常见网段是 192.168.1.0/24 或 10.0.0.0/24最多 254 个 IP。最直观的方法是一个 for 循环逐个发送 ARP 请求每发一个就阻塞等待应答。这种方式代码最简单但性能最差如果目标主机不存在需要等完整超时时间比如 1 秒254 个 IP 就是 254 秒课设演示时老师等不了那么久。第二种方案是每发一个请求就立刻往下发下一个最后统一收应答但因为 socket 接收缓冲区有限大量未应答的请求堆积应答到达时你可能已经错过了前面的包。第三种也是常见做法用非阻塞 socket 加select做事件循环或者开多线程每个线程处理一个 IP 的请求和响应。考虑到课设的复杂度推荐用非阻塞sendto后立即进入select循环的方式代码量适中效率也能接受。多线程方案虽然更炫但会引入共享资源竞争问题——多个线程同时往同一个 socket 发数据、同时从同一个 socket 收数据需要加锁或用每个线程独立的 socket。每个线程一个 socket 更安全但会增加系统资源占用。对课设来说256 个 socket 同时打开没问题只要注意程序退出时关闭全部 socket。struct ScanTask { uint32_t ip; bool done; }; void scan_thread(ScanTask* task, int interval_ms) { int sock socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); // 构造帧、设置 sockaddr_ll、发送略 fd_set fds; struct timeval tv; tv.tv_sec 0; tv.tv_usec interval_ms * 1000; for (int i 0; i 3; i) { // 最多重试 3 次 sendto(sock, frame, 42, 0, (struct sockaddr*)dest, sizeof(dest)); FD_ZERO(fds); FD_SET(sock, fds); int ret select(sock 1, fds, NULL, NULL, tv); if (ret 0) { // 接收并解析应答 int len recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL); // 校验并输出结果 task-done true; break; } } close(sock); }select的tv参数每次调用后会被内核修改为剩余时间如果要在循环里复用必须在每次select前重新赋值。不重新赋值会导致第二次select的超时时间变成“剩余时间”重试间隔会越来越短甚至立即返回。这是网络编程中非常隐蔽的坑我见过不少人在多线程版本里踩到表现是第一次超时后紧接着就重发完全不等。提示多线程扫描时tv和fds必须是每个线程栈上独立的变量不能定义成全局共享否则线程间互相破坏状态。4.2 超时时间与重试次数的权衡从 200ms 到 3 次重试的参数设计超时时间不是拍脑袋定的它取决于你所在局域网的规模和设备响应速度。在同一台交换机下的有线局域网ARP 应答通常在 1-10ms 内到达但如果有 Wi-Fi 设备因为无线信道的半双工特性和重传机制应答可能延迟到 50-100ms。具体到课程设计建议按 200ms 作为单次超时时间连续重试 3 次。如果 3 次都没收到应答判定该 IP 不在线。为什么不用更长的超时50 个 IP 顺序扫描时每个 IP 等 600ms200ms×3总共 30 秒还算忍受范围如果用 1 秒超时50 个 IP 就是 150 秒典型的翻车设计。用并发或 select 循环时总扫描时间 ≈ 单次超时 网络延迟而不是 IP 数乘以超时时间这才是效率的关键。有一个参数容易被忽略发送间隔。如果并发线程同时发出几百个 ARP 请求交换机可能会丢弃一部分广播帧。常见做法是给每线程一个随机偏置发送时间比如在 0-50ms 之间随机延迟后再发送降低同时撞击交换机端口的概率。这个偏置不影响总时间但能显著减少“丢包导致误判离线”的情况。扫描宿舍网或实验室网时特别明显几十台设备同时收到 ARP 广播风暴有些旧网卡处理不过来。5. 课设必踩的 5 个坑从权限问题到虚拟网卡混入5.1 必须有 root 权限sudo 跑不了课设演示怎么办现象socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP))返回 -1错误码 EPERM。原因创建原始套接字需要CAP_NET_RAW权限普通用户默认没有。Linux 对 raw socket 的权限管理很严格这是内核安全机制。解决两种路径。其一直接sudo ./arp_scanner课设演示没问题其二给程序二进制加cap_net_raw能力sudo setcap cap_net_rawep ./arp_scanner这样普通用户也能运行答辩时不用现场输密码避免尴尬。Windows 下则必须安装 Npcap 并以管理员身份运行否则 WinPcap 的pcap_sendpacket会返回权限错误。5.2 字节序不对导致扫描结果全是「无应答」现象向本机 IP 发送 ARP 请求等了 3 秒也没有应答抓包看报文Opcode、Sender IP 等字段全是反的。原因没有使用htons/htonl转换或者把网络序当成主机序直接操作。比如从recvfrom收到的应答里提取 Sender IP然后用%d格式化打印得到 192.168.1.1 变成了 1.1.168.192。解决构造报文时统一用htons/htonl解析报文时统一用ntohs/ntohl。IP 地址转字符串用inet_ntoa而不是自己拼格式化串。结构体加#pragma pack(push, 1)防止字节对齐导致字段偏移错位。5.3 虚拟网卡污染扫描结果VMware 和 VirtualBox 的坑现象扫描结果里出现大量奇怪的 MAC 地址且每次运行结果都不一样但网段里明明没有那么多设备。原因本机装了 VMware 或 VirtualBox它们的虚拟网卡也会响应 ARP 请求且虚拟网卡的回环路径比物理网卡快导致你的程序可能优先收到虚拟网卡的应答。如果你没有过滤网卡GetAdaptersInfo返回的第一块适配器可能就是虚拟网卡导致你的源 MAC 和源 IP 全不对。解决获取本机地址时过滤网卡描述跳过包含 VMware、VirtualBox、Loopback、TAP 字样的项。扫描时使用sll_ifindex绑定到物理网卡如果if_nametoindex返回的设备不对用ip link命令查一下实际网卡名。课设答辩时老师会问你为什么扫出了虚拟机的 MAC主动解释过滤逻辑会加分。5.4 结构体对齐问题为什么 42 字节发出去变 48 字节现象sendto返回值是 48 而不是 42收到的应答解析后 MAC 地址错位。原因定义了包含uint16_t和uint8_t交错的结构体编译器默认 4 字节对齐结构体内插入了 padding 字节导致报文格式错误。解决在结构体定义前加#pragma pack(push, 1)结束后加#pragma pack(pop)或者干脆不用结构体直接用uint8_t frame[42]按偏移手动填充。后者代码长一点但完全可控也不会有对齐问题。5.5 网卡被占导致初始化失败多网卡环境下的正确姿势现象程序启动时报错 bind 失败或 ioctl 失败重启后恢复正常。原因笔记本上有多个网卡接口默认选择的第一块接口可能没有连接网络或者接口处于 down 状态。sendto可以成功但内核路由表把 ARP 广播帧从错误的接口发出去。解决枚举接口列表用SIOCGIFFLAGS检查IFF_UP和IFF_RUNNING标志。选择第一个“活着”且不是回环接口的网卡作为发送通道在sockaddr_ll.sll_ifindex里显式指定不依赖系统默认路由。6. 从课设到实战用超时统计和重传队列把扫描器变成可用工具完成基础版扫描器后想让它真正可用有几个实用的进阶做法。第一个是给扫描结果加一个超时统计表每次发送 ARP 请求记录时间戳收到应答后计算 RTT 并记录均值、最大、最小值。这不仅是课设的创新点也能反过来帮你优化超时参数——如果局域网内 95% 的应答在 30ms 内到达超时设 200ms 就已经留了足够余量还可以考虑压缩到 100ms 提升整体速度。打印格式可以做成表格用left/right对齐控制直接输出到控制台。第二个做法是重传队列而不是固定重试次数。比如某 IP 在第一次请求后 50ms 未应答进入待重传队列等待一个动态间隔后再发送。这个间隔可以按已发送次数递增第一次等 200ms第二次 400ms第三次 800ms。这样对慢速 Wi-Fi 设备更友好也不会让整个扫描时间线性增长。代码实现也不复杂用std::mapuint32_t, uint32_t维护 IP 到重试次数的映射每次超时事件触发时更新到下一个阈值。第三个值得加的是文本导出把扫描结果写成 CSV 文件列名分别是ip,mac,rtt_ms,timestamp。CSV 的好处是可以用 Excel 打开做排序筛选也方便后续写 Python 脚本分析。写文件时注意用ofstream加std::ios::app追加模式避免多次扫描覆盖前一次的数据。文件名带上时间戳比如scan_20250601_143000.csv这样跑多次对比结果也不混淆。项目答辩时老师会关心不止是代码能不能跑还会问“你这工具除了课设还能干什么”这几个点就是很好的回答素材。我说下自己的教训第一次写这个程序时偷懒结构体没加 pack 字节对齐设置抓包对比才发现帧长度多了 6 个字节。第二次加了重传和统计功能但select的tv变量没在循环里重新赋值导致重发节奏完全乱套扫 30 个 IP 用了快 1 分钟。后来把每轮超时时间用独立变量保存每次select前重新拷贝问题才消失。这两次教训让我养成一个习惯涉及时间参数的网络程序所有超时变量统一命名循环体内不直接复用同一个对象。希望帮到你。本文还有配套的精品资源点击获取
返回列表