网络穿透这块知识,看着零散琐碎,实则逻辑闭环极强。
今天这篇文章,我想从 NAT 底层机制,到四种 NAT 类型的穿透差异,再到 STUN 打洞、 TURN 中继、 ICE 智能选路,最后落地 WebRTC 实战和 coturn 生产部署,整个网络穿透流程和大家走一遍。
估摸着,看完你应该能搞定90%的内网穿透、 P2P 连接异常问题了吧。
一、网络穿透解决了什么问题? 先问大家一个问题:为什么两台都连路由器、处于内网的手机或电脑,没法直接点对点通信?
很多人知道“内网设备不能被外网访问”,但从来没深究过背后的逻辑。
这就是网络穿透要解决的终极问题: 打破内网隔离,让两个无公网IP的内网设备,实现双向通信 。
1.1 两端位于内网的设备为何无法直接通信? 你家的电脑连了 Wi-Fi ,你朋友的电脑也连了 Wi-Fi 。你们都打开了一个 P2P 聊天软件,想直接传文件。软件界面上显示"连接中......",然后超时了。
这不就是发消息吗,为什么这么难?
你家路由器有一个公网 IP (比如 203.0.113.5 ),你的电脑内网 IP 是 192.168.1.100 。你朋友家类似,公网 IP 是 203.0.113.8 ,电脑内网 IP 是 192.168.1.101 。
你的电脑给朋友发数据包——数据包先到你家路由器,路由器把源地址从 192.168.1.100:12345 改成 203.0.113.5:54321 ,然后发出去。朋友的路由器收到后,一看目标端口是 54321 ——这个端口压根没开过啊——直接扔掉。
问题出在这儿: NAT 只允许内网主动发起连接时建立映射表,不允许外部主动发起连接。
1.2 公网 IP、私网 IP 与端口的三层地址结构 要搞懂这个问题,咱们得先聊聊 IP 地址。其实工程里真正要看的地址,至少有三层。
第一层是内网 IP 。比如:
192.168.1.8 10.0.2.15 172.16.8.20 这些地址在局域网里活得很好,但放到公网就不行。全世界不知道有多少台机器都叫 192.168.1.8 。这就像公司里十个部门都有一个“张伟”,你在楼下喊一嗓子张伟,谁来?不好说。
第二层是公网 IP 。路由器、企业出口网关、运营商网关对外暴露的地址,可能是:
203.0.113.10 198.51.100.20 公网 IP 才是互联网路由系统能识别的地址。
第三层是端口。 NAT 设备真正做事时,通常不是只改 IP ,而是改 IP 加端口。比如内网的:
192.168.1.8:50000 出网以后可能变成:
203.0.113.10:41001 于是一个完整的通信端点,不是“ IP ”,而是:
传输协议 + IP + 端口 UDP 192.168.1.8:50000 TCP 10.12.3.9:60000 STUN 要发现的,通常就是 NAT 映射后的公网反射地址。 RFC 5389 把 STUN 定位为一种帮助其他协议处理 NAT 穿越的工具,可以让端点获知 NAT 为它分配的 IP 和端口,也可用于连通性检查和保活;它不是一个单独包治百病的穿透方案。
1.3 网络穿透的核心矛盾是 NAT 阻断了外部主动发起的连接 NAT 的核心行为很像门卫。核心矛盾就一句话: NAT 是为"内网主动访问外网"设计的,不是为"外网主动访问内网"设计的。
内网设备主动出去,门卫说:“可以,我登记一下。你是 192.168.1.8:50000 ,出去以后我给你挂成 203.0.113.10:41001 。”
外面的服务器回包,门卫查登记表:“哦,这个 203.0.113.10:41001 对应里面的 192.168.1.8:50000 ,放行。”
但外部陌生主机突然来一句:
我要访问 203.0.113.10:41001 门卫就会皱眉。你谁?里面那台机器有没有先联系过你?这个端口是不是给你开的?如果没有记录,不放。
P2P 通信最难的地方,不是发包,而是让双方的 NAT 都提前出现“这个外部地址可以进来”的状态。后面 UDP 打洞、 ICE 连通性检查,干的都是这件事。听起来像很牛,其实就是趁 NAT 门卫还记得你,赶紧把双方的洞都戳开。
2. NAT 的工作原理与映射表 2.1 NAT 的定义与存在原因 为啥要有 NAT ?三个原因:
第一, IPv4 地址不够用。 IPv4 总共42亿多个地址,地球上70亿人,每人分一个都不够。 NAT 让一个公网 IP 能养活一个局域网几十上百台设备。
第二,网络隔离。 内网设备不直接暴露在公网上,安全系数高一些。黑客想扫你家电脑,得先过路由器这一关。
第三,灵活组网。 企业内网随便加设备,不用跟运营商申请公网 IP 。
有得必有失。 NAT 给了你地址复用和安全边界,代价就是—— 外部无法主动连接内网设备 。
2.2 NAT 基本工作流程 看一个最普通的 UDP 请求。
内网客户端 A 192.168.1.8:50000 目标服务器 S 8.8.8.8:3478 A 发包:
src = 192.168.1.8:50000 dst = 8.8.8.8:3478 包到 NAT 后, NAT 改写源地址:
src = 203.0.113.10:41001 dst = 8.8.8.8:3478 同时建一条映射表:
192.168.1.8:50000 <-> 203.0.113.10:41001 -> 8.8.8.8:3478 服务器回包:
src = 8.8.8.8:3478 dst = 203.0.113.10:41001 NAT 查表,把目标地址改回内网地址:
src = 8.8.8.8:3478 dst = 192.168.1.8:50000 A 收到响应。
这条链路能成立,是因为连接由内网主动发起。 NAT 有表可查。没有表时, NAT 一般不会猜“这个包应该给谁”。
工程里排查穿透问题,我最爱看的就是这张“表”。你在抓包工具里看不到完整 NAT 内部表,但可以通过 STUN 返回、客户端日志、服务端看到的源地址,大概拼出它。拼出来以后,问题通常就清楚一半了。
2.3 NAT 映射表结构解析 映射表长什么样?大概是这样:
+---------------------+----------------------+----------------------+ | 内网地址 | NAT 外侧映射 | 目标地址 | +---------------------+----------------------+----------------------+ | 192.168.1.8:50000 | 203.0.113.10:41001 | 8.8.8.8:3478 | | 192.168.1.9:50000 | 203.0.113.10:41002 | 8.8.4.4:3478 | | 192.168.1.8:50001 | 203.0.113.10:41003 | 1.1.1.1:443 | +---------------------+----------------------+----------------------+ 每一行就是一个会话映射。回包来了, NAT 查这个表决定转发给谁。表项有生命周期,超时了就删掉——这就是为啥你的 P2P 连接如果长时间没数据,会被 NAT "遗忘"。
2.4 NAPT:端口复用的核心机制 严格说,家庭路由器最常见的不是只做地址转换的 NAT ,而是 NAPT ,也就是 Network Address Port Translation 。
内网一百台设备同时访问外网,公网 IP 只有一个。怎么区分回包该给谁?靠端口。
设备A用 192.168.1.100:12345 访问 Google , NAT 分配公网端口 54321 。设备B用 192.168.1.101:12345 访问 Facebook , NAT 分配公网端口 54322 。回包到了,看目标端口就知道该转给谁。
这就是端口复用的本质:用不同的公网端口区分不同的内网会话。
有些老路由器只支持几百个并发会话,现在的企业级设备动辄支持几万甚至几十万。但不管支持多少,本质都一样—— NAT 设备维护着一张映射表,表项有寿命 。
3. NAT 的四种类型 这章是重点。 NAT 穿透能不能成,很大程度上取决于两端的 NAT 是什么类型。搞不懂这个,后面 STUN 、 TURN 、 ICE 全是白搭。
3.1 完全锥型 NAT 完全锥型 NAT ,英文 Full Cone NAT 。最友好的 NAT ,没有之一。
只要内网地址端口产生了一个外部映射,例如:
192.168.1.8:50000 -> 203.0.113.10:41001 此后—— 任何外部主机都可以向 203.0.113.10:41001 发包, NAT 都会转给内网设备 。
"完全锥型"这名字挺形象——就像一个锥形漏斗,图是这样:
Peer X ----\ Peer Y -----+----> 203.0.113.10:41001 ----> 192.168.1.8:50000 Peer Z ----/ 这种 NAT 对 UDP 打洞很友好。A 只要通过 STUN 拿到自己的公网映射地址,把它告诉 B,B 发过来就有机会进来。
这玩意儿穿透成功率最高, STUN 一把梭就行。但现实中完全锥型 NAT 并不多见,家用路由器偶尔能碰上,企业网络基本别想。
3.2 受限锥型 NAT 受限锥型 NAT ,英文 Restricted Cone NAT 。比完全锥型严格一点。也是比较容易穿透的类型。
它的映射仍然固定:
192.168.1.8:50000 -> 203.0.113.10:41001 但外部主机不是谁都能进。只有内网主机曾经发过包给某个外部 IP ,这个外部 IP 才能从任意端口发回来。
A 先发给 Peer X:9000 之后允许: Peer X:任意端口 -> 203.0.113.10:41001 不允许: Peer Y:任意端口 -> 203.0.113.10:41001 它限制 IP ,不限制端口。
P2P 打洞时,双方都要先向对方公网 IP 发包。哪怕第一批包互相丢掉,也没关系。关键是 NAT 记录了“我里面这台机器刚刚联系过对方 IP ”。下一批包就可能进来了。
这也是为什么 UDP 打洞不是一次发包完事,而是要连续发一小段时间。你要给双方 NAT 一个“认人”的机会。
3.3 端口受限锥型 NAT 端口受限锥型 NAT ,英文 Port Restricted Cone NAT 。
它也保持固定映射:
192.168.1.8:50000 -> 203.0.113.10:41001 但过滤更严格。内网主机必须先发包给某个外部 IP 加端口,外部这个 IP 加端口才允许回包。
A 先发给 Peer X:9000 允许: Peer X:9000 -> 203.0.113.10:41001 不允许: Peer X:9001 -> 203.0.113.10:41001 Peer Y:9000 -> 203.0.113.10:41001 它不只认人,还认门牌号。
这种 NAT 下,候选地址交换必须准确。端口不能猜错。两端最好都用同一个 socket 持续发 STUN 、发探测包、发业务包。你要是中途换了 socket ,本地端口变了,映射也可能变。然后你就会看到那种很恶心的 bug :日志上地址都对,但就是连不上。
3.4 对称型 NAT 最狠的来了。
前面三种锥型 NAT ,映射关系是固定的——内网 X:port 永远映射到公网 Y:port ,不管跟谁通信。
对称型 NAT 不一样:每跟一个不同的目标通信, NAT 就分配一个新的公网端口 。
192.168.1.8:50000 -> 8.8.8.8:3478 映射为 203.0.113.10:41001 192.168.1.8:50000 -> 9.9.9.9:3478 映射为 203.0.113.10:41002 192.168.1.8:50000 -> PeerB:60000 映射为 203.0.113.10:41099 这就麻烦了。
A 通过 STUN 服务器拿到的是:
203.0.113.10:41001 然后 A 把这个地址告诉 B。B 往 203.0.113.10:41001 发包。可 A 真正发给 B 时, NAT 可能给 A 生成的是 203.0.113.10:41099 。B 拿着旧地址敲门,当然没人开。
老 STUN RFC 3489 曾按 Full Cone 、 Restricted Cone 、 Port Restricted Cone 、 Symmetric 来描述 NAT 类型,并给出类型发现流程;后来的规范更倾向于把 NAT 行为拆成“映射行为”和“过滤行为”来看,因为现实设备没那么乖,不一定严格落在四个盒子里。
对称型 NAT 面前, STUN 基本等于废了。
3.5 四种 NAT 类型的映射行为与过滤行为对比图解 用一张图粗暴归纳:
再换成打洞视角:
Full Cone A 打一个洞,大家都能从这个洞进来。 Restricted Cone A 打一个洞,但只认 A 主动联系过的外部 IP。 Port Restricted Cone A 打一个洞,只认 A 主动联系过的外部 IP 和端口。 Symmetric A 对每个目标打出来的洞都可能不一样。 注意啊,这只是理论模型。真实设备会混搭。比如映射是 endpoint-independent ,过滤却是 address-and-port-dependent 。 RFC 4787 讨论 UDP NAT 行为时,就把过滤行为拆成 endpoint-independent 、 address-dependent 、 address-and-port-dependent 等类型,并指出更严格的地址加端口过滤可能导致 ICE 需要 UDP relay 。
3.6 NAT 类型对 P2P 穿透成功率的影响分析 P2P 穿透能不能成,最关键看两边 NAT 的组合。
如果两边都是 Full Cone ,成功率很高。你甚至会觉得网络穿透没啥难的。别高兴太早,那是环境宠你。
如果一边 Full Cone ,一边 Restricted 或 Port Restricted ,也大概率能通过同时发包打洞。
如果两边都是 Port Restricted ,还是有机会。关键是双方要准确交换公网 IP 和端口,并在 NAT 映射过期前同时发包。
如果有一边是 Symmetric ,情况开始变糟。尤其两边都是 Symmetric , STUN 拿到的反射地址很可能对真正的 peer 没用。此时不要硬装。该上 TURN 就上 TURN 。省那点服务器带宽,最后花在客服、投诉、事故复盘上的钱更多。
4. STUN 协议,公网地址发现机制 搞懂了 NAT ,再看 STUN 就完全没难度了。 STUN 是所有穿透方案的基础,没有 STUN ,就没法获取设备的真实公网映射地址,打洞也就无从谈起。
4.1 STUN 的定义与协议定位 STUN 现在的名字是 Session Traversal Utilities for NAT 。注意是 Utilities ,工具集。这个名字很重要。
早期很多文章把 STUN 讲成“穿透协议”。这话不算完全错,但容易误导新人。
STUN 本身主要帮你发现:我从公网看起来是谁?我的 NAT 给我映射成了哪个 IP 和端口?它也可用于连通性检查和 NAT binding 保活,但它不保证你能和任意对端直连。 RFC 5389 就明确说, STUN 不是独立的 NAT 穿透解决方案,而是在 NAT 穿透方案上下文中使用的工具。
一个最小 STUN 过程:
Client ---- Binding Request ----> STUN Server Client <--- Binding Response ---- STUN Server XOR-MAPPED-ADDRESS = 203.0.113.10:41001 客户端看到这个结果,就知道:
我本地是 192.168.1.8:50000 公网看我是 203.0.113.10:41001 这个公网地址就叫 server reflexive address ,后面 ICE 里会继续用。
4.2 STUN 的客户端-服务器架构与传输层 STUN 是典型客户端-服务器架构。
客户端通常嵌在 App 、浏览器、软电话、游戏客户端里。服务器部署在公网,有稳定 IP 和端口。客户端向 STUN Server 发 Binding Request ,服务端根据收到包的源地址,回一个 Binding Response 。
STUN 可以跑在 UDP 上,也可以跑在 TCP 、 TLS 等传输之上。 WebRTC 里最常见的是 UDP ,因为实时音视频最关心延迟。 TCP 也能用,但遇到阻塞、重传、队头阻塞,体验就可能打折。 TLS 适合被某些网络策略要求加密传输的环境。
一条典型配置长这样:
const pc = new RTCPeerConnection ({ iceServers : [ { urls : "stun:stun.example.com:3478" }, { urls : "turn:turn.example.com:3478" , username : "user" , credential : "pass" } ] }); MDN 对 RTCPeerConnection 的说明里也把 iceServers 描述为 ICE agent 使用的服务器列表,通常就是 STUN 或 TURN 服务器。
4.3 STUN 消息结构解析 STUN 消息分两部分:消息头 + 消息体(属性列表)。
4.3.1 消息头 STUN 消息是二进制格式。所有 STUN 消息都从 20 字节头开始,后面跟 0 个或多个 Attributes ;头部包含 message type 、 message length 、 magic cookie 和 transaction ID 。
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 类型 (16位) | 长度 (16位) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Magic Cookie (32位,固定0x2112A442) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 事务ID (96位 / 12字节) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ • 类型 : Binding Request 是 0x0001 , Binding Response 是 0x0101 • Magic Cookie :固定值 0x2112A442 ,用来区分 STUN 消息和其他协议 4.3.2 消息体:Attributes 消息体是一串 TLV ( Type-Length-Value )属性。常见属性:
• MAPPED-ADDRESS :客户端的公网映射地址(兼容旧版,已不推荐) • XOR-MAPPED-ADDRESS :异或编码的公网映射地址(推荐使用) • CHANGE-REQUEST :请求服务器改变源 IP /端口,用于 NAT 类型检测 4.4 STUN 事务模型 STUN 事务模型很像一次带事务 ID 的请求响应。
Client STUN Server | | | ---- Binding Request ------------> | | | | <--- Binding Success Response ---- | | | 如果失败:
Client STUN Server | | | ---- Binding Request ------------> | | | | <--- Binding Error Response ------ | | 401 Unauthorized | | 438 Stale Nonce | UDP 上没有连接状态,所以客户端要负责重传、超时、匹配 Transaction ID 。 TCP 上可靠性由传输层兜一部分,但 STUN 自己的事务语义还在。
4.5 XOR-MAPPED-ADDRESS 的设计目的 你可能会问: MAPPED-ADDRESS 不就能告诉客户端公网地址吗?为啥还要搞个 XOR 版本?
因为 NAT 会修改数据包的有效载荷。
有些变态的 NAT 设备不仅改 IP 头,连应用层数据都敢动。 MAPPED-ADDRESS 里的 IP 地址是明文的, NAT 看到了可能顺手就给改了。 XOR-MAPPED-ADDRESS 用 Magic Cookie 和事务 ID 做异或编码。 NAT 看不懂这串东西,就不会乱改。
客户端收到后用同样的方式解码,得到真实的公网地址。 这就是 XOR 的意义——防止 NAT 篡改。
4.6 STUN 的 NAT 类型检测流程 STUN 不仅能告诉你公网地址,还能帮你判断 NAT 类型。方法是 多次请求,改变请求参数,观察响应变化 。
标准流程( RFC 5780 )大致如下:
1. 第一次请求 :普通 Binding Request 到 STUN 服务器。看响应里有没有 XOR-MAPPED-ADDRESS 。如果有,说明你可能在 NAT 后面(或者你在公网上,但 STUN 返回的就是你本身的地址)。 2. 第二次请求 :带 CHANGE-REQUEST 属性,请求服务器从不同的 IP 和端口回响应。如果这次收不到响应,说明 NAT 对源地址有过滤——可能是受限锥型或更严格的类型。 3. 后续测试 :通过组合不同的请求参数,观察映射地址是否变化、外部包能否进入,最终判断出 NAT 的具体类型。 这套检测流程在实际项目中用得不多——大部分应用直接用 ICE 的连通性检查来判断,不专门做 NAT 类型检测。但了解这个流程有助于理解 NAT 的行为模式。
4.7 STUN 的局限性 STUN 最容易被误用的地方,就是“拿到了公网地址,所以一定能连”。
不一定。
STUN 只能告诉你:你访问 STUN Server 时,公网看你是什么地址。它不能保证你访问 Peer 时还是这个地址。对称型 NAT 下,同一个本地地址端口访问不同目标,外部映射可能不同。
它也不能保证防火墙允许对端流量进来。就算 NAT 映射对了,企业防火墙一句“ UDP 禁止”,你也没办法。校园网、酒店 Wi-Fi 、某些移动网络,都能给你上一课。
所以 STUN 是探路灯,不是桥。路通不通,还要 ICE 检查。桥搭不起来,就走 TURN 中继。工程上千万别抱着“我只配 STUN ,省钱”这种侥幸心理。小流量 demo 可以,生产环境会教你做人。
5. UDP 打洞,P2P 直连的核心机制 学会了 STUN 拿公网地址,下一步就是最核心的 UDP 打洞。所有 P2P 音视频通话、点对点传输,核心全是这套机制。
5.1 打洞的本质 UDP 打洞这个名字,听着像黑客攻击。其实它很规矩。
它的本质是:让内网主机主动向外发包,促使 NAT 建立映射和过滤许可。然后对端趁这个映射还活着,把包发回来。
A 内网地址 192.168.1.8:50000 A 公网映射 203.0.113.10:41001 B 内网地址 10.0.0.9:60000 B 公网映射 198.51.100.20:52001 A 要先给 B 的公网地址发 UDP :
A -> 198.51.100.20:52001 B 也要给 A 的公网地址发 UDP :
B -> 203.0.113.10:41001 第一轮包很可能丢。没事。我们要的不是第一轮就成功,而是让 NAT 建表。
NAT-A 看到 A 发给 B,于是允许 B 回来 NAT-B 看到 B 发给 A,于是允许 A 回来 双方洞都打开后,后续包就能进了。
5.2 双方同时向对方公网地址发送 UDP 包的并发策略 为什么要“同时”?
因为 NAT 映射有超时时间。 UDP 没连接, NAT 不知道你聊完没聊完。它只能过一段时间没看到流量就清表。
如果 A 先打洞,等了 30 秒 B 才开始发,A 那边洞可能已经没了。尤其某些移动网络, UDP 映射超时很短,短得让人想骂街。
所以工程里通常这样做:
1. A、B 分别向 STUN Server 获取公网反射地址 2. A、B 通过信令服务器交换候选地址 3. A、B 收到对方地址后,立即开始周期性发送探测 UDP 包 4. 一旦收到对方包,确认直连成功 5. 继续发送 keepalive,防止 NAT 映射过期 探测包不要只发一次。可以短时间内按节奏发,例如 20ms、50ms、100ms,持续几秒。别太猛,别把网络打爆。你不是 DDoS 自己。
5.3 UDP打洞完整时序流程 看图:
信令服务器只负责交换信息,不转发媒体包。它像媒婆,撮合双方认识。真正过日子,还是 A 和 B 自己聊。
这里有个坑。很多新人以为 STUN Server 会参与后续通信。不会。 STUN 帮你发现地址,信令帮你交换地址,打洞成功后,业务流量直接走 P2P 。
除非失败了,才 TURN 上场。
5.4 打洞成功的条件 不是所有场景都能打洞成功,核心取决于四个条件:
NAT 类型要相对友好。 Full Cone 、 Restricted 、 Port Restricted 都还有得打。 Symmetric 就难。
映射要稳定。你从 STUN 拿到的公网端口,最好和你发给 peer 时用的公网端口一致。否则对方拿着错地址打你。
时间窗口要抓住。 NAT 映射过期前,双方探测包要到。
防火墙不能太狠。很多企业网会允许 TCP 443 ,但限制 UDP 。 WebRTC 走 UDP 很香,但如果 UDP 被拦, ICE 会尝试 TCP 或 TURN/TCP/TLS 等候选。延迟就上来了。
5.5 TCP 打洞与 UDP 打洞的差异 UDP 无连接。发就完了。 NAT 看到出站 UDP 包,建个映射。对端发回来,有机会进。
TCP 不一样。 TCP 有三次握手,有 SYN 、 SYN-ACK 、 ACK ,有状态机。 NAT 和防火墙也会跟踪 TCP 状态。你想让两个都在 NAT 后的端点同时发 SYN ,双方还要处理 simultaneous open 。理论可以,工程上更难。
TCP 打洞还容易被中间设备干扰。有些 NAT 不喜欢奇怪的 TCP 状态。有些防火墙只允许客户端主动连服务器,不允许这种互相 SYN 的玩法。
所以实时通信里,能用 UDP 优先 UDP 。 TCP 更多是兜底。 WebRTC 也不是不用 TCP ,但 P2P 直连路径里, UDP 通常更好用。 QUIC 也是基于 UDP ,这也是后面会说它对 NAT 穿透有点东西的原因。
6. TURN 协议,中继兜底方案 有了 STUN 和 UDP 打洞,为什么还要 TURN ?
很简单,前面说过,对称 NAT 、严格企业防火墙环境下,打洞基本必败。想要保证100%连接成功率,必须上中继方案, TURN 就是唯一的工业级选择。
6.1 TURN 的定义与设计目标 TURN 全称 Traversal Using Relays around NAT , RFC 5766 定义(2010年)。
名字翻译就是: 用中继绕过 NAT 。
当 STUN 搞不定、打洞打不通的时候, TURN 上场了。它不试图"穿透" NAT ,而是 直接绕过去 ——在公网上搭一个中继服务器,两边都把数据发给中继,中继帮你转发。
这是兜底方案,不是首选。 因为所有流量都经过中继服务器,延迟增加、带宽成本飙升。
6.2 TURN 与 STUN 的关系 TURN 不是独立发明的新协议—— 它构建在 STUN 消息框架上,使用 STUN 的消息格式、事务机制、属性机制,是 STUN 的扩展 。
TURN 的消息格式跟 STUN 完全一样(除了 ChannelData 消息),端口也一样用 3478 。 TURN 在 STUN 的基础上增加了一组新方法( Allocate 、 Send 、 ChannelBind 等)和新属性。
ICE 的候选地址收集流程中, STUN 和 TURN 是串起来的——先试 STUN 拿反射地址,不行再找 TURN 要中继地址。
这不是说 RFC 5766 没价值。它仍然是理解 TURN 的经典入口。
6.3 TURN 协议的核心工作流程 6.3.1 Allocate 请求:让客户端向 TURN 服务器申请中继资源 TURN 的第一步是 Allocate 。
客户端对 TURN Server 说:
我想申请一个中继地址,帮我收发数据。 时序:
Client TURN Server | | | ---- Allocate Request --------> | | | | <--- Allocate Success Response | | Relay Transport Address | 这一步需要认证。因为 TURN 很贵,谁都能用你的 TURN ,那就是给全世界送带宽。
6.3.2 Allocate 成功响应:中服务器分配 Relay Transport Address Allocate 成功后,服务器返回一个 Relay Transport Address :
Relay = 203.0.113.200:55000 这个地址是公网可达的。对端往这个地址发包, TURN Server 会根据权限和绑定关系,把包转给客户端。
客户端也可以通过 TURN Server 向 peer 发数据。
Client A | | Send Indication / ChannelData v TURN Server Relay Address | v Peer B 从 peer 视角看,它在和 TURN Server 的 relay address 通信。A 的内网地址、 NAT 状态,都被 TURN 藏在后面。
6.3.3 资源生命周期管理 中继资源不是永久分配给你的—— 有生命周期 。
默认生命周期是10分钟(600秒),但服务器可以给一个更短的时间。客户端必须在过期之前发 Refresh Request 来续期。
如果发 Refresh Request 时把 LIFETIME 设为0,就是告诉服务器"不用续了,释放资源"。
生产环境中, TURN 客户端通常每5分钟刷新一次(留点余量,别卡在最后几秒)。
6.4 TURN 的权限管理机制 TURN 是公网中继服务,如果不做权限控制,会被恶意滥用、打流量攻击,所以自带两层权限机制。
6.4.1 权限许可机制 TURN 不是开放代理。它有 Permission 机制。
客户端必须告诉 TURN Server :
允许这个 peer IP 通过 relay 给我发数据。 Permission 是按 peer IP 控制,不是按完整 IP:端口 。它的意义是防止 relay address 变成谁都能打进来的洞。
Permission: Peer IP = 198.51.100.20 Lifetime = 300s 没有 permission 的 peer ,往 relay address 发包, TURN Server 应该丢掉。这个设计很重要。否则 TURN 服务器就是一个昂贵又危险的反射放大工具。
6.4.2 通道绑定机制 TURN 发送数据有两种常见方式: Send Indication 和 ChannelData 。
Send Indication 比较通用,但 STUN 头部开销较大。 ChannelData 更轻。要用 ChannelData ,需要先 ChannelBind 。
Client TURN Server Peer | | | | ---- ChannelBind Request ------> | | | Peer A -> 0x4001 | | | <--- ChannelBind Success ------- | | | | | | ---- ChannelData[0x4001] ------> | ===== data =================> | RFC 5766 规定 ChannelBind 会创建或刷新 channel binding ,同时创建或刷新对应 peer 的 permission ; Channel Number 范围也有约束,比如 0x4000 到 0x7FFE 。
这玩意儿看着细,但高并发音视频里很实用。少几个字节头部,乘上几十万并发流量,就不是小钱了。
6.5 TURN 数据转发路径是 TURN 转发路径很好理解:
A Client <---- NAT-A ----> TURN Server <---- NAT-B ----> B Client 如果 A 和 B 都在 NAT 后面,最稳妥的路径可能是:
A -> TURN -> B B -> TURN -> A 甚至双方都各自使用 relay candidate ,最终路径可能更绕:
A -> TURN-A -> TURN-B -> B ICE 会尽量避免这么贵的路径,但有时没办法。企业网络、防火墙、代理环境一复杂,能通就已经不错了。别嫌丑,能跑的代码有时候就是比漂亮架构值钱。
6.6 TURN 的代价 我做项目时,最头疼的就是 TURN 的成本问题,完全是烧钱的功能。
延迟会增加。原本 A 到 B 可能 30ms,绕 TURN 后可能 60ms、100ms,跨地域更糟。
服务器带宽压力巨大。音视频是双向流量。一个 1Mbps 的视频流, TURN 中继时服务器要收一份、发一份。粗暴算就是 2Mbps 服务器带宽。再乘并发,账单会让财务沉默。
可用性要求也高。 TURN 挂了,原本依赖中继的用户就连不上。它不是边缘小组件,是实时通信系统的生命线之一。
所以生产里要做区域调度、限流、监控、带宽预估、认证、滥用防护。 TURN 不是“ docker run 一下就完事”。能跑 demo ,不代表能扛线上。
6.7 TURN 适用场景 TURN 虽然贵,但有些场景不得不用:
对称型 NAT 。 STUN 搞不定,打洞打不通,只能上 TURN 。
企业防火墙。 很多企业防火墙连 UDP 打洞的包都拦, TURN 用 TCP/443 端口能混过去。
高可靠性要求。 如果你的 SLA 要求连接成功率99.9%以上, TURN 是必须的——作为兜底方案。
移动网络。 运营商级 NAT ( CGNAT )通常是对称型的,移动设备在 4G/5G 网络下经常需要 TURN 。
7. ICE,多候选路径的智能选择 有了 STUN 打洞、 TURN 中继,还需要 ICE 做整合。 ICE 是整套穿透体系的调度大脑。
如果没有 ICE ,我们只能手动尝试直连、中继,效率极低。 ICE 的作用就是 自动收集所有可用路径、自动校验、自动选最优通路、自动优雅降级 。
7.1 ICE 的设计目标与 ICE ,全称 Interactive Connectivity Establishment 。它不是单个传输协议,而是一套候选地址收集、交换、配对、检查、选路的框架。
一句话讲:
ICE = 把所有可能的连接路径列出来,然后用 STUN 检查哪条能通,优先选最好的。 RFC 8445 是 ICE 的核心规范。它定义了 NAT 穿透时如何收集候选、形成 candidate pairs 、执行 connectivity checks 、选择最终可用路径。 RFC 8445 中也明确说,连通性检查是从本地 candidate 向远端 candidate 发送 STUN Binding request 。
你可以把 ICE 想成一个很有耐心的司机。
能走小路直达,就走小路。 小路封了,就走主路。 主路堵了,就上高速。 都不行,就绕远路走收费桥。
这个收费桥就是 TURN 。
7.2 ICE Agent 的三种候选地址类型 ICE 会收集三类候选地址,优先级从高到低排序,优先用最优路径。
7.2.1 Host Candidate:本地网卡地址 Host Candidate 来自本地网卡。
192.168.1.8:50000 10.0.0.9:60000 fe80::xxxx 同一个局域网内, host candidate 可能直接可用。比如两台设备在同一个 Wi-Fi ,甚至不需要 STUN 。
但如果跨公网,私网地址通常没用。你把 192.168.1.8 发给地球另一端的 peer ,它只会一脸无辜。它那边也可能有个 192.168.1.8 ,但不是你。
7.2.2 Server Reflexive Candidate:STUN获取的公网反射地址 Server Reflexive Candidate ,简称 srflx candidate 。它来自 STUN 。
本地地址: 192.168.1.8:50000 STUN 看到: 203.0.113.10:41001 这个 203.0.113.10:41001 就是 srflx candidate 。
它是 UDP 打洞的关键材料。没有它,对端不知道该往哪个公网地址发探测包。
7.2.3 Relay Candidate:TURN分配的中继地址 Relay Candidate 来自 TURN Allocate 。
TURN Relay: 203.0.113.200:55000 它最贵,但最稳。 ICE 不会无脑优先 relay ,因为 relay 延迟高、成本高。通常优先级是:
Host > Server Reflexive > Relay ——甭管什么 NAT 类型、什么防火墙,只要客户端能访问 TURN 服务器,中继就能通。
7.3 候选地址收集流程 Gathering ICE Gathering 就是收集候选。
普通方式是先收集完,再把完整列表通过 SDP 发给对端。这叫常规 ICE 。缺点是慢。尤其 TURN Allocate 、 DNS 查询、网络抖动时,用户会看到连接前卡一下。
Trickle ICE 更聪明。候选一出来,就先发给对端。后续候选继续“滴灌”过去。 RFC 8838 定义的 Trickle ICE 允许候选一旦可用就增量交换,并且可以在候选对形成后立即开始连通性检查,从而加速会话建立。
就像点菜。常规 ICE 是等全桌菜做好,一起端上来。 Trickle ICE 是凉菜先上,热菜边做边上。用户先吃起来,体验就不一样。
7.4 候选地址交换 Signaling A收集完候选地址,得告诉B;B收集完,得告诉A。
这个交换过程通过 信令通道 完成。在 WebRTC 里,信令通道是应用自己实现的( WebSocket 、 HTTP 长轮询、 SIP 等),不是 WebRTC 协议的一部分。
交换的格式通常是 SDP ( Session Description Protocol ),里面用 a=candidate 行描述每个候选地址。
7.5 候选配对 Candidate Pairing A有3个候选,B有3个候选,两两配对就是9对:
• A的 Host 候选 + B的 Server Reflexive 候选 每一对就是一个"候选对"( Candidate Pair )。 ICE Agent 会给每一对算一个优先级,然后按优先级从高到低排序。
7.6 连通性检查 排序完了,开始检查。
7.6.1 检查流程与有序性 ICE 连通性检查,就是对 candidate pair 发 STUN Binding Request 。
Local Candidate ----------------> Remote Candidate STUN Binding Request Local Candidate <---------------- Remote Candidate STUN Binding Response 如果响应回来,这个 candidate pair 就是可用路径候选。
ICE 不会无限并发乱打。它有 check list ,有状态,有 pacing 。否则候选多一点,就会把网络打成烟花。
7.6.2 优先级计算与排序算法 ICE 会给 candidate 和 candidate pair 算优先级。大方向是:
类型偏好 本地偏好 组件 ID 控制角色 Host 通常高于 srflx , srflx 高于 relay 。 IPv6 、 IPv4 、本地接口也可能有不同偏好。具体公式不是本文重点,真要实现协议栈再去啃 RFC 。做业务配置,理解“优先直连,失败再中继”更重要。
不过别手贱乱改优先级。尤其是强制 relay 或禁用 relay ,要知道后果。把 iceTransportPolicy 改成 "relay" ,可以只走 TURN ,但成本会飙。它适合测试 TURN 是否可用,或某些强隐私场景,不适合无脑全量开。
7.6.3 角色机制:Controlling 与 Controlled ICE 会话中双方分两个角色:
• Controlling :负责"做决定"——当多个候选对都能通时,由 Controlling 方选择最终用哪一个 • Controlled :听从 Controlling 方的决定 角色通过 STUN 请求里的 ICE-CONTROLLED 和 ICE-CONTROLLING 属性来协商。谁先发起谁当 Controlling ,一般是这样。
为啥要角色?因为两边都想拍板会乱。总得有一个人说“就这条路吧”。不然 A 选了路 1,B 选了路 2,大家各走各的,场面就像两个后端开会没人写结论。
7.7 候选对的状态机与最终选路 每个候选对经历几个状态:
1. Frozen :还没轮到检查(优先级太低,前面还有更高优先级的在等) 3. In-Progress :正在发 STUN 请求检查 4. Succeeded :收到了 STUN Response ,通了! 当至少一个候选对进入 Succeeded 状态, Controlling 方从中选一个"最佳"的——通常是优先级最高的那个。选完后发一个 Nomination 请求确认。
至此,连接建立完成。
7.8 ICE 的优雅降级策略 总结一句贯穿始终的降级逻辑: 能内网绝不外网,能 P2P 绝不中继 。
同局域网: Host <-> Host 不同 NAT 但可打洞: Srflx <-> Srflx 直连失败: Relay <-> Peer 或 Relay <-> Relay 这比你自己写一堆 if-else 判断 NAT 类型靠谱多了。
工程建议:
默认允许 all candidates 配置可用 STUN 必须配置 TURN 兜底 开启 Trickle ICE 监控 selected candidate pair type 8. WebRTC 中的 ICE 实践 理论讲完,落地到最常用的 WebRTC 场景。所有 WebRTC 音视频通话、屏幕共享、点对点传输,底层全是这套 NAT + STUN + TURN + ICE 逻辑。
8.1 WebRTC 连接建立的全链路 WebRTC 建连流程大概如下:
1. 创建 RTCPeerConnection 2. 添加音视频轨道或 data channel 3. 创建 SDP Offer / Answer 4. 通过信令交换 SDP 5. 收集并交换 ICE Candidate 6. ICE Connectivity Checks 7. 选出可用 candidate pair 8. DTLS 握手 9. SRTP / RTP / RTCP 或 SCTP DataChannel 开始传输 画成环:
Signaling | v ICE Gathering -> Candidate Exchange -> Connectivity Checks | | v v Selected Pair ----------------------> DTLS | v RTP / RTCP / DataChannel ICE 只负责把路打通。媒体安全靠 DTLS-SRTP 。音视频包通常走 RTP/RTCP 。 DataChannel 通常基于 SCTP over DTLS 。
这条链任何一段坏了,用户看到的都是“连不上”。所以排查时别只盯 ICE 。 ICE connected 后没画面,可能是 DTLS 、编解码、权限、轨道、播放器,全都有可能。工程现场就是这么烦人。
8.2 信令服务器的职责 注意! STUN 、 TURN 、 ICE 都不负责信令交互,必须自己搭建轻量信令服务器。
信令服务器是 WebRTC 架构里的"媒人"——帮双方交换信息,但不参与媒体传输。
具体职责:
• 转发 SDP Offer/Answer (包含媒体能力、编解码器、 ICE 候选等) • 转发 ICE Candidate ( Trickle ICE 模式下,候选地址一个个地发) 信令服务器可以用任何技术实现—— WebSocket 、 Socket.io 、 SIP 、 XMPP 都行。 WebRTC 规范不规定信令协议,留给你自己选。
一个极简信令消息:
{ "type" : "candidate" , "roomId" : "room-123" , "from" : "alice" , "to" : "bob" , "candidate" : { "candidate" : "candidate:842163049 1 udp 1677729535 203.0.113.10 41001 typ srflx raddr 192.168.1.8 rport 50000" , "sdpMid" : "0" , "sdpMLineIndex" : 0 } } 8.3 SDP 中的 Candidate 属性解析 一行 candidate 大概长这样:
a=candidate:842163049 1 udp 1677729535 203.0.113.10 41001 typ srflx raddr 192.168.1.8 rport 50000 generation 0 ufrag abc network-id 1 拆一下:
foundation 842163049 component 1 transport udp priority 1677729535 address 203.0.113.10 port 41001 type srflx related address 192.168.1.8 related port 50000 typ host 是本地地址。 typ srflx 是 STUN 反射地址。 typ relay 是 TURN 中继地址。 raddr 和 rport 表示相关地址, srflx 通常会带本地 base 地址, relay 也有自己的关联信息。
排查时重点看:
有没有 srflx 有没有 relay 最终 selected pair 是什么类型 本地和远端端口是否符合预期 如果你只看到 host candidate ,没有 srflx 和 relay ,多半 STUN/TURN 没通,或配置没生效。
8.4 RTCPeerConnection 的 ICE 配置参数 常见配置:
const pc = new RTCPeerConnection ({ iceServers : [ { urls : [ "stun:stun.example.com:3478" ] }, { urls : [ "turn:turn.example.com:3478?transport=udp" , "turns:turn.example.com:5349?transport=tcp" ], username : "webrtc-user" , credential : "webrtc-pass" } ], iceTransportPolicy : "all" , iceCandidatePoolSize : 2 }); iceServers 是 STUN/TURN 列表。 iceTransportPolicy 默认通常是 all ,也就是 host 、 srflx 、 relay 都可用。设为 relay 时,只使用中继候选。 iceCandidatePoolSize 可以预先收集 ICE candidates ,降低后续建连等待。 MDN 提到它默认是 0,设置后可让 ICE agent 在连接尝试前预取候选,从而可能更快连接。
但别把 iceCandidatePoolSize 当神。它会提前消耗资源,也可能让 TURN Allocate 更早发生。移动端尤其要考虑电量、网络切换、后台状态。
8.5 Trickle ICE 与常规 ICE 的差异 常规 ICE :
收集所有候选 -> 放进 SDP -> 发给对端 -> 开始检查 Trickle ICE :
先发 SDP -> 候选出来一个发一个 -> 边收集边检查 WebRTC 里通常通过 onicecandidate 事件把候选发给对端:
pc. onicecandidate = ( event ) => { if (event. candidate ) { signaling. send ({ type : "candidate" , candidate : event. candidate }); } else { signaling. send ({ type : "end-of-candidates" }); } }; 远端收到后:
await pc. addIceCandidate (candidate); MDN 说明 addIceCandidate() 会把远端通过信令通道收到的新 ICE candidate 添加到远端描述中。
Trickle ICE 的体验提升很明显。用户不用等所有候选收集完。 Host candidate 先出来就先试, srflx 后出来继续试, relay 再出来兜底。像赶地铁,能先上车就别在站台等所有朋友买完奶茶。
9. coturn 服务部署与配置 9.1 coturn 项目概述是开源 STUN/TURN 服务器实现 coturn 是现在很常用的开源 STUN/TURN Server 。官方项目介绍里,它是一个免费的开源 TURN 和 STUN Server 实现,可作为 VoIP 媒体流量 NAT traversal server 和 gateway 。
它的优点是成熟、资料多、 Docker 部署方便、 WebRTC 生态常用。缺点也很朴素:配置项多,新人容易配到怀疑人生。
coturn 不是“启动就安全”。默认项、认证、端口范围、 TLS 、日志、带宽控制,都要认真看。尤其公网服务器,别偷懒。
9.2 基础部署方式 源码编译适合你需要定制、调试、打补丁的场景。 Docker 方式更省心,生产环境也推荐用容器编排( Kubernetes )来管理 coturn 集群。
Docker 示例:
docker run -d --name coturn \ --network host \ -v /etc/turnserver.conf:/etc/coturn/turnserver.conf \ coturn/coturn \ -c /etc/coturn/turnserver.conf 如果不用 host 网络,要记得映射监听端口和 relay 端口范围:
docker run -d --name coturn \ -p 3478:3478/udp \ -p 3478:3478/tcp \ -p 5349:5349/tcp \ -p 49160-49200:49160-49200/udp \ -v $( pwd )/turnserver.conf:/etc/coturn/turnserver.conf \ coturn/coturn \ -c /etc/coturn/turnserver.conf 这里最容易漏的是 relay 端口范围。只开放 3478 不够。 TURN Allocate 后真正转发媒体可能用 min-port 到 max-port 范围内的端口。云安全组、防火墙、容器端口映射,都要一起放开。
9.3 核心配置项 一个基础 turnserver.conf :
listening-port=3478 tls-listening-port=5349 listening-ip=0.0.0.0 relay-ip=0.0.0.0 external-ip=203.0.113.200 realm=example.com server-name=turn.example.com lt-cred-mech user=webrtc-user:strong-password fingerprint min-port=49160 max-port=49200 log-file=/var/log/turnserver.log simple-log no-cli listening-port 是 STUN/TURN 监听端口,常用 3478 。 listening-ip 是监听地址。多网卡服务器要谨慎配置。 external-ip 在云主机或 NAT 后部署 coturn 时非常关键。服务器内网 IP 和公网 EIP 不一致时,不配置它,返回给客户端的 relay 地址可能是内网地址。那就尴尬了。 realm 和 user 配合长期凭证认证。 lt-cred-mech 开启 long-term credential mechanism 。 coturn 旧文档也强调 WebRTC 使用 long-term credentials ,需要开启相应选项。 fingerprint 给 STUN 消息加 FINGERPRINT 属性,排查和兼容性更好。 min-port 、 max-port 限制 relay 端口范围,方便防火墙放行和容量管理。 no-tls 、 no-udp 、 no-tcp 这种开关要慎用。你以为自己是在收敛攻击面,可能顺手把用户可用路径砍没了。
coturn 官方 Wiki 里也列了不少运行开关,例如 --stun-only 、 --no-stun 、 --secure-stun 、 --no-cli 等;其中 --server-relay 还被明确标为非标准且危险,不理解就不要用。
9.4 TLS 和 DTLS 加密配置 如果要支持 turns: ,需要 TLS 证书:
tls-listening-port=5349 cert=/etc/ssl/certs/turn.example.com.crt pkey=/etc/ssl/private/turn.example.com.key cipher-list="HIGH" 客户端配置:
const pc = new RTCPeerConnection ({ iceServers : [ { urls : [ "turn:turn.example.com:3478?transport=udp" , "turns:turn.example.com:5349?transport=tcp" ], username : "webrtc-user" , credential : "strong-password" } ] }); TLS/TCP 通常是兜底。某些网络只允许 443 出口,这时可以把 TURN TLS 放在 443 。但如果同机还有 HTTPS 服务,就要做端口规划,或者用独立 IP 。
DTLS 也可以用于 TURN over UDP 的安全传输,但实际部署中, WebRTC 场景更常见的是 UDP TURN 、 TCP TURN 、 TLS TURN 组合。你要根据用户网络决定,不要拍脑袋。
9.5 生产环境运维要点 TURN 运维最核心是带宽。
估算模型很粗:
TURN 出入总带宽 ≈ relay 会话数 × 单路媒体码率 × 2 如果 1000 个用户走 TURN ,每个平均 800kbps:
1000 × 800kbps × 2 = 1600Mbps 这还没算协议开销、峰值、重传、屏幕共享。真上生产,至少按峰值、地域、冗余来估。
监控要看:
allocation 数量 permission 数量 relay candidate 使用比例 入站带宽 出站带宽 包丢失 CPU 内存 端口耗尽情况 认证失败率 区域连接成功率 日志别无脑 debug 。线上高并发 debug 日志能把磁盘写爆。要有采样,要有轮转,要能按用户或 session ID 追踪。
横向扩展可以按区域部署多个 TURN :
turn-cn-east.example.com turn-cn-south.example.com turn-sg.example.com turn-us-west.example.com 客户端按用户地理位置或延迟测速下发 iceServers 。别让上海用户绕到美国 TURN ,除非你想让语音像从月球传回来。
10. 常见故障模式与排查方法 10.1 候选地址收集不完整 现象:
只有 host candidate 没有 srflx candidate 没有 relay candidate 可能原因:
STUN 服务器域名解析失败 UDP 3478 被防火墙拦截 TURN 认证失败 external-ip 配错 relay 端口范围没开放 浏览器策略或客户端配置没生效 排查:
1. 客户端能否解析 STUN/TURN 域名 2. UDP/TCP 3478 是否可达 3. TURN 用户名密码是否正确 4. coturn 日志有没有 Allocate 请求 5. 云安全组是否放行 relay 端口范围 6. WebRTC Internals 里候选是否出现 10.2 ICE 连通性检查超时 现象:
候选收集正常 candidate 已交换 ICE checking 很久 最后 failed 或 disconnected 可能原因:
双方 UDP 被拦 NAT 映射超时太短 双方没有同时探测 信令延迟导致候选过期 STUN Binding Request 被中间网络丢弃 排查可以看 selected pair 是否产生。没有 selected pair ,说明检查没成功。看 candidate pair 状态,如果 srflx-srflx 全 Failed , relay-relay 成功,那就是直连失败但 TURN 兜住了。
如果连 relay 都失败,优先查 TURN 。别继续纠结 NAT 类型。
10.3 仅 STUN 无 TURN 时高失败率场景 只配 STUN ,在这些场景失败率会明显上升:
对称型 NAT 运营商级 NAT 企业网络限制 UDP 校园网出口策略复杂 酒店和机场 Wi-Fi 跨国网络路径不稳定 “我本地可以”没有说服力。开发环境太干净了。你要拿真实用户网络测。至少覆盖:
家庭宽带 4G/5G 公司网络 校园网 公共 Wi-Fi 不同运营商组合 有条件的话,把 NAT 类型、 candidate pair 类型、 ICE 失败原因都打进埋点。别上线后靠用户一句“连不上”猜。猜网络问题很痛苦,像在黑屋里找黑猫,猫还不一定在屋里。
10.4 企业网络、校园网、运营商级 NAT 的特殊问题 企业网络经常有代理、防火墙、 DPI 。 UDP 可能被限。 TCP 也可能只允许 80/443 。某些环境还会做 TLS inspection ,虽然 WebRTC 媒体本身有 DTLS ,但中间策略依旧可能影响连接。
校园网更神奇。不同宿舍楼、不同出口、不同时间段,策略都可能不一样。晚上高峰期丢包像下雨。
运营商级 NAT ,也就是 CGNAT ,会让大量用户共享运营商出口地址。映射行为更复杂,端口资源更紧,超时策略更激进。移动网络切换基站、切 Wi-Fi 、息屏唤醒,也可能导致 ICE 连接断掉再重连。
这就是为什么实时通信系统要支持 ICE restart 。网络换了,旧 candidate pair 可能没用了。不要硬撑。该重协商就重协商。
10.5 排查工具链 10.5.1 Chrome WebRTC Internals 用于查看浏览器内部状态 Chrome 里打开:
chrome://webrtc-internals 有些文章写 about:webrtc-internals , Chrome 里常用的是 chrome://webrtc-internals 。 Firefox 则有自己的 about:webrtc 。
重点看:
ICE connection state ICE gathering state local candidates remote candidates candidate pair selected candidate pair bytesSent / bytesReceived currentRoundTripTime availableOutgoingBitrate packetsLost 这工具看起来丑,但救过我命。线上复现不了的问题,让用户导一份 dump ,常常比十句“你再试一下”有用。
10.5.2 Trickle ICE 独立测试工具用于验证 STUN/TURN 可用性 Trickle ICE 测试页可以输入你的 STUN/TURN 配置,看浏览器能不能收集到 host 、 srflx 、 relay candidates 。
测试逻辑:
输入 iceServers 开始 gathering 观察候选类型 强制 relay 测试 TURN 如果测试页都拿不到 relay candidate ,你的业务代码大概率也拿不到。别甩锅给前端。
10.5.3 coturn 日志与 tcpdump、Wireshark 抓包分析 coturn 日志看:
认证是否成功 Allocate 是否成功 CreatePermission 是否成功 ChannelBind 是否成功 relay 地址和端口 错误码 tcpdump 抓包:
tcpdump -i eth0 udp port 3478 tcpdump -i eth0 udp portrange 49160-49200 Wireshark 可以直接解析 STUN/TURN 。过滤表达式:
stun turnchannel udp.port == 3478 抓包别只抓服务器。客户端、 NAT 前后、服务端三边对比,才能知道包死在哪里。只看一边容易误判。
10.5.4 STUN/TURN 服务连通性测试脚本 用 Node.js 做个简单候选收集测试:
const pc = new RTCPeerConnection ({ iceServers : [ { urls : "stun:stun.example.com:3478" }, { urls : "turn:turn.example.com:3478?transport=udp" , username : "webrtc-user" , credential : "strong-password" } ] }); pc. createDataChannel ( "test" ); pc. onicecandidate = ( e ) => { if (e. candidate ) { console . log (e. candidate . candidate ); } else { console . log ( "gathering complete" ); pc. close (); } }; ( async () => { const offer = await pc. createOffer (); await pc. setLocalDescription (offer); })(); 想强测 TURN :
const pc = new RTCPeerConnection ({ iceTransportPolicy : "relay" , iceServers : [ { urls : "turn:turn.example.com:3478?transport=udp" , username : "webrtc-user" , credential : "strong-password" } ] }); 如果 relay candidate 出不来,别继续谈业务建连。基础设施先修。
11. STUN、TURN 与 ICE 的技术选型 11.1 三种方案的核心对比包括穿透能力、延迟、带宽成本、部署复杂度 来一张工程视角的对比:
STUN 便宜,但不保证通。 TURN 贵,但能兜底。 ICE 是调度大脑,把 host 、 srflx 、 relay 候选组织起来,自动选路。
不要问“我该用 STUN 还是 TURN ”。生产里通常是:
ICE + STUN + TURN 区别只在于 TURN 使用比例要控制,而不是完全不用。
11.2 选型决策矩阵 11.2.1 公网或完全锥型 NAT 环境下 STUN 即可 如果双方都有公网 IP ,或者 NAT 很开放, STUN 获取 srflx 后直连成功率高。
适合:
内网测试 同云 VPC 特定网络 可控设备网络 部分家庭宽带 但公网环境也要注意安全。直接暴露端口不是没有风险。 P2P 应用必须做好认证、加密、权限控制。别以为“只是 UDP ”就没人打你。
11.2.2 受限锥型 NAT 环境下使用 STUN 加 UDP 打洞 这是最经典的 P2P 穿透路径。
STUN 获取公网反射地址 信令交换候选 双方同时 UDP 探测 ICE 检查成功 业务流量直连 适合:
实时语音 视频通话 P2P 游戏 远程协作 文件点对点传输 要点是保持同一个 socket 、控制探测节奏、做好 keepalive 、监控失败率。
11.2.3 对称型 NAT 和企业防火墙下必须启用 TURN 兜底 对称型 NAT 下, STUN 反射地址可能只对 STUN Server 有效。企业防火墙下, UDP 可能被干掉。别硬刚。
配置建议:
const pc = new RTCPeerConnection ({ iceServers : [ { urls : "stun:stun.example.com:3478" }, { urls : [ "turn:turn.example.com:3478?transport=udp" , "turn:turn.example.com:3478?transport=tcp" , "turns:turn.example.com:5349?transport=tcp" ], username : "user" , credential : "pass" } ] }); UDP TURN 优先。 TCP/TLS TURN 兜底。别只配一种路径。真实网络很会打脸。
11.2.4 对连接成功率有 SLA 要求时 ICE 配置中强制包含 TURN 如果你做的是在线会议、远程客服、云游戏、远程桌面、在线教育,连接成功率就是核心指标。
这种场景必须包含 TURN 。甚至要做多 TURN 区域、多运营商线路、自动调度。
SLA 不是 PPT 上写出来的,是靠兜底路径和监控堆出来的。说难听点,用户不关心你遇到了什么 NAT 。他只知道会议进不去,老师听不到,客户掉线了。
11.3 TURN 服务器的安全与成本控制依赖认证鉴权、流量限额和 IP 白名单 TURN 安全不要省。
必须开认证。长期账号密码可以,动态临时凭证更好。 WebRTC 常见做法是业务服务端按用户生成短期 TURN credential :
username = timestamp:userId password = HMAC(secret, username) ttl = 1 hour coturn 支持 REST API 风格的临时凭证机制。这样即使凭证泄露,也只能短时间使用。
还要限制:
总带宽 单用户带宽 单 IP allocation 数 端口范围 可访问 peer 范围 日志告警 异常流量封禁 IP 白名单要谨慎。用户来自动态网络,白名单太死会误伤。但管理接口、运维端口必须只允许内网或堡垒机访问。 coturn 的 CLI 管理也要注意,官方 Wiki 提到默认 CLI 可在 127.0.0.1 的 5766 端口接受 telnet 连接,生产里通常会关闭或严格保护。
11.4 成本估算模型按并发用户数与平均会话时长推算 TURN 带宽需求 估算从三个数开始:
峰值在线会话数 走 TURN 的比例 平均码率 公式:
TURN 峰值带宽 ≈ 会话数 × TURN 比例 × 单会话双向码率 × 2 假设:
峰值 10000 人 两人一通话,所以 5000 路会话 TURN 比例 20% 单端上行码率 800kbps 估算:
5000 × 20% × 800kbps × 2 = 1600Mbps 如果是多人会议 SFU 架构,模型更复杂。 TURN 可能只负责客户端到 SFU 的可达性,不一定是 P2P 两端互通。但只要媒体经过 TURN ,服务器带宽就是真金白银。
成本控制建议:
优化 ICE,让能直连的尽量直连 区域就近分配 TURN 控制视频码率上限 弱网下降级音频优先 监控 relay 使用比例 异常用户限流 别把 TURN 当 CDN 用。它不是为廉价大流量分发设计的。它是兜底,是救命绳。救命绳可以贵,但不能滥用。
12. 总结 12.1 NAT、STUN、TURN、ICE 的关系总览 把整篇文章压成一张图:
附录 A,关键 RFC 文档索引 附录 B,公共 STUN/TURN 测试服务器列表 注意: 公共 STUN 只适合测试, 生产环境请自建或使用云厂商服务。