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>
6.0 KiB
6.0 KiB
客户端联动契约(CLIENT-CONTRACT)
数据源:本任务(
infra/domains/)产出的签名端点文档与公告文档。 消费方:app 服务层(doc/01 §2 服务层 / §4 客户端断网弹性、doc/06 §5 断网矩阵)。 本文件是客户端与分发面之间的接口约定。改 schema / 验签规则 / 兜底链顺序须双方评审。
1. 文档类型与 schema
两类文档共用同一个签名信封(sign.Envelope),只是 payload 不同。
1.1 信封(envelope)
{
"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
{
"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 的静态镜像版)
{
"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. 验签规则(客户端必须实现)
客户端接受一份文档当且仅当全部通过:
- 签名有效:
key_id命中内置公钥环中的某个公钥,且签名校验通过。 - 防回滚:
version严格大于客户端当前已信任的version(首次为 0)。等于或更小一律拒绝(防降级 / 重放攻击)。 - 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 语义说明。