MiBeeSteward v0.3.0 技术内幕:Docker 网络与探测保真度、TLS 证书链采集、三 MIB 拓扑
上一篇讲了 v0.3.0 有什么。这篇讲为什么——本版有三个看似随意、背后有具体工程取舍的决定:Docker 网络模式如何决定 scanner 的探测保真度、TLS 证书链采集为什么能用一个核心覆盖 8 个协议、L2 拓扑为什么需要三个 MIB 才拼得起来。逐个拆。
Docker 网络模式与探测保真度
这是本版最硬的工程发现,也是 v0.3.0 专门发容器镜像的导火索。在测试 LAN(31 台设备)上实测:
| 部署形态 | 发现的设备 MAC 数 |
|---|---|
| 裸金属 center | 31 / 31 |
docker --network host | 30 / 31 |
| docker 默认 bridge | 0 / 26 |
bridge 模式发现的设备 MAC 数是零。这不是 scanner 有 bug,是 network namespace 的物理事实。
scanner 的被动发现里有一步是读 /proc/net/arp——内核维护的 ARP 缓存。在默认 bridge 网络的容器里,容器有自己独立的 network namespace,它的网络栈只看得到 docker0 网桥;这个 namespace 里的 ARP 表只包含「网桥 gateway」一条记录,因为容器从来没和 LAN 上其他主机直接通信过(流量被 NAT 转发了)。宿主机那张塞满 30 条 MAC 的 /proc/net/arp,容器根本看不见。
flowchart TD
LAN["LAN<br/>30 台设备"] --> HOST["宿主机 netns<br/>/proc/net/arp: 30 行"]
LAN -->|"NAT 后看不到"| CTR["容器 netns<br/>/proc/net/arp: 仅 gateway"]
classDef net fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
classDef bad fill:#FFEBEE,stroke:#F44336,color:#C62828
class LAN net
class HOST ok
class CTR bad宿主机 network namespace 能看到完整的 ARP 表,容器 namespace 只能看到 NAT 后的 gateway——这就是 0/26 vs 30/31 的根因。
这就解释了 v0.3.0 的三种 compose profile 为什么这么分:
bridge(默认):容器在独立 netns 里,TCP/SNMP/HTTP 这些主动探测走容器自己的路由表能出去,但 MAC/ARP 这类依赖内核 netns 的被动发现全瞎。给 UI demo 和开发用——你只是想点开页面看看长什么样,不要求扫得准。host(推荐):--network host让容器直接共享宿主机 netns,/proc/net/arp就是宿主机的那张。探测保真度 ≈ 裸金属。这是生产扫描唯一该选的形态。macvlan:给容器分配一个独立的 LAN IP,容器作为 LAN 上的一台设备出现,ARP 直接和 LAN 通信,自然能拿到完整邻居表。适合「让容器就是 LAN 上的一台发现设备」。
非特权镜像里 LLDP/CDP raw frame 发送 + eBPF 被动观测器 ship 成 no-op stub,原因类似但更深一层:发 raw frame 需要 CAP_NET_RAW,跑 eBPF 程序需要 CAP_BPF / CAP_NET_ADMIN。容器默认没有这些能力,强行调用会失败。stub 模式保证镜像在普通 docker run 下不 crash,需要这些能力的人用 make docker-build-priv 在本地构建特权变体。stub 是「能力缺失时降级」而不是「假装能跑」——这些 handler 启动时检查能力,缺失就跳过,结果集里相应字段为空。
TLS 证书链采集架构
v0.3.0 给 TLS 服务加了完整证书链采集,handler 从 21 涨到 29——8 个 TLS-wrapped handler(https / ldaps / smtps / imaps / pop3s / ftpps / ircs / telnets)。乍一看像是「每个协议各写一份」,实际是一个 CollectCertChain 核心被 8 个 handler 共享。
这么设计是因为 Go 的 crypto/tls 把证书链采集和应用层协议完全解耦了。不管你后面跑的是 HTTP 还是 SMTP,TLS 握手在 tls.Dial 那一步就完成了;握手成功后 conn.ConnectionState().PeerCertificates 直接返回完整链([]*x509.Certificate,第 0 个是 leaf)。证书链的解析逻辑跟「上面跑什么应用」一点关系都没有,所以 8 个 handler 共享同一个核心是自然结果——每个 handler 只负责「按自己的端口和协议发起握手」,握手之后的处理走同一条路径。
flowchart TD
DIAL["tls.Dial<br/>完成握手"] --> CS["ConnectionState<br/>.PeerCertificates"]
CS --> LOOP["逐张证书<br/>cert_index 0..N"]
LOOP --> EXTRACT["Subject/Issuer/SAN<br/>serial / validity<br/>sig algo / key algo<br/>SHA-256 / PEM"]
EXTRACT --> DB[("host_tls_certs<br/>(ip,port) + not_after 索引")]
classDef proc fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class DIAL,CS,LOOP,EXTRACT proc
class DB store握手后 PeerCertificates 拿到完整链;逐张抽取结构化字段(cert_index 0 是 leaf,1..N 是 issuer)后批量写入 host_tls_certs。索引建在两个查询路径上。
host_tls_certs 表上建了两个索引,对应两条独立查询路径:
(ip, port)索引:服务「这台设备这些端口的证书是什么」这种主路径查询。设备详情页的 TLS 子面板一次拉这台设备所有端口的证书,靠这个索引。not_after索引:服务「未来 N 天内过期的证书」这种扫描。证书过期巡检是一次全表过滤,没有not_after索引就是全表扫;有了索引,按时间窗口的扫描能走索引。
两张索引服务两个不同查询模式,互不替代。状态色(绿=有效 / 琥珀=<15 天过期 / 红=已过期)在前端按 not_after - now() 算,不依赖额外字段。
retention.host_tls_certs_days 默认 30 天。这个数字权衡的是「证书巡检的历史可追溯性」vs「表增长速度」——证书一般一年有效,30 天足够回溯一次完整的巡检周期,又不会让表无限膨胀。和 v0.2.0 的 retention 设计一样,days<=0 是安全 guard,不会触发清空全表。
L2 拓扑为何需要三个 MIB
v0.2.0 用 Bridge-MIB 起步,单走它能拿到「MAC 在哪个 ifIndex 后面」,但这只是拓扑的一个维度。真实的交换机环境里,单靠 Bridge-MIB 会漏掉三类信息:
- 厂商邻居身份:Bridge-MIB 给你 MAC↔ifIndex 映射,但 ifIndex 是数字,对面的设备是什么厂商、什么型号、叫什么名字——Bridge-MIB 不告诉你。
- VLAN-aware 转发:Bridge-MIB 走的是
dot1dTpFdbPort,对 VLAN-transparent 的 FDB 有效;但 VLAN-aware 的交换机走dot1qTpFdbPort(Q-BRIDGE-MIB),tagged 端口的邻接信息在另一个表里。 - 端口角色:哪条边在转发、哪条边被 STP 阻塞、哪台是 root bridge——Bridge-MIB 不管,得看
dot1dStp(STP-MIB)。
v0.3.0 补的三个 MIB 各自覆盖一块,合并键也不同:
| MIB | 提供 | 合并键 |
|---|---|---|
| Bridge-MIB(v0.2.0) | MAC↔ifIndex 基础邻接 | MAC |
| CDP-MIB | 厂商邻居身份(device id、平台串、IP) | device id |
| Q-BRIDGE-MIB | VLAN-aware MAC→端口 | MAC |
| STP-MIB | 端口角色(root / designated / role / state) | STP port id |
合并键不同是关键:CDP 邻居的合并靠 device id(CDP 自报的设备标识),Bridge-MIB 和 Q-BRIDGE 邻居的合并靠 MAC,STP 事实靠 STP port id。三条数据流各自落到 device_neighbors,最后用 MAC 这条主键 + EnrichDeviceByMAC 把同一台设备在不同协议下的事实合并到一行。
IF-MIB ifName 解析这一步容易被忽略但很重要。Bridge-MIB/Q-BRIDGE 给出的端口是 ifIndex=1011 这种数字,对人毫无意义;走 IF-MIB 的 ifName 把它翻译成 GigabitEthernet0/1,运维才看得懂「这台设备的 1 口连着谁」。拓扑图上展示的端口名就是 ifName 解析后的结果。
邻居身份推断的闭环:CDP/LLDP 邻居会带一个平台串(比如 Cisco ISR4321 或 Hikvision DS-2CD2047G2),这个串喂给 v0.2.0 的 RuleClassifier 推断出厂商/型号/类型。然后 EnrichDeviceByMAC 用 MAC 把「邻居表里的这台设备」和「设备表里的那台设备」对上号——保留设备表里已有的非空字段不被覆盖(比如设备表里手工填的名字优先于推断结果)。v0.2.0 里 neighbor_device_id 一直是 NULL,就是因为这一步推断链没接上;v0.3.0 在查询时实时解析,前端 Neighbors 面板才能展示出有 name/IP/type 的邻居表。
已知限制
几个设计上故意没做的,免得误期待:
- 单实例 SQLite——center 是单实例的,没有多 center 集群。规模到这个量级之前不打算做。和 v0.2.0 一致。
- 非特权镜像不做 LLDP/CDP raw frame + eBPF——
docker pull下来的镜像是非特权变体,这三类探测是 no-op stub。需要的话本地make docker-build-priv构建特权变体,运行时配CAP_NET_RAW/CAP_BPF/CAP_NET_ADMIN。 - host_tls_certs 默认 30 天保留——足够回溯一次证书巡检周期,长期合规归档要自己接(比如定期 dump 到外部存储)。
相关链接
- MiBeeSteward GitHub
- MiBeeSteward v0.3.0 Release Notes
- 部署指南(Docker network mode selection)
- v0.3.0 功能概览(promo 版)
v0.3.0 的工程取舍其实都围绕一件事:scanner 是 network-namespace 敏感的,TLS 证书链是应用层无关的,L2 拓扑是单 MIB 拼不全的。三个事实分别决定了容器镜像的三 profile、TLS 采集的一核心多 handler、拓扑的三 MIB 合并。如果你在搭类似的发现系统,这几个根因比特性列表更值得参考。