7d89ec9d91
- domains.md: 四组域名隔离登记 + 冷备池 ≥5 + 启用流程(不含身份信息) - cdn/terraform: Cloudflare 配置即代码(WAF/bot/速率限制/代理DNS/回源鉴权注入)+ 30min 重放 Runbook - server/internal/originauth: 回源鉴权中间件,非 CDN 网段或鉴权头不符一律 403,支持双值轮换 - tools/endpoint-signer: 离线 Ed25519 签名 CLI(端点 + 公告文档,单调版本防回滚,key_id 双公钥轮换) - tools/publish-mirrors: ≥3 镜像发布 + hash 一致性校验 + 故障转移取回 - CLIENT-CONTRACT.md: schema/验签/防回滚/合并/兜底链/channel 客户端契约 - 出站独立出口要求写入部署文档;私钥/token/身份信息一律不入库 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
6.0 KiB
Markdown
116 lines
6.0 KiB
Markdown
# 客户端联动契约(CLIENT-CONTRACT)
|
||
|
||
> 数据源:本任务(`infra/domains/`)产出的签名端点文档与公告文档。
|
||
> 消费方:app 服务层(doc/01 §2 服务层 / §4 客户端断网弹性、doc/06 §5 断网矩阵)。
|
||
> 本文件是客户端与分发面之间的**接口约定**。改 schema / 验签规则 / 兜底链顺序须双方评审。
|
||
|
||
## 1. 文档类型与 schema
|
||
|
||
两类文档共用同一个**签名信封**(`sign.Envelope`),只是 `payload` 不同。
|
||
|
||
### 1.1 信封(envelope)
|
||
|
||
```jsonc
|
||
{
|
||
"version": 3, // uint64,单调递增(防回滚)
|
||
"issued_at": "2026-06-13T00:00:00Z", // RFC3339 UTC,仅供展示/审计
|
||
"key_id": "ed25519-2026q2", // 选择验签公钥
|
||
"payload": { /* 见下 */ },
|
||
"sig": "base64(ed25519 签名)"
|
||
}
|
||
```
|
||
|
||
- 签名覆盖 **去掉 `sig` 后** 的规范化 JSON(键按字典序排序、无多余空白、数组顺序保留)。客户端验签前必须用同样的规范化算法重算签名字节。规范化定义见 `tools/internal/sign`(`Canonicalize`)。
|
||
- 客户端**只内置公钥**,绝不内置私钥(doc/06 §3)。
|
||
|
||
### 1.2 端点文档(`endpoints.v1.json`)payload
|
||
|
||
```jsonc
|
||
{
|
||
"api_domains": ["api-a.example.com", "api-b.example.net"], // 必填,≥1,已去重排序
|
||
"mirror_urls": ["https://.../endpoints.v1.json", "..."], // 必填,≥1,下一份文档的镜像
|
||
"emergency_nodes_hint": ["builtin"], // 可选,指向应急节点参数的不敏感线索(非参数本身)
|
||
"notice": { /* 见 1.3,可选内联公告 */ },
|
||
"channel": "" // 可选,渠道标识(见 §6)
|
||
}
|
||
```
|
||
|
||
### 1.3 公告文档(`notices.v1.json`)payload(/v1/notices 的静态镜像版)
|
||
|
||
```jsonc
|
||
{
|
||
"notices": [
|
||
{
|
||
"id": "2026-q2-mirror-move", // 必填,唯一
|
||
"level": "warning", // info | warning | critical
|
||
"title_zh": "…", "title_en": "…", // 必填(双语)
|
||
"body_zh": "…", "body_en": "…",
|
||
"url": "https://…", // 可选
|
||
"published_at": "2026-06-13T00:00:00Z"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`notice` 既可内联进端点文档(随端点更新一起到达),也可作为独立 `notices.v1.json` 单独发布。两者 schema 相同。
|
||
|
||
## 2. 验签规则(客户端必须实现)
|
||
|
||
客户端接受一份文档**当且仅当**全部通过:
|
||
|
||
1. **签名有效**:`key_id` 命中内置公钥环中的某个公钥,且签名校验通过。
|
||
2. **防回滚**:`version` **严格大于**客户端当前已信任的 `version`(首次为 0)。等于或更小一律拒绝(防降级 / 重放攻击)。
|
||
3. **schema 校验**:端点文档 `api_domains≥1`、`mirror_urls≥1`、域名 / URL 合法;公告 `level` 合法、双语标题与 `published_at` 齐备、`id` 唯一。
|
||
|
||
任一不过 → 丢弃该文档、保留当前已信任版本、记审计。参考实现:`tools/internal/endpoint`(`VerifyDocument`)。
|
||
|
||
## 3. 公钥内置与轮换(双公钥过渡)
|
||
|
||
- 客户端内置一个**公钥环** `key_id → 公钥`(可含多把)。
|
||
- 轮换时(doc/06 §6 每季):先发版让客户端**同时内置新旧两把公钥**;签名侧切到新 `key_id` 出文档;旧 key 在所有存量客户端都已带新公钥后再退役。过渡期内新旧 `key_id` 的文档都能验签。
|
||
- `key_id` 不命中公钥环 → 视为验签失败(拒绝)。
|
||
|
||
## 4. 合并策略
|
||
|
||
验签通过后并入本地端点池:
|
||
|
||
- `api_domains`:与本地池**并集去重**;保留客户端「成功端点置顶记忆」(doc/01 §4)——历史可用端点优先,再追加新域名。
|
||
- `mirror_urls`:替换为文档值(镜像列表以最新签名文档为准)。
|
||
- `emergency_nodes_hint`:覆盖式更新。
|
||
- `notice`:按 `id` 去重入公告位;`critical` 置顶。
|
||
- 整体只有 `version` 更大的文档才会触发合并(见 §2.2)。
|
||
|
||
## 5. 客户端兜底链顺序(断网弹性,doc/01 §4 + doc/06 §5)
|
||
|
||
服务层按下面顺序逐级递降,**每一层的信任锚都在安装包内离线可用**:
|
||
|
||
```
|
||
1. 缓存目录 ——上次成功的节点目录 + 端点池(带 version/时间戳),启动即用
|
||
2. 内置端点池 ——安装包内置 api_domains + IP 直连兜底(按渠道分包,§6)
|
||
3. DoH 多提供方 ——域名解析优先 DoH 轮询,绕本地 DNS 污染
|
||
4. IP 直连兜底 ——DoH 仍失败时用内置 IP 直连
|
||
5. 签名端点更新 ——从 mirror_urls(≥3 镜像)拉新文档,验签(§2)后合并(§4),刷新端点池/域名
|
||
6. 应急节点 ——emergency_nodes_hint 指向的内置应急参数,低速仅够拉新目录
|
||
7. TG 频道 ——最终人工广播渠道,「联系我们」离线可见
|
||
```
|
||
|
||
要点:
|
||
|
||
- 上层失败才降到下层;任一层成功即停止下降并回升。
|
||
- 第 5 层取文档时按 `mirror_urls` 顺序**故障转移**:单镜像失效仍从其余镜像取(与 `tools/publish-mirrors` 的 `Fetch` 行为一致)。
|
||
- 第 1~4、6、7 层都不依赖「正在故障的那个组件」自身恢复。
|
||
|
||
## 6. channel 字段(敏感参数按渠道分包轮换)
|
||
|
||
- `channel` 标识分发渠道(如应用商店 / 官网直装 / 某分发包)。空字符串 = 适用所有渠道。
|
||
- 用途:敏感内置参数(IP 子集、应急节点)按渠道分包轮换,**泄露一个渠道不烧全部**(doc/06 §3 客户端 / doc/01 §4 注意框)。
|
||
- 客户端行为:携带自身 `channel` 标识;只接受 `channel` 为空或与自身匹配的文档(端点合并时按此过滤)。
|
||
|
||
## 7. 域名被墙检测(接口需求,本任务不实现)
|
||
|
||
复用 **#15 探针体系**做域名级 DNS 污染 / TCP 阻断探测(doc/05 §3)。客户端 / 控制面只需消费探测结论触发兜底链与启用流程;本任务只登记接口需求,不实现探针。详见 `domains.md` §4。
|
||
|
||
## 8. 评审
|
||
|
||
本契约需与**客户端任务负责人**评审对齐后冻结。冻结后任何 schema / 验签 / 兜底链顺序变更都要双方签字并升 `version` 语义说明。
|