用 ESP32 替代虚拟机做网络拨测 —— esp32-blackbox 项目实战
起因
我在市区不同地方有几个局域网,相互之间大概隔了 10 公里左右。为了让这几个网络能互通,我用 NetBird、ZeroTier、Cloudflare Tunnel 这类工具搭了一套跨地域的虚拟局域网。
网络搭好了,但稳定性怎么保障?毕竟这些隧道要穿过公网,中间的链路质量参差不齐。最直接的办法就是用 Prometheus 的 blackbox_exporter 做拨测——定期 HTTP 请求、Ping、DNS 查询,把结果丢进时序数据库,配上告警规则,出问题第一时间知道。
但问题来了:blackbox_exporter 得跑在一台机器上。为了一个拨测服务专门开个虚拟机,电费和硬件成本都不划算。家里那台跑 Proxmox 的服务器已经够费电了,再加一台实在没必要。
正好手上有几块 ESP32 开发板。ESP32 本身就是为网络连接设计的芯片,跑个 HTTP 请求、发个 ICMP 包完全不在话下。功耗还低,USB 供电就能跑,一个月电费几毛钱。于是就有了这个项目:esp32-blackbox。
整体架构
先看下这个系统在整体网络监控里的位置:
graph TB
A@{shape: hex, label: "站点A<br/>ESP32 + 路由器"}
B@{shape: hex, label: "站点B<br/>ESP32 + 路由器"}
C@{shape: hex, label: "站点C<br/>ESP32 + 路由器"}
A <--> OV["虚拟组网层<br/>NetBird / ZeroTier / CF Tunnel"] <--> B
B <--> OV
C <--> OV
A -->|":9090/metrics"| P@{shape: cyl, label: "Prometheus"}
B -->|":9090/metrics"| P
C -->|":9090/metrics"| P
P --> G@{shape: doc, label: "Grafana"}
classDef site fill:#bbdefb,stroke:#2196F3,color:#1B5E20
classDef overlay fill:#fff3e0,stroke:#FF9800,color:#BF360C
classDef store fill:#c8e6c9,stroke:#4CAF50,color:#1B5E20
classDef view fill:#f3e5f5,stroke:#9C27B0,color:#4A148C
class A,B,C site
class OV overlay
class P store
class G view每个站点放一块 ESP32,通过各自的出口网络去做拨测。Prometheus 从各节点的 9090 端口拉取指标,Grafana 负责展示和告警。
跨地域组网方案
简单介绍下用的几个组网工具:
- NetBird:基于 WireGuard 的 mesh VPN,P2P 打洞成功后延迟很低,管理界面也方便
- ZeroTier:软件定义的二层虚拟网络,稳定性不错,适合做备用链路
- Cloudflare Tunnel:反代隧道,不需要公网端口就能暴露内网服务,适合那些不支持 P2P 的场景
三层组网的好处是互为冗余。NetBird 挂了还有 ZeroTier,都不行了 Cloudflare Tunnel 还能兜底。但冗余越多,需要监控的链路也越多,这也正是 ESP32 拨测发挥作用的地方。
ESP32 Blackbox 项目介绍
项目地址:github.com/Mi-Bee-Studio/esp32-blackbox
硬件选型
目前支持两款芯片:
| 芯片 | 推荐开发板 | 特点 |
|---|---|---|
| ESP32-C3 | SuperMini | 便宜,淘宝十来块钱 |
| ESP32-C6 | XIAO ESP32C6 | 支持 WiFi 6,性能更好 |
我手上用的是 ESP32-C3 SuperMini,跑这个项目绰绰有余。
支持的探测类型
| 协议 | 说明 |
|---|---|
| HTTP/HTTPS | GET/POST 请求,状态码校验 |
| TCP | TCP 连接测试 |
| TCP+TLS | TLS 握手计时 |
| DNS | DNS 解析测试 |
| ICMP Ping | 原生 socket 实现,RTT 测量 |
| WebSocket/WSS | WS 连接测试 |
基本上 blackbox_exporter 能做的它都能做。
首次启动零配置
这个设计我比较满意。第一次上电,ESP32 自动进入 AP 模式,手机连上 ESP32_Blackbox 这个热点(密码 12345678),浏览器打开 192.168.4.1 就能配 WiFi。配完重启就自动连上了,不需要串口操作。
flowchart TD
A["上电启动"] --> B@{shape: diam, label: "NVS 有<br/>WiFi 凭据?"}
B -->|"否"| C["进入 AP 模式<br/>手机连热点配 WiFi<br/>192.168.4.1"]
C --> D["保存凭据并重启"]
B -->|"是"| E["STA 模式<br/>连接 WiFi"]
D --> E
E --> F["启动探测 / Web:80<br/>Metrics:9090"]
classDef src fill:#bbdefb,stroke:#2196F3,color:#1B5E20
classDef decision fill:#f3e5f5,stroke:#9C27B0,color:#4A148C
classDef config fill:#fff3e0,stroke:#FF9800,color:#BF360C
classDef running fill:#c8e6c9,stroke:#4CAF50,color:#1B5E20
class A src
class B decision
class C,D config
class E,F runningWeb 管理界面
STA 模式下,浏览器打开 ESP32 的 IP 就能看到管理界面:

界面上可以直接编辑 JSON 配置,改完点保存就行,不需要重新编译固件。还支持热加载,改了配置 POST 一下 /api/reload 就生效了。
配置文件格式
探测目标通过 JSON 配置,存在 SPIFFS 文件系统里:
| |
modules 定义探测行为(用什么协议、超时多久、校验规则),targets 定义探测目标,目标通过 module 字段引用模块配置。想加个新探测目标?编辑 JSON 就行,不用碰代码。
Prometheus 集成
ESP32 Blackbox 完全兼容 Prometheus 的抓取方式。/metrics 端点输出标准 Prometheus 文本格式:
| |
在 Prometheus 里配上 scrape job:
| |
第二种写法用了 /probe 端点,跟原版 blackbox_exporter 的用法一模一样。你甚至可以把 Prometheus 里原来指向 blackbox_exporter 的配置直接改个 IP 指向 ESP32,其他不用动。
实际部署
我把几块 ESP32 分别放在不同的站点,每个站点配置不同的探测目标:
- 站点 A 的 ESP32 去拨测站点 B、C 的服务
- 站点 B 的 ESP32 去拨测站点 A、C 的服务
- 站点 C 同理
这样任意两个站点之间的链路质量都有数据。Grafana 里拉个 Dashboard,延迟、丢包率、HTTP 成功率一目了然。
graph TD
EA@{shape: hex, label: "ESP32 节点集群<br/>站点A / B / C<br/>互相拨测"}
EA -->|":9090/metrics"| P@{shape: cyl, label: "Prometheus"}
P --> G@{shape: doc, label: "Grafana"}
classDef site fill:#bbdefb,stroke:#2196F3,color:#1B5E20
classDef store fill:#c8e6c9,stroke:#4CAF50,color:#1B5E20
classDef view fill:#f3e5f5,stroke:#9C27B0,color:#4A148C
class EA site
class P store
class G view构建 & 烧录
项目基于 ESP-IDF v6.0,提供了几种构建方式:
| |
如果用的是 ESP32-C6,把 esp32c3 换成 esp32c6 就行。
总结
说白了就一句话:不想为跑个 blackbox_exporter 多开一台服务器。ESP32 几块钱一块,功耗不到 1W,USB 充电器供电就行,放哪都不心疼。
项目开源在 GitHub 上,有兴趣可以试试:Mi-Bee-Studio/esp32-blackbox