416 lines
58 KiB
Markdown
416 lines
58 KiB
Markdown
# 部署
|
||
|
||
节点(Agent)使用 NativeAOT。Linux 的开发版和正式版共用 `ci/build-linux-agent.sh`:musl arm64 静态链接 MsQuic、OpenSSL 和 .NET 加密 shim;glibc amd64/arm64 附带 MsQuic、NUMA、zstd 和 zlib 共享库,主机仍需提供 OpenSSL 3 和对应 libc。Agent OCI 镜像复用同版本的原生包:amd64 使用 glibc 与 Debian 基底,arm64 使用静态 musl 与 Alpine 基底。OpenWrt 更新包也使用这份二进制,安装与签名清单沿用设备端现有协议。
|
||
|
||
## 1. 构建与打包
|
||
|
||
### 拨号方 ↔ Agent 连接日志
|
||
|
||
管理端节点日志收集节点通过心跳上传的近期日志;原始日志可从 systemd journal、容器 logs 或 OpenWrt `logread` 查看。排障时同时查看 Agent,并按时间、`remote` 端点对应两端记录。
|
||
|
||
- 拨号方 每次重连生成独立 `attempt_id`,附带 `agent`、`ip_version`。阶段依次为 DNS、TCP 连接、敲门、TLS、对等认证、时钟同步、QUIC 握手和测量。失败记录包括阶段、耗时、异常及重试间隔。
|
||
- Agent 控制会话带 `dialer`、`ip_version`、`control_id`,记录替换、主动断开、吊销、空闲超时、控制连接异常和会话时长。通过敲门验证前的连接保持静默。
|
||
- 每条 QUIC 连接带独立 `quic_id`,记录远端、本地 UDP 端口、握手、DATAGRAM 协商状态及最大长度、首个数据报、首个通过认证的测量数据报和关闭状态。首个认证数据报在 Agent 端关联公开 拨号方 ID;这些关联 ID 均为本地诊断标记,两端的 ID 不相同。
|
||
- QUIC 原生关闭码同时保留十六进制和可识别的原因,例如 Linux `0x0000006E` 为 `connection_timeout`。原生收发统计来自回调缓存,`stats_age_ms` 表示缓存年龄;空值表示尚无快照,不能据此断言没有发包,确认网络丢包仍需抓包。
|
||
- 测量数据报拒绝区分格式、未知流、未知会话、MAC 校验、准入策略和限速。拒绝日志每个传输实例最多每 30 秒一条,计数始终累计;发送失败也按连接限频。正常会话每 60 秒输出摘要,避免逐包日志干扰测量。
|
||
|
||
日志不记录会话密钥、会话凭据、MAC、签名或包内容。近期日志缓冲仍有容量和心跳批次预算;大量重连时可能覆盖旧记录,管理端的 dropped 数量表示丢弃的日志条数。
|
||
|
||
### IPv6 状态诊断
|
||
|
||
Agent 在 hello 初始化及心跳中上报 IPv6 检测结果,包括监听状态、全局地址、默认路由和网络命名空间。容器可提供只读宿主 `/proc` 挂载,附带宿主网络状态。只传布尔状态,不传 IP 或网卡名称。Docker 和 Podman 推荐宿主网络模式;自定义桥接网络时,需要启用 IPv6 并提供 `/proc` 宿主挂载,前端会提示宿主和容器网络状态是否不同。
|
||
|
||
### 启用 IPv6 测量
|
||
|
||
升级 Backend、Agent 后,在管理端的 Agent 编辑页保留 IPv4 地址,并填写 IPv6 地址或带 AAAA 记录的域名。未填写 IPv6 地址的 VPS 只采集 IPv4;拨号方 没有 IPv6 路由或 IPv6 连接失败时,IPv4 会话继续工作。
|
||
|
||
Agent 的新默认监听地址为 `::`(双栈);现有 `agent.json` 如果显式配置了 `0.0.0.0`,需要在宿主机改为 `::` 并重启节点。系统不支持 IPv6 套接字时,新默认值退回 IPv4 监听。防火墙需允许 IPv6 的 `7443/tcp`、`7443/udp`(QUIC)、`7444/tcp`(自定义端口时用实际端口);容器推荐宿主网络。管理端的「IPv6 监听」显示节点实际是否启用。
|
||
|
||
原来的 SQLite 数据库自动迁移,旧原始块、指标和测试结果继续属于 IPv4。先升级 Backend,再升级节点,最后填写 IPv6 地址;旧 Agent 不会上报 IPv6 支持,Backend 不会给它安排第二条会话。
|
||
|
||
上线后,在管理端的 Agent 页面查看「IPv6 诊断」。它显示检测网络范围、全局地址和默认路由;机器有 IPv6 但容器没有时,检查是否使用宿主网络。没有填 Agent IPv6 测量地址时,诊断仍会上报,但面板不会建立该 Agent 的 IPv6 测量线路。
|
||
|
||
管理端把节点审批状态与进程运行状态分开显示。运行状态为初始化中、运行中、暂停探测、维护中、停止中、失联或更新中;Agent 停用/拨号方 吊销显示为维护中,拨号方 没有任何面板下发的 Agent 目标时显示暂停探测。节点通过心跳上报运行状态,正常关闭时 Fleet 连接会发送停止标记;突然断网或心跳超时显示失联。线路只有在 Agent 都处于运行中且 Agent 确认了 拨号方 会话时才显示已连接。
|
||
|
||
```text
|
||
scripts/publish.sh <agent|backend|fleet-enroll> <rid> → backend/artifacts/publish/<project>/<rid>/
|
||
scripts/package.sh <agent> <rid> → backend/artifacts/packages/moshiro-<role>-<version>-<rid>.tar.gz(附 .sha256)
|
||
```
|
||
|
||
构建信息(程序里显示的 tag、提交、构建时间):`publish.sh` 会用 `git describe --tags --always --dirty`、`git rev-parse` 和当前时间自动设置环境变量 `MOSHIRO_TAG`、`MOSHIRO_COMMIT`、`MOSHIRO_BUILT_AT`,直接用 `dotnet publish` 或 CI 构建时自己设置它们(容器镜像的 `*.NativeBuild.Containerfile` 用同名的 `--build-arg`,CI 脚本会传);不设置就是空,本地构建照常可重复。版本号取自 `backend/Directory.Build.props` 的 `Version`;仓库里它只是占位(`0.0.0-dev`),CI 构建时由 `ci/version.sh` 写进工作区(见下文「版本号」)。前端的信息由 `web/build-info.mjs` 在构建时写入(版本取自 `web/package.json`,同样由 CI 填写)。Agent、Backend 启动时都会在日志里打印一行构建信息:版本、tag、提交、构建时间(没设置环境变量的本地构建显示 `unknown`)。
|
||
|
||
| rid | 目标 | 构建方式 |
|
||
| --- | --- | --- |
|
||
| `linux-x64`、`linux-arm64` | Debian/Ubuntu glibc 服务器 | Linux amd64 runner 通过 AotAnywhere 交叉编译 |
|
||
| `linux-musl-arm64` | ARM64 Alpine、aarch64 OpenWrt 路由器 | Linux amd64 runner 通过 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 流水线在 Linux runner 上构建 Agent 与 `fleet-enroll` 的三个 Linux RID;Windows runner 负责 macOS 和 Windows。Backend OCI 镜像在 Linux runner 上构建。
|
||
|
||
## Backend 容器
|
||
|
||
Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/moshiro/moshiro-backend:<version>` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器使用 glibc NativeAOT 单文件程序(动态链接,运行时要加载 libssl;musl 的静态产物做不到),基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0`(Debian),以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。程序启用 InvariantGlobalization,无需 .NET 运行时或 ICU;时间查询使用 UTC。SQLite 从固定版本的官方源码编译并静态链接,不再随镜像携带 `libe_sqlite3.so`。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。
|
||
|
||
前端内嵌在 Backend 里:CI 构建 Backend 前先构建 `web/display` 和 `web/admin`,`Backend.Api` 把两个 `dist` 嵌进程序,展示端在 `/`,管理端在 `/admin/`,所以面板的域名(如 `https://mo.oeo.one/admin/`)打开就是管理端,passkey 才能绑定在这个域名上(浏览器只允许页面的来源与 RP 域名一致,本机 `localhost` 上的页面注册不了绑定 `mo.oeo.one` 的 passkey)。本地没有构建前端时不嵌入,Backend 只提供 API。文件是启动时读进内存的白名单,请求时不访问文件系统;带 `..`、反斜杠、`%`、控制字符或空段的路径一律 404,缺失的文件(带扩展名)404,只有无扩展名的路径回退到入口页;`/api`、`/openapi`、`/install`、`/healthz` 下没有匹配到的路径仍是后端的 JSON 404。页面带 CSP(脚本只许同源)、`nosniff`、`X-Frame-Options: DENY`,入口页 `no-cache`,哈希文件名的资源 `immutable`,不嵌入 source map。部署时设置 `MoShiro__PublicUrl=https://<面板域名>`(`deploy-node.sh backend --public-url …`),WebAuthn 的域名和允许的来源由它派生。
|
||
|
||
用 compose 部署(Docker 和 Podman 通用,需要 `docker compose` 或 `podman compose`):compose 文件是静态的,在仓库的 `packaging/compose/backend/compose.yaml`,每个发布版本的 Gitea Release 里也有一份 `moshiro-compose-backend.yaml`(第一次部署时还没有面板,就从这里下载)。镜像用 `:${MOSHIRO_VERSION:-latest}`:
|
||
|
||
```sh
|
||
mkdir moshiro-backend && cd moshiro-backend
|
||
curl -fsSL https://git.oeo.one/justme/MoShiro/releases/download/<版本>/moshiro-compose-backend.yaml -o compose.yaml
|
||
cat > .env <<'EOF'
|
||
MoShiro__PublicUrl=https://panel.example.com # 面板对外的地址;passkey 绑定在这个域名上,改了会让已注册的 passkey 失效
|
||
# MoShiro__ClaimCode= # 认领码(可选),不写就随机生成并打印在日志里
|
||
EOF
|
||
podman compose up -d # Docker 用 docker compose
|
||
```
|
||
|
||
更新:先备份数据卷,再 `podman compose pull && podman compose up -d`。新版启动时自动迁移数据库,但旧版不认比自己新的数据库:想回滚要先恢复备份,只改 `MOSHIRO_VERSION` 不够。Backend 能指挥所有节点,更新它尽量用 Quadlet(`packaging/quadlet/moshiro-backend.container`,见下文「Backend 的更新」:管理端确认,更新前自动备份数据库),或者在 `.env` 里把 `MOSHIRO_VERSION` 固定到经过核对的版本或 digest。
|
||
|
||
首次认领(没有默认管理员):面板里还没有任何用户时,打开 `/admin/` 会进入认领向导:
|
||
1. 填认领码(首次启动时随机生成并写进后端日志,`journalctl -u moshiro-backend` 或 `docker logs`;也可以用 `MoShiro__ClaimCode` / `MOSHIRO_CLAIM_CODE` 指定)、管理员的用户名和密码;
|
||
2. 注册你的第一把 passkey;
|
||
3. 用它给操作员集合签名,建立信任根。
|
||
谁先打开页面谁就能当管理员,所以认领码只有能看到后端日志的人拿得到;创建完第一个管理员,码作废、入口关闭(再提交返回 409)。认领码比较不分大小写、忽略分隔符;失败按来源地址限流。向导可以中途离开,passkey 和信任根之后随时在「用户 → 我的 Passkey」和「系统 → 信任」里补;没有 passkey 也能用密码登录管理。
|
||
|
||
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在命名卷 `moshiro-backend-data`(Docker 和 Podman 通用)。首次启动成功创建管理员后,改密应在管理端完成。
|
||
|
||
Backend 的 NativeAOT 发布默认启用 `SqliteStaticLink`,支持 Linux glibc/musl、macOS、Windows 的 x64/arm64 RID;跨平台构建使用 `-p:UseAotAnywhere=true`。SQLite 源码版本、下载年份和 SHA-256 统一配置在根目录 `Directory.Packages.props`,构建会校验下载文件并按 RID 缓存原生对象。普通 JIT 开发构建仍使用 NuGet 原生库。发布 Backend 时设置 `-p:OpenApiGenerateDocuments=false`,API 文档在普通开发构建中生成。
|
||
|
||
健康检查端点为 `GET /healthz`。容器定义见 [`packaging/container/Backend.Containerfile`](../../packaging/container/Backend.Containerfile)。
|
||
|
||
## 节点二进制发布
|
||
|
||
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上构建 Agent 的 Linux、macOS 和 Windows 产物。Linux amd64/arm64 使用 glibc,OpenWrt ARM64 使用静态 musl;Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。glibc sidecar 选用 Debian 12 兼容构建,musl sidecar 来自 Alpine v3.22。Agent OCI 镜像使用 amd64 glibc/Debian 和 arm64 musl/Alpine,直接携带相应归档中的共享库,并通过 `LD_LIBRARY_PATH` 加载。Linux 原生库包按 RID 缓存在 CI runner 的 `.nuget/moshiro-quic-runtime`,索引最多缓存 24 小时,已下载的版本包会复用。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [Agent](../../backend/agent/README.md) 文档。
|
||
|
||
## CI 工作流
|
||
|
||
### Linux runner 的运行环境
|
||
|
||
Linux workflow 使用 `ci=domain-linux-native`、`platform=linux/amd64` 标签和 Woodpecker `local` backend;步骤里的 `image: bash` 是执行程序,构建工具来自 runner 环境。测试、前端和 Backend 编译使用 .NET 10、Node/npm、Python、clang 和 Zig;Agent 的 Linux 原生包通过 Buildah 的 NativeAOT builder 编译,OCI 镜像从已发布的 glibc amd64、musl arm64 包组装。
|
||
|
||
WSL 的 agent 通过 rootless Podman 运行,systemd 模板见 `ci/woodpecker-agent-podman.service`。工具全部在 `packaging/container/Woodpecker.Agent.Containerfile` 中安装,包括 .NET 10 SDK、Node/npm、Buildah、netavark/aardvark-dns、Git、Python、clang、Woodpecker agent/clone 插件和测试用 MsQuic 库;AotAnywhere SDK、Zig 与三个 Linux RID 的 NativeAOT 包由 `NativeAot.Builder.Containerfile` 预热。容器 PID 上限是 8192,避免多个并行 AotAnywhere/Zig 链接时撞到默认 2048 的限制。容器只把 `/home/justme` 作为可写工作区和缓存,不挂载宿主工具目录。连接信息与 7897 代理配置放在宿主 `/home/justme/woodpecker/agent-podman.env`,不要提交密钥。容器允许 `unshare`、提供 `SYS_ADMIN` 和 `/dev/fuse`,以便 Buildah 挂载镜像层;不要通过 `chroot /host` 启动 agent,因为 Linux 会拒绝 chroot 进程创建新的用户命名空间。
|
||
|
||
以 WSL 的 `justme` 用户运行 `bash ci/build-runner-image.sh` 构建工具镜像,验证后安装 systemd 模板并重启 agent。该脚本使用 Podman 的持久镜像层缓存和 `--pull=missing`,服务启动使用 `--pull=never`;流水线和服务重启都不会执行工具安装。NativeAOT builder 只在 SDK/工具版本或 Containerfile 改变时更新,不受每次开发构建的版本号影响。它按镜像 ID 导出到 `woodpecker/cache/runner-image`,Linux Agent 的共享 Buildah store 从本地归档导入一次,之后直接复用镜像。只有所需归档不存在或工具配置改变时,构建脚本才回退到 Containerfile 构建。应用的 NuGet restore 缓存独立持久化在 `woodpecker/cache/nuget/packages`,启动时从镜像复制已有工具包作为初始缓存。
|
||
|
||
每个 Linux workflow 先运行 `restore-linux-dependencies`:它一次准备 NativeAOT 工具链,为三个 Agent RID 分别生成 restore 镜像,并在工作区预还原 Backend 目标。后续 `build-amd64-agent`、`build-arm64-agent`、`build-arm64-agent-musl` 和 Backend 任务都依赖这一步;Agent 直接从对应 restore 镜像继续编译,Backend 用同一个 artifacts 路径执行 `publish --no-restore`。restore 串行写共享项目资产,NativeAOT 的 Agent/enroll 发布阶段仍并行。NuGet 包放在 runner 的持久 `NUGET_PACKAGES` 目录,restore 任务读写该目录,后续构建只读;工具链镜像按内容 key 导出到 runner OCI 缓存,跨流水线复用。
|
||
|
||
`Agent.Native.proj` 在一个 MSBuild 图内编译两份程序和公共依赖。随后两个独立 stage 用 `publish --no-build --no-restore` 完成 NativeAOT,Buildah `--jobs 2` 并行执行;中间目录随 stage 隔离。最终 scratch stage 只保留两份可执行文件,`ci/extract-node-binaries.sh` 在同一个 `buildah unshare` 会话内挂载、复制和卸载。`ci/package-linux-agent.sh` 生成原生安装归档,`ci/release-node-images.sh` 复用已发布归档生成 OCI 镜像。
|
||
|
||
任务结束时清理本次上下文、导出目录和临时包,失败退出也清理临时容器。构建持有共享 store 锁和当前 RID 的锁;清理需要独占 store 锁;`ci/cleanup-build-cache.sh` 清理超过 7 天的中间镜像与旧版本应用镜像,保留具名工具镜像和当前构建镜像,删除时不强制移除仍被引用的层。已退役的 Probe、独立 Agent 架构及索引 store,在存储元数据连续 7 天未更新且可取得独占锁时整目录清理;近期使用、锁定或没有可识别元数据的目录保留。持久 NuGet 包目录和 runner OCI 归档独立于 Buildah 层清理;不使用 `buildah prune`。
|
||
|
||
所有镜像构建、索引及签名清单发布脚本通过 `ci/buildah-env.sh` 显式配置 Buildah 的 `graphroot` 和 `runroot`。Agent 原生构建的三个 RID 共用 `buildah-home/linux-agent`,Backend 和 OCI 发布使用各自的 store,运行状态留在当前 runner 的临时目录;容器内 UID 0 不再使各步骤落到同一个 `/var/lib/containers/storage`。`RUN` 默认使用 `chroot` isolation,避免额外的 OCI runtime/cgroup 嵌套;这与上面禁止的 agent 启动方式是不同阶段。
|
||
|
||
WSL 的 systemd 服务不会单独阻止发行版空闲退出。Windows 登录用户运行 `powershell -ExecutionPolicy Bypass -File ci/start-wsl-runner.ps1`,注册并启动隐藏的登录任务,持续运行 Debian 的前台 `sleep infinity`,让 CI server/agent 保持在线。任务使用当前用户的普通权限;需要暂停 WSL 时先停止任务 `MoShiro WSL runner (Debian)`。agent 单元等待 `user@1000.service` 并设置用户 D-Bus 地址,让 rootless Podman 使用已就绪的用户会话。
|
||
|
||
### Pull request 与 main 验证
|
||
|
||
Pull request 和 `main` 的 push 会跑三个 workflow:
|
||
|
||
- `verify`:后端的快测试(除了标了 `Category=Slow` 的全部,约 20 秒);
|
||
- `web-build`:前端构建和 CSS 检查,和 `verify` 并行(约 15 秒);
|
||
- 标了 `Category=Slow` 的端到端测试(真实时间的 Hy2 阶梯、IPv6 和真探针的几条,一百来秒):只在 PR 和没带 `[CI]` 的推送上由 `e2e` workflow 在 `verify` 之后跑;带 `[CI]` 的推送只出镜像。
|
||
|
||
后端测试用增量编译(`ci/verify-backend.sh`):源码按内容同步到 CI 主机上一个固定目录,保留 `obj/bin`,只重编译变了的项目;同一时间只有一条流水线用这份缓存。失败的测试会单独重跑(最多两次),重跑通过算通过。给新的慢测试打标记:`[Trait("Category", "Slow")]`。
|
||
|
||
提交信息后缀 `[SKIP TESTS]` 跳过 `verify` 和 `e2e` 里的后端测试,前端构建仍会运行。Gitea PR 事件的 `CI_COMMIT_MESSAGE` 是 PR 标题,因此在 PR 中跳过测试时,把后缀写入 PR 标题;普通分支 push 则写在 commit message 中。项目只使用 `main` 一个工作分支。
|
||
|
||
### 版本号
|
||
|
||
版本都由 CI 填写,仓库里写死的只是占位(`backend/Directory.Build.props` 的 `<Version>` 和三个 `web/**/package.json` 的 `version` 都是 `0.0.0-dev`),发版不需要提交 bump。规则在 `ci/version.sh`:
|
||
|
||
| 构建 | 版本 | 构建信息里的 tag |
|
||
| --- | --- | --- |
|
||
| 推 `vX.Y.Z` tag(正式版) | `X.Y.Z` | `vX.Y.Z` |
|
||
| `main` 上带 `[CI]` 的 push(开发镜像) | `X.Y.(Z+1)-dev.<N>-<提交前 7 位>` | `v` + 同一个版本 |
|
||
|
||
开发版本的基准是最近一个 `v*` tag(`X.Y.Z`),`N` 是这个 tag 之后的提交数(`git rev-list --count`),所以同一个提交算出来永远一样,也按语义化版本排在下一个正式版之前,例如 `0.4.1-dev.12-044ce0c`。算版本要拿得到 tag,所以开发镜像的 workflow 克隆时带 `tags: true`。镜像标签不带 `v`(和正式版一致),构建信息和签名清单里的 `tag` 带 `v`。
|
||
|
||
### main 开发镜像
|
||
|
||
`main` 上只有提交信息包含 `[CI]` 的 push 才构建镜像,并且要等上面的 `verify`(快测试)和 `web-build` 都通过(提交信息写 `[SKIP TESTS]` 可以跳过测试);慢的端到端测试在镜像推完之后才跑,不拦着出镜像。合并 PR 时,请把 `[CI]` 放入 PR 标题或合并提交标题。Backend 构建 Linux/amd64;Agent 构建 Linux/amd64 和 Linux/arm64,并把两个架构发布为多架构索引到同一个 Zot Registry 下独立的 `moshiro-dev/` 仓库:
|
||
|
||
| 镜像 | 滚动标签 | 固定版本标签 |
|
||
| --- | --- | --- |
|
||
| Backend | `registry.oeo.one/moshiro-dev/moshiro-backend:latest` | `registry.oeo.one/moshiro-dev/moshiro-backend:<版本>`(如 `0.4.1-dev.12-044ce0c`) |
|
||
| Agent | `registry.oeo.one/moshiro-dev/moshiro-agent:latest` | `registry.oeo.one/moshiro-dev/moshiro-agent:<版本>` |
|
||
|
||
Agent 也发布架构固定标签 `:<版本>-amd64` 与 `:<版本>-arm64`。原生构建在 amd64 的 `sdk:10.0-noble-aot` 衍生 builder 中使用 AotAnywhere 编译三个 Linux RID,ARM64 无需 QEMU;`restore-linux-dependencies` 预热固定版本的 SDK、Zig 和运行时包,并生成按提交与 RID 标记的 restore 镜像,Agent 构建直接使用这些镜像。NuGet 包缓存与工具链 OCI 归档持久保存在 runner,跨流水线复用。amd64 镜像基底为 `runtime-deps:10.0`,arm64 为 `runtime-deps:10.0-alpine`。开发镜像不创建 Gitea Release,也不修改正式版 `moshiro/` 仓库中的标签。普通 main push 不会构建镜像。节点和 Backend 判断自己是不是开发构建,看构建信息里的 tag 含 `-dev.`(旧的 `dev-<sha>`、`main-<sha>` 也认)。
|
||
|
||
### 正式版发布
|
||
|
||
在 `main` 上推送 `vMAJOR.MINOR.PATCH` tag 会触发完整跨平台构建、正式 OCI 镜像推送和 Gitea Release,版本取自 tag,不需要先提交版本号。正式镜像使用 `registry.oeo.one/moshiro/` 路径;Agent 提供 amd64 与 arm64 多架构标签,稳定版本更新正式仓库自己的 `latest`。这与 `moshiro-dev/` 的 dev `latest` 相互独立。
|
||
|
||
Agent 镜像发布为 `registry.oeo.one/moshiro/moshiro-agent:<version>`,包含 amd64、arm64 多架构索引;也可指定 `:<version>-amd64` 或 `:<version>-arm64`。稳定版本同时更新 `latest`。镜像以非 root 用户(UID/GID 1654)运行,默认读取 `/etc/moshiro/agent.json`。使用 `packaging/compose/agent` 下的标准 Compose 模板,配置和身份放在命名 volume 中;首次注册/导入、宿主指标挂载与更新器见下文。
|
||
|
||
Agent 的默认发布配置:NativeAOT、完整裁剪、`OptimizationPreference=Size`、符号剥离、相同方法体折叠、InvariantGlobalization;关闭 Server/Concurrent GC、异常栈、EventSource、调试、Hot Reload、HTTP tracing、HTTP/3 和 .NET Metrics。JSON 全部使用源码生成上下文,禁用反射序列化回退;LINQ 使用体积优化实现。HTTP/3 裁剪不影响现有 HTTP/WebSocket/TLS 连接,也不影响程序自身上报的监控指标。
|
||
|
||
`fleet-enroll` 使用同一套体积优化开关。Fleet 共享库已声明 AOT 兼容,由 Agent、Backend 或注册工具的最终发布一起裁剪;GC 和运行时功能开关配置在各可执行项目中。
|
||
|
||
NativeAOT 可执行文件本身的体积如下,不包含 Linux 包附带的 MsQuic 等共享库,最新数值以各 RID 的 CI 产物为准:
|
||
|
||
| 程序 | linux-musl-x64 | linux-x64 | win-x64 |
|
||
| --- | --- | --- | --- |
|
||
| moshiro-agent | 7.7 MB | 7.7 MB | 5.6 MB |
|
||
| moshiro-agent | 8.4 MB | 8.4 MB | 7.3 MB |
|
||
| fleet-enroll | 6.4 MB | 6.4 MB | — |
|
||
|
||
Linux 产物比 Windows 大,是因为 .NET 的加密、压缩、网络本机库被静态链接进来;Linux tar.gz 另含 MsQuic 和支持库。
|
||
|
||
## 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
|
||
|
||
# 拨号方(家宽网络里的机器或 OpenWrt 路由器):用面板里 agent 角色的加入令牌注册,密钥由它自己生成
|
||
./install.sh agent --server https://panel.example.com --join <加入令牌> --name home-1
|
||
|
||
./install.sh uninstall agent [--purge] # --purge 同时删除身份(节点需要重新注册)
|
||
```
|
||
|
||
| 位置 | 内容 | 所有者 |
|
||
| --- | --- | --- |
|
||
| `/usr/lib/moshiro/` | 二进制、MsQuic/NUMA/zstd/LTTng 共享库 | root;二进制 0755、共享库 0644,采集进程不能改写 |
|
||
| `/etc/moshiro/` | `agent.json` 或 `agent.json`、身份(`node.json`、`node.key`、`tls.pfx`)、`agents.json` | `moshiro` 用户,0750 |
|
||
|
||
安装脚本会:检查 OpenSSL、创建 `moshiro` 用户(`useradd`、busybox `adduser`,OpenWrt 没有这些时直接写 `/etc/passwd`)、安装二进制、QUIC sidecar 共享库和服务文件;缺少配置时只安装、不启动,并打印下一步。
|
||
|
||
### systemd(`packaging/systemd/*.service`)
|
||
|
||
以 `moshiro` 用户运行,沙箱:`NoNewPrivileges`、`ProtectSystem=strict`、`ProtectHome`、`PrivateTmp/Devices`、`ProtectKernel*`、`RestrictAddressFamilies=AF_INET AF_INET6`、`RestrictNamespaces`、`MemoryDenyWriteExecute`(NativeAOT 不 JIT)、`SystemCallArchitectures=native`,`MemoryMax`(Agent 128 MB、拨号方 64 MB)。
|
||
|
||
- Agent 只保留 `CAP_NET_RAW`(ICMP 目标任务用),`/etc/moshiro` 只读;拨号方 没有任何 capability,只对 `agents.json` 所在的目录可写。
|
||
- 监听 1024 以下的端口(例如把数据面放到 443)需要在 drop-in 里加 `AmbientCapabilities=CAP_NET_BIND_SERVICE` 和对应的 `CapabilityBoundingSet`。默认端口是 7443/tcp(拨号方 控制通道)、7443/udp(QUIC Datagram 数据面)、7444/tcp(TCP 测量)。Agent 在 UDP 端口固定监听;拨号方 的 QUIC 连接使用轮换的临时本地源端口。
|
||
|
||
### OpenWrt(`packaging/openwrt/`)
|
||
|
||
#### 原生一键安装与自动更新
|
||
|
||
管理端「添加 拨号方」提供 OpenWrt · 原生二进制 命令,以 root 执行。支持 ARM64/aarch64,不需要容器或 .NET;自动安装依赖、验签下载、固定属主并注册、配置 procd。身份在 `/etc/moshiro/`,更新策略在 `/etc/moshiro-native.conf`(`CHANNEL=dev|release`、`POLICY=auto|manual`、`INTERVAL=30`)。原生包使用静态 musl 可执行文件,实际设备兼容性需实机验证。
|
||
|
||
带 `[CI]` 的 main push 发布 dev,稳定 tag 发布 release,沿用 `gitea_token` 和 `release_signing_key`;Gitea generic package `moshiro-openwrt` 需允许匿名下载。制品发布后,在线节点收到通知就唤醒更新器,另有 30 秒轮询兜底,不等待 10 分钟。更新器验签、防重放、原子切换完整版本,健康检查失败回滚;这些步骤由独立的更新器完成,不在 拨号方 进程内执行。
|
||
|
||
```sh
|
||
logread -e moshiro
|
||
/etc/init.d/moshiro-agent status
|
||
/etc/init.d/moshiro-agent-update status
|
||
/usr/libexec/moshiro-openwrt check # 立即检查;auto 策略下有新版就安装
|
||
cat /var/run/moshiro-update/status.json
|
||
```
|
||
|
||
默认测量上限 20 Mbps/2000 pps,可修改 `agent.json`。sysupgrade 保留身份和更新配置,升级固件后重新校验安装脚本并执行 `install` 恢复程序,不需新加入码;旧部署需要人工迁移,不覆盖现有身份。首次安装没有旧版可回滚。发布公钥轮换需 root 手动更新本机公钥。隔离测试:`python3 scripts/test-openwrt-updater.py`。
|
||
|
||
#### 旧的解压包安装方式
|
||
|
||
统一安装 Agent。`install.sh agent` 安装 `/etc/init.d/moshiro-agent`(procd,崩溃自动重启,日志进 logd)并写入 `/lib/upgrade/keep.d/moshiro`,sysupgrade 时保留 `/etc/moshiro/`。
|
||
|
||
OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 24.10 之后用 `apk add libopenssl3`)。安装包会把 MsQuic、NUMA 和相关共享库放到 `/usr/lib/moshiro`,procd 会设置 `LD_LIBRARY_PATH`;OpenSSL 3 仍由系统提供。闪存很小时注意:拨号方 二进制约 7.7 MB,配置只在 Agent 列表变化时才写。路由器上建议把 `agent.json` 的 `limits.max_test_mbps` 调低。
|
||
|
||
## 3. 验证
|
||
|
||
`backend/tests/MoShiro.Backend.Tests/AgentMeshTests.cs` 在回环网络中启动两个默认开启监听的 Agent,检查双向连接、通过 Backend 心跳汇总的线路状态,以及两个方向的原始测量块入库。其余慢测覆盖 TCP/QUIC 数据面、阶段测试、重连、IPv6 和公开数据边界。
|
||
|
||
```sh
|
||
bash ci/fetch-quic-test-runtime.sh /tmp/moshiro-quic
|
||
LD_LIBRARY_PATH=/tmp/moshiro-quic/runtime dotnet test --solution backend/MoShiro.slnx -c Release
|
||
python scripts/test-openwrt-updater.py
|
||
bash packaging/update/tests/feed-test.sh
|
||
```
|
||
|
||
真实 OpenWrt 设备、跨公网线路及各平台 NativeAOT 发布仍需对应环境验证。
|
||
|
||
### 家宽机器上的 Backend
|
||
|
||
没有公网入站的家宽机器上跑 Backend 时,用隧道(Cloudflare Tunnel 之类)把一个公网域名转到 Backend 的端口;Backend 只写一个监听地址(同时写 `0.0.0.0` 和 `[::]` 会报地址已被占用);拨号方 的 `fleet.server` 就是这个面板地址。小 VPS 上把 `limits.max_total_mbps` 调低。家宽的 IPv6 前缀会在路由器重新拨号(比如停电)后变化,用 IPv6 直连时地址要跟着换;走隧道的域名不受影响。
|
||
|
||
国家和运营商:Backend 用 Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`MoShiro__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。Agent 的地址优先用它的对外地址(`public_host`),没有就用 IPv6 地址,再没有(NAT 后面、没有上报公网地址)才用它连面板的来源地址;面板前面有反向代理时,要配置 `MoShiro__TrustedProxies`,否则来源地址是代理的内网地址,这类节点查不到国家。没查到的节点每隔十分钟在心跳里重试一次。
|
||
|
||
从别处看这个面板:本机跑前端,在页面里把数据源指向面板的地址,用 API 令牌访问,Backend 设 `MoShiro__CorsOrigins` 放行前端的来源(见 [web/README.md](../../web/README.md))。
|
||
|
||
## 容器部署(Podman Quadlet 优先,Docker 用 compose)
|
||
|
||
普通用户执行 Podman 即为 rootless,使用命名 volume 不需要 `keep-id` 或手动 `chown`。Agent 的配置、原有身份和缓存保存在 `moshiro-agent-config`,拨号方 保存在 `moshiro-agent-config`,更新状态使用独立的 `moshiro-<role>-status` volume,在业务容器内只读。不会重新生成已有身份。更新/重建容器不删除 volume;不要删除这些 volume(`podman volume rm`),会丢身份和配置。
|
||
|
||
Quadlet 安装(Agent,Podman 系优先,Linux + Podman 4.6 及以上):管理端「添加 Agent」生成加入令牌之后,第一条命令就是它(面板地址、一次性码、信任根指纹和属主都是公开的值,没有私钥):写 env(权限 600)、从面板下载静态的 Quadlet 单元并核对 SHA-256、用一个 drop-in 把镜像仓库指向面板配置的、启动用户服务:
|
||
|
||
```sh
|
||
mkdir -p ~/.config/moshiro ~/.config/containers/systemd/moshiro-agent.container.d && cat > ~/.config/moshiro/agent.env <<'EOF'
|
||
MOSHIRO_SERVER=https://panel.example.com
|
||
MOSHIRO_JOIN=jt_…
|
||
MOSHIRO_GENESIS=…
|
||
MOSHIRO_CONTROLLERS=…
|
||
# MOSHIRO_UPDATE_POLICY=manual
|
||
EOF
|
||
chmod 600 ~/.config/moshiro/agent.env && curl -fsSL 'https://panel.example.com/install/quadlet/agent.container' -o "$HOME/.config/containers/systemd/moshiro-agent.container" && echo "<sha256> $HOME/.config/containers/systemd/moshiro-agent.container" | sha256sum -c - && printf '[Container]\nImage=registry.oeo.one/moshiro/moshiro-agent:latest\n' > ~/.config/containers/systemd/moshiro-agent.container.d/10-image.conf && systemctl --user daemon-reload && systemctl --user start moshiro-agent
|
||
```
|
||
|
||
- 单元是静态的(`packaging/quadlet/moshiro-agent.container`,作为嵌入资源打进 Backend,由 `/install/quadlet/{agent}.container` 原样提供,不含任何一次性的值)。镜像仓库不写死在单元里,由命令里的 drop-in 指定,来自面板的 `MoShiro__NodeImageRegistry` 和 `MoShiro__ImageNamespace`(默认 `registry.oeo.one/moshiro`,开发镜像 `moshiro-dev`)。单元显式设置 `LD_LIBRARY_PATH=/usr/local/bin`,从 Alpine 镜像随包提供的 MsQuic sidecar 加载 QUIC 运行库。
|
||
- 只读根文件系统、丢掉全部能力、`no-new-privileges`、命名卷;用宿主网络(Agent 对外暴露端口,也靠它识别本机公网地址),并只读挂载 `/proc`、`/sys`、`/` 读主机指标。
|
||
- 身份和配置在命名卷 `moshiro-<role>-config` 里:镜像入口脚本(`packaging/container/entrypoint.sh`)第一次启动用 `MOSHIRO_SERVER`、`MOSHIRO_JOIN`(和信任根指纹、属主)注册并写配置,之后重启、换新镜像重建容器都不需要再给参数,所以注册成功之后可以把 env 里的 `MOSHIRO_JOIN` 删掉。缺了信任根指纹或属主就不注册(只有还没有信任根的老面板才用 `MOSHIRO_ALLOW_NO_OWNER=1`)。
|
||
- 开机自启:单元的 `[Install]` 已经写了,退出登录后继续运行需要管理员执行一次 `loginctl enable-linger <用户>`。
|
||
- 更新:见下一节,默认自动。想固定版本,把 drop-in 里的 `:latest` 换成版本号。回滚就是改回旧版本再 `systemctl --user restart moshiro-<role>`。
|
||
- 信任:节点信任镜像仓库(Podman 校验其 TLS 证书);单元和命令带有哈希,面板被攻破也改不了镜像地址;节点不验发布签名。
|
||
- 没有 Podman 4.6 或不是 Linux:用下面的 compose(Docker)、一行安装脚本或发布的二进制。
|
||
|
||
compose 安装(Agent,Docker;Podman 系请优先用上面的 Quadlet):管理端「添加 Agent」生成加入令牌之后,给出一段命令(面板地址、一次性码、信任根指纹和属主都是公开的值,没有私钥):建一个目录,把参数写进 `.env`(权限 600),下载面板提供的静态 compose 文件、核对 SHA-256,一致才启动:
|
||
|
||
```sh
|
||
mkdir -p moshiro-agent && cd moshiro-agent && cat > .env <<'EOF'
|
||
MOSHIRO_SERVER=https://panel.example.com
|
||
MOSHIRO_JOIN=jt_…
|
||
MOSHIRO_GENESIS=…
|
||
MOSHIRO_CONTROLLERS=…
|
||
MOSHIRO_IMAGE=registry.oeo.one/moshiro/moshiro-agent
|
||
EOF
|
||
chmod 600 .env && curl -fsSL 'https://panel.example.com/install/compose/agent.yaml' -o compose.yaml && echo "<sha256> compose.yaml" | sha256sum -c - && podman compose up -d
|
||
```
|
||
|
||
- compose 文件是静态的(`packaging/compose/agent|backend/compose.yaml`,作为嵌入资源打进 Backend,由 `/install/compose/{agent,backend}.yaml` 原样提供,不含任何一次性的值)。镜像是 `${MOSHIRO_IMAGE}:${MOSHIRO_VERSION:-latest}`,由 CI 渠道决定,不由面板决定;`MOSHIRO_IMAGE` 取自面板的 `MoShiro__NodeImageRegistry` 和 `MoShiro__ImageNamespace`(默认 `registry.oeo.one/moshiro`,开发镜像 `moshiro-dev`)。
|
||
- 只读根文件系统、丢掉全部能力、`no-new-privileges`、命名卷;Linux 上用宿主网络(Agent 对外暴露端口,也靠它识别本机公网地址,并只读挂载 `/proc`、`/sys`、`/` 读主机指标)。Agent 的 compose 只适用于 Linux;拨号方 在 macOS 和 Windows(Docker Desktop)上没有宿主网络,面板给的命令会在 `.env` 里写 `MOSHIRO_NETWORK_MODE=bridge`。
|
||
- 身份和配置在命名卷 `moshiro-<role>-config` 里:镜像入口脚本(`packaging/container/entrypoint.sh`)第一次启动用 `MOSHIRO_SERVER`、`MOSHIRO_JOIN`(和信任根指纹、属主)注册并写配置,之后重启、换新镜像重建容器都不需要再给参数,所以注册成功之后可以把 `.env` 里的 `MOSHIRO_JOIN` 删掉。缺了信任根指纹或属主就不注册(只有还没有信任根的老面板才用 `MOSHIRO_ALLOW_NO_OWNER=1`)。
|
||
- Podman 的开机自启要靠 `podman-restart.service`(`systemctl enable --now podman-restart`;rootless 用 `--user`,并 `loginctl enable-linger`);Docker 用 compose 里的 `restart: unless-stopped`。
|
||
- 更新:`podman compose pull && podman compose up -d`(Docker 用 `docker compose`),只重建镜像变了的容器,身份和配置不动,节点几十秒内重连。想固定版本,在 `.env` 里写 `MOSHIRO_VERSION=<版本号>`;想要更严,写成 `MOSHIRO_VERSION=<版本号>@sha256:<digest>`,digest 取自面板信任页或签名清单(那里有发布签名)。回滚就是改回旧版本再 `up -d`。
|
||
- 信任:compose 来自面板或 CI 渠道,面板被攻破也改不了镜像地址;但客户端不再验发布签名,拉取靠 TLS 加「注册表可信」这个假设(文档里本来就把 CI、Gitea、注册表放在可信范围内)。compose 不会自动更新(见下节),也没有管理端的「更新」列和确认更新按钮。
|
||
|
||
一行安装脚本(备用,Docker 或 Podman,Agent 共用):管理端弹窗中折叠着一行安装命令,先核对脚本哈希再执行,脚本向面板取签名清单、用内置发布公钥验签、按 digest 拉镜像:
|
||
|
||
```sh
|
||
f=$(mktemp) && curl -fsSL 'https://panel.example.com/install' -o "$f" && echo "<sha256> $f" | sha256sum -c - >/dev/null \
|
||
&& MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=jt_… MOSHIRO_GENESIS=… MOSHIRO_CONTROLLERS=… sh "$f" # Linux、macOS
|
||
# Windows(PowerShell):irm 'https://panel.example.com/install.ps1' 下载后用 Get-FileHash 核对,不一致就 throw,再带同样的环境变量运行
|
||
```
|
||
|
||
脚本是静态的(`packaging/install/install.sh` 和 `install.ps1`,嵌入 Backend,不替换任何值),所以它的 SHA-256 在一个版本内固定;角色、面板地址、代码和属主走环境变量。环境变量 `MOSHIRO_NAME`、`MOSHIRO_ENGINE=docker|podman`、`MOSHIRO_DRY_RUN=1`(只打印 run 命令);Podman 会顺便启用 `podman-restart`。
|
||
|
||
用脚本部署和更新(Agent;推荐用 Quadlet,Backend 只有 Quadlet 和 compose):`packaging/container/deploy-node.sh` 可以重复运行,每次都会拉取镜像并固定到 digest、把配置迁移到当前版本需要的样子(例如旧的 `listen.bind` 为 `0.0.0.0` 的 Agent 改成 `::`,这样 IPv6 线路才能建立)、重装单元和更新器,然后重启节点;已有的身份不会动,更新策略沿用上次的。
|
||
|
||
```sh
|
||
# 首次部署(需要 root;脚本和 packaging/ 里的 systemd/、update/ 放在一起)
|
||
sudo sh deploy-node.sh agent --image-repo registry.oeo.one/moshiro/moshiro-agent --server https://<backend> --join <token> --name <name>
|
||
sudo sh deploy-node.sh agent --image-repo registry.oeo.one/moshiro/moshiro-agent --server https://<backend> --join <Agent 令牌> --name <name>
|
||
# 之后更新:不带参数(旧版 拨号方 的 agent.json 没有 fleet 段,加上 --server --join --name 重新运行即可,旧文件备份为 agent.json.old)
|
||
sudo sh deploy-node.sh agent
|
||
```
|
||
|
||
Agent 的 `public_host` 和 `ipv6_host` 由 Agent 上报自动识别(需要宿主网络,Quadlet 单元和一行安装脚本默认如此,监听 `::`),IPv6 不用额外配置。
|
||
|
||
更新见「Podman(Quadlet)节点的更新」:节点退出再被拉起就是更新,默认自动。
|
||
|
||
Backend 用 `packaging/compose/backend/compose.yaml`(见上文「Backend 容器」),数据库和原始数据在命名卷 `moshiro-backend-data` 里。首次安装 Podman 与用户 subuid/subgid 映射需要管理员权限,之后普通用户运行无需 sudo。
|
||
|
||
已在 Fedora CoreOS 的 SELinux Enforcing 环境、Podman 6.0.2 rootless 下用同一套加固设置实测:Agent 注册、宿主指标、拨号方 登录和 TLS 连接正常,IPv4/IPv6 ICMP 目标任务收到有效回包,容器保持 `cap_drop: ALL`,未添加 NET_RAW。Linux ICMP 优先使用 datagram ping socket,不依赖外部 ping 程序;需要 `net.ipv4.ping_group_range` 包含容器映射后的 GID(测试环境为 `0 2147483647`)。宿主禁止普通用户 ping 时,需管理员配置该范围;例如 rootful Docker 的用户 GID 1654 可设置 `sysctl -w "net.ipv4.ping_group_range=1654 1654"`。rootless 应使用映射后的宿主 GID,不能直接套用容器 GID。单加 `cap_add: NET_RAW` 不保证非 root 用户获得有效 capability。有原始套接字权限的部署仍保留原有 Ping 回退路径。其他操作系统继续使用 System.Net.Ping。ICMP 目标任务当前由 Agent 执行,拨号方 没有独立目标任务入口。
|
||
|
||
### 定时重启
|
||
|
||
Agent 每天固定时刻有序退出一次,由服务管理器拉起(所有 systemd 单元和 OpenWrt 的 procd 脚本都是「总是重启」,正常退出也会被拉起)。节点只是自己退出,没有任何执行命令的能力;重启期间测量会中断几十秒。
|
||
|
||
- 在管理端控制:系统 → 设置 →「定时重启」,开关、时刻和时区(默认开,北京时间 04:30)。保存后马上通知所有在线节点,新连上的节点会补发。
|
||
- 节点本地覆盖:节点本地写了就以本地为准(面板的设置对它不生效)。`agent.json` 或 `agent.json` 里加 `"restart_at": "04:30+08:00"`,或者设置环境变量 `MOSHIRO_RESTART_AT`(优先)。写法 `HH:mm`(按 UTC)、`HH:mmZ` 或 `HH:mm±hh:mm`;写错了启动时就会报错。拨号方 的容器同样可以用环境变量。
|
||
- 启动后不满 10 分钟不会触发,所以重启之后不会立刻又重启一次。日志里有 `restart scheduled at=… source=panel|local` 和 `stopping result=scheduled_restart`。
|
||
- Windows 上没有自带的服务单元,需要服务管理器配置为退出后重启。
|
||
|
||
### Agent 的系统指标挂载
|
||
|
||
Agent 的 Quadlet 单元已经包含以下只读挂载和环境变量:
|
||
|
||
| 宿主来源 | 容器路径 | 配置 | 指标 |
|
||
| --- | --- | --- | --- |
|
||
| `/proc` | `/host/proc` | `HOST_PROC=/host/proc` | CPU、内存、累计网络流量、瞬时收发速率、运行时长 |
|
||
| `/sys` | `/host/sys` | `HOST_SYS=/host/sys` | 网卡驱动报告的链路速率 |
|
||
| `/` | `/host/root` | `HOST_ROOT=/host/root` | 宿主根文件系统容量和已用空间 |
|
||
| `/etc/os-release` | `/etc/os-release` | 无 | 宿主发行版 |
|
||
|
||
系统目录不加 `:z` 或 `:Z`,容器统一设置 `--security-opt label=disable`。读取失败的指标返回空值;配置了 `HOST_ROOT` 但没有挂载时不会改读容器 overlay 根分区。未设置这些环境变量时,原生进程继续读取本机默认路径。
|
||
|
||
磁盘指标是根文件系统容量,不是所有硬盘合计,也不采集磁盘 I/O。链路速率是参与流量统计的接口速率合计,单位 Mbit/s,不是互联网测速结果;任一接口不支持读取速率时显示空值。接口仍按名称排除回环、容器、网桥和部分隧道,复杂 bonding/VLAN 配置可能存在重复计数。主机名、硬件型号、MAC/IP 不作为资源指标上报。
|
||
|
||
拨号方 也上报同样的汇总数字(`agent.json` 的 `reporting.host_stats` 默认开,设为 `false` 就一个字也不上报,同时可以去掉 `/proc`、`/sys`、`/` 这三个挂载),所以 拨号方 的 Quadlet 单元和 Agent 一样挂载它们并设置 `HOST_*` 环境变量。Docker Desktop(macOS、Windows)上读到的是 Docker 的虚拟机,不是宿主系统本身;Windows 上直接运行的 Agent 用系统调用读 CPU、内存和物理网卡的流量(不读网卡名和地址);macOS 目前只有磁盘和运行时长。
|
||
|
||
## Podman(Quadlet)的更新(Agent、Backend,推荐)
|
||
|
||
节点跑成一个 Podman Quadlet 单元(`packaging/quadlet/moshiro-agent.container`,Podman 4.6 及以上),更新是销毁容器、重新拉最新镜像再启动:该路径下节点本身不下载、不替换文件,只是有序退出;Quadlet 生成的 systemd 服务用 `podman run --pull always` 启动,所以被拉起时先从镜像仓库拉最新的镜像。默认自动更新:面板发现新版本后,节点运行满 10 分钟(防止重启循环)就自己退出重启;想改成在管理端点「更新」才更新,在 env 文件里写 `MOSHIRO_UPDATE_POLICY=manual`。
|
||
|
||
```text
|
||
CI 推镜像到 registry ─▶ 面板收到 CI 签名的清单 ─▶ 转发给节点(只是「建议」)
|
||
节点:比对清单和自己的版本 ─▶ 心跳报「有新版本」─▶ 管理端显示
|
||
自动(默认):节点运行满 10 分钟 ─▶ 有序退出;手动(MOSHIRO_UPDATE_POLICY=manual):管理员点「更新」─▶ 面板请求 ─▶ 节点有序退出
|
||
然后:systemd 重启单元(Pull=always)─▶ 新镜像、新容器
|
||
```
|
||
|
||
- 安装(rootless,普通用户):管理端「添加 Agent」的第一条命令(Linux + Podman)就是它:env 写进 `~/.config/moshiro/<role>.env`(权限 600),从面板的 `/install/quadlet/<role>.container` 下载单元并核对 SHA-256,用一个 drop-in 把镜像仓库指向面板配置的(开发通道是 `moshiro-dev`),`systemctl --user start`。再让管理员执行一次 `loginctl enable-linger <用户>`。rootful 的路径和改法写在模板开头的注释里。身份和配置仍在命名卷 `moshiro-<role>-config`,换新镜像不丢。
|
||
- compose 不能自动更新:compose 的 `restart: unless-stopped` 是引擎直接重启同一个容器,不会拉新镜像(`pull_policy: always` 只在 `compose up` 时生效),容器自己又没法重建自己,所以 compose(Docker)仍是手动 `compose pull && compose up -d`;要自动更新就用 Quadlet。
|
||
- 信任:信任镜像仓库(`registry.oeo.one`,Podman 校验它的 TLS 证书),并按标签拉取(默认 `latest`,想固定版本就把模板里的标签换成版本号)。节点不验证清单:它不安装任何东西,最多被管理员请求一次重启,和定时重启是同一种能力。
|
||
- 只有设了 `MOSHIRO_UPDATE=restart` 的节点才报告更新状态(模板里写好了):别的部署方式退出之后不会重新拉取,开了也不会更新。
|
||
- 别的自动更新方式:另外启用 `podman-auto-update.timer` 并在模板的 `[Container]` 里加 `AutoUpdate=registry`,Podman 按计划比对镜像 digest 并重启,不依赖面板。
|
||
- 回滚:新镜像起不来时 systemd 一直重试;把模板里的标签改回旧版本再 `systemctl --user restart` 即可。
|
||
- 测试:`packaging/quadlet` 里的模板用 `/usr/libexec/podman/quadlet -dryrun -user` 检查过(Podman 5.4);节点一侧的逻辑见 `RestartUpdaterTests`。
|
||
|
||
## 宿主机更新器(一行脚本装的节点)
|
||
|
||
节点和 Backend 的更新都走上面的 Quadlet。下面的宿主机更新器现在只服务一行安装脚本装的节点(它们没有 systemd 的 Pull=always 可用)。
|
||
|
||
更新器支持 rootless Podman 用户服务、rootful Podman 和 Docker 系统服务。配置、身份和更新请求通过命名 volume 传递,不需要指定宿主数据目录。短时辅助容器读取请求、写入独立状态 volume;辅助镜像必须已在本机拉取,更新器不会自动拉取辅助镜像。配置、公钥和已安装版本记录留在容器外。
|
||
|
||
Rootless Podman 普通用户安装示例(先将对应 rootless 服务单元复制到用户 systemd 目录):
|
||
|
||
```sh
|
||
sh packaging/update/install-updater.sh agent --user \
|
||
--volume moshiro-agent-config --status-volume moshiro-agent-status \
|
||
--helper-image registry.oeo.one/moshiro/moshiro-agent:<version> \
|
||
--image-repo registry.oeo.one/moshiro/moshiro-agent \
|
||
--unit moshiro-agent-rootless.service --policy auto
|
||
```
|
||
|
||
使用 Docker 时以管理员运行相同安装命令,去掉 `--user`,加 `--engine docker`,单元选择 `moshiro-agent-docker.service`。rootful Podman 去掉 `--user`,单元选择 `moshiro-agent-container.service`。拨号方 替换角色名。初始化对应 `<role>.image` 为 `IMAGE=<当前镜像引用>` 后启动容器单元。辅助镜像可用本机已有的 digest 引用,避免标签变化。Docker rootless 更新尚未适配。
|
||
|
||
命名 volume 的更新服务由 systemd timer 每 30 秒检查一次。`--policy auto` 验签后自动应用,默认 `manual` 等待面板写入应用请求。清单必须通过 ECDSA P-256 签名校验,组件、镜像仓库和递增 seq 都必须匹配本机配置;镜像按签名清单中的 digest 拉取。更新后观察服务 20 秒,启动失败或崩溃循环会恢复旧镜像引用并重启。无效签名、重放清单、拉取失败不会切换当前镜像。同一组件的更新通过 flock 串行处理。
|
||
|
||
更新器依赖 Linux、systemd、OpenSSL、util-linux/flock 和所选引擎。用户部署把配置、公钥和镜像引用放在 `~/.config/moshiro/`、版本记录放在 `~/.local/state/moshiro-update/`;系统部署使用 `/etc/moshiro-update/` 和 `/var/lib/moshiro-update/`。私有仓库的 `--authfile` 对 Podman 使用其认证文件,对 Docker 使用标准 `config.json`,不改写全局 Docker 配置。旧 `--data-dir` 安装方式保留兼容,但新部署统一使用 `--volume`。
|
||
|
||
发布私钥只放在 Woodpecker 的 `release_signing_key` secret;发布公钥位于 `packaging/update/release-key.pub.pem`。`RELEASE_API_URL` 指向面板,CI 把签名清单 POST 到 `/api/release/offer`:这个入口不需要令牌,面板用内嵌的发布公钥验签,签名无效就拒绝,所以 CI 不需要在面板里持有任何凭证(清库或重装后照常登记,也没有长期令牌可偷)。面板保存并转发签名清单,Agent 将它与更新请求写进配置 volume;更新器验签后才调用容器引擎和 systemd。管理端仍能看到可更新、更新中、已回滚和无效清单状态。纯二进制更新器尚未实现。
|
||
|
||
自有发布密钥(自己构建、自己发布的部署;默认关闭):管理端「系统 → 信任 → 可信资产来源」。官方 CI 发布密钥始终信任,不能关;另外导入自己的发布公钥(P-256 的 PEM,面板拒绝私钥,私钥留在你自己的机器或 CI 密钥库)之后,用你的私钥签的清单也会被面板登记和分发。签名方法:`openssl dgst -sha256 -sign release.key -out manifest.sig.bin manifest.json`,再 `base64 -w0` 得到签名;清单是一行扁平的 JSON(字段见 `ci/release-manifest.sh`),交给面板用 `POST /api/release/offer`,或者像官方那样放进你镜像仓库里的清单镜像。节点宿主机的更新器只认本机的公钥文件,所以用你自己的密钥发布时,还要把你的公钥放进这些节点的 `release-key.pub.pem`,并且清单里的 `image` 必须等于它本机配置的镜像仓库。停用自有密钥时,只有它签过的已登记清单会被丢掉。
|
||
|
||
换发布密钥(泄露或定期轮换,不用重装节点):
|
||
|
||
1. 生成新密钥对;CI 给发布流水线加 `RELEASE_NEXT_KEY`(新公钥 PEM),仍用旧私钥签名。清单里会带 `next_key`(SPKI DER 的 base64)。
|
||
2. 宿主机更新器验过这份清单后,把新公钥存到自己的状态目录(`<组件>.key.pem`),之后只认新密钥,旧密钥签的清单一律拒绝。清单里的 `next_key` 不是合法公钥就整份拒绝。
|
||
3. 下一个版本起把 `release_signing_key` 换成新私钥、去掉 `RELEASE_NEXT_KEY`,并把 `packaging/update/release-key.pub.pem`、`install.sh`、`install.ps1` 里的公钥换成新的(安装脚本哈希随清单变化)。
|
||
4. 还没收到过渡清单的节点(离线很久)会因为签名对不上而停在旧版本,需要手动换 `release-key.pub.pem`;旧密钥泄露时,这些节点是唯一的暴露面。
|
||
|
||
一行安装:页面给的命令先下载静态安装脚本(`/install`,所有人拿到的字节都一样)并核对它的 SHA-256,一致才执行;面板地址、角色、一次性码、信任根指纹(`MOSHIRO_GENESIS`)和属主(`MOSHIRO_CONTROLLERS`)走环境变量,都是公开值,没有私钥。脚本向面板取 CI 签名的发布清单(`/install/manifest`、`/install/manifest.sig`),用脚本里内置的发布公钥(`packaging/update/release-key.pub.pem`)验签,再按清单里的 digest 拉镜像(`镜像仓库@sha256:…`);签名对不上、组件不对或 digest 格式不对就拒绝安装;需要本机有 `openssl`(Windows 用 .NET 自带的)。面板还没有这个组件的已签名清单时,设 `MOSHIRO_ALLOW_UNVERIFIED=1` 和 `MOSHIRO_IMAGE=镜像` 才会安装。CI 把脚本的 SHA-256 写进签名清单和发布页,面板的「系统 → 信任」页显示它和清单是否一致。详见 [access.md](../security/access.md) 第 1 节。
|
||
|
||
## 签名清单放在镜像仓库里(更新和首次部署都不依赖面板)
|
||
|
||
CI 给每个镜像的 digest 签名之后,除了(面板在线时)登记给面板,一定会把签名清单推到镜像仓库:`<镜像仓库>-manifest:<通道>`,例如 `registry.oeo.one/moshiro-dev/moshiro-backend-manifest:dev`、`registry.oeo.one/moshiro/moshiro-backend-manifest:release`。它是一个没有任何文件层的小镜像,清单和签名放在标签 `org.moshiro.manifest`(base64)和 `org.moshiro.signature` 里。`dev` 通道每次 `[CI]` 构建都会更新,`release` 通道只在打 tag 时更新,所以不发 Release 的构建照样能被更新到。
|
||
|
||
```text
|
||
信任锚点:仓库里的发布公钥 packaging/update/release-key.pub.pem(随安装脚本和 deploy-node.sh 分发)
|
||
取清单的人:宿主机更新器(定时)、deploy-node.sh(首次部署):pull 清单镜像 → 读标签 → 本机公钥验签 → 按清单里的 digest 拉真正的镜像
|
||
仓库、网络、面板都不需要可信:被改只会让验签失败;面板不在线、清库、没有 CI 都不影响
|
||
```
|
||
|
||
- 更新器:配置里 `FEED=auto`(默认)时先看仓库里的清单,验签通过才用;取不到或验不过就用面板放进信箱的那份。`MANIFEST_REF` 可以指定清单镜像,`FEED=off` 关掉;通道取配置里的 `CHANNEL`,没有就按仓库路径推断(`moshiro-dev` → dev,`moshiro` → release)。仓库里的清单验签失败而信箱里又没有东西时,状态会报「无效清单」而不是悄悄当作没有更新。
|
||
- 面板自己取:安装脚本向面板要某个组件的签名清单时,面板里没有(清过库、CI 登记那一步网络失败)或者已经放了十分钟以上,面板就按 OCI 分发协议匿名读一次仓库里的清单镜像(只读清单和它的配置两个小 JSON,不拉镜像层),用内嵌的 CI 发布公钥验过签才登记;验不过(仓库或路上被改)就当没有。所以清库之后不用手工补登记,也不会被一份伪造的清单骗到。后台还每 5 分钟替 Agent 和 Backend 自己各取一次:CI 把清单登记给面板那一步是尽力而为的(网络不通只警告,流水线仍是绿的),没有这个后台轮询的话,面板收不到登记就看不到 Backend 的新版,宿主机上旧版的更新器也就一直报「最新」。仓库要匿名可读(`moshiro/`、`moshiro-dev/`)。
|
||
- 发版:打 `vMAJOR.MINOR.PATCH` tag 时,节点镜像(Agent 的多架构镜像)也和 Backend 一样签清单,放到 `moshiro/…-manifest:release`。
|
||
- 压缩包的校验和也签名:Release 里除了每个压缩包的 `.sha256`,还有汇总的 `SHA256SUMS` 和 `SHA256SUMS.sig`(用发布私钥签,base64 的 DER ECDSA P-256,和清单同一把密钥)。验证:`base64 -d SHA256SUMS.sig > s.bin && openssl dgst -sha256 -verify packaging/update/release-key.pub.pem -signature s.bin SHA256SUMS`,通过之后再 `sha256sum -c SHA256SUMS`。macOS 的 Developer ID 签名和公证、Windows 的 Authenticode 需要苹果和微软的证书,目前没有做。
|
||
- 首次部署:`deploy-node.sh` 默认就走这条路,不需要面板,也不用手动指定镜像(Backend 第一次部署时本来就没有面板)。`--tag` 指定标签时不验签并有警告;`--image` 直接用你给的引用。
|
||
- 别人自己部署后端,什么都不用改:仓库对匿名读开放(`moshiro/*` 和各 `*-manifest` 仓库),然后按 `packaging/quadlet/moshiro-backend.container` 的说明装 Backend(`MoShiro__PublicUrl` 写自己的域名),只是配置,不改代码、不需要自己的 CI;passkey 绑定在自己的域名上。自己 fork 并自建镜像的人要换自己的发布公钥和仓库地址(`--image-repo`)并重新构建,这是 fork 的正常代价。
|
||
- 测试:`bash packaging/update/tests/feed-test.sh`(假的 podman + 真的 openssl,覆盖篡改、别的密钥、旧 seq、换钥、仓库不可用回退、首次部署的验签)。
|
||
|
||
## Backend 的更新
|
||
|
||
Backend 和节点是同一种更新:`packaging/quadlet/moshiro-backend.container`(Quadlet,`Pull=always`、`Restart=always`、`Environment=MOSHIRO_UPDATE=restart`)。CI 推镜像并把 CI 签名的清单登记到面板,管理端显示「Backend 有新版本」,管理员点「更新并重启」(`POST /api/admin/backend/update`,带当时登记的清单 `seq`,清单变了就要重新确认);Backend 先把数据库备份到数据卷的 `backups/pre-update-<时间>-<seq>.db`(`VACUUM INTO`,保留最近 3 份,备份失败就不更新),再有序退出,systemd 重启单元并拉最新镜像。
|
||
|
||
- 不自动重启:Backend 指挥所有节点,重启会让它们全部断一两分钟,所以只提示,不像节点默认那样自动退出。
|
||
- 没有 `MOSHIRO_UPDATE=restart` 就不能从管理端更新(compose、一行脚本装的 Backend 没有重新拉取的机制),管理端会提示;这种部署继续用 `compose pull && compose up -d`。
|
||
- 通道:`MoShiro__UpdateChannel` 为 `auto` 时,镜像命名空间 `moshiro-dev` 选 `dev`,`moshiro` 选 `release`;没写命名空间就按当前运行的构建 tag 判断。
|
||
- 回滚:新版启动时会迁移数据库,旧版不认比自己新的库。要回旧版:停服务,把 `backups/pre-update-*.db` 复制成数据卷里的 `moshiro.db`(删掉旧的 `-wal`、`-shm`),把单元里的标签固定成旧版本号再启动。新版起不来时 systemd 会一直重试同一个镜像,没有自动回滚,也没有 `/healthz` 的探针(镜像里没有 curl)。
|
||
- 首次升级:还没有这个机制的旧版 Backend 不认 `MOSHIRO_UPDATE`,第一次要手动 `systemctl --user restart moshiro-backend`(Pull=always 拉到新镜像),之后就能从管理端更新。
|