·约 11 分钟阅读·DeepSeek

NAT 穿透到底是怎么工作的:STUN、TURN、UDP 打洞,以及它为什么会失败

一篇讲清机制的 NAT 穿透科普:NAT 为什么会破坏点对点连接、四种 NAT 分类以及为什么对称型 NAT 是硬骨头、STUN 如何发现你的反射地址、UDP 打洞的分步过程、TURN 作为中继兜底、ICE 作为统一框架,以及 CGNAT、端口随机化、QUIC 和基于 WireGuard 的 overlay 在今天改变了什么。

networkingself-hostingsecurity

两台机器想要直连。可它们都没有公网地址。一台躲在家用路由器后面,另一台藏在运营商后面。这才是 互联网的常态,也正是点对点网络如果不借助外力就活不下去的状态。NAT 穿透就是在这种情况下硬 挤出一条直连路径的一整套技巧——而它为什么大多数时候能成、又为什么会在某一类可预测的场合失败, 归根结底取决于中间那台 NAT 是怎么分配映射的。

这是一篇讲机制的拆解。不吹不黑。我会讲清楚:NAT 为什么破坏点对点、经典的 NAT 分类以及为什么 对称型 NAT 是那块硬骨头、STUN 如何发现你的反射地址、UDP 打洞的分步过程、TURN 作为中继兜底、ICE 作为把它们串起来的框架,以及 2026 年都变了什么:CGNAT、端口随机化、 QUIC,以及基于 WireGuard 的 overlay。

NAT 为什么会破坏点对点

NAT 之所以存在,一是因为 IPv4 地址用光了,二是因为家庭和企业网络希望只有一个出口来做策略和状态 管理。它做了两件在这里至关重要的事。

第一,它隐藏地址。你的笔记本是 192.168.1.20,而互联网只看到一台机器 203.0.113.7。对端 没法去拨 192.168.1.20,因为这个地址出了你的局域网就毫无意义,并且在另外几百万个局域网里也 存在。你没法宣告一个自己并不拥有的可达地址。

第二,它默认是有状态且单向的。NAT 只有在握有一条”内部 (ip, port) 映射到外部 (ip, port)”的表项时,才知道该怎么翻译一个包。而这条表项是你这一侧先发流量出去时才会创建的—— 出站创建状态,入站需要状态。所以从互联网不请自来的 SYN 或 UDP 报文会被丢弃:根本没有映射可以拿来 翻译它。正是这种不对称,让”监听”不可能,而”主动拨号”可行。

两件事合在一起:两台藏在 NAT 后面的主机都能主动拨向公网,但谁都无法接受拨入。如果双方都有公网 地址,P2P 会易如反掌;而 NAT 恰好把 P2P 需要的那个属性给拿掉了。无论是发现问题(“我该把自己的 什么地址告诉对端?”),还是可达性问题(“包真的能到吗?”),都是 NAT 造成的。

NAT 的分类,以及为什么对称型 NAT 是硬骨头

民间的分类法各不相同,但经典的四类模型(来自 RFC 3489 时代的 STUN)在预判打洞能否成功这一点上 相当好用。它们之间的差别在于:NAT 对谁可以向已有映射发入站有多宽容,以及它如何挑选外部端口。

类型出站映射谁能向该映射发入站
全锥型(Full-cone)每个内部 (ip, port) 固定一个外部端口任何人
受限锥型(Restricted cone)每个内部 (ip, port) 固定一个外部端口只有内部侧曾发过包的主机,端口不限
端口受限锥型(Port-restricted cone)每个内部 (ip, port) 固定一个外部端口只有内部侧曾发过的那个精确 (ip, port)
对称型(Symmetric)每个目的地址分配一个新的外部端口只有那一个目的 (ip, port)

前三种都是”锥型” NAT:无论目的是谁,外部端口都是内部端点的一个稳定函数。这种稳定性正是打洞能 成功的全部理由——你可以预测这个映射,并把它告诉对端。

对称型 NAT 把这一点打破了。它会针对你通信的每一个目的地址分配一个不同的外部端口。 你想预测对端的端口,一定会猜错,因为 NAT 早就另选了一个。更糟的是,探测这件事本身——你往对端 地址发包——所创建的,恰恰就是对端猜不到的那条映射。对称型 NAT 通常出现在企业设备、部分移动 运营商那里,如今在 CGNAT 后面也越来越常见。它把入站限制在你最近一次发过包的单个 (ip, port) 上,是表格里最严格的一行,也是击溃朴素打洞的那一行。

有一个值得内化的细节:映射行为与过滤行为是相互独立的。一台 NAT 可以采用端点无关的映射 (锥型式、端口稳定),却使用依赖地址的过滤。RFC 4787 的现代术语把这两个维度分开描述。对穿透 而言,映射维度通常决定成败;过滤维度决定第一个探测包能不能过去。

