news 2026/10/29 11:15:22

C# SharpPcap ARP抓包与局域网IP冲突排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# SharpPcap ARP抓包与局域网IP冲突排查

简介:这份面向局域网运维与C#网络编程学习者的代码包,围绕ARP数据包抓取实现网络设备发现与IP冲突设备识别,适合需要排查地址冲突、了解底层协议监听的中级开发者参考。项目以SharpPcap与PacketDotNet为核心,将抓包逻辑封装为可复用类,并附带调用示例;压缩包内附WinPcap_4_1_3安装程序及依赖DLL,开发环境为VS2015与.NET 4.5.2,可较快跑通测试。整包共36个文件,约1002KB,以11个cs源码为主体,辅以dll依赖、exe程序、config配置和resx资源等,结构紧凑。已有560人学习,读者可从中获得ARP监听、设备列表采集、冲突判定与依赖配置的完整实现思路,并可直接替换到自己的监测工具中。

1. ARP 抓包为什么能在局域网里“看见”所有设备:从一次地址冲突说起

局域网里的 IP 冲突现场往往很安静:某台工控机下午三点突然掉线,运维到现场重启一遍交换机,好了两天又犯,最后只能靠拔网线挨个试。想定位是哪一个 MAC 占了别人的地址,用 ping 加 arp -a 基本抓瞎,因为冲突双方的 ARP 表会不停互相覆盖。用 C# 配合 SharpPcap 在镜像口或网关侧被动抓 ARP,几十行代码就能把“谁在用哪个 IP”实时摊开,冲突设备列表也随之出来,而且不向网络里发一个探测包。下面这份实践从抓包骨架、报文解析、冲突判定一路写到避坑和离线回放,适合需要在局域网里做设备清点、地址冲突排查的 C# 开发者照着复现。

2. 搭起 SharpPcap 抓包骨架:网卡选型、过滤器与回调线程

2.1 先想清楚你要听的是哪个广播域

ARP 是二层广播,不跨三层。这句话决定了整个方案的边界:你把程序跑在办公区的一台 PC 上,只能听见这台 PC 所在 VLAN 里的 ARP,跨 VLAN 的其他网段一个包都收不到。常见做法是把抓包点放在三个位置之一——核心交换机的镜像口、网关服务器所在 VLAN、或者目标网段里一台 7×24 常开的机器。如果网络是三层到接入的架构,每个 VLAN 都要有自己的探针,或者干脆在核心上做 RSPAN 把多个 VLAN 的流量引到一处。

为什么不直接用 ICMP 扫描或 SNMP 轮询?因为那两种方式都会主动发包,在工控、医疗、安防这类环境里,扫描流量本身就可能触发报警或被防火墙拦掉;而被动抓包是“听”,不产生任何额外流量。更关键的是,开了主机防火墙的设备不响应 ping,但只要它还要通信,就一定会发 ARP,被动方式反而看得更全。这也是我在这类现场优先选抓包而不是扫描的原因。

需要提前接受的一个物理限制:交换机的 MAC 地址表老化时间通常是 300 秒,ARP 缓存也差不多这个量级。所以你的程序必须常驻监听,靠一段时间累积才能拼出完整的设备清单,跑一次就退出的脚本只能看到当下正在通信的那部分设备。

2.2 装依赖与最小可跑骨架

SharpPcap 本身只是托管层的封装,底下必须有原生的抓包驱动。Windows 上装 Npcap,安装时勾选“WinPcap API-compatible Mode”,否则老版本的 SharpPcap 调用会报找不到 wpcap.dll;Linux 上装 libpcap 开发包,运行时要 root 或者给可执行文件加setcap cap_net_raw,cap_net_admin=eip。这一步没做对,后面所有代码都是白写,报错信息还特别含糊。

