Hotdry.
systems

无特权 LAN 发现:mDNS/SSDP/ARP 缓存触发的 Go 并发架构

基于 mDNS、SSDP 与 ARP 缓存触发的用户空间网络发现架构,Go 并发模型与无 root 权限约束下的工程实践。

传统 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+ 的私有地址功能),此时厂商识别将失效。


资料来源:

查看归档