打洞不是技术,是利用 NAT 自己的规则。 你先发过包的对端,NAT 就给它开门。
13.1 说 WebRTC 走 UDP,这一节讲它怎么把 UDP 包送进 NAT 后面。 13.2.2 讲 STUN 与 TURN 分别帮什么忙。
一、NAT 只放行「你先找过的人」
家用路由器做的那件事很简单:内网能出去,外面不能随便进来。 具体规则是「你向谁发过包,谁回包才放进来」。 这条规则既挡住了扫描,也挡住了 WebRTC。
打洞利用的正是这条规则: 两边同时往对方的公网地址发一个包,各自在自己的 NAT 上开一个口子。 口子一开,对方的包就能进来——这就是「穿透」。
所以顺序是关键:先发,再收。 如果只等不发,对方的包会被 NAT 直接丢掉, 而丢包是静默的,你只会看到「一直 connecting」。 这就是为什么自建 WebRTC 时「两边都在等」是典型错误。
二、真打一次:先发再收
环回上没有 NAT,所以这一步只能证明「UDP 知道地址就能发」。 但顺序仍然要看:先发再收,和不发只收,结果完全不同。
动作 结果
A 先发一个包给 B 发出去了 B 回一个包给 A A 收到 reply B 收到 A 的第一个包没有 收到了 hole
两边都通了,因为这里没有 NAT。 要注意的是 A 第一个包早在 B 回包之前就发了——这个「先发」的动作就是打洞。
在有 NAT 的网络里,少了这一步,对方的包一定进不来。 而且这个包不需要有意义,空包也算—— 它在 NAT 上留下的是一条映射表项,不是一个请求。 所以打洞包发出去之后就可以丢了,对方收到也直接丢掉。
三、四种 NAT,两种结局
打洞不是总能成,差别就在「公网端口怎么分配」:
NAT 类型 访问 STUN 时的公网端口 访问对端时的公网端口 预测对不对
锥形 40128 40128 对 对称 40065 40143 错
锥形 NAT 一个内网端口只映射一个公网端口, 所以「问 STUN 得到的地址」直接就能给对端用,打洞成功。 对称 NAT 每访问一个新目标就换一个公网端口, 问 STUN 得到的那个地址是废的——对端往那儿发包,打不到你。
这一步是模型推导,不是实测:沙箱里没有真 NAT 设备。 但它推的就是 RFC 里那两条规则的直接结果,结论可靠。
所以「我家网络打洞总失败」不是配置问题,是网络结构问题。 对称 NAT 下唯一的办法是中继: 两边都连到一台公网服务器,由它转发。这就是 TURN。
四、还要看防火墙这一层
NAT 之外还有一道墙:企业防火墙。 它可能直接封 UDP,只留 80 与 443。 这时候连 STUN 都问不出去,候选表里只剩内网地址。
标准解法是把 TURN 也跑在 443 上,用 TCP 或 TLS 伪装成 HTTPS。 这样墙以为是网页流量,就放过去了。 代价是延迟变高、服务器带宽翻倍——但至少能通。
所以一套完整的 WebRTC 服务通常要准备三套地址: UDP 3478 走 STUN、UDP 中继段走 TURN、TCP 443 走 TURN over TLS。 前两套不通时,第三套兜底。 这条「逐级降级」的链路,才是能不能商用的分界线。
五、动手 Demo:NAT 穿透——先发一个包出去
两个 UDP socket 互发一次,看顺序带来的差别。 再按映射表推一遍两种 NAT 的打洞结果。
# 两个 socket 互发,先发再收
a = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
a.bind(("127.0.0.1", 0))
b.bind(("127.0.0.1", 0))
a.settimeout(2)
b.settimeout(2)
a.sendto(b"hole", b.getsockname()) # 先发:这一步就是打洞
hb, ab = b.recvfrom(100)
b.sendto(b"reply", ab) # 再回
ha, aa = a.recvfrom(100)
print("B 收到", hb, "来自", ab)
print("A 收到", ha, "来自", aa)
# 两种 NAT 的端口映射(按 RFC 规则推,不是实测)
NATS = [("锥形", 40128, 40128), ("对称", 40065, 40143)]
for name, p_stun, p_peer in NATS:
ok = (p_stun == p_peer)
print(name, "STUN 看到", p_stun, "对端看到", p_peer,
"打洞", "成" if ok else "败")
实跑输出:
--------------------------------------------------------------------
NAT 穿透——先发一个包出去
--------------------------------------------------------------------
**① NAT 只放行「你先找过的人」**
**家用路由器做的那件事很简单:内网能出去,外面不能随便进来。**
**具体规则是「你向谁发过包,谁回包才放进来」。**
**这条规则既挡住了扫描,也挡住了 WebRTC。**
**打洞利用的正是这条规则:**
**两边同时往对方的公网地址发一个包,各自在自己的 NAT 上开一个口子。**
**口子一开,对方的包就能进来——这就是「穿透」。**
**所以 U 盘直连那个「P2P 加速」功能,本质就是这一步。**
**② 真打一次:两个 UDP socket 互发**
**环回上没有 NAT,所以这一步只能证明「UDP 知道地址就能发」。**
**但顺序仍然要看:先发再收,和不发只收,结果完全不同。**
动作 结果
--------------------------------------------
A 先发一个包给 B 发出去了
B 回一个包给 A A 收到 reply
B 收到 A 的第一个包没有 收到了 hole
**两边都通了,因为这里没有 NAT。**
**要注意的是 A 第一个包早在 B 回包之前就发了**——**这个「先发」的动作就是打洞,**
**在有 NAT 的网络里,少了这一步,对方的包一定进不来。**
**③ 但打洞不是总能成**
**两种 NAT 的差别就在「公网端口怎么分配」:**
**锥形 NAT 一个内网端口只映射一个公网端口;**
**对称 NAT 每访问一个新目标就换一个公网端口。**
NAT 类型 访问 STUN 时的公网端口 访问对端时的公网端口 预测对不对
-------------------------------------------------------------------------------
锥形 40128 40128 对
对称 40065 40143 错
**锥形 NAT 两次映射到同一个端口,所以「问 STUN 得到的地址」**
**直接就能给对端用,打洞成功。**
**对称 NAT 两次端口不同,问 STUN 得到的那个地址是废的**——**对端往那儿发包,打不到你。**
**这一步是模型推导,不是实测:沙箱里没有真 NAT 设备。**
**但它推的就是 RFC 里那两条规则的直接结果,结论可靠。**
**对称 NAT 下唯一的办法是中继:**
**两边都连到一台公网服务器,由它转发。这就是 TURN。**
**所以「打洞失败」不是配置问题,是网络结构问题。**
A 发出去了,B 收到了 hole,B 回了 reply,A 也收到了。 两边都通,因为这里根本没有 NAT 挡路。 这个 Demo 能证明的只有一件事:UDP 只要知道地址就能发。 它证明不了打洞成功——打洞是否成功,取决于两侧 NAT 的类型。 下面那张映射表是推的:锥形两次映射同端口,预测对; 对称两次不同端口,预测错,只能走中继。
30 秒实验
- 把
a.sendto那行删掉,只留recvfrom,猜会怎样——两边都超时,因为谁也开不了口子。 - 把 B 的
recvfrom改成先睡 1 秒再收,猜还能不能收到——能,UDP 的包在内核队列里等着,但队列满了就丢。 - 换成对称 NAT 的企业网,猜候选表里还剩什么——只剩内网地址与 srflx,而且 srflx 是错的,只能走 TURN。
常见误区
- 「打洞是黑科技」——它只是让 NAT 按自己的规则开门。
- 「打洞包必须有内容」——空包也行,NAT 只看地址。
- 「打洞失败一定是代码问题」——对称 NAT 下必然失败。
自测三问(点开对答案)
1. 打洞利用的是 NAT 的哪条规则? —— **「你先发过包的对端,回包才放进来」**,所以要先发。 2. 对称 NAT 为什么打不通? —— **每换一个目标就换一个公网端口**,STUN 报的地址对不上。 3. 防火墙只留 443 时怎么办? —— **把 TURN 跑在 443 上走 TCP 或 TLS**,伪装成网页流量。- ☐ (本节完,下一节 13.2.2 STUN 与 TURN)