docs: merge the Fleet docs into backend/fleet/README.md, add TODO.md, unify the Backend compose, fix stale references [CI SKIP]

This commit is contained in:
justme committed 2026-10-07 03:36:34 +08:00
1 parent a286a453f4
commit c974a7dddb
24 files changed
+218 -525

No files matched your search

+3 -7
View File
@@ -69,7 +69,7 @@ Backend 用段文件保存原始数据,用 SQLite 保存索引、指标与配
- **测量数据面**:Probe 到 Agent 的 TLS 控制通道、UDP 和 TCP 数据通道(默认 `7443/tcp`、`7443/udp`、`7444/tcp`);会话用 `session_id` 和 MAC 认证、不绑定五元组,NAT 映射变化后自动跟随。
- **REST + SSE**:`/api/public`(展示,可匿名)、`/api/admin`(管理);OpenAPI 在 `backend/openapi/v1.json`,前端据此生成类型。
细节见 [`docs/protocol.md`](docs/protocol.md)、[`docs/fleet/link.md`](docs/fleet/link.md)、[`docs/api.md`](docs/api.md)、[`docs/enrollment.md`](docs/enrollment.md)。
细节见 [`docs/protocol.md`](docs/protocol.md)、[`backend/fleet/README.md`](backend/fleet/README.md)、[`docs/api.md`](docs/api.md)、[`docs/enrollment.md`](docs/enrollment.md)。
## 如何构建
@@ -97,7 +97,7 @@ Backend、Agent、Probe 以 OCI 镜像发布(Linux amd64、arm64),Agent
| 文档 | 内容 |
| --- | --- |
| [`docs/architecture.md`](docs/architecture.md) | 架构、数据链路、安全边界、设计决策 |
| [`docs/protocol.md`](docs/protocol.md)、[`docs/fleet/`](docs/fleet/overview.md) | 线协议、Fleet 节点框架(身份、连接、更新) |
| [`docs/protocol.md`](docs/protocol.md)、[`backend/fleet/README.md`](backend/fleet/README.md) | 线协议、Fleet 节点框架(身份、注册、连接) |
| [`docs/enrollment.md`](docs/enrollment.md) | 注册、身份、Probe 与 Agent 的接入 |
| [`docs/measurement.md`](docs/measurement.md)、[`docs/analysis.md`](docs/analysis.md) | 采集引擎与任务模型、分析层与指标 |
| [`docs/storage.md`](docs/storage.md)、[`docs/query.md`](docs/query.md) | 存储、展示数据查询 |
@@ -109,11 +109,7 @@ Backend、Agent、Probe 以 OCI 镜像发布(Linux amd64、arm64),Agent
## 待办
- **纯二进制节点的更新**:`fleet-updater`、`fleet-release`、Windows 服务和 OpenWrt(procd)适配器;设计见 [`docs/fleet/update.md`](docs/fleet/update.md)。容器节点的签名发布和宿主机更新已经实现。
- **更新的灰度和自动策略**:按标签分批发布;`POLICY=auto` 的线上验证;后端自身的更新。
- **OpenWrt 打包**:procd 脚本、ipk/apk 包、sysupgrade 保留列表,在 aarch64 路由器上长时间运行验证。
- **安全加固**:对 UDP 包、控制帧、RawChunk、JSON-RPC 做模糊测试;伪造签名和重放的专项测试。
- **以后**:告警(Webhook)、真实 QUIC 握手积木、pcap/eBPF 采集。
见 [`TODO.md`](TODO.md)。
## 许可证
+50
View File
@@ -0,0 +1,50 @@
# TODO
按主题记录还没做的事,做完就删掉对应的条目。已经实现的东西以 [`docs/`](docs/) 和代码为准。
## 测量
- IPv6
- 路由跳数、路径漂移和 trace
- 握手耗时:TCP 连接耗时,TLS / QUIC 握手耗时(QUIC 目前没有真实握手积木)
- 网页长截图
- pcap / eBPF 采集
## 任务与面板
- 单个 Agent 的任务(目前是线路模板加按 Agent 的覆盖,没有只属于某一个 Agent 的独立任务)
- 原生 webhook 投递;告警(基于 webhook)
- Agent 和 Probe 的定时重启(可选,例如早上四点)
## 部署
- 后端自动下载前端资源:用 Podman、Docker 部署时,默认拉取最新的前端资源(现在前端要自己构建,镜像里没有)。
## 访客页面
- 人机验证:访客页面要通过验证才能拉数据,前端访问时静默触发。候选 tiago2/cap 和 Cloudflare Turnstile。
## 更新
- 更新遥测(现在面板只有每个节点的「当前更新状态」)
- **回滚路径的实机测试**:容器节点更新后观察 20 秒、失败回滚,只有单元和脚本测试,没有在真实节点上触发过。
- **更新的灰度和自动策略**:按标签分批发布;`POLICY=auto` 的长期验证;Backend 自身的更新(Backend 目前没有更新器,要手动拉镜像)。
- **纯二进制节点的更新**(`fleet-updater`、`fleet-release`、Windows 服务和 OpenWrt procd 适配器)。设计要点,实现时照这个来:
- 信任来自签名:只接受离线发布密钥签名的清单,发布源可以是面板、GitHub Releases 或任意 HTTP 服务;面板被攻破也推不出恶意二进制。
- 本地说了算:更新策略(`auto` / `notify` / `pinned:<version>`)和信任的公钥写在节点本地配置里,面板只能「建议」;支持多把公钥以便轮换。
- 清单带 `app`、`channel`、`version`、`min_from`、各 RID 制品的 `sha256` 和大小、`key_id`、签名(对规范化 JSON 签名);本地记录已装过的最高版本,拒绝降级和旧清单重放。
- 流程:下载 → 校验 → 解包到 `versions/<v>/` → 原子切换 `current`(Linux 符号链接,Windows 目录联结)→ 重启服务 → 等健康信标确认 → 失败切回旧版本并把该版本标记为坏版本。
- 权限分离:更新进程只能写安装目录和重启被管理的服务,读不到节点身份私钥。
- OpenWrt:`keep_versions = 1`,下载前检查闪存剩余空间。
- 容器节点已经实现的那一套(清单签名、`seq` 防重放、按 digest 拉取、回滚)见 [`docs/deployment.md`](docs/deployment.md)。
## 安全
- **Agent、Backend、Probe 的零信任设计**:目前 Backend 是三者之间的信任中心,需要梳理哪些信任可以去掉。
- **extension**(Web SSH、TTY、更新遥测,敏感 operator):设计时必须保住「节点是纯采集器」这条边界,见 [`README.md`](README.md) 的项目边界。
- 对 UDP 包、控制帧、RawChunk、JSON-RPC 做模糊测试;伪造签名和重放的专项测试。
## 打包
- OpenWrt:procd 脚本、ipk/apk 包、sysupgrade 保留列表,在 aarch64 路由器上长时间运行验证。
- Windows 服务适配。
+1 -1
View File
@@ -18,7 +18,7 @@ Backend 是 MoShiro 的控制面和数据服务,负责 Agent 注册与连接
## 环境要求
使用 .NET SDK 10。仓库根目录的 `backend/global.json` 指定所需 SDK 版本及滚动策略。
使用 .NET SDK 10。仓库根目录的 `global.json` 指定所需 SDK 版本及滚动策略。
## 构建与测试
+1 -1
View File
@@ -14,7 +14,7 @@ Probe ──TLS 控制 / UDP、TCP 测量──> Agent ──长连接与数据
## 构建
需要 .NET SDK 10。为应用 `backend/global.json` 的 SDK 设置,请从 `backend/` 目录构建:
需要 .NET SDK 10。为应用 根目录的 `global.json` 的 SDK 设置,请从 `backend/` 目录构建:
```sh
cd backend
+101
View File
@@ -0,0 +1,101 @@
# Fleet:通用节点框架
「很多节点连到一个面板」这类产品都要解决同一批问题:节点怎么接入、怎么证明身份、怎么保持连接、怎么可靠上报、怎么接收配置。Fleet 把这些做成与业务无关的库,MoShiro 是它的第一个使用者;产品只写业务。Fleet 不引用任何 `MoShiro.*`,也不出现产品的名字、端口和协议前缀,由 `ArchitectureTests` 在构建时检查。
```text
┌──────────────────── 产品层(MoShiro:Protocol · Measurement · Agent · Probe · Backend)────────────────────┐
└────────────────────────────────────────────┬─────────────────────────────────────────────────────────────┘
│ 只依赖 Fleet
┌───────────────────────────── Fleet ────────┴─────────────────────────────────────────────────────────────┐
│ 节点侧(只依赖 BCL,NativeAOT) 服务端侧(ASP.NET Core) │
│ Fleet.Core Result、退避、有界队列、ID、令牌哈希 Fleet.Server.Abstractions IFleetStore 等接口 │
│ Fleet.Identity 密钥、签名、证书、KeyStore Fleet.Server.Enrollment 加入令牌、注册、审批 │
│ Fleet.Rpc JSON-RPC 信封、路由、二进制帧 Fleet.Server.Link wss 网关、在线表、状态下发 │
│ Fleet.Link 会话、认证、重连、发件箱、状态同步、点对点 │
│ Fleet.Hosting 模块宿主、配置、日志、服务外壳 工具:fleet-enroll(一次性注册) │
│ Fleet.Enrollment.Client 注册流程(给 fleet-enroll 用) │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
## 设计原则
1. **与业务无关**:产品通过扩展点接入:元数据类型、RPC 方法、发件箱消息类型、期望状态文档类型。
2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。
3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。
4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 序列化,不使用反射。
5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/deployment.md`](../../docs/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
## 身份
| 对象 | 算法 | 保存位置 |
| --- | --- | --- |
| 节点身份密钥 | ECDSA P-256(BCL 自带) | `<identity_dir>/node.key`(0600);服务端只保存公钥 |
| 点对点 TLS 证书 | 自签名 ECDSA P-256 | `<identity_dir>/tls.pfx`;对端按 SPKI SHA-256 指纹校验 |
| 签名串 | `"fleet|v1|<purpose>|" + 各字段` | 带用途前缀,防止同一个签名在不同场景被复用 |
用途:`auth`(连接服务端)、`peer`(点对点)、`enroll`(注册时的持有证明)。产品可以追加自己的用途,不能占用 `fleet` 保留的。
## 注册
```text
管理员 ── 创建加入令牌 {labels, max_uses, expires_at, auto_approve} ──> 服务端
<── 令牌明文(只返回一次)+ 安装命令
节点:fleet-enroll --server https://panel --join jt_xxx --name jp-1 --identity-dir /etc/myapp
├─ 生成身份密钥和 TLS 证书
├─ POST 注册端点 { join_token, name, pubkey, tls_spki, proof = 签名("enroll", 令牌哈希 + pubkey), node_version, meta }
│ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发产品的 IEnrollmentHook
└─ 写入 <identity_dir>/node.json { node_id, server, … },然后退出
```
- 同名节点已存在时,新注册进入 `pending`,管理员选择「接管」(旧记录换成新公钥,历史数据保留)或「作为新节点」。
- 节点状态:`pending`(能连接,但不下发状态、不接收上报)、`active`、`disabled`(拒绝连接)。
## RPC
- JSON-RPC 2.0;方法名 `领域.动作`,`fleet.*` 保留给框架,产品用自己的前缀(MoShiro 是 `moshiro.*`)。
- 二进制帧 `u8 channel | 正文`:`0`–`15` 框架保留,产品从 `16` 开始。
- 错误:`error.data.code` 为 snake_case,框架的以 `fleet_` 开头,产品可以扩展。
- 与传输无关:同一套路由跑在 wss 上,也跑在 TLS 帧(`u32 长度 | u8 类型 | 正文`)上。
| 保留方法 | 方向 | 说明 |
| --- | --- | --- |
| `fleet.challenge` / `fleet.auth` | 服务端→节点 / 节点→服务端 | 签名认证(`auth` 用途,nonce 30 秒内有效) |
| `fleet.hello` | 节点→服务端 | 版本、能力、本地策略摘要、当前时间 → 返回时钟偏差;产品的元数据也放在这里 |
| `fleet.heartbeat` | 节点→服务端 | 每 10 秒一次,产品可以附加计数 |
| `fleet.state.sync` | 服务端→节点 | `{version, doc}` → `{applied_version, rejected[]}` |
| `fleet.outbox.ack` | 服务端→节点 | `{ids[], duplicates[]}` |
| `fleet.revoked` | 服务端→节点 | 节点被禁用或接管,随后断开 |
## 连接
```text
connect(wss) → fleet.challenge → fleet.auth → fleet.hello → 状态同步 → 正常运行
▲ │
└──── 指数退避重连(1 s → 60 s,带抖动;401 或 revoked 时停止重试)◀─ 断开
```
本地策略:`allow_methods`、`max_frame_bytes`、`max_inflight`。
**可靠发件箱**:产品 `EnqueueAsync` → 有界队列(按字节和条数;满时 `DropOldest` 或 `Reject`)→ 发送 → 服务端收件箱(`IInbox`)持久化后 `fleet.outbox.ack` → 移除。至少一次,按 `msg_id`(UUIDv7)去重;在途数据默认限 4 MiB;丢弃会累计缺口,通过 `OnGap` 通知产品。只在内存里,不落盘。
**期望状态同步**:服务端 `IStateProvider<TDoc>` 给每个节点算 `{version, doc}`,变化时 `NotifyChanged` 推送;节点 `StateHandler<TDoc>` 调和后返回 `{applied_version, rejected[]}`。下发的是**完整快照**,天然幂等,重连后服务端总会重发当前版本。`rejected[]` 带结构化原因(`unsupported`、`budget_exceeded`、`policy_denied`),面板可以直接展示。
## 点对点会话
```text
节点 A(发起方,如 Probe)── TLS(按 SPKI 校验证书)──> 节点 B(监听方,如 Agent)
<── fleet.challenge ──
── fleet.auth(peer 用途,A 的私钥签名)──>
B 用服务端下发的对端公钥列表验签(经 state.sync 下发),之后复用同一套 Rpc:路由、二进制通道、发件箱
```
对端公钥和允许连接的节点由产品通过期望状态下发给监听方,Fleet 只管认证和会话,拓扑由产品决定;UDP、原始 TCP 数据面由产品自己实现,Fleet 只提供会话密钥派生和首帧 MAC 的辅助函数。
## 宿主与服务端抽象
- `ModuleHost`:按依赖顺序启动模块、相反顺序停止,隔离故障,重启时退避。
- `NodeConfig`:源生成 JSON 读取,环境变量覆盖,启动时校验。
- `ConfigStore`:常驻进程里**唯一**允许写文件的地方,只能写配置目录,写入原子替换、权限 0600;父目录不存在会创建。
- `ServiceShell`:SIGTERM、Ctrl+C,systemd `sd_notify`、OpenWrt procd(前台运行)和 Windows 服务。
- 服务端接口:`IFleetStore`(节点、令牌、公钥、状态版本)、`IStateProvider<TDoc>`、`IInbox`、`INodeEventSink`、`IEnrollmentHook`。MoShiro 把 `IFleetStore` 放进自己的 `moshiro.db`,和业务数据由同一个单写入者写入。
+1 -1
View File
@@ -17,7 +17,7 @@ Probe 使用面板生成的 `probe.json` 读取自身身份、面板地址(`se
## 构建
需要 .NET SDK 10。为应用 `backend/global.json` 的 SDK 设置,请从 `backend/` 目录构建:
需要 .NET SDK 10。为应用 根目录的 `global.json` 的 SDK 设置,请从 `backend/` 目录构建:
```sh
cd backend
@@ -3,7 +3,7 @@ using System.Xml.Linq;
namespace MoShiro.Backend.Tests;
/// <summary>
/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制,等价于文档里的 <c>eng/check-deps</c>。
/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制。
/// 读取仓库里所有 .csproj 和源文件,违反规则就失败。
/// </summary>
public sealed class ArchitectureTests
+3 -2
View File
@@ -12,7 +12,7 @@ services:
MoShiro__InitialAdminUser: ${MOSHIRO_ADMIN_USER:-admin}
MoShiro__InitialAdminPassword: "${MOSHIRO_ADMIN_PASSWORD:?Set MOSHIRO_ADMIN_PASSWORD in the environment}"
volumes:
- moshiro-data:/data
- moshiro-backend-data:/data
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
@@ -23,4 +23,5 @@ services:
stop_grace_period: 30s
volumes:
moshiro-data:
moshiro-backend-data:
name: moshiro-backend-data
+1 -5
View File
@@ -134,9 +134,6 @@
| GET | `/api/admin/events?agent_id=&kind=&from=&to=` | 事件 |
| GET | `/api/admin/analysis/recompute` | 指标重算进度 |
| GET/POST/DELETE | `/api/admin/api-tokens` | 作用域为 `read`、`admin` 或 `release`(只能登记发布清单,给 CI 用,什么都读不了);明文只显示一次 |
| GET | `/api/admin/releases` | 各应用、各通道的当前版本,以及节点的版本分布(由 `Fleet.Server.Releases` 提供) |
| 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/POST | `/api/admin/releases` | CI 签名的发布清单:POST 登记(`{manifest, signature}`,面板不验签、原样转给在线的节点和之后连上的节点),GET 看每个组件最新的一份 |
@@ -156,9 +153,8 @@
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/api/agent/v1/enroll` | 由 `MapFleetEnrollment` 提供(使用加入令牌认证),见 [fleet/link.md](fleet/link.md) 第 3 节 |
| POST | `/api/agent/v1/enroll` | 由 `MapFleetEnrollment` 提供(使用加入令牌认证),见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「注册」一节 |
| GET | `/api/agent/v1/rpc` | 由 `MapFleetLink` 提供,升级为 WebSocket;MoShiro 的方法见 [protocol.md](protocol.md) |
| GET | `/fleet/releases/{app}/{channel}/latest.json`、`/fleet/releases/{app}/{version}/…` | 由 `MapFleetReleases` 提供的公开发布源(清单和制品),供 `fleet-updater` 轮询;也可以改用 GitHub Releases |
## 5. 运维端点
+5 -5
View File
@@ -53,9 +53,9 @@
| 主题 | 要点 | 文档 |
| --- | --- | --- |
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主、更新都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [fleet/overview.md](fleet/overview.md) |
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../backend/fleet/README.md) |
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](enrollment.md) |
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。容器节点(Agent、Probe)已实现:CI 对镜像 digest 签名,宿主机上的 `moshiro-update` 验签、按 digest 拉取、重启;纯二进制的 `fleet-updater` 还只是设计 | [deployment.md](deployment.md)、[fleet/update.md](fleet/update.md) |
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。容器节点(Agent、Probe)已实现:CI 对镜像 digest 签名,宿主机上的 `moshiro-update` 验签、按 digest 拉取、重启;纯二进制节点的更新是待办([TODO.md](../TODO.md)) | [deployment.md](deployment.md) |
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) |
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) |
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) |
@@ -97,7 +97,7 @@ Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问
- **本地硬上限**写在节点本机的配置文件中,Backend 下发的任务如果超出,节点直接拒绝:允许访问的目标网段(默认为空,即不开放目标任务)、总码率和包速率上限、允许监听的端口。
- **构建期强制**:常驻节点进程使用 `BannedSymbols.Node.txt`,禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](project-layout.md) 第 7 节。
- **权限分离**:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制安装的 `fleet-updater` 还只是设计。
- **权限分离**:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新是待办(见 [TODO.md](../TODO.md))。
- **容器运行**:Agent、Probe 的镜像基于 Alpine(musl,NativeAOT),宿主机用 rootful podman 加 systemd 运行:UID/GID 1654、只读文件系统、`--cap-drop all`、`no-new-privileges`、`/tmp` 用 tmpfs;配置和身份放在挂载的数据目录里。
- **最小权限运行**:非 root 用户;只在需要时授予 `CAP_NET_RAW`(ICMP);默认端口都高于 1024,不需要 `CAP_NET_BIND_SERVICE`,只有在 Agent 本地显式开启 443 时才授予。
- **身份**:节点使用本地生成的 ECDSA P-256 密钥对签名认证(Probe 的密钥由面板生成、只返回一次,同一把用来连面板和连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
@@ -142,9 +142,9 @@ Kind 目录编译在二进制中,节点不接受远程下发代码(安全边
| D11 | API 分为 public / admin / agent 三层;实时数据用 SSE | 前端只读场景足够;反向代理和 CDN 更友好 |
| D12 | 单仓库,三个独立项目加 Web(展示端、管理端);契约以 C# 代码为真源 | 改一处,编译期即可发现不一致 |
| D13 | Probe/Agent 运行时只依赖 BCL;.NET 10 NativeAOT | 轻量 |
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;`samples/EchoNode` 作为第二个使用者,防止过度设计 |
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
| D15 | 注册独立为 `fleet-enroll` 工具;常驻进程不包含注册代码 | 常驻进程去掉 HttpClient,边界更纯 |
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。容器节点已实现(宿主机 `moshiro-update`,清单里是镜像 digest);纯二进制的 `fleet-updater` 待做 | 面板被攻破时也无法推送恶意二进制;可以同时管理多个产品 |
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。容器节点已实现(宿主机 `moshiro-update`,清单里是镜像 digest);纯二进制节点的更新见 [TODO.md](../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
| D17 | Probe 支持 OpenWrt:正式支持 `linux-musl-x64`、`linux-musl-arm64`;armv7 视工具链验证结果决定;MIPS 不支持,改为在局域网设备上运行 Probe | 由用户确认;NativeAOT 不支持 MIPS |
| D18 | 默认测量端口不使用 443:控制 7443/tcp,UDP 数据 7443/udp,TCP 数据 7444/tcp;443 只在 Agent 本地显式开启时才允许使用 | 由用户确认;避免与 VPS 上的真实服务冲突;结果会记录实际端口 |
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 由用户确认;5 分钟、15 分钟这类视图足够精细且快 |
+3 -3
View File
@@ -75,15 +75,15 @@
## 9. 纯采集边界(Probe/Agent 与 Fleet 节点侧)
- 由 `BannedSymbols.Node.txt` 在编译期强制:禁止启动进程、动态加载程序集或本机库、使用 `HttpClient`、在配置目录以外写文件。唯一的豁免点是 `Fleet.Hosting.ConfigStore`,豁免必须写 `#pragma` 并附上理由注释。注册和更新分别在 `fleet-enroll` 和 `fleet-updater` 中完成,常驻进程不需要这些能力。
- 由 `BannedSymbols.Node.txt` 在编译期强制:禁止启动进程、动态加载程序集或本机库、使用 `HttpClient`、在配置目录以外写文件。唯一的豁免点是 `Fleet.Hosting.ConfigStore`,豁免必须写 `#pragma` 并附上理由注释。注册在 `fleet-enroll` 中完成,更新由节点宿主机上的更新器完成,常驻进程不需要这些能力。
- 节点不能新增任何“由远端触发的非采集动作”对应的 RPC 方法。凡是新增 RPC 方法的 PR,都要在描述中说明它不会扩大权限边界。
- Backend 下发的任何参数都要先经过 `ParamSpec` 校验,再经过本地硬上限(`BudgetGuard`)检查,**两道校验都不能省略**。
- 日志中不能出现私钥、签名、`session_key`、令牌或 MAC。
## 9.1 Fleet 的通用性
- `backend/fleet/**` 不能引用任何 `MoShiro.*`,也不能出现产品相关的概念、命名或默认值(例如端口 7443、`moshiro` 字样),由 `scripts/check-deps` 和代码评审把关。
- Fleet 的每个公开 API 都要在 `samples/EchoNode` 或 `EchoServer` 中有用法。新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。
- `backend/fleet/**` 不能引用任何 `MoShiro.*`,也不能出现产品相关的概念、命名或默认值(例如端口 7443、`moshiro` 字样),由 `ArchitectureTests` 和代码评审把关。
- 新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。
- Fleet 永远不提供 exec、shell、文件读写或远程下发代码的方法,这条规则不接受例外。
- 公开 API 的变更记录在 `PublicAPI.Unshipped.txt` 中;PR 描述需要说明是否属于破坏性变更。
+2 -2
View File
@@ -32,7 +32,7 @@ Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/moshiro/moshiro-backe
docker compose up -d
```
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `moshiro-data`。首次启动成功创建管理员后,改密应在管理端完成。
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在命名卷 `moshiro-backend-data`(Docker 和 Podman 通用,`podman compose` 也可以用同一份文件)。首次启动成功创建管理员后,改密应在管理端完成。
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 文档在普通开发构建中生成。
@@ -208,7 +208,7 @@ Probe 在 `packaging/compose/probe` 下,将面板下载的身份文件通过
纯 Compose 使用 `compose pull && compose up -d` 更新镜像;面板自动更新使用下述 systemd 单元和更新器。Rootless Podman 使用 `moshiro-<role>-rootless.service`,安装到 `~/.config/systemd/user/`,镜像文件为 `~/.config/moshiro/<role>.image`;rootful Podman 使用 `moshiro-<role>-container.service`,Docker 使用 `moshiro-<role>-docker.service`,安装到 `/etc/systemd/system/`,镜像文件为 `/etc/moshiro-update/<role>.image`。所有单元均使用相同的配置和状态 volume 名称。自动启动分别使用 `systemctl --user enable --now <unit>` 或 `sudo systemctl enable --now <unit>`。Rootless 在退出登录后继续运行需管理员执行一次 `loginctl enable-linger <用户名>`。
Backend 的 `packaging/compose/backend/compose.yaml` 使用 `moshiro-backend-data` 保存数据库和原始数据,设置 `MOSHIRO_INITIAL_ADMIN_PASSWORD` 后即可启动。首次安装 Podman 与用户 subuid/subgid 映射需要管理员权限,之后普通用户运行无需 sudo。
Backend 用根目录的 `compose.yaml`(见上文「Backend 容器」),数据库和原始数据在命名卷 `moshiro-backend-data` 里。首次安装 Podman 与用户 subuid/subgid 映射需要管理员权限,之后普通用户运行无需 sudo。
已在 Fedora CoreOS 的 SELinux Enforcing 环境、Podman 6.0.2 rootless 下用同一份 Compose 实测:Agent 注册、宿主指标、Probe 登录和 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 执行,Probe 没有独立目标任务入口。
+7 -7
View File
@@ -1,6 +1,6 @@
# MoShiro 的注册与身份
通用的注册、身份、认证机制由 Fleet 提供,见 [fleet/link.md](fleet/link.md) 第 2~3 节。本文只描述 **MoShiro 在 Fleet 之上的约定**:Agent 注册后如何自动接入测量、Probe 的身份以及 Agent 列表同步,以及节点本地的硬上限。
通用的注册、身份、认证机制由 Fleet 提供,见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「身份」「注册」两节。本文只描述 **MoShiro 在 Fleet 之上的约定**:Agent 注册后如何自动接入测量、Probe 的身份以及 Agent 列表同步,以及节点本地的硬上限。
## 1. 身份一览
@@ -10,7 +10,7 @@
| Probe | Fleet 节点身份(同时用于连面板和连 Agent) | 面板生成 `probe.json`(可以有多个 Probe,每个独立吊销) | 公钥 |
| 面板用户 | 账号 + 密码(PBKDF2-SHA256) | 面板 | 密码哈希 |
| API 令牌 | 随机高熵令牌,作用域为 `read` 或 `admin` | 面板 | SHA-256 |
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在节点宿主机的更新器里配置公钥(容器节点是 `moshiro-update`,见 [deployment.md](deployment.md);纯二进制方案的 `fleet-updater` 还只是设计),**面板不保存私钥,也不验签** |
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在节点宿主机的更新器里配置公钥(容器节点是 `moshiro-update`,见 [deployment.md](deployment.md);纯二进制节点的更新是待办),**面板不保存私钥,也不验签** |
## 2. Agent 接入流程
@@ -19,13 +19,13 @@
<── 安装命令:
curl -fsSL https://panel/install.sh | sh -s -- --join jt_xxx --name jp-1
install.sh(只做安装,不包含任何远程控制功能;下面的更新器部分是纯二进制方案的设计,还没有实现,容器节点的做法见 deployment.md)
├─ 下载 moshiro-agent、fleet-enroll、fleet-updater,并验证发布签名
├─ 创建系统用户:moshiro(运行 Agent)、fleet-update(运行更新器)
install.sh(只做安装,不包含任何远程控制功能;容器节点的做法见 deployment.md)
├─ 下载 moshiro-agent、fleet-enroll,并校验 SHA-256
├─ 创建系统用户 moshiro(运行 Agent)
├─ fleet-enroll --server https://panel --join jt_xxx --name jp-1 --identity-dir /etc/moshiro
│ └─ 注册元数据(MoShiro 定义):{ probe_tcp_port, probe_udp_ports, public_host?, kinds[] }
├─ 写入 agent.json(本地硬上限)和 /etc/fleet/updater.json
└─ 启用 moshiro-agent.service 和 fleet-updater.service
├─ 写入 agent.json(本地硬上限)
└─ 启用 moshiro-agent.service
面板收到 OnEnrolled(MoShiro 挂钩)
├─ public_host 未提供时,取注册请求的来源 IP
-137
View File
@@ -1,137 +0,0 @@
# Fleet:身份、注册与连接
## 1. 概览
```text
fleet-enroll(一次性运行)
│ ① 用加入令牌注册,生成身份
▼
┌────────── 节点 ──────────┐ ┌──────────── 服务端 ────────────┐
│ Fleet.Hosting │ ② wss 连接 │ Fleet.Server.Link │
│ └─ Fleet.Link ─────────────── 签名认证 ────>│ 连接注册表 · 在线事件 │
│ ├─ Rpc 路由 │<── state.sync ──│ 期望状态提供者(产品实现) │
│ ├─ 期望状态调和 │── outbox.push ─>│ 收件箱(产品实现,持久化后 ACK) │
│ └─ 可靠发件箱 │<── outbox.ack ──│ │
│ │ │ Fleet.Server.Enrollment │
│ Fleet.Link(点对点)──────── TLS + 签名 ────> 另一个节点(例如 Probe → Agent) │
└──────────────────────────┘ └────────────────────────────────┘
```
## 2. 身份(Fleet.Identity)
| 对象 | 算法 | 保存位置 |
| --- | --- | --- |
| 节点身份密钥 | ECDSA P-256(BCL 自带;Ed25519 不在 BCL 中) | `<identity_dir>/node.key`(0600);服务端只保存公钥 |
| 点对点 TLS 证书 | 自签名,ECDSA P-256 | `<identity_dir>/tls.pfx`;对端按 SPKI SHA-256 指纹校验 |
| 签名串 | `"fleet|v1|<purpose>|" + 各字段` | 带用途前缀,防止同一个签名在不同场景下被复用 |
用途前缀的取值:`auth`(连接服务端)、`peer`(点对点)、`enroll`(注册时的持有证明)。产品可以追加自己的用途(例如 `myapp.xxx`),但不能占用 `fleet` 保留的用途。
## 3. 注册(Fleet.Enrollment)
```text
管理员 ── 创建加入令牌 {labels, max_uses, expires_at, auto_approve, product_meta?} ──> 服务端
<── 令牌明文(只返回一次)+ 安装命令
节点:fleet-enroll --server https://panel --join jt_xxx --name jp-1 --identity-dir /etc/myapp
├─ 生成身份密钥和 TLS 证书
├─ POST <enroll 端点>
│ { join_token, name, pubkey, tls_spki, proof = 签名("enroll", join_token 哈希 + pubkey),
│ node_version, meta(产品自定义的 JSON,经产品的 JsonTypeInfo 校验)}
│ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发 OnEnrolled 事件(产品挂钩)
└─ 写入 <identity_dir>/node.json { node_id, server, … },然后退出
```
- **名称冲突**:同名节点已存在时,新注册进入 `pending`,由管理员选择“接管”(旧记录换成新公钥,历史数据保留)或“作为新节点”。
- **节点状态**:`pending`(能连接,但不下发状态、不接收上报)、`active`、`disabled`(拒绝连接)。
- 注册逻辑只存在于 `fleet-enroll` 中,**常驻的业务进程不包含注册代码,也不使用 HttpClient**。
- 产品可以复用 `fleet-enroll` 本身,也可以把 `Fleet.Enrollment.Client` 嵌入自己的安装工具中。
## 4. RPC(Fleet.Rpc)
- 信封采用 JSON-RPC 2.0;方法名为 `领域.动作`。`fleet.*` 保留给框架,产品使用自己的前缀,例如 `moshiro.*`。
- 二进制帧:`u8 channel | 正文`。channel `0` 到 `15` 保留给框架,产品从 `16` 开始注册自己的二进制通道。
- 错误:`error.data.code` 为 snake_case 字符串,框架的错误码以 `fleet_` 开头,产品可以扩展。
- **与传输方式无关**:同一套路由既可以跑在 wss 上,也可以跑在 TLS 帧(`u32 长度 | u8 类型 | 正文`)上。
框架保留的方法:
| 方法 | 方向 | 说明 |
| --- | --- | --- |
| `fleet.challenge` / `fleet.auth` | 服务端→节点 / 节点→服务端 | 签名认证(`auth` 用途,nonce 30 秒内有效) |
| `fleet.hello` | 节点→服务端 | 节点版本、能力列表(框架 + 产品)、本地策略摘要、当前时间 → 返回时钟偏差 |
| `fleet.heartbeat` | 节点→服务端 | 每 10 s 一次;产品可以附加进程级计数 |
| `fleet.state.sync` | 服务端→节点 | `{version, doc}` → `{applied_version, rejected[]}` |
| `fleet.outbox.ack` | 服务端→节点 | `{ids[], duplicates[]}` |
| `fleet.revoked` | 服务端→节点 | 节点被禁用或接管,随后断开 |
## 5. 连接生命周期(Fleet.Link)
```text
connect(wss) → fleet.challenge → fleet.auth → fleet.hello → 状态同步 → 正常运行
▲ │
└──── 指数退避重连(1 s → 60 s,带随机抖动;401 或 revoked 时停止重试)◀─ 断开
```
- 能力协商:双方交换 capabilities;调用对方不支持的方法时,在本地直接返回 `fleet_unsupported`,不会发出请求。
- 节点本地策略:`allow_methods`(白名单,默认只包含本地已注册的方法)、`max_frame_bytes`、`max_inflight`。
## 6. 可靠发件箱(Outbox)
```text
产品 → outbox.EnqueueAsync(msg) → 有界队列(按字节和条数限制;满时按策略:DropOldest 或 Reject)
→ 发送(二进制通道或 JSON)→ 服务端收件箱处理并持久化 → fleet.outbox.ack → 从队列移除
```
- 投递语义为**至少一次**:每条消息都带有 `msg_id`(UUIDv7),服务端按此去重。
- 在途数据按字节限制(默认 4 MiB),保证服务端慢时不会把内存撑爆。
- 丢弃消息时会累计缺口计数,通过 `OnGap` 回调通知产品,由产品决定如何上报。
- 默认只保存在内存中,不落盘。产品如果需要持久化,可以自行实现 `IOutboxStore`(MoShiro 不使用)。
## 7. 期望状态同步(State Sync)
```text
服务端:IStateProvider<TDoc>.GetAsync(node) → {version, doc}
发生变化时调用 NotifyChanged(node) → 推送 fleet.state.sync
节点: IStateReconciler<TDoc>.ApplyAsync(doc) → {applied_version, rejected[]}
重连后,服务端总会重新推送当前版本
```
- 下发的是**完整快照**而不是命令,所以天然幂等,断线重连后也能自动收敛。
- `rejected[]` 为结构化原因,例如 `unsupported`、`budget_exceeded`、`policy_denied`,服务端可以把它展示在面板上。
## 8. 节点间点对点会话
```text
节点 A(发起方,例如 Probe)── TLS(按 SPKI 校验证书)──> 节点 B(监听方,例如 Agent)
<── fleet.challenge ──
── fleet.auth(peer 用途,节点 A 的私钥签名)──>
节点 B 用服务端下发的对端公钥列表验签(通常经 state.sync 下发)
之后使用同一套 Fleet.Rpc:方法路由、二进制通道、发件箱都可以复用
```
- 对端的公钥和允许连接的节点列表,由产品通过期望状态下发给监听方。Fleet 只负责认证和会话,拓扑由产品决定。
- 数据面的 UDP、原始 TCP 流等通道由产品自己实现。Fleet 只提供会话密钥派生(`session_key`)和首帧 MAC 的辅助函数。
## 9. 宿主(Fleet.Hosting)
- `ModuleHost`:按依赖顺序启动模块,按相反顺序停止;隔离故障,重启时退避。
- `NodeConfig<T>`:源生成 JSON 读取,支持环境变量覆盖,启动时校验。
- `ConfigStore`:业务进程唯一允许写文件的地方,只能写配置目录。
- `ServiceShell`:处理 SIGTERM 和 Ctrl+C,支持 systemd `sd_notify`、OpenWrt procd(前台运行,由 procd 负责重启)和 Windows 服务。
- `HealthBeacon`:进程完成启动并与服务端握手后,写出 `{app, version, pid, ok_at}` 到本地信标文件或命名管道,供 `fleet-updater` 确认新版本是否健康。
## 10. 服务端存储抽象
```csharp
public interface IFleetStore
{
ValueTask<JoinToken?> FindJoinTokenAsync(ReadOnlyMemory<byte> tokenHash, CancellationToken ct);
ValueTask<Result<NodeRecord>> CreateNodeAsync(NewNode node, CancellationToken ct);
ValueTask<NodeRecord?> GetNodeAsync(NodeId id, CancellationToken ct);
ValueTask UpdatePresenceAsync(NodeId id, Presence presence, CancellationToken ct);
// 令牌、审批、接管、禁用等
}
```
产品可以使用 `Fleet.Server.Sqlite`,也可以把这些表放进自己的数据库中。MoShiro 的做法是把它们放进自己的 `moshiro.db`,并由同一个单写入者负责写入。
-106
View File
@@ -1,106 +0,0 @@
# Fleet:通用节点框架
> **实现状态**:`Fleet.Update`、`Fleet.Server.Releases`、`fleet-updater`、`fleet-release` 还只是设计;容器节点的更新目前由宿主机上的 `moshiro-update` 脚本和 MoShiro 的 `ReleaseService` 完成(见 [deployment.md](../deployment.md))。其余组件已实现。
> 名称暂定。Fleet 与业务无关,MoShiro 是它的第一个使用者。
## 1. 定位
凡是“很多节点连到一个面板”的产品,都要解决同一批问题:节点怎么接入、怎么证明身份、怎么保持连接、怎么可靠上报、怎么接收配置、怎么更新自己。Fleet 把这些做成**通用库和工具**,产品只需要写业务。
```text
┌──────────────────────────── 产品层(各产品自己写) ─────────────────────────────┐
│ MoShiro: Protocol(Kind/任务) · Measurement(积木) · Agent · Probe · Backend │
│ 其他产品: YourApp.Node · YourApp.Server │
└──────────────────────────────────┬─────────────────────────────────────────────┘
│ 只依赖 Fleet;Fleet 不认识任何产品
┌───────────────────────────────── Fleet ───────────────────────────────────────┐
│ 节点侧(只依赖 BCL,支持 AOT) 服务端侧(ASP.NET Core) │
│ Fleet.Core Result/退避/有界队列 Fleet.Server.Enrollment 注册与审批 │
│ Fleet.Identity 密钥/签名/证书/存储 Fleet.Server.Link wss 网关/在线表 │
│ Fleet.Rpc JSON-RPC/路由/二进制帧 Fleet.Server.Releases 通道与灰度(可选)│
│ Fleet.Link 会话/认证/重连/能力/ Fleet.Server.Sqlite 存储实现(可选)│
│ 发件箱/状态同步/点对点 IFleetStore 存储抽象 │
│ Fleet.Hosting 模块宿主/配置/日志/服务外壳/健康信标 │
│ Fleet.Update 清单/验签/安装/切换/回滚 │
│ 工具:fleet-enroll(注册) fleet-updater(更新) fleet-release(CI 签名) │
└──────────────────────────────────────────────────────────────────────────────┘
```
## 2. 设计原则
1. **与业务无关**:Fleet 中不出现任何产品概念。产品通过扩展点接入:元数据类型、RPC 方法、发件箱消息类型、期望状态文档类型。`scripts/check-deps` 强制规定 `backend/fleet/**` 不能引用 `MoShiro.*`。
2. **安全默认值**:
- Fleet **永远不提供** exec、shell、文件读写、远程下发代码这类方法;
- 节点只响应本地注册过的方法,本地策略 `allow_methods` 还可以进一步收窄;
- 更新必须通过离线密钥签名验证,面板即使被攻破也推不出恶意二进制。
3. **权限分离**:注册(`fleet-enroll`,一次性运行)、常驻业务进程、更新(`fleet-updater`)是三个独立的进程,使用不同的系统用户,各自只拥有最小权限。
4. **NativeAOT 优先**:节点侧只依赖 BCL;泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 实现序列化,不使用反射。
5. **只抽取真正用到的**:每项能力都必须同时被 MoShiro 和 `samples/EchoNode` 使用,防止过度设计。
## 3. 组件
| 组件 | 侧 | 职责 | 详细 |
| --- | --- | --- | --- |
| `Fleet.Core` | 两侧 | `Result<T>`、错误码基类、退避、有界队列、ULID/UUIDv7 | — |
| `Fleet.Identity` | 两侧 | ECDSA P-256 身份密钥、自签名证书、SPKI 指纹、带用途前缀的签名串、`KeyStore`(文件权限 0600) | [link.md](link.md) §2 |
| `Fleet.Rpc` | 两侧 | JSON-RPC 2.0 信封、方法路由、请求与通知、二进制帧、错误映射;与传输方式无关 | [link.md](link.md) §4 |
| `Fleet.Link` | 节点 | 连接服务端(wss)或其他节点(TLS);认证、心跳、重连、能力协商、可靠发件箱、期望状态同步 | [link.md](link.md) |
| `Fleet.Enrollment.Client` | 节点 | 加入令牌注册流程(供 `fleet-enroll` 使用) | [link.md](link.md) §3 |
| `Fleet.Hosting` | 节点 | 模块宿主、配置(源生成 JSON)、logfmt 日志、服务外壳(systemd/procd/Windows)、健康信标 | [link.md](link.md) §8 |
| `Fleet.Update` | 节点 | 清单格式、验签、下载校验、多版本目录与原子切换、服务适配器、健康确认、回滚 | [update.md](update.md) |
| `Fleet.Server.Enrollment` | 服务端 | 加入令牌、注册端点、审批与接管 | [link.md](link.md) §3 |
| `Fleet.Server.Link` | 服务端 | wss 端点、签名认证、连接注册表、在线事件、状态下发、收件箱 ACK | [link.md](link.md) |
| `Fleet.Server.Releases` | 服务端(可选) | 托管清单、管理通道、灰度发布、汇总各节点版本 | [update.md](update.md) §6 |
| `Fleet.Server.Sqlite` | 服务端(可选) | `IFleetStore` 的 SQLite 实现(节点、令牌、公钥、状态版本) | — |
| `fleet-enroll` | 工具 | 节点注册:生成密钥、调用注册端点、写入配置 | [link.md](link.md) §3 |
| `fleet-updater` | 工具 | 节点更新守护进程,可以同时管理多个产品 | [update.md](update.md) |
| `fleet-release` | 工具 | 在 CI 或离线环境中生成并签名发布清单 | [update.md](update.md) §3 |
## 4. 目录结构
```text
fleet/ 以后可以原样拆成独立仓库并发布 NuGet 包
├─ Directory.Build.props 导入仓库根目录的 props
├─ src/
│ ├─ Fleet.Core/ Fleet.Identity/ Fleet.Rpc/ Fleet.Link/ Fleet.Hosting/ Fleet.Update/
│ ├─ Fleet.Enrollment.Client/
│ └─ Fleet.Server.Enrollment/ Fleet.Server.Link/ Fleet.Server.Releases/ Fleet.Server.Sqlite/
├─ tools/ fleet-enroll/ fleet-updater/ fleet-release/
├─ samples/ EchoNode/ EchoServer/ 最小示例:不依赖 MoShiro 的第二个使用者
├─ tests/
└─ docs → 仓库 docs/fleet/
```
## 5. 演进策略
- **现阶段**(已确认,暂不独立):放在 MoShiro 仓库的 `backend/fleet/` 目录中,通过项目引用使用,一处修改两边同时编译。
- **拆分条件**:公开 API 连续两个里程碑没有破坏性变更,并且 `samples` 能覆盖全部能力。满足后拆成独立仓库,按 semver 发布 NuGet 包,MoShiro 改为包引用。
- **公开 API 审查**:`backend/fleet/src/*` 的 public 类型使用 `PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt` 跟踪(使用 `Microsoft.CodeAnalysis.PublicApiAnalyzers`,只在构建期生效),任何变更都会在 PR 中显示出来。
## 6. 产品接入一览
**节点侧**:
```csharp
var identity = NodeIdentity.Load(config.IdentityDir); // 由 fleet-enroll 生成
var link = FleetLink.ToServer(config.Server, identity, MyRpcContext.Default)
.WithCapabilities(["myapp.v1"])
.WithState<MyDesired>(MyRpcContext.Default.MyDesired, reconciler) // 期望状态同步(可选)
.WithOutbox<MyReport>(MyReportCodec.Instance, maxBytes: 16 << 20) // 可靠上报(可选)
.On<DoThingParams, DoThingResult>("myapp.do_thing", HandleAsync); // 业务方法
var host = new ModuleHost(log).Add(link).Add(myModules);
await ServiceShell.RunAsync(host, HealthBeacon.From(config)); // 同时提供更新所需的健康确认
```
**服务端**:
```csharp
builder.Services.AddFleetServer(o => o.UseStore<SqliteFleetStore>())
.AddState<MyDesired>(desiredProvider) // 为每个节点计算期望状态
.AddInbox<MyReport>(reportHandler) // 持久化之后再 ACK
.OnNodeEvent(presenceHandler); // 上线、下线、版本变化
app.MapFleetEnrollment("/fleet/enroll");
app.MapFleetLink("/fleet/rpc");
app.MapFleetReleases("/fleet/releases"); // 可选
```
-112
View File
@@ -1,112 +0,0 @@
# Fleet:通用更新(Fleet.Update / fleet-updater)
> **实现状态**:容器节点(Agent、Probe)的更新已经实现,见 [deployment.md](../deployment.md) 的「容器节点的更新」:同样的信任模型(离线发布密钥签名、本机配置公钥和仓库、`seq` 防重放、面板只通知和请求),清单里是镜像 digest,更新器是宿主机上的 `moshiro-update`。本文下面描述的纯二进制方案(`Fleet.Update`、`fleet-updater`、`fleet-release`、健康信标)还没有实现。
## 1. 原则
- **与产品无关**:`fleet-updater` 通过本地配置声明它管理哪些应用,一台机器上可以同时管理多个产品。
- **信任来自签名,而不是来源**:只接受由**离线发布密钥**签名的清单。发布源可以是面板、GitHub Releases、对象存储或任意 HTTP 服务。**面板即使被攻破,也推不出恶意二进制。**
- **本地说了算**:更新策略(`auto`、`notify`、`pinned`)写在节点本地的配置中,面板只能“建议”目标版本。
- **权限分离**:业务进程不能改写自己的二进制;更新进程读不到业务身份私钥,也不参与业务。
```text
┌─ 节点 ─────────────────────────────────────────────────────────────────────┐
│ myapp(业务进程) 用户 myapp:只读 /opt/myapp,只能写配置目录 │
│ └─ HealthBeacon ──写──> /run/fleet/myapp.beacon │
│ fleet-updater 用户 fleet-update:只能写 /opt/myapp/versions, │
│ ├─ 读取信标 通过 polkit/sudoers 规则只允许重启 myapp.service │
│ └─ 不读取 node.key │
└────────────────────────────────────────────────────────────────────────────┘
```
## 2. 本地配置(`/etc/fleet/updater.json`)
```json
{
"trust": [
{ "id": "rel-2026a", "alg": "ecdsa-p256", "pubkey": "BASE64..." },
{ "id": "rel-2027a", "alg": "ecdsa-p256", "pubkey": "BASE64..." }
],
"check_interval_s": 3600,
"apps": [
{
"id": "moshiro-agent",
"sources": ["https://panel.example.com/fleet/releases", "https://github.com/org/moshiro/releases/download"],
"channel": "stable",
"policy": "notify",
"rid": "linux-x64",
"install_dir": "/opt/moshiro-agent",
"keep_versions": 2,
"service": { "kind": "systemd", "name": "moshiro-agent" },
"health": { "kind": "beacon", "path": "/run/fleet/moshiro-agent.beacon", "timeout_s": 60 }
}
]
}
```
- `trust` 可以配置多把公钥,用于轮换;清单中的 `key_id` 用来选择验证哪一把。
- `policy` 取值:`auto`(验签通过即安装)、`notify`(只下载并上报“有可用更新”,等待面板上的人工批准)、`pinned:<version>`(锁定版本)。
- 面板上的批准只对 `notify` 生效,并且**批准的版本同样必须通过签名验证**。
## 3. 发布清单(由 `fleet-release` 在 CI 或离线环境中生成)
```json
{
"schema": 1,
"app": "moshiro-agent",
"channel": "stable",
"version": "0.4.2",
"released_at": "2026-10-01T00:00:00Z",
"min_from": "0.3.0",
"artifacts": [
{ "rid": "linux-x64", "url": "moshiro-agent-0.4.2-linux-x64.tar.gz", "sha256": "…", "size": 4120334 },
{ "rid": "linux-arm64", "url": "moshiro-agent-0.4.2-linux-arm64.tar.gz", "sha256": "…", "size": 3987112 },
{ "rid": "linux-musl-arm64", "url": "moshiro-agent-0.4.2-linux-musl-arm64.tar.gz", "sha256": "…", "size": 3990021 },
{ "rid": "win-x64", "url": "moshiro-agent-0.4.2-win-x64.zip", "sha256": "…", "size": 4455010 }
],
"notes_url": "https://…/CHANGELOG#0.4.2",
"key_id": "rel-2026a",
"signature": "BASE64(ECDSA(规范化 JSON,不含 signature 字段))"
}
```
- 签名范围是**规范化后的 JSON**:键排序、无空白、不含 `signature` 字段。规范化规则由 `Fleet.Update` 实现,签名端和验签端使用同一份代码。
- 发布源的目录约定:`<source>/<app>/<channel>/latest.json`(指向当前版本)和 `<source>/<app>/<version>/manifest.json`。
- 防回滚:updater 在本地记录每个应用已安装过的最高版本;除非本地配置显式允许降级,否则拒绝安装更低的版本,防止旧的签名清单被重放。
## 4. 更新流程
```text
轮询(间隔加随机抖动)
└─ 拉取 latest.json → 验签 ✗ → 记录并忽略
├─ 版本 ≤ 当前版本,或当前版本 < min_from → 跳过(后者需要人工介入)
├─ policy = notify → 下载并校验 → 上报“可以更新” → 等待批准
└─ policy = auto 或已批准:
下载 → 校验 sha256 和大小 → 解包到 versions/<v>/ → 检查可执行文件
→ 原子切换 current → versions/<v>(Linux 用符号链接,Windows 见 §5)
→ 重启服务 → 等待健康信标(version == v,并且 ok_at 晚于重启时刻)
✓ → 记录成功,按 keep_versions 清理旧版本
✗ 超时 → 切回旧版本 → 重启 → 记录失败,该版本标记为坏版本,不再自动尝试
```
## 5. 平台适配
| 平台 | 切换方式 | 服务管理 |
| --- | --- | --- |
| Linux | `current` 符号链接 + `rename(2)` 原子替换 | systemd:`systemctl restart <name>`,由 polkit 规则限定只能操作被管理的单元 |
| Windows | 服务的 ImagePath 指向 `current\app.exe`;先停服务,再切换目录联结(junction),然后启动 | SCM:updater 的服务账户只被授予这几个服务的启动和停止权限 |
| OpenWrt | 与 Linux 相同的符号链接切换;默认 `keep_versions = 1`,下载前检查闪存剩余空间 | procd:`/etc/init.d/<name> restart`(updater 以 root 运行,这是 OpenWrt 的常见做法;仍然不读取节点身份目录) |
| 其他(以后) | launchd、OpenRC | 通过适配器接口 `IServiceAdapter` 扩展 |
## 6. 面板侧(`Fleet.Server.Releases`,可选)
- 托管清单和制品(也可以只做代理或索引,制品放在 GitHub 或对象存储)。
- **通道与灰度**:每个通道可以指定目标版本,并按标签分批发布,例如先更新 1 台节点,观察 N 分钟没有失败后再更新其余节点。
- **状态汇总**:节点在 `fleet.hello` 中上报 `{app, version}`,updater 上报 `{available, installing, failed, rolled_back}`。面板据此展示版本分布和更新进度。
- updater 与面板通信时,**不使用业务进程的身份**。首版只轮询公开的通道清单,完全不需要身份,状态也由业务进程在 `fleet.hello` 中代为上报。给 updater 单独一对身份密钥是以后的扩展。
## 7. 发布密钥管理
- 私钥只存在于 CI 的密钥服务或离线环境中,**永远不上传到面板**。
- 轮换方式:新旧两把公钥同时写进 `trust` → 用新密钥发布 → 所有节点都更新到信任新密钥的版本后 → 移除旧公钥。
- 密钥泄露时:发布一个用新密钥签名、并从 `trust` 中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。
+33 -116
View File
@@ -4,7 +4,7 @@
1. **三个独立项目加一个前端**:Probe、Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(`web/`)。
2. **通用能力只写一次**:TCP/UDP/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 `MoShiro.Measurement` 中,Probe 和 Agent 共用;采集方法(Kind)由积木组合声明而成。
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主、更新,由与业务无关的 **Fleet** 框架提供(见 [fleet/overview.md](fleet/overview.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主,由与业务无关的 **Fleet** 框架提供(见 [../backend/fleet/README.md](../backend/fleet/README.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
4. **改一处,整条链路受益**:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有**一个真源**。改动之后,下游要么自动更新,要么在编译期或 CI 中直接报错。
5. **模块化**:Probe/Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
6. **Probe/Agent 轻量**:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。
@@ -13,24 +13,23 @@
```text
moshiro/
├─ README.md .gitignore
├─ docs/ docs/fleet/
├─ scripts/ # publish / package / smoke / stability 等脚本(check-deps 等检查也放这里)
├─ README.md TODO.md AGENTS.md .gitignore
├─ docs/
├─ scripts/ # publish / package / smoke / stability 等脚本
├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件
├─ web/ # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json
│
├─ global.json Directory.Packages.props MoShiro.slnx # SDK、NuGet 版本和全部 .NET 项目入口
├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制)
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界
│
├─ ci/MoShiro.Tar/ # CI 归档工具,也加入根解决方案
└─ backend/ # 服务和节点 C# 代码(构建产物在 backend/artifacts/)
├─ Directory.Build.props MoShiro.slnx # 后端编译设置和后端目录解决方案
├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制)
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界
├─ fleet/ # 通用节点框架(不认识 MoShiro,以后可以拆成独立仓库)
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update
│ │ Fleet.Enrollment.Client Fleet.Server.{Enrollment,Link,Releases,Sqlite}
│ ├─ tools/ fleet-enroll fleet-updater fleet-release
│ ├─ samples/ EchoNode EchoServer
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting
│ │ Fleet.Enrollment.Client Fleet.Server.{Abstractions,Enrollment,Link}
│ ├─ tools/ fleet-enroll
│ └─ tests/
├─ shared/ # MoShiro 自己的共享库
│ ├─ MoShiro.Protocol/ # 契约真源:Kind/Plan 目录、任务集、moshiro.* 方法、UDP 包、RawChunk、错误码
@@ -50,7 +49,7 @@ moshiro/
└─ MoShiro.Backend.slnx
```
节点机器上的发布物是三个独立进程,使用不同的系统用户:`moshiro-agent`(或 `moshiro-probe`)、`fleet-enroll`(一次性运行)、`fleet-updater`(常驻或定时运行)。
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。容器节点的更新由宿主机上的 `moshiro-update` 完成,见 [deployment.md](deployment.md)。
## 3. 依赖关系
@@ -81,7 +80,6 @@ Probe, Agent ──> Measurement ──> MoShiro.Protocol ──> Fleet.Core /
Probe, Agent ──> Fleet.Link / Fleet.Hosting / Fleet.Identity
Backend.* ──> MoShiro.Protocol + Fleet.Server.* (不引用 Measurement)
fleet-enroll ──> Fleet.Enrollment.Client ──> Fleet.Identity / Core
fleet-updater ──> Fleet.Update ──> Fleet.Core (不引用 Link,也不读取节点身份)
Web ──> openapi/v1.json
```
@@ -91,42 +89,30 @@ Web ──> openapi/v1.json
| `Fleet.Server.*` | ASP.NET Core、`Microsoft.Data.Sqlite`(仅 Sqlite 项目)、节点侧 Fleet 库 | 任何 `MoShiro.*` |
| `MoShiro.Protocol` | Fleet.Core、Fleet.Rpc | 任何 I/O、套接字、日志 |
| `MoShiro.Measurement` | MoShiro.Protocol、Fleet.Core | NuGet;日志(只通过计数接收器上报);读取配置;统计或解释逻辑 |
| Probe / Agent | 上述库,以及 Fleet.Link / Hosting / Identity | NuGet 运行时包;彼此互相引用;Fleet.Update;Fleet.Enrollment.Client |
| Probe / Agent | 上述库,以及 Fleet.Link / Hosting / Identity | NuGet 运行时包;彼此互相引用;Fleet.Enrollment.Client |
| Backend.* | MoShiro.Protocol、Fleet.Server.*,以及第 5.2 节列出的包 | Measurement、EF Core 等依赖反射的库 |
分析器类包(`PrivateAssets="all"`)不受限制。`scripts/check-deps` 在 CI 中检查上述规则。
分析器类包(`PrivateAssets="all"`)不受限制。`ArchitectureTests` 在构建时检查上述规则。
## 4. 共享库
### 4.1 Fleet(通用节点框架)
组件、扩展点与接入方式见 [fleet/overview.md](fleet/overview.md),连接细节见 [fleet/link.md](fleet/link.md),更新见 [fleet/update.md](fleet/update.md)。
> **实现状态**:`Fleet.Core`、`Fleet.Identity`、`Fleet.Rpc`、`Fleet.Link`、`Fleet.Hosting`、`Fleet.Enrollment.Client`、`Fleet.Server.*`(Abstractions、Enrollment、Link)和 `fleet-enroll` 已实现。`Fleet.Update`、`Fleet.Server.Releases`、`fleet-updater`、`fleet-release` 还只是设计(纯二进制方式的更新);容器节点的更新是宿主机上的 `moshiro-update` 脚本和面板的 `ReleaseService`,见 [deployment.md](deployment.md)。原来的 `MoShiro.Common` 和 `MoShiro.NodeHost` 已经并入 Fleet:
| 原位置 | 现位置 |
| --- | --- |
| `MoShiro.Common`(Result、缓冲区、VarInt、有界队列、重放窗口、退避、ID、令牌哈希) | `Fleet.Core` |
| `MoShiro.NodeHost`(模块宿主、日志、配置、ConfigStore、服务外壳) | `Fleet.Hosting` |
| 节点密钥与签名 | `Fleet.Identity` |
| JSON-RPC 信封与路由 | `Fleet.Rpc` |
| Agent 注册 | `Fleet.Enrollment.Client` + `fleet-enroll` |
组件、扩展点和协议细节见 [../backend/fleet/README.md](../backend/fleet/README.md)。
### 4.2 MoShiro.Protocol(MoShiro 的契约真源)
```text
MoShiro.Protocol/
├─ ProtocolVersion ProtocolLimits MoShiroErrorCode(在 Fleet 错误码基础上扩展)
├─ ProtocolLimits MoShiroErrors(在 Fleet 错误码基础上扩展)
├─ Ids/ AgentId(= Fleet NodeId)、ProbeId、TaskId、TestId、ChunkId、StreamId
├─ Catalog/
│ ├─ Kinds/ KindSpec 声明(积木组合 + ParamSpec + 记录列 + 预算)
│ ├─ Plans/ 多阶段测试方案
│ └─ ParamSpec 参数定义(类型、默认值、上下限),节点与 Backend 共用同一套校验
├─ Tasks/ TaskSet(Fleet 期望状态的文档类型)、调和结果
├─ Rpc/ moshiro.* 方法名、params/result、二进制通道号(从 16 开始)
├─ Wire/ UDP 包、数据连接首帧
├─ Raw/ 自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
└─ Json/ AgentRpcContext、ProbeRpcContext(按信道拆分,便于裁剪)
└─ Raw/ 自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
```
累加器、直方图等统计类型**不在 Protocol 中**,只属于 `Backend.Analysis`。
@@ -198,7 +184,7 @@ AgentApp Probe(ProbeApp)
- **Api 不能引用 Storage**(编译期保证);Api 和 Gateway 只调用 Application。
- Application 返回 `Result<T>`。**错误码到 HTTP 状态的映射只在 Api 层做一次**(`ErrorCodeHttpMap`);到 JSON-RPC 错误的映射由 Fleet.Rpc 统一完成。
- Api 层按调用方分目录:`Public/`、`Admin/`,公共部分放在 `Common/`(分页、ETag、认证过滤器、ProblemFactory、`EndpointConventions`)。每类资源一个端点文件,新增资源时在 `ApiModule.Map` 中加一行。
- 注册端点、签名认证、连接注册表、状态推送、收件箱 ACK、发布通道都来自 Fleet(`MapFleetEnrollment`、`MapFleetLink`、`MapFleetReleases`)。MoShiro 只提供:
- 注册端点、签名认证、连接注册表、状态推送、收件箱 ACK 都来自 Fleet(`MapFleetEnrollment`、`MapFleetLink`)。MoShiro 只提供:
- `IFleetStore` 的实现;
- `TaskSet` 的状态提供者;
- `RawChunk` 的收件箱;
@@ -211,86 +197,18 @@ Backend.Analysis 只依赖 MoShiro.Protocol 和 Fleet.Core,没有 I/O,可以
Backend 允许的包:ASP.NET Core 共享框架、`Microsoft.Data.Sqlite`、`Microsoft.AspNetCore.OpenApi`、`Microsoft.Extensions.ApiDescription.Server`(仅构建期使用)。
## 6. “改一处,整条链路受益”的机制
## 6. 编译设置
| 改动 | 真源 | 链路上的效果 |
| --- | --- | --- |
| 采集密度(频率、包大小、采样间隔) | 任务参数(API) | 经 `fleet.state.sync` 下发后立即生效,无需发布 |
| 新增采集方法(现有积木可以组合) | `Protocol/Catalog/Kinds` 中加 1 条 | 节点执行、Recorder 自动生成列、Backend 校验和目录、Analysis 按列名读取 |
| 新增采集方法 | 在 `Kinds` 里加声明(含流的 `Driver`/`Key`),在 `Measurement/Blocks` 里实现它的拨号方和监听方并登记到 `BlockCatalog` | Probe 和 Agent 同时获得,合并不需要写代码 |
| 原始数据格式 | `Protocol/Raw`(自描述) | 新增列不需要升级版本;四处使用同一份编解码器 |
| 新增统计指标 | `Analysis/Metrics` 中加 1 行 `MetricDef` | 存储、降采样、API、SSE、目录全部自动处理;回填 30 天历史 |
| 服务评分 | `Analysis/Models` | 查询时计算,立即对全部历史生效 |
| 新增 RPC 能力 | `MoShiro.Protocol/Rpc` + 组装处加 1 行 | 通过 Fleet 的 capabilities 协商,旧节点不受影响 |
| 注册、认证、重连、发件箱、状态同步 | `backend/fleet/` | MoShiro 和所有其他产品同时受益 |
| 更新机制 | `Fleet.Update` + `fleet-updater` | 所有产品共用;产品只提供发布清单 |
| REST DTO 与端点 | `Backend.Api` | 生成 OpenAPI,CI 检查 diff,前端的 TS 类型随之更新 |
| 错误码 | Fleet 错误码 + `MoShiroErrorCode` | REST、JSON-RPC、OpenAPI、TS 全部一致 |
| 版本、编译设置、风格、禁用 API | `backend/` 下的 props、`.editorconfig`、`BannedSymbols*.txt` | 所有项目 |
- 所有 .NET 项目共用 `backend/Directory.Build.props`(目标框架、可空引用、警告当错误、最新分析器、确定性构建、禁用 API 清单 `BannedSymbols.txt`、版本号)和根目录的 `Directory.Packages.props`(NuGet 版本、SQLite 源码版本),SDK 版本在根目录的 `global.json`。
- `backend/probe/`、`backend/agent/` 另有 `Directory.Build.props`:NativeAOT、体积优先、关闭 EventSource、调试支持和 Server/Concurrent GC 等,具体开关以文件为准;保留 StackTraceSupport,线上崩溃需要可读的调用栈。
- `BannedSymbols.Node.txt` 适用于常驻节点进程(Probe、Agent,以及 Fleet 的节点侧运行库),主要条目:
- `System.Diagnostics.Process`;
- `System.Reflection.Assembly.Load*`;
- `System.Runtime.InteropServices.NativeLibrary.Load*`;
- `System.IO.File.Write*`、`File.Create`、`FileStream` 构造函数(只有 `Fleet.Hosting.ConfigStore` 可以通过 `#pragma` 豁免);
- `System.Net.Http.HttpClient`(没有豁免:注册在 `fleet-enroll` 里,它是独立的一次性工具,允许 HTTP 和写入身份目录)。
## 7. 编译设置
`backend/Directory.Build.props`(所有项目):
```xml
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<IsAotCompatible>true</IsAotCompatible>
<InvariantGlobalization>true</InvariantGlobalization>
<UseArtifactsOutput>true</UseArtifactsOutput>
<Deterministic>true</Deterministic>
<Version>0.1.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
</ItemGroup>
</Project>
```
`backend/fleet/src/*` 另外启用 `Microsoft.CodeAnalysis.PublicApiAnalyzers`(`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`),公开 API 的任何变更都会在 PR 中显示出来。
`backend/probe/`、`backend/agent/` 下的 `Directory.Build.props`(先导入根 props,再追加轻量设置):
```xml
<PropertyGroup>
<PublishAot>true</PublishAot>
<OptimizationPreference>Size</OptimizationPreference>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
<!-- 保留 StackTraceSupport:线上崩溃需要可读的调用栈 -->
</PropertyGroup>
<ItemGroup>
<!-- 纯采集边界:编译期禁止 -->
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../BannedSymbols.Node.txt" />
</ItemGroup>
```
`BannedSymbols.Node.txt` 适用于常驻节点进程(Probe、Agent,以及 Fleet 的节点侧运行库),主要条目:
- `System.Diagnostics.Process`;
- `System.Reflection.Assembly.Load*`;
- `System.Runtime.InteropServices.NativeLibrary.Load*`;
- `System.IO.File.Write*`、`File.Create`、`FileStream` 构造函数(只有 `Fleet.Hosting.ConfigStore` 可以通过 `#pragma` 豁免);
- `System.Net.Http.HttpClient`(**不再有豁免**:注册已经移到 `fleet-enroll`)。
`fleet-enroll` 和 `fleet-updater` 是独立的工具,使用各自的禁用清单:
- `fleet-updater` 允许写安装目录、调用服务管理接口,但禁止读取节点身份目录;
- `fleet-enroll` 允许 HTTP,允许写入身份目录。
## 8. 轻量预算
## 7. 轻量预算
M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失败):
@@ -299,7 +217,6 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
| Probe | ≤ 8 MB(Linux/OpenWrt,静态链接了 .NET 的加密、压缩、网络库;Windows 实测 5.6 MB) | ≤ 32 MB(Linux 实测约 21 MB) | 同时连接 15 个 Agent 并运行默认模板时 CPU < 2%;`hy2_ladder` 100 Mbps 时 CPU < 50%(单核) |
| Agent | ≤ 10 MB(Linux 实测 8.4 MB) | ≤ 48 MB(Linux 实测约 23 MB) | 运行默认模板时 ≤ 64 MB;同时跑一个 200 Mbps 测试时 CPU < 60%(1 vCPU) |
| fleet-enroll | ≤ 4 MB | — | 一次性运行 |
| fleet-updater | ≤ 4 MB | ≤ 16 MB | 空闲时每小时轮询一次 |
| Backend | 不设硬性限制 | ≤ 150 MB | — |
为满足预算,实现上需要遵守:
@@ -309,7 +226,7 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
- stream 和 bulk 发送使用预先填充的缓冲区,只统计计数,不复制数据;
- 不使用 LINQ。
## 9. 发布
## 8. 发布
- 目标平台:
@@ -318,16 +235,16 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
| Agent | `linux-x64`、`linux-arm64` | VPS |
| Probe | `linux-x64`、`linux-arm64`、`win-x64` | 普通 PC 或服务器 |
| Probe(OpenWrt) | `linux-musl-x64`、`linux-musl-arm64` | 正式支持;`linux-musl-arm`(armv7)视 M0 的工具链验证结果决定是否支持 |
| fleet-enroll / fleet-updater | 与所在节点相同 | |
| fleet-enroll | 与所在节点相同 | |
| Backend | `linux-x64`、`linux-arm64`、`win-x64` | |
- **MIPS 不支持**(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 Probe 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。
- NativeAOT 需要目标平台的原生工具链:musl 目标在 Alpine 容器中构建,arm64 使用交叉编译镜像。CI 矩阵为 Windows、Linux glibc、Linux musl。
- `scripts/publish.(sh|ps1) <probe|agent|backend|fleet-enroll|fleet-updater> <rid>` 输出到 `artifacts/publish/<project>/<rid>/`。
- 发布时由 `fleet-release` 在 CI 中生成并签名清单(见 [fleet/update.md](fleet/update.md)),节点通过 `fleet-updater` 更新。
- `scripts/publish.sh <probe|agent|backend|fleet-enroll> <rid>` 输出到 `backend/artifacts/publish/<project>/<rid>/`。
- 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](deployment.md);纯二进制节点的更新见 [TODO.md](../TODO.md)。
- Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。
### 9.1 OpenWrt 上的 Probe
### 8.1 OpenWrt 上的 Probe
| 事项 | 做法 |
| --- | --- |
@@ -339,8 +256,8 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
| 内存 | 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(`limits.max_test_mbps`),防止路由器 CPU 成为瓶颈,反过来影响测量结果 |
| 测量偏差 | Probe 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 Probe 在 `moshiro.event` 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明 |
| 打包 | 提供 `tar.gz` 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供 |
| 更新 | `fleet-updater` 使用 procd 适配器;`keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
| 更新 | 纯二进制节点的更新器还没有实现([TODO.md](../TODO.md));实现时 `keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
## 10. Web
## 9. Web
`web/` 是 npm workspaces:`display`(展示端,匿名可读)、`admin`(管理端)、`shared`(API 客户端、图表、组件、主题)。Vite 8、React 19、TypeScript,图表用 uPlot;唯一的契约是 `backend/openapi/v1.json`,用 `openapi-typescript` 生成类型(`npm run gen:api`)。颜色只能写在 `shared/theme/tokens.css`,由 stylelint 强制。详见 [web/README.md](../web/README.md)。
+3 -3
View File
@@ -2,8 +2,8 @@
MoShiro 的两条控制链路都建立在 Fleet 之上:
- **Agent ↔ Backend**:Fleet 连接服务端(wss),见 [fleet/link.md](fleet/link.md);
- **Probe ↔ Agent**:Fleet 节点间点对点会话(TLS),见 [fleet/link.md](fleet/link.md) 第 8 节。
- **Agent ↔ Backend**:Fleet 连接服务端(wss),见 [../backend/fleet/README.md](../backend/fleet/README.md);
- **Probe ↔ Agent**:Fleet 节点间点对点会话(TLS),见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「点对点会话」一节。
认证、心跳、重连、能力协商、可靠发件箱、期望状态同步、JSON-RPC 信封都由 Fleet 负责。本文只定义 **MoShiro 自己的内容**:`moshiro.*` 方法、状态文档、二进制通道,以及 Fleet 之外的数据面(UDP 包和数据连接)。所有类型都放在 `MoShiro.Protocol` 中。
@@ -94,7 +94,7 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
## 4. 版本演进
- Fleet 层的兼容规则见 [fleet/link.md](fleet/link.md)。
- Fleet 层的兼容规则见 [../backend/fleet/README.md](../backend/fleet/README.md)。
- `moshiro.*` 方法:新增方法或可选字段不需要升级版本,接收方忽略未知字段。
- RawChunk:块头自描述,新增列或新增 Kind 不需要升级 `container_version`;只有容器格式变化时才升级。
- UDP 包和数据连接首帧的布局变化时,必须升级 `protocol_version`,并更新黄金字节测试。
+3 -1
View File
@@ -28,7 +28,7 @@ data/
## 3. 表结构
`agents`、`join_tokens` 以及公钥和状态版本字段,同时作为 Fleet 的 `IFleetStore` 存储(由 `MoShiro.Backend.Storage.Sqlite` 实现),和 MoShiro 的其他数据放在同一个 `moshiro.db` 中,由同一个单写入者写入。发布与灰度相关的表(`fleet_releases`、`fleet_rollouts`)由 `Fleet.Server.Releases` 自带的迁移脚本创建(职责见 [fleet/update.md](fleet/update.md) 第 6 节)。
`agents`、`join_tokens` 以及公钥和状态版本字段,同时作为 Fleet 的 `IFleetStore` 存储(由 `MoShiro.Backend.Storage.Sqlite` 实现),和 MoShiro 的其他数据放在同一个 `moshiro.db` 中,由同一个单写入者写入。CI 签名的发布清单存在 `settings` 表里(`release.<组件>.manifest`、`release.<组件>.signature`),面板只原样保存和转发。
```sql
-- 身份与注册 -------------------------------------------------------------
@@ -164,6 +164,8 @@ CREATE INDEX ix_events_time ON events (at);
| `metric_1h`、`tests`、`test_results` | 永久 |
| `events` | 365 天 |
保留期在管理端的设置里改,对应的键:`retention.raw_days`、`retention.test_raw_days`、`retention.metric_10s_hours`、`retention.metric_1m_days`、`retention.metric_5m_days`、`retention.events_days`。
降采样由 Application 中的 `Downsampler` 负责:读取下一级的累加器状态,用 `AccumulatorCodec.Merge` 合并后写回。所有保留期都可以配置。
## 5. 容量估算
-15
View File
@@ -1,15 +0,0 @@
# Docker / Podman 共用,数据库和原始数据保存在命名 volume 中。
services:
backend:
image: ${MOSHIRO_IMAGE:-registry.oeo.one/moshiro/moshiro-backend}:${MOSHIRO_VERSION:-0.1.1}
container_name: moshiro-backend
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
MoShiro__InitialAdminPassword: ${MOSHIRO_INITIAL_ADMIN_PASSWORD:?Set MOSHIRO_INITIAL_ADMIN_PASSWORD}
volumes:
- backend-data:/data
volumes:
backend-data:
name: moshiro-backend-data