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

6.0 KiB
Raw Blame History

客户端联动契约(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/signCanonicalize)。
  • 客户端只内置公钥,绝不内置私钥(doc/06 §3)。

1.2 端点文档(endpoints.v1.jsonpayload

{
  "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.jsonpayload/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. 验签规则(客户端必须实现)

客户端接受一份文档当且仅当全部通过:

  1. 签名有效key_id 命中内置公钥环中的某个公钥,且签名校验通过。
  2. 防回滚version 严格大于客户端当前已信任的 version(首次为 0)。等于或更小一律拒绝(防降级 / 重放攻击)。
  3. schema 校验:端点文档 api_domains≥1mirror_urls≥1、域名 / URL 合法;公告 level 合法、双语标题与 published_at 齐备、id 唯一。

任一不过 → 丢弃该文档、保留当前已信任版本、记审计。参考实现:tools/internal/endpointVerifyDocument)。

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-mirrorsFetch 行为一致)。
  • 第 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 语义说明。