资讯详情

资讯详情

基于 SharpPcap 的 ARP 被动监听:局域网设备发现与 IP 冲突检测

简介该资源面向需要在局域网内做设备发现与IP冲突排查的C#开发者、网络运维人员及安全入门学习者通过抓取ARP报文识别网内在线设备并整理出存在IP地址冲突的设备清单适合作为轻量级网络监测功能直接集成到自有工具中。压缩包共36个文件约1002KB以11个cs源码文件为主体辅以sln与csproj工程文件、exe可执行程序、config配置、dll依赖及resources资源文件工程结构完整可直接编译调试。作者已将抓取ARP包的核心逻辑封装成一个独立类并附上调用示例同时打包了WinPcap_4_1_3安装程序、SharpPcap.dll与PacketDotNet.dll等必需依赖省去读者自行查找底层抓包库与环境配置的麻烦。开发环境为VS2015、C#与.NET 4.5.2。目前已有560人学习下载便于参考其类设计思路与报文过滤方式快速复用到网络设备巡检、冲突告警等实际场景。1. 局域网里最完整的设备清单靠监听而不是 ping 扫段多数人排查局域网的第一步是写个 for 循环把整个网段 ping 一遍认为这样能把在线设备找全。而在局域网里一台设备只要想发起通信就必须先用 ARP 包把自己的 IP 与 MAC 的映射关系广播出去当两台设备争抢同一个 IP 时冲突也会以“两个不同 MAC 回答同一个 IP”的形式直接暴露在链路上。也就是说网络设备列表和 IP 冲突记录原来靠监听ping 扫段并不能拿到。这件事值得搞清楚的地方在于一次原本像“网有点卡”的模糊排障因此变成能列出时间、IP、MAC 三要素的取证过程。而用 C# 配合 SharpPcap 抓 ARP 包正是把这种被动监测做成常驻工具最短的一条路。2. 为什么被动抓包比主动扫段更接近真相ARP 协议栈的机制拆解主动扫描得到的是“某个瞬间有人回了我一个包”被动监听得到的是“这台设备自己承认了自己的地址”。两者的信息质量不在一个量级上差别来自 ARP 协议本身的设计。2.1 28 字节里的字段语义ARP 帧究竟告诉了你什么以太网帧头之后紧跟着 28 字节的 ARP 载荷(IPv4 over Ethernet 的固定形态)。它不是自描述协议没有版本号也没有长度字段全靠固定偏移去读所以每一段的含义必须记牢偏移长度字段常见取值与含义02硬件类型0x0001以太网22协议类型0x0800IPv441硬件地址长度651协议地址长度462操作码 opcode1 请求2 应答86发送方 MAC请求方自己的 MAC144发送方 IP请求方自己的 IP186目标 MAC请求时通常全 0应答时是对方 MAC244目标 IP想问的那个 IP关键在于最后两段。请求帧里的目标 MAC 一般填全 0因为发送方根本不知道对方 MAC这正是它发问的原因而应答帧里发送方 MAC 与发送方 IP 一定是自洽的一对。任何一台设备在链路上“自称”的身份都写在这 8 个字节里。opcode 只有 1 和 2 两种语义看起来简陋却带来一个很好的性质ARP 是无状态、无序号、无认证的。它不校验“这个 IP 是不是真的属于你”只负责把答案抄下来。这个设计对协议本身是效率上的胜利对监测工具则是自由只要有人声称某个 IP 归他你就能看到两个人同时声称你也能看到。2.2 免费 ARP 与 IP 冲突冲突为什么总在 ARP 层先露头IPv4 协议本身没有地址冲突检测机制——这一点常被忽略。协议栈给你配上一个 IP就默认它是唯一的重复了也没人拦。真正会“拦一下”的是设备开机时自己做的探测常见的实现思路是主机在正式使用某个 IP 之前先发一个发送方 IP 填 0.0.0.0、目标 IP 填待用地址的 ARP 请求(业界叫 ARP Probe免费 ARP 的一种形态)。如果这个请求收到了应答说明链路上已经有人占着这个地址系统就弹窗报冲突。这就是冲突监测的原理基础冲突不是被“检测算法”发现的而是被 ARP 协议本身逼出来的。免费 ARP 通常有三种形态——探测(发送方 IP 为 0)、通告(发送方 IP 等于目标 IP用于告诉别人“我换 MAC 了”)、以及无故应答(没人问也回答多用于冗余设备切换)。做监测时这三种都值得单独打标记因为它们的“可信度”和“意图”完全不同。同样值得记住的是冲突的两种表现形态。第一种是同一 IP 被两个 MAC 声称这是真冲突第二种是同一 MAC 声称了多个 IP这绝大多数时候不是冲突而是网关做代理 ARP、负载均衡设备的虚拟 MAC、或者一台机器配了多个地址。把后者当冲突报出来是把工具做成噪音的最快方式。2.3 被动监听的物理边界你能看到多少取决于你站在哪抓包工具最容易被误解的一点是以为开了混杂模式就能看到全网。混杂模式只是让网卡不再丢弃“目的 MAC 不是我”的帧但交换机根本不会把这些帧送到你所在的端口上。交换机按 MAC 表转发只有当帧是广播、组播、未知单播泛洪或者目标就是你时才会出现在你的口上。好消息是 ARP 恰好大量使用广播ARP 请求的目的 MAC 是全 F天然会泛洪到同一广播域的所有端口。所以你在一个接入端口上就能看到整个 VLAN 里大部分请求流量。看不到的是别人的应答——应答是单播交换机会只送给问的人。这就决定了几个硬边界跨 VLAN 看不到(除非配了镜像或汇聚)跨路由看不到因为路由器不转发广播你只能看到自己网关的 ARP 交互无线侧还叠加了客户端隔离和省电休眠设备静默期可能长达几分钟。提示如果你的目标只是“摸清我所在的这个网段有哪些设备、有没有地址冲突”那接入端口加混杂模式是够的。想做全楼层的资产盘点抓包点必须挪到镜像口或者核心交换机上这不是代码能弥补的。3. SharpPcap 的收包链路从网卡驱动到 C# 回调的每一跳知道了该看什么接下来的问题是包怎么从网卡进到你的 C# 代码里中间哪几跳最容易出问题。SharpPcap 本质上是一层托管封装真正干活的是底层的抓包驱动它在内核态维护一个环形缓冲区你的进程只是从里面搬运。3.1 打开设备混杂模式、读超时和缓冲区各管什么初始化的顺序很有讲究参数填错的表现往往不是报错而是“能跑但抓不到”using SharpPcap; using SharpPcap.LibPcap; // 1. 枚举网卡实际项目里应让用户选按描述和 IP 匹配更稳 var devices CaptureDeviceList.Instance; var device devices.FirstOrDefault(); if (device null) throw new InvalidOperationException(没有可用抓包设备检查驱动是否安装); // 2. 注册回调要在 StartCapture 之前否则会漏掉开头几个包 device.OnPacketArrival OnArpPacketArrived; // 3. 打开设备混杂模式 1000ms 读超时 4MB 内核缓冲 // 不同版本的 SharpPcap 重载不同等价老写法是 device.Open(DeviceModes.Promiscuous, 1000) device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 接收目的 MAC 不是本机的帧 ReadTimeout 1000, // 无包时的返回间隔影响关闭响应速度 BufferSize 4 * 1024 * 1024 // 内核环形缓冲抗突发 }); // 4. 过滤器交给驱动编译只把 ARP 帧搬进用户态 device.Filter arp; device.StartCapture();三个参数的作用要分清。混杂模式决定网卡是否丢弃“别人的帧”在读超时期间没抓到包底层调用会正常返回而不是一直阻塞这个值设太大(比如 10 秒)会让停止采集时卡顿明显。缓冲区大小是最容易被忽略的一个ARP 流量平时稀疏但设备批量上电或 DHCP 集中续租时会瞬间冲高缓冲区太小就直接丢包而丢包在你代码里是完全静默的表里少了设备你还以为是没人。Filter是字符串形式的 BPF 表达式由驱动在内核态编译执行。写成arp意味着所有非 ARP 帧在进入用户态之前就被丢掉CPU 和内存开销能降一到两个数量级。想再窄一点可以写arp and ether broadcast只收广播帧代价是丢掉所有单播应答——而单播应答恰恰是判断“这台设备真的在这个 IP 上”的最强证据一般不建议这么收窄。3.2 回调线程模型为什么不能在里面更新界面OnPacketArrival是在抓包线程上同步触发的。这个线程的职责只有一个把帧从内核缓冲区搬走。一旦你在里面做耗时操作——查库、写日志文件、更新 WinForm 控件、发 HTTP 请求——搬运速度就会掉下来内核缓冲区填满后开始丢包表现是“平时挺好一忙就漏设备”。正确的做法是回调只做最廉价的一件事拷贝字节塞进无锁队列立刻返回。解析和入库交给另一个线程批量消费。// 抓包线程与解析线程之间的唯一交接点 private readonly System.Collections.Concurrent.ConcurrentQueuebyte[] _rawQueue new(); private void OnArpPacketArrived(object sender, PacketCapture e) { // GetPacket() 返回的数组是复用的必须自己拷一份再入队 var data e.GetPacket().GetPacket().Data.ToArray(); _rawQueue.Enqueue(data); // 这里不要做任何解析、日志、UI 操作 }这里有一处实现细节很容易出错底层给到你的缓冲区会被循环复用同一个数组下一秒就可能装进另一个包。所以入队前必须拷贝。反过来如果解析写得很轻(只是读固定偏移、更新字典)直接在回调里做也不是不行但一旦以后加了“冲突时发邮件”这种逻辑就会变成间歇性丢包的隐患——这类问题事后排查成本极高属于不报错的失败。消费侧的节奏建议按批处理每次从队列里取到 500 条或累计 200 毫秒就处理一轮然后Thread.Sleep(10)让出 CPU。批处理的好处是字典的锁竞争从“每包一次”降到“每批一次”这在千兆环境下差别很明显。4. 设备表与冲突表怎么建状态机与判定阈值抓到的包本身没有价值价值在于把一串无状态的帧收敛成有状态的表。这里面真正需要设计的是三件事键怎么选、时间窗口怎么定、什么条件下才敢下冲突结论。4.1 表结构二级字典加时间戳别用单一映射最直觉的做法是维护一个DictionaryIPAddress, PhysicalAddress。这个结构一旦遇到冲突就崩了同一个 IP 只能存一个 MAC第二条记录要么覆盖第一条(冲突就此消失)要么被丢弃(冲突同样消失)。冲突检测的前提是允许一个键对应多个值所以要用二级结构。public sealed class ArpRecord { public IPAddress Ip IPAddress.None; public PhysicalAddress Mac PhysicalAddress.None; public long FirstSeenTicks; // 用单调时钟别用 DateTime.Now public long LastSeenTicks; public int HitCount; // 该 (IP, MAC) 对被确认的次数 public bool FromGratuitous; // 是否来自免费 ARP } public sealed class ArpTable { // 外层键是 IP内层键是 MAC天然容纳一个 IP 多个 MAC private readonly DictionaryIPAddress, DictionaryPhysicalAddress, ArpRecord _byIp new(); private readonly DictionaryPhysicalAddress, HashSetIPAddress _byMac new(); }时间戳用Environment.TickCount64或Stopwatch不用DateTime.Now。原因很实在系统校时、夏令时切换、笔记本休眠唤醒都会让墙上时钟出现跳跃一个向前跳两分钟就足以让时间窗口判断全部失准你会在日志里看到“两台设备同时出现”却怎么也对不上。单调时钟只增不减专为测间隔设计。_byMac这张反向表是专门为“同一 MAC 多个 IP”准备的。它不产出冲突告警但它能告诉你一台设备换了地址、或者网关在做代理 ARP这在解读结果时很有用。4.2 冲突判定两个条件同时成立才下结论判定逻辑的核心是“两个 MAC 在时间上重叠地活跃着”。如果一个 MAC 已经十分钟没出现过那更可能是 DHCP 把地址重新分给了新设备属于正常流转不是冲突。// 判定窗口两个 MAC 必须都在这个时间内被确认过才算同时活跃 private readonly long _activeWindowMs 120_000; private const int MinHits 2; // 单次出现不算压低随机丢包造成的误报 public IEnumerableConflictInfo DetectConflicts(long nowTicks) { foreach (var (ip, macs) in _byIp) { var active macs.Values .Where(r nowTicks - r.LastSeenTicks _activeWindowMs r.HitCount MinHits) .OrderByDescending(r r.LastSeenTicks) .ToList(); if (active.Count 2) continue; // 最新的两个 MAC 作为冲突双方其余作为附加证据一并带出 yield return new ConflictInfo { Ip ip, Current active[0].Mac, Previous active[1].Mac, DetectedAtTicks nowTicks, MacCount active.Count }; } }_activeWindowMs取 120 秒是个经验值。太短(比如 10 秒)会漏掉那些只在开机时喊一次就沉默的设备太长(比如半小时)会把正常的地址回收再分配误判成冲突。这个值跟你的 DHCP 租约时间、设备换机频率直接相关实际部署时值得按现场情况调。MinHits是抑制误报的第二道闸。只看单次出现就报警遇到丢包重传、抓包点切换、甚至同一帧被两次捕获(镜像口配置重叠时真会发生)都会产生假阳性。要求同一个 (IP, MAC) 对至少被确认两次代价是首次发现的延迟多几秒换来的是告警可信度大幅提升。判断流程本身也要有先后先判断这个 IP 是不是网关、是不是已知的虚拟地址段(冗余网关的虚拟 MAC 有固定格式末尾通常是 00:00:5E:00:01:xx)命中的直接标记为“疑似代理/虚拟”不进入冲突候选。这一步能把大部分噪音在源头掐掉。4.3 主动免费 ARP 补齐被动盲区被动监听有个天生的短板只能看到“有人在说话”。那些安静的服务器、打印机、IoT 设备可能几小时都不发一个包你在表里永远看不到它。补齐的办法是有限度地主动发问构造一个发送方 IP 为 0.0.0.0 的 ARP 请求目标 IP 填你要探测的地址广播出去谁占着这个地址谁就会应答。应答帧是单播回给你的所以这一招不受交换机转发限制。SharpPcap 打开设备后可以直接发送原始帧。// 构造 ARP ProbeSPA 填 0.0.0.0避免污染对方的 ARP 缓存 private static byte[] BuildArpProbe(PhysicalAddress selfMac, IPAddress targetIp) { var frame new byte[42]; // 14 以太网头 28 ARP Array.Fill(frame, (byte)0xFF, 0, 6); // 目的 MAC 全 F广播 selfMac.GetAddressBytes().CopyTo(frame, 6); // 源 MAC frame[12] 0x08; frame[13] 0x06; // EtherType ARP frame[14] 0x00; frame[15] 0x01; // 硬件类型 以太网 frame[16] 0x08; frame[17] 0x00; // 协议类型 IPv4 frame[18] 6; frame[19] 4; // 地址长度 frame[20] 0x00; frame[21] 0x01; // opcode request selfMac.GetAddressBytes().CopyTo(frame, 22); // 偏移 28..31 是发送方 IP保持全 0这是 Probe 与普通请求的关键区别 targetIp.GetAddressBytes().CopyTo(frame, 38); return frame; }发送方 IP 填 0 不是可选项。填成你自己的 IP对方的协议栈会顺手把这个映射写进它的 ARP 缓存等于你悄悄改了别人的表填 0 则不会产生任何缓存副作用这就是 ARP Probe 的标准做法。探测要控速。整个 /24 扫一遍是 254 个包按每秒 20 个发十几秒结束对现代交换机的冲击可以忽略但如果无节制地全速发会明显拉高广播域负载在老旧设备上甚至可能触发广播抑制。扫描间隔建议不低于 5 分钟把主动探测当成定期补漏主力仍然是持续被动监听。5. 抓不到包、误报冲突五条踩坑记录这一节记录的都是会真实消耗时间的问题。它们的共同点是程序不报错甚至看起来在正常工作只是结果不对。5.1 现象程序能启动但列表里只有本机原因排查到最后基本落在两处。一是抓包点选错笔记本接在接入交换机的一个普通口上交换机只把广播和发给你的帧送过来别人的单播应答你一条都看不到表里自然只有自己。二是根本没装抓包驱动CaptureDeviceList.Instance返回空集合代码里FirstOrDefault()拿到 null 后被后续逻辑悄悄绕过。解决办法是先在代码里显式断言设备列表非空并抛出明确异常别让空集合流入后续流程。抓包点的问题只能靠环境解决要么把程序挪到核心交换机的镜像口要么接受“只能看到广播 ARP 请求”这个现实并且把界面上“设备总数”的口径写清楚别让使用者误以为是全网资产数。5.2 现象抓到的 MAC 全是乱码IP 也对不上十有八九是 VLAN 环境。带 802.1Q 标签的帧以太网头后面多了 4 字节标签ARP 载荷的起始偏移从 14 变成了 18。代码里如果硬编码frame[14]开始读硬件类型读到的其实是标签和 ARP 头的混合体解析出来的 MAC 自然是一串无意义的字节。解决方法是解析前先看 EtherType等于 0x8100 就说明有标签偏移加 4再看下一层的 EtherType 是不是 0x0806。这个判断要写成循环而不是一次 if因为双标签(QinQ)在汇聚链路上确实存在。另外 BPF 表达式里的vlan关键字会改变后续偏移的语义过滤器写vlan and arp时驱动层的偏移假设可能和你的解析代码不一致两边必须对齐着改。5.3 现象冲突告警刷屏但实际网络没问题这是把工具做成噪音的典型场景。来源通常有三种笔记本和手机在 DHCP 续租前后短暂出现两个 MAC手机开启了 MAC 随机化同一个 IP 在不同时间对应不同随机 MAC网络里存在冗余网关或负载均衡它们用同一个虚拟 MAC 应答多个 IP。对应解法是三层过滤。第一层在时间上要求两个 MAC 的活跃窗口重叠而不是“历史上都出现过”。第二层在频次上单次出现不报警要求至少两次确认。第三层在语义上维护一份排除清单把网关 MAC、虚拟 MAC 段、已知的代理 ARP 设备放进去。三层叠加后剩下的告警基本都值得看一眼。5.4 现象跑一段时间后设备越来越少重启就恢复这是缓冲区溢出导致的静默丢包。表现很有迷惑性启动后数据很全越跑越稀疏重启程序又正常看起来像内存泄漏实际上是消费速度跟不上生产速度。ARP 流量本身不大但如果回调里做了写数据库、更新界面这类操作单包处理时间上到毫秒级峰值期就顶不住。解法是把回调压到极限只拷贝字节、入队、返回。解析和存储全部挪到消费线程批处理。判断有没有丢包可以对照底层统计里的接收计数和实际入队计数两个数字对不上就说明中间漏了。这个对照最好做成常驻指标而不是出问题才想起来加。5.5 现象程序在开发机正常部署到目标机器就报权限错误抓包是特权操作。Windows 上需要管理员权限并且装了对应的抓包驱动Linux 上要么 root要么给二进制加 CAP_NET_RAW 能力。更隐蔽的一种是程序以服务方式运行时用的是系统账户权限有但选中的网卡索引在服务会话里跟交互式会话不一致枚举出来的设备名可能对不上。解法是启动时先做一次自检枚举设备、尝试打开、立即关闭任何一步失败就输出带明确原因的错误而不是等到运行几分钟后没数据才让人去猜。设备选择也别靠索引用描述字符串或者绑定的 IP 去匹配稳定性好得多。6. 设计取舍被动为主、主动为辅以及阈值这件事没人能替你决定这套东西做完之后最值得回头看的一个决定是为什么不干脆全用主动扫描。答案在于两类手段的失败模式完全不同。主动扫描的失败是“漏”设备开了防火墙、静默不响应你就永远看不到它被动监听的失败是“慢”设备不说话你就要等到它下次开口。两者叠加漏报被互补掉了代价是要维护两套逻辑和一份排除清单。第二个取舍是抓包位置。把程序放在每个终端上跑部署成本低但视野只有自己那一口放在镜像口上视野是整个广播域但部署要网络管理员配合而且镜像口的带宽是共享的抓全量流量会拖垮采集端。我的选择是镜像口只放 ARP 过滤器把采集压到最窄用极小的流量换取全域视野。第三个取舍关于阈值。_activeWindowMs和MinHits这两个数字没有任何一组取值能适配所有网络。写字楼里设备频繁换机窗口要短机房里的服务器地址几年不变窗口长一点反而更准。所以这两个值必须做成可配置并且在界面上把“当前窗口”显示出来——让人知道自己看的是多大时间尺度上的结论比偷偷用一组默认值诚实得多。验证方法其实很朴素找两台机器手动配上同一个空闲 IP看工具在多少秒内报出来、报的是哪两个 MAC再把其中一台的网线拔掉看这条告警多久之后自动过期。这两个动作能把判定逻辑的正确性和时效性都验一遍比看代码可靠。毕竟冲突检测这件事没能报出来和报了假警对使用者的伤害是同等的。从那以后我每接手一个陌生网段第一件事都是先挂十分钟纯被动监听把设备表和冲突表拉出来看一遍再决定要不要做主动探测——先偷听再发问顺序反了就容易把排查变成制造问题。希望帮到你。这套 C# 工程把上面提到的帧解析、二级表结构、冲突判定和排除清单都写成了可直接编译运行的代码拿下来对着自己的网段跑一遍比读十遍协议文档都直观。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →