tailscale nat穿透流程及关键原理
下面对 Tailscale 如何做 NAT 穿透(包括直连建立、保持与回退)的关键原理拆开讲。
1)总体线路:三条路,优先直连
- 优先直连(UDP over Internet):两端先尝试点对点直连,成功后所有数据都直接走这条路(性能最好、时延最低)。(tailscale.com)
- 协商/引导与兜底(DERP):在直连之前与失败时,先经就近 DERP 中继建立“控制/引导通道”,用它交换候选地址、发起“请你打洞”等信号;若直连始终失败,则走 DERP 作为加密转发 的备胎。(tailscale.com)
- 更细的穿透协议(DISCO + STUN):Tailscale 基于 WireGuard 自研了“DISCO”发现/心跳层,配合 STUN 获取公网映射与 NAT 类型,驱动 UDP 打洞。(tailscale.com)
2)打洞的关键“料”:同一 UDP socket、STUN、候选地址、互打
- 同一 UDP socket:获取公网映射必须用和后续真正传数的同一个 UDP 套接字来发 STUN/接回包,否则你测到的
公网IP:端口
跟实际传输不同,洞就白打了。这是很多实现失败的根源。(tailscale.com) - STUN 获取外网映射与 NAT 性质:客户端向 STUN 发请求,得到“我在外面的
IP:port
是啥”,也能侧面推断 NAT 类型(如对称型更难打)。(tailscale.com) - 交换候选地址:双方把自己当前可用的候选 端点(endpoints)(含 IPv4/IPv6、多个
IP:port
映射)通过控制面/DERP 互换。(tailscale.com) - 双向同时发起(UDP hole punching):A、B 对彼此的候选
IP:port
同步“互打”若干小包(包含 DISCO 探测),在两端 NAT 上同时打开出站映射,让对方进入我的 NAT 表,从而“外部包”也能被 NAT 放行给我。(tailscale.com)
3)DISCO 层干了什么?(比 WireGuard 更懂 NAT 的“前戏”)
- DISCO ping / 心跳:
tailscale ping
默认发的是 DISCO 探测,不是 ICMP;它能告诉你“目前是经 DERP 还是直连哪个IP:port
”。(tailscale.com) - CallMeMaybe:当 A 已经对 B 的候选端点开火时,会经 DERP 送一个 CallMeMaybe 给 B,意思是“我在打你的这些端口了,你也回打一发,把回程洞打开”。(Go Packages)
- (新版)CallMeMaybeVia / UDP relay endpoint:还有一种 Via 形式,提示通过中继候选来协助开路(对“UDP 被限”网络更友好)。(Go Packages)
- 选择 & 优先级:如果已经有可靠直连,端上可以不再开启新的路径;直连优先于“经中继的路径”。(Go Packages)
4)魔法插座:magicsock 把所有这些都“接”在一起
- magicsock 是 Tailscale 在用户态的“魔法 UDP 套接字”,同一个 socket 同时承载 WireGuard 数据、STUN、DISCO,以及穿透时需要的各种小包和心跳,从而保证“测到的映射=用来传数的映射”。(Go Packages)
- 这也是为何 Tailscale 不直接用内核态 WireGuard:因为 NAT 穿透的许多动作必须跟数据在同一个用户态 socket里做,方便灵活控制与观测重绑定(rebinding)、心跳、计时器等。(Reddit)
5)失败与回退:DERP 作为发现通道 + 永久兜底
- 发现阶段:最初几包通常先走最近的 DERP,用于互递 DISCO / 候选地址;一旦直连打通,就切换到直连通道。(tailscale.com)
- 兜底阶段:若遇到 对称 NAT + 防火墙 等强限制,直连打不通,就全流量走 DERP(加密、稳定,但带宽/时延逊于直连)。(iam.bitbeats.io)
- 现实世界并不完美:官方也承认 NAT 穿透是“长期拉锯战”,需要持续遥测与改进。(tailscale.com)
6)连通保持与漫游(rebind)处理
- 保活心跳:穿透成功后,DISCO 会以低频心跳维持 NAT 映射;社区里称过“silent disco”等优化,尽量减少无谓心跳。(GitHub)
- NAT 重绑定:当一端公网
IP:port
变化(例如路由器重启/蜂窝到 Wi-Fi 切换),magicsock 会通过 DISCO/STUN 迅速探测到并重建可用路径。原理还是“同一 socket 上继续打洞 + 候选更新”。(Go Packages)
7)IPv6 情况
- 双栈最优:若两端都有可达 IPv6,通常不需要穿透就能直连(无 NAT、路径更稳定);若一端仅 IPv4-NAT,仍按上面流程打洞或回退 DERP。(tailscale.com)
8)你能在现场怎么“看懂它在做什么”
- 看当前连接类型与路径
1 | tailscale status --peers |
这些命令能直接看到“是否直连、走哪个 IP:port
、DERP 节点名”等。(tailscale.com)
- 网络自检
1 | tailscale netcheck |
用于判断 UDP 是否可出、就近 DERP、是否存在对称 NAT 等(输出因版本而异)。文档对“连接类型”、“DERP 角色”有解释。(tailscale.com)
9)把整个流程串起来(顺序图式说明)
- 两端启动 → 通过控制面注册/换公钥,并各自连上就近 DERP。(tailscale.com)
- STUN 获取自己的公网
IP:port
与 NAT 线索;把“候选端点”发给对端(经控制面/DERP)。(tailscale.com) - A/B 同时对彼此候选端点发 DISCO 探测(互打)→ NAT 表里建立出站/入站映射。必要时 A 还会经 DERP 发 CallMeMaybe 让 B 主动回打。(Go Packages)
- 任一条直连能收发 DISCO/数据包 → 选定最佳直连路径并切换数据面到直连。(tailscale.com)
- 若所有直连失败 → 全流量走 DERP,同时周期性再尝试直连以便条件改变时“脱离中继”。(iam.bitbeats.io)
进一步阅读(权威/源码级)
- 官方长文:How NAT traversal works(强烈推荐通读,解释了为何“必须同一个 UDP socket”)。(tailscale.com)
- DERP 服务器与角色(发现与兜底)。(tailscale.com)
- DISCO 协议与
CallMeMaybe
/CallMeMaybeVia
类型(Go 包文档)。(Go Packages) - magicsock 源码包(用户态“魔法 socket”,NAT 打洞的执行场)。(Go Packages)
- Ping 类型说明(DISCO/TSMP/ICMP 的区别与用途)。(tailscale.com)