NAT 打洞:STUN、双端协作与三层中继
专栏:VNT 2.0 组网源码 · 第 6 / 11 篇:::info 学习目标 完成本篇后你能够:解释 Cone/Symmetric NAT 的区别与打洞策略;描述 VNT 双端协作打洞的完整时序;说出三层中继兜底与路由评分公式。 前置:第 3、5 篇完成。预计时长:60 分钟。 :::
:::note 本章术语速查(新手建议先读)
- Cone NAT:同一内网端口对外映射稳定——容易打洞。
- Symmetric NAT:每连一个新目标就换一个公网端口——难打洞,需广撒网。
- 公网映射:NAT 给“内网 IP”分配的“公网 IP”。
- RTT:一个包去和回来的时间(毫秒),衡量链路快慢。
- 丢包率:发出去但没到达的包的比例。 :::
两台设备都在 NAT 后面,谁也连不到谁——这是虚拟组网的核心难题。VNT 的策略是”一切为了直连,中继只是兜底”:持续后台打洞,打通了直连,打不通走三层中继。
第一步:NAT 类型检测
启动后,节点创建 2 个主 UDP + 82 个子 UDP socket + TCP 多路复用(上限 2),然后用 STUN 服务器(默认含 stun.miwifi.com:3478、Google/B 站公共 STUN)检测 NAT 类型:
// vnt-core/src/tunnel_core/p2p/transport/nat_test.rs:161-184(节选)
let (nat_type, public_ips, port_range) =
rust_p2p_core::stun::stun_test_nat(stun_server, default_interface).await...;
nat_info.nat_type = nat_type;
...
let model = match nat_type {
NatType::Cone => Model::Low, // 锥形 NAT:映射端口稳定 → 低强度探测
NatType::Symmetric => Model::High, // 对称 NAT:每目标不同端口 → 高强度探测
};
udp_mgr.switch_model(model)?;
Cone(锥形)NAT:同一内网源端口对外映射稳定——只要对端往映射地址发包就能进。Symmetric(对称)NAT:对不同目标分配不同公网端口——需要向”可能的端口范围”广撒网。检测结果每 30 分钟重测,每个 socket 的公网映射持续维护(未知时 2 秒刷新,稳定后 60 秒)。
第二步:双端协作打洞
打洞不能单方面进行——必须两端同时向对方的公网映射地址发包,让双方的 NAT 都打开”洞”。VNT 每 5 秒一轮(随机挑最多 5 个未直连对端、只挑 dest_ip > src_ip 的一半避免双向浪费),通过服务器交换 NAT 信息后同时开火:
// vnt-core/src/tunnel_core/p2p/transport/punch.rs:127-148(节选)
let punch_info = rust_p2p_core::punch::PunchInfo::new(PunchModel::all(), nat_info.nat_info);
puncher.punch_now(Some(buf), buf, punch_info).await?;
// PunchModel::all() = 向对端已知的所有公网 IP:端口组合 + 端口范围预测发包
失败的打洞进入指数退避:base 3 秒 × 失败次数,上限 1 小时——不放弃但也不骚扰。对端收到 PunchStart1 会检查频率限制(防被打洞洪泛)。
第三步:三层中继兜底
打洞不总是成功(两侧都是 Symmetric NAT 等),VNT 有三层兜底,按优先级:
- 服务器中继:无直连时的默认路径(第 9 篇);
- P2P 邻居帮忙转发:收到非发给自己的包时,只要
ttl≥1且自己有到目的地的路由就就地转发(p2p/inbound.rs 的”帮忙转发”分支)——mesh 网络的自愈能力; - 主动中继探测:每 5 分钟对最多 20 个未直连目标抽样,各向最多 3 个直连对端发
RelayProbe(TTL=2),用应答的 metric(=max_ttl−curr_ttl,跳数)评估路径质量,好路径入路由表。
路由表与评分
多条候选路径并存时按分数选优(p2p/route_table.rs):
score = weight × (10000 - loss) × 100 / (rtt × (10000 + loss × 10))
直连 weight=120,中继 weight=100;每对端最多 3 条路由;10 秒无活动清理
Ping(每 5 秒、P2P ping_all)同时维护 RTT 与丢包率——路由选择是数据驱动的。
与其他模块的联系
- 第 5 篇:PunchReq 是加密包(payload 只有时间戳防重放);STUN 包不加密(协议要求);
- 第 7 篇:打洞成果直接改变数据面的出站选择(直连路由优先);
- 第 9 篇:PunchStart1/2 经服务器中转——服务端的第四个职责。
常见踩坑
- 两侧都是 Symmetric NAT 打洞成功率低——正常现象,RelayProbe 与服务器中继兜底;
- 同一时间反复打洞——检查退避是否被日志证伪(
限制打洞频率是对端限流); - UDP 被运营商 QoS——可试
tunnel_port指定非常用端口,或启用 rtx(QUIC over UDP 也可能被限)。
随堂练习(带验收标准)
- 组网后用
vnt2_ctrl route查看路由表,观察打洞成功前后 metric 与路径的变化; - 用日志统计一次打洞从 PunchStart1 到直连路由建立的耗时;
- 进阶:两侧设备都放在对称 NAT 后(手机热点测试),验证三层兜底的依次接管。