STUN:发现你的反射地址

在你能告诉别人”往这里发包”之前,你得先知道 NAT 分给你的公网 (ip, port)。这正是 STUN (Session Traversal Utilities for NAT,NAT 会话穿透工具)要做的事。STUN 客户端往一台公网 STUN 服务器发一个 Binding Request;服务器查看这个包到达时的源地址,再通过 Binding Response 把它反射回来。这个被观察到的地址就是你的**服务器反射(server-reflexive)**候选地址。

客户端                    NAT (203.0.113.7)         STUN 服务器
  |                             |                        |
  |-- Binding Request -------------------------------->  |
  |   (源 192.168.1.20:54321)   |                        |
  |                             | 分配                   |
  |                             | 203.0.113.7:62000      |
  |                             |                        |
  |  <---------------------- Binding Response ---------  |
  |   XOR-MAPPED-ADDRESS = 203.0.113.7:62000             |

STUN 便宜(几个 UDP 往返),服务器端无状态。它不是中继,也完全不认识你的对端。有两点现实很关键:

  • 多问几台服务器。 如果两台 STUN 服务器对同一个本地 socket 返回了不同的外部端口,那你就在 对称型 NAT(或按服务器做映射的 NAT)后面。这一次比较,是你手头最廉价的对称型 NAT 检测手段。
  • STUN 对过滤行为一无所知。 知道自己的外部端口,并不意味着对端发往它的包会被放行;那取决于 过滤维度。这件事只能靠试着发来验证。

如果关于 STUN 你只记得一件事:它只回答”我从外面看是什么样”,从不回答”我能不能被连上”。

UDP 打洞,分步拆解

打洞是一种协调技巧。洞察在于:虽然两端都无法接受不请自来的入站包,但双方都能创建那条让对端 进来的映射——只要顺序对。UDP 是首选介质,因为它没有连接握手,所打的映射只要 NAT 的 UDP 定时器 允许就能一直留着,而且不会有谁在收到意外首包时甩出一个 RST。

唯一的前提是一台汇合服务器(rendezvous server):一台有公网地址、两端都够得着、且两端都 愿意把地址交给它的第三方主机。在 ICE 里它就是信令通道;在 mesh VPN 里它就是锚点节点。

Alice           汇合服务器(公网)          Bob
  |                   |                    |
  | 1. 注册            |                    |
  |------------------>|                    |
  |                   |<------ 注册 -------|
  |                   |                    |
  | 2. "谁在这个房间?" |                    |
  |------------------>|                    |
  |<-- Bob=198.51.100.9:40000 ------------>
  |                   |--- Alice=203.0.113.7:62000 -->
  |                   |                    |
  | 3. 打洞:往 Bob 的地址发 UDP            |
  |---------------------------------------->  (NAT-A 打开一条朝向
  |                   |                      Bob 的映射;可能先被
  |                   |                      B 的过滤丢掉)
  | 4. 打洞:往 Alice 的地址发 UDP          |
  |<----------------------------------------  (NAT-B 打开一条朝向
  |                   |                      Alice 的映射)
  | 5. 现在两条映射都已在位:               |
  |<================= 直连 UDP ============>|

顺序很重要。第 3 步的包可能被 Bob 的过滤丢掉,但关键在于:它为 Alice 创建了一条朝向 Bob 地址的 出站映射。当 Bob 第 4 步的包到达时,那条映射已经存在,并且是被允许的(对受限型 NAT 而言,因为 Alice 确实发往了 Bob 的精确地址)。对称地,Bob 的包为他自己创建了出站映射,于是 Alice 重传 的那个包现在就能过去了。所以第 5 步通常需要一次重传:每个方向上的第一个洞负责开门,第二个包才 真正走进门里。

# 对每个对端,一旦地址通过汇合服务器交换完毕:
for attempt in 1..N:
    sendto(bob_addr, "PUNCH")     # 持续打开/刷新我这边的映射
    recv()                        # 收到 Bob 的 PUNCH 就说明洞通了
    if peer_seen: break
    sleep(backoff)                # 重传是必需项,不是可选项
if not peer_seen:
    回落到中继                     # 见下文 TURN

两个运维层面的提醒。第一,打洞本质上是一个时序和保活问题:NAT 的 UDP 映射会过期(空闲时 常在 30–120 秒,CGNAT 上有时更短),所以活着的路径需要周期性流量,否则会悄悄失效。第二,一旦 路径打通,就应该切到一个加密的短保活包——这正是真实协议不会”打一次就完事”的原因。

TURN:你付费买来的中继

