Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/149-覆盖网络顶层架构全貌.md
T
admin e6207aa691
build / build-and-scan (push) Canceled after 0s
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

20 KiB
Raw Blame History

149 · 覆盖网络顶层架构全貌(去中心化形态)

  • 日期:2026-09-21
  • 触发(用户原话):「这不是网状结构还是树形的,参考区块链的去中心化的逻辑 重新规划顶层架构」 |「是不是 没有ROOT,或者多个ROOT(区块链共识机制发送签名),ROOT只给manager发,manager给接入放发送」 |「考虑全面后 画整体图看网络架构 全貌是什么样子」
  • 上游:148(多 Manager 多区域联邦形态)|序③ 四层密钥模型(src/net/relay/identity.ts)|105 §3.2(骨干资格签发)
  • 状态:📋 方案(规划态 · 未实施)。本档零代码 / 零服务器改动;只做「全貌收敛 + 缺口登记」。
  • 与前档的关系:不取代 148,而是修正 148 的两处过强表述,并把「信任图 / 数据图」分离后重画全貌。
  • 同位文档:交接单_一机一钥与信任根_20260917.md|覆盖网络_问题逐条推演与解决方案_20260916.md §A4

TL;DR

  1. 用户的三层结构判断是对的,代码里已经这么做了 —— 根只签签名者、签名者只签节点、节点本地校验。
  2. 「没有 ROOT」不成立,但 ROOT 已弱化到「只在签发那一刻存在」:不下发节点、不参与会话、不在数据路径上。
  3. 「多个 ROOT」已经支持,且是 any-of-N(任一即可)不是 M-of-N(多数共识) —— 见 §二 决定性证据。
  4. 架构全貌 = 两张图分离:信任图(中心化签发)|数据图(网状,无根无中心)。148 把这两张图画混了,是「看着像树」的根因。
  5. 🔴 一个必须先修的真缺口:SignerSet / NodeGrant 只有 issuedAt,没有序号 ⇒ 旧名单可被重放,已撤销的签名者会「复活」。必须在 F2(区域名录)之前修,否则名录本身成为重放载体。
  6. 🔴 一处红线待拍板:148 §7.1 的「骨干资格签发权」在千区下必须改写(本区 Manager 签发 + 根背书),三条候选见 §五。
  7. ✅ 「Manager 可以升级为 ROOT」成立且代码天然支持 —— 根密钥与签名者密钥同形(generateAuthorityKey = generateNodeKey,:517 原文「同形」),角色由公钥被放进哪个清单决定,升级不需换密钥。⇒ N 台 = N 根,解决了「起初没有根」。⚠️ 但 G6 撤根机制是开放升级的前置条件(见 §二·补)。

一、先定性:这是什么形态,不是什么形态

说法 判定
「树形」 ❌ 错。树的特征是「根既签发又是必经路径」。这里根只在签发那一刻存在,运行时不在数据路径上。
「网状」 ⚠️ 半对。数据图是网状;信任图是中心化签发。两张图必须分开说,混着说就必然画成树。
「去中心化」 ✅ 对,但要点在校验权下放:签发可以集中(离线根),校验必须在每个节点自己做(D2)。这才是去中心化的实质。
「区块链式共识」 ❌ 不需要。区块链解决的是「互不信任的参与方就同一份账本达成一致」;这里信任根是用户自己的,不存在需要共识的对立方。引入共识只换来运维复杂度。真正要补的是防重放序号,比共识轻得多且足够。

二、「多个 ROOT」——代码里已经支持(决定性证据)

src/net/relay/identity.ts:492   return envList(env, 'DSHS_OVERLAY_ROOT_PUBKEYS')      ← 复数,逗号分隔多把
src/net/relay/identity.ts:591   roots: readonly string[]
src/net/relay/identity.ts:610   const verdict = verifySignerSet(loaded.doc, loaded.sig, opts.roots)

关键在验签实现(identity.ts:301-314):

