The versions page shows each node's update state with update buttons, API tokens get a release scope that can only register releases (used by CI), and the CI image and release builds now pass the tag, commit and build time.
21 KiB
部署
节点(Agent、Probe)是单个 NativeAOT 可执行文件,没有运行时依赖,只需要系统的 OpenSSL 3(libssl.so.3)。启动时会自检,缺少时第一步就明确报错。
1. 构建与打包
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),然后启动:
docker compose up -d
Compose 将容器端口只绑定到宿主机 127.0.0.1:8080,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 monitor-data。首次启动成功创建管理员后,改密应在管理端完成。
健康检查端点为 GET /healthz。容器定义见 ../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/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 测量:
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 都测试过):
# 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.up0、loss.down7.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。
浏览器 / 本机前端 ──► 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)。
实测(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)
面板通知、节点验签、宿主机更新,面板即使被攻破也装不上假镜像:
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,只能登记清单);环境变量RELEASE_API_URL(面板地址)。没有release_signing_key就只构建镜像、不发清单。 - 发布公钥在
packaging/update/release-key.pub.pem;私钥不入库,请离线备份。更换密钥:节点上换新公钥之后再用新私钥发布。 - 纯二进制的安装方式(
fleet-updater、fleet-release,见 fleet/update.md)还没有实现;容器方式优先。