MiBeeSteward v0.2.0: 分布式发现 + 变更检测 + 拓扑 + 指纹库
v0.1.0 发布的时候我写了一句:它能回答「网络上有哪些设备、它们是什么、它们之间什么关系」。但说实话,v0.1.0 只答好了前半句——而且只限于 center 所在的那一个 LAN。
工作室的真实场景是:办公室一个网段、机房一个网段、家里跑测试机又一个网段。center 坐在办公室,机房的摄像头它根本看不见。要让 center 去扫机房,得跨子网穿透,要么开 SNMP 路由转发,要么干脆把 center 搬过去——两个都不优雅。
更扎心的是:v0.1.0 只会记录状态,不会告诉你变了什么。昨晚新接的一台 NAS、白天换 IP 的那台树莓派、上周掉线的那台交换机——全都淹没在设备列表里,得自己肉眼 diff。
v0.2.0 就是冲着这两个痛点来的。一句话总结:把 MiBeeSteward 从单网段发现工具升级成分布式多 LAN 设备画像系统。完整发布说明见 GitHub Release v0.2.0。
这一次 ships 两个二进制——mibee-steward(center + Web UI)和 mibee-agent(远程 LAN 发现),都是纯 Go、CGO-free、普通用户就能跑。
分布式发现:center + agent
最核心的变化是新增了 mibee-agent 二进制。它跑在远程 LAN 里,用和 center 一样的 scannerv2 引擎扫本地网络,然后把结果汇报给 center。
设计上选了 pull 模型——agent 主动发起所有连接,center 不需要对外开任何端口。这对家庭网络和跨 NAT 的场景特别友好:agent 装在内网,出站 HTTPS 到 center 就行,防火墙一个洞都不用打。
sequenceDiagram
participant A as mibee-agent<br/>(远程 LAN)
participant C as center<br/>(Web UI + API)
A->>C: POST /agents/report<br/>(存活设备 + 状态哈希)
C-->>A: 200 OK
loop 每 60s
A->>C: GET /agents/commands
C-->>A: 待执行扫描命令(可空)
A->>C: POST .../commands/{id}/ack
A->>C: POST .../commands/{id}/complete
endagent 每 30 秒推一次存活设备报告,同时每 60 秒拉一次 center 下发的临时扫描命令。center 收到报告后通过 device bridge 转成本地设备画像。agent 管理的网段会从 center 自己的跨子网探测里排除——agent 的报告本身就是存活信号,没必要重复扫。

Agents 管理页——agent token 的创建/吊销(明文只显示一次)、命令历史、手动触发扫描。每个 token 绑定一个 network_id + agent_id,center 从 token 反推网络,配错的 agent 没法把设备盖到别的网段上。
变更检测引擎
这是我个人最想要的特性。v0.1.0 记录设备状态但不计算 diff,v0.2.0 补上了这一刀:三类事件写到新的 change_log 表里——device_added(新设备出现)、device_changed(已知设备的身份字段变了)、device_lost(设备掉线)。
device_changed 跟踪 12 个字段:名称、类型、品牌、型号、MAC、IP、状态、开放端口、检测到的服务、Prometheus URL、node_exporter URL、扫描属性。每条事件存的是结构化的 before→after 差异,不是整条快照,查起来很直观。
查询走 GET /api/v1/changes(分页、按网络+类型过滤),想实时看就接 GET /api/v1/changes/watch 的 SSE 流,带 keepalive。Prometheus 那边也能看到 mibee_changes_total{type=...} 计数器。

Changes 页——变更历史列表,点开任意一条看结构化 before→after diff。SSE 连上后新事件实时推进来,不用刷新。
被动发现
定时扫描有个天然盲区:两轮扫描之间新上线的设备要等下一轮才发现。v0.2.0 加了一个长跑的被动发现服务,近零流量地盯着「最近冒出来的新面孔」,喂给同一个 device bridge。凌晨两点启动的设备也能立刻拿到一个 device_added 事件。
flowchart TB
SRC["路由器 ARP / 缓存<br/>组播 mDNS + SSDP"] --> DEDUP["5 分钟去重"]
DEDUP --> PRE["MAC 预检<br/>已知主机直接认"]
PRE -->|"未知 MAC"| BRIDGE["device bridge<br/>触发识别扫描"]
PRE -->|"已知 MAC"| SKIP["仅刷新<br/>不重扫"]
classDef src fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef out fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class SRC src
class DEDUP,PRE,BRIDGE proc
class SKIP out三个数据源各自可单独开关:路由器 SNMP ARP 表(跨子网覆盖最广)、本地 ARP 缓存(读 /proc/net/arp,零网络流量)、组播监听(mDNS :5353 + SSDP :1900,只听不发,能捡到摄像头、打印机、IoT、Mac、UPnP 设备)。
一个关键优化是 MAC 优先的已知主机预检:一台设备因为 DHCP 续期从 .143 漂到 .144,靠 MAC 就能直接认出来,不必触发一轮昂贵的识别扫描。5 分钟去重窗口还能压掉突发 burst。运行时计数和最近 20 条发现在 GET /api/v1/discovery/status 暴露,前端有专门的 Discovery 页展示。

