Files
dsh_shenxian/dsh-server-docs/04-调整方案/116-覆盖网络-问题逐条推演与解决方案.md
T
admin 04776af4b1 docs: 工作区根项目文档批量入仓(04-调整方案 113–128 / 交接单 T09–T21 / ops / archive)
起因:用户 2026-09-17 明确「所有文档都要同步,都放在开发仓库 docs 对应文件夹下」。
判据:工作区根 *.md 中在仓库(git ls-files --quotepath=false)搜不到的那些。

入仓 33 份(一律复制,工作区根原件保留不动,避免引用断链):
- 04-调整方案/113–128(16 份 · 原子占号后落盘):覆盖网络 传输方案取舍 / 应用场景与待完善清单 /
  插件化vs改内核 / 问题逐条推演 / 参数表与观测口径;会合中继拆分取证与改造方案;
  集群化改造方案 Manager-Worker;跨节点迁移与节点自举;项目代码分层范式与迭代风险评估;
  搬运与共享重建方案 guest w47→w106;方案规划方法提炼;文档无效信息审计报告;
  会话接续机制复盘与修复;会话接续规范;dsh 客户端化部署方案;dsh 桌面客户端开发方案
- 交接单/archive/交接单-已完成/T09–T21(13 份 · 覆盖网络线已完成单归档)
- ops/(2 份运行态指针:接续入口 / 接续包 · 覆盖网络线)
- archive/(2 份临时与内部简报)

已排除(无需重复入仓):覆盖网络线 10 份方案正文已入档案 103–112(文件名不同)。

登记:INDEX.md §二 新增 04-113–128 共 16 行 + §四 追加 T09–T21 说明 + 机器摘要行刷新
(⛔ 未跑 docs-index-stats.py --write:该脚本会按 \r\n 归一化全文件行尾,故改为字节级单行替换);
README.md 追加 1 条入仓指针;docs-manifest.json 复跑 scripts/docs-manifest.py 刷新。

验收:工作区根 47 份 .md —— 同名已入仓 8 / 本次内容一致 29 / 已知改名映射 10 / 未入仓 0;
git status 待提交清单只含本次新增与登记 3 件(未涉 src/ 与 relay 代码面)。
2026-09-17 18:24:19 +08:00