# Linux 侧的准备 sudo apt-get install -y libpcap-dev # 项目里引入 SharpPcap 和报文解析库 dotnet add package SharpPcap dotnet add package PacketDotNet
using SharpPcap; using SharpPcap.LibPcap; using PacketDotNet; var devices = CaptureDeviceList.Instance; if (devices.Count == 0) { Console.WriteLine("未发现任何抓包设备,先确认 Npcap / libpcap 已安装"); return; } // 打印全部设备,第一次运行时先看这一屏再决定匹配规则 foreach (var d in devices) Console.WriteLine($"[{d.Name}] {d.Description}"); // 按描述关键字挑网卡;不要用索引,机器重启后网卡顺序会变 var device = devices.FirstOrDefault(d => d.Description != null && d.Description.Contains("Realtek", StringComparison.OrdinalIgnoreCase)); if (device is null) { Console.WriteLine("没有匹配到目标网卡,检查关键字"); return; } // 混杂模式 + 1000ms 读超时;超时值决定回调的响应粒度 device.Open(DeviceModes.Promiscuous, 1000); device.Filter = "arp"; // 内核层过滤,只把 ARP 送上来的包,省 CPU device.OnPacketArrival += (_, e) => { var capture = e.GetPacket(); // SharpPcap 6.x var raw = Packet.ParsePacket(capture.LinkLayerType, capture.Data.ToArray()); var arp = raw.Extract<ArpPacket>(); if (arp is null) return; Console.WriteLine($"{arp.Operation} {arp.SenderProtocolAddress} -> {arp.SenderHardwareAddress}"); }; device.StartCapture(); Console.WriteLine("开始监听,回车停止"); Console.ReadLine(); device.StopCapture(); device.Close();

这段骨架里有三个参数值得单独说。DeviceModes.Promiscuous打开混杂模式,网卡会接收所有经过它的帧,而不只是发给自己的;在交换式网络里,这只能让你看到本端口能收到的广播和镜像流量,不要指望它把整个交换机都“看穿”。第二个参数1000是读超时毫秒数,它决定了驱动缓冲区里的包最多攒多久才交给回调,设得太大会让实时性变差,设成 0 在某些驱动上会变成忙等把 CPU 打满。过滤器"arp"用的是 BPF 语法,交给内核执行,比在回调里判断 EtherType 高效得多,如果后面要同时看 ICMP 做存活探测,可以写成"arp or icmp"。

OnPacketArrival是在抓包线程上同步触发的,回调里做的事情越多,驱动缓冲区堆积越快,丢包就越严重。这一点在后面第 5 章还会展开,先记住一个原则:回调里只做解析和入队,判定、写库、发告警都丢到另一个线程去。

2.3 回调线程与缓冲区的几个约束

SharpPcap 的抓包线程是独立的后台线程,回调串行执行。这意味着你在回调里Thread.Sleep(100)一次,整个抓包就停了 100 毫秒,这期间驱动环形缓冲区满了就开始丢包。局域网里 ARP 的常态是几十个包每秒,看着不多,但设备批量上电、DHCP 续租、网络抖动时会出现瞬时几百包每秒的尖峰,缓冲区一满,恰好把冲突发生的那几秒丢掉,事后复盘什么都没有。

稳妥的结构是回调里只把 (源 IP, 源 MAC, 目标 IP, 操作码, 时间戳) 这五个字段塞进Channel<T>,另一个消费者线程从 channel 里取出来做去重和判定。Channel 用有界队列,容量给个 8192 就够,满了丢最老的而不是阻塞生产者,因为旧数据对冲突判定价值低于新数据。

还有一点容易忽略:device.Close()之前必须StopCapture(),顺序反了在部分驱动上会抛异常或者卡死。程序退出时用Console.CancelKeyPress挂一个清理逻辑,别指望进程被杀时能优雅收尾,Windows 服务方式部署尤其要注意。

3. 拆 ARP 报文:以太网帧头之后的 28 个字节怎么读

3.1 以太网帧头 14 字节:EtherType 0x0806 是入口

一个 ARP 包在链路层的结构非常固定:前 6 字节是目的 MAC,接着 6 字节是源 MAC,然后 2 字节的 EtherType,值为0x0806就表示后面跟的是 ARP。这 14 字节之后,才是 ARP 报文本体,长度 28 字节(在以太网 + IPv4 的组合下)。整个帧 42 字节,短于这个长度的直接判定为畸形包丢掉。

理解这个偏移量很重要,因为有些现场拿到的不是标准以太网帧——比如带 802.1Q VLAN 标签的帧,EtherType 位置会往后挪 4 字节,变成0x8100,真正的类型字段在偏移 16 处。如果你的抓包点接在 trunk 口上,一定要处理这个情况,否则过滤器"arp"虽然能匹配上(BPF 对 VLAN 有特殊处理),但你自己按固定偏移解析就会读出乱码。稳妥写法是先判断frame[12..13]是不是0x8100,是的话偏移整体加 4。