function verifySigned(payload: string, sig: unknown, keys: readonly string[]): IdentityReason | 'ok' {
  if (typeof sig !== 'string' || sig.trim() === '') return 'no-signature'
  const sigBuf = Buffer.from(sig.trim(), 'base64')
  if (sigBuf.length !== 64) return 'bad-signature-length'
  if (keys.length === 0) return 'no-trusted-keys'      // 不可验 = 不接受
  const data = Buffer.from(payload, 'utf8')
  for (const spec of keys) {                            // ← 遍历
    const key = publicKeyFrom(spec)
    if (key === undefined) continue
    if (cryptoVerify(null, data, key, sigBuf)) return 'ok'   // ← 命中任意一把即通过
  }
  return 'signature-mismatch'
}

结论:for 循环 + 命中即 return 'ok' ⇒ any-of-N,不是 M-of-N。

面 含义
✅ 好处 根丢失 / 某把根保管出问题 ⇒ 其余根照样恢复。实打实的容灾。
⚠️ 代价 N 把根里坏了一把,那一把单独就能授权一整批签名者,其他根拦不住。这是「恢复优先」而非「共谋防御」的设计取舍。
🔴 后果 多根时每一把根的保管等级必须一致(任一把沦陷 = 全网沦陷)。⛔ 不可「一把自留、几把随意外放」。

SignerSet 绑定网名(identity.ts:81:「签名者只在它被授权的网里有效」,跨网签发 ⇒ network-mismatch)⇒ 满足「ROOT 只给 manager 发」;signNodeGrant 跑在线签名者侧(identity.ts:395 注释「现网 = 47」)⇒ 满足「manager 给接入方发」。


二·补、「Manager 可以升级为 ROOT」——成立,且代码天然支持

用户原话:「还有manager 可以升级为 ROOT」

2.1 决定性前提:根密钥与签名者密钥同形

identity.ts:509   export function generateNodeKey(): { privateKeyPem: string; publicKey: string } { ... }
identity.ts:517   export const generateAuthorityKey = generateNodeKey
identity.ts:517   /** 生成一把 Ed25519 根 / 签名者密钥(**同形**,分开只为读起来不漏"这把是干什么用的")。 */

⇒ generateAuthorityKey 就是 generateNodeKey 的别名,「根」与「签名者」在密码学上没有任何区别。

2.2 由此推出的关键结论

结论 说明
角色不由密钥决定 「是根还是签名者」不取决于密钥怎么生成,而取决于它的公钥被放进哪个清单:进 SignerSet.signers[] ⇒ 它是签名者;进 DSHS_OVERLAY_ROOT_PUBKEYS ⇒ 它是根。
升级 = 改授权关系,⛔ 不需换密钥 华东 Manager 已有签名者密钥(init-signer 生成的 overlay-signer-key.pem)⇒ 升级为根不需要重新生成任何密钥,只需离线根把同一把公钥加入根清单并重新下发。
可降级 反向同样成立:从根清单移除该公钥 ⇒ 退回签名者身份,密钥不变。⚠️ 但移除同样受 G6 约束(见 §五 G6)。

2.3 升级路径(三步)

  1. 华东 Manager 已持有签名者密钥与 .pub 公钥(init-signer 已产出,scripts/overlay-keyring.cjs:142-149)。
  2. 离线根把该公钥追加进 DSHS_OVERLAY_ROOT_PUBKEYS(逗号分隔多值,identity.ts:492),重新下发到各节点 / relay。
  3. 各节点重新装载后,该 Manager 签出的 SignerSet 开始被认;any-of-N 语义下与原有根并列生效。

2.4 这解决了「起初没有根」

信任闭环:第 1 台自己造根、自己签自己 ⇒ 第 2 台造自己的根、两把根互相列入对方清单 ⇒ N 台 = N 根,各根并列入受信清单。 ⇒ 不需要提前存在一个「总根」。联邦可以从任意一台开始自举,根集合随节点数增长而增长。这正是「没有 ROOT」那个直觉的正确落地形态 —— 不是真的没有根,而是没有唯一的根。

2.5 🔴 但代价必须写清楚(与 §二 同源)