275 lines
20 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.
# 覆盖网络方案 · 问题逐条推演与解决方案(2026-09-16)
> **性质**:只读推演稿(调研 + 推演)。⛔ 未改代码、未动服务器、未写文档库(全局执行锁被占用)。
> **方法**:每条按固定骨架 —— **① 问题 ② 查到什么资料 ③ 推演 ④ 结论(能不能解 / 怎么解 / 代价)**。
> **结论先行**:**P0 五条全部可解**,其中 2 条有成熟范式可直接照抄、3 条要自建但路径清晰;**3 条推演缺口全部可补**;**但有 3 处新发现的坑**(见 §D),其中 1 处(地址段冲突)**不提前处理必然出事故**。
---
## A. P0 五条
### A1 · 缺「网(network)」抽象
**① 问题**:骨干被同时当作"平台 Worker 隧道"和"用户设备 P2P"的会合点,两者权限模型完全不同;13 份文档从未出现"网络标识"概念。
**② 资料(Tailscale 范式)**:每个 **tailnet 是独立命名空间**,各自一份 ACL 策略文件(huJSON),结构为 `tagOwners` / `groups` / `acls` / `tests` / `postures`;权限**挂在 tag(角色)上而非 IP 上**;跨网共享用"**只共享单个节点**"(Machines → Share),不下发全网名单。ACL 支持 `tests` 断言(可在 CI 里验证"某来源**不能**到达某目标")。
**③ 推演**:我们需要的不是一张网,而是**三类网并存**:
| 网 | 成员 | 可见性 | 谁会用到 |
|---|---|---|---|
| **运维网** | 我们自己的机器(47 / 106 / 未来的中继与骨干) | 管理员专有;Worker 只对 Manager 可见 | 平台自身(现有 SSH 隧道的位置) |
| **用户网** | **每用户一张网**:该用户全部设备 + 该用户显式授权的他人设备 | 默认只见自己名下设备 | 场景 1/2/3(跨机访问、文件交换) |
| **发布层**(见 A5) | 公网转发节点 | 对外端口/域名,**不是网内成员** | 游戏、服务暴露 |
**关键判断:用户网要"每用户一张网",不要"一张巨网 + ACL"。**
- 理由一:隔离从**策略性**变成**结构性** —— 后者写错一条 ACL 就泄露,前者结构上不可能越界。
- 理由二:ACL 的 `tests` 可以进 CI,把"不能到达"变成可回归的断言。
- 理由三:跨用户协作走"**只共享单节点**",这与项目"权限只准收窄"一致。
**④ 结论**:✅ 可解,**直接照抄 tailnet 范式**。代价 = 控制面租户模型里把**用户 ID 提升为网络标识**(我们已有租户表,这一维是加一列而非重做)。⚠️ **现在只有 1 个用户 ⇒ 结构成本几乎为零,但一旦多人就省下一次大改** —— 所以**要在第一次落地时就分开,不能等**。
---
### A2 · 首次入网引导(bootstrap)
**① 问题**:会合地址从 env 来,但新设备第一次怎么拿到它、首次没有缓存签名目录时怎么办,无人回答。
**② 资料(headscale 范式)**:客户端**只需要知道一个 `server_url`**,其余(DERP 中继地图)全部由控制面下发 —— DERP map 来源支持 `urls`(远程 JSON)与 `paths`(本地 YAML),并有 `auto_update_enabled` + `update_frequency`(默认 3h–24h)定期刷新;DERP 强制 HTTPS/TLS;`region_id` 在 map 内必须唯一;客户端侧有本地缓存。
**③ 推演** —— 三级引导链:
1. **引导种子(内置)**:客户端二进制里写死 **2–3 个 HTTPS 引导地址**(不同地域)。它只回答"第一次问谁"。
2. **签名目录(下发 + 缓存)**:控制面返回经签名的"可用会合点 / 骨干 / 中继"列表。客户端缓存,`update_frequency` 级别刷新。
3. **离线降级**:缓存过期仍可用(只影响新节点加入,不影响已建连接)。
**🔑 推演出的关键设计(资料里没直说,但不做会成灾)**:**引导地址必须能通过已建立的连接在线下发新引导地址**。否则将来换域名/换机器 = 所有客户端必须升级重装。
**④ 结论**:✅ 可解,**很成熟**。代价:一个域名 + TLS 证书 + **N+1(至少 2 个引导点)**;以及"引导地址轮换"这条运维流程要写进方案。
---
### A3 · 地址规划与名字解析
**① 问题**:内部地址段怎么分、怎么避开用户内网、名字谁解析。
**② 资料(含一条对我们的硬警告)**:
- Tailscale 客户端**硬编码**两个段:IPv4 `100.64.0.0/10`(CGNAT 段)+ IPv6 `fd7a:115c:a1e0::/48`;headscale 的 `prefixes` **必须是这两者的子集**,否则"undefined behaviour / break in subtle, hard-to-debug ways"。
- **⚠️ 官方与社区共同警告:「避免与 CGNAT 段(100.64.0.0/10)重叠」** —— 而**中国移动等运营商的大内网正好就是 100.64.0.0/10**(社区文档明确点名"类似于中国移动宽带的 NAT 网段")。
- MagicDNS:`hostname.user.basedomain`;**`base_domain` 必须与 `server_url` 域名不同**以免冲突;`nameservers.split` 做按域分流(split DNS);`override_local_dns` 有开关;实践中不少人建议 `magic_dns: false` + 客户端 `--accept-dns=false`,避免覆盖用户系统 DNS。
**③ 推演 —— 这是本项目最容易踩死的一处**:我们的设备池里有 **200 台 CGNAT + 150 台移动网**(千台推演 §0.1)。这些节点**本机很可能就处在 100.64.0.0/10 内**。若把覆盖网内部地址也分配到该段,会出现:① **宿主路由冲突**(发往覆盖网对端的包被送进运营商网关)② 表现是"部分节点时通时不通",**极难排查**。
**方案(两条硬前提)**:
1. **主寻址走 IPv6 ULA**(`fd00::/8` 内选一段)—— 唯一性有保证、与用户内网几乎不冲突,正好吃满补遗已列的"IPv6 优先"红利。
2. **IPv4 只作兼容层**,且**必须做本地网段冲突检测**:检测到冲突 ⇒ 该地址**自动让路**(回落到 IPv6 或名字寻址),并在客户端明确报错(不能静默)。
**名字解析**:MagicDNS 范式,但三条约束 —— ① `base_domain` 用**子域**(如 `net.<我们的域名>`),与门户域名分开;② **必须 split DNS**(我们的名字走我们的解析器);③ **默认不接管用户系统 DNS**(提供显式开关,默认关)。
**④ 结论**:✅ 可解。**代价 = 冲突检测必须写进客户端首版**(事后加极难,因为要改路由层)。
---
### A4 · 信任根与密钥生命周期
**① 问题**:只写了"一机一钥 + 可吊销",缺根密钥保管/轮换、私钥丢失恢复、设备被盗吊销。
**② 资料(Tailnet Lock 白皮书,几乎是为我们这个问题写的)**:
- 控制面必须分发**节点公钥**,所以"被攻破的控制面可以插入攻击者节点"是这套架构的**固有弱点**。
- Tailnet Lock 的解法:引入 **TLK(Ed25519)签名密钥集合**;**新节点的公钥必须带一个受信任 TLK 的签名**,**每个节点在本地校验**,验不过就不建立会话。
- **TLK 私钥由本地保管,控制面看不到也改不了**;受信任 TLK 集合的变更本身也要签名 + 本地校验;还有 **disablement secret**(关闭机制)。
- 冲突更新用**权重**裁决。
- 配套:`key expiry`(用户节点定期过期 / tagged 节点不过期)、ephemeral 节点超时删除、Noise 私钥丢失 ⇒ 所有客户端重注册。
**③ 推演 —— 四层密钥模型**:
| 层 | 放哪 | 用途 | 丢失后果 |
|---|---|---|---|
| **根(离线)** | 用户手里(纸质恢复码 / 离线设备) | 只用于授权/撤销"签名者" | **最严重** ⇒ 全网重建 |
| **签名者(在线,多把)** | 每台管理员设备一把,受根授权 | 签发节点入网凭据 | 换一把(根仍在) |
| **节点密钥** | 每设备一把(系统密钥库) | 设备身份 | 该设备重签 |
| **会话密钥** | 内存 | 隧道(定期 rekey) | 无感 |
**恢复路径(都不需要控制面参与)**:私钥丢 ⇒ 根密钥重签;设备被盗 ⇒ 用签名者密钥撤销该节点签名。
**④ 结论**:✅ 可解,**照抄 Tailnet Lock 的形状**。代价 = 客户端多一层概念 + **必须做"根密钥恢复演练"**;⚠️ 根密钥必须有 **≥2 份离线副本**,否则根丢失 = 全网重置。
---
### A5 · 外部玩家如何进入游戏服(原报告的"逻辑跳跃")
**① 问题**:S3 算出"服放家宽 ⇒ 600 Mbps 过中继",但玩家是**外部客户端、不是覆盖网络成员**,凭什么走我们的中继?这条路径 13 份文档没定义。
**② 资料(三种形态,业界都很成熟)**:
| 方案 | 玩家要不要装东西 | 延迟 | 暴露面 | 代表 |
|---|---|---|---|---|
| 端口转发 | 不要 | **最低**(原生) | **暴露家宽 IP** ⇒ 被 DDoS / 关联到个人信息 | 传统做法 |
| **内网穿透 / 隧道** | **不要** | +10–50 ms(多一跳) | 中继侧暴露 | **playit.gg**、frp、ngrok |
| 虚拟局域网 | **要(每人装)** | 低(P2P 成功时) | 高(加密私网) | Tailscale / ZeroTier |
- playit.gg 模式:**本地服主动拨出**到服务商 → 服务商给一个公网地址 → **玩家零安装直连该地址**。
- 已知代价(官方/社区共同口径):中继一跳的延迟、**家宽上行决定玩家数(常见 5–10 人上限)**、无 DDoS 防护、免费档限带宽。
- frp 还支持 **XTCP(P2P,流量不过服务器)** —— 即"先经隧道协商、再打洞直连",与我们中继的设计同构。
**③ 推演 —— 三种可能形态,只有一种对**:
| 形态 | 判定 |
|---|---|
| 服在**有公网 IP 的节点** | ✅ **最优**:玩家直连,覆盖网不参与(= S3 的 L1 情形,中继 0) |
| 服在**家宽/CGNAT 后** | ✅ **走"内网穿透"**:服主动拨出到**发布节点**,发布节点对外开地址;**玩家零安装** |
| 玩家**装客户端入网** | ❌ 千台口径下不可行(玩家是海量外部客户端,不是网络成员) |
**🔑 结论:方案缺了一层"发布层 / 服务暴露层"**,而且它有两条硬边界:
1. **发布层不能复用用户贡献的骨干** —— 否则用户机器在替第三方对公网转发流量、且暴露其家宽 IP ⇒ **命中 R5 且是我们不能替用户承诺的事**。
2. **发布层必须与内部中继物理/逻辑分开** —— 一个是对内数据面,一个是对外暴露面,安全加固要求完全不同(DDoS 防护、端口占用、UDP 支持、滥用封禁)。
⇒ **瓶颈性质因此改变**:S3 的"600 Mbps"不是"内部中继容量",而是**对外出口带宽 + DDoS 承压面**。
**④ 结论**:✅ 可解(有现成范式)。代价 = **新增一个独立组件 + 独立的加固与计费**;且这是**权限面扩大**(命中 R5),必须先出权限影响评估。
---
## B. 推演缺口三条
### B1 · agent 预算标定(此前完全没有数值)
**① 问题**:§5 结论写"单房间上限 = min(扇出预算, presence 预算, **agent 预算**)",但 agent 预算从未标定。
**② 资料(业界给的数非常具体)**:
- 通用安全参数:`max_messages_per_minute: 5` · `cooldown_seconds: 10` · `max_consecutive_self_replies: 2` · 每日 API 预算上限。
- **硬执行上限**:单次任务**最大跳数 ≤ 5**(超过转人工);**⛔ 不传原始对话历史**,改传校验过的结构化状态。
- **结构化拒绝**代替自由文本批评(防 ping-pong):评审方只能回 `{approved, errorCode 枚举, 具体修改≤200字}`。
- **⛔ 关键结论:限流必须在基础设施层强制** —— 应用层自限无效(卡死的循环、配置错误、prompt 注入都能绕过),**OWASP LLM Top 10 的 LLM04 就是把"无限制的 agent 循环"列为 top-10 风险**,要求"在 agent 控制之外强制"。
- 还有:令牌桶 + **全抖动**退避 + 熔断器 + 集群级(而非单 agent 级)配额。
**③ 推演 —— 现在可以给出数**:
| 层级 | 建议默认值 | 依据 |
|---|---|---|
| 单 agent | **≤5 条/分钟**、cooldown **10 s**、连续自回复 **≤2** | 业界通用安全参数 |
| 单次触发链 | **≤5 跳**,超限转人工 | 生产实践("3–5 跳解不了,给 10 跳也解不了") |
| 房间 agent 数 | **≤ 房间人数 / 10** | 原方案已有 |
| 强制点 | **服务端网关**(不在 agent prompt 里) | OWASP LLM04 |
**🔑 推演出的新结论(扇出重新算过)**:
1000 人房按 200 个 agent(S5 设定)× 5 条/分钟 = **16.7 条/秒** ⇒ 扇出 1000 ⇒ **16,700 投递/秒**。
- **这与 presence 的 16,700/秒 同量级!** 两者叠加 = **≈33,400 事件/秒**,是任何单场景的**两倍**。
- ⇒ 原结论"单房间上限由 presence + agent 预算决定"**得到验证**,而且现在**两个预算都有数了**。
- ⚠️ **顺带发现一处自相矛盾**:S5 设"1000 人房里聚集 200 个 agent",但硬约束③写"房间 agent ≤ 人数/10"= **100**。**200 这个设定违反了自家约束** —— 按约束应取 100,则 agent 扇出降为 8,350/秒,叠加后 ≈25,000/秒。
**④ 结论**:✅ 缺口可补,**以上数值可直接进方案**。
---
### B2 · 并发叠加(此前逐场景独立推,从未叠加)
**① 问题**:S1–S11 是串行列举,真实最坏情况是多个场景同时发生。
**② 方法推演(资料给的是机制,方法是推出来的)**:叠加推演 = **时间轴重叠检查 + 共享资源争用矩阵 + 主导项法**。
- 业界对应机制:集群级配额(而非 per-agent)、熔断器、重试预算、失败域隔离 —— 这些正是为"叠加"设计的。
**③ 推演**:
**(a) 时间轴重叠检查**
| 场景 | 时间窗 |
|---|---|
| 游戏高峰(攻城战) | 20:00–22:00 |
| 群聊活跃 / 1000 人大房 | 20:00–23:00 |
| **agent 活跃** | 随真人(⇒ 与上两者重叠) |
| **备份窗口** | 02:00 |
| 迁移 | 用户驱动 |
⇒ **游戏 + 群聊 + agent 三者天然重叠**;**备份与游戏高峰错开是既有设计,不是巧合**(这点值得写进方案当作硬约束保留)。
**(b) 共享资源争用矩阵**
| 共享资源 | 谁在抢 | 叠加后量级 |
|---|---|---|
| **发布/中继出口带宽** | 游戏 >> 备份 > 消息 | 游戏 600 Mbps 主导;峰值 1.2–2.0 Gbps |
| 控制面 req/s | 心跳 50/s + 重连突发 1000 | 令牌桶吸收(可忽略) |
| **presence 通道** | presence 16,700/s + agent 16,700/s | **≈33,400/s** ← 真正的叠加瓶颈 |
| 客户端上行 | 备份 + 文件传输 | 低优先级队列 |
**(c) 最坏组合**:**游戏攻城战 + 1000 人大房 + agent 风暴 + 中继故障重连**(四件同时)
⇒ 出口带宽吃满 2.0 Gbps、presence/agent 通道 33,400 事件/秒、同时 160 台重连。
**④ 结论**:✅ 可推,方法就是"**时间轴重叠 + 主导项(差一个数量级可忽略)**"。**重要副产品:叠加后瓶颈排序变了** —— 出口带宽仍是第一,但**第二从"presence"变成"presence × agent 叠加"**。
---
### B3 · 输入参数表(此前散在 6 份文档,不可复算)
**① 问题**:设备占比 / 打洞率 / 每玩家带宽 / 消息频率散落各处,无统一表 ⇒ 推演不可复算、不可仿真。
**② 资料**:libp2p 的连接管理器与拨号默认值、资源管理器上限、中继自荐参数、headscale 的 prefixes/allocation/心跳 —— 都是**可以直接固化的默认值**。
**③ 推演 —— 直接给表**:
| 类别 | 参数 | 建议值 | 来源 |
|---|---|---|---|
| 地址 | 内部 IPv6 | `fd00::/8` 内选一段 | Tailscale ULA 范式(**刻意不用 100.64/10**,见 A3) |
| 地址 | 内部 IPv4(兼容层) | 仅在无冲突时启用 + 冲突检测 | 同上 |
| 连接 | 每对端并发拨号 | **≤4** | libp2p 默认 |
| 连接 | 总并发拨号 | **100**,超时 **30 s** | libp2p 默认 |
| 连接 | 高低水位 / 宽限期 | **100 / 400 / 1 min** | libp2p Connection Manager |
| 中继 | 每节点预约数 | **≤2** | libp2p autoRelay |
| 中继 | 自荐广告延迟 / TTL | **15 min / 30 min** | libp2p HOP relay |
| 心跳 | 保活间隔 | **20–25 s** | 现网既有(落在 NAT 老化安全区) |
| presence | 批合并刷写 | **1 s**;grace 5–15 s;离线 debounce 30 s | Slack 范式 |
| agent | 见 B1 表 | 5/分 · 10 s · ≤2 · ≤5 跳 | 业界通用 |
| 退避 | 公式 | `min(cap, base×2^n)` + **全抖动** | AWS 架构框架 |
| 传输 | 同时上传对象上限 | **4**,30 s 随机试新对端 | BitTorrent |
| 发布层 | 对外端口 | 见 A5,**待用户拍板**(涉及花钱与暴露面) | — |
**④ 结论**:✅ **这一步是纯手工活,没有任何阻塞**,应立刻做(它是"从推演到仿真"的前置)。
---
## C. P1 九条的处置建议(合并给出)
| # | 问题 | 建议 | 阻塞? |
|---|---|---|---|
| 1 | 30 条未验证项未成取证计划 | 收敛成一份「取证清单」:**最该先测三项** = 中继 jitter / 打洞率 / 真实带宽 | 无 |
| 2 | 限流参数表空 | **= B3 的表,已给出** | 无 |
| 3 | 观测最小集缺失 | 采 5 项:路径类型 · 打洞率 · jitter · 重连次数 · 中继利用率;告警阈值留待实测后标定 | 无 |
| 4 | **权限面评估缺失(R5)** | 三处扩权:**虚拟网卡驱动(需管理员)** / 骨干开端口 / 发布层对外暴露 —— **必须出「权限影响评估」** | ⚠️ 红线 |
| 5 | 成本模型缺失 | = 发布层 + 会合 + 中继的机器与带宽;**属花钱项,需用户拍板** | ⚠️ 边界外 |
| 6 | 滥用与事件处置 | 复制现成范式:封禁节点签名 + 撤资格 + 流量审计;发布层要单独的反滥用 | 无 |
| 7 | 卸载与退出 | 卸载清单:虚拟网卡 / 路由表 / DNS / 常驻进程 / 缓存凭据 | 无 |
| 8 | 协议选型未收敛 | **建议结论**:先沿用 SSH→多对端隧道(S0–S4 已定),**传输层选 WireGuard 用户态**,打洞选 **STUN + 同时发包(DCUtR 式)**,发布层用 **frp 式(含 XTCP P2P 回退)** | 无(技术选型自决) |
| 9 | 最小可用规模路径缺失 | 见 §D 的 3–5 台清单 | 无 |
---
## D. 新发现的 3 处坑(本次推演独有)
1. 🔴 **地址段冲突(最严重)** —— 我们的节点池里有 200 台 CGNAT + 150 台移动网,**本机很可能就在 100.64.0.0/10 内**;若覆盖网也用这段,会出现路由黑洞且**极难排查**。⇒ **主寻址必须走 IPv6 ULA,IPv4 冲突检测必须进首版**。(业界共识警告 + 我们的设备画像,两者叠加出来的一条。)
2. 🟠 **游戏瓶颈看错层** —— S3 的 600 Mbps 不是"内部中继容量",而是**对外出口带宽 + DDoS 承压面**;且**不能复用用户贡献的骨干**(命中 R5)。
3. 🟡 **S5 自相矛盾** —— "1000 人房 200 个 agent" 与自家约束"房间 agent ≤ 人数/10(=100)"冲突 ⇒ 按约束取 100。
---
## E. 汇总结论
| 项 | 能否解 | 关键前提 |
|---|---|---|
| A1 网抽象 | ✅ 照抄 tailnet | **第一次落地就要分层,不能等** |
| A2 引导 | ✅ 成熟 | 引导地址要能在线轮换 |
| A3 地址与 DNS | ✅ 可解 | **IPv6 ULA 优先 + 冲突检测** |
| A4 信任根 | ✅ 照抄 Tailnet Lock | 根密钥 ≥2 份离线副本 + 恢复演练 |
| A5 外部玩家 | ✅ 有范式 | **新增发布层组件 + 走 R5 评估** |
| B1 agent 预算 | ✅ 数值可直接用 | 在**服务端网关**强制 |
| B2 并发叠加 | ✅ 方法已定 | 叠加后**第二瓶颈换人** |
| B3 参数表 | ✅ 纯手工活 | **无阻塞,应立刻做** |
**⇒ 整个方案没有"解不了"的问题;真正卡住的只有两件**:① **权限影响评估(R5 红线)** ② **成本承诺(花钱,属边界外需用户拍板)**。
---
## F. 本次未做
- ⛔ 未改代码、未动 47 / 106、未写文档库(全局执行锁被 `修复轮-决策方法-2b` 占用)。
- 📌 沿用上轮口径:**未新增上抛项**,待拍板仍是骨干服务范围(A 自用 / B 全网)+ 本轮新增的"发布层形态与成本"。