用 ESP32 替代虚拟机做网络拨测 —— esp32-blackbox 项目实战

起因

我在市区不同地方有几个局域网,相互之间大概隔了 10 公里左右。为了让这几个网络能互通,我用 NetBird、ZeroTier、Cloudflare Tunnel 这类工具搭了一套跨地域的虚拟局域网。

网络搭好了,但稳定性怎么保障?毕竟这些隧道要穿过公网,中间的链路质量参差不齐。最直接的办法就是用 Prometheus 的 blackbox_exporter 做拨测——定期 HTTP 请求、Ping、DNS 查询,把结果丢进时序数据库,配上告警规则,出问题第一时间知道。

但问题来了:blackbox_exporter 得跑在一台机器上。为了一个拨测服务专门开个虚拟机,电费和硬件成本都不划算。家里那台跑 Proxmox 的服务器已经够费电了,再加一台实在没必要。

正好手上有几块 ESP32 开发板。ESP32 本身就是为网络连接设计的芯片,跑个 HTTP 请求、发个 ICMP 包完全不在话下。功耗还低,USB 供电就能跑,一个月电费几毛钱。于是就有了这个项目:esp32-blackbox

整体架构

先看下这个系统在整体网络监控里的位置:

mermaid
graph TB
    subgraph SiteA["站点 A(家)"]
        A1[ESP32 Blackbox]
        A2[路由器/交换机]
        A1 --- A2
    end
    subgraph SiteB["站点 B(办公室)"]
        B1[ESP32 Blackbox]
        B2[路由器/交换机]
        B1 --- B2
    end
    subgraph SiteC["站点 C(其他)"]
        C1[ESP32 Blackbox]
        C2[路由器/交换机]
        C1 --- C2
    end

    subgraph Overlay["虚拟组网层"]
        N1[NetBird]
        N2[ZeroTier]
        N3[Cloudflare Tunnel]
    end

    A2 <--> N1 <--> B2
    B2 <--> N2 <--> C2
    A2 <--> N3 <--> C2

    subgraph Monitor["监控中心"]
        P[Prometheus]
        G[Grafana]
    end

    A1 -->|":9090/metrics"| P
    B1 -->|":9090/metrics"| P
    C1 -->|":9090/metrics"| P
    P --> G

每个站点放一块 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-C3SuperMini便宜,淘宝十来块钱
ESP32-C6XIAO ESP32C6支持 WiFi 6,性能更好

我手上用的是 ESP32-C3 SuperMini,跑这个项目绰绰有余。

支持的探测类型

协议说明
HTTP/HTTPSGET/POST 请求,状态码校验
TCPTCP 连接测试
TCP+TLSTLS 握手计时
DNSDNS 解析测试
ICMP Ping原生 socket 实现,RTT 测量
WebSocket/WSSWS 连接测试

基本上 blackbox_exporter 能做的它都能做。

首次启动零配置

这个设计我比较满意。第一次上电,ESP32 自动进入 AP 模式,手机连上 ESP32_Blackbox 这个热点(密码 12345678),浏览器打开 192.168.4.1 就能配 WiFi。配完重启就自动连上了,不需要串口操作。

mermaid
flowchart TD
    A[上电启动] --> B{NVS 有 WiFi 凭据?}
    B -->|否| C[进入 AP 模式]
    C --> D[手机连接 ESP32_Blackbox 热点]
    D --> E["浏览器打开 192.168.4.1"]
    E --> F[选择 WiFi 并输入密码]
    F --> G[保存到 NVS]
    G --> H[重启]
    B -->|是| I[STA 模式连接 WiFi]
    H --> I
    I --> J[启动探测任务]
    I --> K[启动 Web 管理界面 :80]
    I --> L[启动 Metrics 服务 :9090]

Web 管理界面

STA 模式下,浏览器打开 ESP32 的 IP 就能看到管理界面:

界面上可以直接编辑 JSON 配置,改完点保存就行,不需要重新编译固件。还支持热加载,改了配置 POST 一下 /api/reload 就生效了。

配置文件格式

