简介:这是一份面向网络管理初学者与运维人员的局域网扫描工具源码,基于 Visual C++ 编写,核心功能是快速发现同一网段内所有主机的 IP 与 MAC 地址,适用于网络设备盘点、IP 冲突排查、未授权设备识别以及 DHCP 环境下的地址核对等场景。资源包共 15 个文件,以 h 头文件与 cpp 源文件为主,另含 rc 资源脚本、dsw/dsp 工程文件、ico 图标及 clw 类向导文件,整体约 13KB,属于轻量级 VC 工程,可直接用 Visual Studio 打开编译调试。据描述,该工具约 30 秒即可完成一个网段 255 台主机的扫描,通过 ARP 或 ICMP 协议获取设备物理地址与逻辑地址的对应关系。目前已有 733 人学习下载,读者可借此理解局域网二层通信原理、MAC 地址在数据链路层的作用,并掌握基于 C++ 的网络扫描实现思路与工程组织方式。
1. ScanLan 扫局域网:为什么 ARP 表比 ping 扫段更靠谱
很多人第一次写局域网设备发现,第一反应是写个 for 循环把 192.168.1.1 到 192.168.1.254 全 ping 一遍,能通的就是活着的机器。这个思路在十台设备的小网络里能跑,一旦网段里塞了三四十台机器、还混着几台不响应 ICMP 的打印机和摄像头,结果就开始玄学了——明明在线的设备扫不出来,扫出来的 IP 又对不上 MAC。ScanLan 这类工具的核心不是「发多少包」,而是「问谁要答案」:它走的是 ARP(地址解析协议),直接向本网段广播 who-has 询问,谁在线谁就得回,回包里天然带着 IP 和 MAC 的对应关系。
这篇文章讲的就是怎么用 ARP 这套机制,把局域网里所有机子的 IP 和 MAC 地址一次性捞出来。适合两类人:一类是运维或安全岗,需要定期盘点内网资产、排查 IP 冲突和私接设备;另一类是嵌入式或工控方向的工程师,现场调试时经常要确认某台 PLC、HMI 的网口 MAC 到底是多少。读完你能拿到一套可复现的实现路径、几个必调参数,以及我在真实网络里踩过的坑。局域网设备发现这件事,工具只是壳,底层协议才是那个不会骗你的黑匣子。
2. ARP 扫描的底层逻辑与三种实现路线
2.1 为什么 ping 扫段会漏设备,ARP 不会
ping 走的是 ICMP,属于网络层。设备可以关掉 ICMP 响应,防火墙可以丢弃 ICMP 包,但 ARP 不行——ARP 是链路层协议,只要设备接在同一个二层网络里、网卡是活的,它就必须响应针对自己 IP 的 ARP 请求。这是协议栈的硬性要求,不是可选项。所以用 ARP 做发现,漏报率远低于 ping 扫段。
具体流程是这样的:主机 A 想知道 192.168.1.50 的 MAC,就往广播地址 FF:FF:FF:FF:FF:FF 发一个 ARP Request,内容大意是「谁的 IP 是 192.168.1.50,请告诉 192.168.1.1」。网段内所有设备都收到这个广播,只有 IP 匹配的那台会单播回一个 ARP Reply,里面带着自己的 MAC。ScanLan 做的事就是批量构造这些 Request,然后监听回复,把 IP-MAC 对收集起来。
这里有个关键点:ARP 只能发现同一广播域内的设备。跨网段、跨 VLAN 的设备,ARP 请求到不了,自然扫不到。这不是工具的问题,是协议边界。如果你的网络做了 VLAN 划分,每个 VLAN 都得单独扫一遍。
2.2 三种实现路线:raw socket、scapy、系统 ARP 表
实现 ARP 扫描有三条路,复杂度从高到低,可控性也是从高到低。
第一条是 raw socket,自己构造以太网帧和 ARP 报文。Python 里用socket.AF_PACKET(Linux)或 WinPcap/Npcap(Windows)。这条路最灵活,能控制发送速率、重试次数、超时时间,但代码量大,跨平台麻烦。
第二条是用 scapy 这类库。scapy 把 ARP 报文封装好了,几行代码就能发出去收回来,开发效率最高。缺点是依赖 scapy 安装,而且默认发送速率偏快,在老旧交换机上可能触发广播风暴保护。
第三条最省事:直接读系统 ARP 表。先 ping 扫一遍网段(或者用其他方式触发 ARP),然后读arp -a的输出。这条路不需要特殊权限,但依赖系统缓存,时效性差,而且 ping 不通的设备不会进 ARP 表。
我一般会选 scapy 路线做原型验证,确认逻辑没问题后再根据部署环境决定是否换成 raw socket。下面给出 scapy 的完整实现。
2.3 用 scapy 写一个最小可用的 ARP 扫描器
先装依赖:
pip install scapy然后是最核心的扫描脚本:
from scapy.all import ARP, Ether, srp import ipaddress def scan_lan(network="192.168.1.0/24", timeout=3, retry=2): """ ARP 扫描局域网,返回 IP-MAC 列表 network: 目标网段,CIDR 格式 timeout: 每个包的等待超时(秒) retry: 重试次数,应对丢包 """ # 构造 ARP 请求:问整个网段谁的 MAC 是多少 arp = ARP(pdst=network) # 以太网帧,目的地址是广播 ether = Ether(dst="ff:ff:ff:ff:ff:ff") # 组合成完整包 packet = ether / arp # srp 发送并接收二层包,verbose=0 关闭冗余输出 result = srp(packet, timeout=timeout, retry=retry, verbose=0)[0] devices = [] for sent, received in result: devices.append({ "ip": received.psrc, "mac": received.hwsrc }) return devices if __name__ == "__main__": devs = scan_lan("192.168.1.0/24") print(f"发现 {len(devs)} 台设备") for d in sorted(devs, key=lambda x: ipaddress.ip_address(x["ip"])): print(f"{d['ip']:16s} {d['mac']}")逻辑说明:ARP(pdst=network)里的pdst设成整个网段,scapy 会自动为网段内每个 IP 生成一个 ARP 请求。Ether(dst="ff:ff:ff:ff:ff:ff")确保帧以广播方式发出。srp是「send and receive at layer 2」的缩写,专门处理二层包,返回两个列表,第一个是「已发送且收到回复」的包对。
参数说明:timeout=3意味着每个请求等 3 秒,网段大时总耗时会线性增长,/24 网段大约 3 到 5 秒扫完。retry=2是重试次数,无线网络或负载高的交换机上建议设 2 到 3,有线环境设 1 就够。如果网段是 /16,254 个地址变成 65534 个,必须分批扫,否则内存和广播压力都扛不住。
2.4 扫描结果的去重与厂商识别
扫出来的原始结果可能有重复——同一台设备可能回多个包,或者你重试时它回了两次。去重很简单,用 MAC 做 key 就行。但更有价值的是识别厂商:MAC 地址的前三个字节是 OUI(组织唯一标识符),能查出网卡厂商。
def dedup_and_vendor(devices): seen = {} for d in devices: mac = d["mac"].upper() if mac not in seen: # OUI 是 MAC 前 3 字节,这里只做展示 oui = mac[:8] seen[mac] = {"ip": d["ip"], "mac": mac, "oui": oui} return list(seen.values())OUI 查厂商需要一张映射表,完整的 IEEE OUI 库有几十万条,本地存一份 CSV 就行。实际排查时,看到 OUI 是00:1B:21就知道是 Intel 网卡,看到B8:27:EB就知道是树莓派,对定位设备类型很有帮助。这一步不是必须的,但在资产盘点场景里能省很多事。
3. 把扫描器做成能日常用的工具:参数、并发与输出
3.1 网段大小与超时时间的取舍
/24 网段有 254 个可用地址,scapy 默认一次性全发出去,在千兆有线网络里没问题。但如果是 /16 网段,65534 个 ARP 请求同时广播,交换机的 CPU 可能直接跑满,这就是广播风暴。正确做法是分批:
def scan_large_network(network, batch_size=256, timeout=2): net = ipaddress.ip_network(network, strict=False) all_devices = [] hosts = list(net.hosts()) for i in range(0, len(hosts), batch_size): batch = hosts[i:i+batch_size] # 把一批 IP 拼成逗号分隔的字符串 batch_str = ",".join(str(ip) for ip in batch) devs = scan_lan(batch_str, timeout=timeout) all_devices.extend(devs) return all_devicesbatch_size=256是个经验值,对应一个 C 类网段的规模。timeout在分批后可以适当降低,因为每批设备少,丢包概率低,2 秒足够。如果网络里有老旧的工业交换机,建议把 batch_size 降到 64,timeout 提到 3 秒。
3.2 多网卡环境下的源地址绑定
一台机器有多张网卡时(比如同时接了办公网和工控网),scapy 默认从路由表选出口,可能把包发到错误的网卡上。这时候必须显式指定接口:
from scapy.all import conf def scan_with_interface(network, iface): # iface 是网卡名,Linux 下如 eth0,Windows 下如 "以太网" arp = ARP(pdst=network) ether = Ether(dst="ff:ff:ff:ff:ff:ff") packet = ether / arp result = srp(packet, timeout=3, iface=iface, verbose=0)[0] return [{"ip": r.psrc, "mac": r.hwsrc} for _, r in result]iface参数在 Linux 下填eth0、enp3s0这种名字,Windows 下填「以太网」「WLAN」这种显示名。不确定的话用conf.ifaces列出来看。这个坑我在工控现场踩过:笔记本同时插了内网口和外网口,不指定接口时扫描结果全是外网的设备,内网一台都扫不到,排查了半天才发现是路由选路的问题。
3.3 输出成 CSV 和 JSON,方便后续比对
扫描结果要能存下来才能做资产比对。CSV 适合人看,JSON 适合程序处理:
import csv import json from datetime import datetime def save_results(devices, prefix="scan"): ts = datetime.now().strftime("%Y%m%d_%H%M%S") # CSV 输出 with open(f"{prefix}_{ts}.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=["ip", "mac"]) writer.writeheader() writer.writerows(devices) # JSON 输出 with open(f"{prefix}_{ts}.json", "w") as f: json.dump({"time": ts, "devices": devices}, f, indent=2)有了历史记录,就能做增量比对:今天扫出来的 MAC 列表和昨天比,多出来的就是新接入设备,少掉的就是离线设备。这个能力在排查私接设备时特别有用——突然冒出一个陌生 MAC,基本就是有人偷偷接了东西。
3.4 定时扫描与 IP-MAC 变更告警
把扫描脚本挂到 cron 或计划任务里,每小时跑一次,然后比对结果:
def diff_scan(old_file, new_devices): with open(old_file) as f: old = json.load(f)["devices"] old_map = {d["mac"]: d["ip"] for d in old} new_map = {d["mac"]: d["ip"] for d in new_devices} added = set(new_map) - set(old_map) removed = set(old_map) - set(new_map) # MAC 没变但 IP 变了,说明 DHCP 重新分配或有人手动改了 changed = {m for m in set(old_map) & set(new_map) if old_map[m] != new_map[m]} for m in added: print(f"[新增] {new_map[m]} {m}") for m in removed: print(f"[离线] {old_map[m]} {m}") for m in changed: print(f"[IP变更] {m}: {old_map[m]} -> {new_map[m]}")changed这个集合最值得关注。MAC 不变但 IP 变了,通常是 DHCP 租约到期重新分配,但如果同一台设备频繁变 IP,可能是 IP 冲突的前兆。反过来,IP 不变但 MAC 变了,那基本可以确定是有人手动改了 MAC 地址,或者设备被替换了。
4. 避坑与排查:ARP 扫描翻车的五种典型情况
4.1 扫出来全是网关的 MAC
现象:结果里所有 IP 对应的 MAC 都是同一个,通常是网关或某台交换机的 MAC。
原因:网络里开了 ARP 代理(proxy ARP),或者中间有设备做了 ARP 欺骗/拦截。代理 ARP 会让网关代替其他设备回复 ARP 请求,导致所有 IP 都指向网关。
解决:先确认网络拓扑里有没有三层交换机或防火墙开了代理 ARP。如果是,需要登录设备关闭该功能,或者换用其他发现方式(比如读交换机的 MAC 地址表)。如果是 ARP 欺骗,那说明网络里已经有问题了,得先处理安全事件。
4.2 部分设备扫不到,但手动 ping 能通
现象:脚本扫出来 20 台,实际网段里有 30 台,漏掉的那几台手动 ping 是通的。
原因:这些设备对 ARP 请求的响应有延迟,或者网卡省电模式导致响应慢,在 timeout 内没回来。也可能是交换机端口隔离(port isolation)导致广播包到不了某些端口。
解决:把 timeout 从 3 秒提到 5 秒,retry 从 1 提到 3。如果是端口隔离,需要检查交换机配置,看是否关闭了端口间的广播转发。无线网络里这个问题更常见,因为 AP 可能对广播包做了限速。
4.3 Windows 下报权限错误
现象:脚本在 Linux 下跑得好好的,到 Windows 上直接抛PermissionError。
原因:scapy 在 Windows 下需要 Npcap 驱动才能发原始二层包,而且必须以管理员权限运行。
解决:先装 Npcap(安装时勾选「WinPcap API-compatible mode」),然后用管理员身份打开命令行再跑脚本。如果还是不行,检查 Npcap 是否被安全软件拦截了。这个坑没有捷径,Windows 下做二层操作就是需要额外驱动和权限。
4.4 扫描导致网络短暂卡顿
现象:跑扫描脚本时,同网段的其他同事反馈网络变慢,视频会议卡顿。
原因:ARP 请求是广播包,网段内所有设备都要处理。如果发送速率太快、包太多,交换机和终端的 CPU 都会被占用。
解决:降低发送速率。scapy 的srp有个inter参数控制包间隔:
result = srp(packet, timeout=3, inter=0.01, verbose=0)[0]inter=0.01表示每个包间隔 10 毫秒,254 个包大约 2.5 秒发完,对网络的影响可以忽略。如果网段更大,把 inter 调到 0.05 甚至 0.1。宁可扫得慢一点,也别把生产网络搞挂。
4.5 虚拟机和容器扫不到宿主机
现象:在虚拟机里跑扫描,宿主机和其他虚拟机的 IP 扫不出来。
原因:虚拟机的网卡通常是 NAT 模式或桥接模式。NAT 模式下虚拟机在独立子网里,ARP 广播到不了宿主机所在网段。桥接模式理论上可以,但某些虚拟化平台会对广播包做过滤。
解决:把虚拟机网卡改成桥接模式,并确认虚拟交换机的「混杂模式」和「广播转发」是开启的。容器环境更复杂,Docker 默认的 bridge 网络是独立的,需要用--network host才能和宿主机同网段。这个问题本质上是二层可达性问题,不是扫描器能解决的。
5. 进阶:把 ARP 扫描和交换机 MAC 表联动做资产测绘
单靠 ARP 扫描只能拿到 IP 和 MAC,拿不到设备接在哪个交换机端口上。但在排查「哪台机器在疯狂发包」或者「这个陌生 MAC 从哪个口进来的」时,端口信息才是关键。做法是把 ARP 扫描结果和交换机的 MAC 地址表做关联。
以常见的可管理交换机为例,通过 SNMP 能读到dot1dTpFdbTable,里面记录了每个 MAC 对应哪个桥端口。把 ARP 扫描拿到的 MAC 列表和这张表 join 一下,就能得到「IP - MAC - 交换机端口」的完整映射:
# 伪代码示意,实际用 pysnmp 读取 # mac_to_port = snmp_get_bridge_table(switch_ip, community) # for dev in arp_devices: # port = mac_to_port.get(dev["mac"]) # print(f"{dev['ip']} {dev['mac']} port={port}")这张表的价值在于:当 ARP 扫描发现一个陌生 MAC 时,你能立刻定位到它接在哪个物理端口,直接去机房拔线就行,不用一层层排查。我在一次内网异常流量排查里就是靠这个组合,十分钟锁定了问题设备——ARP 扫描发现陌生 MAC,SNMP 查到端口号,去机房一看是有人私接了一台设备。
另一个进阶用法是结合历史数据做基线。正常网络里,设备数量和 MAC 列表在一段时间内是稳定的。把每天扫描的结果存下来,跑一个简单的统计:如果某天新增 MAC 超过阈值,或者某个 MAC 的出现时间集中在非工作时段,就触发告警。这套逻辑不需要多复杂的算法,一个 diff 加一个计数器就够了,但实战效果比很多商业资产管理系统都直接。
最后说个我自己的习惯:每次到新环境部署,第一件事就是跑一遍 ARP 扫描存基线,然后配个定时任务每小时扫一次。这样网络里任何风吹草动——新设备接入、IP 被占、MAC 被改——都逃不过。工具不复杂,难的是坚持做基线、做比对。希望帮到你。
本文还有配套的精品资源,点击获取