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

20 KiB
Raw Blame History

覆盖网络方案 · 问题逐条推演与解决方案(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 全网)+ 本轮新增的"发布层形态与成本"。