风险 说明
🔴 任一根沦陷 = 全网沦陷 any-of-N 下,新加入的一把根与原有根权限完全相同。升级动作降低了全网的整体安全下限到最弱那把根。⇒ 升级必须有准入门槛(谁有资格被升级、保管条件如何核验),⛔ 不能因为「技术上只需加一行」就随意升级。
🔴 升级当前不可逆(G6) 撤根无任何机制(见 §五 G6)⇒ 一旦某台 Manager 升级为根且其密钥泄露,今天没有作废手段,只能逐台改 env 重下发。⇒ G6 是「允许升级」的前置条件:先有撤根机制,才谈得上安全地开放升级。
⚠️ 根集合无收敛机制 多根 + 可升格 ⇒ 根数量会单调增长,而 MAX_ENTRIES = 8 这类上限今天只存在于 directory.ts(地址目录),根清单无对应上限。⇒ 需要为根清单定容量与轮换规则。

⇒ 判定:方向成立、代码天然支持,但「开放升级」必须排在 G6(撤根机制)之后。 在此之前,升级只能是人工、离线、低频的动作。


三、全貌:两张图分离

3.1 信任图(只在「谁能入网」这一刻生效)

层 放哪 用途 边界
根(离线,可多把) 用户手里 / 托管方 / 灾备 只授权·撤销「签名者」 ⛔ 不签节点、⛔ 不加密数据、⛔ 不参与会话
签名者(在线,多把,按区) 各区 Manager(/etc/dshs/overlay-signer-key.pem) 签发本区节点入网凭据 换一把即可,根仍在
节点密钥(每机一把) 每台机器(0600,属主正确) 设备身份;对接时证明握有私钥 该设备重签
会话密钥(内存) 隧道 传输加密 无感

根离线 ⇒ 不影响任何已入网节点运行;只影响「新节点入网」与「换签名者」。

3.2 数据图(运行时常态)

  • 节点之间直接建隧道(打洞优先、relay 兜底);relay 只转发密文,不懂业务。
  • ❌ 无根、❌ 无中心 —— 根不在数据路径上(这是与树的分水岭)。
  • 一台 relay 可同时承载多张网(main.ts:140-141 注释原文:「relay 服务的网是每会话声明的,不是整机一个属性(一台 relay 可以同时承载多张网)」)。
  • 跨网在「能不能拨」这一步就走不到:dialers: Map<network, Set<hostId>> 默认拒绝(registry.ts:5)。结构性隔离,不是配置约定。

3.3 权威归属(三层,判据三句)

权威 管什么 判据句
根 区域名录 · 用户全局唯一名 · 联邦吊销 「全局唯一」的标识只在根
区 Manager 本区归属 · 租约 · 资格 「区域内唯一」的状态只在区 Manager、根 ⛔ 不镜像
区内 Worker/节点 本机运维数据 「跟着用户走」的数据留锚定区、⛔ 不跨区同步

明确不做(写下来防止后人当 bug 修):

  • ⛔ 用户数据跨区同步
  • ⛔ 区域 PG 互联或双向复制
  • ⛔ 根当区域代理(根不转发用户业务流量)
  • ⛔ 区域自取区名 / 自行扩区

四、全貌图(四张,正文见会话)

图 内容
图一 信任图 vs 数据图分离(本档 §三的文字版)
图二 区域层与权威归属(根只管三件事 + 三区并列 + 三条「不做」)
图三 设备入网六步 + 跨区访问路径(标 ✅/⚠️/🔴)+ 四条入口路径 A/B/C/D
图四 (上一轮)多根与多签名者三层拓扑

五、缺口登记(按修复顺序)

G1 🔴 SignerSet / NodeGrant 无序号 ⇒ 旧名单可重放(最高优先)

证据:

identity.ts:136   signerSetPayload = [TAG, version, network, issuedAt, signers.join(',')]   ← issuedAt 参与签名
identity.ts:236-241  parseSignerSet 只检查 issuedAt 是合法 ISO 字符串
identity.ts:458    issuedAt 唯一被读取的位置是 verifyPeerGrant 的「签发时刻容差」判定(只查"太新")
identity.ts:57     version 恒 = IDENTITY_VERSION = 1(文档版本,不是清单版本)

⇒ 一份旧的 SignerSet(已撤销的签名者还在名单里)重放给节点,验签会通过 —— 因为签名本身是真的。

