feat!: probes link to the panel; signed release updates for container nodes [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Probes connect to the panel directly and get the agent list as desired state.
CI signs the image digest of agent and probe releases, the panel forwards the
manifest, and a host updater verifies it with a local public key, pulls by
digest and rolls back when the new version is unhealthy.
This commit is contained in:
justme committed 2026-10-06 22:42:12 +08:00
1 parent 938ca1eb21
commit 993dd5c41c
57 files changed
+1640 -559

No files matched your search

+4 -2
View File
@@ -91,7 +91,7 @@
`GET /api/public/about`:`{show_build, backend}`。`show_build` 关着时 `backend` 是 `null`;打开后 `backend` 是 `{version, tag, built_at}`(后端自己的版本、tag、构建时间,不含提交号)。
- 系统和架构是节点自报的:Agent 在 `fleet.hello` 里,Probe 随列表同步(`monitor.probe.agents` 的 `platform`)告诉 Agent,Agent 再放进心跳的 `probes[]`。只有粗粒度的名字,没有主机名、内核版本、CPU 型号。
- 系统和架构是节点自报的:Agent 和 Probe 都在各自连面板时的 `fleet.hello` 里自报。只有粗粒度的名字,没有主机名、内核版本、CPU 型号。
- 国家和运营商由 **Backend** 用节点的来源地址向 IP 归属服务查一次(`Monitor__GeoLookupUrl`,缺省 `https://ip.007995.xyz`,留空就不查):Probe 用 Agent 看到的来源 IP,Agent 用它的公开地址。**Probe 和 Agent 不访问任何外部服务。** 只查公网地址(回环、内网、CGNAT、链路本地一律不发出去);同一个地址一周内只查一次,失败 10 分钟后再试;查到的只留国家代码和运营商,地址只存在 Backend 内部的 `node_meta.geo_host`,不对外。
### 2.4 测试结果与目录
@@ -135,7 +135,9 @@
| POST | `/api/admin/releases/{app}/{channel}/rollout` | `{version, batches:[{labels?, count?}], soak_minutes}`:分批灰度;只能选择已经签名的清单 |
| POST | `/api/admin/releases/{app}/approve` | 对 `policy = notify` 的节点批准更新 `{version, agents?}` |
| GET/PATCH | `/api/admin/settings` | 站点公开或私有、是否对访客显示构建信息(`site.show_build`)、保留期等 |
| GET | `/api/admin/build-info` | 后端和每个 Agent、Probe 的版本、tag、提交、构建时间、系统(管理端「系统 → 版本」) |
| GET | `/api/admin/build-info` | 后端和每个 Agent、Probe 的版本、tag、提交、构建时间、系统,以及节点宿主机更新器报告的更新状态(管理端「系统 → 版本」) |
| GET/POST | `/api/admin/releases` | CI 签名的发布清单:POST 登记(`{manifest, signature}`,面板不验签、原样转给在线的节点和之后连上的节点),GET 看每个组件最新的一份 |
| POST | `/api/admin/nodes/{id}/update` | 请求一个在线的 Agent 或 Probe 更新到它收到的清单(节点不在线返回 409) |
| GET/POST/PATCH | `/api/admin/users` | 首次启动时通过命令行或环境变量创建初始管理员 |
任务的请求体示例:
+220 -195
View File
@@ -1,195 +1,220 @@
# 部署
节点(Agent、Probe)是单个 NativeAOT 可执行文件,没有运行时依赖,只需要系统的 OpenSSL 3(`libssl.so.3`)。启动时会自检,缺少时第一步就明确报错。
## 1. 构建与打包
```text
scripts/publish.sh <agent|probe|backend|fleet-enroll> <rid> → backend/artifacts/publish/<project>/<rid>/
scripts/package.sh <agent|probe> <rid> → backend/artifacts/packages/monitor-<role>-<version>-<rid>.tar.gz(附 .sha256)
```
构建信息(程序里显示的 tag、提交、构建时间):`publish.sh` 会用 `git describe --tags --always --dirty`、`git rev-parse` 和当前时间自动设置环境变量 `MONITOR_TAG`、`MONITOR_COMMIT`、`MONITOR_BUILT_AT`,直接用 `dotnet publish` 或 CI 构建时自己设置它们(容器的 Fastlane 构建用同名的 `--build-arg`);不设置就是空,本地构建照常可重复。版本号取自 `backend/Directory.Build.props` 的 `Version`。前端的信息由 `web/build-info.mjs` 在构建时写入(版本取自 `web/package.json`)。
| rid | 目标 | 构建方式 |
| --- | --- | --- |
| `linux-x64`、`linux-arm64` | Debian/Ubuntu glibc 服务器 | Windows CI 主机通过 AotAnywhere 交叉编译 |
| `linux-musl-x64`、`linux-musl-arm64` | Alpine、amd64 OpenWrt、aarch64 OpenWrt 路由器 | Windows CI 主机通过 AotAnywhere 交叉编译 |
| `osx-x64`、`osx-arm64` | Intel Mac、Apple Silicon | Windows CI 主机通过 AotAnywhere 交叉编译;发布包未做 Developer ID 签名或 Apple 公证 |
| `win-x64`、`win-arm64` | Windows x64、ARM64 | Windows CI 主机上的 Visual Studio C++ 工具链 |
macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。通过浏览器下载时,macOS Gatekeeper 可能阻止启动;面向普通用户分发前需要 Developer ID 签名和 Apple 公证。MIPS 不在当前目标列表中(见 `project-layout.md`)。
仓库内的 `scripts/publish.sh` 面向本机目标平台;Woodpecker 的 tag 流水线在 Windows runner 上构建 Agent、Probe 和 `fleet-enroll` 的所有发布目标,Backend OCI 镜像仍在 Linux runner 上构建。
## Backend 容器
Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/monitor/monitor-backend:<version>` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器基于 .NET 10 ASP.NET noble-chiseled-extra 运行时镜像,以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。`extra` 变体带 IANA 时区数据,供按时区查询使用。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。
在部署目录保存此仓库的 `compose.yaml`,先通过部署平台的环境变量或密钥管理设置 `MONITOR_ADMIN_PASSWORD`(可选设置 `MONITOR_ADMIN_USER`、`MONITOR_VERSION`),然后启动:
```sh
docker compose up -d
```
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `monitor-data`。首次启动成功创建管理员后,改密应在管理端完成。
健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。
## 节点二进制发布
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux glibc 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,支持 `linux/amd64` 和 `linux/arm64`;musl、macOS、Windows 仍通过对应平台归档分发。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。
## CI 工作流
### Pull request 与 dev 验证
Pull request 和 `dev` push 会运行 Backend tests;通过后,独立的 `web-build` workflow 才会构建前端并检查 CSS。提交信息后缀 `[SKIP TESTS]` 只跳过 Backend tests,前端构建仍会运行。Gitea PR 事件的 `CI_COMMIT_MESSAGE` 是 PR 标题,因此在 PR 中跳过测试时,把后缀写入 PR 标题;普通分支 push 则写在 commit message 中。
### main 开发镜像
`main` 上只有提交信息包含 `[CI]` 的 push 才构建镜像。合并 PR 时,请把 `[CI]` 放入 PR 标题或合并提交标题。Backend、Agent、Probe 的 Linux/amd64 镜像会并行构建,并推送到同一个 Zot Registry 下独立的 `monitor-dev/` 仓库:
| 镜像 | 滚动标签 | 固定提交标签 |
| --- | --- | --- |
| Backend | `registry.oeo.one/monitor-dev/monitor-backend:latest` | `registry.oeo.one/monitor-dev/monitor-backend:dev-<12位SHA>` |
| Agent | `registry.oeo.one/monitor-dev/monitor-agent:latest` | `registry.oeo.one/monitor-dev/monitor-agent:dev-<12位SHA>` |
| Probe | `registry.oeo.one/monitor-dev/monitor-probe:latest` | `registry.oeo.one/monitor-dev/monitor-probe:dev-<12位SHA>` |
Dev 镜像目前仅支持 amd64。它们不创建 Gitea Release,也不修改正式版 `monitor/` 仓库中的标签。普通 main push 不会构建镜像。
### 正式版发布
推送匹配项目版本的 `vMAJOR.MINOR.PATCH` tag 会触发完整跨平台构建、正式 OCI 镜像推送和 Gitea Release。正式镜像使用 `registry.oeo.one/monitor/` 路径;Agent、Probe 提供 amd64 与 arm64 多架构标签,稳定版本更新正式仓库自己的 `latest`。这与 `monitor-dev/` 的 dev `latest` 相互独立。
Agent 和 Probe 镜像分别发布为 `registry.oeo.one/monitor/monitor-agent:<version>` 与 `registry.oeo.one/monitor/monitor-probe:<version>`,包含 amd64、arm64 多架构索引;也可指定 `:<version>-amd64` 或 `:<version>-arm64`。稳定版本同时更新 `latest`。镜像以非 root 用户(UID/GID 1654)运行,默认读取 `/etc/monitor/agent.json` 或 `/etc/monitor/probe.json`。运行容器时挂载对应配置目录;Probe 还会在该目录写入 `agents.json`,因此目录需可由 1654 用户写入。Agent、Probe 需使用宿主网络以保持节点端口和测量流量行为,并按需添加 `NET_RAW` 能力以执行 ICMP 测量:
```sh
docker run -d --name monitor-agent --restart unless-stopped --network host \
--cap-add NET_RAW -v "$PWD/agent-config:/etc/monitor" \
registry.oeo.one/monitor/monitor-agent:<version>
docker run -d --name monitor-probe --restart unless-stopped --network host \
--cap-add NET_RAW -v "$PWD/probe-config:/etc/monitor" \
registry.oeo.one/monitor/monitor-probe:<version>
```
宿主机配置目录应允许 UID/GID 1654 读写;Probe 需要写权限来更新 `agents.json`。也可通过 Docker Compose 配置相同的网络、卷和能力。
实测体积(`StackTraceSupport=false`、`IlcFoldIdenticalMethodBodies=true`、`OptimizationPreference=Size`):
| 程序 | linux-musl-x64 | linux-x64 | win-x64 |
| --- | --- | --- | --- |
| monitor-probe | 7.7 MB | 7.7 MB | 5.6 MB |
| monitor-agent | 8.4 MB | 8.4 MB | 7.3 MB |
| fleet-enroll | 6.4 MB | 6.4 MB | — |
Linux 上比 Windows 大,是因为 .NET 的加密、压缩、网络本机库被静态链接进来。tar.gz 压缩后 Probe 约 3.6 MB。
## 2. 安装
解压 tar.gz,在解压目录里运行 `install.sh`(POSIX sh,root 运行;Debian、Ubuntu、Alpine、OpenWrt 都测试过):
```sh
# Agent(服务器):注册并启动
./install.sh agent --server https://panel.example.com --join <加入令牌> --name hk-1
# Probe(家宽网络里的机器或 OpenWrt 路由器):先在面板生成身份(POST /api/admin/probes,只能下载一次)
./install.sh probe --probe-json ./probe.json
./install.sh uninstall agent|probe [--purge] # --purge 同时删除身份(节点需要重新注册)
```
| 位置 | 内容 | 所有者 |
| --- | --- | --- |
| `/usr/lib/monitor/` | 二进制 | root,0755:采集进程不能改写自己的可执行文件 |
| `/etc/monitor/` | `agent.json` 或 `probe.json`、身份(`node.json`、`node.key`、`tls.pfx`)、`agents.json` | `monitor` 用户,0750 |
安装脚本会:检查 OpenSSL、创建 `monitor` 用户(`useradd`、busybox `adduser`,OpenWrt 没有这些时直接写 `/etc/passwd`)、安装二进制和服务文件;缺少配置时只安装、不启动,并打印下一步。
### systemd(`packaging/systemd/*.service`)
以 `monitor` 用户运行,沙箱:`NoNewPrivileges`、`ProtectSystem=strict`、`ProtectHome`、`PrivateTmp/Devices`、`ProtectKernel*`、`RestrictAddressFamilies=AF_INET AF_INET6`、`RestrictNamespaces`、`MemoryDenyWriteExecute`(NativeAOT 不 JIT)、`SystemCallArchitectures=native`,`MemoryMax`(Agent 128 MB、Probe 64 MB)。
- **Agent** 只保留 `CAP_NET_RAW`(ICMP 目标任务用),`/etc/monitor` 只读;Probe 没有任何 capability,只对 `agents.json` 所在的目录可写。
- 监听 1024 以下的端口(例如把数据面放到 443)需要在 drop-in 里加 `AmbientCapabilities=CAP_NET_BIND_SERVICE` 和对应的 `CapabilityBoundingSet`。默认端口是 7443/tcp+udp(Probe 控制通道和 UDP 数据面)、7444/tcp。
### OpenWrt(`packaging/openwrt/`)
只支持 Probe(Agent 面向服务器)。`install.sh probe` 安装 `/etc/init.d/monitor-probe`(procd,崩溃自动重启,日志进 logd)并写入 `/lib/upgrade/keep.d/monitor`,sysupgrade 时保留 `/etc/monitor/`。
OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 24.10 之后用 `apk add libopenssl3`)。闪存很小时注意:二进制 7.7 MB,配置只在 Agent 列表变化时才写。路由器上建议把 `probe.json` 的 `limits.max_test_mbps` 调低。
## 3. 验证
`scripts/smoke-linux.sh` 在 Linux 容器里用发布的二进制跑完整链路(后端 → 注册 → Agent → Probe → 数据落库),`MODE=install` 用打包产物和 `install.sh` 安装、以 `monitor` 用户运行。`scripts/smoke-systemd.sh` 在 systemd 容器里验证服务单元。已经验证过:
- Alpine 3.22(musl)、Debian 12(glibc):直接运行和 `install.sh` 安装都通;
- OpenWrt(官方 rootfs,busybox,无 `install`/`useradd`/`su`):安装、建用户、卸载、二进制启动和 OpenSSL 自检;
- systemd(Debian 12):两个服务 active,Agent 只有 `CAP_NET_RAW`、Probe 没有 capability,`NoNewPrivs`、seccomp 生效,Agent 无法写 `/etc/monitor`,没有重启和错误日志。
没有验证的:arm64 构建、真正的 OpenWrt 设备(有 procd 和真实的内存/闪存限制)、跨公网的真实线路。
实测常驻内存(RSS,回环,默认模板,稳定后):Linux 上 Probe 约 21 MB、Agent 约 23 MB;Windows 上约 22 MB、25 MB。
### 跨机器的真实链路(Windows Probe → WSL2 里的 Agent 和 Backend)
`scripts/smoke-wsl.py` 用 Windows 上的 NativeAOT Probe 连 WSL2 里的 Agent(真实的虚拟网络,两端的单调时钟原点相差约 38 小时),并用 `tc netem` 在 WSL 里注入故障。结果(Probe 的 RTT 基线约 0.4 ms):
| 注入 | 期望 | 实测 |
| --- | --- | --- |
| Agent→Probe 方向 10% 丢包、30 ms ± 5 ms 延迟(egress netem,只匹配 7443/7444) | 丢包在下行,下行抖动变大 | `loss.down` 8.2%、`loss.up` 0;`jitter.down.p95` 8.7 ms、`jitter.up.p95` 0.18 ms;RTT +31 ms;TCP:`tcp.rtt.p99` 1.2 s、`tcp.slow` 28.8%、`tcp.retrans` 5.9%(Agent 一侧内核计数) |
| Probe→Agent 方向 10% 丢包、20 ms ± 3 ms 延迟(ingress + ifb) | 丢包在上行 | `loss.up` 9.4%、`loss.down` 0;`jitter.up.p95` 5.2 ms、`jitter.down.p95` 0 |
| 8 秒黑洞(tc ingress drop 7443/7444) | UDP 连续丢包 ~80 个;TCP 不丢包,只有几秒的长尾 | UDP 最长连续丢包 80(10 Hz × 8 s);`tcp.rtt.max` 9.5 s、`tcp.stall_max` 9.6 s |
| `hy2_ladder`(10/25/50/100 Mbit/s) | 无丢包 | `train.loss` 0.006%、`train.jitter.p95` 0.64 ms、可持续 100 Mbit/s |
| 杀掉 Agent / 杀掉 Probe 再启动 | 自动恢复 | Agent 重启后 2.1 s 恢复在线并重新连上 Probe;Probe 重启后 0.5 s 重新连上 |
验证中发现并修掉的两个问题:
- **Probe 重启后,Agent 沿用了上一个会话的记录**:Probe 重启之后序号从 0 开始,Agent 里同一个序号的旧 t2/t3 还在,于是真正的上行丢失被算成了下行丢失(黑洞测试里 `loss.up` 0、`loss.down` 7.4%)。现在新的 Probe 会话开始时 Agent 清空所有流的记录。
- **TCP 回显的几秒中断被记成丢包**:Probe 像 UDP 那样等 2 秒就把没有回复的行交出去,TCP 随后才到的回复没有行可以回填,最严重的长尾反而在 RTT 里看不到(黑洞期间 `tcp.rtt.max` 只有 4 ms)。现在 TCP 的行等回复,30 秒仍然没有才当作连接已断(t4 缺失)。
没有覆盖的:Windows 上的 Agent(需要给 Windows 防火墙加入站规则)、NAT 重绑定。跨公网的线路见下一节。
### 跨公网的真实线路(广东移动家宽 → 日本 IIJ 的 VPS)
2026-10-03 在一条真实线路上试跑过:家宽机器(Debian 13,用户态运行,没有公网 IPv4 入站)跑 Probe 和 Backend,VPS(Debian 12,1 核 470 MB,上面本来就跑着 xray、hysteria2、caddy 等服务)跑 Agent。
```text
浏览器 / 本机前端 ──► https://<公网域名> ──隧道(Cloudflare Tunnel 之类)──► 家宽机器: Backend (:29181)
家宽机器: Probe ──TCP/UDP 7443、TCP 7444──► VPS: Agent
VPS: Agent ──WSS 到 https://<公网域名>──► Backend
```
做法和要点:
- **Backend 通过隧道暴露**:隧道把一个公网域名转到家宽机器的 `29181`;Backend 监听 `http://0.0.0.0:29181`(只写一个监听地址,同时写 `0.0.0.0` 和 `[::]` 会报地址已被占用),家宽路由器的防火墙不放行这个端口,公网只能经隧道进来。Agent 用 `fleet-enroll --server https://<公网域名>` 注册,之后用 WSS 连回 Backend。
- **经隧道注册之后,检查 Agent 的 `public_host`**:Backend 看到的来源地址是隧道的出口,不是 VPS 的地址,要改成 VPS 的真实地址,Probe 才连得上。
- **先注册 Agent,再创建 Probe**:`probe.json` 的 `bootstrap` 里才有这个 Agent;Probe 启动后会通过 Agent 同步列表。
- **小 VPS 把 `limits.max_total_mbps` 调低**(试跑时用 100);Agent 常驻约 23 MB,VPS 上原有的服务不受影响,结束后删除 `/opt/…`、停掉进程即可,没有留下任何东西。
- 家宽的 IPv6 前缀会在路由器重新拨号(比如停电)后变化,SSH 地址要跟着换;面板走隧道的域名,不受影响。
- 本地电脑没有直连 VPS 的路径时,用家宽机器当跳板(`SSH_ASKPASS` + `SSH_ASKPASS_REQUIRE=force`,密码放环境变量,不写进文件)。
**常驻(开机自启)**:
- 家宽机器(没有 root 时):`~/.config/systemd/user/monitor-backend.service` 和 `monitor-probe.service`,`EnvironmentFile` 指向放配置的 `backend.env`,`Restart=always`,`systemctl --user enable --now …`,再用 `sudo loginctl enable-linger <用户>` 让它们不登录也能起来。Probe 的命令行是 `monitor-probe --config <probe.json 路径>`(默认路径是 `/etc/monitor/probe.json`)。
- VPS:`/etc/systemd/system/monitor-agent.service`,`Restart=always`,加 `NoNewPrivileges`、`ProtectSystem=strict`、`ReadWritePaths=/opt/monitor`、`ProtectHome`、`PrivateTmp`。这是不走安装脚本的手动方式;正式安装用上面的 `packaging/systemd`。
- 日志进 journal:`journalctl -u monitor-agent`、`journalctl --user -u monitor-probe`。重启 Backend 用 `systemctl` 而不是手动再起一个进程,否则会端口冲突。
**国家和运营商**:Backend 用 Probe、Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`Monitor__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Probe、Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。
**从别处看这个面板**:本机跑前端,在页面里把数据源指向这个域名,用 API 令牌访问,Backend 设 `Monitor__CorsOrigins` 放行前端的来源(见 [web/README.md](../web/README.md))。
实测(Probe 在广东移动家宽,Agent 在日本 IIJ):
| 项目 | 结果 |
| --- | --- |
| 延迟 p50 / p95 / p99 | 107 / 107 / 114 ms |
| 丢包 | 上行 0%,下行 8.1%(`hy2_ladder` 在 10、25、50、100 Mbit/s 四档下的丢包都是 8~10%,与速率无关:随机丢包,不是限速) |
| 抖动 p95(上 / 下) | 0.9 / 5.2 ms |
| TCP | 重传 1.9%(批量传输时 5.8%),RTT 最大 412 ms |
| `reality_session` | 建连成功率 100%,TCP 建连 p95 115 ms,TLS 握手 p95 117 ms,30 秒长连接无重置;4 秒批量传输峰值 133 Mbit/s、中位数 90 Mbit/s(Agent 上限 100) |
| 评分 | REALITY 82(含按需测试)、Hy2 42 |
| Agent 主机状态 | CPU 1.5%,内存 274 / 493 MB,已运行约 20.5 天 |
这次试跑发现并修掉的问题:`hy2_ladder` 的阶梯按「块」而不是按「阶段」计算,真实的 Probe 把一个阶段切成几块,4 个阶段出了 7 步(模拟数据不切块,所以之前的测试没有发现);现在同一阶段的块先合并。
# 部署
节点(Agent、Probe)是单个 NativeAOT 可执行文件,没有运行时依赖,只需要系统的 OpenSSL 3(`libssl.so.3`)。启动时会自检,缺少时第一步就明确报错。
## 1. 构建与打包
```text
scripts/publish.sh <agent|probe|backend|fleet-enroll> <rid> → backend/artifacts/publish/<project>/<rid>/
scripts/package.sh <agent|probe> <rid> → backend/artifacts/packages/monitor-<role>-<version>-<rid>.tar.gz(附 .sha256)
```
构建信息(程序里显示的 tag、提交、构建时间):`publish.sh` 会用 `git describe --tags --always --dirty`、`git rev-parse` 和当前时间自动设置环境变量 `MONITOR_TAG`、`MONITOR_COMMIT`、`MONITOR_BUILT_AT`,直接用 `dotnet publish` 或 CI 构建时自己设置它们(容器的 Fastlane 构建用同名的 `--build-arg`);不设置就是空,本地构建照常可重复。版本号取自 `backend/Directory.Build.props` 的 `Version`。前端的信息由 `web/build-info.mjs` 在构建时写入(版本取自 `web/package.json`)。
| rid | 目标 | 构建方式 |
| --- | --- | --- |
| `linux-x64`、`linux-arm64` | Debian/Ubuntu glibc 服务器 | Windows CI 主机通过 AotAnywhere 交叉编译 |
| `linux-musl-x64`、`linux-musl-arm64` | Alpine、amd64 OpenWrt、aarch64 OpenWrt 路由器 | Windows CI 主机通过 AotAnywhere 交叉编译 |
| `osx-x64`、`osx-arm64` | Intel Mac、Apple Silicon | Windows CI 主机通过 AotAnywhere 交叉编译;发布包未做 Developer ID 签名或 Apple 公证 |
| `win-x64`、`win-arm64` | Windows x64、ARM64 | Windows CI 主机上的 Visual Studio C++ 工具链 |
macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。通过浏览器下载时,macOS Gatekeeper 可能阻止启动;面向普通用户分发前需要 Developer ID 签名和 Apple 公证。MIPS 不在当前目标列表中(见 `project-layout.md`)。
仓库内的 `scripts/publish.sh` 面向本机目标平台;Woodpecker 的 tag 流水线在 Windows runner 上构建 Agent、Probe 和 `fleet-enroll` 的所有发布目标,Backend OCI 镜像仍在 Linux runner 上构建。
## Backend 容器
Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/monitor/monitor-backend:<version>` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器基于 .NET 10 ASP.NET noble-chiseled-extra 运行时镜像,以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。`extra` 变体带 IANA 时区数据,供按时区查询使用。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。
在部署目录保存此仓库的 `compose.yaml`,先通过部署平台的环境变量或密钥管理设置 `MONITOR_ADMIN_PASSWORD`(可选设置 `MONITOR_ADMIN_USER`、`MONITOR_VERSION`),然后启动:
```sh
docker compose up -d
```
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `monitor-data`。首次启动成功创建管理员后,改密应在管理端完成。
健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。
## 节点二进制发布
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux glibc 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,支持 `linux/amd64` 和 `linux/arm64`;musl、macOS、Windows 仍通过对应平台归档分发。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。
## CI 工作流
### Pull request 与 dev 验证
Pull request 和 `dev` push 会运行 Backend tests;通过后,独立的 `web-build` workflow 才会构建前端并检查 CSS。提交信息后缀 `[SKIP TESTS]` 只跳过 Backend tests,前端构建仍会运行。Gitea PR 事件的 `CI_COMMIT_MESSAGE` 是 PR 标题,因此在 PR 中跳过测试时,把后缀写入 PR 标题;普通分支 push 则写在 commit message 中。
### main 开发镜像
`main` 上只有提交信息包含 `[CI]` 的 push 才构建镜像。合并 PR 时,请把 `[CI]` 放入 PR 标题或合并提交标题。Backend、Agent、Probe 的 Linux/amd64 镜像会并行构建,并推送到同一个 Zot Registry 下独立的 `monitor-dev/` 仓库:
| 镜像 | 滚动标签 | 固定提交标签 |
| --- | --- | --- |
| Backend | `registry.oeo.one/monitor-dev/monitor-backend:latest` | `registry.oeo.one/monitor-dev/monitor-backend:dev-<12位SHA>` |
| Agent | `registry.oeo.one/monitor-dev/monitor-agent:latest` | `registry.oeo.one/monitor-dev/monitor-agent:dev-<12位SHA>` |
| Probe | `registry.oeo.one/monitor-dev/monitor-probe:latest` | `registry.oeo.one/monitor-dev/monitor-probe:dev-<12位SHA>` |
Dev 镜像目前仅支持 amd64。它们不创建 Gitea Release,也不修改正式版 `monitor/` 仓库中的标签。普通 main push 不会构建镜像。
### 正式版发布
推送匹配项目版本的 `vMAJOR.MINOR.PATCH` tag 会触发完整跨平台构建、正式 OCI 镜像推送和 Gitea Release。正式镜像使用 `registry.oeo.one/monitor/` 路径;Agent、Probe 提供 amd64 与 arm64 多架构标签,稳定版本更新正式仓库自己的 `latest`。这与 `monitor-dev/` 的 dev `latest` 相互独立。
Agent 和 Probe 镜像分别发布为 `registry.oeo.one/monitor/monitor-agent:<version>` 与 `registry.oeo.one/monitor/monitor-probe:<version>`,包含 amd64、arm64 多架构索引;也可指定 `:<version>-amd64` 或 `:<version>-arm64`。稳定版本同时更新 `latest`。镜像以非 root 用户(UID/GID 1654)运行,默认读取 `/etc/monitor/agent.json` 或 `/etc/monitor/probe.json`。运行容器时挂载对应配置目录;Probe 还会在该目录写入 `agents.json`,因此目录需可由 1654 用户写入。Agent、Probe 需使用宿主网络以保持节点端口和测量流量行为,并按需添加 `NET_RAW` 能力以执行 ICMP 测量:
```sh
docker run -d --name monitor-agent --restart unless-stopped --network host \
--cap-add NET_RAW -v "$PWD/agent-config:/etc/monitor" \
registry.oeo.one/monitor/monitor-agent:<version>
docker run -d --name monitor-probe --restart unless-stopped --network host \
--cap-add NET_RAW -v "$PWD/probe-config:/etc/monitor" \
registry.oeo.one/monitor/monitor-probe:<version>
```
宿主机配置目录应允许 UID/GID 1654 读写;Probe 需要写权限来更新 `agents.json`。也可通过 Docker Compose 配置相同的网络、卷和能力。
实测体积(`StackTraceSupport=false`、`IlcFoldIdenticalMethodBodies=true`、`OptimizationPreference=Size`):
| 程序 | linux-musl-x64 | linux-x64 | win-x64 |
| --- | --- | --- | --- |
| monitor-probe | 7.7 MB | 7.7 MB | 5.6 MB |
| monitor-agent | 8.4 MB | 8.4 MB | 7.3 MB |
| fleet-enroll | 6.4 MB | 6.4 MB | — |
Linux 上比 Windows 大,是因为 .NET 的加密、压缩、网络本机库被静态链接进来。tar.gz 压缩后 Probe 约 3.6 MB。
## 2. 安装
解压 tar.gz,在解压目录里运行 `install.sh`(POSIX sh,root 运行;Debian、Ubuntu、Alpine、OpenWrt 都测试过):
```sh
# Agent(服务器):注册并启动
./install.sh agent --server https://panel.example.com --join <加入令牌> --name hk-1
# Probe(家宽网络里的机器或 OpenWrt 路由器):先在面板生成身份(POST /api/admin/probes,只能下载一次)
./install.sh probe --probe-json ./probe.json
./install.sh uninstall agent|probe [--purge] # --purge 同时删除身份(节点需要重新注册)
```
| 位置 | 内容 | 所有者 |
| --- | --- | --- |
| `/usr/lib/monitor/` | 二进制 | root,0755:采集进程不能改写自己的可执行文件 |
| `/etc/monitor/` | `agent.json` 或 `probe.json`、身份(`node.json`、`node.key`、`tls.pfx`)、`agents.json` | `monitor` 用户,0750 |
安装脚本会:检查 OpenSSL、创建 `monitor` 用户(`useradd`、busybox `adduser`,OpenWrt 没有这些时直接写 `/etc/passwd`)、安装二进制和服务文件;缺少配置时只安装、不启动,并打印下一步。
### systemd(`packaging/systemd/*.service`)
以 `monitor` 用户运行,沙箱:`NoNewPrivileges`、`ProtectSystem=strict`、`ProtectHome`、`PrivateTmp/Devices`、`ProtectKernel*`、`RestrictAddressFamilies=AF_INET AF_INET6`、`RestrictNamespaces`、`MemoryDenyWriteExecute`(NativeAOT 不 JIT)、`SystemCallArchitectures=native`,`MemoryMax`(Agent 128 MB、Probe 64 MB)。
- **Agent** 只保留 `CAP_NET_RAW`(ICMP 目标任务用),`/etc/monitor` 只读;Probe 没有任何 capability,只对 `agents.json` 所在的目录可写。
- 监听 1024 以下的端口(例如把数据面放到 443)需要在 drop-in 里加 `AmbientCapabilities=CAP_NET_BIND_SERVICE` 和对应的 `CapabilityBoundingSet`。默认端口是 7443/tcp+udp(Probe 控制通道和 UDP 数据面)、7444/tcp。
### OpenWrt(`packaging/openwrt/`)
只支持 Probe(Agent 面向服务器)。`install.sh probe` 安装 `/etc/init.d/monitor-probe`(procd,崩溃自动重启,日志进 logd)并写入 `/lib/upgrade/keep.d/monitor`,sysupgrade 时保留 `/etc/monitor/`。
OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 24.10 之后用 `apk add libopenssl3`)。闪存很小时注意:二进制 7.7 MB,配置只在 Agent 列表变化时才写。路由器上建议把 `probe.json` 的 `limits.max_test_mbps` 调低。
## 3. 验证
`scripts/smoke-linux.sh` 在 Linux 容器里用发布的二进制跑完整链路(后端 → 注册 → Agent → Probe → 数据落库),`MODE=install` 用打包产物和 `install.sh` 安装、以 `monitor` 用户运行。`scripts/smoke-systemd.sh` 在 systemd 容器里验证服务单元。已经验证过:
- Alpine 3.22(musl)、Debian 12(glibc):直接运行和 `install.sh` 安装都通;
- OpenWrt(官方 rootfs,busybox,无 `install`/`useradd`/`su`):安装、建用户、卸载、二进制启动和 OpenSSL 自检;
- systemd(Debian 12):两个服务 active,Agent 只有 `CAP_NET_RAW`、Probe 没有 capability,`NoNewPrivs`、seccomp 生效,Agent 无法写 `/etc/monitor`,没有重启和错误日志。
没有验证的:arm64 构建、真正的 OpenWrt 设备(有 procd 和真实的内存/闪存限制)、跨公网的真实线路。
实测常驻内存(RSS,回环,默认模板,稳定后):Linux 上 Probe 约 21 MB、Agent 约 23 MB;Windows 上约 22 MB、25 MB。
### 跨机器的真实链路(Windows Probe → WSL2 里的 Agent 和 Backend)
`scripts/smoke-wsl.py` 用 Windows 上的 NativeAOT Probe 连 WSL2 里的 Agent(真实的虚拟网络,两端的单调时钟原点相差约 38 小时),并用 `tc netem` 在 WSL 里注入故障。结果(Probe 的 RTT 基线约 0.4 ms):
| 注入 | 期望 | 实测 |
| --- | --- | --- |
| Agent→Probe 方向 10% 丢包、30 ms ± 5 ms 延迟(egress netem,只匹配 7443/7444) | 丢包在下行,下行抖动变大 | `loss.down` 8.2%、`loss.up` 0;`jitter.down.p95` 8.7 ms、`jitter.up.p95` 0.18 ms;RTT +31 ms;TCP:`tcp.rtt.p99` 1.2 s、`tcp.slow` 28.8%、`tcp.retrans` 5.9%(Agent 一侧内核计数) |
| Probe→Agent 方向 10% 丢包、20 ms ± 3 ms 延迟(ingress + ifb) | 丢包在上行 | `loss.up` 9.4%、`loss.down` 0;`jitter.up.p95` 5.2 ms、`jitter.down.p95` 0 |
| 8 秒黑洞(tc ingress drop 7443/7444) | UDP 连续丢包 ~80 个;TCP 不丢包,只有几秒的长尾 | UDP 最长连续丢包 80(10 Hz × 8 s);`tcp.rtt.max` 9.5 s、`tcp.stall_max` 9.6 s |
| `hy2_ladder`(10/25/50/100 Mbit/s) | 无丢包 | `train.loss` 0.006%、`train.jitter.p95` 0.64 ms、可持续 100 Mbit/s |
| 杀掉 Agent / 杀掉 Probe 再启动 | 自动恢复 | Agent 重启后 2.1 s 恢复在线并重新连上 Probe;Probe 重启后 0.5 s 重新连上 |
验证中发现并修掉的两个问题:
- **Probe 重启后,Agent 沿用了上一个会话的记录**:Probe 重启之后序号从 0 开始,Agent 里同一个序号的旧 t2/t3 还在,于是真正的上行丢失被算成了下行丢失(黑洞测试里 `loss.up` 0、`loss.down` 7.4%)。现在新的 Probe 会话开始时 Agent 清空所有流的记录。
- **TCP 回显的几秒中断被记成丢包**:Probe 像 UDP 那样等 2 秒就把没有回复的行交出去,TCP 随后才到的回复没有行可以回填,最严重的长尾反而在 RTT 里看不到(黑洞期间 `tcp.rtt.max` 只有 4 ms)。现在 TCP 的行等回复,30 秒仍然没有才当作连接已断(t4 缺失)。
没有覆盖的:Windows 上的 Agent(需要给 Windows 防火墙加入站规则)、NAT 重绑定。跨公网的线路见下一节。
### 跨公网的真实线路(广东移动家宽 → 日本 IIJ 的 VPS)
2026-10-03 在一条真实线路上试跑过:家宽机器(Debian 13,用户态运行,没有公网 IPv4 入站)跑 Probe 和 Backend,VPS(Debian 12,1 核 470 MB,上面本来就跑着 xray、hysteria2、caddy 等服务)跑 Agent。
```text
浏览器 / 本机前端 ──► https://<公网域名> ──隧道(Cloudflare Tunnel 之类)──► 家宽机器: Backend (:29181)
家宽机器: Probe ──WSS 到 https://<公网域名>──► Backend(身份、Agent 列表)
家宽机器: Probe ──TCP/UDP 7443、TCP 7444──► VPS: Agent
VPS: Agent ──WSS 到 https://<公网域名>──► Backend
```
做法和要点:
- **Backend 通过隧道暴露**:隧道把一个公网域名转到家宽机器的 `29181`;Backend 监听 `http://0.0.0.0:29181`(只写一个监听地址,同时写 `0.0.0.0` 和 `[::]` 会报地址已被占用),家宽路由器的防火墙不放行这个端口,公网只能经隧道进来。Agent 用 `fleet-enroll --server https://<公网域名>` 注册,之后用 WSS 连回 Backend。
- **经隧道注册之后,检查 Agent 的 `public_host`**:Backend 看到的来源地址是隧道的出口,不是 VPS 的地址,要改成 VPS 的真实地址,Probe 才连得上。
- **Probe 直连面板**:`probe.json` 里的 `server` 就是面板地址(取自 `Monitor__PublicUrl`,没配就用创建它时的请求地址),Probe 用它连面板,Agent 列表由面板下发;先注册还是先创建 Probe 都可以。
- **小 VPS 把 `limits.max_total_mbps` 调低**(试跑时用 100);Agent 常驻约 23 MB,VPS 上原有的服务不受影响,结束后删除 `/opt/…`、停掉进程即可,没有留下任何东西。
- 家宽的 IPv6 前缀会在路由器重新拨号(比如停电)后变化,SSH 地址要跟着换;面板走隧道的域名,不受影响。
- 本地电脑没有直连 VPS 的路径时,用家宽机器当跳板(`SSH_ASKPASS` + `SSH_ASKPASS_REQUIRE=force`,密码放环境变量,不写进文件)。
**常驻(开机自启)**:
- 家宽机器(没有 root 时):`~/.config/systemd/user/monitor-backend.service` 和 `monitor-probe.service`,`EnvironmentFile` 指向放配置的 `backend.env`,`Restart=always`,`systemctl --user enable --now …`,再用 `sudo loginctl enable-linger <用户>` 让它们不登录也能起来。Probe 的命令行是 `monitor-probe --config <probe.json 路径>`(默认路径是 `/etc/monitor/probe.json`)。
- VPS:`/etc/systemd/system/monitor-agent.service`,`Restart=always`,加 `NoNewPrivileges`、`ProtectSystem=strict`、`ReadWritePaths=/opt/monitor`、`ProtectHome`、`PrivateTmp`。这是不走安装脚本的手动方式;正式安装用上面的 `packaging/systemd`。
- 日志进 journal:`journalctl -u monitor-agent`、`journalctl --user -u monitor-probe`。重启 Backend 用 `systemctl` 而不是手动再起一个进程,否则会端口冲突。
**国家和运营商**:Backend 用 Probe、Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`Monitor__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Probe、Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。
**从别处看这个面板**:本机跑前端,在页面里把数据源指向这个域名,用 API 令牌访问,Backend 设 `Monitor__CorsOrigins` 放行前端的来源(见 [web/README.md](../web/README.md))。
实测(Probe 在广东移动家宽,Agent 在日本 IIJ):
| 项目 | 结果 |
| --- | --- |
| 延迟 p50 / p95 / p99 | 107 / 107 / 114 ms |
| 丢包 | 上行 0%,下行 8.1%(`hy2_ladder` 在 10、25、50、100 Mbit/s 四档下的丢包都是 8~10%,与速率无关:随机丢包,不是限速) |
| 抖动 p95(上 / 下) | 0.9 / 5.2 ms |
| TCP | 重传 1.9%(批量传输时 5.8%),RTT 最大 412 ms |
| `reality_session` | 建连成功率 100%,TCP 建连 p95 115 ms,TLS 握手 p95 117 ms,30 秒长连接无重置;4 秒批量传输峰值 133 Mbit/s、中位数 90 Mbit/s(Agent 上限 100) |
| 评分 | REALITY 82(含按需测试)、Hy2 42 |
| Agent 主机状态 | CPU 1.5%,内存 274 / 493 MB,已运行约 20.5 天 |
这次试跑发现并修掉的问题:`hy2_ladder` 的阶梯按「块」而不是按「阶段」计算,真实的 Probe 把一个阶段切成几块,4 个阶段出了 7 步(模拟数据不切块,所以之前的测试没有发现);现在同一阶段的块先合并。␍
## 容器节点的更新(Agent、Probe)
面板通知、节点验签、宿主机更新,**面板即使被攻破也装不上假镜像**:
```text
CI 构建并推镜像(Zot)──► ci/release-manifest.sh:对镜像 digest 的清单签名(ECDSA P-256,私钥只在 Woodpecker 密钥 release_signing_key 里)
└─ POST /api/admin/releases ──► 面板:只存下并原样转发(不验签)──► 在线的 Agent、Probe(连上时补发)
节点进程:只把清单和签名写进数据目录的 update/,不执行任何命令
│ systemd.path 监视到 offer.sig
▼
宿主机 monitor-update(root):复制到临时目录 → 用本机的发布公钥验签 → 核对组件和镜像仓库 → seq 必须比已安装的大
→ status:available / invalid(写进 update-status/,节点随心跳报给面板,管理端「版本」页显示)
→ 管理员点「更新」(或本机配置 POLICY=auto)→ podman pull <仓库>@sha256:<digest>(内容寻址,Registry 或链路上换不了)
→ 写 IMAGE=… 到 /etc/monitor-update/<组件>.image,重启容器单元;20 秒内没有健康就换回旧镜像(rolled_back)
```
- **安装更新器**(每个容器节点一次,root):`packaging/update/install-updater.sh agent --data-dir /opt/monitor/etc --image-repo registry.oeo.one/monitor/monitor-agent --unit monitor-agent-container.service [--policy auto] [--authfile <Registry 认证文件>]`。它装好 `monitor-update` 脚本和发布公钥,写 `/etc/monitor-update/<组件>.conf`,启用两个 systemd 路径单元。容器单元要用 `EnvironmentFile=/etc/monitor-update/<组件>.image` 和 `${IMAGE}` 启动容器。
- **镜像仓库、服务名、公钥都在本机配置里**,清单里写什么都不会改变拉取哪个仓库;清单被篡改、签名对不上、组件或仓库不符、`seq` 不比已安装的大(重放旧清单),都会被拒绝,状态是 `invalid`。
- **业务进程不能改二进制,也不执行命令**:它只能往 `update/` 里放清单和请求;状态在 root 拥有的 `update-status/` 里(只读)。
- **CI 的配置**:Woodpecker 密钥 `release_signing_key`(发布私钥,PEM)、`release_api_token`(面板的 API 令牌,管理员权限,用来登记清单);环境变量 `RELEASE_API_URL`(面板地址)。没有 `release_signing_key` 就只构建镜像、不发清单。
- **发布公钥**在 `packaging/update/release-key.pub.pem`;私钥不入库,请离线备份。更换密钥:节点上换新公钥之后再用新私钥发布。
- 纯二进制的安装方式(`fleet-updater`、`fleet-release`,见 [fleet/update.md](fleet/update.md))还没有实现;容器方式优先。
+11 -12
View File
@@ -7,7 +7,7 @@
| 主体 | 凭据 | 生成方 | Backend 保存 |
| --- | --- | --- | --- |
| Agent | Fleet 节点身份(ECDSA P-256)+ 面向 Probe 的 TLS 证书 | `fleet-enroll` | 公钥、SPKI 指纹 |
| Probe | Fleet 节点身份 | 面板生成 `probe.json`(可以有多个 Probe,每个独立吊销) | 公钥 |
| Probe | Fleet 节点身份(同时用于连面板和连 Agent) | 面板生成 `probe.json`(可以有多个 Probe,每个独立吊销) | 公钥 |
| 面板用户 | 账号 + 密码(PBKDF2-SHA256) | 面板 | 密码哈希 |
| API 令牌 | 随机高熵令牌,作用域为 `read` 或 `admin` | 面板 | SHA-256 |
| 发布签名 | 离线的 ECDSA 发布密钥 | CI 或离线环境 | 只在节点的 `fleet-updater` 中配置公钥,**面板不保存** |
@@ -30,7 +30,7 @@ install.sh(只做安装,不包含任何远程控制功能)
面板收到 OnEnrolled(Monitor 挂钩)
├─ public_host 未提供时,取注册请求的来源 IP
├─ 按“线路任务模板”为该 Agent 计算有效任务集(TaskSet)
└─ 通知 Probe(通过 monitor.probe.agents_changed)→ Probe 自动连接新 Agent
└─ 面板把新的 Agent 列表作为期望状态推给所有 Probe → Probe 自动连接新 Agent
从安装到开始出数据,全程不需要手工配置
```
@@ -38,21 +38,20 @@ install.sh(只做安装,不包含任何远程控制功能)
## 3. Probe 身份与 Agent 列表同步
Probe 不认识 Backend,它需要的 Agent 列表经由任意一个已连接的 Agent 获取。
Probe 直接连面板(Fleet 长连接,身份是下面生成的密钥),Agent 列表由面板作为期望状态下发;测量数据仍然只走 Probe → Agent。
```text
面板:POST /api/admin/probes {name}
└─ 生成 Fleet 节点身份,返回 probe.json(只能下载一次;轮换密钥 /rotate 会使旧密钥失效,吊销 /revoke 会让所有 Agent 断开它)
{ probe_id, private_key, bootstrap: [ {agent_id, host, tcp_port, spki}, ... ],
└─ 生成 Fleet 节点身份,返回 probe.json(只能下载一次;轮换密钥 /rotate 会使旧密钥失效,吊销 /revoke 会让面板和所有 Agent 断开它)
{ probe_id, private_key, server: "https://panel...",
limits: { max_test_mbps, max_total_pps } } ← 本地硬上限,面板无法修改;在路由器上建议调低
Probe 启动
├─ 依次尝试 bootstrap 中的 Agent,与其中一个建立 Fleet 点对点会话(peer 用途签名)
├─ monitor.probe.agents ──> Agent ──> Backend
│ <── { version, agents: [ {agent_id, host, tcp_port, udp_ports, spki}... ] }
├─ 调和:新增的 Agent → 建立会话;被移除或禁用的 → 断开
└─ 之后列表的任何变化,都通过已连接的 Agent 推送 monitor.probe.agents_changed;
Probe 用 Fleet.Hosting.ConfigStore 把最新列表写回 bootstrap(这是配置,不是测量数据)
├─ 连接 server:Fleet 长连接(用 probe_id 和私钥认证),fleet.hello 带上系统、架构和构建信息
├─ 面板推送期望状态 probe_agents:{ version, agents: [ {agent_id, host, tcp_port, udp_ports, spki}... ] }
├─ 调和:新增的 Agent → 建立点对点会话(peer 用途签名);被移除或禁用的 → 断开
└─ 之后列表的任何变化(Agent 注册、禁用、地址或证书变化),面板都重新推送同一种期望状态;
Probe 用 Fleet.Hosting.ConfigStore 把最新列表写回 agents.json(这是配置,不是测量数据;面板暂时连不上时,Probe 靠它继续测量)
```
- 每个 Agent 通过自己的期望状态(`TaskSet.peers`)获得 Probe 的公钥,用来验证点对点会话的签名。
@@ -70,7 +69,7 @@ Probe 启动
"allowed_target_cidrs": [],
"max_concurrent_tests": 1
},
"fleet_policy": { "allow_methods": ["monitor.test.run", "monitor.test.cancel", "monitor.probe.agents_changed"] }
"fleet_policy": { "allow_methods": ["monitor.test.run", "monitor.test.cancel"] }
}
```
+2
View File
@@ -1,5 +1,7 @@
# Fleet:通用更新(Fleet.Update / fleet-updater)
> **实现状态**:容器节点(Agent、Probe)的更新已经实现,见 [deployment.md](../deployment.md) 的「容器节点的更新」:同样的信任模型(离线发布密钥签名、本机配置公钥和仓库、`seq` 防重放、面板只通知和请求),清单里是镜像 digest,更新器是宿主机上的 `monitor-update`。本文下面描述的纯二进制方案(`Fleet.Update`、`fleet-updater`、`fleet-release`、健康信标)还没有实现。
## 1. 原则
- **与产品无关**:`fleet-updater` 通过本地配置声明它管理哪些应用,一台机器上可以同时管理多个产品。
+1 -2
View File
@@ -155,13 +155,12 @@ Agent Program.cs
peers = FleetLink.PeerListener(cfg.Listen, identity, ProbeRpcContext.Default) Probe 会话
host = new ModuleHost(log)
├─ link、peers (Fleet)
├─ ProbeDirectoryRelay 转发 monitor.probe.agents / agents_changed
├─ UdpMux / TcpAcceptor UDP/TCP/TLS 数据端口(Measurement 共享),按 (session, stream) 分发给监听方积木;AgentAdmission 提供重放、限速
├─ ChunkModule 合并 Probe 与本端的列 → 切块 → link.Outbox
└─ TestModule 按需测试的阶段编排
Probe Program.cs
├─ AgentDirectoryModule 维护 Agent 列表(bootstrap → monitor.probe.agents → agents_changed)
├─ FleetLink(到面板) 身份、hello、期望状态 probe_agents(Agent 列表)
├─ PeerPool(Fleet) 每个 Agent 一个 FleetLink.ToPeer:认证、重连
├─ TaskReconciler 每个会话各自的期望状态(与 Agent 相同的代码)
├─ ColumnShipper 本端的列通过会话的 Outbox 发送(二进制通道 17)
+1 -5
View File
@@ -11,7 +11,7 @@ Monitor 的两条控制链路都建立在 Fleet 之上:
| Monitor 需要的能力 | 使用的 Fleet 机制 | Monitor 提供的内容 |
| --- | --- | --- |
| 下发任务 | 期望状态 `fleet.state.sync` | 文档类型 `TaskSet`,包括 `tasks[]`、`peers`(Probe 公钥),以及 `probe_agents` 的版本 |
| 下发任务 | 期望状态 `fleet.state.sync` | 文档类型 `TaskSet`,包括 `tasks[]`、`peers`(Probe 公钥),(Probe 的 Agent 列表是另一种期望状态,见下) |
| 上报原始块 | 发件箱,二进制通道 **16** | `RawChunk`(自描述,见 [measurement.md](measurement.md) 第 4 节),`msg_id = chunk_id` |
| Probe 交付列块 | 点对点会话的发件箱,二进制通道 **17** | 列块:`u32 stream_id \| u32 first_seq \| u32 count \| 列数据(Brotli)` |
| 节点自身状态 | `fleet.heartbeat` 的附加字段 | `sessions`、`pps_in/out`、`dropped_invalid`、`dropped_rate_limited`、`queue_bytes` |
@@ -30,8 +30,6 @@ Monitor 的两条控制链路都建立在 Fleet 之上:
| `monitor.event` | A→B | 通知 | `kind`(`probe_connected`(`detail` 是 `probe_id`,`probe` 里带版本、远端地址和连接时间)、`probe_disconnected`、`endpoint_changed`、`session_rejected`、`clock_step`、`gap`、`cpu_saturated`(节点进程 CPU 饱和,可能影响测量,由 Probe 或 Agent 上报))、`at_utc`、`detail` |
心跳的附加字段里有 `probes[]`:Agent 当前连着的每个 Probe(`id`、`version`、`remote`、`since_utc_ms`),每次都是完整快照,Backend 靠它纠正错过的连接事件。点对点握手的认证消息里可以带 Probe 的 `version`(只用于展示)。块头里的 `probe_id` 以 Agent 认证过的会话为准,Probe 自报的值会被覆盖。
| `monitor.probe.agents` | A→B | 请求 | 代理 Probe 获取 Agent 列表 → `version`、`agents[]` |
| `monitor.probe.agents_changed` | B→A | 通知 | Agent 列表发生变化,由 Agent 转发给 Probe |
### Probe ↔ Agent(点对点会话)
@@ -42,8 +40,6 @@ Monitor 的两条控制链路都建立在 Fleet 之上:
| `monitor.test.phase` | A→P | 请求 | 测试的一个阶段:`test_id`、`phase`、`kind`、`params`、`stream_id`、`duration_s`、`port` → `accepted`;Probe 在回复之前准备好自己一侧的角色(单向数据流:先发几个探活包让 Agent 学到源地址) |
| `monitor.test.phase_end` | A→P | 请求 | `sent_count`(Agent 发出的包数)→ `fragments`:Probe 收尾,把列块交给 Agent,回复交出的个数;Agent 等全部列块合并上传之后才进入下一阶段 |
| `monitor.test.abort` | A→P | 通知 | `test_id` |
| `monitor.probe.agents` | P→A | 请求 | `have_version` → `version`、`agents[]`:Agent 代 Probe 向 Backend 查询(链路断开时 `fleet_node_offline`) |
| `monitor.probe.agents_changed` | A→P | 通知 | `version`、`agents[]`(Agent 从 Backend 转发) |
| `fleet.outbox.ack` | 双向 | 通知 | Probe 的列块(通道 17)是至少一次投递,Agent 每收到一个就确认,重复到达的只处理一次 |
会话密钥不需要单独的方法:点对点握手(TLS 固定证书 → `peer.challenge` → Probe 用身份私钥签 `(peer_id, nonce, client_pub, server_pub)` → 两端各自 ECDH + HKDF)直接得到 `session_id` 和 `session_key`,`peer.auth` 的应答只回显 `session_id` 用来互相确认。
+2 -2
View File
@@ -40,7 +40,7 @@
## M3 Backend:注册、RPC、存储
- 在 Fleet 之上接入:`IFleetStore` 实现、`TaskSet` 状态提供者(有效任务集的计算与下发)、`RawChunk` 收件箱、`monitor.*` 方法、`monitor.probe.agents` 同步。
- 在 Fleet 之上接入:`IFleetStore` 实现、`TaskSet` 状态提供者(有效任务集的计算与下发)、`RawChunk` 收件箱、`monitor.*` 方法、`probe_agents` 期望状态(Probe 直连面板)同步。
- 段文件与索引、发件箱 ACK、去重、缺口;管理员账号、Cookie 会话、API 令牌;`/api/admin` 中的节点、令牌、Probe 身份、任务管理。
**验收**:用一行安装命令部署一台新 VPS,自动注册 → 自动生成线路任务 → Probe 自动连接 → 1 分钟内在 `/api/admin` 中看到它的原始块。整个过程不需要手工配置。
@@ -95,7 +95,7 @@
- **M5 的一部分**:保留清理。
- **F1 的节点部分**:`Fleet.Link`(链路、发件箱、点对点监听与连接)、`Fleet.Hosting`、`Fleet.Enrollment.Client` 与 `fleet-enroll`。
- **Agent(M1 的一部分)**:`Monitor.Agent`(NativeAOT 可发布)与 `Monitor.Measurement`(列块合并、预算守卫)。UDP/TCP 数据面(认证、重放窗口、限速、端点学习)、线路任务调和、目标任务(`icmp_echo`、`connect`)、按需测试编排(`udp_train` 由 Agent 发包)。测试里的假 Probe(`ProbeSimulator`)通过真实的点对点握手连接真实的 Agent 与后端,端到端覆盖。详见 `backend/agent/README.md`。
- **Probe(M0/M2 的一部分)**:`Monitor.Probe`(NativeAOT,5.6 MB,常驻约 22 MB)——Agent 列表(bootstrap → 由 Agent 代为查询 → 推送,写回 `agents.json`)、每个 Agent 一个自动重连的会话、`udp_echo`/`tcp_echo`/`connect`/`hold` 角色、`udp_train` 接收端(按序号范围切块)、时钟偏移估计、有界发件箱。真实 Probe + 真实 Agent + 真实后端的端到端测试,以及用发布的 AOT 二进制做过的冒烟(内存稳定、CPU < 1%)。详见 `backend/probe/README.md`。
- **Probe(M0/M2 的一部分)**:`Monitor.Probe`(NativeAOT,5.6 MB,常驻约 22 MB)——Agent 列表(由面板经 Fleet 长连接直接下发,写回 `agents.json`)、每个 Agent 一个自动重连的会话、`udp_echo`/`tcp_echo`/`connect`/`hold` 角色、`udp_train` 接收端(按序号范围切块)、时钟偏移估计、有界发件箱。真实 Probe + 真实 Agent + 真实后端的端到端测试,以及用发布的 AOT 二进制做过的冒烟(内存稳定、CPU < 1%)。详见 `backend/probe/README.md`。
- **采集积木统一(重构)**:Probe 和 Agent 的数据面合并成 `Monitor.Measurement` 里的一份实现(每种采集方法一个文件、拨号方和监听方一对角色,`UdpMux`/`Tcp`/`RowStore` 共用);合并改成由 Kind 的流声明(`Driver`、`Key`)驱动;测试方案移回 Backend;架构测试禁止采集层出现业务概念。
- **TCP 内核采样与长尾**:`tcp_echo` 两端各自采样套接字的 `TCP_INFO`(Linux,`union` 流拼接);`tcp.rtt` 增加 `p999`、`max`、长尾比 `tail`,新增 `tcp.slow`(块内基线的慢回显占比);序列接口增加 `anchor=latest`。
- **打包与部署(M6 的一部分)**:`scripts/publish.sh`(linux-musl-x64、linux-x64 在 NativeAOT SDK 容器里构建)、`scripts/package.sh`、`packaging/`(`install.sh`、systemd 单元、OpenWrt procd 脚本)、节点启动时的 OpenSSL 自检。在 Alpine、Debian、OpenWrt 官方 rootfs、带 systemd 的 Debian 容器里验证过。详见 `docs/deployment.md`。