规模化联邦方案(去火管化:每群/频道独立主题 + DHT/见证发现 + 真 nm-federation + NAT 直投 + E2E 配对)
现有联邦靠一条 gossip 主题全量广播所有群/频道/私聊/命名,nm-federation 仍是 16 行桩。 目标:按主题订阅——流量与可见性收敛到相关方,可扩展、无单点、为 E2E 预留。 这是一条纯后端 crate 线(crates/nm-federation/nm-gossip/nm-node),惠及桌面+移动,不碰 clients/app 与 clients/mobile,可与移动端并行。 基线见 CURRENT_STATE_ANALYSIS.md。本文为方案稿(评审后动手)。
0. 背景与「火管」三宗罪
当前跨节点收敛(群/频道/私聊/命名)统一走一个联邦级 gossip 主题 nmspace-groups:<fed>,承载 GroupGossip{ Group announce | Gram msg | Gram direct | NameRecord },全量广播给联邦内每个节点,各记录按 origin 去自环、LWW/墓碑合并。它当初是为绕开「跨 NAT s2s 直拨超时」而快速打通的权宜之计,但:
1. 设计目标与原则
- 最小可见性:一条消息只到达它该到的成员/订阅者。
- 可扩展:单节点开销随其实际所在的群/频道数,而非联邦规模。
- 无单点、可自托管:发现可去中心(DHT),弱锚(见证节点)仅做 bootstrap。
- 兼容既有:复用 dial-by-key、GroupGossip 信封、LWW/墓碑、content_hash 去重。
- 平滑迁移:灰度双写、版本协商,不停机切换。
- 为 E2E 预留:主题边界 ↔ 加密群边界(MLS)天然对齐。
2. 架构总览
3. 主题模型(Topic Model)
| 群 | nmspace-group:<gid> | 群成员 | 消息=gossip;成员/元信息=iroh-docs |
| 频道 | nmspace-channel:<cid> | 订阅者 | 同上(频道现为内存态,顺带持久化) |
| 命名 | nmspace-names:<domain> | 关心该域的节点 | NameRecord LWW/墓碑(见 §7) |
| 私聊 | 不上广播主题 | —— | 直投 / 收件箱主题(见 §6) |
- GroupGossip 信封基本复用,但按 topic 隔离;origin 去自环、content_hash 去重照旧。
- 一个节点可同时在很多主题里;由 nm-federation 统一管理其订阅集与生命周期。
4. 发现与引导(Discovery & Bootstrap)
- 症结:iroh-gossip join 一个主题需要已知 bootstrap peer;而非成员根本不在主题里,无从得知第一个 peer。
- 方案 A(DHT / pkarr):把 群公钥 → {topic, 种子 peer 列表, 成员根哈希} 发布到 iroh/pkarr 的 DHT,键=群公钥;新成员查询即得 bootstrap。去中心、无锚。
- 方案 B(见证/锚定节点 witness/anchor):每群指定少量锚定节点(创建者 home + 若干,可轮换),常驻该主题、对外 dial-by-key 可达,充当稳定 bootstrap + 成员目录。弱锚、可多副本。
- 推荐 A+B 混合:锚定节点给稳定冷启动,DHT 做无锚兜底;群公钥不可枚举,避免群被遍历发现。
5. nm-federation 真实化(从桩到模块)
把散落在 nm-node 的 gossip 收敛逻辑收敛进 crates/nm-federation,职责分层:
- 传输/实时:iroh-gossip 每主题一个 GossipTopic——低延迟消息广播。
- 状态/收敛:iroh-docs 每群一个复制文档(成员集、角色、群资料、墓碑),只在成员间同步——强收敛、可回放。
- 订阅生命周期:join/leave/rejoin、announce 心跳、peer 采样与重连、主题 GC。
API 草图:
pub trait Federation {
async fn join_group(&self, gid: GroupId) -> Result<GroupHandle>;
async fn leave_group(&self, gid: GroupId) -> Result<()>;
async fn publish(&self, gid: GroupId, env: GroupGossip) -> Result<()>;
fn subscribe(&self, gid: GroupId) -> impl Stream<Item = GroupGossip>;
async fn members(&self, gid: GroupId) -> Result<MemberSet>; // 来自 iroh-docs
}
注意版本对齐:iroh 1.2 与 iroh-gossip/iroh-docs 0.10x 的兼容是前置工作(分析里已标记)。
6. 私聊与 NAT 直投症结
- 根因:私聊被塞进火管,是因为跨 NAT 的 s2s 直拨当初超时(fed sync failed: s2s request timed out)。去火管的前提是恢复 NAT 健壮的直投。
- 路径2(首选):修好 iroh relay + holepunch + 连接复用;收件人在线→直投,离线→入其 home node 的 redb inbox 补投(home 经 relay 稳定可达)。第一要务是定位当初超时真因:relay url 未配 / ALPN 不符 / 每次新建连接未复用 / dial 目标是节点而非其 home。
- 路径3(兜底):nmspace-inbox:<recipient_pub> 每收件人一主题,仅收件人与其 home join;发件方 publish。避免全广播又不依赖直拨;元数据仅对 join 者可见,配 E2E 缓解。
7. 命名去火管化
- 现:NameRecord 走火管。目标:按域主题 nmspace-names:<domain>,只有关心该域的节点 join;或并入 DHT(domain→node 本就经 nm-domain/pkarr 发现)。
- LWW + 墓碑(空 client_pubkey + 更高 serial)语义不变;home_node 自签名不变。
8. E2E(MLS)配对
- 每群一主题 ↔ 每群一个 MLS 群:预留的 openmls 终于可接。主题传输密文 + MLS 握手(Welcome/Commit);成员增减 ↔ MLS add/remove(与 §5 的成员文档联动)。
- 私聊用 1:1 MLS 或 X25519 封装。去火管 + E2E 一起,才真正消除「明文广播全联邦」。
9. 迁移与兼容(灰度,不停机)
10. 分阶段路线(每阶段可跨网验证)
- F0 · nm-federation 骨架:topic 注册表 + 订阅 API + 单测;内部仍走火管,线上行为不变。
- F1 · 群/频道切 per-topic:成员 join 自己群主题;双写 + 去重;锚定节点 bootstrap。
- F2 · DHT/pkarr 发现:无锚也能 join;群公钥不可枚举。
- F3 · 私聊去火管:定位并修 NAT 直投;inbox 主题兜底。
- F4 · 命名去火管:按域主题 / DHT。
- F5 · E2E + 退役火管:每群 MLS 接入;联邦火管下线。
- 里程碑沿用现有多 seed + 集成测试 + 跨网真机(见 memory seed-deploy-pipeline / federation-delivery-mechanisms)。
11. 风险与开放问题
- 频道是否也要成员鉴权(公开频道 vs 私有频道的主题可见性)?
- 私聊兜底选「直投为主」还是「inbox 主题为主」,取决于 relay 实测结论。
- E2E 先上私聊还是先上群?(群的成员变更+MLS 更复杂)
- 是否顺带把频道做持久化(现为内存态,重启丢)。




