docs: merge the API guidelines into api.md; drop the one-off trial reports; fix the node-update description (Quadlet restart vs host updater); tidy TODO [CI]
This commit is contained in:
1 parent
900bbde46a
commit
4609048865
12 files changed
+172
-224
No files matched your search
@@ -5,7 +5,7 @@
|
|||||||
## 测量
|
## 测量
|
||||||
|
|
||||||
- 路由跳数、路径漂移和 trace
|
- 路由跳数、路径漂移和 trace
|
||||||
- QUIC 握手耗时:TCP 建连和 TLS 握手已经有了(`connect` 任务)。.NET 的 QUIC 依赖系统里的 msquic 原生库,和节点「只依赖 OpenSSL、NativeAOT 单文件」冲突,要做只能自己写最小的 QUIC Initial 握手探测
|
- QUIC 握手耗时:TCP 建连和 TLS 握手已经有了(`connect` 任务)。.NET 的 QUIC 依赖系统里的 msquic 原生库,和节点「只依赖 OpenSSL、NativeAOT 单文件」冲突,要做只能自己写最小的 QUIC Initial 握手探测;另外 .NET 还没有 QUIC datagram([dotnet/runtime#123418](https://github.com/dotnet/runtime/issues/123418) 只是提案),`System.Net.Quic` 只有可靠的流,重传会盖住丢包,测不了 UDP 丢包率
|
||||||
- 网页长截图
|
- 网页长截图
|
||||||
- pcap / eBPF 采集
|
- pcap / eBPF 采集
|
||||||
|
|
||||||
@@ -14,10 +14,6 @@
|
|||||||
- 单个 Agent 的任务(目前是线路模板加按 Agent 的覆盖,没有只属于某一个 Agent 的独立任务)
|
- 单个 Agent 的任务(目前是线路模板加按 Agent 的覆盖,没有只属于某一个 Agent 的独立任务)
|
||||||
- 原生 webhook 投递;告警(基于 webhook)。实现之前先定约束(只允许 HTTPS、禁止私网和回环、不跟随重定向、限制超时和响应大小、解析后再检查地址),见 [`docs/security/security.md`](docs/security/security.md) 第 9.2 节
|
- 原生 webhook 投递;告警(基于 webhook)。实现之前先定约束(只允许 HTTPS、禁止私网和回环、不跟随重定向、限制超时和响应大小、解析后再检查地址),见 [`docs/security/security.md`](docs/security/security.md) 第 9.2 节
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
- 后端自动下载前端资源:用 Podman、Docker 部署时,默认拉取最新的前端资源(现在前端要自己构建,镜像里没有)。
|
|
||||||
[security.md](docs/security/security.md)
|
|
||||||
## 访客页面
|
## 访客页面
|
||||||
|
|
||||||
- 人机验证:访客页面要通过验证才能拉数据,前端访问时静默触发。候选 tiago2/cap 和 Cloudflare Turnstile。
|
- 人机验证:访客页面要通过验证才能拉数据,前端访问时静默触发。候选 tiago2/cap 和 Cloudflare Turnstile。
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。
|
2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。
|
||||||
3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。
|
3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。
|
||||||
4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 序列化,不使用反射。
|
4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 序列化,不使用反射。
|
||||||
5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/ops/deployment.md`](../../docs/ops/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
|
5. **更新不在 Fleet 里**:节点进程没有自更新能力。Quadlet 装的节点由退出后 systemd 重新拉镜像、Backend 和脚本装的节点由宿主机上的更新器验签更新,见 [`docs/ops/deployment.md`](../../docs/ops/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
|
||||||
|
|
||||||
## 身份
|
## 身份
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
|
|||||||
namespace MoShiro.Backend.Tests;
|
namespace MoShiro.Backend.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对照 docs/api/api-guidelines.md 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。
|
/// 对照 docs/api/api.md 第 7 节 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。
|
||||||
/// 新增端点违反规范时,这里会直接失败(等价于文档里的 <c>eng/check-openapi</c>)。
|
/// 新增端点违反规范时,这里会直接失败(等价于文档里的 <c>eng/check-openapi</c>)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Collection("backend")]
|
[Collection("backend")]
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
| 安全 | [security.md](security/security.md) | 零信任:威胁模型、缺口与状态、协议卡口、监听隐身、extension |
|
| 安全 | [security.md](security/security.md) | 零信任:威胁模型、缺口与状态、协议卡口、监听隐身、extension |
|
||||||
| | [access.md](security/access.md) | 访问控制与入网信任链:能力、API key、passkey、操作员集合、属主、安装的供应链 |
|
| | [access.md](security/access.md) | 访问控制与入网信任链:能力、API key、passkey、操作员集合、属主、安装的供应链 |
|
||||||
| | [enrollment.md](security/enrollment.md) | 注册与身份:Agent、Probe 的接入,节点本地硬上限 |
|
| | [enrollment.md](security/enrollment.md) | 注册与身份:Agent、Probe 的接入,节点本地硬上限 |
|
||||||
| 接口 | [api.md](api/api.md)、[api-guidelines.md](api/api-guidelines.md) | 端点说明;API 规范与约束 |
|
| 接口 | [api.md](api/api.md) | 端点说明;API 设计约束 |
|
||||||
| 运维 | [deployment.md](ops/deployment.md) | 构建、发布、CI、部署与更新、发布密钥 |
|
| 运维 | [deployment.md](ops/deployment.md) | 构建、发布、CI、部署与更新、发布密钥 |
|
||||||
| 开发 | [project-layout.md](dev/project-layout.md)、[coding-style.md](dev/coding-style.md) | 仓库结构与依赖规则;代码风格 |
|
| 开发 | [project-layout.md](dev/project-layout.md)、[coding-style.md](dev/coding-style.md) | 仓库结构与依赖规则;代码风格 |
|
||||||
|
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
# API 设计约束
|
|
||||||
|
|
||||||
本文是所有 Backend REST 端点都必须遵守的**规则**;具体端点见 [api.md](api.md)。规则参考了 [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 [protocol.md](../design/protocol.md) 第 4 节。
|
|
||||||
|
|
||||||
## 1. 契约生成链路
|
|
||||||
|
|
||||||
```text
|
|
||||||
Minimal API 端点(TypedResults 返回类型 + 显式元数据)
|
|
||||||
│ dotnet build(Microsoft.Extensions.ApiDescription.Server)
|
|
||||||
▼
|
|
||||||
backend/openapi/v1.json(OpenAPI 3.1,提交到仓库)
|
|
||||||
│ CI:git diff --exit-code,未提交的变更会导致失败
|
|
||||||
▼
|
|
||||||
web:openapi-typescript 生成类型,调用方的类型错误会直接暴露
|
|
||||||
```
|
|
||||||
|
|
||||||
- 端点**必须**声明精确的返回类型,例如 `Results<Ok<AgentDto>, NotFound<ProblemDetails>>`,不能返回 `IResult` 或匿名对象,否则 OpenAPI 文档会不准确。
|
|
||||||
- 每个端点都必须设置 `WithName("agents_list")`(即 operationId,snake_case、`资源_动作`)、`WithSummary`、`WithTags`。
|
|
||||||
- 请求和响应 DTO 放在 `MoShiro.Backend.Api/Contracts/`,并注册到 Backend 的 JSON 源生成上下文。**不要直接暴露 `MoShiro.Protocol` 中的线协议类型**,因为 REST 与线协议的演进节奏不同。
|
|
||||||
- **结构强类型,指标弱类型**:资源结构(Agent、任务、测试等)必须是强类型 DTO;而**指标值和评分**统一使用字典 `metrics: { "<metric_id>": number }`、`scores: { "<model_id>": number }`,键的含义、单位、版本由 `/api/public/catalog/*` 给出。这样新增指标或模型不需要改 OpenAPI,也不需要改前端类型。
|
|
||||||
|
|
||||||
## 2. 版本与兼容性
|
|
||||||
|
|
||||||
- 当前版本为 v1,路径前缀为 `/api/public`、`/api/admin`、`/api/agent/v1`。出现破坏性变更时,新增 `/api/v2/public`、`/api/v2/admin` 等路径。v1 内只允许**非破坏性变更**:
|
|
||||||
|
|
||||||
| 非破坏性变更(v1 内允许) | 破坏性变更(需要新建 v2) |
|
|
||||||
| --- | --- |
|
|
||||||
| 新增端点 | 删除或重命名端点、字段 |
|
|
||||||
| 响应中新增字段 | 修改字段类型、单位或语义 |
|
|
||||||
| 新增可选的请求字段或查询参数 | 可选字段改为必填 |
|
|
||||||
| 枚举新增值 | 删除枚举值 |
|
|
||||||
| 放宽校验 | 收紧校验、修改默认值 |
|
|
||||||
| 新增错误码 | 修改已有错误码对应的 HTTP 状态 |
|
|
||||||
|
|
||||||
- 客户端必须忽略未知字段,也必须能处理未知的枚举值。
|
|
||||||
- 需要 v2 时,v1 和 v2 至少并存一个发布周期;OpenAPI 文档按版本分文件(`v1.json`、`v2.json`)。
|
|
||||||
|
|
||||||
## 3. URL 与方法
|
|
||||||
|
|
||||||
- **按调用方分层**:`/api/public`(只读展示)、`/api/admin`(管理)、`/api/agent`(节点)。同一个资源在不同层中可以有不同的 DTO,例如 public 层不暴露 `pubkey`、`local_limits`。
|
|
||||||
- 路径全部小写,资源名用复数名词,多个单词用 `-` 连接:`/api/admin/join-tokens`。
|
|
||||||
- 嵌套最多一层;子资源一旦有全局唯一 ID,就提供顶层路径(`/probes/{probe_id}`)。
|
|
||||||
- 查询参数和 JSON 字段使用 snake_case。
|
|
||||||
|
|
||||||
| 操作 | 方法 | 成功状态 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 列表 | GET 集合 | 200 | 分页 |
|
|
||||||
| 读取 | GET 单个 | 200 | |
|
|
||||||
| 创建 | POST 集合 | 201 + `Location` | 响应体返回完整资源 |
|
|
||||||
| 部分更新 | PATCH | 200 | `application/merge-patch+json`(RFC 7396) |
|
|
||||||
| 删除 | DELETE | 204 | 资源不存在时同样返回 204(幂等) |
|
|
||||||
| 非 CRUD 动作 | POST `/{resource}/{id}/{verb}` | 200 或 204 | 例如 `revoke`、`rotate-token` |
|
|
||||||
|
|
||||||
- GET 请求不能产生副作用。PUT 不使用:配置类资源都通过 PATCH 修改。
|
|
||||||
|
|
||||||
## 4. 数据表示
|
|
||||||
|
|
||||||
| 类别 | 规则 | 示例 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 时间点 | RFC 3339 UTC,以 `Z` 结尾,精确到毫秒。**唯一例外**:列式时序响应中的时间轴数组 `t` 使用 Unix 毫秒(见 [query.md](../design/query.md)) | `2026-09-27T10:00:00.000Z` |
|
|
||||||
| 时长 | 整数 + 单位后缀 | `interval_ms: 1000` |
|
|
||||||
| 延迟 | 带小数的毫秒值(指标字典中的单位见目录) | `rtt_p95_ms: 58.93` |
|
|
||||||
| 百分比 | 0–100 的数字 + `_pct` | `loss_pct: 0.5` |
|
|
||||||
| 计数 | 整数,不使用浮点数 | `attempts: 600` |
|
|
||||||
| ID | 带类型前缀的 ULID 字符串 | `prb_01J9Z...` |
|
|
||||||
| 枚举 | snake_case 字符串 | `"udp_echo"` |
|
|
||||||
| 缺失值 | 字段始终存在;未知值写 `null`,不省略字段 | `"last_seen_at": null` |
|
|
||||||
| 布尔值 | 使用 `is_`/`has_` 前缀,或者用意义明确的形容词 | `online`、`enabled` |
|
|
||||||
|
|
||||||
## 5. 错误
|
|
||||||
|
|
||||||
所有 4xx/5xx 响应都使用 `application/problem+json`(RFC 9457),并附加扩展字段:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "about:blank",
|
|
||||||
"title": "Validation failed",
|
|
||||||
"status": 400,
|
|
||||||
"code": "validation_failed",
|
|
||||||
"detail": "interval_ms must be >= 200",
|
|
||||||
"trace_id": "00-4bf92f...-01",
|
|
||||||
"errors": { "interval_ms": ["must be >= 200"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `code` 取自共享的 `ErrorCode` 枚举,是客户端做判断的**唯一**依据;`title`/`detail` 只供人阅读,内容可能变化。
|
|
||||||
- `errors` 只在 400 `validation_failed` 中出现,键名是 JSON 字段路径。
|
|
||||||
- 5xx 响应不能泄露异常信息或调用栈,只提供 `trace_id`,用于在日志中定位问题。
|
|
||||||
|
|
||||||
| 状态码 | 使用场景 |
|
|
||||||
| --- | --- |
|
|
||||||
| 400 | 请求格式错误或校验失败 |
|
|
||||||
| 401 | 未认证 |
|
|
||||||
| 404 | 资源不存在 |
|
|
||||||
| 403 | 已认证但作用域不足 |
|
|
||||||
| 409 | 名称冲突、状态冲突(例如 `probe_offline`) |
|
|
||||||
| 410 | 资源曾经存在但已过保留期(例如 `raw_expired`) |
|
|
||||||
| 412 | `If-Match` 不匹配 |
|
|
||||||
| 413 | 请求体超过上限 |
|
|
||||||
| 422 | 语义上无法满足(例如 `range_too_large`) |
|
|
||||||
| 429 | 超过限流,需要带 `Retry-After` |
|
|
||||||
| 503 | 存储不可用,或正在关闭 |
|
|
||||||
|
|
||||||
## 6. 集合与查询
|
|
||||||
|
|
||||||
- 分页由服务端驱动:`?limit=`(默认 50,最大 500)和 `?cursor=`(不透明字符串);响应格式为 `{ "items": [...], "next_cursor": string | null }`。排序必须稳定(按 ID 或时间 + ID)。
|
|
||||||
- 时间范围参数统一使用 `from`/`to`,区间为左闭右开 `[from, to)`。缺省时 `to = now`,`from = to − 1h`。
|
|
||||||
- 所有查询都有上限(点数、时间跨度、结果条数);超出时返回 422 并在 `detail` 中给出可行的替代方案,不能悄悄截断。
|
|
||||||
|
|
||||||
## 7. 并发控制
|
|
||||||
|
|
||||||
- 配置类资源(Agent、Probe、任务)在响应中返回 `ETag`,PATCH/DELETE 支持 `If-Match`,不匹配时返回 412。没有带 `If-Match` 的请求按“最后写入者获胜”处理。
|
|
||||||
|
|
||||||
## 8. 安全
|
|
||||||
|
|
||||||
- 只提供 HTTPS。认证方式有两种:
|
|
||||||
- 浏览器使用 Cookie 会话:`HttpOnly; Secure; SameSite=Strict`;所有非 GET 请求都必须带 `X-CSRF-Token` 请求头;
|
|
||||||
- 脚本使用 `Authorization: Bearer <api_token>`,作用域为 `read` 或 `admin`。
|
|
||||||
- 凭据不能放在 URL 或查询参数中;SSE 依靠 Cookie 或同源令牌认证。
|
|
||||||
- 登录接口需要限流(例如每个 IP 每分钟 10 次),失败时统一返回 401,不区分“用户不存在”和“密码错误”。
|
|
||||||
- public 层根据站点设置和 `public_visible` 过滤数据:被隐藏的资源对匿名访客一律返回 404,而不是 403。
|
|
||||||
- 令牌明文只在创建或轮换的响应中出现一次;这类响应带 `Cache-Control: no-store`。
|
|
||||||
- 请求体上限为 64 KiB;查询接口有超时(默认 10 秒)。
|
|
||||||
|
|
||||||
## 9. 实时推送(SSE)
|
|
||||||
|
|
||||||
- 实时数据使用 `text/event-stream`。`event` 名称就是数据类型(`line`、`node`、`point`、`test`);`data` 是单行 JSON,遵守第 4 节的数据表示规则。
|
|
||||||
- 每 15 秒发送一行注释作为心跳。重连后服务端先推送当前快照,再推送增量,不补发历史。
|
|
||||||
- 订阅的主题数和连接数都有上限,超出时返回 429 `too_many_streams`。
|
|
||||||
- 推送的内容必须能由对应的 REST 查询重建:SSE 只负责“更快地获得变化”,不承载独有的数据。
|
|
||||||
|
|
||||||
## 10. 新增端点检查清单
|
|
||||||
|
|
||||||
1. 符合第 3 节的路径与方法规则,并有 operationId、summary 和 tag;
|
|
||||||
2. 返回类型精确(`TypedResults`),错误使用 problem+json,错误码来自 `ErrorCode`;
|
|
||||||
3. DTO 符合第 4 节的数据表示规则,并注册到 JSON 源生成上下文;
|
|
||||||
4. 列表支持分页,查询有上限;
|
|
||||||
5. 选择正确的层(public / admin / agent),public 层只暴露可以公开的字段;
|
|
||||||
6. 重新生成并提交 `openapi/v1.json`,在 PR 中说明是否属于破坏性变更;
|
|
||||||
7. 测试覆盖:成功路径、校验失败、404、鉴权失败、隐藏资源;
|
|
||||||
8. 同步更新 [api.md](api.md)。
|
|
||||||
+143
-1
@@ -1,6 +1,6 @@
|
|||||||
# Backend API 规范(v1)
|
# Backend API 规范(v1)
|
||||||
|
|
||||||
本文列出 Backend 的全部对外接口。通用规则见 [api-guidelines.md](api-guidelines.md);OpenAPI 3.1 文档在运行时由 `/openapi/v1.json` 提供,构建时同时生成快照 `backend/openapi/v1.json`。两者冲突时,以 OpenAPI 为准。
|
本文列出 Backend 的全部对外接口。通用规则见本文第 7 节「设计约束」;OpenAPI 3.1 文档在运行时由 `/openapi/v1.json` 提供,构建时同时生成快照 `backend/openapi/v1.json`。两者冲突时,以 OpenAPI 为准。
|
||||||
|
|
||||||
## 1. 分层
|
## 1. 分层
|
||||||
|
|
||||||
@@ -191,3 +191,145 @@
|
|||||||
| 422 | `range_too_large`、`invalid_step`、`unknown_metric`、`unknown_preset`、`too_many_metrics` |
|
| 422 | `range_too_large`、`invalid_step`、`unknown_metric`、`unknown_preset`、`too_many_metrics` |
|
||||||
| 429 | `too_many_requests`、`too_many_streams` |
|
| 429 | `too_many_requests`、`too_many_streams` |
|
||||||
| 503 | `storage_unavailable` |
|
| 503 | `storage_unavailable` |
|
||||||
|
|
||||||
|
## 7. 设计约束
|
||||||
|
|
||||||
|
本文是所有 Backend REST 端点都必须遵守的**规则**;具体端点见上面第 1–6 节。规则参考了 [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 [protocol.md](../design/protocol.md) 第 4 节。
|
||||||
|
|
||||||
|
### 7.1 契约生成链路
|
||||||
|
|
||||||
|
```text
|
||||||
|
Minimal API 端点(TypedResults 返回类型 + 显式元数据)
|
||||||
|
│ dotnet build(Microsoft.Extensions.ApiDescription.Server)
|
||||||
|
▼
|
||||||
|
backend/openapi/v1.json(OpenAPI 3.1,提交到仓库)
|
||||||
|
│ CI:git diff --exit-code,未提交的变更会导致失败
|
||||||
|
▼
|
||||||
|
web:openapi-typescript 生成类型,调用方的类型错误会直接暴露
|
||||||
|
```
|
||||||
|
|
||||||
|
- 端点**必须**声明精确的返回类型,例如 `Results<Ok<AgentDto>, NotFound<ProblemDetails>>`,不能返回 `IResult` 或匿名对象,否则 OpenAPI 文档会不准确。
|
||||||
|
- 每个端点都必须设置 `WithName("agents_list")`(即 operationId,snake_case、`资源_动作`)、`WithSummary`、`WithTags`。
|
||||||
|
- 请求和响应 DTO 放在 `MoShiro.Backend.Api/Contracts/`,并注册到 Backend 的 JSON 源生成上下文。**不要直接暴露 `MoShiro.Protocol` 中的线协议类型**,因为 REST 与线协议的演进节奏不同。
|
||||||
|
- **结构强类型,指标弱类型**:资源结构(Agent、任务、测试等)必须是强类型 DTO;而**指标值和评分**统一使用字典 `metrics: { "<metric_id>": number }`、`scores: { "<model_id>": number }`,键的含义、单位、版本由 `/api/public/catalog/*` 给出。这样新增指标或模型不需要改 OpenAPI,也不需要改前端类型。
|
||||||
|
|
||||||
|
### 7.2 版本与兼容性
|
||||||
|
|
||||||
|
- 当前版本为 v1,路径前缀为 `/api/public`、`/api/admin`、`/api/agent/v1`。出现破坏性变更时,新增 `/api/v2/public`、`/api/v2/admin` 等路径。v1 内只允许**非破坏性变更**:
|
||||||
|
|
||||||
|
| 非破坏性变更(v1 内允许) | 破坏性变更(需要新建 v2) |
|
||||||
|
| --- | --- |
|
||||||
|
| 新增端点 | 删除或重命名端点、字段 |
|
||||||
|
| 响应中新增字段 | 修改字段类型、单位或语义 |
|
||||||
|
| 新增可选的请求字段或查询参数 | 可选字段改为必填 |
|
||||||
|
| 枚举新增值 | 删除枚举值 |
|
||||||
|
| 放宽校验 | 收紧校验、修改默认值 |
|
||||||
|
| 新增错误码 | 修改已有错误码对应的 HTTP 状态 |
|
||||||
|
|
||||||
|
- 客户端必须忽略未知字段,也必须能处理未知的枚举值。
|
||||||
|
- 需要 v2 时,v1 和 v2 至少并存一个发布周期;OpenAPI 文档按版本分文件(`v1.json`、`v2.json`)。
|
||||||
|
|
||||||
|
### 7.3 URL 与方法
|
||||||
|
|
||||||
|
- **按调用方分层**:`/api/public`(只读展示)、`/api/admin`(管理)、`/api/agent`(节点)。同一个资源在不同层中可以有不同的 DTO,例如 public 层不暴露 `pubkey`、`local_limits`。
|
||||||
|
- 路径全部小写,资源名用复数名词,多个单词用 `-` 连接:`/api/admin/join-tokens`。
|
||||||
|
- 嵌套最多一层;子资源一旦有全局唯一 ID,就提供顶层路径(`/probes/{probe_id}`)。
|
||||||
|
- 查询参数和 JSON 字段使用 snake_case。
|
||||||
|
|
||||||
|
| 操作 | 方法 | 成功状态 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 列表 | GET 集合 | 200 | 分页 |
|
||||||
|
| 读取 | GET 单个 | 200 | |
|
||||||
|
| 创建 | POST 集合 | 201 + `Location` | 响应体返回完整资源 |
|
||||||
|
| 部分更新 | PATCH | 200 | `application/merge-patch+json`(RFC 7396) |
|
||||||
|
| 删除 | DELETE | 204 | 资源不存在时同样返回 204(幂等) |
|
||||||
|
| 非 CRUD 动作 | POST `/{resource}/{id}/{verb}` | 200 或 204 | 例如 `revoke`、`rotate-token` |
|
||||||
|
|
||||||
|
- GET 请求不能产生副作用。PUT 不使用:配置类资源都通过 PATCH 修改。
|
||||||
|
|
||||||
|
### 7.4 数据表示
|
||||||
|
|
||||||
|
| 类别 | 规则 | 示例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 时间点 | RFC 3339 UTC,以 `Z` 结尾,精确到毫秒。**唯一例外**:列式时序响应中的时间轴数组 `t` 使用 Unix 毫秒(见 [query.md](../design/query.md)) | `2026-09-27T10:00:00.000Z` |
|
||||||
|
| 时长 | 整数 + 单位后缀 | `interval_ms: 1000` |
|
||||||
|
| 延迟 | 带小数的毫秒值(指标字典中的单位见目录) | `rtt_p95_ms: 58.93` |
|
||||||
|
| 百分比 | 0–100 的数字 + `_pct` | `loss_pct: 0.5` |
|
||||||
|
| 计数 | 整数,不使用浮点数 | `attempts: 600` |
|
||||||
|
| ID | 带类型前缀的 ULID 字符串 | `prb_01J9Z...` |
|
||||||
|
| 枚举 | snake_case 字符串 | `"udp_echo"` |
|
||||||
|
| 缺失值 | 字段始终存在;未知值写 `null`,不省略字段 | `"last_seen_at": null` |
|
||||||
|
| 布尔值 | 使用 `is_`/`has_` 前缀,或者用意义明确的形容词 | `online`、`enabled` |
|
||||||
|
|
||||||
|
### 7.5 错误
|
||||||
|
|
||||||
|
所有 4xx/5xx 响应都使用 `application/problem+json`(RFC 9457),并附加扩展字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "about:blank",
|
||||||
|
"title": "Validation failed",
|
||||||
|
"status": 400,
|
||||||
|
"code": "validation_failed",
|
||||||
|
"detail": "interval_ms must be >= 200",
|
||||||
|
"trace_id": "00-4bf92f...-01",
|
||||||
|
"errors": { "interval_ms": ["must be >= 200"] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `code` 取自共享的 `ErrorCode` 枚举,是客户端做判断的**唯一**依据;`title`/`detail` 只供人阅读,内容可能变化。
|
||||||
|
- `errors` 只在 400 `validation_failed` 中出现,键名是 JSON 字段路径。
|
||||||
|
- 5xx 响应不能泄露异常信息或调用栈,只提供 `trace_id`,用于在日志中定位问题。
|
||||||
|
|
||||||
|
| 状态码 | 使用场景 |
|
||||||
|
| --- | --- |
|
||||||
|
| 400 | 请求格式错误或校验失败 |
|
||||||
|
| 401 | 未认证 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 403 | 已认证但作用域不足 |
|
||||||
|
| 409 | 名称冲突、状态冲突(例如 `probe_offline`) |
|
||||||
|
| 410 | 资源曾经存在但已过保留期(例如 `raw_expired`) |
|
||||||
|
| 412 | `If-Match` 不匹配 |
|
||||||
|
| 413 | 请求体超过上限 |
|
||||||
|
| 422 | 语义上无法满足(例如 `range_too_large`) |
|
||||||
|
| 429 | 超过限流,需要带 `Retry-After` |
|
||||||
|
| 503 | 存储不可用,或正在关闭 |
|
||||||
|
|
||||||
|
### 7.6 集合与查询
|
||||||
|
|
||||||
|
- 分页由服务端驱动:`?limit=`(默认 50,最大 500)和 `?cursor=`(不透明字符串);响应格式为 `{ "items": [...], "next_cursor": string | null }`。排序必须稳定(按 ID 或时间 + ID)。
|
||||||
|
- 时间范围参数统一使用 `from`/`to`,区间为左闭右开 `[from, to)`。缺省时 `to = now`,`from = to − 1h`。
|
||||||
|
- 所有查询都有上限(点数、时间跨度、结果条数);超出时返回 422 并在 `detail` 中给出可行的替代方案,不能悄悄截断。
|
||||||
|
|
||||||
|
### 7.7 并发控制
|
||||||
|
|
||||||
|
- 配置类资源(Agent、Probe、任务)在响应中返回 `ETag`,PATCH/DELETE 支持 `If-Match`,不匹配时返回 412。没有带 `If-Match` 的请求按“最后写入者获胜”处理。
|
||||||
|
|
||||||
|
### 7.8 安全
|
||||||
|
|
||||||
|
- 只提供 HTTPS。认证方式有两种:
|
||||||
|
- 浏览器使用 Cookie 会话:`HttpOnly; Secure; SameSite=Strict`;所有非 GET 请求都必须带 `X-CSRF-Token` 请求头;
|
||||||
|
- 脚本使用 `Authorization: Bearer <api_token>`,作用域为 `read` 或 `admin`。
|
||||||
|
- 凭据不能放在 URL 或查询参数中;SSE 依靠 Cookie 或同源令牌认证。
|
||||||
|
- 登录接口需要限流(例如每个 IP 每分钟 10 次),失败时统一返回 401,不区分“用户不存在”和“密码错误”。
|
||||||
|
- public 层根据站点设置和 `public_visible` 过滤数据:被隐藏的资源对匿名访客一律返回 404,而不是 403。
|
||||||
|
- 令牌明文只在创建或轮换的响应中出现一次;这类响应带 `Cache-Control: no-store`。
|
||||||
|
- 请求体上限为 64 KiB;查询接口有超时(默认 10 秒)。
|
||||||
|
|
||||||
|
### 7.9 实时推送(SSE)
|
||||||
|
|
||||||
|
- 实时数据使用 `text/event-stream`。`event` 名称就是数据类型(`line`、`node`、`point`、`test`);`data` 是单行 JSON,遵守第 7.4 节的数据表示规则。
|
||||||
|
- 每 15 秒发送一行注释作为心跳。重连后服务端先推送当前快照,再推送增量,不补发历史。
|
||||||
|
- 订阅的主题数和连接数都有上限,超出时返回 429 `too_many_streams`。
|
||||||
|
- 推送的内容必须能由对应的 REST 查询重建:SSE 只负责“更快地获得变化”,不承载独有的数据。
|
||||||
|
|
||||||
|
### 7.10 新增端点检查清单
|
||||||
|
|
||||||
|
1. 符合第 7.3 节的路径与方法规则,并有 operationId、summary 和 tag;
|
||||||
|
2. 返回类型精确(`TypedResults`),错误使用 problem+json,错误码来自 `ErrorCode`;
|
||||||
|
3. DTO 符合第 7.4 节的数据表示规则,并注册到 JSON 源生成上下文;
|
||||||
|
4. 列表支持分页,查询有上限;
|
||||||
|
5. 选择正确的层(public / admin / agent),public 层只暴露可以公开的字段;
|
||||||
|
6. 重新生成并提交 `openapi/v1.json`,在 PR 中说明是否属于破坏性变更;
|
||||||
|
7. 测试覆盖:成功路径、校验失败、404、鉴权失败、隐藏资源;
|
||||||
|
8. 同步更新本文上面的端点说明。
|
||||||
+15
-15
@@ -60,7 +60,7 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../../backend/fleet/README.md) |
|
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../../backend/fleet/README.md) |
|
||||||
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](../security/enrollment.md) |
|
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](../security/enrollment.md) |
|
||||||
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。容器节点(Agent、Probe)已实现:CI 对镜像 digest 签名,宿主机上的 `moshiro-update` 验签、按 digest 拉取、重启;纯二进制节点的更新是待办([TODO.md](../../TODO.md)) | [deployment.md](../ops/deployment.md) |
|
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。CI 对镜像 digest 签名;Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 验签、按 digest 拉取、失败回滚;Podman Quadlet 装的 Agent、Probe 更简单:节点有序退出,systemd 用 `--pull always` 重新拉最新镜像,节点自己不下载、不验证清单;纯二进制节点的更新是待办([TODO.md](../../TODO.md)) | [deployment.md](../ops/deployment.md) |
|
||||||
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) |
|
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) |
|
||||||
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) |
|
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) |
|
||||||
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) |
|
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) |
|
||||||
@@ -69,23 +69,23 @@
|
|||||||
| 展示查询 | 任意时间段(相对或绝对);自动步长(每条曲线最多约 720 个点);1s 内存热层(1 小时)加上 10s/1m/5m/1h 各层;累加器现场合并,汇总精确;列式响应;SSE 实时跟随 | [query.md](query.md) |
|
| 展示查询 | 任意时间段(相对或绝对);自动步长(每条曲线最多约 720 个点);1s 内存热层(1 小时)加上 10s/1m/5m/1h 各层;累加器现场合并,汇总精确;列式响应;SSE 实时跟随 | [query.md](query.md) |
|
||||||
| API | `/api/public`(只读,可设为公开)、`/api/admin`(登录)、`/api/agent`(注册与 RPC);实时数据用 SSE | [api.md](../api/api.md) |
|
| API | `/api/public`(只读,可设为公开)、`/api/admin`(登录)、`/api/agent`(注册与 RPC);实时数据用 SSE | [api.md](../api/api.md) |
|
||||||
|
|
||||||
### 4.0 容器节点的更新
|
### 4.0 节点的更新
|
||||||
|
|
||||||
|
发布清单(镜像 digest)由 CI 签名,放进镜像仓库(`<仓库>-manifest` 小镜像的标签里);Backend 定时取、验签后登记(也接受 CI 直接 `POST /api/release/offer`),再原样转发给在线的 Agent / Probe。之后分两条路:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CI ──签名的发布清单(镜像 digest)──▶ 镜像仓库(<仓库>-manifest 小镜像的标签里)
|
Podman Quadlet 节点(Agent、Probe,推荐)
|
||||||
│ │
|
节点比对清单和自己的版本 ──▶ 心跳报「有新版本」──▶ 管理端显示
|
||||||
Backend 定时取、验签后登记(也接受 CI 直接 POST /api/release/offer)
|
自动(默认,运行满 10 分钟)或管理员点「更新」──▶ 节点有序退出
|
||||||
▼ │
|
──▶ systemd 重启单元(Pull=always)──▶ 新镜像、新容器
|
||||||
Backend ──原样转发(offer)──▶ Agent / Probe 进程 │ 宿主机更新器也可以直接读
|
节点不下载、不替换任何东西,也不验证清单;最多被请求一次有序重启
|
||||||
│ 只写进数据目录的 update/ │
|
|
||||||
▼ ▼
|
Backend 和一行脚本装的节点
|
||||||
宿主机 moshiro-update(root):用本机公钥验签 → 核对组件、仓库和 seq → podman pull 仓库@digest → 重启 → 不健康就回滚
|
宿主机 moshiro-update(root):用本机公钥验签 → 核对组件、仓库和 seq → podman pull 仓库@digest → 重启 → 不健康就回滚
|
||||||
│ 状态写进只读的 update-status/
|
──▶ 状态写进只读的 update-status/ ──▶ 节点随心跳上报 ──▶ Backend ──▶ 管理端(有新版本时点「更新」)
|
||||||
▼
|
|
||||||
节点随心跳上报更新状态 ──▶ Backend ──▶ 管理端(有新版本时点「更新」)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
细节见 [deployment.md](../ops/deployment.md)「容器节点的更新」。
|
细节见 [deployment.md](../ops/deployment.md)「Podman(Quadlet)节点的更新」和「宿主机更新器」。
|
||||||
|
|
||||||
### 4.1 回显的四个时间戳
|
### 4.1 回显的四个时间戳
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ Kind 目录编译在二进制中,节点不接受远程下发代码(安全边
|
|||||||
| D13 | Probe/Agent 运行时只依赖 BCL;.NET 10 NativeAOT | 轻量 |
|
| D13 | Probe/Agent 运行时只依赖 BCL;.NET 10 NativeAOT | 轻量 |
|
||||||
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
|
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
|
||||||
| D15 | 注册独立为 `fleet-enroll` 工具;常驻进程不包含注册代码 | 常驻进程去掉 HttpClient,边界更纯 |
|
| D15 | 注册独立为 `fleet-enroll` 工具;常驻进程不包含注册代码 | 常驻进程去掉 HttpClient,边界更纯 |
|
||||||
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。容器节点已实现(宿主机 `moshiro-update`,清单里是镜像 digest);纯二进制节点的更新见 [TODO.md](../../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
|
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。已实现:宿主机 `moshiro-update`(Backend、脚本装的节点,清单里是镜像 digest),Quadlet 节点则靠退出后由 systemd 重新拉镜像;纯二进制节点的更新见 [TODO.md](../../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
|
||||||
| D17 | Probe 支持 OpenWrt:正式支持 `linux-musl-x64`、`linux-musl-arm64`;armv7 视工具链验证结果决定;MIPS 不支持,改为在局域网设备上运行 Probe | 由用户确认;NativeAOT 不支持 MIPS |
|
| 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 上的真实服务冲突;结果会记录实际端口 |
|
| D18 | 默认测量端口不使用 443:控制 7443/tcp,UDP 数据 7443/udp,TCP 数据 7444/tcp;443 只在 Agent 本地显式开启时才允许使用 | 由用户确认;避免与 VPS 上的真实服务冲突;结果会记录实际端口 |
|
||||||
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 由用户确认;5 分钟、15 分钟这类视图足够精细且快 |
|
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 由用户确认;5 分钟、15 分钟这类视图足够精细且快 |
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api-guidelines.md](../api/api-guidelines.md) 中“时间用 RFC 3339”规则的明确例外**,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api.md](../api/api.md) 第 7.4 节“时间用 RFC 3339”规则的明确例外**,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
||||||
- 某个桶没有数据时填 `null`(不填 0),前端据此绘制断线。
|
- 某个桶没有数据时填 `null`(不填 0),前端据此绘制断线。
|
||||||
- `summary` 是**整个时间段**的精确合并结果。`coverage` = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
|
- `summary` 是**整个时间段**的精确合并结果。`coverage` = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
|
||||||
- `gaps` 和 `events` 用于在曲线上做标注(断线、端点变化、时钟跳变等)。
|
- `gaps` 和 `events` 用于在曲线上做标注(断线、端点变化、时钟跳变等)。
|
||||||
|
|||||||
@@ -108,4 +108,4 @@
|
|||||||
|
|
||||||
- 提交信息使用 `<范围>: <摘要>` 格式,范围取值为 `probe`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`。
|
- 提交信息使用 `<范围>: <摘要>` 格式,范围取值为 `probe`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`。
|
||||||
- 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
|
- 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
|
||||||
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api-guidelines.md](../api/api-guidelines.md) 第 2 节判断是否属于破坏性变更。
|
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api.md](../api/api.md) 第 7 节的「版本与兼容性」判断是否属于破坏性变更。
|
||||||
@@ -49,7 +49,7 @@ moshiro/
|
|||||||
└─ MoShiro.Backend.slnx
|
└─ MoShiro.Backend.slnx
|
||||||
```
|
```
|
||||||
|
|
||||||
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。容器节点的更新由宿主机上的 `moshiro-update` 完成,见 [deployment.md](../ops/deployment.md)。
|
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。更新:Quadlet 装的 Agent、Probe 由节点退出后 systemd 重新拉镜像,Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 完成,见 [deployment.md](../ops/deployment.md)。
|
||||||
|
|
||||||
## 3. 依赖关系
|
## 3. 依赖关系
|
||||||
|
|
||||||
|
|||||||
+6
-55
@@ -175,68 +175,19 @@ OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 2
|
|||||||
|
|
||||||
实测常驻内存(RSS,回环,默认模板,稳定后):Linux 上 Probe 约 21 MB、Agent 约 23 MB;Windows 上约 22 MB、25 MB。
|
实测常驻内存(RSS,回环,默认模板,稳定后):Linux 上 Probe 约 21 MB、Agent 约 23 MB;Windows 上约 22 MB、25 MB。
|
||||||
|
|
||||||
### 跨机器的真实链路(Windows Probe → WSL2 里的 Agent 和 Backend)
|
### 故障注入
|
||||||
|
|
||||||
`scripts/smoke-wsl.py` 用 Windows 上的 NativeAOT Probe 连 WSL2 里的 Agent(真实的虚拟网络,两端的单调时钟原点相差约 38 小时),并用 `tc netem` 在 WSL 里注入故障。结果(Probe 的 RTT 基线约 0.4 ms):
|
`scripts/smoke-wsl.py`:Windows 上的 NativeAOT Probe 连 WSL2 里的 Agent 和 Backend(真实的虚拟网络,两端的单调时钟原点不同),用 `tc netem` 注入单向丢包和延迟、8 秒黑洞、Agent / Probe 重启,检查指标落在预期的一侧:上行丢包只出现在 `loss.up`、下行只出现在 `loss.down`;黑洞期间 UDP 连续丢包、TCP 不丢包只在 RTT 里出现几秒的长尾;重启之后几秒内自动恢复。具体数字以脚本的输出为准,不记在这里。
|
||||||
|
|
||||||
| 注入 | 期望 | 实测 |
|
这类验证发现过的几个问题,都已经修掉,改动时要留意它们为什么存在:Probe 重启后序号从 0 开始,Agent 不能沿用上一个会话的记录(否则真正的上行丢失会被算成下行丢失);TCP 回显的几秒中断必须记成延迟而不是丢包(TCP 的行要等回复,最长 30 秒才交出);`hy2_ladder` 的阶梯要把同一阶段的块先合并再算(真实的 Probe 会把一个阶段切成几块)。
|
||||||
| --- | --- | --- |
|
|
||||||
| Agent→Probe 方向 10% 丢包、30 ms ± 5 ms 延迟(egress netem,只匹配 7443/7444) | 丢包在下行,下行抖动变大 | `loss.down` 8.2%、`loss.up` 0;`jitter.down.p95` 8.7 ms、`jitter.up.p95` 0.18 ms;RTT +31 ms;TCP:`tcp.rtt.p99` 1.2 s、`tcp.slow` 28.8%、`tcp.retrans` 5.9%(Agent 一侧内核计数) |
|
|
||||||
| Probe→Agent 方向 10% 丢包、20 ms ± 3 ms 延迟(ingress + ifb) | 丢包在上行 | `loss.up` 9.4%、`loss.down` 0;`jitter.up.p95` 5.2 ms、`jitter.down.p95` 0 |
|
|
||||||
| 8 秒黑洞(tc ingress drop 7443/7444) | UDP 连续丢包 ~80 个;TCP 不丢包,只有几秒的长尾 | UDP 最长连续丢包 80(10 Hz × 8 s);`tcp.rtt.max` 9.5 s、`tcp.stall_max` 9.6 s |
|
|
||||||
| `hy2_ladder`(10/25/50/100 Mbit/s) | 无丢包 | `train.loss` 0.006%、`train.jitter.p95` 0.64 ms、可持续 100 Mbit/s |
|
|
||||||
| 杀掉 Agent / 杀掉 Probe 再启动 | 自动恢复 | Agent 重启后 2.1 s 恢复在线并重新连上 Probe;Probe 重启后 0.5 s 重新连上 |
|
|
||||||
|
|
||||||
验证中发现并修掉的两个问题:
|
### 家宽机器上的 Backend
|
||||||
|
|
||||||
- **Probe 重启后,Agent 沿用了上一个会话的记录**:Probe 重启之后序号从 0 开始,Agent 里同一个序号的旧 t2/t3 还在,于是真正的上行丢失被算成了下行丢失(黑洞测试里 `loss.up` 0、`loss.down` 7.4%)。现在新的 Probe 会话开始时 Agent 清空所有流的记录。
|
没有公网入站的家宽机器上跑 Backend 时,用隧道(Cloudflare Tunnel 之类)把一个公网域名转到 Backend 的端口;Backend 只写一个监听地址(同时写 `0.0.0.0` 和 `[::]` 会报地址已被占用);Probe 的 `fleet.server` 就是这个面板地址。小 VPS 上把 `limits.max_total_mbps` 调低。家宽的 IPv6 前缀会在路由器重新拨号(比如停电)后变化,用 IPv6 直连时地址要跟着换;走隧道的域名不受影响。
|
||||||
- **TCP 回显的几秒中断被记成丢包**:Probe 像 UDP 那样等 2 秒就把没有回复的行交出去,TCP 随后才到的回复没有行可以回填,最严重的长尾反而在 RTT 里看不到(黑洞期间 `tcp.rtt.max` 只有 4 ms)。现在 TCP 的行等回复,30 秒仍然没有才当作连接已断(t4 缺失)。
|
|
||||||
|
|
||||||
没有覆盖的:Windows 上的 Agent(需要给 Windows 防火墙加入站规则)、NAT 重绑定。跨公网的线路见下一节。
|
|
||||||
|
|
||||||
### 跨公网的真实线路(广东移动家宽 → 日本 IIJ 的 VPS)
|
|
||||||
|
|
||||||
2026-10-03 在一条真实线路上试跑过:家宽机器(Debian 13,用户态运行,没有公网 IPv4 入站)跑 Probe 和 Backend,VPS(Debian 12,1 核 470 MB,上面本来就跑着 xray、hysteria2、caddy 等服务)跑 Agent。
|
|
||||||
|
|
||||||
```text
|
|
||||||
浏览器 / 本机前端 ──► https://<公网域名> ──隧道(Cloudflare Tunnel 之类)──► 家宽机器: Backend (:29181)
|
|
||||||
家宽机器: Probe ──WSS 到 https://<公网域名>──► Backend(身份、Agent 列表)
|
|
||||||
家宽机器: Probe ──TCP/UDP 7443、TCP 7444──► VPS: Agent
|
|
||||||
VPS: Agent ──WSS 到 https://<公网域名>──► Backend
|
|
||||||
```
|
|
||||||
|
|
||||||
做法和要点:
|
|
||||||
|
|
||||||
- **Backend 通过隧道暴露**:隧道把一个公网域名转到家宽机器的 `29181`;Backend 监听 `http://0.0.0.0:29181`(只写一个监听地址,同时写 `0.0.0.0` 和 `[::]` 会报地址已被占用),家宽路由器的防火墙不放行这个端口,公网只能经隧道进来。Agent 用 `fleet-enroll --server https://<公网域名>` 注册,之后用 WSS 连回 Backend。
|
|
||||||
- **对外地址自动识别**:Agent 在 hello 里上报本机的公网地址(IPv4 公网地址、IPv6 全球单播地址,排除私网、CGNAT、临时地址),面板自动填 `public_host` 和 `ipv6_host`,不需要手填。经隧道注册时来源地址是隧道出口,面板不会用它。只有 Agent 在 NAT 后面、本机看不到公网地址时才需要在管理端手填;手填的优先,勾选“自动识别”可以撤销。
|
|
||||||
- **Probe 直连面板**:`probe.json` 里的 `fleet.server` 就是面板地址,Probe 用它连面板,Agent 列表由面板下发;密钥由 Probe 注册时自己生成,身份目录(`node.json`、`node.key`、`tls.pfx`)在 `fleet.identity_dir`。
|
|
||||||
- **小 VPS 把 `limits.max_total_mbps` 调低**(试跑时用 100);Agent 常驻约 23 MB,VPS 上原有的服务不受影响,结束后删除 `/opt/…`、停掉进程即可,没有留下任何东西。
|
|
||||||
- 家宽的 IPv6 前缀会在路由器重新拨号(比如停电)后变化,SSH 地址要跟着换;面板走隧道的域名,不受影响。
|
|
||||||
- 本地电脑没有直连 VPS 的路径时,用家宽机器当跳板(`SSH_ASKPASS` + `SSH_ASKPASS_REQUIRE=force`,密码放环境变量,不写进文件)。
|
|
||||||
|
|
||||||
**常驻(开机自启)**:
|
|
||||||
|
|
||||||
- 家宽机器(没有 root 时):`~/.config/systemd/user/moshiro-backend.service` 和 `moshiro-probe.service`,`EnvironmentFile` 指向放配置的 `backend.env`,`Restart=always`,`systemctl --user enable --now …`,再用 `sudo loginctl enable-linger <用户>` 让它们不登录也能起来。Probe 的命令行是 `moshiro-probe --config <probe.json 路径>`(默认路径是 `/etc/moshiro/probe.json`)。
|
|
||||||
- VPS:`/etc/systemd/system/moshiro-agent.service`,`Restart=always`,加 `NoNewPrivileges`、`ProtectSystem=strict`、`ReadWritePaths=/opt/moshiro`、`ProtectHome`、`PrivateTmp`。这是不走安装脚本的手动方式;正式安装用上面的 `packaging/systemd`。
|
|
||||||
- 日志进 journal:`journalctl -u moshiro-agent`、`journalctl --user -u moshiro-probe`。重启 Backend 用 `systemctl` 而不是手动再起一个进程,否则会端口冲突。
|
|
||||||
|
|
||||||
**国家和运营商**:Backend 用 Probe、Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`MoShiro__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Probe、Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。
|
**国家和运营商**:Backend 用 Probe、Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`MoShiro__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Probe、Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。
|
||||||
|
|
||||||
**从别处看这个面板**:本机跑前端,在页面里把数据源指向这个域名,用 API 令牌访问,Backend 设 `MoShiro__CorsOrigins` 放行前端的来源(见 [web/README.md](../../web/README.md))。
|
**从别处看这个面板**:本机跑前端,在页面里把数据源指向面板的地址,用 API 令牌访问,Backend 设 `MoShiro__CorsOrigins` 放行前端的来源(见 [web/README.md](../../web/README.md))。
|
||||||
|
|
||||||
实测(Probe 在广东移动家宽,Agent 在日本 IIJ):
|
|
||||||
|
|
||||||
| 项目 | 结果 |
|
|
||||||
| --- | --- |
|
|
||||||
| 延迟 p50 / p95 / p99 | 107 / 107 / 114 ms |
|
|
||||||
| 丢包 | 上行 0%,下行 8.1%(`hy2_ladder` 在 10、25、50、100 Mbit/s 四档下的丢包都是 8~10%,与速率无关:随机丢包,不是限速) |
|
|
||||||
| 抖动 p95(上 / 下) | 0.9 / 5.2 ms |
|
|
||||||
| TCP | 重传 1.9%(批量传输时 5.8%),RTT 最大 412 ms |
|
|
||||||
| `reality_session` | 建连成功率 100%,TCP 建连 p95 115 ms,TLS 握手 p95 117 ms,30 秒长连接无重置;4 秒批量传输峰值 133 Mbit/s、中位数 90 Mbit/s(Agent 上限 100) |
|
|
||||||
| 评分 | REALITY 82(含按需测试)、Hy2 42 |
|
|
||||||
| Agent 主机状态 | CPU 1.5%,内存 274 / 493 MB,已运行约 20.5 天 |
|
|
||||||
|
|
||||||
这次试跑发现并修掉的问题:`hy2_ladder` 的阶梯按「块」而不是按「阶段」计算,真实的 Probe 把一个阶段切成几块,4 个阶段出了 7 步(模拟数据不切块,所以之前的测试没有发现);现在同一阶段的块先合并。
|
|
||||||
|
|
||||||
## 容器部署(Podman Quadlet 优先,Docker 用 compose)
|
## 容器部署(Podman Quadlet 优先,Docker 用 compose)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
| Probe | Fleet 节点身份(连面板)+ 另一把独立的密钥(连 Agent,放在 `tls.pfx`) | `fleet-enroll`,和 Agent 一样用加入令牌(`role=probe`);可以有多个 Probe,每个独立吊销 | 两把公钥 |
|
| Probe | Fleet 节点身份(连面板)+ 另一把独立的密钥(连 Agent,放在 `tls.pfx`) | `fleet-enroll`,和 Agent 一样用加入令牌(`role=probe`);可以有多个 Probe,每个独立吊销 | 两把公钥 |
|
||||||
| 面板用户 | 账号 + 密码(PBKDF2-SHA256) | 面板 | 密码哈希 |
|
| 面板用户 | 账号 + 密码(PBKDF2-SHA256) | 面板 | 密码哈希 |
|
||||||
| API 令牌 | 随机高熵令牌,带一组能力(见 [access.md](access.md)) | 面板 | SHA-256 |
|
| API 令牌 | 随机高熵令牌,带一组能力(见 [access.md](access.md)) | 面板 | SHA-256 |
|
||||||
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在节点宿主机的更新器里配置公钥(容器节点是 `moshiro-update`,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新是待办),**面板不保存私钥**;登记清单时面板用公钥验签,节点宿主机的更新器之后再验一次 |
|
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在宿主机的更新器里配置公钥(Backend 和脚本装的节点用 `moshiro-update`,见 [deployment.md](../ops/deployment.md);Quadlet 节点不验证清单,只是退出后重新拉镜像;纯二进制节点的更新是待办),**面板不保存私钥**;登记清单时面板用公钥验签,宿主机的更新器之后再验一次 |
|
||||||
|
|
||||||
## 2. Agent 接入流程
|
## 2. Agent 接入流程
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user