From f4aa3f24825ffff55e714de6f9e5cff3f53a65c6 Mon Sep 17 00:00:00 2001 From: JustMe Date: Wed, 7 Oct 2026 03:29:05 +0800 Subject: [PATCH] docs: audit the docs, merge the Fleet docs into backend/fleet/README.md and add TODO.md [CI SKIP] Drop the unimplemented fleet updater design, the stale project-layout sections and broken references, fix the API table, and rename the four Monitor*.cs files left over from the rename. --- README.md | 10 +- TODO.md | 43 +++++ backend/README.md | 2 +- backend/agent/README.md | 2 +- backend/fleet/README.md | 101 ++++++++++++ backend/probe/README.md | 2 +- .../Ids/{MonitorIds.cs => MoShiroIds.cs} | 0 .../{MonitorErrors.cs => MoShiroErrors.cs} | 0 .../Rpc/{MonitorRpc.cs => MoShiroRpc.cs} | 0 ...itorFleetHooks.cs => MoShiroFleetHooks.cs} | 0 .../ArchitectureTests.cs | 2 +- docs/api.md | 6 +- docs/architecture.md | 10 +- docs/coding-style.md | 6 +- docs/enrollment.md | 14 +- docs/fleet/link.md | 137 ---------------- docs/fleet/overview.md | 106 ------------- docs/fleet/update.md | 112 ------------- docs/project-layout.md | 149 ++++-------------- docs/protocol.md | 6 +- docs/storage.md | 4 +- 21 files changed, 206 insertions(+), 506 deletions(-) create mode 100644 TODO.md create mode 100644 backend/fleet/README.md rename backend/shared/MoShiro.Protocol/Ids/{MonitorIds.cs => MoShiroIds.cs} (100%) rename backend/shared/MoShiro.Protocol/{MonitorErrors.cs => MoShiroErrors.cs} (100%) rename backend/shared/MoShiro.Protocol/Rpc/{MonitorRpc.cs => MoShiroRpc.cs} (100%) rename backend/src/MoShiro.Backend.Application/Agents/{MonitorFleetHooks.cs => MoShiroFleetHooks.cs} (100%) delete mode 100644 docs/fleet/link.md delete mode 100644 docs/fleet/overview.md delete mode 100644 docs/fleet/update.md diff --git a/README.md b/README.md index da70858..72b36bd 100644 --- a/README.md +++ b/README.md @@ -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)。 ## 许可证 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..93ee1ba --- /dev/null +++ b/TODO.md @@ -0,0 +1,43 @@ +# TODO + +按主题记录还没做的事,做完就删掉对应的条目。已经实现的东西以 [`docs/`](docs/) 和代码为准。 + +## 测量 + +- IPv6 +- 路由跳数、路径漂移和 trace +- 握手耗时:TCP 连接耗时,TLS / QUIC 握手耗时(QUIC 目前没有真实握手积木) +- 网页长截图 +- pcap / eBPF 采集 + +## 任务与面板 + +- 单个 Agent 的任务(目前是线路模板加按 Agent 的覆盖,没有只属于某一个 Agent 的独立任务) +- 原生 webhook 投递;告警(基于 webhook) +- 早上四点定时重启 +- cap + +## 更新 + +- 更新遥测(现在面板只有每个节点的「当前更新状态」) +- **回滚路径的实机测试**:容器节点更新后观察 20 秒、失败回滚,只有单元和脚本测试,没有在真实节点上触发过。 +- **更新的灰度和自动策略**:按标签分批发布;`POLICY=auto` 的长期验证;Backend 自身的更新(Backend 目前没有更新器,要手动拉镜像)。 +- **纯二进制节点的更新**(`fleet-updater`、`fleet-release`、Windows 服务和 OpenWrt procd 适配器)。设计要点,实现时照这个来: + - 信任来自签名:只接受离线发布密钥签名的清单,发布源可以是面板、GitHub Releases 或任意 HTTP 服务;面板被攻破也推不出恶意二进制。 + - 本地说了算:更新策略(`auto` / `notify` / `pinned:`)和信任的公钥写在节点本地配置里,面板只能「建议」;支持多把公钥以便轮换。 + - 清单带 `app`、`channel`、`version`、`min_from`、各 RID 制品的 `sha256` 和大小、`key_id`、签名(对规范化 JSON 签名);本地记录已装过的最高版本,拒绝降级和旧清单重放。 + - 流程:下载 → 校验 → 解包到 `versions//` → 原子切换 `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 服务适配。 diff --git a/backend/README.md b/backend/README.md index 7e0021f..e50ea49 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,7 +18,7 @@ Backend 是 MoShiro 的控制面和数据服务,负责 Agent 注册与连接 ## 环境要求 -使用 .NET SDK 10。仓库根目录的 `backend/global.json` 指定所需 SDK 版本及滚动策略。 +使用 .NET SDK 10。仓库根目录的 `global.json` 指定所需 SDK 版本及滚动策略。 ## 构建与测试 diff --git a/backend/agent/README.md b/backend/agent/README.md index 8fa7afd..3b13a4b 100644 --- a/backend/agent/README.md +++ b/backend/agent/README.md @@ -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 diff --git a/backend/fleet/README.md b/backend/fleet/README.md new file mode 100644 index 0000000..fea7018 --- /dev/null +++ b/backend/fleet/README.md @@ -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` 序列化,不使用反射。 +5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/deployment.md`](../../docs/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。 + +## 身份 + +| 对象 | 算法 | 保存位置 | +| --- | --- | --- | +| 节点身份密钥 | ECDSA P-256(BCL 自带) | `/node.key`(0600);服务端只保存公钥 | +| 点对点 TLS 证书 | 自签名 ECDSA P-256 | `/tls.pfx`;对端按 SPKI SHA-256 指纹校验 | +| 签名串 | `"fleet|v1||" + 各字段` | 带用途前缀,防止同一个签名在不同场景被复用 | + +用途:`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 + └─ 写入 /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` 给每个节点算 `{version, doc}`,变化时 `NotifyChanged` 推送;节点 `StateHandler` 调和后返回 `{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`、`IInbox`、`INodeEventSink`、`IEnrollmentHook`。MoShiro 把 `IFleetStore` 放进自己的 `moshiro.db`,和业务数据由同一个单写入者写入。 diff --git a/backend/probe/README.md b/backend/probe/README.md index 4831dc7..0b72774 100644 --- a/backend/probe/README.md +++ b/backend/probe/README.md @@ -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 diff --git a/backend/shared/MoShiro.Protocol/Ids/MonitorIds.cs b/backend/shared/MoShiro.Protocol/Ids/MoShiroIds.cs similarity index 100% rename from backend/shared/MoShiro.Protocol/Ids/MonitorIds.cs rename to backend/shared/MoShiro.Protocol/Ids/MoShiroIds.cs diff --git a/backend/shared/MoShiro.Protocol/MonitorErrors.cs b/backend/shared/MoShiro.Protocol/MoShiroErrors.cs similarity index 100% rename from backend/shared/MoShiro.Protocol/MonitorErrors.cs rename to backend/shared/MoShiro.Protocol/MoShiroErrors.cs diff --git a/backend/shared/MoShiro.Protocol/Rpc/MonitorRpc.cs b/backend/shared/MoShiro.Protocol/Rpc/MoShiroRpc.cs similarity index 100% rename from backend/shared/MoShiro.Protocol/Rpc/MonitorRpc.cs rename to backend/shared/MoShiro.Protocol/Rpc/MoShiroRpc.cs diff --git a/backend/src/MoShiro.Backend.Application/Agents/MonitorFleetHooks.cs b/backend/src/MoShiro.Backend.Application/Agents/MoShiroFleetHooks.cs similarity index 100% rename from backend/src/MoShiro.Backend.Application/Agents/MonitorFleetHooks.cs rename to backend/src/MoShiro.Backend.Application/Agents/MoShiroFleetHooks.cs diff --git a/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs b/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs index 83096a4..8eb2b01 100644 --- a/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; namespace MoShiro.Backend.Tests; /// -/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制,等价于文档里的 eng/check-deps。 +/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制。 /// 读取仓库里所有 .csproj 和源文件,违反规则就失败。 /// public sealed class ArchitectureTests diff --git a/docs/api.md b/docs/api.md index 1a1c182..6adfeb5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. 运维端点 diff --git a/docs/architecture.md b/docs/architecture.md index 7c5d6ba..9cc7ad5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 分钟这类视图足够精细且快 | diff --git a/docs/coding-style.md b/docs/coding-style.md index 37e7049..cb2e2db 100644 --- a/docs/coding-style.md +++ b/docs/coding-style.md @@ -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 描述需要说明是否属于破坏性变更。 diff --git a/docs/enrollment.md b/docs/enrollment.md index b03e4e6..70922f5 100644 --- a/docs/enrollment.md +++ b/docs/enrollment.md @@ -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 diff --git a/docs/fleet/link.md b/docs/fleet/link.md deleted file mode 100644 index 735cbf2..0000000 --- a/docs/fleet/link.md +++ /dev/null @@ -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 中) | `/node.key`(0600);服务端只保存公钥 | -| 点对点 TLS 证书 | 自签名,ECDSA P-256 | `/tls.pfx`;对端按 SPKI SHA-256 指纹校验 | -| 签名串 | `"fleet|v1||" + 各字段` | 带用途前缀,防止同一个签名在不同场景下被复用 | - -用途前缀的取值:`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 - │ { join_token, name, pubkey, tls_spki, proof = 签名("enroll", join_token 哈希 + pubkey), - │ node_version, meta(产品自定义的 JSON,经产品的 JsonTypeInfo 校验)} - │ 服务端:校验令牌 → uses+1 → 创建节点(active 或 pending)→ 触发 OnEnrolled 事件(产品挂钩) - └─ 写入 /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.GetAsync(node) → {version, doc} - 发生变化时调用 NotifyChanged(node) → 推送 fleet.state.sync -节点: IStateReconciler.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`:源生成 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 FindJoinTokenAsync(ReadOnlyMemory tokenHash, CancellationToken ct); - ValueTask> CreateNodeAsync(NewNode node, CancellationToken ct); - ValueTask GetNodeAsync(NodeId id, CancellationToken ct); - ValueTask UpdatePresenceAsync(NodeId id, Presence presence, CancellationToken ct); - // 令牌、审批、接管、禁用等 -} -``` - -产品可以使用 `Fleet.Server.Sqlite`,也可以把这些表放进自己的数据库中。MoShiro 的做法是把它们放进自己的 `moshiro.db`,并由同一个单写入者负责写入。 diff --git a/docs/fleet/overview.md b/docs/fleet/overview.md deleted file mode 100644 index 1580fed..0000000 --- a/docs/fleet/overview.md +++ /dev/null @@ -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` 实现序列化,不使用反射。 -5. **只抽取真正用到的**:每项能力都必须同时被 MoShiro 和 `samples/EchoNode` 使用,防止过度设计。 - -## 3. 组件 - -| 组件 | 侧 | 职责 | 详细 | -| --- | --- | --- | --- | -| `Fleet.Core` | 两侧 | `Result`、错误码基类、退避、有界队列、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(MyRpcContext.Default.MyDesired, reconciler) // 期望状态同步(可选) - .WithOutbox(MyReportCodec.Instance, maxBytes: 16 << 20) // 可靠上报(可选) - .On("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()) - .AddState(desiredProvider) // 为每个节点计算期望状态 - .AddInbox(reportHandler) // 持久化之后再 ACK - .OnNodeEvent(presenceHandler); // 上线、下线、版本变化 -app.MapFleetEnrollment("/fleet/enroll"); -app.MapFleetLink("/fleet/rpc"); -app.MapFleetReleases("/fleet/releases"); // 可选 -``` diff --git a/docs/fleet/update.md b/docs/fleet/update.md deleted file mode 100644 index f379c5e..0000000 --- a/docs/fleet/update.md +++ /dev/null @@ -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:`(锁定版本)。 -- 面板上的批准只对 `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` 实现,签名端和验签端使用同一份代码。 -- 发布源的目录约定:`///latest.json`(指向当前版本)和 `///manifest.json`。 -- 防回滚:updater 在本地记录每个应用已安装过的最高版本;除非本地配置显式允许降级,否则拒绝安装更低的版本,防止旧的签名清单被重放。 - -## 4. 更新流程 - -```text -轮询(间隔加随机抖动) - └─ 拉取 latest.json → 验签 ✗ → 记录并忽略 - ├─ 版本 ≤ 当前版本,或当前版本 < min_from → 跳过(后者需要人工介入) - ├─ policy = notify → 下载并校验 → 上报“可以更新” → 等待批准 - └─ policy = auto 或已批准: - 下载 → 校验 sha256 和大小 → 解包到 versions// → 检查可执行文件 - → 原子切换 current → versions/(Linux 用符号链接,Windows 见 §5) - → 重启服务 → 等待健康信标(version == v,并且 ok_at 晚于重启时刻) - ✓ → 记录成功,按 keep_versions 清理旧版本 - ✗ 超时 → 切回旧版本 → 重启 → 记录失败,该版本标记为坏版本,不再自动尝试 -``` - -## 5. 平台适配 - -| 平台 | 切换方式 | 服务管理 | -| --- | --- | --- | -| Linux | `current` 符号链接 + `rename(2)` 原子替换 | systemd:`systemctl restart `,由 polkit 规则限定只能操作被管理的单元 | -| Windows | 服务的 ImagePath 指向 `current\app.exe`;先停服务,再切换目录联结(junction),然后启动 | SCM:updater 的服务账户只被授予这几个服务的启动和停止权限 | -| OpenWrt | 与 Linux 相同的符号链接切换;默认 `keep_versions = 1`,下载前检查闪存剩余空间 | procd:`/etc/init.d/ 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` 中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。 diff --git a/docs/project-layout.md b/docs/project-layout.md index d7f1e34..3150bac 100644 --- a/docs/project-layout.md +++ b/docs/project-layout.md @@ -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`。**错误码到 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 - - - net10.0 - latest - enable - enable - true - latest-recommended - true - true - true - true - true - 0.1.1 - - - - - - -``` - -`backend/fleet/src/*` 另外启用 `Microsoft.CodeAnalysis.PublicApiAnalyzers`(`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`),公开 API 的任何变更都会在 PR 中显示出来。 - -`backend/probe/`、`backend/agent/` 下的 `Directory.Build.props`(先导入根 props,再追加轻量设置): - -```xml - - true - Size - true - false - false - false - false - false - - - - - - -``` - -`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) ` 输出到 `artifacts/publish///`。 -- 发布时由 `fleet-release` 在 CI 中生成并签名清单(见 [fleet/update.md](fleet/update.md)),节点通过 `fleet-updater` 更新。 +- `scripts/publish.sh ` 输出到 `backend/artifacts/publish///`。 +- 容器镜像由 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)。 diff --git a/docs/protocol.md b/docs/protocol.md index 0352ece..880285f 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -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`,并更新黄金字节测试。 diff --git a/docs/storage.md b/docs/storage.md index bdea426..752532c 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -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. 容量估算