下面这段是纯手工解析,不依赖任何第三方库,用来对照理解每个字节的含义:

static void ParseArpByHand(byte[] frame) { if (frame.Length < 42) return; int offset = 0; ushort etherType = (ushort)((frame[12] << 8) | frame[13]); // 802.1Q VLAN 标签:类型挪到 16,ARP 头跟着往后移 4 字节 if (etherType == 0x8100) { offset = 4; etherType = (ushort)((frame[16] << 8) | frame[17]); } if (etherType != 0x0806) return; int p = 14 + offset; // ARP 报文起点 ushort hwType = (ushort)((frame[p] << 8) | frame[p + 1]); // 1 = 以太网 ushort protoType = (ushort)((frame[p + 2] << 8) | frame[p + 3]); // 0x0800 = IPv4 byte hwLen = frame[p + 4]; // 6 byte protoLen = frame[p + 5]; // 4 ushort opCode = (ushort)((frame[p + 6] << 8) | frame[p + 7]); // 1 请求 / 2 应答 string senderMac = string.Join(":", frame.Skip(p + 8).Take(6).Select(b => b.ToString("X2"))); string senderIp = $"{frame[p + 14]}.{frame[p + 15]}.{frame[p + 16]}.{frame[p + 17]}"; string targetMac = string.Join(":", frame.Skip(p + 18).Take(6).Select(b => b.ToString("X2"))); string targetIp = $"{frame[p + 22]}.{frame[p + 23]}.{frame[p + 24]}.{frame[p + 25]}"; Console.WriteLine($"op={opCode} {senderIp}/{senderMac} -> {targetIp}/{targetMac}"); }

对照着看偏移:p+0到p+1是硬件类型,p+2到p+3是协议类型,p+4、p+5是两个长度字段,p+6到p+7是操作码,之后依次是发送方 MAC(6 字节)、发送方 IP(4 字节)、目标 MAC(6 字节)、目标 IP(4 字节)。IP 直接按字节拼字符串就行,不需要处理字节序,因为 IPv4 地址在报文里就是网络序的高位在前,逐字节读出来正好是点分十进制的顺序。MAC 同理。

真正会踩字节序坑的是操作码和类型字段。(frame[p] << 8) | frame[p+1]这种写法是在做“大端转本机整数”,x86 是小端机器,如果你图省事用BitConverter.ToUInt16(frame, p),读出来的 0x0806 会变成 0x0608,判断永远不成立。这个坑我在第一次写的时候踩了整整一个下午。

3.2 ARP 报文 28 字节的逐字段读法

把 28 字节拆开看,每个字段的存在意义都对应一个判定逻辑:

偏移(相对 ARP 起点)长度字段取值与用途
0–12硬件类型以太网固定为 1,其他值直接跳过
2–32协议类型IPv4 为 0x0800,IPv6 用 NDP 不走 ARP
41硬件地址长度6
51协议地址长度4
6–72操作码1=请求,2=应答,其他值少见
8–136发送方 MAC设备清单的主键之一
14–174发送方 IP设备清单的主键之一
18–236目标 MAC请求包里通常是全 0
24–274目标 IP请求包里是被询问的地址

操作码这一列值得多说两句。日常抓到的绝大多数是 1(请求)和 2(应答),但有些系统会发操作码为 3、4 的 RARP,还有一些老设备会发探测性质的 ARP。做设备清点时我只认 1 和 2,其他一律忽略,避免把异常流量当成设备。

另一个常被忽略的点:ARP 请求包的目标 MAC 字段是全零,这不是错误,而是“我还不知道对方 MAC”的正常表达。很多人写解析代码时看到全零就以为是坏包丢掉,结果把所有请求都滤没了,只剩下应答,设备清单自然缺一半。正确的做法是:请求包用发送方字段做记录,应答包发送方和目标方都记录。

3.3 用 PacketDotNet 少写一半代码

手工解析适合理解原理和排查诡异问题,工程里我一般还是用 PacketDotNet,它把链路层、网络层的偏移都处理好了,包括 VLAN 标签的自动剥离。

using PacketDotNet; static (string Op, string SenderIp, string SenderMac, string TargetIp)? ReadArp(byte[] data, LinkLayers link) { var packet = Packet.ParsePacket(link, data); var arp = packet.Extract<ArpPacket>(); if (arp is null) return null; // PhysicalAddress.ToString() 输出的是 XX-XX-XX 形式,统一成冒号分隔便于比对 string mac = string.Join(":", arp.SenderHardwareAddress .GetAddressBytes() .Select(b => b.ToString("X2"))); return (arp.Operation.ToString(), arp.SenderProtocolAddress.ToString(), mac, arp.TargetProtocolAddress.ToString()); }

Extract<ArpPacket>()会在整条协议栈里递归查找,所以即使帧里套了 VLAN 标签、甚至套了 PPPoE,它也能把 ARP 捞出来。返回null表示这个包里没有 ARP,直接跳过即可。PhysicalAddress.ToString()在不同平台上的分隔符不一样,Windows 上是连字符,Linux 上可能是冒号,做集合去重时一定要自己统一格式,否则同一个 MAC 会被当成两个设备,后面冲突判定全是误报。

还有一个细节:arp.Operation是枚举,转成字符串比较慢,热点路径上建议直接和ArpOperation.Request、ArpOperation.Response比较。每秒几百个包看不出来,但如果你把程序部署在一台低配工控机上长期跑,这种小开销累积起来也是负担。

4. IP 冲突判定:同一地址出现多个 MAC 之后怎么下结论

4.1 判定模型:IP 到 MAC 的集合加时间窗口

冲突的数学定义很朴素:同一个 IP 在同一个时间窗口内,被两个及以上不同的 MAC 声明过。落到数据结构上就是Dictionary<string, Dictionary<string, DateTime>>,外层键是 IP,内层键是 MAC,值是最后一次见到的时间。

这里必须带时间窗口,原因是 ARP 缓存会老化,一个设备换 IP、下线、重新分配,历史 MAC 会永远留在集合里。如果不做老化,跑一天之后几乎每个 IP 都会“冲突”,列表变成噪音。窗口取多大?我一般用 5 分钟,略大于常见交换机的 MAC 老化时间,既能覆盖 DHCP 租约切换的过渡期,又不至于把早上和下午的两台设备混为一谈。

public sealed class ArpTracker { private readonly object _gate = new(); private readonly Dictionary<string, Dictionary<string, DateTime>> _ipToMacs = new(); private readonly TimeSpan _window; public ArpTracker(TimeSpan window) => _window = window; // 返回该 IP 当前窗口内的全部 MAC;数量 >= 2 即判定为冲突 public IReadOnlyList<string> Observe(string ip, string mac, DateTime now) { lock (_gate) { if (!_ipToMacs.TryGetValue(ip, out var macs)) _ipToMacs[ip] = macs = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase); macs[mac] = now; // 老化:窗口外的条目必须清掉,否则历史设备会一直挂在集合里造成假冲突 var expired = macs.Where(kv => now - kv.Value > _window) .Select(kv => kv.Key) .ToList(); foreach (var key in expired) macs.Remove(key); return macs.Keys.ToList(); } } // 定期清理完全空掉的 IP 条目,长时间运行必须做 public void Compact(DateTime now) { lock (_gate) { var empty = _ipToMacs.Where(kv => kv.Value.Count == 0 || kv.Value.Values.All(t => now - t > _window)) .Select(kv => kv.Key) .ToList(); foreach (var key in empty) _ipToMacs.Remove(key); } } }

lock是必要的,因为抓包回调和定期清理线程会同时访问这个字典。有人会想着换成ConcurrentDictionary图省事,但这里的操作是“读-改-写”复合操作,ConcurrentDictionary的原子方法覆盖不了,还是得自己加锁。锁的粒度控制在单次 Observe 内部,微秒级,不会成为瓶颈。

4.2 免费 ARP 是最有价值的信号

普通的 ARP 请求是“我要找 X,谁是 X”,只有一问一答。而免费 ARP(Gratuitous ARP)是设备主动宣告“我是 X,我的 MAC 是 Y”,发送方 IP 和目标 IP 相同,目标 MAC 全零。这个包在设备刚上电、网卡重启、IP 地址变更时必发,是设备清单里最干净的一条记录——它不依赖任何其他设备的响应,也不受交换机 MAC 表老化影响。

判定免费 ARP 的条件:操作码为 1 且发送方 IP 等于目标 IP,或者操作码为 2 且发送方 IP 等于目标 IP(有些系统用应答形式发公告)。两种都认,别只认一种。

免费 ARP 在冲突判定里的价值还体现在时序上。正常情况下一台设备上电发一次公告,之后就是常规请求应答。如果同一个 IP 在几秒内连续出现两条来自不同 MAC 的免费 ARP,那基本可以确定是两台设备配了同一个静态地址,或者 DHCP 池配错了范围。这种“短时间内双公告”的模式,比单纯统计 MAC 数量更可靠,我在告警规则里把它单独拎出来做高优先级。

顺带说一个反向用法:如果一个 MAC 在窗口内声明了多个不同 IP,那不是冲突,是 ARP 欺骗或者代理 ARP 的典型特征。这两个方向的统计都要做,前者生成冲突列表,后者生成可疑列表,分开呈现,别混在一张表里让人误判。

4.3 把结果落成设备清单和冲突清单

原始数据攒齐之后,输出要分成两张表。设备清单是IP | MAC | 首次出现 | 最后出现 | 来源(免费ARP/请求/应答),冲突清单是IP | MAC集合 | 首次冲突时间 | 冲突持续秒数。分开的原因是使用场景不同:设备清单给资产盘点用,冲突清单给排障用,混在一起会让运维在一堆正常设备里找那两行异常。

public static void ExportCsv( IEnumerable<(string Ip, string Mac, DateTime First, DateTime Last)> devices, IEnumerable<(string Ip, string Macs, DateTime First, int Seconds)> conflicts, string dir) { Directory.CreateDirectory(dir); var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); File.WriteAllLines(Path.Combine(dir, $"devices_{stamp}.csv"), new[] { "ip,mac,first_seen,last_seen" } .Concat(devices.Select(d => $"{d.Ip},{d.Mac},{d.First:O},{d.Last:O}"))); File.WriteAllLines(Path.Combine(dir, $"conflicts_{stamp}.csv"), new[] { "ip,mac_list,first_conflict,duration_sec" } .Concat(conflicts.Select(c => $"{c.Ip},\"{c.Macs}\",{c.First:O},{c.Seconds}"))); }

MAC 集合那一列里含逗号,CSV 里必须用双引号包起来,否则 Excel 打开会错列。时间用O格式(ISO 8601 带时区),跨机器对比时不会因为时区设置不同而错位。文件名带时间戳而不是覆盖,是为了保留历史,冲突这种事经常需要回溯“上周三下午那次到底是谁”。

还有一个实用技巧:把设备清单里的 MAC 前缀(前 3 字节)提取出来做厂商粗分类,能快速看出冲突双方是不是同一批采购的设备。如果两个冲突 MAC 的 OUI 完全一样,八成是同型号设备配置时复制了模板,这种问题在现场极其常见。

5. 避坑与排查:抓不到、误报、跑久了崩的五种现场

5.1 现象:程序跑起来一个包都收不到

原因:三种情况最常见。一是网卡选错了,机器上有虚拟网卡、蓝牙网卡、Hyper-V 虚拟交换机,列表按序号排,选中的是个永远没流量的;二是 Npcap 装的时候没勾 WinPcap 兼容模式,或者 Linux 上没给 raw socket 权限,Open直接抛异常被 catch 吞掉了;三是过滤器写错,比如写成了"arp "带空格,或者大小写混用某些实现不认。

解决:第一步先不加过滤器、不解析,纯打印e.GetPacket().Data.Length,能看到数字就说明链路通了,问题在过滤或解析。第二步检查device.Name是不是\Device\NPF_{GUID}形式的真实网卡,虚拟网卡的名字里通常带 Virtual、Loopback 字样。第三步在 Linux 上临时用sudo跑一次确认是权限问题,再决定用 setcap 还是 systemd 的AmbientCapabilities。过滤器统一用小写、不带多余空格,写完先用tcpdump -i eth0 arp -c 5在同一个位置验证有没有包。

5.2 现象:只抓到本机自己的 ARP,别人的一个没有

原因:抓包点在拓扑上位置不对。接在一台普通接入交换机下联口,那个口只能收到广播和发给本机的单播,而 ARP 请求虽然目的 MAC 是全 F 的广播,但交换机会把它泛洪到同一 VLAN 的所有端口——理论上你应该能收到。收不到通常是因为交换机开了端口隔离(PVLAN),或者你所在端口和网关不在同一个 VLAN,或者无线网络侧做了客户端隔离。

解决:把程序挪到网关所在的那台机器上跑,或者让网络管理员在核心交换机上配一个镜像口,把目标 VLAN 的流量镜像过来。确认方法是同时开tcpdump -i any arp和你的程序,如果 tcpdump 有包而程序没有,是程序问题;两个都没有,是拓扑问题,别再折腾代码了。镜像口方向也要确认,只镜像 ingress 会漏掉交换机自己发出的 ARP。

5.3 现象:IP 地址显示成 192.168.1.10 变成了别的样子,或者操作码判断永远不成立

原因:字节序。报文里多字节字段是大端(网络序),x86 机器是小端,直接BitConverter.ToUInt16会翻转高低字节。另一种表现是 IP 用BitConverter.ToInt32转成了一个大整数,再拼字符串就成了乱码。

解决:要么手动移位拼接(hi << 8) | lo,要么用BinaryPrimitives.ReadUInt16BigEndian(span),后者更现代也更快。IP 地址一律逐字节拼字符串,别走整数转换。如果坚持用 BitConverter,记得转换后调用Array.Reverse,但这个写法多一次分配,热点路径上不划算。修完之后拿 Wireshark 对同一个包,字段值必须逐一吻合,这是我每次改解析代码后的固定动作。

5.4 现象:冲突列表里全是网关地址和几台核心服务器

原因:把正常的网络架构当成了冲突。三层交换机做网关时,多台设备可能通过 VRRP 共享一个虚拟 IP,虚拟 MAC 是固定的00:00:5E:00:01:XX;Windows NLB 集群会用一个共享 MAC 承载多个 IP;某些防火墙开代理 ARP,会替下游设备应答。这些场景下,同一个 IP 被多个 MAC 声明是设计使然,不是故障。

解决:维护一个白名单,把网关 IP、VRRP 虚拟 MAC 段(00:00:5E:00:01:00到00:00:5E:00:01:FF)、HSRP 段(00:00:0C:07:AC:00到00:00:0C:07:AC:FF)预先排除。白名单要能配置,不要硬编码在代码里,不同现场的网络架构差异很大。另外把“一个 MAC 多个 IP”和“一个 IP 多个 MAC”分开统计,前者归入可疑列表,后者才是冲突列表,这张表分开之后误报会立刻降下来。

5.5 现象:程序跑几个小时之后内存涨到几个 G,或者回调越来越慢直到丢包

原因:两个独立问题叠在一起。内存涨是因为把每个包对象都塞进了某个集合,或者事件订阅没解绑导致对象无法回收;变慢是因为回调里做了同步 IO、字符串拼接、或者对一个大字典做线性扫描。SharpPcap 的PacketCapture对象持有的缓冲区不是无限供应的,回调里保存引用就等于拖住了整个缓冲区。

解决:回调里只提取需要的五个字段,立刻转成值类型或短字符串,包对象本身不保留。数据结构用有界 Channel,满了丢最老的。定期清理任务单独开一个线程,每分钟对 Tracker 调一次Compact,把空条目删掉。上线前用dotnet-counters monitor挂上去看 GC 和堆大小,跑满 24 小时曲线平稳才算通过。我现在的习惯是任何常驻抓包程序都必须先压测一天,这个后悔药没处买。

6. 把抓包做成可回放的巡检工具:离线 pcap 验证与定时任务

在线抓包有个天然缺陷:出问题的那一刻你不在现场,等赶到时冲突已经结束。所以我在任何现场部署之前,都会要求对方先抓一段 pcap 存下来,用离线回放把判定逻辑跑一遍,确认输出的冲突列表和人工排查的结论一致,再上线。SharpPcap 的CaptureFileReaderDevice可以直接读 pcap 文件,事件模型和在线抓包完全一致,代码几乎不用改。

using SharpPcap; using SharpPcap.LibPcap; using PacketDotNet; var reader = new CaptureFileReaderDevice("/data/arp_20250312.pcap"); reader.Open(); var tracker = new ArpTracker(TimeSpan.FromMinutes(5)); var conflicts = new List<(string Ip, string Macs, DateTime First, int Seconds)>(); reader.OnPacketArrival += (_, e) => { var cap = e.GetPacket(); var arp = Packet.ParsePacket(cap.LinkLayerType, cap.Data.ToArray()).Extract<ArpPacket>(); if (arp is null) return; // 回放时用包自带的时间戳,而不是 DateTime.Now,否则窗口判定全乱 var ts = cap.Timeval.Date; var macs = tracker.Observe(arp.SenderProtocolAddress.ToString(), FormatMac(arp.SenderHardwareAddress), ts); if (macs.Count >= 2) conflicts.Add((arp.SenderProtocolAddress.ToString(), string.Join(" | ", macs), ts, 0)); }; // 循环读直到文件结束,GetNextPacket 返回负数表示 EOF while (reader.GetNextPacket() >= 0) { } reader.Close(); Console.WriteLine($"共发现 {conflicts.Count} 条冲突记录"); foreach (var c in conflicts.DistinctBy(c => c.Ip)) Console.WriteLine($"{c.Ip} 被 {c.Macs} 同时占用");

这里最容易翻车的是时间戳:回放时如果用了DateTime.Now,整个 pcap 会在几百毫秒内被读完,所有事件挤进同一个瞬间,5 分钟窗口失效,冲突判定必然出错。必须用cap.Timeval.Date,这个字段就是包被捕获时的真实时间。用对了之后,一段 8 小时的抓包文件可以完整复现当时的冲突演化过程,比在线上盯着看有效得多。

验证通过之后,把它包成常驻服务:每 60 秒输出一次增量设备清单,冲突出现时立刻写一条带时间戳的记录并触发告警(写文件、发 HTTP 请求都行),每小时把明细归档成一个 CSV。部署位置选在网关机器上,用 systemd 或 Windows 服务托管,日志按天切分。长期跑下来你会发现,最有价值的不是实时告警,而是那份按天归档的设备清单——设备什么时候上线的、IP 什么时候变的、哪个 OUI 的设备在某天集中出现,全在里面。

一个具体的小技巧:把每天清单里的 MAC 集合和前一天做差集,新增的设备单独列出来。在管控严格的网络里,这个差集往往比冲突告警更早发现问题。我第一版没做差集,结果一台未经登记的笔记本在某网段待了三天才被发现,从那以后我每次部署这个工具都强制把日环比加进去,宁可多写二十行代码。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 11:13:24

成本打下来了!史上最便宜REA搭建方案(Deepseek V4.1+LeanREA)

【技术干货】轻量级AI代码审计Agent&#xff1a;技能文件、证据链与低成本大模型实战 **摘要&#xff1a;**本文拆解轻量级AI代码审计Agent的实现方法&#xff0c;通过技能文件约束工具选择&#xff0c;以证据分级降低模型幻觉&#xff0c;并使用Python接入大模型API&#xff0…

作者头像 李华
网站建设 2026/10/29 11:10:08

大模型数据模型选取

问题输入├─ 路径 A&#xff1a;关键词匹配└─ 路径 B&#xff1a;字段 / 知识 / 样例召回 LLM 选表↓ 两条路径结果取并集↓ 是否存在合适的表&#xff1f;├─ 是 → 输出表选择列表└─ 否 → 使用配置中的默认表 → 输出表选择列表↓ 仅将选中表的结构传给 NL2SQL路径 A…

作者头像 李华
网站建设 2026/10/29 11:07:35

2026年共享网盘深度横评:6款产品实测,告诉你哪个共享最好用

文章目录一、共享≠同步&#xff1a;先理清被混淆的两个概念二、测评方法论&#xff1a;共享能力的5个评价维度三、6款产品共享能力逐一测评3.1 坚果云 —— 共享权限粒度行业最细&#x1f527; 共享能力技术要点✅ 优势⚠️ 局限&#x1f4cb; 使用建议3.2 百度网盘 —— 公开…

作者头像 李华