当打洞失败时——任意一侧是对称型 NAT、防火墙全部丢弃、或者两端就是没法就某条路径达成一致——剩下 的唯一选择就是别再想着直连。TURN(Traversal Using Relays around NAT)在一台公网服务器上 分配一个中继地址(relay address)。两端都把流量发给中继,由中继转发。从每台 NAT 的视角看, 这不过是普通的、发往一台公网服务器的出站流量,而它总是能成。

Alice ----(出站 UDP)----> TURN 中继 <----(出站 UDP)---- Bob
        (NAT-A 映射)                 (NAT-B 映射)

TURN 是那个有保证的兜底,也是 NAT 穿透之所以可靠、而不仅仅是碰运气的原因。它同时也是昂贵的 那一环:这一对节点之间的全部数据现在都要穿过你的中继,既产生实实在在的出口成本,又增加延迟—— 路径不再是最近的那条,而是两台 NAT 都允许的那条绕行路。这就是”优先直连,实在不行才中继”背后的 全部经济学:一条走中继的会话,成本可能是直连的数量级之差。基于 UDP 的 TURN 是常态;UDP 被彻底 封死的网络里还有基于 TCP 或 TLS 的 TURN,代价是延迟。

ICE:把它们统一起来的框架

实践中,你不会单独去实现 STUN 或 TURN。ICE(Interactive Connectivity Establishment)才是 跑通这一切的状态机:收集候选、通过信令交换、两两配对、逐个测试,然后挑出最好的那条能用的。

每一侧会收集:

  • 主机候选(host candidate)——本地地址(在局域网里有用)。
  • 服务器反射候选(server-reflexive candidate)——STUN 发现的那条公网映射。
  • 中继候选(relayed candidate)——TURN 分配来的地址。

这些候选通过信令通道交换(你自己应用层的协议——WebRTC 的 offer/answer,mesh VPN 的对端列表)。 接着 ICE 执行连通性检查(connectivity check):从每个本地候选向每个远端候选发一个 STUN Binding Request,并等待回应。一条双向都能应答的候选对,就是一条可用路径。ICE 会给可用候选对按 偏好排序——直连优先于中继、延迟低的优先——然后提名最优的那条。它还会让备选路径保持存活,这样 路径失效时无需重新协商就能切换。

ICE 的核心结论:穿透是一场协商,而不是一个协议。STUN 给你地址,打洞给你路径,TURN 给你 保证,而 ICE 是把它们排出优先级的策略。

到 2026 年都变了什么

经典图景仍然是正确的心智模型,但环境确实移位了。

  • CGNAT 成了常态。 许多家宽和移动用户都处在运营商级 NAT 后面,有时还是多层嵌套。你根本不 拥有公网地址;那个”公网”映射其实在运营商的 NAT 内部。CGNAT 常常使用端点相关(对称型)映射, 这是打洞的最坏情况,于是中继的使用多于十年前。部分部署还按用户共享地址并划分端口段,这在概念 上什么都没改变,但会让你手工测试的结果变得毫无意义。
  • 端口随机化。 许多 NAT 从一大片随机池里挑外部端口,而不是顺序分配。这并不改变打洞能否 成功(协调是在带外进行的),但它废掉了所有”预测下一个端口”的启发式,也击溃了朴素的端口扫描。
  • QUIC 与基于 UDP 的传输。 QUIC 跑在 UDP 上,并拥有自己的连接 ID,因此连接连续性与 IP/端口 元组无关。一条 QUIC 连接于是能在会杀死 TCP 连接的 NAT 重绑定中存活下来。它本身不会让打洞更 容易,但能让一条已经打通的路径更有韧性。
  • 基于 WireGuard 的 overlay。 现代 mesh VPN(比如我在这里跑的那套) 把自己的协议封装在 UDP 里,再在 overlay 内部跑类似 STUN 的发现,找出彼此被反射映射的端点, 然后打洞。结果是一张扁平的虚拟子网,每个节点都能寻址到任意节点,中继则作为路由层的兜底。这些 overlay 到底给什么、不给什么,在这篇伴文里。

一段话总结

NAT 之所以破坏 P2P,是因为它隐藏地址,并拒绝不请自来的入站流量。STUN 告诉你 NAT 分给你的地址, 却不告诉你有没有人能连上它。打洞利用”出站会创建状态”这一事实,让两端协调着互相对拨,从而使彼此 的包能穿过对方的过滤器——前提是 NAT 把端口保持稳定。对称型 NAT 和 CGNAT 之所以难缠,是因为它们 按目的地址分配端口,这也正是 TURN 存在的理由:一个永远能用、也永远要花钱的中继。ICE 是那个先试 直连、再回落中继的框架;也正因如此,“NAT 穿透”最好别被理解为某一个协议,而应被理解为一组关于 中间设备会如何行事的、按序下注的博弈。