今天为何没爆:signer-set 文件由管理员下发、节点不主动拉取。联邦化之后「各区签名者名单要常态下发」一旦成立,这条路就敞开。

修法(小):SignerSet / RevocationList 加 serial: number;节点持久化「已见最大序号」,≤ 则拒。⛔ 不用 issuedAt 替代 —— 时钟不可信。

时机:🔴 必须在 F2(区域名录)之前。名录本身就是一份要下发的签名清单,会成为重放载体。

🔴 且 G1 是 G6 的前置:撤掉一把根的唯一补救 = 换新 SignerSet,而 SignerSet 无序号 ⇒「换新名单」这条路本身也不可靠。G1 与 G6 必须一起解决。

G2 🔴 区域名录缺失(三方缺口同源)

新机器不知道有哪些区可加入;自建区无法进联邦;区身份无背书载体 —— 三个症状,一个缺口。 实现建议:复用 directory.ts 已验证的机制(签名下发 + 本地缓存 + 长 TTL + 失败关闭),新建 DIRECTORY_PAYLOAD_TAG,独立条目上限。 ⚠️ 与 DSHS_OVERLAY_ROOT_PUBKEYS 必须是两把不同的密钥、两个不同的用途(identity.ts:487-489 明文警告):那把签「地址目录」(可轮换的下发物),这把签「签名者名单」(身份根)。⛔ 不要合并成一个变量 —— 合并等于让「能换地址的人」顺带能加签名者。

G3 🔴 users 无区字段 ⇒ 跨区路由无从判断

schema.ts:112-123 无 host_id / region;username TEXT UNIQUE 只在单库内唯一。 ⇒ 「路由到锚定区」这一步今天无判据。148 §三 G3 已登记。

G4 🔴 四条入口路径中 C / D 无通路

  • A 新服务器首启 ⇒ 首管理员设置页(一次性)—— ⚠️ 今天只有 CLI(cli.ts:203 bootstrapAdmin),无 Web 通路
  • B 设备加入已有区 ⇒ ✅ 机制齐备(邀请码绑定 network + 区签发 + 本地校验)
  • C 自建独立区(不联联邦)⇒ ⚠️ 今天无通路(但不需要任何根,理论上自成一网)
  • D 自建区加入联邦 ⇒ 🔴 今天无通路(需根背书区身份,即 G2)

G5 🔴 骨干资格签发权红线(待拍板,同 148 §7.1)

105 §3.2 原文「骨干资格只能由控制面签发」⇒ 千区下须改写为「本区 Manager 签发 + 根背书」。

  • A 严格照 105:根签发所有骨干资格。优点:与既有红线一致、实现最直接、审计面单一。缺点:根成为千区瓶颈、根离线时全区无法扩骨干、与「几百上千台」目标冲突。
  • B 区签 + 根背书(148 倾向):本区 Manager 签,根只背书区身份。优点:根不在关键路径、可扩展到千区、与去中心化方向一致。缺点:须改写 105 红线、跨区信任传递多一跳、需要 G2 先落地。
  • C 折中:日常区签,骨干级(跨区可见的)仍须根签。优点:风险最小、可渐进。缺点:两套规则并存、判据分叉、长期维护成本高。

🔴 F4 必须在 105 §五-2 之前拍板(148 §八)。


六、推进顺序(承接 148 §八 F1–F7,本次修正)

序 项 相对 148 的修正
F1 区域身份 不变
F1.5 🔴 签名清单序号(G1 + G6) 新增,必须插在 F2 之前;序号机制同时是「作废一把根」的唯一载体
F2 区域名录(G2) 不变,但依赖 F1.5
F3 用户锚点(G3) 不变
F4 权威归属改写(G5 · 待拍板) 不变,🔴 必须在 105 §五-2 之前
F5 跨区级联吊销 依赖 F1.5 的序号机制
F6 跨区可见性 不变
F7 插件版本真源下沉到区域 不变(business_plugins.tgz_path 存 Manager 绝对路径 ⇒ 跨区迁移指向错区)
F8 根集合治理(G7):上限 / 准入 / 轮换 + Manager 升格流程 新增,🔴 依赖 F1.5(G6 撤根机制) —— 撤根机制落地前,升格只允许人工离线低频操作