探测目标通过 JSON 配置,存在 SPIFFS 文件系统里:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "scrape_interval": 30,
  "metrics_port": 9090,
  "modules": {
    "http_2xx": {
      "prober": "http",
      "timeout": 10,
      "http": {
        "method": "GET",
        "valid_status_codes": [200]
      }
    },
    "icmp_ping": {
      "prober": "icmp",
      "timeout": 5,
      "icmp": {
        "packets": 3,
        "payload_size": 56
      }
    }
  },
  "targets": [
    {
      "name": "httpbin_http",
      "target": "httpbin.org",
      "module": "http_2xx"
    },
    {
      "name": "dns_google",
      "target": "8.8.8.8",
      "module": "dns_resolve"
    }
  ]
}

modules 定义探测行为(用什么协议、超时多久、校验规则),targets 定义探测目标,目标通过 module 字段引用模块配置。想加个新探测目标?编辑 JSON 就行,不用碰代码。

Prometheus 集成

ESP32 Blackbox 完全兼容 Prometheus 的抓取方式。/metrics 端点输出标准 Prometheus 文本格式:

text
1
2
3
4
5
6
7
# HELP probe_success Whether the probe succeeded
# TYPE probe_success gauge
probe_success{target="httpbin_http", module="http_2xx"} 1

# HELP probe_duration_seconds Duration of the probe in seconds
# TYPE probe_duration_seconds gauge
probe_duration_seconds{target="httpbin_http", module="http_2xx"} 0.234

在 Prometheus 里配上 scrape job:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
scrape_configs:
  # 直接拉取 ESP32 上所有探测结果
  - job_name: 'esp32-blackbox'
    static_configs:
      - targets: ['192.168.1.100:9090']

  # 或者用 /probe 端点做即席探测(跟原版 blackbox_exporter 一样)
  - job_name: 'blackbox_http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets: ['httpbin.org:80']
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - target_label: __address__
        replacement: 192.168.1.100:9090

第二种写法用了 /probe 端点,跟原版 blackbox_exporter 的用法一模一样。你甚至可以把 Prometheus 里原来指向 blackbox_exporter 的配置直接改个 IP 指向 ESP32,其他不用动。

实际部署

我把几块 ESP32 分别放在不同的站点,每个站点配置不同的探测目标:

  • 站点 A 的 ESP32 去拨测站点 B、C 的服务
  • 站点 B 的 ESP32 去拨测站点 A、C 的服务
  • 站点 C 同理

这样任意两个站点之间的链路质量都有数据。Grafana 里拉个 Dashboard,延迟、丢包率、HTTP 成功率一目了然。

mermaid
graph LR
    subgraph SiteA["站点 A"]
        EA["ESP32 #1"]
    end
    subgraph SiteB["站点 B"]
        EB["ESP32 #2"]
    end
    subgraph SiteC["站点 C"]
        EC["ESP32 #3"]
    end
    subgraph Monitor["监控"]
        P[Prometheus]
        G[Grafana]
    end

    EA -->|"拨测 B/C"| EB
    EA -->|"拨测 B/C"| EC
    EB -->|"拨测 A/C"| EA
    EB -->|"拨测 A/C"| EC
    EC -->|"拨测 A/B"| EA
    EC -->|"拨测 A/B"| EB

    EA -->|:9090/metrics| P
    EB -->|:9090/metrics| P
    EC -->|:9090/metrics| P
    P --> G

构建 & 烧录

项目基于 ESP-IDF v6.0,提供了几种构建方式:

bash
1
2
3
4
5
6
7
# 推荐:Python 脚本,一条命令搞定
python build.py esp32c3 flash COM3

# 或者直接用 idf.py
idf.py set-target esp32c3
idf.py build
idf.py -p COM3 flash

如果用的是 ESP32-C6,把 esp32c3 换成 esp32c6 就行。

总结

说白了就一句话:不想为跑个 blackbox_exporter 多开一台服务器。ESP32 几块钱一块,功耗不到 1W,USB 充电器供电就行,放哪都不心疼。

项目开源在 GitHub 上,有兴趣可以试试:Mi-Bee-Studio/esp32-blackbox