简介:面向使用 C# 进行局域网运维与网络监测的开发者和网管人员,这份资源围绕抓取 ARP 数据包来识别在线网络设备,并整理出存在 IP 冲突的设备列表,适合需要实现设备发现、冲突排查或底层抓包入门的中级读者参考。压缩包共 36 个文件,以 cs 源码、sln/csproj 工程文件、config 配置、dll 组件以及 exe 程序等为主,整体约 1002KB,结构上兼顾可编译工程与直接运行示例。资源将抓 ARP 包功能封装为独立类,并附带使用示例及 WinPcap、SharpPcap、PacketDotNet 相关依赖,便于读者在 VS2015、.NET 4.5.2 环境下对照理解采集流程、设备列表生成和冲突判断思路。目前已有 562 人学习,适合作为局域网设备监测与 IP 冲突侦测功能落地的参考实现。
1. 局域网设备发现为什么从 ARP 入手:一个广播包里的设备清单
局域网里做设备盘点,最省事的入口往往不是 ping 全网,而是抓 ARP。用 C# 加 SharpPcap 监听二层广播,把 ARP 请求和应答里的发送方 MAC/IP 收进设备表,再按同一 IP 多个 MAC 的规则聚合冲突设备列表。这个做法不用逐台登录,也不用先知道网段里有哪些地址,只要抓包机处在同一个广播域,就能先拿到一批真实在线的设备线索。
它适合做内网资产巡检、IP 冲突排查、临时网络排障的 C# 开发者。新手能照着把控制台程序跑起来,熟手可以把设备表和冲突聚合器拆出来,接进自己的巡检服务。下面按“选网卡、抓 ARP、建设备表、判冲突、排坑、二次开发”的顺序拆。
2. 环境准备与抓包入口:SharpPcap 选网卡、开混杂模式、过滤 ARP
2.1 开发环境与依赖安装:控制台项目里引入 SharpPcap 和 PacketDotNet
我一般先用一个控制台项目把抓包链路跑通,再谈 UI 或服务化。SharpPcap 负责网卡枚举、打开设备、设置过滤器和收包事件,PacketDotNet 负责把原始字节解析成 ARP 包对象。两者搭配是这套场景里最常见的组合,代码量少,排查路径也短。
dotnet new console -n ArpWatcher cd ArpWatcher dotnet add package SharpPcap dotnet add package PacketDotNet这两条安装命令不写版本号,实际项目里按当前依赖解析结果锁定版本即可。控制台项目的好处是权限问题、网卡选错、过滤器写错都能第一时间暴露出来,不会藏在界面层后面。
依赖装完先别急着写业务,先确认程序能枚举出网卡。SharpPcap 的设备列表里会混着物理网卡、回环设备、虚拟化网卡和已经断开的适配器,名字和描述不一定直观。选错网卡最典型的现象是程序不报错,但一个包都抓不到。
2.2 枚举网卡并打开设备:别在回环网卡上白等十分钟
下面这段代码把设备列表打印出来,按编号让使用者选,然后以混杂模式打开,读超时设为一秒。混杂模式的意思是网卡尽量把经过的帧都交给抓包组件,但它不等于交换机镜像,后面排坑会细说。
using SharpPcap; var devices = CaptureDeviceList.Instance; if (devices.Count == 0) { Console.WriteLine("没有发现可用抓包设备,先检查抓包驱动和权限。"); return; } for (var i = 0; i < devices.Count; i++) { var dev = devices[i]; Console.WriteLine($"[{i}] {dev.Name}"); Console.WriteLine($" 描述: {dev.Description}"); } Console.Write("选择网卡编号: "); var index = int.Parse(Console.ReadLine() ?? "0"); var device = devices[index]; // 1000 毫秒读超时,避免收包线程长期阻塞在底层读取上 device.Open(DeviceModes.Promiscuous, 1000); Console.WriteLine($"已打开: {device.Name}");参数里DeviceModes.Promiscuous控制混杂模式,1000是读超时毫秒数。读超时不是抓包时长,它影响底层读取的等待节奏,设得太长会让停止抓包变慢,设得太短会增加空转。控制台程序里 500 到 1000 毫秒都常见,服务化后可以按退出响应要求调整。
如果列表里出现多个相似名称,优先选有默认网关的那张网卡,或者先用系统命令确认网卡对应的网段。选到回环设备、已断开的虚拟网卡,程序通常不会崩,但抓不到目标广播域的 ARP,这种“静默失败”最耗时间。
2.3 过滤器与回调骨架:让 ARP 包进入自己的处理队列
打开设备后设置 BPF 过滤器arp,只把 ARP 帧送进回调,能明显减少无关流量。回调里不要做控制台输出、文件写入和网络请求,先把解析出的观测记录放进队列,后台线程再消费。
using PacketDotNet; using SharpPcap; device.Filter = "arp"; device.OnPacketArrival += (sender, capture) => { var raw = capture.GetPacket(); var packet = Packet.ParsePacket(raw.LinkLayerType, raw.Data); var arp = packet.Extract<ArpPacket>(); if (arp is null) { return; } // 这里只做轻量解析,重活交给后台队列 var senderMac = arp.SenderHardwareAddress?.ToString(); var senderIp = arp.SenderProtocolAddress?.ToString(); if (string.IsNullOrWhiteSpace(senderMac) || string.IsNullOrWhiteSpace(senderIp)) { return; } Console.WriteLine($"{DateTime.Now:HH:mm:ss} {arp.Operation} {senderIp} -> {senderMac}"); }; device.StartCapture(); Console.WriteLine("抓包中,按回车停止..."); Console.ReadLine(); device.StopCapture(); device.Close();device.Filter = "arp"是过滤器,写错成ARP在部分环境里也能过,但 BPF 表达式习惯用小写。capture.GetPacket()拿到原始帧,Packet.ParsePacket按链路层类型解析,Extract<ArpPacket>()取 ARP 层。arp.Operation用来区分请求和应答,SenderHardwareAddress和SenderProtocolAddress是发送方 MAC 与 IP,这两个字段是设备表的主料。
这段骨架能跑通,说明网卡、权限、过滤器、解析链路都通了。接下来再把Console.WriteLine换成入队和聚合,别在这一步就写复杂业务,否则抓不到包时很难判断是解析问题还是业务问题。
3. ARP 报文解析与设备表设计:从 MAC/IP 到在线设备模型
3.1 ARP 包字段拆解:Operation、SenderProtocolAddress 和 SenderHardwareAddress
ARP 包不大,但字段方向容易看混。请求包里发送方 MAC/IP 是请求者的信息,目标 MAC 通常是全零,目标 IP 是被询问的地址;应答包里发送方 MAC/IP 是应答者的信息。做设备发现时,重点看发送方字段,因为那是实际发出这个 ARP 帧的设备。
private static void HandleArp(ArpPacket arp) { var operation = arp.Operation; // Request 或 Reply var senderMac = arp.SenderHardwareAddress?.ToString(); var senderIp = arp.SenderProtocolAddress?.ToString(); var targetIp = arp.TargetProtocolAddress?.ToString(); if (string.IsNullOrWhiteSpace(senderMac) || string.IsNullOrWhiteSpace(senderIp)) { return; } Console.WriteLine($"op={operation}, sender={senderIp}/{senderMac}, targetIp={targetIp}"); }Operation用来判断这个包是请求还是应答。SenderHardwareAddress取发送方 MAC,SenderProtocolAddress取发送方 IP,TargetProtocolAddress取目标 IP。目标 MAC 在请求包里经常是零地址,不能拿它当设备 MAC。参数说明里要记住:设备表写入用发送方 MAC/IP,不要用目标字段反推。
同一个 MAC 可能对应多个 IP,比如一块网卡配了多个地址,或者设备刚换过 IP 还在发旧地址的 ARP。设备表不能假设 MAC 和 IP 是一对一,否则后面冲突判断会乱。
3.2 设备表结构:以 MAC 为主键、IP 集合为值
设备表用 MAC 做主键更合适,因为 MAC 在同一广播域里相对固定,IP 会变。每个设备记录里放一个 IP 集合、首次出现时间、最近出现时间和 ARP 计数。IP 集合用HashSet<string>,比较器忽略大小写,MAC 统一成冒号分隔大写,避免AA-BB和aa:bb被当成两个设备。
using System.Collections.Concurrent; public sealed class DeviceRecord { public string Mac { get; init; } = ""; public HashSet<string> Ips { get; } = new(StringComparer.OrdinalIgnoreCase); public DateTime FirstSeen { get; init; } = DateTime.UtcNow; public DateTime LastSeen { get; set; } = DateTime.UtcNow; public int ArpCount { get; set; } public object SyncRoot { get; } = new(); } public sealed class DeviceTable { private readonly ConcurrentDictionary<string, DeviceRecord> _devices = new(StringComparer.OrdinalIgnoreCase); public void Observe(string mac, string ip, DateTime seenAt) { mac = NormalizeMac(mac); var record = _devices.GetOrAdd(mac, key => new DeviceRecord { Mac = key }); lock (record.SyncRoot) { record.Ips.Add(ip); record.LastSeen = seenAt; record.ArpCount++; } } public IReadOnlyList<DeviceRecord> Snapshot() { return _devices.Values .OrderBy(x => x.Mac, StringComparer.OrdinalIgnoreCase) .ToList(); } public void RemoveStale(DateTime now, TimeSpan ttl) { foreach (var pair in _devices) { lock (pair.Value.SyncRoot) { if (now - pair.Value.LastSeen > ttl) { _devices.TryRemove(pair.Key, out _); } } } } private static string NormalizeMac(string mac) { return mac.Replace('-', ':').ToUpperInvariant(); } }ConcurrentDictionary保证不同 MAC 的并发插入安全,但DeviceRecord内部的Ips和计数还是需要锁,因为同一个 MAC 的多个 ARP 包可能同时进来。Observe的参数是 MAC、IP 和观测时间,时间用 UTC,输出时再转本地时区。RemoveStale按最近出现时间清理,TTL 可以设 10 到 30 分钟,局域网设备下线后不会立刻从表里消失,这个延迟要接受。
3.3 线程安全写入与过期清理:ConcurrentDictionary 加定时任务
抓包回调是高频路径,设备表写入要尽量短。定时清理不要放在回调里,用System.Threading.Timer每 30 秒跑一次,把超过 TTL 的设备删掉,同时把冲突聚合器里过期的 IP 观察记录清掉。
var table = new DeviceTable(); var cleanupTimer = new Timer(_ => { var now = DateTime.UtcNow; table.RemoveStale(now, TimeSpan.FromMinutes(15)); }, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));TimeSpan.FromMinutes(15)是设备 TTL,30秒是清理间隔。TTL 太短会把偶尔发一次 ARP 的设备误判为下线,太长会让表里堆历史设备。清理定时器要在程序退出时Dispose,否则控制台程序关闭时可能残留后台线程。
设备表快照输出时不要直接遍历ConcurrentDictionary的Values做复杂排序,先ToList再排序,避免抓包线程同时写入造成枚举开销。这个细节在设备多的时候能减少回调阻塞。
4. IP 冲突判定策略:同 IP 多 MAC、重复应答与时间窗口
4.1 冲突判定不是简单计数:方向、时间窗口和主动扫描
最直接的冲突规则是:同一个 IP 在时间窗口内出现多个发送方 MAC。但不能一看到两个 MAC 就报冲突,因为网关可能代答 ARP,虚拟化环境里也可能出现地址漂移,设备更换网卡时短时间会有两个 MAC 交替。判定要加时间窗口、忽略名单和人工复核入口。
在交换网络里,ARP 请求通常是广播,抓包机在同一个广播域能看到;ARP 应答往往是单播,抓包机不一定看得到别人之间的应答。所以设备发现主要靠广播请求里的发送方字段,冲突检测如果只依赖应答会漏。常见做法是同时监听请求和应答,再对可疑 IP 主动发 ARP 请求做二次确认。
public sealed record ArpObservation( string Ip, string Mac, bool IsReply, DateTime SeenAt);这个记录结构把 IP、MAC、是否应答和观测时间带到聚合器。IsReply用来给冲突结果加权重,应答里的同 IP 多 MAC 更值得关注,请求里的则先记录。实际项目里还可以加SourceInterface,多网卡抓包时区分来源。
4.2 冲突聚合器实现:IpObservation 与 ConflictRecord
聚合器以 IP 为主键,里面维护一个 MAC 到最近出现时间的字典。每次观测先清掉窗口外的旧 MAC,再看剩余 MAC 数量是否大于一。冲突记录保留 IP、MAC 列表、首次时间和最近时间,输出时按最近时间倒序。
using System.Collections.Concurrent; public sealed class MacSeen { public DateTime LastSeen { get; set; } public int Count { get; set; } } public sealed class IpObservation { public string Ip { get; init; } = ""; public Dictionary<string, MacSeen> Macs { get; } = new(StringComparer.OrdinalIgnoreCase); public DateTime FirstSeen { get; init; } = DateTime.UtcNow; public DateTime LastSeen { get; set; } = DateTime.UtcNow; public bool IsConflict { get; set; } } public sealed record ConflictRecord( string Ip, string[] Macs, DateTime FirstSeen, DateTime LastSeen); public sealed class ConflictDetector { private readonly ConcurrentDictionary<string, IpObservation> _ips = new(); private readonly TimeSpan _window; private readonly HashSet<string> _ignoreIps; public ConflictDetector(TimeSpan window, IEnumerable<string> ignoreIps) { _window = window; _ignoreIps = new HashSet<string>(ignoreIps, StringComparer.OrdinalIgnoreCase); } public void Observe(ArpObservation observation) { if (_ignoreIps.Contains(observation.Ip)) { return; } var item = _ips.GetOrAdd( observation.Ip, key => new IpObservation { Ip = key, FirstSeen = observation.SeenAt }); lock (item) { item.LastSeen = observation.SeenAt; if (!item.Macs.TryGetValue(observation.Mac, out var macSeen)) { macSeen = new MacSeen(); item.Macs[observation.Mac] = macSeen; } macSeen.LastSeen = observation.SeenAt; macSeen.Count++; var expired = item.Macs .Where(kv => observation.SeenAt - kv.Value.LastSeen > _window) .Select(kv => kv.Key) .ToList(); foreach (var mac in expired) { item.Macs.Remove(mac); } item.IsConflict = item.Macs.Count > 1; } } public IReadOnlyList<ConflictRecord> Snapshot() { var now = DateTime.UtcNow; var result = new List<ConflictRecord>(); foreach (var item in _ips.Values) { lock (item) { if (item.IsConflict && now - item.LastSeen <= _window) { result.Add(new ConflictRecord( item.Ip, item.Macs.Keys.ToArray(), item.FirstSeen, item.LastSeen)); } } } return result.OrderByDescending(x => x.LastSeen).ToList(); } public void RemoveStale(DateTime now) { foreach (var pair in _ips) { lock (pair.Value) { if (now - pair.Value.LastSeen > _window * 3) { _ips.TryRemove(pair.Key, out _); } } } } }_window是冲突观察窗口,常见设 2 到 5 分钟。太短会把正常的地址切换打成冲突,太长会把历史 MAC 留在记录里。_ignoreIps放网关、保留地址和已知的虚拟地址,避免误报。Observe里先清理过期 MAC 再判断数量,Snapshot只返回窗口内还有活动的冲突,RemoveStale按窗口的三倍做兜底清理。
4.3 输出冲突设备列表:控制台表格和 CSV 落盘
冲突列表要能落到文件,方便后续和交换机 MAC 表、DHCP 租约表比对。控制台按 IP、MAC 列表、最近时间打印,CSV 用 UTF-8,字段里有逗号时加引号转义。输出频率不要跟抓包回调同步,定时拉快照即可。
using System.Text; public static class ConflictWriter { public static void WriteCsv(string path, IEnumerable<ConflictRecord> rows) { var sb = new StringBuilder(); sb.AppendLine("ip,macs,first_seen,last_seen"); foreach (var row in rows) { var macs = string.Join("|", row.Macs); sb.AppendLine( $"{Escape(row.Ip)},{Escape(macs)},{row.FirstSeen:O},{row.LastSeen:O}"); } File.WriteAllText(path, sb.ToString(), Encoding.UTF8); } private static string Escape(string value) { if (!value.Contains(',') && !value.Contains('"')) { return value; } return $"\"{value.Replace("\"", "\"\"")}\""; } }WriteCsv的rows来自ConflictDetector.Snapshot(),落盘路径由调用方传。Escape处理逗号和双引号,时间用O格式保留可排序的 ISO 字符串。CSV 不是唯一出口,后面二次开发可以换成数据库或消息队列,但文件落盘是最容易验证的一步。
5. 常见问题与排查:抓不到包、冲突误报和长时间运行的坑
5.1 抓不到 ARP 包:先查网卡和广播域,再查权限
现象:程序能启动,设备列表也打印了,但回调一条记录都没有,控制台安静得像没跑。原因通常不是代码,而是选错网卡、抓包权限不足,或者抓包机不在目标广播域。解决顺序是先用系统命令确认网卡和地址,再用管理员权限运行,最后确认抓包口和被测设备在同一广播域。
Get-NetAdapter | Format-Table Name, Status, LinkSpeed Get-NetIPConfiguration | Format-ListGet-NetAdapter看网卡状态,Get-NetIPConfiguration看地址和网关。Linux 下可以用ip link和ip addr看接口状态。SharpPcap 在 Windows 下依赖抓包驱动组件,安装时没装全或者权限不够,打开设备就会失败;Linux 下普通用户没有原始套接字权限,需要提权或给可执行文件加能力。别在回环网卡上排查业务问题,先换到有广播流量的物理口。
5.2 冲突列表误报:网关代答、虚拟化和时间窗口
现象:冲突列表里某个 IP 长期挂着两个 MAC,其中一个还是网关或虚拟化设备的 MAC。原因可能是网关代答 ARP,也可能是虚拟化环境里地址漂移,或者时间窗口设得太短,把正常的网卡切换打成了冲突。解决是把已知网关和虚拟地址加入忽略名单,把窗口从 1 分钟拉到 3 到 5 分钟,再对可疑记录做主动 ARP 确认。
var detector = new ConflictDetector( window: TimeSpan.FromMinutes(3), ignoreIps: new[] { "192.168.1.1", "192.168.1.254" });window控制观察窗口,ignoreIps是忽略名单。忽略名单不要写死在代码里,放到配置文件或环境变量,现场排障时能改。冲突结果只作为线索,不要直接当成故障结论,尤其是网关、防火墙、虚拟化宿主机相关的地址,先人工复核。
5.3 回调里做重活:包丢了、内存涨了
现象:跑十几分钟后控制台开始卡,ARP 计数和实际设备对不上,内存曲线往上走。原因是在OnPacketArrival里直接Console.WriteLine、写文件或者做复杂 LINQ 查询,回调线程被拖慢,底层缓冲区顶不住。解决是把解析结果放进BlockingCollection或Channel,后台任务批量消费,控制台输出限频。
using System.Collections.Concurrent; var queue = new BlockingCollection<ArpObservation>(boundedCapacity: 10000); // 抓包回调里只入队,队列满时丢弃并计数 if (!queue.TryAdd(observation)) { Interlocked.Increment(ref droppedCount); } // 后台消费 Task.Run(() => { foreach (var item in queue.GetConsumingEnumerable()) { // 批量更新设备表、冲突聚合器,或定时落盘 } });boundedCapacity是队列上限,设 10000 是经验值,设备多、ARP 风暴时能防止内存无限涨。TryAdd失败要计数,别静默丢包。后台消费里也不要每条都写盘,按时间窗口批量写。回调只做轻量解析和入队,这条纪律在长时间运行里比任何优化都管用。
5.4 过滤器和混杂模式的边界:不是开了就全都能看见
现象:以为开了混杂模式就能看到整个局域网的所有 ARP,结果只看到自己和广播包。原因是交换机会按 MAC 表转发,单播 ARP 应答不会发到抓包口,混杂模式只影响网卡交付,不改变交换机转发。解决是接入镜像端口,或者接受“主要靠广播 ARP 请求做发现”的现实,再补主动 ARP 扫描。
过滤器写成arp只保留 ARP,但不会帮你扩大广播域。抓包口如果是普通接入端口,能看到本口广播和本机流量,其他端口之间的单播看不到。现场有条件就做端口镜像,没条件就把程序部署在网关旁边或核心交换机的镜像口上。这个边界不搞清楚,后面所有冲突判断都会缺数据。
6. 二次开发:把 ARP 抓包器接进资产巡检和告警链路
这套代码再往前走,最值钱的不是控制台打印,而是把设备表和冲突表变成可查询的数据。我一般会先加一个配置对象,把网卡名、抓包窗口、忽略 IP、输出路径抽出去,再用后台任务每 30 秒拉一次快照,写进数据库或发给告警接口。改造点不用多,先把“抓包、聚合、输出”三段拆开,后面换存储、换告警渠道都不动核心解析。
| 改造点 | 落点 | 验证方法 |
|---|---|---|
| 配置化 | 网卡名、冲突窗口、忽略 IP 放配置文件 | 改窗口后重启,冲突列表数量应变化 |
| 主动扫描 | 定时发 ARP 请求补齐单播盲区 | 对比只监听时的设备数,应只增不减 |
| 资产比对 | 设备表与 DHCP 租约、交换机 MAC 表对账 | 随机抽 10 条 MAC 查归属 |
| 告警 | 冲突记录按 IP 去重后推送 | 构造同 IP 双 MAC 测试,确认只告警一次 |
主动扫描这块,可以用 SharpPcap 的发包能力构造 ARP 请求,目标 IP 从配置的网段列表里轮询。发之前先确认本机有权限,发之后仍然靠回调收结果,不要把发包和收包写成同步等待。另一个技巧是给冲突记录加“首次发现”和“最近发现”两个时间,告警只对首次发现推送,最近发现用来更新状态,避免同一冲突反复刷屏。
验证改造是否有效,我习惯用两个土办法:一是找一台设备手动改 IP 制造同 IP 双 MAC,观察冲突表是否在窗口内出现两条 MAC;二是把抓包口拔掉,看设备表是否按 TTL 逐步清空。两个都过,再谈接数据库和告警。从那以后我每次加新输出通道,都强制先跑一遍“拔网线”和“双 MAC”这两个场景,不然数据看着有,真出事时查不到根上。希望帮到你。
本文还有配套的精品资源,点击获取