docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

- Group docs by category with an index (docs/README.md); fix every link,
  code comment and the protocol conformance test that read docs/protocol.md.
- access.md and security.md rewritten to what is implemented: removed the
  unadopted member-proof chain, roles and recovery key that do not exist,
  the old implementation order and the budgets; gaps now carry a status.
- enrollment, deployment, api: the install flow is the static, hash-checked
  script with environment variables; no default admin.
- README and architecture: new topology and update-flow diagrams (operator
  passkey, CI signing, registry feed, auth and trust modules).
This commit is contained in:
justme committed 2026-10-08 19:37:56 +08:00
1 parent 636a784043
commit e014a19554
45 files changed
+592 -966

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
# C# 代码风格
基线是 [dotnet/runtime 编码风格](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md),再加上本项目的补充规则。所有能自动检查的规则都写在 `.editorconfig` 或 `BannedSymbols.txt` 里,**违反即编译失败**(`EnforceCodeStyleInBuild` + `TreatWarningsAsErrors`);无法自动检查的规则由代码评审把关。
## 1. 格式与命名(`.editorconfig` 强制)
- 使用 Allman 大括号(左大括号独占一行),四空格缩进,不使用 Tab;`if`/`for` 等语句必须加大括号。
- 使用文件作用域 namespace;namespace 与目录一致(`MoShiro.Agent.Sessions` 对应 `Sessions/`)。
- `using` 放在文件顶部、namespace 之外,`System.*` 排在最前面,其余按字母排序。
- 始终显式写出可见性,并且放在修饰符的第一位;`static readonly` 的顺序固定。
| 元素 | 规则 | 示例 |
| --- | --- | --- |
| 类型、方法、属性、常量、局部函数 | PascalCase | `SessionTable`、`MaxFrameBytes` |
| 私有和内部实例字段 | `_camelCase` | `_sessions` |
| 静态字段 | `s_camelCase` | `s_idleTimeout` |
| 线程静态字段 | `t_camelCase` | `t_buffer` |
| 局部变量、参数、主构造函数参数 | camelCase | `windowSeq` |
| 接口 | `I` 前缀 | `IMeasurement` |
| 异步方法 | `Async` 后缀 | `SendReportAsync` |
- `var` 只在右侧类型显而易见时使用(`new`、强制转换);类型名使用语言关键字(`int`,而不是 `Int32`)。
- 用 `nameof(...)` 代替字符串字面量;不写多余的 `this.`。
- 字段放在类型的最前面;一个文件只放一个主要类型,文件名与类型名相同。
- 内部类和私有类默认使用 `sealed`(或 `static`),除非确实需要被继承。
## 2. 命名语义
- **名称里带单位**:`timeoutMs`、`rttUs`、`payloadBytes`。API 内部统一用 `TimeSpan`,只在边界(线协议、配置、JSON)使用带单位后缀的整数。
- 不使用 `Manager`、`Helper`、`Util`、`Service`(ASP.NET 约定位置除外)之类的模糊名称,名称要说明职责:`UploadQueue`、`SessionTable`、`ResultWriter`。
- 布尔值使用 `is`/`has`/`can` 前缀;返回“成功与否加输出值”的方法使用 `TryXxx(out ...)`。
## 3. 类型与数据
- 契约和领域值使用 `sealed record` 或 `readonly record struct`,并且不可变。
- 热路径上的计数器和缓冲区使用可变 `struct` 或 `sealed class`,**所有权要写清楚**(谁创建、谁归还池)。
- ID 使用强类型(`readonly record struct ProbeId(string Value)`),不直接传裸 `string`。
- 枚举序列化成 snake_case 字符串(`JsonStringEnumConverter<T>` + `JsonStringEnumMemberName`);新增枚举值不属于破坏性变更,但所有消费方都必须能处理未知值。
- 可空性:公共成员必须标注;使用 `!` 抑制警告时,同一行要写注释说明理由。
## 4. 错误处理
- **预期内的失败不抛异常**:超时、拒绝连接、认证失败、校验失败都返回结构化结果,其中包含 `ErrorCode`。
- 异常只用于程序缺陷或无法继续运行的状态;只在边界(会话循环、请求管线、进程入口)捕获,并记录日志。
- 禁止空 `catch`;捕获 `OperationCanceledException` 时必须确认它确实来自自己的取消令牌。
## 5. 异步与并发
- 所有 I/O 都是异步的,并且接受、传递 `CancellationToken`(作为最后一个参数)。
- 禁止使用 `.Result`、`.Wait()`、`Thread.Sleep`、`async void`(事件处理函数除外),`BannedSymbols.txt` 强制执行。
- 库代码中的 `await` 不需要 `ConfigureAwait(false)`,因为三个程序都没有同步上下文,统一省略即可。
- 生产者和消费者之间使用有界 `Channel<T>`,明确指定 `BoundedChannelFullMode`。
- 共享可变状态要么只由单个循环拥有(推荐),要么使用 `lock` 保护;不要混用多种方式。
## 6. 时间
- 时间来源一律通过 `TimeProvider` 获得:墙钟使用 `GetUtcNow()`,耗时使用 `GetTimestamp()`/`GetElapsedTime()`。
- `DateTime.Now`、`DateTime.UtcNow`、`DateTimeOffset.Now`、`Stopwatch.StartNew` 列入禁用名单。测试使用 `FakeTimeProvider` 驱动窗口和超时(这个包只出现在测试项目中)。
## 7. NativeAOT 与轻量化
- 序列化只使用 `System.Text.Json` 源生成上下文(或手写的二进制编解码);禁用无上下文的 `JsonSerializer.Serialize<T>(T)` 重载。
- 不做程序集扫描、`Activator.CreateInstance`、`Type.GetType(string)`、动态代理或 `Reflection.Emit`。
- **Probe/Agent**:不使用 DI 容器,在 `Program.cs` 中手工组装对象,依赖关系一眼可见;不使用 LINQ;不使用 `HttpClient`(Agent 的 WebSocket 使用 `ClientWebSocket`)。
- **Backend**:使用 `CreateSlimBuilder` + Minimal API;只能通过 `Microsoft.Data.Sqlite` 执行手写的参数化 SQL,禁止字符串拼接 SQL。
- 热路径:使用 `Span<T>`、`stackalloc`(≤ 256 B)、`ArrayPool<byte>.Shared`;每个包的处理过程中不分配内存、不写日志。
- 新增任何第三方包之前,需要先更新 [project-layout.md](project-layout.md) 第 3 节的依赖表,并在 PR 中说明理由。
## 8. 日志
- 统一使用 logfmt 字段名:`ts`、`level`、`msg`、`agent`、`probe`、`session`、`task`、`result`、`err`。
- Backend 使用 `Microsoft.Extensions.Logging` 源生成的 `[LoggerMessage]`;Probe/Agent 使用 `NodeLog`,两者输出的字段名相同。
- 令牌、`session_key` 和 MAC 不能出现在日志里;IP 地址可以记录。
- 每个会话或连接的生命周期事件记录为 Info;逐包事件不记录,只累加计数。
## 9. 纯采集边界(Probe/Agent 与 Fleet 节点侧)
- 由 `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` 字样),由 `ArchitectureTests` 和代码评审把关。
- 新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。
- Fleet 永远不提供 exec、shell、文件读写或远程下发代码的方法,这条规则不接受例外。
- 公开 API 的变更记录在 `PublicAPI.Unshipped.txt` 中;PR 描述需要说明是否属于破坏性变更。
## 10. 声明式扩展的写法
- **Kind**:只放在 `Protocol/Catalog/Kinds`,一个 Kind 一个静态字段;参数一律使用 `P.Int`、`P.Enum` 等 `ParamSpec` 写出默认值和上下限,禁止写没有上下限的参数。
- **积木**:实现对应的积木接口,在 `BlockRegistry` 中注册一行;积木内部不能读取全局配置,所有输入都来自参数。
- **指标**:只放在 `Analysis/Metrics`,一行一个 `MetricDef`;指标 id 使用 `领域.名称`(如 `tcp.stall`),导出值为 `id.统计量`(如 `rtt.p95`)。已发布的 id 不能改名,只能新增。
- **算子**:纯函数,输入列视图,输出累加器更新;每个算子都要有边界用例测试(空输入、单个样本、乱序、时钟回绕)。
- **模型**:只读取指标,不读取原始列;评分函数需要有单调性测试(指标变差时,分数不能升高)。
## 11. 测试
- 使用 xUnit v3。测试项目与被测项目一一对应,放在同名目录下的 `tests/` 中。
- 测试名采用 `方法_场景_期望` 格式,例如 `Merge_TwoHistograms_PreservesP95`。
- 协议编解码必须有“黄金字节”测试:固定的输入对应固定的字节序列。布局发生变化时测试会失败,提醒升级协议版本。
- 采集端必须覆盖:逐包记录的完整性(发送数 = 记录数)、丢失标记、重复、乱序、非法长度、重放、会话过期、版本不兼容。
- 分析模型必须有“黄金原始块 → 期望输出”的测试;模型升版时,同时更新期望输出并说明原因。
- e2e 测试在 Windows 和 Linux 上对 **AOT 发布产物**运行,而不是对 JIT 构建运行。
## 12. 提交与评审
- 提交信息使用 `<范围>: <摘要>` 格式,范围取值为 `probe`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`。
- 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api-guidelines.md](../api/api-guidelines.md) 第 2 节判断是否属于破坏性变更。
+263
View File
@@ -0,0 +1,263 @@
# 项目结构与工程约束
## 1. 目标
1. **三个独立项目加一个前端**:Probe、Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(`web/`)。
2. **通用能力只写一次**:TCP/UDP/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 `MoShiro.Measurement` 中,Probe 和 Agent 共用;采集方法(Kind)由积木组合声明而成。
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主,由与业务无关的 **Fleet** 框架提供(见 [../backend/fleet/README.md](../../backend/fleet/README.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
4. **改一处,整条链路受益**:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有**一个真源**。改动之后,下游要么自动更新,要么在编译期或 CI 中直接报错。
5. **模块化**:Probe/Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
6. **Probe/Agent 轻量**:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。
## 2. 仓库布局
```text
moshiro/
├─ 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 项目入口
│
├─ 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.Enrollment.Client Fleet.Server.{Abstractions,Enrollment,Link}
│ ├─ tools/ fleet-enroll
│ └─ tests/
├─ shared/ # MoShiro 自己的共享库
│ ├─ MoShiro.Protocol/ # 契约真源:Kind/Plan 目录、任务集、moshiro.* 方法、UDP 包、RawChunk、错误码
│ ├─ MoShiro.Measurement/ # 采集引擎:积木、Recorder、任务调和(见 measurement.md)
│ └─ tests/
├─ probe/ src/MoShiro.Probe/ tests/ # 家宽端采集器
├─ agent/ src/MoShiro.Agent/ tests/ # VPS 端采集器
├─ src/ # Backend(面板)
│ ├─ MoShiro.Backend/ # 宿主与组合根(Program.cs)
│ ├─ MoShiro.Backend.Api/ # API 层:HTTP 端点、DTO、错误映射
│ ├─ MoShiro.Backend.Gateway/ # 在 Fleet.Server.Link 上注册 MoShiro 的方法、状态与收件箱
│ ├─ MoShiro.Backend.Application/ # 用例层 + 端口接口
│ ├─ MoShiro.Backend.Analysis/ # 分析(纯计算,见 analysis.md)
│ └─ MoShiro.Backend.Storage.Sqlite/ # 端口接口的实现,同时实现 IFleetStore
├─ tests/
├─ openapi/v1.json # 构建生成并提交
└─ MoShiro.Backend.slnx
```
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。容器节点的更新由宿主机上的 `moshiro-update` 完成,见 [deployment.md](../ops/deployment.md)。
## 3. 依赖关系
```text
┌────────┐ ┌────────┐ ┌──────────────────────────────┐ ┌─────┐
│ Probe │ │ Agent │ │ Backend (Api/Gateway/App/ │─openapi─>│ Web │
└─┬──┬───┘ └─┬──┬───┘ │ Analysis/Storage) │ └─────┘
│ └───┬───┘ │ └───┬───────────────┬──────────┘
│ ▼ │ │ │
│ ┌───────────┴┐ │ │
│ │Measurement │ │ │
│ └─────┬──────┘ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────┐ ┌─────────────────────────┐
│ MoShiro.Protocol │ │ Fleet.Server.* │
└────────────────┬─────────────────┘ └───────────┬─────────────┘
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Fleet(节点侧):Core · Identity · Rpc · Link · Hosting │
└──────────────────────────────────────────────────────────────────┘
反向依赖一律禁止:fleet/** ✗→ MoShiro.*
```
简要来说:
```text
Probe, Agent ──> Measurement ──> MoShiro.Protocol ──> Fleet.Core / Fleet.Rpc
Probe, Agent ──> Fleet.Link / Fleet.Hosting / Fleet.Identity
Backend.* ──> MoShiro.Protocol + Fleet.Server.* (不引用 Measurement)
fleet-enroll ──> Fleet.Enrollment.Client ──> Fleet.Identity / Core
Web ──> openapi/v1.json
```
| 项目 | 允许依赖 | 禁止 |
| --- | --- | --- |
| `Fleet.*`(节点侧) | BCL、其他节点侧 Fleet 库 | 任何 NuGet 运行时包;任何 `MoShiro.*` |
| `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.Enrollment.Client |
| Backend.* | MoShiro.Protocol、Fleet.Server.*,以及第 5.2 节列出的包 | Measurement、EF Core 等依赖反射的库 |
分析器类包(`PrivateAssets="all"`)不受限制。`ArchitectureTests` 在构建时检查上述规则。
## 4. 共享库
### 4.1 Fleet(通用节点框架)
组件、扩展点和协议细节见 [../backend/fleet/README.md](../../backend/fleet/README.md)。
### 4.2 MoShiro.Protocol(MoShiro 的契约真源)
```text
MoShiro.Protocol/
├─ ProtocolLimits MoShiroErrors(在 Fleet 错误码基础上扩展)
├─ Ids/ AgentId(= Fleet NodeId)、ProbeId、TaskId、TestId、ChunkId、StreamId
├─ Catalog/
│ ├─ Kinds/ KindSpec 声明(积木组合 + ParamSpec + 记录列 + 预算)
│ └─ ParamSpec 参数定义(类型、默认值、上下限),节点与 Backend 共用同一套校验
├─ Tasks/ TaskSet(Fleet 期望状态的文档类型)、调和结果
├─ Rpc/ moshiro.* 方法名、params/result、二进制通道号(从 16 开始)
├─ Wire/ UDP 包、数据连接首帧
└─ Raw/ 自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
```
累加器、直方图等统计类型**不在 Protocol 中**,只属于 `Backend.Analysis`。
### 4.3 MoShiro.Measurement(采集引擎)
包含:
- 积木:Transport、Pacer、Payload、Behavior、Sampler;
- 根据列定义自动生成的 Recorder;
- `KindAssembler`;
- `TaskReconciler`(实现 Fleet 的 `IStateReconciler<TaskSet>`);
- `BudgetGuard`。
详见 [measurement.md](../design/measurement.md) 第 6 节。
## 5. 模块化
### 5.1 Agent 与 Probe:组装
节点的组装点是 `AgentApp` 和 `ProbeApp`:在构造函数里**显式组装**,不使用 DI 容器,也不做反射扫描;没有全局状态,测试可以在同一进程里启动多个。连接、认证、重连、发件箱、状态同步都由 Fleet 提供,MoShiro 只写业务:
```text
AgentApp Probe(ProbeApp)
FleetLink(到面板) FleetLink(到面板)
状态 TaskSet、方法 moshiro.test.* 状态 probe_agents(Agent 列表)、hello 带系统和构建信息
发件箱 RawChunk(通道 16) 心跳带更新器的状态;moshiro.update.* 通知
心跳带更新器的状态;moshiro.update.* AgentDirectory → 每个 Agent 一个 AgentConnection(点对点会话)
PeerListener / PeerHub(Probe 会话) 会话内:线路任务集 line_task_set 的调和、角色、列块发件箱(通道 17)
UdpMux / TcpAcceptor(数据端口)
TaskManager / TestRunner / ChunkAssembler
UpdateBridge(和宿主机更新器交换文件) UpdateBridge
```
模块之间通过构造函数传入的接口或 `Channel<T>` 通信,禁止通过静态全局状态共享数据。
**新增 RPC 方法**:在 `MoShiro.Protocol/Rpc` 中定义 params/result,然后在组装处加一行 `.On(Methods.X, HandleX)`。
### 5.2 Backend:分层与 API 层
```text
HTTP (public | admin) wss /api/agent/v1/rpc(Fleet.Server.Link) SSE /api/public/live
│ │ ▲
┌──────────────▼───────────────┐ ┌───────────▼──────────┐ │
│ Backend.Api │ │ Backend.Gateway │ │
│ Public/ Admin/ │ │ 注册 moshiro.* 方法 │ │
│ 认证(Cookie/CSRF/令牌) │ │ TaskSet 状态提供者 │ │
│ 错误映射 · OpenAPI · SSE 端点 │ │ RawChunk 收件箱 │ │
└──────────────┬───────────────┘ └───────────┬──────────┘ │
└───────────────┬──────────────┘ │
▼ │
┌─────────────────────────────────────────────────────────┐ ┌───────────────────┴──┐
│ Backend.Application │───>│ LiveHub(Application)│
│ Agents · Probe · Tasks(有效任务集的计算与下发) │ │ 内存快照 + 主题订阅 │
│ Tests · Ingest(chunk → 存储 → 分析队列)· SeriesQuery │ └──────────────────────┘
│ AnalysisScheduler · Downsampler · Auth · Releases 视图 │ 调用 ─> Backend.Analysis
└───────────────────────────▲─────────────────────────────┘ 纯计算:算子/指标/模型
│ 实现端口
┌───────────────────────────┴─────────────────────────────┐
│ Backend.Storage.Sqlite:段文件 + SQLite(只做读写) │
│ 同时实现 IFleetStore(Fleet 的节点和令牌表放在 moshiro.db 中)│
└─────────────────────────────────────────────────────────┘
端口:IAgentStore、ITaskStore、IRawStore、IMetricStore、ITestStore、IAuthStore
```
规则:
- **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`)。MoShiro 只提供:
- `IFleetStore` 的实现;
- `TaskSet` 的状态提供者;
- `RawChunk` 的收件箱;
- `moshiro.*` 方法。
- public 层的 DTO 单独定义,只暴露可以公开的字段,并统一经过“可见性过滤器”。
- 请求校验:简单字段约束使用 .NET 10 内置的 Minimal API 校验;任务参数统一用 `ParamSpec` 校验(与节点共用);跨字段规则放在 Application 层。
- Backend 使用 ASP.NET Core 自带的 DI 容器,只在组合根显式注册。
Backend.Analysis 只依赖 MoShiro.Protocol 和 Fleet.Core,没有 I/O,可以单独做成离线 CLI 使用。
Backend 允许的包:ASP.NET Core 共享框架、`Microsoft.Data.Sqlite`、`Microsoft.AspNetCore.OpenApi`、`Microsoft.Extensions.ApiDescription.Server`(仅构建期使用)。
## 6. 编译设置
- 所有 .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. 轻量预算
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 | — | 一次性运行 |
| Backend | 不设硬性限制 | ≤ 150 MB | — |
为满足预算,实现上需要遵守:
- 热路径使用 `Span<T>` 和 `ArrayPool<byte>`,不逐包分配;
- UDP 使用 `SocketAddress` 系列非分配 API;
- stream 和 bulk 发送使用预先填充的缓冲区,只统计计数,不复制数据;
- 不使用 LINQ。
## 8. 发布
- 目标平台:
| 程序 | RID | 说明 |
| --- | --- | --- |
| 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 | 与所在节点相同 | |
| 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 <probe|agent|backend|fleet-enroll> <rid>` 输出到 `backend/artifacts/publish/<project>/<rid>/`。
- 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新见 [TODO.md](../../TODO.md)。
- Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。
### 8.1 OpenWrt 上的 Probe
| 事项 | 做法 |
| --- | --- |
| C 库 | musl:使用 `linux-musl-*` 目标发布 |
| TLS | .NET 在 Linux 上依赖 OpenSSL,而 OpenWrt 默认使用 mbedTLS/wolfSSL。需要安装 `libopenssl3`(安装脚本自动检查并提示)。启动时自检,缺少时明确报错,不会静默降级 |
| 全球化 | 已启用 `InvariantGlobalization`,不依赖 ICU |
| 服务管理 | procd:提供 `/etc/init.d/moshiro-probe`(`procd_set_param respawn`,前台运行,日志输出到 logd);`Fleet.Hosting.ServiceShell` 在 procd 下不需要 `sd_notify` |
| 存储 | 闪存很小,而且需要避免频繁写入:二进制放在 `/usr/lib/moshiro/`,配置和身份放在 `/etc/moshiro/`(sysupgrade 时通过 `/lib/upgrade/keep.d/` 保留);测量数据本来就不落盘;`ConfigStore` 只在 Agent 列表变化时写入 |
| 内存 | 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(`limits.max_test_mbps`),防止路由器 CPU 成为瓶颈,反过来影响测量结果 |
| 测量偏差 | Probe 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 Probe 在 `moshiro.event` 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明 |
| 打包 | 提供 `tar.gz` 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供 |
| 更新 | 纯二进制节点的更新器还没有实现([TODO.md](../../TODO.md));实现时 `keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
## 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)。