传统 LAN 扫描工具如 nmap、arp-scan 依赖原始套接字或 CAP_NET_RAW 能力,在容器化部署、受限用户环境中往往无法直接使用。Whosthere 项目提供了一种完全用户空间的替代方案:通过 mDNS、SSDP 服务发现协议与 ARP 缓存触发技术,在无 root 权限条件下实现局域网设备枚举。这一架构对安全审计工具的权限最小化设计具有参考价值。
无特权扫描的核心约束
发送原始 ARP 请求需要 CAP_NET_RAW 能力。传统工具直接构造链路层帧广播 ARP who-has 查询,这在普通用户进程中被内核拒绝。绕过这一限制的思路是:不主动发送 ARP,而是利用正常的传输层连接尝试,让内核自动完成 ARP 解析,随后从系统 ARP 缓存中读取结果。
Linux 系统的 ARP 缓存可通过 /proc/net/arp 读取,macOS 则通过 arp -a 命令或系统调用获取。这些接口对普通用户开放,无需特殊权限。
三种发现机制的技术原理
Whosthere 并发运行三个独立的扫描器,各自针对不同类型的设备:
mDNS 扫描器工作在 UDP 5353 端口,向组播地址 224.0.0.251 发送 DNS-SD 查询。支持 Bonjour/Avahi 的设备(如 Apple 设备、打印机、NAS)会响应其服务类型与主机名。Go 生态中 hashicorp/mdns 库提供了开箱即用的实现:
entriesCh := make(chan *mdns.ServiceEntry, 8)
go func() {
for entry := range entriesCh {
// 处理发现的服务
}
}()
mdns.Lookup("_services._dns-sd._udp", entriesCh)
SSDP 扫描器向 239.255.255.250:1900 发送 M-SEARCH 请求,这是 UPnP 设备发现的标准协议。路由器、智能电视、媒体服务器等设备会响应其设备描述 URL:
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: ssdp:all
ARP 缓存触发器是最关键的组件。它对本地子网的每个 IP 地址发起 TCP 或 UDP 连接尝试(通常是短暂的 SYN 或 UDP 探测)。即使目标端口关闭或无响应,内核在尝试建立连接前必须先完成 ARP 解析,这会将目标 MAC 地址写入 ARP 缓存。扫描完成后读取缓存即可获得活跃设备列表。
Go 并发模型的工程实现
三个扫描器需要并发执行并在超时后聚合结果。Go 的 goroutine 与 channel 机制天然适合这一场景:
type Device struct {
IP string
MAC string
Hostname string
Manufacturer string
}
func scan(ctx context.Context, duration time.Duration) []Device {
results := make(chan Device, 256)
var wg sync.WaitGroup
wg.Add(3)
go func() { defer wg.Done(); scanMDNS(ctx, results) }()
go func() { defer wg.Done(); scanSSDP(ctx, results) }()
go func() { defer wg.Done(); sweepSubnet(ctx, results) }()
go func() {
wg.Wait()
close(results)
}()
devices := make(map[string]Device)
for d := range results {
// 去重与合并
if existing, ok := devices[d.IP]; ok {
d = mergeDeviceInfo(existing, d)
}
devices[d.IP] = d
}
return mapToSlice(devices)
}
子网扫描的并发度需要控制。对 /24 子网的 254 个地址同时发起连接会产生大量 goroutine,通过 worker pool 或 semaphore 限制并发数(如 64)可避免资源耗尽:
func sweepSubnet(ctx context.Context, results chan<- Device) {
sem := make(chan struct{}, 64)
for ip := range subnetIPs() {
sem <- struct{}{}
go func(ip string) {
defer func() { <-sem }()
probeAndReport(ctx, ip, results)
}(ip)
}
}
配置参数与运维要点
Whosthere 的默认配置提供了合理的起点,但生产环境需要根据网络规模调整:
| 参数 | 默认值 | 调优建议 |
|---|---|---|
| scan_interval | 20s | 大型子网可延长至 60s 减少负载 |
| scan_duration | 10s | 需大于子网扫描时间,否则结果不完整 |
| port_scanner.timeout | 5s | 跨 VLAN 场景可能需要增加 |
mDNS 与 SSDP 依赖组播流量,某些企业网络的交换机会过滤这些包。排查时可用 tcpdump 确认组播包是否到达:
tcpdump -i eth0 port 5353 or port 1900
Daemon 模式的 HTTP API 适合与监控系统集成。/devices 端点返回 JSON 格式的设备列表,可接入 Prometheus 的 blackbox exporter 或自定义采集器:
curl http://localhost:8080/devices | jq '.[] | {ip, mac, manufacturer}'
安全边界与限制
无特权扫描存在固有限制。ARP 缓存触发只能发现响应连接尝试或已在缓存中的设备;完全静默的设备(如某些 IoT 网关)可能被遗漏。此外,mDNS/SSDP 仅对实现这些协议的设备有效,传统嵌入式设备往往不支持。
从安全角度看,这种用户空间扫描方式本身就是一种权限最小化实践。它避免了授予扫描工具过高权限带来的风险,同时在功能上覆盖了大多数日常场景。对于需要完整 ARP 扫描能力的场景,仍需回退到特权工具。
OUI 查询通过 IEEE 的公开数据库将 MAC 地址前缀映射到厂商名称,这一信息对资产管理有价值,但需注意部分设备使用随机化 MAC(如 iOS 14+ 的私有地址功能),此时厂商识别将失效。
资料来源:
- Whosthere 项目:https://github.com/ramonvermeulen/Whosthere
- RFC 6762 Multicast DNS:https://datatracker.ietf.org/doc/html/rfc6762