七、对 148 的修订(本次新增)

148 中以下两句必须改正,原文过强:

148 原文 问题 改为
「根=唯一权威」 混淆了「签发权威」与「校验权威」 「根=唯一的签发权威;校验在每台节点本地(identity.ts:420 D2)」
「根只在登录时被碰一次」 描述的是旧假设,非代码事实 「根只在签发签名者名单时被碰;运行时(含登录)完全不在路径上」

八、验收

  1. DSHS_OVERLAY_ROOT_PUBKEYS 配置多把根,任一把签的 SignerSet 均验通(验证 any-of-N)。
  2. 构造一份旧 SignerSet(含已撤签名者)重放 ⇒ 当前会通过(复现 G1)⇒ 修后必须被拒。
  3. 新机器冷启动:仅凭邀请码 ⇒ 能列出可加入的区(G2 落地后)。
  4. 自建区加入联邦:根背书区身份 ⇒ 跨区互认(G2 + G5 落地后)。
  5. 撤一台设备 ⇒ 仅该设备的凭据失效,不牵动全网换密钥。

九、未验证

  1. G1 重放:未构造真实重放实验(issuedAt 不参与判定是读码推断,非实测)。⚠️ 落地前须实测复现。
  2. DSHS_OVERLAY_ROOT_PUBKEYS 生产实际配了几把 —— 未查(服务器侧)。
  3. overlay-keyring.cjs recover-root 的恢复演练是否在真机跑过 —— 未查。
  4. 🔴 新发现缺口 G6:撤根无机制,且比「撤签名者」更严重。
    • 撤签名者:有机制 —— 根签一份新 SignerSet(identity.ts:103 明文:「撤销签名者不走这里(那是"根签一份新的 SignerSet"的职责)」)。
    • 撤根:无任何机制。RevocationList 只有 hosts[] + nodeKeys[](identity.ts:107-111),不涉及根;revoke 前缀的 IdentityReason 只有 revoked-host / revoked-node-key(identity.ts:127-128)两种。
    • ⇒ 多根形态下,一把根沦陷无法被作废 —— 只能改 DSHS_OVERLAY_ROOT_PUBKEYS 并逐台机器重新下发,且 any-of-N 语义下,被撤那把若仍留在任一节点的 env 里就仍然有效。
    • ⚠️ 且与 G1 重放叠加:撤掉一把根的唯一补救是换 SignerSet,而 SignerSet 无序号 ⇒ 旧 SignerSet 可重放 ⇒ 「换新名单」这条路本身也不可靠。
    • ⇒ G1 与 G6 必须一起解决:序号机制既治重放,也是「作废一把根」的唯一载体。
  5. 区之间是否允许互相签发(peer delegation)—— 未定,见 148 §7.2。
  6. 跨区可见性默认值 —— 倾向默认互不可见,待拍板。
  7. 🔴 G7(本节新增):根集合无容量上限、无轮换规则、无准入标准。 「Manager 可升级为 ROOT」(§二·补)一旦开放,根数量会单调增长;MAX_ENTRIES = 8 只约束 directory.ts 的地址目录,根清单无对应约束。⇒ 需定「根清单上限 / 升级准入条件 / 轮换流程」,且排在 G6 之后。
  8. 「升级为 ROOT」是否要求离线参与(即必须由现有根持有人在离线环境操作)—— 倾向是(sign-signerset 注释原文「⛔ 只在离线跑」),但若开放自助升级则需重新设计,未定。

附 · 自我批评

  • 148 把「信任图」与「数据图」混在一张图里讲 ⇒ 必然画成树。本次分离后才是真实形态。这是本轮最有价值的修正。
  • 上一轮我说「区块链共识不建议引入」是对的,但当时没查 verifySigned 的实现就断言「多根已支持」—— 本次补上了代码证据(any-of-N)。
  • G6(撤根无机制)是本次新发现的,上一轮和 148 都漏了。

档案号:149(.lock-149 原子占用后释放) 关联:148 · 147 · 144 · 序③ · 105 §3.2