资讯详情

资讯详情

SharpPcap ARP抓包与IP冲突检测:局域网设备发现实践

简介这是一份面向C#开发者与局域网运维人员的网络监测源码资料重点演示如何借助SharpPcap抓取ARP报文来发现局域网设备并汇总存在IP冲突的设备列表适合需要实现设备巡检、冲突告警与网络排障功能的读者参考。压缩包共36个文件约1002KB其中11个cs文件承载核心逻辑解决方案与项目文件便于直接打开工程dll、exe、config、resx等覆盖依赖、运行与配置环节包内附抓包依赖组件、安装程序及使用示例能减少环境搭建与联调成本。已有560人学习读者可参考抓包类的封装方式、报文解析与设备识别流程、冲突判定思路以及示例调用方法并据此验证抓包环境、定位配置问题为后续扩展监测功能提供可复用的实现基础。尤其适合已有一定C#基础、希望理解底层报文处理与冲突检测细节的开发者。1. ARP 是局域网里最不可靠的协议IP 冲突为何总是最后才被发现在局域网里ARP 抓包、IP 冲突排查、设备列表盘点这几件事本质上是在处理同一个问题二层网络上并没有一份权威的设备登记表。交换机只维护 MAC 地址表不知道 IP 归属DHCP 服务器只知道谁申请过地址不知道谁在使用地址而 ARP 协议本身没有认证、没有序号、没有应答确认任何一台机器都可以声明自己是某个 IP。C# 配 SharpPcap 从帧层面把 ARP 报文抓下来就是把这份“口头声明”变成可追溯的证词。它解决的问题很具体当两台设备的 IP 冲突时网络里没有任何日志记录这件事只能靠观察谁在广播里反复声明同一个地址。值得深入理解的原因在于ARP 的行为决定了你能观测到什么也决定了你抓到的设备列表有没有偏差。2. ARP 报文剖析28 个字节里藏着哪些判断依据ARP 是极少数没有传输层、直接封装在以太网帧里的协议。它简单到只有 28 字节的有效载荷但也正因为简单所有判断设备身份的信息都必须从这几个字段里推导出来。想做出可用的设备列表和冲突检测先要弄清每个字节代表什么、哪些字段在什么场景下会变成无效值。2.1 逐字段拆解偏移、长度与常见取值ARP 报文固定 28 字节紧跟在 14 字节的以太网头之后以太类型字段为 0x0806。字段布局是这样的偏移相对 ARP 载荷长度字段关键取值与陷阱02硬件类型1 表示以太网为 1 才继续解析22协议类型0x0800 表示 IPv4IPv6 走 NDP不在这里41硬件地址长度固定 651协议地址长度固定 462操作码1 请求2 应答3/4 是 RARP86发送方 MAC冲突检测的第一主键144发送方 IP冲突检测的第二主键186目标 MAC请求包里通常是全 0不能当有效地址用244目标 IP请求包里是被询问的那一方这里有一个容易出错的地方以太网最小帧长是 60 字节不含 CRC而 ARP 帧只有 14 28 42 字节不足的部分由网卡填充 18 字节的零。这意味着解析时不能用“帧长度减去头部”来推断载荷长度必须按固定偏移读取。另外操作码为 1 的请求包里目标 MAC 字段是全零如果有人把它当成“目标设备的真实 MAC”写进设备表就会得到一堆 00:00:00:00:00:00 的无效条目。2.2 请求广播与应答单播的不对称决定了你只能看到一半ARP 的工作方式是广播问、单播答。A 想知道 192.168.1.20 的 MAC就向 ff:ff:ff:ff:ff:ff 发一个请求交换机把这个帧泛洪到同一 VLAN 的所有端口所以接在任何一个端口上的抓包程序都能看到这个请求。但 B 的应答是直接单播回给 A 的交换机会查 MAC 地址表把它精确转发到 A 所在的端口抓包机如果不在这个路径上就完全看不到这个应答。这个不对称性是整个方案的核心约束被动抓包时你能稳定看到的是“所有 ARP 请求”和“所有广播型 ARP免费 ARP、ARP 探测”而“普通的 ARP 应答”只有在它恰好经过你所在的端口时才会出现。所以设备列表的完整性取决于你是否同时利用了两个信息源请求包的发送方字段谁在问说明谁在线和应答包的发送方字段谁在答说明谁在线。只统计应答列表会少一大半只统计请求会漏掉那些只被查询、从不主动发问的静默设备比如打印机、摄像头、部分 IoT 终端。2.3 四类冲突信号把“谁在说自己是某个 IP”变成可判定的证据冲突检测的本质不是“发现两个 MAC”而是“在合理的时间窗口内同一个 IP 被两个不同的 MAC 声明为自有”。按证据强度从高到低可以拆成四类信号信号 A同一 IP 双 MAC 声明窗口内 IP 相同、发送方 MAC 不同。这是最经典的 IP 冲突但也可能是 DHCP 地址池回收后重新分配需要用时间窗口区分。信号 B本机地址被他人声明抓到的 ARP 请求里目标 IP 等于本机 IP而发送方 MAC 不是本机 MAC。这说明有人正在询问“谁是本机 IP”而提问者自己很可能就占着这个地址——这是本机 IP 冲突最直接的证据。信号 C免费 ARP 不一致发送方 IP 等于目标 IP 的报文Gratuitous ARP通常用于设备上线时通告自己、或者检测地址是否被占用。如果这个通告里的 MAC 与本机记录的历史 MAC 不符就是抢占信号。信号 D单 MAC 多 IP 声明一个 MAC 在窗口内声称拥有多个 IP。这大概率不是冲突而是路由器子接口、虚拟化网桥、代理 ARP 或双 IP 配置。把它当成冲突上报会让告警列表里有很多误报。把 D 单独拆出来是这套方案能否在生产环境可用的一条明显分界线。很多自制脚本把“一个 MAC 多个 IP”和“一个 IP 多个 MAC”混为一谈结果在一个有虚拟化集群的网段里告警会持续不断。3. SharpPcap 抓包链路从网卡混杂模式到托管回调SharpPcap 本身不做协议解析它只是 libpcap/Npcap 的托管封装。理解这一层封装在哪、不封装在哪决定了你能不能写出不丢包、不乱序、不误判的采集器。整条链路是打开设备 → 设置混杂模式与快照长度 → 下发 BPF 过滤器 → 注册回调 → 启动抓包循环。每一环都有明确的失败模式。3.1 打开设备混杂模式、读超时与快照长度using SharpPcap; using SharpPcap.LibPcap; // 枚举所有网卡优先挑选已启用、描述里带 Ethernet 的那块 var devices CaptureDeviceList.Instance; var device devices.FirstOrDefault(d d.Description ! null d.Description.Contains(Ethernet, StringComparison.OrdinalIgnoreCase)) ?? devices[0]; device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 混杂模式收下所有经过网卡的帧而不只是发给本机的 ReadTimeout 1000, // 毫秒没有包时让抓包循环及时返回 Snaplen 65535 // 快照长度上限ARP 帧才 42 字节给足即可 }); device.Filter arp; // BPF 表达式必须在 StartCapture 之前下发 device.OnPacketArrival OnArpArrived; device.StartCapture();Mode Promiscuous是能否看到“别人的 ARP”的前提。默认模式下网卡只把目的 MAC 是自己或广播/组播的帧交给上层混杂模式则全部上交。这里要注意两点一是混杂模式在交换机环境下并不能突破端口隔离你仍然只能看到泛洪到本端口的帧二是ReadTimeout不是“抓包超时”而是底层pcap_next_ex的等待上限设得太大会让停止抓包时卡住设得太小会增加空转开销1000 毫秒是比较稳妥的取值。3.2 BPF 过滤器把裁剪放在内核态做device.Filter arp这行的价值常被低估。BPF 表达式会被编译成字节码下发给驱动在数据进入用户态之前就完成过滤。一个千兆网段在业务高峰期的帧速率可以轻松到几十万 PPS如果不加过滤全量上交托管层的回调会被无关流量占满ARP 包被丢弃在内核缓冲区里表现出来就是“设备列表偶尔缺少某些设备”。等价的写法还有ether proto 0x0806可读性差一些但语义更直接。如果还想顺带判断 DHCP 租约可以写成arp or (udp and (port 67 or port 68))把地址分配的时间点和 ARP 声明的时间点对齐误报率会明显下降。需要注意的细节是过滤器要在StartCapture之前设置如果抓包已经启动修改Filter会触发重新下发中间极短的时间窗口内容易漏包。3.3 回调线程模型单线程同步执行不能在这里做重活OnPacketArrival不是每个包起一个线程而是在 libpcap 的抓包循环线程里同步调用。也就是说回调里花的时间就是丢包的时间。慢一毫秒内核环形缓冲区就多积压一批帧积压满之后pcap_stats的ps_drop计数开始上涨而你从业务层面完全看不出来只会觉得“抓包结果不稳定”。正确的做法是回调里只做“取帧、拷贝、入队”解析和聚合放到独立线程using System.Threading.Channels; // 有界队列容量给到 8 万帧满了就走 DropOldest避免内存无限增长 var queue Channel.CreateBoundedRawFrame(new BoundedChannelOptions(80_000) { FullMode BoundedChannelFullMode.DropOldest, SingleReader true, SingleWriter true }); device.OnPacketArrival (_, e) { var raw e.GetPacket(); // 只做最小化处理拷贝数据 带上抓包时间戳 queue.Writer.TryWrite(new RawFrame(raw.Data, raw.Timeval.Date)); }; device.StartCapture(); // 消费线程解析、聚合、判定都在这里做不阻塞抓包循环 var consumer Task.Run(async () { await foreach (var frame in queue.Reader.ReadAllAsync()) _collector.Ingest(frame); });e.GetPacket().Data返回的是当次抓包缓冲区的一份拷贝可以安全地跨线程传递但Timeval.Date一定要用抓包时间戳而不是DateTime.Now否则在队列积压时会因为消费延迟导致时间窗口判断整体偏移把正常换机误判成冲突。这一点很容易被忽略因为单机压测时队列几乎不满问题只在真实网络里才会暴露属于典型的“上线才触发的问题”。4. 聚合模型把杂乱报文整理成设备表与冲突表抓包只是原料真正决定结果质量的是聚合模型。原始 ARP 报文是一串无状态的事件流同一个设备在几分钟内可能发出几十个请求、收到几十个应答。要把它变成“设备列表”和“冲突列表”两张表需要确定三件事主键怎么选、状态怎么流转、记录怎么老化。4.1 数据结构与主键为什么以 IP 为主、MAC 为辅设备表的主键应该选 IP 而不是 MAC。原因是冲突检测的命题天然是“这个 IP 现在归谁”MAC 只是证据。反过来以 MAC 为主键一台设备配了多个 IP 时会被拆成多条记录冲突判定也就失去了基准。// 单个 MAC 对某个 IP 的声明证据 public sealed class MacEvidence { public DateTime FirstSeen { get; init; } public DateTime LastSeen { get; set; } public int HitCount { get; set; } public ushort LastOp { get; set; } // 1请求 2应答区分主动询问与主动声明 } public sealed class IpRecord { public IPAddress Ip { get; } public object Gate { get; } new(); // 每个 IP 一把锁避免全局锁成为瓶颈 public DictionaryPhysicalAddress, MacEvidence Macs { get; } new(); public ConflictKind Verdict { get; set; } ConflictKind.Normal; public IpRecord(IPAddress ip) Ip ip; }LastOp这个字段看着不起眼实际决定了冲突的定性。如果某个 MAC 只在LastOp 1请求里出现说明它只是“问过这个 IP 是谁”并不等于它自称拥有这个 IP——不过在多数实现里问之前它自己往往已经配上了这个地址。真正强证据是LastOp 2应答或免费 ARP因为这两种报文里发送方 IP 必须是发送方自认为拥有的地址。按 IP 分锁是为了并发。聚合线程通常是单线程消费但如果你把解析拆成多个 worker用全局锁会让高流量网段的吞吐直接掉一个数量级。4.2 冲突判定从观察到定性的状态流转public void Observe(ushort op, PhysicalAddress smac, IPAddress sip, PhysicalAddress tmac, IPAddress tip, DateTime ts) { var rec _byIp.GetOrAdd(sip, ip new IpRecord(ip)); lock (rec.Gate) { if (!rec.Macs.TryGetValue(smac, out var ev)) rec.Macs[smac] ev new MacEvidence { FirstSeen ts }; ev.LastSeen ts; ev.HitCount; ev.LastOp op; // 只有落在时间窗口内的 MAC 才算当前有效声明 var alive rec.Macs.Count(kv ts - kv.Value.LastSeen _window); rec.Verdict alive 1 ? ConflictKind.IpConflict : ConflictKind.Normal; } // 免费 ARP发送方 IP 目标 IP通常是上线通告或地址占用检测 if (sip.Equals(tip)) _gratuitous.Record(smac, sip, ts); // 有人在问谁是本机 IP而提问者不是本机 —— 本机地址被占用 if (tip.Equals(_selfIp) !smac.Equals(_selfMac)) _alerts.Push(new Alert(AlertLevel.High, sip, smac, 本机 IP 被他人声明)); }这段逻辑里有三个判断需要说清。第一alive用的是ts - LastSeen _window而不是“历史上出现过几个 MAC”这样 DHCP 地址回收再分配不会误报。第二sip.Equals(tip)这个条件能同时命中两种报文免费 ARP 请求操作码 1和免费 ARP 应答操作码 2前者用于“我上线了”后者用于“我确认这个地址是我的”语义不同但都是强声明。第三本机 IP 被占用这条告警是唯一可以做到“零延迟”的因为它在报文到达的瞬间就能判定不需要等时间窗口。4.3 老化策略与导出快照窗口长度不能随意设定。经验值取 DHCP 租约时间的一半租约 8 小时就取 4 小时租约 2 小时就取 1 小时。窗口太短设备休眠再唤醒会被误判为“MAC 变更”窗口太长地址真正被重新分配后旧 MAC 迟迟不老化冲突表里会长期保留已经解决的记录。public IReadOnlyListDeviceRow Snapshot(DateTime now) { var rows new ListDeviceRow(); foreach (var rec in _byIp.Values) { ListKeyValuePairPhysicalAddress, MacEvidence alive; lock (rec.Gate) { alive rec.Macs .Where(kv now - kv.Value.LastSeen _window) .OrderByDescending(kv kv.Value.LastSeen) .ToList(); } if (alive.Count 0) continue; // 全部老化视为下线 rows.Add(new DeviceRow( Ip: rec.Ip, PrimaryMac: alive[0].Key, // 最近一次声明的 MAC 作为当前归属 MacCount: alive.Count, FirstSeen: alive.Min(kv kv.Value.FirstSeen), LastSeen: alive.Max(kv kv.Value.LastSeen), Verdict: alive.Count 1 ? ConflictKind.IpConflict : ConflictKind.Normal)); } return rows.WithMacMultiIpMarked(_byMac); // 单独标记一个 MAC 多个 IP }PrimaryMac取最近一次声明的 MAC这个选择在冲突场景下是有意义的谁最后广播谁大概率是刚刚配好地址的那台往往也是“后来的那台”。把MacCount 1直接映射成冲突同时用_byMac反向索引标记多 IP 情况两张表就能互相印证如果某个 IP 冲突里的两个 MAC 同时都在“多 IP”列表里那大概率是虚拟化环境不是真冲突。5. 避坑与排查ARP 抓包在真实网络里的五个失败场景5.1 设备列表只有请求方没有应答方现象抓了几分钟_byIp里有记录的设备绝大多数只在LastOp 1出现过冲突判定几乎从不触发。原因交换机按 MAC 地址表精确转发单播帧ARP 应答是单播抓包机不在通信路径上就收不到。混杂模式在交换机环境里不能突破这个限制。解决把抓包点换到能看见全量流量的位置比如网关所在端口、交换机镜像口或者干脆接一个老式集线器做临时旁路。如果拿不到镜像权限就补一条主动探测定期向网段内所有地址发 ARP 请求用应答补齐列表代价是会引入额外流量也可能触发某些终端的安全告警。5.2 混杂模式没生效收到的帧数和预期严重不符现象明明是有几百台设备的网段OnPacketArrival一分钟只回调几十次。原因底层抓包驱动没装好或者没勾选支持混杂模式的选项Open返回成功但模式被静默降级另一种情况是抓包程序跑在虚拟机的虚拟网卡上宿主机那一层已经把非本机流量过滤掉了。解决抓包前先打印device.MacAddress和device.Flags确认设备可用用pcap_stats里的ps_recv和ps_drop对比如果ps_recv本身就极小说明根本没进混杂模式换物理网卡、重装驱动后再试。5.3 大量误报DHCP 续租被当成 IP 冲突现象冲突列表里同一对 IP/MAC 反复出现又消失时间点集中在整点或租约到期的时刻。原因设备续租或重新申请地址时可能短暂出现两个 MAC 声明同一 IP 的中间态如果时间窗口设得很短比如 60 秒这个中间态会被直接判成冲突。解决把窗口放宽到租约时间的一半以上并且对同一 IP 的冲突记录做“连续命中”要求——只有在连续 N 个采样周期内都能看到双 MAC才升级为确认冲突。这一步能把误报压掉一个数量级。5.4 一个 MAC 对应一堆 IP被误判成冲突现象冲突表里出现同一台设备占用十几个 IP 的记录。原因路由器子接口、虚拟化网桥、代理 ARP、双栈配置都会造成这个现象它们不是冲突是正常的多地址行为。解决把“单 MAC 多 IP”和“单 IP 多 MAC”拆成两个独立的判定维度前者只做标记不做告警再叠加一条规则——如果某个 MAC 同时是网关地址的持有者直接把它列入白名单。5.5 时间戳用 DateTime.Now窗口判定整体漂移现象抓包高峰期冲突判定结果和实际发生时间对不上回放同一份数据也无法复现。原因回调入队之后才打时间戳队列积压时延迟可达数秒用本地时间还会受系统时钟调整影响。解决一律用e.GetPacket().Timeval.Date它在帧被内核接收时就已确定。回放 pcap 文件时也用它同一份数据在不同机器上能跑出完全一致的结论。6. 设计取舍被动监听拿不到全貌主动探测又要付出代价把 ARP 抓包做成设备发现和冲突检测最后一定会遇到一个问题到底该信任观测结果到什么程度。被动监听的优点是零侵入、不改动网络、不会引入任何额外流量缺点是它的可见范围完全由网络拓扑决定——在交换机环境里你看得到广播看不到单播所以设备列表天然是“偏向爱说话的那部分设备”。主动探测补上了静默设备代价是每个探测包都可能被终端的安全软件记录成“异常扫描”在有些环境里这本身就是需要报备的行为。我的取舍是被动采集做基础事实主动探测只做补漏且严格限速。具体做法是把设备记录分成“确认”和“推测”两档被动观测到的标为确认只能通过主动探测发现的标为推测导出时带上一列来源。这样一份设备列表在给别人看的时候对方能知道哪条是硬证据、哪条是推断比混在一起报一个“共 N 台设备”有用得多。第二个取舍在冲突判定上。严格判定要求双 MAC 都出现过应答或免费 ARP误报极低但会漏掉那些只发请求不发应答的场景宽松判定只要窗口内出现双 MAC 就算覆盖全但在虚拟化和 DHCP 频繁的环境里几乎不可用。我倾向于严格判定加一个“疑似”中间态疑似记录不告警但保留证据链等人去核对。这套东西的价值不在于自动下结论而在于把“黑匣子”变成“可以翻的记录”——出了问题时能回答“这个 IP 在什么时间被哪些 MAC 声明过”而不是只能说“好像是 IP 冲突”。最后一个取舍是老化窗口。窗口越长历史证据越完整误判越少但地址真的被重新分配之后旧记录残留会让冲突表越滚越大看的人很快就麻木了。我最后选的是双层窗口判定窗口取租约一半展示窗口取判定窗口的四倍老记录降级成“历史”而不是删除。这样既不丢证据也不会让当前告警被历史数据积压得难以处理。从那以后我每次做这类采集工具都会强制先在目标网段跑一遍“只统计不判定”的观察模式把ps_recv、ps_drop、请求与应答的比例、单 MAC 多 IP 的分布先看清楚再决定窗口和阈值。这些参数没有通用解只能拿真实流量去试。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →