
简介这份资源面向需要在局域网中排查IP冲突、清点在线设备的C#开发者与网络运维人员核心思路是通过抓取ARP报文来识别网络中的设备并整理出发生IP冲突的设备列表。作者已将抓包逻辑封装为独立类便于直接集成到自己的项目中。压缩包共36个文件约1002KB包含11个cs源码文件、4个exe可执行程序、2个dll动态库以及config、resx、csproj、sln等工程配置与资源文件并附带了使用示例、WinPcap_4_1_3安装包、SharpPcap.dll与PacketDotNet.dll依赖库开发环境为VS2015与.NET 4.5.2。目前已有560人学习下载。拿到后可直接参考示例工程理解ARP抓包与冲突判定的调用方式省去自行搭建抓包框架、处理依赖库引用的时间适合作为网络监测工具的原型或二次开发基础。1. 从一次 IP 冲突排查说起这个 C# 工具到底能干什么上周隔壁工位的同事突然断网重启路由、换网线、重装驱动折腾了一下午都没用最后抓包一看局域网里有两台设备抢同一个 IPARP 应答来回打架网关的 MAC 表被刷得乱七八糟。这种问题用 ping 根本定位不到因为冲突双方都会响应你 ping 谁都有回包。当时我就想与其每次靠运气排查不如写个工具常驻在网段里专门盯着 ARP 流量把在线设备和冲突设备都列出来。这份源码干的就是这件事基于 C# 和 SharpPcap 抓取局域网 ARP 包解析出设备 IP、MAC 和冲突关系输出一份能直接看的设备清单。它适合做网络运维、内网资产管理或者单纯想搞清楚自己家里到底连了多少设备的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份代码拆开讲一遍。2. 技术选型与抓包原理为什么是 SharpPcap 而不是原生 Socket2.1 抓 ARP 为什么绕不开 WinPcap/Npcap 这一层普通 Socket 只能收到发给本机或者广播到自己网卡的数据包而 ARP 请求虽然是广播但交换机不会把所有端口的流量都泛洪给你尤其是单播 ARP 应答目标 MAC 不是你的网卡就直接被网卡硬件过滤掉了。想看到整个网段的 ARP 对话必须把网卡切到混杂模式绕过协议栈直接从链路层拿帧。Windows 上做这件事的通用方案就是装 NpcapWinPcap 的现代替代然后通过它提供的驱动接口读原始帧。SharpPcap 是这套底层库的 .NET 封装把pcap_open_live、pcap_next_ex这些 C 接口包成了ICaptureDevice、CaptureEventArgs这样的托管对象写起来比 P/Invoke 舒服得多。常见做法是项目里同时引用SharpPcap和PacketDotNet两个包前者负责打开设备、收包后者负责把RawCapture里的字节解析成ArpPacket省得自己按偏移量抠字段。这份源码也是这个组合所以你在.csproj里会看到类似这样的依赖声明ItemGroup !-- 抓包核心库负责打开网卡、混杂模式、收原始帧 -- PackageReference IncludeSharpPcap Version5.4.0 / !-- 协议解析库把字节流翻译成 ARP/Ethernet 对象 -- PackageReference IncludePacketDotNet Version1.4.7 / /ItemGroup版本号不用死磕5.x 的 SharpPcap 和 1.4.x 的 PacketDotNet 搭配是社区里跑得比较稳的一组。注意 Npcap 安装时一定要勾选「WinPcap API-compatible Mode」否则 SharpPcap 找不到设备列表CaptureDeviceList.Instance会返回空这是新手第一个翻车点。2.2 枚举网卡与打开设备别一上来就抓所有网卡SharpPcap 拿到设备列表后不要图省事对每个设备都开一个抓包线程。虚拟网卡VMware、Hyper-V、Docker 的 vEthernet会混在里面它们上面的 ARP 流量要么是宿主机内部的要么根本抓不到真实网段白白占资源还污染结果。正确姿势是先列出所有设备让用户选或者按网卡描述过滤掉虚拟的再打开。// 获取本机所有可抓包的网卡设备 var devices CaptureDeviceList.Instance; if (devices.Count 0) { Console.WriteLine(没找到任何网卡检查 Npcap 是否安装并勾选了兼容模式); return; } // 打印出来让用户选避免抓到虚拟网卡上 for (int i 0; i devices.Count; i) { Console.WriteLine($[{i}] {devices[i].Description}); } Console.Write(请输入要监听的网卡编号: ); int index int.Parse(Console.ReadLine()); ICaptureDevice device devices[index]; // 打开设备1000ms 读超时混杂模式1MB 内核缓冲 device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 关键混杂模式才能看到别人的 ARP ReadTimeout 1000, BufferSize 1024 * 1024 });DeviceModes.Promiscuous是核心参数不开这个你只能看到广播和发给自己的包冲突检测基本废掉。ReadTimeout设 1000 毫秒是为了让收包循环有机会响应退出信号不然GetNextPacket会一直阻塞。BufferSize给 1MB 是防止高流量下丢包内网 ARP 量不大但突发广播多的时候缓冲大一点更稳。2.3 收包循环与 ARP 解析从原始帧里抠出 IP 和 MAC打开设备后挂上OnPacketArrival事件每来一个包就判断是不是 ARP。PacketDotNet 的解析链路是RawCapture → Packet.ParsePacket → EthernetPacket → ArpPacket逐层剥。ARP 包里我们关心四个字段发送方 IP、发送方 MAC、目标 IP、目标 MAC以及操作码1 是请求2 是应答。// 收到包时触发e.Packet 是原始帧 device.OnPacketArrival (sender, e) { var raw e.GetPacket(); var packet Packet.ParsePacket(raw.LinkLayerType, raw.Data); var arp packet.ExtractArpPacket(); if (arp null) return; // 不是 ARP 直接跳过 string senderIp arp.SenderProtocolAddress.ToString(); string senderMac arp.SenderHardwareAddress.ToString(); string targetIp arp.TargetProtocolAddress.ToString(); string op arp.Operation ArpOperation.Request ? 请求 : 应答; Console.WriteLine($[{op}] {senderIp} ({senderMac}) - {targetIp}); }; // 开始抓包-1 表示一直抓到调用 StopCapture device.StartCapture();ExtractArpPacket()返回 null 就说明这帧不是 ARP可能是 IPv4、IPv6 或者别的直接 return 省得后面空引用。SenderHardwareAddress是 6 字节物理地址ToString()出来是AA:BB:CC:DD:EE:FF格式。这里先只打印冲突判断的逻辑放到下一章讲因为那部分需要维护状态表单独拆出来更清楚。3. 设备列表与冲突检测状态表怎么建、冲突怎么判3.1 用字典维护 IP→MAC 映射别用 List 硬扫抓到的每个 ARP 包都携带一组「IP-MAC」对应关系我们要做的是持续收集这些关系并且当同一个 IP 出现不同 MAC 时标记冲突。最直接的数据结构是Dictionarystring, HashSetstringkey 是 IPvalue 是这个 IP 出现过的所有 MAC 集合。用 HashSet 而不是单个字符串是因为冲突场景下同一个 IP 会对应两个甚至更多 MAC得全记下来才能判断。// IP - 该 IP 出现过的所有 MAC 集合 private readonly Dictionarystring, HashSetstring _ipMacMap new Dictionarystring, HashSetstring(); // 处理一个 ARP 包更新映射表 private void RecordArp(string ip, string mac) { if (string.IsNullOrEmpty(ip) || ip 0.0.0.0) return; if (!_ipMacMap.TryGetValue(ip, out var macSet)) { macSet new HashSetstring(); _ipMacMap[ip] macSet; } macSet.Add(mac); // 集合里超过一个 MAC说明这个 IP 被多台设备用了 if (macSet.Count 1) { Console.WriteLine($[冲突] IP {ip} 被以下 MAC 占用: {string.Join(, , macSet)}); } }TryGetValue比先ContainsKey再索引取值少一次哈希查找高频收包场景下这点差别会累积。0.0.0.0是 ARP 探测包设备刚上线时发的免费 ARP里常见的源 IP记进去会污染结果直接过滤。冲突判断放在Add之后因为只有新增 MAC 才可能触发冲突重复的 MAC 加进去 HashSet 不会变也就不会重复报警。3.2 冲突判定不能只看一次包要加时间窗口上面那段代码有个隐患如果一台设备换了网卡旧 MAC 的 ARP 缓存还没过期新 MAC 又发了一次短时间内会被误判成冲突。真实冲突的特征是两个 MAC 持续交替出现而不是偶尔一次。所以合格的做法是给每个 MAC 记录最后出现时间只有在一个时间窗口内比如 30 秒两个 MAC 都活跃才认定为冲突。// IP - (MAC - 最后出现时间戳) private readonly Dictionarystring, Dictionarystring, DateTime _ipMacTime new Dictionarystring, Dictionarystring, DateTime(); private const int ConflictWindowSeconds 30; private void RecordArpWithTime(string ip, string mac) { if (string.IsNullOrEmpty(ip) || ip 0.0.0.0) return; if (!_ipMacTime.TryGetValue(ip, out var macTimes)) { macTimes new Dictionarystring, DateTime(); _ipMacTime[ip] macTimes; } macTimes[mac] DateTime.Now; // 只保留窗口内还活跃的 MAC var now DateTime.Now; var activeMacs macTimes .Where(kv (now - kv.Value).TotalSeconds ConflictWindowSeconds) .Select(kv kv.Key) .ToList(); if (activeMacs.Count 1) { Console.WriteLine($[冲突] IP {ip} 活跃 MAC: {string.Join(, , activeMacs)}); } }ConflictWindowSeconds设 30 秒是个经验值ARP 缓存一般几分钟过期30 秒内两个 MAC 都发过包基本可以坐实冲突。窗口太小会漏判冲突设备发包间隔可能超过窗口太大又会把换网卡误判进来。如果你的网段设备很安静可以调到 60 秒如果设备频繁通信30 秒足够。3.3 输出设备列表按 IP 排序冲突项单独标出来抓了一段时间后要把结果整理成人能看的列表。直接遍历字典输出就行但要注意排序和冲突标记不然一堆乱序的 IP 看着头疼。下面这个方法是定时调用的比如每 10 秒刷新一次控制台。// 输出当前设备列表冲突项用 [冲突] 前缀标出 public void PrintDeviceList() { Console.WriteLine(---- 当前网段设备列表 ----); foreach (var ip in _ipMacTime.Keys.OrderBy(k IPToLong(k))) { var macTimes _ipMacTime[ip]; var now DateTime.Now; var activeMacs macTimes .Where(kv (now - kv.Value).TotalSeconds ConflictWindowSeconds) .Select(kv kv.Key) .ToList(); string flag activeMacs.Count 1 ? [冲突] : ; Console.WriteLine(${flag}{ip,-16} {string.Join( / , activeMacs)}); } } // 把 IP 转成 long 用于正确排序不然 192.168.1.10 会排在 192.168.1.2 前面 private static long IPToLong(string ip) { var parts ip.Split(.).Select(byte.Parse).ToArray(); return ((long)parts[0] 24) | ((long)parts[1] 16) | ((long)parts[2] 8) | parts[3]; }IPToLong是为了解决字符串排序的坑按字符串排192.168.1.10会排在192.168.1.2前面因为逐字符比较时1小于2。转成 32 位整数再排就正常了。activeMacs只取窗口内活跃的历史 MAC 不显示列表更干净。冲突项加前缀标记一眼就能看到哪几个 IP 有问题。4. 避坑与常见问题抓不到包、误报冲突、性能掉底4.1 设备列表为空一个网卡都看不到现象是运行后CaptureDeviceList.Instance.Count为 0控制台直接打印「没找到任何网卡」。原因通常是 Npcap 没装或者装的时候没勾选 WinPcap 兼容模式SharpPcap 5.x 默认走的是 Npcap 的 API兼容模式不开就枚举不到。解决办法是去 Npcap 官网重新下载安装包安装向导里明确勾上「Install Npcap in WinPcap API-compatible Mode」装完重启一次再跑。如果还不行用管理员权限运行程序枚举网卡需要驱动层权限。4.2 能抓到包但全是自己的流量看不到别人的 ARP现象是只收到本机发出的 ARP 请求和发给本机的应答其他设备的对话完全看不到。原因是网卡没进混杂模式或者你抓的是虚拟网卡。先确认DeviceConfiguration.Mode设成了DeviceModes.Promiscuous再检查选中的网卡描述里有没有 VMware、Hyper-V、VirtualBox 字样有的话换成物理网卡。还有一种情况是交换机做了端口隔离每个端口只能看到自己的流量这种网络环境下抓包工具本身就没法看到全网 ARP得换镜像端口或者用支持监控的交换机。4.3 冲突误报明明没冲突却一直提示现象是列表里频繁出现[冲突]但实际网段里 IP 都是唯一的。原因多半是时间窗口设太大把设备换网卡、虚拟机迁移、DHCP 续租这些正常行为当成了冲突。解决办法是把ConflictWindowSeconds调小比如从 30 秒降到 10 秒同时观察冲突项的 MAC 是不是同一个厂商前缀OUI如果是同一台设备的不同网卡基本可以排除。另外免费 ARP 探测包里的源 IP 是0.0.0.0如果没过滤掉会污染映射表确认RecordArp里有ip 0.0.0.0的判断。4.4 跑久了内存涨控制台卡顿现象是程序运行几小时后内存占用持续上升输出越来越慢。原因是_ipMacTime字典只增不减离线设备的记录一直留着时间戳字典也越来越大。解决办法是加一个清理任务定期把超过窗口时间几倍的记录删掉。比如每 5 分钟扫一遍把最后出现时间超过 5 分钟的 MAC 从字典里移除IP 对应的 MAC 字典空了就把 IP 也删掉。这样内存占用就稳定在「当前活跃设备数」这个量级不会无限涨。4.5 抓包线程和 UI 线程抢资源界面卡死现象是如果把这个逻辑塞进 WinForm 或 WPF界面会卡住不响应。原因是OnPacketArrival回调跑在 SharpPcap 的后台线程上直接在里面更新 UI 控件会跨线程访问轻则卡顿重则崩溃。解决办法是用ConcurrentDictionary存状态UI 线程定时比如用System.Windows.Forms.Timer去读快照刷新界面抓包线程只管写。两边通过线程安全容器解耦谁也不阻塞谁。5. 二次开发与改造从控制台工具到可落地的内网监测5.1 把结果落库做历史查询和告警控制台打印只适合临时排查真要长期监测得把数据存下来。我一般会加一张 SQLite 表字段就ip、mac、first_seen、last_seen、is_conflict每次RecordArpWithTime更新时 upsert 一次。SQLite 单文件、零配置适合这种轻量场景。下面是一个简化的写入方法// 用 SQLite 持久化设备记录冲突状态一并存 private void UpsertDevice(string ip, string mac, bool isConflict) { using var conn new SQLiteConnection(Data Sourcedevices.db); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO devices(ip, mac, first_seen, last_seen, is_conflict) VALUES($ip, $mac, $now, $now, $conflict) ON CONFLICT(ip, mac) DO UPDATE SET last_seen $now, is_conflict $conflict; cmd.Parameters.AddWithValue($ip, ip); cmd.Parameters.AddWithValue($mac, mac); cmd.Parameters.AddWithValue($now, DateTime.Now.ToString(s)); cmd.Parameters.AddWithValue($conflict, isConflict ? 1 : 0); cmd.ExecuteNonQuery(); }ON CONFLICT是 SQLite 3.24 以上支持的 upsert 语法(ip, mac)建唯一索引就能用。DateTime.Now.ToString(s)是 ISO 8601 格式排序和跨时区都没问题。存下来之后就能做「某个 IP 历史上被哪些 MAC 用过」这种查询排查私接设备特别有用。5.2 加一个简单的 Web 面板手机也能看控制台工具最大的问题是得坐在电脑前看。改造思路是加一个极简的 HTTP 服务把设备列表以 JSON 吐出来前端一个静态页面定时拉取渲染成表格。用HttpListener就够了不用引 ASP.NET Core 那一套重家伙。核心就几十行// 极简 HTTP 服务暴露 /devices 接口返回 JSON var listener new HttpListener(); listener.Prefixes.Add(http://:8080/); listener.Start(); Task.Run(async () { while (listener.IsListening) { var ctx await listener.GetContextAsync(); if (ctx.Request.Url.AbsolutePath /devices) { var json JsonSerializer.Serialize(GetDeviceSnapshot()); var bytes Encoding.UTF8.GetBytes(json); ctx.Response.ContentType application/json; ctx.Response.OutputStream.Write(bytes, 0, bytes.Length); } ctx.Response.Close(); } });GetDeviceSnapshot()返回一个ListDeviceInfo包含 IP、MAC 列表、是否冲突。前端用fetch(/devices)每 5 秒拉一次渲染成表格冲突行标红。这样手机连上同一个局域网就能看不用装客户端。注意http://:8080/需要管理员权限或者提前用netsh注册 URL ACL否则Start()会抛异常。5.3 验证改造是否生效三个必查点改完之后别急着上线按这三步验证一遍。第一抓包是否正常跑起来后看控制台有没有持续输出 ARP 记录如果几分钟一条都没有检查网卡选择和混杂模式。第二冲突判定是否准确手动在另一台设备上把 IP 改成和某台在线设备一样看工具是否在 30 秒内报冲突改回去后冲突标记是否消失。第三持久化和接口是否可用查 SQLite 里有没有数据浏览器访问http://localhost:8080/devices看 JSON 是否正常返回。这三点过了基本就能扔到内网里长期跑了。5.4 一个我踩过的坑别在收包回调里做重活最早我把数据库写入直接放在OnPacketArrival回调里结果高流量时回调被阻塞SharpPcap 的内核缓冲区很快填满开始丢包冲突检测漏掉关键帧。后来改成回调里只更新内存字典另起一个定时任务每 5 秒批量落库丢包问题就没了。从那以后我每次写抓包相关的代码都强制把「收包」和「处理」拆成两级回调里只做最轻的解析和入队重活交给独立线程。这个习惯帮我省了不少后悔药。希望这份拆解能帮你把这份源码跑起来改出自己顺手的版本。本文还有配套的精品资源点击获取