NAT 的问题修不好,只能绕:静态映射绕「不可入」,打洞绕「回不来」。 打洞成不成看 NAT 类型,对称型是死结,所以最后一定要有中转兜底。
5.6.4 列的四个问题,这一节给对策,而且要说清「哪个能修、哪个只能绕」。 5.7 的隧道是兜底手段的技术底座,TURN 中转本质就是一条隧道。
一、先分清能修和不能修
四个问题里,两个能缓解,两个只能绕开:
| 问题 | 性质 | 对策 |
|---|---|---|
| 外面连不进来 | 结构性 | 静态映射或打洞 |
| 端口耗尽 | 容量性 | 加地址、限并发 |
| 载荷里的地址 | 协议性 | 改被动模式或 ALG |
| 身份不可见 | 记录性 | 补映射日志 |
第一行「结构性」的意思是「改配置没用」:只要还有 NAT,外面的第一个包就一定进不来, 必须有人先把一个包送出去,把表项建起来。 这个「先送一个包」的动作,就是打洞的全部思想。
二、静态映射:把洞钉死
最直白的办法是「事先在表里钉一条」: 把公网地址的某个端口,永久映射到内网的某台机器的某个端口。 这样外面连进来时,表里已经有项了。 它的代价是「钉死的门永远开着」:只要映射在,扫描就不会停, 所以「用完就关」不是洁癖,是必需的卫生习惯。 家用场景更常见的是一种自动开洞: UPnP、NAT-PMP、PCP 这几个协议,让内网程序向路由器「申请一个映射」。 好处是不用进管理页面点鼠标, 坏处是「哪个程序开了哪个门」你未必知道——这也是很多安全建议让人把 UPnP 关掉的原因。
三、NAT 的四种类型
打洞能不能成,取决于 NAT「给不同目的分配端口」的规则:
| 类型 | 映射规则 | 过滤规则 | 打洞 |
|---|---|---|---|
| 全锥型 | 所有目的共用一个端口 | 谁都能进 | 总能成 |
| 受限锥型 | 共用一个端口 | 只收我发过的 IP | 通常能成 |
| 端口受限 | 共用一个端口 | 只收我发过的 IP 加端口 | 常能成 |
| 对称型 | 每个目的一个端口 | 只收我发过的 | 基本打不通 |
前三行的共同点是「所有目的共用一个公网端口」: 所以「我在服务器上看到的端口」就是「别人连我时该用的端口」。 第四行破了这个前提:每个新目的都会分到一个新端口, 于是「学到的端口」和「连对方时用的端口」不是同一个,对方按学到的端口打,永远打不进来。 这也是「家用宽带打洞常成功、手机热点常失败」的原因: 运营商级 NAT 为了省地址,普遍是对称型。
四、打洞的四步与三件套
打洞(hole punching)的动作只有四步:
| 步 | 谁做 | 做什么 |
|---|---|---|
| 1 | 两边都连服务器 | 服务器记下各自的公网地址端口 |
| 2 | 服务器把对方的地址告诉它 | 这一步叫「交换候选」 |
| 3 | 两边同时向对方发包 | 各自的 NAT 里就都有了对方 |
| 4 | 后续直连 | 服务器可以退场 |
第 3 步的「同时」很关键:如果只有一边发,另一边的 NAT 里没有项,包还是会被丢。 所以严格说不是「同时」,而是「都发过一次」—— 只要有先后,先发的那一边的包会被丢,但表项建起来了,后发的就能进。 围绕这四步,长出了三个东西:
| 手段 | 做什么 | 什么时候用 |
|---|---|---|
| STUN | 问服务器「我的公网地址是什么」 | 锥型,最便宜 |
| TURN | 打不通就全部走服务器中转 | 对称型,兜底 |
| ICE | 挨个试,能直连就直连 | 现在的标准做法 |
第三行是「框架」不是「协议」: 它把 STUN 和 TURN 都用上,按优先级一个个试——先试直连,不行再中转。 所以「视频通话为什么有时连不上」的答案往往是: 两边都是对称型 NAT,只能走 TURN 中转,而中转服务器满了或离得远。
五、真正的解法是 v6
上面这些全是「在 NAT 存在的前提下想办法」,而 NAT 存在的前提是「地址不够」。 所以根治的办法只有一个:让地址够用。 v6 地址够用到「每台设备一个公网地址」, 于是不需要 NAT,不需要端口映射,不需要打洞,不需要 STUN 和 TURN。 但要分清两件事: NAT 消失不等于「没有防火墙」—— 防火墙是「策略」,NAT 是「地址转换」,以前它们凑巧装在同一台设备上,所以容易被混为一谈。 v6 时代依然要有策略,只是不再靠「没有地址」来当安全措施。
六、动手 Demo:打洞为什么有时候打不通
把四种 NAT 类型和三种穿透手段摆在一起看。 先看「映射规则」那一列,再看「打洞结果」。
# 对称型的关键:公网端口随目的变化
stun_port = 40001 # 从 STUN 服务器学到的
peer_port = 40002 # 连对方时实际用的
print('学到的端口', stun_port, '实际用的端口', peer_port)
print('对方按', stun_port, '打过来 -> 打不通')
实跑输出:
--------------------------------------------------------------------
打洞为什么有时候打不通
--------------------------------------------------------------------
**打洞(hole punching)的原理**:**两边都先向对方的公网地址发一个包**,
**各自的 NAT 表里就都有了对方,后续就能直连。**
**但能不能成,取决于 NAT 的类型**:
NAT 类型 映射规则 打洞结果
-----------------------------------------------------------------------
**全锥型** **任何外部都能进来** **几乎总能成功**
**受限锥型** **只接受我发过的 IP** **通常能成功**
**端口受限锥型** **只接受我发过的 IP 加端口** **常能成功**
**对称型** **每个目的一个端口** **基本打不通**
**第四行是「对称型」的死结**:**它给「每个不同的目的地址」分配不同的公网端口**,
**所以「你从 STUN 服务器学到的端口」和「你连对方时用的端口」不是同一个。**
**对方按学到的端口打,永远打不进来。**
**于是有了三种手段**:
手段 做什么 代价
---------------------------------------------------------------------------
**STUN** **问服务器「我的公网地址是什么」** **最便宜,锥型够用**
**TURN** **打不通就全部走服务器中转** **一定能通,但费带宽**
**ICE** **挨个试,能直连就直连** **现在的标准做法**
**第三行是「框架」不是「协议」**:
**它把 STUN 和 TURN 都用上,按优先级去试**——**先试直连,不行再中转。**
**所以「视频通话为什么有时连不上」的答案往往是**:
**两边都是对称型 NAT,只能走 TURN 中转,而中转服务器满了。**
上面三行代码把「死结」写清楚了:打洞的前提是「别人能猜到你用哪个端口」, 而对称型恰恰让这个前提不成立。 所以「对称型 NAT 用户」在 P2P 网络里是二等公民:他们只能靠中转,而中转要花服务器的带宽, 这也是「免费的视频通话软件为什么要限制时长」的一个原因。 最后一个判断口诀:「两边至少有一边是锥型,才有直连的可能」; 两边都是对称型,别折腾了,直接上中转。
30 秒实验
- 想一下:为什么打洞必须「两边都先发一次」?
- 算一下:一个 TURN 服务器要给 1000 路通话中转,按每路 1 Mbps 需要多少带宽?
- 想一下:为什么「有 NAT 所以安全」这句话是错的?
常见误区
- 以为「打洞是一种协议」。 它是一种技巧,STUN、ICE 只是把它工程化。
- 以为「打洞能穿透任何 NAT」。 对称型穿不过,必须中转兜底。
- 以为「上了 v6 就不需要防火墙」。 NAT 和防火墙是两件事,v6 只是不再需要地址转换。
自测三问(点开对答案)
1. 打洞的核心动作是什么? —— **两边都先向对方的公网地址发一个包**,把各自的 NAT 表项建起来,之后就能直连。 2. 哪种 NAT 打洞一定失败? —— **对称型**:它给每个目的分配不同的公网端口,「学到的端口」和「实际用的端口」不是同一个。 3. STUN、TURN、ICE 三者什么关系? —— **STUN 负责「问地址」,TURN 负责「打不通就中转」,ICE 是把两者按优先级串起来的框架。**下一节讲一种「把整个包塞进另一个包」的技术:IP 隧道。
- ☐ (本节完,下一节 5.7 IP隧道)