回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复: - 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录) - tmp/(32.4 M,按接续棒命名的过程临时区) - .workbuddy/tmp/(39.5 M) - 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物) - tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留 入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与 接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、 .workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。 排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、 打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
20 KiB
覆盖网络方案 · 问题逐条推演与解决方案(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 内必须唯一;客户端侧有本地缓存。
③ 推演 —— 三级引导链:
- 引导种子(内置):客户端二进制里写死 2–3 个 HTTPS 引导地址(不同地域)。它只回答"第一次问谁"。
- 签名目录(下发 + 缓存):控制面返回经签名的"可用会合点 / 骨干 / 中继"列表。客户端缓存,
update_frequency级别刷新。 - 离线降级:缓存过期仍可用(只影响新节点加入,不影响已建连接)。
🔑 推演出的关键设计(资料里没直说,但不做会成灾):引导地址必须能通过已建立的连接在线下发新引导地址。否则将来换域名/换机器 = 所有客户端必须升级重装。
④ 结论:✅ 可解,很成熟。代价:一个域名 + TLS 证书 + N+1(至少 2 个引导点);以及"引导地址轮换"这条运维流程要写进方案。
A3 · 地址规划与名字解析
① 问题:内部地址段怎么分、怎么避开用户内网、名字谁解析。
② 资料(含一条对我们的硬警告):
- Tailscale 客户端硬编码两个段:IPv4
100.64.0.0/10(CGNAT 段)+ IPv6fd7a: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 内。若把覆盖网内部地址也分配到该段,会出现:① 宿主路由冲突(发往覆盖网对端的包被送进运营商网关)② 表现是"部分节点时通时不通",极难排查。
方案(两条硬前提):
- 主寻址走 IPv6 ULA(
fd00::/8内选一段)—— 唯一性有保证、与用户内网几乎不冲突,正好吃满补遗已列的"IPv6 优先"红利。 - 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 后 | ✅ 走"内网穿透":服主动拨出到发布节点,发布节点对外开地址;玩家零安装 |
| 玩家装客户端入网 | ❌ 千台口径下不可行(玩家是海量外部客户端,不是网络成员) |
🔑 结论:方案缺了一层"发布层 / 服务暴露层",而且它有两条硬边界:
- 发布层不能复用用户贡献的骨干 —— 否则用户机器在替第三方对公网转发流量、且暴露其家宽 IP ⇒ 命中 R5 且是我们不能替用户承诺的事。
- 发布层必须与内部中继物理/逻辑分开 —— 一个是对内数据面,一个是对外暴露面,安全加固要求完全不同(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 处坑(本次推演独有)
- 🔴 地址段冲突(最严重) —— 我们的节点池里有 200 台 CGNAT + 150 台移动网,本机很可能就在 100.64.0.0/10 内;若覆盖网也用这段,会出现路由黑洞且极难排查。⇒ 主寻址必须走 IPv6 ULA,IPv4 冲突检测必须进首版。(业界共识警告 + 我们的设备画像,两者叠加出来的一条。)
- 🟠 游戏瓶颈看错层 —— S3 的 600 Mbps 不是"内部中继容量",而是对外出口带宽 + DDoS 承压面;且不能复用用户贡献的骨干(命中 R5)。
- 🟡 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 全网)+ 本轮新增的"发布层形态与成本"。