# 架构设计 ## 1. 目标与定位 只关注网络层的线路质量面板:单体面板(Backend)、轻量节点(Agent)、长连接 RPC 与任务下发。 - 采集:多个 拨号方(家宽等需要观测的出口)与多台海外 VPS Agent 之间记录逐包、逐事件的原始数据。哪些 拨号方 连哪些 Agent 由面板决定,默认全连,也可以手动选择。 - 分析:Backend 的分析层推算在某条线路上部署 REALITY 类、Hysteria2 类或 Minecraft 服务的体验,并对多条线路排序和对比。 - 主机状态:Agent 读取 `/proc`,上报 CPU 占用、内存、根分区、网卡累计流量与瞬时网速、运行时长这类汇总数字。不采集 CPU 型号、主机名、网卡名、IP 和进程。拨号方 的上报可在 `agent.json` 中用 `reporting.host_stats: false` 关闭。展示页以卡片呈现每个节点的主机状态,线路在卡片下方的面板中。 - 构建信息:节点连接面板时自报系统、架构和构建信息(版本、tag、提交、构建时间),用于展示和更新。国家和运营商由 Backend 依据节点来源地址查询,节点不访问任何外部服务,展示页不含任何 IP。 - 节点能力:当前 Agent 只包含测量与上报,不提供 SSH、远程执行、终端和文件读写;更新由节点宿主机上的更新器或服务管理器完成,节点进程只把面板转来的发布清单和更新请求写入数据目录。后续能力的规划见 [security.md](../security/security.md) 第 9 节。 ## 2. 拓扑 ```text Web | REST / SSE | Backend ^ ^ | wss | 注册、任务、拨号目标、状态、数据 | | Agent A <=====> Agent B 直接测量 ``` 所有节点都是 Agent,默认同时监听和拨号。Backend 为每个节点下发拨号目标以及允许接入的公钥名册,自动连接开启时,所有 Agent 拨其他可拨号 Agent;A→B 和 B→A 分别编排、存储和展示。单方向的手动设置优先于自动连接开关。可在本地关闭监听,但不产生另一种节点角色。 Agent → Backend 的心跳包含运行状态、拨出线路的控制层与任务层状态、失败原因、重试次数与等待时间,以及当前接入会话。管理端的 Agent 详情展示拨出与接入连接,线路画板汇总所有方向并提供开关和立即重试。Backend 不转发节点之间的测量流量。 各组件的代码位置: | 组件 | 代码 | | --- | --- | | 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 注册与状态) | [`AgentService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/AgentService.cs)、[`AgentService.cs`](../../backend/src/MoShiro.Backend.Application/Agents/AgentService.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(哪些 拨号方 连哪些 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)、拨号方 会话 [`PeerHub.cs`](../../backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs) | 拨号方 | [`AgentApp.cs`](../../backend/agent/src/MoShiro.Agent/AgentApp.cs)、Agent 连接 [`Link/DialConnection.cs`](../../backend/agent/src/MoShiro.Agent/Dial/DialConnection.cs)(控制层)、[`Link/TaskLink.cs`](../../backend/agent/src/MoShiro.Agent/Dial/TaskLink.cs)(QUIC 数据通道) | 采集引擎与协议 | [`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):一对(拨号方,Agent)。可以有多个 拨号方(身份由 拨号方 自己生成并用加入令牌注册,可以吊销、删除),每个 拨号方 连接多个 Agent,每个 Agent 同时服务多个 拨号方;一条线路的数据按 `line:` 单独存放,互不混合。对外的线路 id 是 `~`。 - 目标任务(target):某个 Agent → 指定的 IP:端口(ICMP、TCP/TLS 建连),只能访问 Agent 本地允许的网段。 ## 3. 数据链路 ```text 采集(只记录) 传输(有界、至少一次) 分析(注册制、可重算) 展示 ┌───────┐ 原始列(10s) ┌────────┐ RawChunk(10s) ┌──────────────────────────────────────────┐ │ 拨号方 │───────────>│ Agent │──outbox(16)──>│ 段文件(30天) → Analysis → 指标窗口(1m) │──> REST │ 积木 │<══════════>│ 积木 │ wss │ ↓ 流式处理未满窗口 ↓ 降采样 5m/1h │──> SSE └───────┘ 数据通道 └────────┘ │ LiveHub(内存快照) │ └──────────────────────────────────────────┘ ``` | 角色 | 职责 | 明确不做 | | --- | --- | --- | | 拨号方 | 连接 Backend(身份、Agent 列表、系统与构建信息、更新);连接面板指定的 Agent;执行线路任务中 拨号方 一侧的角色;记录本端原始列并交给对应的 Agent | 统计;落盘测量数据;任何主机操作 | | Agent | 注册;接受 拨号方;执行 Agent 一侧的角色和目标任务;合并两侧的列并上传;上报主机资源的汇总数字 | 统计;落盘测量数据;exec / 终端 / 文件操作(当前未提供);更新由宿主机上的更新器完成;采集主机的详细信息 | | Backend | 注册与身份;任务编排;决定哪些 拨号方 连哪些 Agent;持久化原始数据;分析;实时汇聚;API;分发 CI 签名的发布清单 | 主动连接 拨号方 或 Agent(都是节点拨进来);验签和安装更新(在节点宿主机上) | 角色对应的代码:拨号方 见 [`AgentApp.cs`](../../backend/agent/src/MoShiro.Agent/AgentApp.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` 和加入令牌自动注册,之后用签名认证;拨号方 用自己的身份直连面板,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 更简单:节点有序退出,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。之后分两条路: ```text Podman Quadlet(Agent、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 流都由 拨号方 发起;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)。 ### 4.3 线路连接:控制层与任务层 拨号方 到每个 Agent 的连接分两层,各有一个状态机和独立的重试节奏([`LinkStatus.cs`](../../backend/agent/src/MoShiro.Agent/Dial/LinkStatus.cs))。 ```text 控制层 TCP+TLS 长连接:控制 RPC(时钟、任务集、测试阶段、OutboxAck)+ 回传数据(发件箱) 控制层断开,Agent 节点 才算失联。 dialing ─▶ authenticating ─▶ syncing ─▶ up ▲ │ RPC 关闭 / 120 s 无应答 / keepalive 失败 └─────────── backoff ◀────────────────┘ 任务层 测量链路,控制层在线期间运行 ├─ tcp 各 TCP 类积木(tcp_echo / connect / hold / tcp_bulk)自带套接字,积木内部断线重拨,不依赖 QUIC └─ quic QUIC datagram(udp_echo / udp_train) connecting ─▶ ready ─▶ retrying ─▶ connecting … 控制层不在线时为 idle ``` | | 控制层 | 任务层 QUIC | | --- | --- | --- | | 断开的含义 | 失联;数据面凭据随控制会话失效,所有积木停止 | 仅 UDP 类积木受影响,控制连接和 TCP 类积木继续 | | 重试节奏 | 1、2、4、8、16 s,32 s × 5,之后每 60 s | 1、2、3、5 s,之后固定 5 s,带 ±20% 抖动;连接稳定 10 s 后节奏复位 | | 数据面静默 | 120 s 没有收到任何控制帧即断开重拨 | 发送中 30 s 没有收到任何报文即关闭并重拨 | | 手动「立即重试」 | 跳过退避 | 跳过重拨等待 | 回传数据(`RawChunk` 列块)进入属于这条连接的发件箱,经控制连接发送;控制层重连后重发未确认的块(Backend 按 `chunk_id` 去重)。会话结束前,积木先把已稳定的行交给发件箱。 状态随心跳上报(`LinkHealth`:`control`、`task.tcp`、`task.quic`),失败原因带阶段和分类(`phase`、`kind`:`timeout`、`refused`、`reset`、`dns`、`tls`、`auth`、`closed`、`silent`)。Backend 据此给出线路状态:`control_connecting`、`control_down`(失联)、`task_connecting`、`task_retrying`、`connected`([`LinkEndpoints.cs`](../../backend/src/MoShiro.Backend.Api/Admin/LinkEndpoints.cs))。日志中控制层的每次迁移写一条 `link state`,QUIC 只在可用与不可用的边界上各写一条,重拨过程中的失败只计入握手统计。 ## 5. 安全边界 > 本节描述现状。Backend 或节点被攻破后的威胁模型、现有缺口(例如 拨号方 会拨 Backend 给的任意地址、测量数据没有产生者签名)及状态见 [security.md](../security/security.md)。 ```text Agent 能做的 执行编译在二进制中的 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-agent`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新见[路线图](../roadmap.md)。 - 容器运行:Agent 的镜像基于 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 都自己生成;拨号方 有两把独立的密钥,一把连面板、一把连 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));读不到的项返回空值,不会退回去读容器自己的数据。拨号方 的 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. 可靠性与资源 - 拨号方:每个 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 块。