NSGW · WireGuard Peer Relay 协议层
本页是 architecture.md §2.2 的协议层附录,描述 NSGW 已实现的不解密 WireGuard 中继:如何在不持有任何密钥的前提下把访问端与发布端的端到端 WireGuard 会话桥接起来,以及它与原生 WireGuard 客户端的兼容矩阵。设计参照 Tailscale Peer Relay,代码位于
nsgw-wg/src/relay.rs。适合的读者:NSGW 实现者、要用原生 WG 客户端接入 NSIO relay 的运维。
1. NSGW 能直接看到的明文字段
Section titled “1. NSGW 能直接看到的明文字段”WireGuard 只有四种报文,布局固定,前几个字节为明文:
| Type | 名称 | 长度 | NSGW 用到的明文字段 |
|---|---|---|---|
| 1 | HandshakeInit | 148 B | sender_index(4..8)+ mac1(116..132) |
| 2 | HandshakeResponse | 92 B | sender_index(4..8)+ receiver_index(8..12) |
| 3 | CookieReply | 64 B | receiver_index(4..8) |
| 4 | TransportData | 变长 | receiver_index(4..8)+ ciphertext |
NSGW 只读 index 与 mac1,报文按字节原样转发,不修改、不重组、不解密。
2. 生命周期:materialize → register → demux → forward
Section titled “2. 生命周期:materialize → register → demux → forward”2.1 配对表(materialize)
Section titled “2.1 配对表(materialize)”NSD 按 pair 投影 gateway_wg_relay(键为目标端公钥),NSGW replace_config 装入配对表并为每个目标预计算
mac1_key[target] = HASH("mac1----" ‖ target.pubkey) # HASH = BLAKE2s同一 generation 的重复投影与已 admitted 身份比较后跳过重建(PLAN-131)。每个 pair 绑定 realm、两端机器身份、peer key、generation、选定网关与当前 NSD 授权;capability 描述端点行为但不替代授权。
2.2 NAT 源学习(register)
Section titled “2.2 NAT 源学习(register)”发布端 NS 从其真实 WG 数据 socket 发送签名的 wg_relay.register_source 信封;NSGW 校验 Ed25519 签名(machine_key_pub pin 到投影身份)后绑定 target.pubkey → 观察到的 NAT 源 (ip:port),TTL 90 s,由发布端周期刷新。没有学到源时回退到投影里的静态 nsn_endpoint,两者都没有则丢弃。
2.3 用 mac1 分流 HandshakeInit(demux)
Section titled “2.3 用 mac1 分流 HandshakeInit(demux)”on init_packet(p, src): for (target, key) in mac1_keys: if MAC(key, p[0..116]) == p[116..132]: dst = learned_source(target) or target.static_endpoint or drop admit(cookie, token_bucket[target]) or drop sessions.install(client_index = u32(p[4..8]), src → dst) # 双向 forward(p, dst); return drop("init_no_mac1_match")复杂度 O(N) BLAKE2s-MAC per Init(N = 配对表目标数);握手每 2 分钟一次,可负担。
2.4 响应配对与转发(forward)
Section titled “2.4 响应配对与转发(forward)”- HandshakeResponse 的
receiver_index等于访问端的sender_index;NSGW 据此把响应配回访问端,并把发布端分配的sender_index安装为反向路由键。同一目标有多个 pending init 时,选择按响应刷新前last_seen最新的那一个,确保重试的 init 与重绑的 NAT 源仍解析到同一个客户端;同一端点后的不同目标保持 fail-closed(BUG-031)。 - CookieReply 与 TransportData 按
(receiver_index, src)单跳查表转发。 - 转发受因果租约约束:会话与签名的转发 epoch 绑定,撤销、替换或切到 direct 后立即失效;quarantined lease 可恢复(PLAN-108 / 110 / 111)。
整体即一个加了 mac1 索引、带租约与准入的 UDP NAT。
2.5 数据流图
Section titled “2.5 数据流图”访问端 NS NSGW :51821 发布端 NS │ Init {sndr=A, mac1=X} │ │ ├─────────────────────────►│ mac1 命中 target.pubkey │ │ │ dst = register_source 学到的源 │ │ ├─────────────────────────────►│ │ │ Response {sndr=B, rcv=A} │ │ │◄─────────────────────────────┤ │ Response │ A→访问端;安装 B→发布端 │ │◄─────────────────────────┤ │ │ Transport {rcv=B} │ │ ├─────────────────────────►│ (B, src) 单跳查表 │ │ ├─────────────────────────────►│ │ │ Transport {rcv=A} │ │◄─────────────────────────┼──────────────────────────────┤3. 准入与 DoS 边界
Section titled “3. 准入与 DoS 边界”| 参数 | 默认 | 含义 |
|---|---|---|
wg_relay_max_sessions | 4096 | 会话总数上限 |
wg_relay_max_pending_inits | 1024 | 未配对 init 上限 |
wg_relay_handshake_rate / burst | 64/s / 128 | per-target 令牌桶 |
wg_relay_cookie_threshold | 16 | 触发 cookie 卸载的负载阈值 |
超限时丢弃并计数;决策见 docs/decisions/2026-06-10-wg-relay-dos-bounds.md。
4. 为什么客户端完全无感
Section titled “4. 为什么客户端完全无感”- Endpoint 与 PublicKey 解耦:peer 的
Endpoint = NSGW:51821只决定初始往哪发,PublicKey仍是对端真实静态公钥;NSGW 不持任何相关密钥,物理上无法解密。 - Roaming Endpoint:mac1 校验通过且解密成功的握手会让两端把对端 endpoint 锁定到 NSGW,后续出包自然走 NSGW。
NS 侧的 wg_relay 指令就是把该 peer 的 endpoint 指向网关 relay 地址,同时保留本地合成的 wss_relay_fallback 以便阶梯降级。
5. 与原生 WireGuard 的兼容矩阵
Section titled “5. 与原生 WireGuard 的兼容矩阵”原生客户端只需把 Endpoint 指向 NSGW relay 端口即可被中继;所有遵循标准报头的实现(Linux 内核 WG、wireguard-go、BoringTun、官方 App、OpenWrt、商用客户端)均兼容。但授权仍来自 NSD:pair 不在配对表、发布端未 register_source、租约失效时一律丢弃。
| 特性 | NSGW 的处理 | 客户端要求 |
|---|---|---|
mac1 | 解析以路由 Init;Response / Transport 透传 | 所有现行实现都支持 |
mac2 / Cookie | 透传;NSGW 自身在高负载下也可要求 cookie | 按协议正常工作 |
| PSK | 透传 | 两端共享即可 |
PersistentKeepalive | 建议 ≤ 25 s | NAT 映射 30~60 s 过期 |
| IPv4 / IPv6 endpoint | 双栈 | 任意 |
| MTU | 透传不重组 | 保持默认 |
6. 与终结模式的分工
Section titled “6. 与终结模式的分工”终结设备(:51820)与 relay socket(:51821)是两个独立端口,不再依赖 mac1 在同端口区分自身与他人。终结、公网边缘、Egress、Exit 才有网关侧报文级 ACL、MSS clamp、按目标计费与应用层观测;relay 模式按设计放弃这些能力,换取端到端加密与存量客户端兼容。
7. 边界情形与失败模式
Section titled “7. 边界情形与失败模式”| 情形 | NSGW 行为 | 客户端表现 |
|---|---|---|
| 目标公钥不在配对表 | mac1 不匹配 → 丢包 + 计数 | 握手超时 |
| 发布端未注册源且无静态端点 | 丢弃 | 握手超时 |
| 长时间空闲,NAT 失效 | 会话与学习源自然过期 | WG 重新握手自动恢复 |
| 访问端漫游 | 旧键失配 → 新一轮 Init | 自动 rekey |
sender_index 碰撞 | (index, src) 复合键 + last_seen 消歧 | 各自正常 |
| 授权撤销 / pair 切到 direct | 租约立即失效 | 该 pair 停止转发 |
| 超过 DoS 边界 | 丢弃 + cookie 卸载 | 重试 |
| NSGW 重启 | 会话丢失 | 下次 retransmit 重建 |
| UDP 被阻断 | 不在 NSGW 侧处理 | NS 阶梯降级到同网关 WSS |
8. 与 NSD 的契约
Section titled “8. 与 NSD 的契约”- 模式在登记时声明(
--transport能力集),由 NSD 为每条 pair 选定并投影WgConfig的 per-peer 指令与gateway_wg_relay;没有”直连失败 → 申请中继”的运行期 API。 - 配对投影的规模与增量:NSD PLAN-125(有界 / 分片 / 增量)与 NSGW PLAN-130(增量应用,草案:delta 序列绑定签名转发 epoch 与完整快照 digest)。
- 同网关 WSS override 由 NSD 的 pair transport 权威投影,NSGW 无需改动。
9. 不在 Peer Relay 范围内
Section titled “9. 不在 Peer Relay 范围内”- NSGW 自己作为 WG 端点参与握手(终结模式)。
- WSS 不透明缝合(另一条并行的 relay 通道)。
- STUN / ICE / 打洞本身(relay socket 只提供反射探测)。
- 应用层策略 / 计费 / 审计(终结、公网边缘、Egress、Exit 承担)。