简介:这是一份面向C#网络编程开发者与运维人员的实用工具类资源,聚焦局域网设备发现与IP冲突排查场景。它通过抓取ARP数据包的方式,实时侦测网络中的在线设备,并整理出存在IP地址冲突的设备列表,适合需要做网络监控、故障定位或安全审计的中级开发者参考。资源包共36个文件,约1002KB,以cs源码文件为主,辅以exe可执行程序、config配置、dll动态库、resx资源及sln解决方案等,结构完整,可直接在VS2015与.NET 4.5.2环境下编译运行。作者已将抓ARP包的核心逻辑封装成独立类,并附上使用示例,同时打包了WinPcap_4_1_3安装程序、SharpPcap.dll与PacketDotNet.dll依赖,省去自行搜集环境组件的麻烦。目前已有560人学习下载,读者可借此快速理解ARP抓包原理、掌握设备发现与冲突检测的实现思路,并在此基础上扩展自己的网络监测工具。
1. 局域网设备发现:用 SharpPcap 抓 ARP 包摸清谁在线上、谁在打架
机房巡检时最怕什么?不是服务器宕机,而是 IP 冲突。两台设备配了同一个地址,网络时通时断,业务方催着排查,你登录交换机一层层翻 MAC 表,半小时过去了还没定位到人。更麻烦的是,有些设备是临时接入的笔记本或摄像头,DHCP 租约里根本查不到。这种场景下,与其被动等报障,不如主动监听局域网里的 ARP 流量——谁在线、谁和谁撞了 IP,ARP 包会老老实实告诉你。这个项目就是干这件事的:基于 C# 和 SharpPcap 抓取局域网 ARP 报文,解析出设备 IP-MAC 映射表,并自动识别同一 IP 对应多个 MAC 的冲突情况。适合做网络运维工具、资产扫描器或教学演示的开发者,代码量不大,但涉及网卡抓包、协议解析和冲突判定三个关键环节,跑通之后能直接嵌进自己的运维面板里。
2. 环境搭建与抓包入口:从零把 SharpPcap 跑起来
2.1 依赖选型与项目初始化
这个项目用 C# 写,核心依赖是 SharpPcap,它是对 libpcap/Npcap 的 .NET 封装,跨 Windows 和 Linux 都能用。Windows 上需要先装 Npcap 驱动(安装时勾选 WinPcap 兼容模式),Linux 上装 libpcap-dev 即可。新建一个 .NET 控制台项目,通过 NuGet 引入三个包:SharpPcap、PacketDotNet 和 System.CommandLine(可选,用于命令行参数)。我一般会把目标框架设成 net6.0 或 net8.0,前者兼容性好,后者性能更优。
dotnet new console -n ArpMonitor cd ArpMonitor dotnet add package SharpPcap --version 6.2.0 dotnet add package PacketDotNet --version 1.4.7装完之后先别急着写业务代码,跑一个最小示例确认能列出网卡。SharpPcap 的CaptureDeviceList.Instance会返回当前机器所有可抓包的接口,如果列表为空,九成是 Npcap 没装好或者权限不够。Linux 下需要用 sudo 运行,或者给 dotnet 进程赋 CAP_NET_RAW 能力。
using SharpPcap; // 列出所有可抓包网卡 var devices = CaptureDeviceList.Instance; if (devices.Count == 0) { Console.WriteLine("未发现网卡,请检查 Npcap/libpcap 是否安装"); return; } foreach (var dev in devices) { // 打印网卡名称和描述,方便选择 Console.WriteLine($"{dev.Name} - {dev.Description}"); }这段代码的作用是枚举本机网卡。dev.Name在 Windows 上是\Device\NPF_{GUID}格式,Linux 上是eth0、ens33这类名字。参数方面没有需要调的,重点看输出里有没有你实际连接局域网的那块网卡。常见坑是虚拟机里有多块虚拟网卡,选错了就抓不到真实流量。
2.2 打开网卡并设置过滤器
确定网卡后,下一步是打开设备并设置 BPF 过滤器。ARP 报文在以太网层的类型字段是 0x0806,用过滤器arp就能只抓 ARP 包,避免把 HTTP、DNS 流量全捞进来导致内存暴涨。打开设备时把ReadTimeout设成 1000 毫秒,这样主循环不会死等。
var device = devices[0]; // 实际使用时根据名称选择 device.Open(new DeviceConfiguration { Mode = DeviceModes.Promiscuous, // 混杂模式,能抓到非本机目标的 ARP ReadTimeout = 1000 }); // 只抓 ARP 包,减少无关流量 device.Filter = "arp";DeviceModes.Promiscuous是混杂模式,交换机环境下如果端口没做隔离,能收到广播域内其他设备的 ARP 请求和应答。ReadTimeout控制GetNextPacket的阻塞时长,设太小会空转耗 CPU,设太大退出时不够灵敏。过滤器字符串遵循 BPF 语法,除了arp,还可以写arp and src net 192.168.1.0/24来限定网段。
2.3 抓包主循环与事件回调
SharpPcap 有两种消费方式:事件回调和主动轮询。事件回调适合长时间后台运行,轮询适合需要控制节奏的场景。这个项目用轮询更直观,主循环里不断取包,取到就交给解析函数。
while (true) { var status = device.GetNextPacket(out PacketCapture capture); if (status != GetPacketStatus.PacketRead) continue; // 超时或出错,继续下一轮 var rawPacket = capture.GetPacket(); // 交给 ARP 解析模块处理 ArpParser.Process(rawPacket); }GetNextPacket返回状态枚举,只有PacketRead才表示拿到了有效包。capture.GetPacket()拿到的是原始字节,后面用 PacketDotNet 解析。这里要注意,如果 Ctrl+C 退出,需要调用device.Close()释放句柄,否则下次运行可能报“设备已被占用”。我一般会注册Console.CancelKeyPress事件来做清理。
3. 核心模块拆解:ARP 报文解析与 IP-MAC 映射表维护
3.1 从原始字节到 ARP 结构体
拿到原始包之后,用 PacketDotNet 逐层剥开。以太网帧头之后如果是 ARP,就转成ArpPacket。ARP 报文里关键字段有四个:发送方 IP、发送方 MAC、目标 IP、目标 MAC。请求包的 TargetMac 是全零,应答包的 TargetMac 是请求方的 MAC。
using PacketDotNet; public static void Process(RawCapture rawPacket) { var packet = Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); var arp = packet.Extract<ArpPacket>(); if (arp == null) return; // 操作码 1=请求 2=应答 bool isReply = arp.Operation == ArpOperation.Response; string senderIp = arp.SenderProtocolAddress.ToString(); string senderMac = arp.SenderHardwareAddress.ToString(); string targetIp = arp.TargetProtocolAddress.ToString(); // 更新映射表 ArpTable.Update(senderIp, senderMac, isReply); }arp.Operation是 ushort 类型,1 表示 who-has 请求,2 表示 is-at 应答。SenderProtocolAddress是 IPAddress 类型,ToString()得到点分十进制。SenderHardwareAddress是物理地址,格式化成AA-BB-CC-DD-EE-FF。这里有个细节:ARP 请求包里 SenderMac 是发起方的真实 MAC,TargetMac 是全零;应答包里 SenderMac 是应答方的 MAC。两种包都能用来学习映射关系,但请求包的可靠性更高,因为它是广播的,不容易被中间设备篡改。
3.2 映射表的数据结构与冲突判定
映射表用ConcurrentDictionary<string, HashSet<string>>,key 是 IP,value 是该 IP 出现过的所有 MAC 集合。正常情况一个 IP 只对应一个 MAC,集合大小是 1;如果集合大小大于 1,说明有冲突。用 ConcurrentDictionary 是因为抓包回调和定时清理可能在不同线程。
public static class ArpTable { // IP -> 多个 MAC private static readonly ConcurrentDictionary<string, HashSet<string>> _table = new(); public static void Update(string ip, string mac, bool isReply) { var macs = _table.GetOrAdd(ip, _ => new HashSet<string>()); lock (macs) { macs.Add(mac); if (macs.Count > 1) { // 冲突:同一 IP 出现多个 MAC Console.WriteLine($"[冲突] IP {ip} 对应 MAC: {string.Join(", ", macs)}"); } } } public static IReadOnlyDictionary<string, HashSet<string>> Snapshot() => _table; }GetOrAdd保证 IP 首次出现时创建集合,lock保护 HashSet 的并发写入。冲突判定逻辑很简单:集合元素超过 1 就报警。但实际场景里要区分“真冲突”和“设备换网卡”——比如一台笔记本先插网线后连 WiFi,MAC 变了但 IP 没变,这不算冲突。我的做法是加一个时间窗口,比如 5 分钟内同一 IP 出现两个 MAC 才判定为冲突,超过窗口的旧 MAC 自动过期。这个逻辑可以放在定时任务里做,每 30 秒清理一次超过 5 分钟未更新的条目。
3.3 定时输出与设备列表持久化
抓包是持续的,但人不能一直盯着控制台。加一个定时器,每 10 秒把当前映射表快照输出到控制台或写入 CSV。CSV 字段包括 IP、MAC 列表、首次发现时间、最后更新时间、是否冲突。
private static Timer _reportTimer; public static void StartReporting() { _reportTimer = new Timer(_ => { var snapshot = ArpTable.Snapshot(); foreach (var kv in snapshot) { bool conflict = kv.Value.Count > 1; // 写入 CSV 或打印 Console.WriteLine($"{kv.Key},{string.Join("|", kv.Value)},{conflict}"); } }, null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); }Timer的回调在 ThreadPool 线程执行,和抓包线程不冲突。输出格式用逗号分隔,方便后续用 Excel 或脚本处理。如果设备多,控制台刷屏太快,可以改成只在有变化时输出,或者写文件。我一般会同时写一份 JSON 快照,方便前端面板读取。
4. 避坑与排查:抓不到包、误报冲突、性能抖动的处理
4.1 抓不到任何 ARP 包
现象:程序启动后控制台一直空白,映射表始终为空。原因通常是网卡选错或权限不足。Windows 上如果没装 Npcap 或者装了但没勾选“WinPcap 兼容模式”,CaptureDeviceList可能返回空列表。Linux 上非 root 用户没有 raw socket 权限,device.Open会抛异常。解决:Windows 重装 Npcap 并勾选兼容模式;Linux 用sudo dotnet run或者setcap cap_net_raw+ep /usr/bin/dotnet赋权。另外确认选中的网卡确实连接了目标局域网,虚拟机里默认网卡可能是 NAT 模式,抓不到物理局域网的广播包。
4.2 同一 IP 频繁出现两个 MAC 但并非冲突
现象:映射表里某个 IP 对应两个 MAC,但网络实际正常。原因可能是设备同时开启了有线网卡和无线网卡,或者做了网卡绑定(bonding)。ARP 请求会从两个网卡分别发出,导致映射表里出现两个 MAC。解决:在冲突判定里加时间窗口和 MAC 厂商前缀过滤。如果两个 MAC 的 OUI 相同(同一厂商),大概率是同一台设备的多网卡;如果 OUI 不同,才更可能是真冲突。另外可以只统计 ARP 应答包,因为应答包通常只从主用网卡发出。
4.3 长时间运行内存持续增长
现象:程序跑几个小时后内存占用从几十 MB 涨到几百 MB。原因:映射表只增不减,过期条目没有清理。解决:加定时清理任务,遍历映射表,把最后更新时间超过阈值(比如 10 分钟)的 MAC 从集合里移除,集合为空时删除整个 IP 条目。清理频率不用太高,每 30 秒一次足够。另外注意RawCapture对象用完即弃,不要缓存,否则 GC 压力很大。
4.4 混杂模式下抓到大量无关包导致 CPU 高
现象:CPU 占用率长期在 20% 以上,风扇狂转。原因:过滤器没设或者设得太宽,抓到了整个广播域的所有流量。解决:确保device.Filter = "arp"生效,如果还高,可以进一步限定arp and ether broadcast只抓广播 ARP。另外ReadTimeout不要设成 0(无限阻塞),设 500 到 1000 毫秒比较平衡。如果网段里设备特别多(超过 500 台),可以考虑用arp and src net 192.168.1.0/24限定网段。
4.5 在 Windows 服务里运行时报会话错误
现象:把程序注册成 Windows 服务后,启动就报“无法打开设备”或“会话失败”。原因:Windows 服务默认运行在 Session 0,没有桌面交互权限,Npcap 驱动不允许 Session 0 进程直接抓包。解决:不要用 Windows 服务方式运行,改用计划任务(Task Scheduler)并勾选“不管用户是否登录都要运行”,或者用 NSSM 这类工具把控制台程序包装成服务并指定用户会话。如果必须用服务,考虑换用基于 WinPcap 的旧接口或者改用 ETW 事件追踪。
5. 二次开发与扩展:从 ARP 监控到资产测绘面板
5.1 把控制台输出改成 Web API
控制台程序适合调试,但实际运维更想要一个能远程访问的面板。改造思路是把抓包逻辑封装成一个后台服务类,用 ASP.NET Core 的IHostedService托管,然后暴露两个接口:GET /api/devices返回当前设备列表,GET /api/conflicts只返回冲突条目。映射表用单例注册,抓包服务在ExecuteAsync里启动循环,StopAsync里关闭网卡。
public class ArpMonitorService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken token) { var device = CaptureDeviceList.Instance[0]; device.Open(new DeviceConfiguration { ReadTimeout = 1000 }); device.Filter = "arp"; while (!token.IsCancellationRequested) { var status = device.GetNextPacket(out PacketCapture capture); if (status == GetPacketStatus.PacketRead) ArpParser.Process(capture.GetPacket()); await Task.Delay(10, token); // 让出线程 } device.Close(); } }BackgroundService是 .NET 里托管长任务的标配,ExecuteAsync在应用启动后自动运行。注意Task.Delay不能省,否则循环会占满一个 CPU 核心。token用于优雅退出,StopAsync里调device.Close()释放句柄。接口层用Controller或Minimal API都行,返回 JSON 时把 HashSet 转成数组。
5.2 增加 OUI 厂商识别
光有 MAC 地址不够直观,运维更想知道“这个 MAC 是哪家厂商的”。MAC 前三个字节是 OUI,可以离线维护一份 OUI 映射表(网上有公开的 CSV),加载到内存里做前缀匹配。改造点在ArpTable.Update里,拿到 MAC 后查表得到厂商名,一起存进设备记录。
private static readonly Dictionary<string, string> _ouiMap = LoadOuiCsv("oui.csv"); public static string GetVendor(string mac) { // MAC 格式 AA-BB-CC-DD-EE-FF,取前 8 个字符 string prefix = mac.Substring(0, 8).Replace("-", ":").ToUpper(); return _ouiMap.TryGetValue(prefix, out var vendor) ? vendor : "未知"; }OUI 表大概几万条,用 Dictionary 加载后查询是 O(1)。注意 MAC 格式要统一,SharpPcap 输出的是AA-BB-CC-DD-EE-FF,OUI 表通常是AA:BB:CC,需要做分隔符转换。这个功能加上之后,冲突告警里就能显示“IP 192.168.1.100 被某厂商设备占用”,排查效率提升明显。
5.3 冲突告警推送到内部通知渠道
发现冲突后光打印日志不够,得让人第一时间知道。可以接内部的通知接口,比如企业微信机器人或邮件 SMTP。改造点在冲突判定分支里,当macs.Count > 1且超过时间窗口时,触发一次告警。告警内容包含 IP、冲突 MAC 列表、发现时间。为了避免重复告警,加一个告警冷却期,同一 IP 5 分钟内只告警一次。
private static readonly Dictionary<string, DateTime> _lastAlert = new(); private static void SendAlert(string ip, IEnumerable<string> macs) { if (_lastAlert.TryGetValue(ip, out var last) && (DateTime.Now - last).TotalMinutes < 5) return; // 冷却期内不重复告警 _lastAlert[ip] = DateTime.Now; string msg = $"IP冲突: {ip} 对应 MAC {string.Join(", ", macs)}"; // 调用通知接口 Notifier.Send(msg); }冷却字典也要定期清理,否则长时间运行会积累大量过期 key。可以在清理映射表的同一个定时任务里顺带清理_lastAlert,把超过 30 分钟的条目删掉。
5.4 验证改造效果的方法
改完之后怎么确认没跑偏?我的习惯是搭一个最小测试环境:找两台机器,手动配同一个静态 IP,然后运行程序,看 10 秒内是否报冲突。再改回不同 IP,确认告警消失。对于 OUI 识别,拿几个已知厂商的设备(比如某品牌路由器、某品牌手机)连上,看厂商名是否匹配。对于 Web API,用 curl 或 Postman 请求/api/conflicts,确认返回结构符合预期。最后跑一个 24 小时稳定性测试,观察内存曲线是否平稳、有没有漏报。
从那以后我每次做抓包类工具,都强制走一遍“最小冲突环境验证 + 24 小时内存观察”的流程,不然上线后半夜被叫起来查问题,血泪经验。希望帮到你。
本文还有配套的精品资源,点击获取