Discovery 页——被动发现的运行时漏斗(各源命中数、去重数、触发识别扫描数),下方是最近 20 条发现记录。
Bridge-MIB 拓扑与新探测
v0.2.0 开始往 L2 拓扑方向铺路。新增的 Bridge-MIB 邻居探测会走交换机的 dot1dTpFdbAddress / dot1dTpFdbPort / dot1dBasePortIfIndex,搞清楚哪个 MAC 在哪个交换机端口后面,结果存进新的 device_neighbors 表。这是拓扑图的基础,LLDP/CDP 留给后续版本。
flowchart TB
SW["交换机<br/>SNMP 可达"] --> FDB["走 BRIDGE-MIB<br/>FDB 表"]
FDB --> ADJ["L2 邻接<br/>MAC ↔ 端口"]
ADJ --> DB[("device_neighbors<br/>protocol=Bridge-MIB")]
classDef dev fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class SW dev
class FDB,ADJ proc
class DB store探测集也从 v0.1.0 的 13 个扩到 15 个,这次新增的几个都很有针对性:
| 探测 | v0.1.0 | v0.2.0 | 新增能力 |
|---|---|---|---|
| Bridge-MIB 邻居 | ❌ | ✅ | L2 邻接,拓扑图基础 |
| SMB2 Negotiate | ❌(仅端口兜底) | ✅ | 解析 SMB 方言(如 “SMB 3.1.1”)+ OS 串 |
| FTP banner | ⚠️ 不稳 | ✅ | 更可靠的 banner 抓取 |
| TLS cert CN | ⚠️ | ✅ | 识别 OpenWrt / GL.iNet / iStoreOS(CN + issuer org 联合判定) |
| 路由器 ARP | ✅ | ✅ 增强 | 跨子网 MAC 解析 |
iStoreOS 比较有意思——它把产品名放在证书 CN 里,issuer 组织放 “OpenWrt”,联合两个字段才能准确认出来。
指纹规则库:数据驱动
v0.1.0 的设备识别规则是硬编码在 Go 里的,加一个设备签名要改代码、重新编译。v0.2.0 把识别规则变成了数据——YAML 文件,启动时由 RuleClassifier 加载。
flowchart TB
YAML["YAML 规则文件<br/>或二进制内嵌"] --> LOAD["RuleClassifier<br/>启动加载"]
LOAD --> MATCH["证据匹配<br/>品牌/型号/类型"]
MATCH --> PORTRAIT["设备画像"]
classDef data fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef out fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class YAML data
class LOAD,MATCH proc
class PORTRAIT out加一条设备签名 = 写一条 YAML,不用碰 Go 代码。规则路径留空就用二进制内嵌的规则集(零配置),也可以指向外部目录方便热更新。
开箱即用的规则数量相当可观:约 2554 条——20 条手写 builtin + 约 2534 条从 Rapid7 的 Recog 语料库导入(Apache-2.0)。nmap 的 NPSL 因为许可证不干净故意不导入。独立的指纹引擎现在拆到了 mibee-fingerprints-go 模块,作为普通依赖被消费。
少数没法用单条声明式规则表达的逻辑还是留在 Go 里(有文档说明):SNMP bitmask 设备类型启发式、摄像头交叉证据融合、数据库/远程访问的字节偏移分类器。还加了一个 parity 测试,断言数据驱动分类器的输出和它替换掉的手写分类器字节级一致。
管理 UI
这次前端加了 4 个新页面和 2 个可复用组件:
- Networks(管理员)— 创建/编辑/删除逻辑网络,agent 绑定到网络上。以前得直接连 SQLite 改。
- Discovery — 被发现服务的运行时漏斗 + 最近发现。
- Changes — 变更历史 + 结构化 before/after diff + SSE 实时更新。
- Agents(管理员)— agent token 的 CRUD(明文一次性显示)+ 命令历史 + 触发扫描。
- ColumnPicker 组件 — Devices 页每用户可切换可选列(厂商、MAC、主机名、RTT、推断类型、OS…),存 localStorage。
- ChangeDiff 组件 — 按 added/lost/changed 各自正确的形状渲染事件。
升级
center 是 drop-in 升级。新表(change_log、agent_tokens、scan_snapshots、agent_commands、device_neighbors、topology_edges、subnets)都走 CREATE TABLE IF NOT EXISTS,自动建,无需手工迁移,没有新的必填 center 配置项,现有设备数据原样保留。
agent 是可选的,装一台远程 LAN 探测节点五步:
| |
Bug 修复与运维改进
- CSRF-safe 导出:CSV/JSON 下载改走 API 客户端——之前用裸
fetch绕过了 API 层,丢了 CSRF 头(一个真实的鉴权 bug)。 - Server bind-retry:systemd 重启时若上一个 socket 还在 TIME_WAIT,最多重试 30s 绑定,而不是直接 crash-loop。
- Agent HTTP keep-alive 死锁修复:center 重启后池化 keep-alive 连接半开,会卡住读取好几分钟;现在
IdleConnTimeout=10s+ 有界 dialer,同时强制扫描截止时间。 - golangci-lint v2:v0.1.0 发布时根本没跑过 lint,现在 CI 强制 v2.12.2 全规则集,代码库全绿。
- Retention 配置整合:统一到
retention.*下(心跳 7d、扫描结果 30d、审计 90d、change_log 30d,各表可配)。
相关链接
v0.2.0 的标签我写的是「分布式发现 · 拓扑 · 变更检测 · 指纹库」。如果你在多网段环境里跑过 v0.1.0,强烈建议升级试一下 agent——远程 LAN 一接上,那种「终于看全了」的感觉是很实在的。
项目采用 PolyForm Noncommercial 许可证(非商业免费使用)。有想法或碰到问题,欢迎提 Issue。