Files
pangolin/infra/domains/CLIENT-CONTRACT.md
T
wangjia 7d89ec9d91 feat(infra/domains): 域名池 + CDN 前置 + 签名端点分发 (tsk_NU9JuUweHWMt)
- 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>
2026-06-13 14:21:55 +08:00

116 lines
6.0 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.
# 客户端联动契约(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` 语义说明。