Files
MoShiro/docs/design/architecture.md
T
justme d5ebf29e57
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/e2e Pipeline was successful
docs: tighten wording, soften node capability claims, link architecture to code
- Remove the node capability paragraph from README; soften "pure collector"
  and "no self-update" statements in docs, AGENTS.md and READMEs
- architecture.md: drop scalability goals, design decisions and open items;
  add a component-to-code table and code links
- Replace rhetorical contrasts in security docs with direct statements
- AGENTS.md: add project mental model, change map and writing style rules
2026-10-21 06:52:01 +08:00

158 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
## 1. 目标与定位
只关注网络层的线路质量面板:单体面板(Backend)、轻量节点(Probe、Agent)、长连接 RPC 与任务下发。
- 采集:多个 Probe(家宽等需要观测的出口)与多台海外 VPS Agent 之间记录逐包、逐事件的原始数据。哪些 Probe 连哪些 Agent 由面板决定,默认全连,也可以手动选择。
- 分析:Backend 的分析层推算在某条线路上部署 REALITY 类、Hysteria2 类或 Minecraft 服务的体验,并对多条线路排序和对比。
- 主机状态:Agent 和 Probe 读取 `/proc`,上报 CPU 占用、内存、根分区、网卡累计流量与瞬时网速、运行时长这类汇总数字。不采集 CPU 型号、主机名、网卡名、IP 和进程。Probe 的上报可在 `probe.json` 中用 `reporting.host_stats: false` 关闭。展示页以卡片呈现每个节点的主机状态,线路在卡片下方的面板中。
- 构建信息:节点连接面板时自报系统、架构和构建信息(版本、tag、提交、构建时间),用于展示和更新。国家和运营商由 Backend 依据节点来源地址查询,节点不访问任何外部服务,展示页不含任何 IP。
- 节点能力:当前 Agent 和 Probe 只包含测量与上报,不提供 SSH、远程执行、终端和文件读写;更新由节点宿主机上的更新器或服务管理器完成,节点进程只把面板转来的发布清单和更新请求写入数据目录。后续能力的规划见 [security.md](../security/security.md) 第 9 节。
## 2. 拓扑
```text
操作员 passkey ──签名「操作员集合」──┐ CI 发布私钥 ──签名「镜像 digest 清单」──▶ 镜像仓库(清单 + 镜像)
▼ ▲
Web(展示端 · 管理端) │ 定时取清单
▲ REST + SSE │
┌──────────────────────────────────────────────────────────────┐
│ Backend(面板) │──────────────────────────────┘
│ Api · Auth(会话 · passkey · API 令牌 · 审计) │
│ Trust(操作员集合 · 服务器身份)· Registry · Planner · Links │
│ Releases(发布清单 · 可信发布密钥) │
│ Ingest · Analysis · LiveHub │
│ Store(段文件 + SQLite) │
└──────────────────────────────────────────────────────────────┘
▲ ▲
│ wss 长连接(节点发起,应用层加密) │ wss 长连接(节点发起,应用层加密)
│ 任务、数据、状态、更新通知 │ 身份、Agent 列表、状态、更新通知
┌────────────────────┐ ┌──────────────────────┐
│ Agent × 10+ │ ◀── 测量会话 ── │ Probe × N │
│ 有公网 IP,监听 │ 先敲门再 TLS │ 家宽等出口,NAT 后 │
└────────────────────┘ └──────────────────────┘
测量会话:TLS 控制通道 + QUIC Datagram/TCP 数据通道,由 Probe 主动拨出(Agent 固定 QUIC 端口,Probe 本地源端口轮换)
更新(Agent、Probe、Backend):面板登记 CI 签名的清单 → 管理端确认 → 节点(Backend 先备份数据库)有序退出 → systemd Pull=always 拉最新镜像重启
```
各组件的代码位置:
| 组件 | 代码 |
| --- | --- |
| Backend 宿主与组合根 | [`MoShiro.Backend`](../../backend/src/MoShiro.Backend) |
| Api(REST、SSE、OpenAPI) | [`MoShiro.Backend.Api`](../../backend/src/MoShiro.Backend.Api),实时推送 [`LiveEndpoints.cs`](../../backend/src/MoShiro.Backend.Api/Public/LiveEndpoints.cs)
| Auth(会话、passkey、API 令牌、审计) | [`Application/Auth`](../../backend/src/MoShiro.Backend.Application/Auth)、[`Application/Audit`](../../backend/src/MoShiro.Backend.Application/Audit) |
| Trust(操作员集合、服务器身份) | [`TrustService.cs`](../../backend/src/MoShiro.Backend.Application/Auth/TrustService.cs)、[`ServerIdentityService.cs`](../../backend/src/MoShiro.Backend.Application/Auth/ServerIdentityService.cs);节点侧验证 [`OperatorTrust.cs`](../../backend/fleet/src/Fleet.Identity/OperatorTrust.cs)、[`TrustAnchor.cs`](../../backend/fleet/src/Fleet.Hosting/TrustAnchor.cs)
| Registry(Agent、Probe 注册与状态) | [`AgentService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/AgentService.cs)、[`ProbeService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/ProbeService.cs)、[`MoShiroFleetHooks.cs`](../../backend/src/MoShiro.Backend.Application/Agents/MoShiroFleetHooks.cs)
| Planner(有效任务集) | [`TaskPlanner.cs`](../../backend/src/MoShiro.Backend.Application/Tasks/TaskPlanner.cs)、[`EffectiveTasks.cs`](../../backend/src/MoShiro.Backend.Application/Tasks/EffectiveTasks.cs)
| Links(哪些 Probe 连哪些 Agent) | [`LinkService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/LinkService.cs)
| Releases(发布清单、可信发布密钥) | [`Application/Updates`](../../backend/src/MoShiro.Backend.Application/Updates),轮询 [`ReleasePoller.cs`](../../backend/src/MoShiro.Backend.Application/Updates/ReleasePoller.cs)
| Gateway(Fleet 网关、状态提供者、收件箱) | [`MoShiro.Backend.Gateway`](../../backend/src/MoShiro.Backend.Gateway),入库 [`RawChunkInbox.cs`](../../backend/src/MoShiro.Backend.Gateway/RawChunkInbox.cs)、[`IngestService.cs`](../../backend/src/MoShiro.Backend.Application/Ingest/IngestService.cs)
| Analysis(算子、指标、模型) | [`MoShiro.Backend.Analysis`](../../backend/src/MoShiro.Backend.Analysis),调度 [`AnalysisScheduler.cs`](../../backend/src/MoShiro.Backend.Application/Analysis/AnalysisScheduler.cs)
| LiveHub(热层与 SSE 主题) | [`LiveHub.cs`](../../backend/src/MoShiro.Backend.Application/Live/LiveHub.cs)
| Store(段文件 + SQLite) | [`MoShiro.Backend.Storage.Sqlite`](../../backend/src/MoShiro.Backend.Storage.Sqlite),原始块 [`SqliteRawStore.cs`](../../backend/src/MoShiro.Backend.Storage.Sqlite/SqliteRawStore.cs)
| Agent | [`AgentApp.cs`](../../backend/agent/src/MoShiro.Agent/AgentApp.cs)、任务 [`TaskManager.cs`](../../backend/agent/src/MoShiro.Agent/Tasks/TaskManager.cs)、Probe 会话 [`PeerHub.cs`](../../backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs)
| Probe | [`ProbeApp.cs`](../../backend/probe/src/MoShiro.Probe/ProbeApp.cs)、Agent 连接 [`Link/AgentConnection.cs`](../../backend/probe/src/MoShiro.Probe/Link/AgentConnection.cs)
| 采集引擎与协议 | [`MoShiro.Measurement`](../../backend/shared/MoShiro.Measurement)、[`MoShiro.Protocol`](../../backend/shared/MoShiro.Protocol) |
| Fleet 节点框架 | [`backend/fleet`](../../backend/fleet),注册工具 [`fleet-enroll`](../../backend/fleet/tools/fleet-enroll) |
- 线路(line):一对(Probe,Agent)。可以有多个 Probe(身份由 Probe 自己生成并用加入令牌注册,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:<probe_id>` 单独存放,互不混合。对外的线路 id 是 `<agent_id>~<probe_id>`。
- 目标任务(target):某个 Agent → 指定的 IP:端口(ICMP、TCP/TLS 建连),只能访问 Agent 本地允许的网段。
## 3. 数据链路
```text
采集(只记录) 传输(有界、至少一次) 分析(注册制、可重算) 展示
┌───────┐ 原始列(10s) ┌────────┐ RawChunk(10s) ┌──────────────────────────────────────────┐
│ Probe │───────────>│ Agent │──outbox(16)──>│ 段文件(30天) → Analysis → 指标窗口(1m) │──> REST
│ 积木 │<══════════>│ 积木 │ wss │ ↓ 流式处理未满窗口 ↓ 降采样 5m/1h │──> SSE
└───────┘ 数据通道 └────────┘ │ LiveHub(内存快照) │
└──────────────────────────────────────────┘
```
| 角色 | 职责 | 明确不做 |
| --- | --- | --- |
| Probe | 连接 Backend(身份、Agent 列表、系统与构建信息、更新);连接面板指定的 Agent;执行线路任务中 Probe 一侧的角色;记录本端原始列并交给对应的 Agent | 统计;落盘测量数据;任何主机操作 |
| Agent | 注册;接受 Probe;执行 Agent 一侧的角色和目标任务;合并两侧的列并上传;上报主机资源的汇总数字 | 统计;落盘测量数据;exec / 终端 / 文件操作(当前未提供);更新由宿主机上的更新器完成;采集主机的详细信息 |
| Backend | 注册与身份;任务编排;决定哪些 Probe 连哪些 Agent;持久化原始数据;分析;实时汇聚;API;分发 CI 签名的发布清单 | 主动连接 Probe 或 Agent(都是节点拨进来);验签和安装更新(在节点宿主机上) |
角色对应的代码:Probe 见 [`ProbeApp.cs`](../../backend/probe/src/MoShiro.Probe/ProbeApp.cs);Agent 见 [`AgentApp.cs`](../../backend/agent/src/MoShiro.Agent/AgentApp.cs),上传块由 [`ChunkAssembler.cs`](../../backend/agent/src/MoShiro.Agent/Chunks/ChunkAssembler.cs)合并;Backend 接收见 [`FleetGateway.cs`](../../backend/src/MoShiro.Backend.Gateway/FleetGateway.cs)。
## 4. 关键设计(详见各专题文档)
| 主题 | 要点 | 文档 | 代码 |
| --- | --- | --- | --- |
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 Fleet 提供;MoShiro 只写业务,其他产品也能复用 | [Fleet 文档](../../backend/fleet/README.md) | [`backend/fleet`](../../backend/fleet) |
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](../security/enrollment.md) | [`NodeIdentity.cs`](../../backend/fleet/src/Fleet.Identity/NodeIdentity.cs)、[`fleet-enroll`](../../backend/fleet/tools/fleet-enroll) |
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。CI 对镜像 digest 签名;Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 验签、按 digest 拉取、失败回滚;Podman Quadlet 装的 Agent、Probe 更简单:节点有序退出,systemd 用 `--pull always` 重新拉最新镜像,节点自己不下载、不验证清单;纯二进制节点的更新见[路线图](../roadmap.md) | [deployment.md](../ops/deployment.md) | [`moshiro-update.sh`](../../packaging/update/moshiro-update.sh)、[`RestartUpdater.cs`](../../backend/shared/MoShiro.Measurement/Update/RestartUpdater.cs)、[`packaging/quadlet`](../../packaging/quadlet) |
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) | [`TaskSet.cs`](../../backend/shared/MoShiro.Protocol/Tasks/TaskSet.cs)、[`TaskPlanner.cs`](../../backend/src/MoShiro.Backend.Application/Tasks/TaskPlanner.cs) |
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) | [`Kinds.cs`](../../backend/shared/MoShiro.Protocol/Catalog/Kinds/Kinds.cs)、[`BlockCatalog.cs`](../../backend/shared/MoShiro.Measurement/Blocks/BlockCatalog.cs) |
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) | [`MetricRegistry.cs`](../../backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs) |
| 协议 | 控制链路使用 Fleet(JSON-RPC 2.0 + 二进制通道);MoShiro 只定义 `moshiro.*` 方法、`TaskSet` 状态文档和 QUIC Datagram/TCP 数据面 | [protocol.md](protocol.md) | [`MoShiroMethodTable.cs`](../../backend/shared/MoShiro.Protocol/Rpc/MoShiroMethodTable.cs)、[`MethodTable.cs`](../../backend/fleet/src/Fleet.Rpc/MethodTable.cs) |
| 存储 | 原始块追加写入段文件,SQLite 记录偏移量;指标窗口通用存储并降采样 | [storage.md](storage.md) | [`Schema.cs`](../../backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs) |
| 展示查询 | 任意时间段(相对或绝对);自动步长(每条曲线最多约 720 个点);1s 内存热层(1 小时)加上 10s/1m/5m/1h 各层;累加器现场合并,汇总精确;列式响应;SSE 实时跟随 | [query.md](query.md) | [`QueryService.cs`](../../backend/src/MoShiro.Backend.Application/Query/QueryService.cs)、[`HotLayer.cs`](../../backend/src/MoShiro.Backend.Application/Live/HotLayer.cs) |
| API | `/api/public`(只读,可设为公开)、`/api/admin`(登录)、`/api/agent`(注册与 RPC);实时数据用 SSE | [api.md](../api/api.md) | [`v1.json`](../../backend/openapi/v1.json)、[`Api`](../../backend/src/MoShiro.Backend.Api) |
Kind 目录编译在二进制中,节点不接受远程下发代码。节点在 `fleet.hello` 的 capabilities 中上报支持的 Kind,Backend 只下发它支持的类型。
### 4.0 节点的更新
发布清单(镜像 digest)由 CI 签名,放进镜像仓库(`<仓库>-manifest` 小镜像的标签里);Backend 定时取、验签后登记(也接受 CI 直接 `POST /api/release/offer`),再原样转发给在线的 Agent / Probe。之后分两条路:
```text
Podman Quadlet(Agent、Probe、Backend,推荐)
节点比对清单和自己的版本 ──▶ 心跳报「有新版本」──▶ 管理端显示
自动(默认,运行满 10 分钟)或管理员点「更新」──▶ 节点有序退出
──▶ systemd 重启单元(Pull=always)──▶ 新镜像、新容器
该路径下节点本身不下载、不替换文件,也不验证清单;最多被请求一次有序重启
Backend:不自动,只有管理员点「更新并重启」;退出之前先把数据库备份到 backups/
一行脚本装的节点
宿主机 moshiro-update(root):用本机公钥验签 → 核对组件、仓库和 seq → podman pull 仓库@digest → 重启 → 不健康就回滚
──▶ 状态写进只读的 update-status/ ──▶ 节点随心跳上报 ──▶ Backend ──▶ 管理端(有新版本时点「更新」)
```
实现:宿主机更新器 [`moshiro-update.sh`](../../packaging/update/moshiro-update.sh)(安装 [`install-updater.sh`](../../packaging/update/install-updater.sh)),节点退出逻辑 [`RestartUpdater.cs`](../../backend/shared/MoShiro.Measurement/Update/RestartUpdater.cs),Backend 更新与备份 [`BackendUpdates.cs`](../../backend/src/MoShiro.Backend.Application/Updates/BackendUpdates.cs),清单登记 [`ReleaseService.cs`](../../backend/src/MoShiro.Backend.Application/Updates/ReleaseService.cs),Quadlet 单元 [`packaging/quadlet`](../../packaging/quadlet)。
细节见 [deployment.md](../ops/deployment.md)「Podman(Quadlet)的更新」(一行脚本装的节点另见「宿主机更新器」)。
### 4.1 回显的四个时间戳
回显类 Kind 记录 `t1..t4`,用于扣除 Agent 处理耗时、分别判断上下行丢包和计算各方向的抖动,不要求两端时钟同步。推导见 [measurement.md](measurement.md) 第 3.4 节。
### 4.2 NAT / CGNAT 适配
所有连接和 UDP 流都由 Probe 发起;Agent 把回复发往请求的实际源地址。会话以 `session_id` 和 MAC 认证,不绑定五元组,NAT 映射变化后自动跟随新地址。持续的 UDP 回显本身就起到保活作用。准入逻辑(重放窗口、令牌桶、源地址学习)见 [`Registries.cs`](../../backend/agent/src/MoShiro.Agent/DataPlane/Registries.cs),数据报路由见 [`MeasurementDatagramRouter.cs`](../../backend/shared/MoShiro.Measurement/Net/MeasurementDatagramRouter.cs)。
## 5. 安全边界
> 本节描述现状。Backend 或节点被攻破后的威胁模型、现有缺口(例如 Probe 会拨 Backend 给的任意地址、测量数据没有产生者签名)及状态见 [security.md](../security/security.md)。
```text
Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问本地配置允许的端口与网段;受本地硬上限约束
当前未提供的能力 exec / shell / 终端 / 文件读写(配置目录除外)/ 加载外部代码 / 采集主机标识(型号、主机名、IP、进程);更新在节点进程之外完成
即使 Backend 被攻破 攻击者最多能在本地硬上限内发测量流量,拿不到主机权限,也无法把 Agent 当成扫描器或放大器;
也推不出恶意更新(更新清单必须由发布密钥签名,节点宿主机上的更新器用本机公钥验签,并且只认本机配置的镜像仓库)
```
- 本地硬上限写在节点本机的配置文件中([`AgentConfig.cs`](../../backend/agent/src/MoShiro.Agent/AgentConfig.cs)),由 [`BudgetGuard.cs`](../../backend/shared/MoShiro.Measurement/Execution/BudgetGuard.cs) 检查,Backend 下发的任务如果超出,节点直接拒绝:允许访问的目标网段(默认为空,即不开放目标任务)、总码率和包速率上限、允许监听的端口。
- 构建期强制:常驻节点进程使用 [`BannedSymbols.Node.txt`](../../backend/BannedSymbols.Node.txt),禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](../dev/project-layout.md) 第 7 节。
- 权限分离:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新见[路线图](../roadmap.md)。
- 容器运行:Agent、Probe 的镜像基于 Alpine(musl,NativeAOT),宿主机用 rootful podman 加 systemd 运行:UID/GID 1654、只读文件系统、`--cap-drop all`、`no-new-privileges`、`/tmp` 用 tmpfs;配置和身份放在挂载的数据目录里。
- 属主检查:[`OwnerGuard.cs`](../../backend/fleet/src/Fleet.Hosting/OwnerGuard.cs);预握手敲门:[`KnockGate.cs`](../../backend/fleet/src/Fleet.Identity/KnockGate.cs)。
- 最小权限运行:非 root 用户;只在需要时授予 `CAP_NET_RAW`(ICMP);默认端口都高于 1024,不需要 `CAP_NET_BIND_SERVICE`,只有在 Agent 本地显式开启 443 时才授予。
- 身份([`NodeIdentity.cs`](../../backend/fleet/src/Fleet.Identity/NodeIdentity.cs)):节点使用本地生成的 ECDSA P-256 密钥对签名认证(Agent 和 Probe 都自己生成;Probe 有两把独立的密钥,一把连面板、一把连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
- 防放大([`Registries.cs`](../../backend/agent/src/MoShiro.Agent/DataPlane/Registries.cs)):未认证的包静默丢弃;回复长度与请求相等;每个流按声明速率限速;使用重放窗口。
- 容器里的主机指标:容器里的 Agent 通过只读挂载宿主机的 `/proc`、`/sys`、根目录和 `/etc/os-release`(环境变量 `HOST_PROC`、`HOST_SYS`、`HOST_ROOT`)读取宿主机的资源占用和系统名(采样见 [`HostSampler.cs`](../../backend/shared/MoShiro.Measurement/Platform/HostSampler.cs));读不到的项返回空值,不会退回去读容器自己的数据。Probe 的 compose 也一样挂载这几项。
- 自身状态:节点上报进程内的计数(会话数、包速率、队列、丢弃数);Agent 另外每隔几秒用 `moshiro.host` 上报一份主机资源占用的汇总数字(可以在 `agent.json` 里用 `reporting.host_stats = false` 整个关掉),最新值实时推给展示页,Backend 每分钟存一个样本(默认保留 7 天,`retention.host_days`),见 [`HostStatusService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/HostStatusService.cs)。
## 6. 可靠性与资源
- Probe:每个 Agent 未交付的原始列 ≤ 8 MB;Agent:上传队列 ≤ 64 MB,满时丢弃最旧的块,并通过 `moshiro.event(gap)` 上报缺口。
- 至少一次投递:Backend 持久化之后才回复 ACK,按 `chunk_id` 去重([`RawChunkInbox.cs`](../../backend/src/MoShiro.Backend.Gateway/RawChunkInbox.cs))。
- 节点重启会丢失未上传的数据,并体现为数据缺口。
- 窗口时间以 Agent 时钟为准;时钟偏差超过 5 秒时记录告警事件。
- 规模:10+ 个 Agent × 2~4 种持续 Kind × 每 10 秒一块,总计约每秒 2~6 块。