跳到主要内容

企业网与移动网穿透

企业网与移动网穿透

公网理想环境下 ICE 几乎从不失败,真实世界里最难啃的是两块场景:企业网络(严苛防火墙、对称 NAT、TLS 拦截)和移动网络(CGNAT、4G/5G/Wi-Fi 切换、IPv6 双栈)。这篇文档把这些环境的失败模式、检测方法和应对策略一次说清。

不重述 NAT 类型(Full Cone / Restricted Cone / Symmetric 这些 STUN RFC 3489 的旧术语已被弃用),只讲真实网络下行为差异对 ICE 选路的影响

1. 真实网络中 ICE 失败的分布

按 Google、Twilio、声网公开过的统计数据(2023-2025)综合来看:

失败场景全球占比应对成本
对称 NAT(含企业 NAT)8-15%必须 TURN
企业防火墙阻塞 UDP5-12%TURN over TLS/443
CGNAT(运营商级 NAT)3-8%必须 TURN
Wi-Fi/4G 切换未做 ICE Restart3-6%客户端侧实现
TLS 中间人代理(公司 SSL 拦截)1-3%极难,部分场景无解
IPv6-only 网络无 v4 回退< 1%双栈 STUN/TURN

这就是为什么"只配 STUN"的方案在生产里一定会出问题——上面任何一项命中,都需要 TURN 兜底。

2. 对称 NAT 与端口预测

经典 NAT 行为分类已经被 RFC 4787(BEHAVE)取代。两个工程上重要的维度:

维度行为 A行为 B
Mapping(同一内网源对不同外网目的)Endpoint-Independent(同一公网映射)Address/Port-Dependent(每个目的不同映射)
Filtering(外部能否回包)Endpoint-Independent(任何外部都能回)Address/Port-Dependent(只有刚连过的能回)

WebRTC 圈说的"对称 NAT"=Address-and-Port-Dependent Mapping同一台机器访问不同公网目的时分到不同源端口。这种 NAT 后,STUN 给出的映射对真实媒体路径无效——因为 STUN 服务器和对端是不同目的地,NAT 给的端口不一样。

本机 192.168.1.10:50000 → STUN 服务器 → NAT 映射成 203.0.113.5:40001(srflx)
本机 192.168.1.10:50000 → 对端公网 IP → NAT 映射成 203.0.113.5:40002(实际!)

对端按 srflx 发包到 40001 → 丢

2.1 端口预测(Port Prediction)

学术界提出过通过多次 STUN 探测预测 NAT 下次会分配的端口。WebRTC 没实现,coturn 也没实现。原因:

  • 现代企业 NAT 用随机或伪随机端口分配
  • 即使预测对,还要赌对端发包"恰好"是这一刻
  • TURN 中继成本远低于运维一套预测系统

结论:对称 NAT 后的客户端 = 必走 TURN。relay-relay 候选对会成为 selected pair。

2.2 用 STUN 测自己的 NAT 行为

# coturn 自带的 NAT 行为发现工具(RFC 5780)
turnutils_natdiscovery -m -f stun.l.google.com
turnutils_natdiscovery -p -f stun.l.google.com

# 输出可能:
# Mapping behavior is: Address and Port Dependent ← 对称 NAT
# Filtering behavior is: Address and Port Dependent

部署期摸清自己机房 / 客户群的 NAT 行为分布很有用,决定 TURN 实例容量规划。

3. CGNAT(运营商级 NAT)

移动网络(4G/5G)和家用宽带越来越多走 CGNAT:运营商在自己的边界做一层 NAT,用户分到的 100.64.0.0/10(共享地址段)才是"真公网"。CGNAT 的特点:

  • 行为基本是对称的(不同目的不同映射)
  • NAT 表项寿命短(部分运营商 30-60 秒就回收 UDP 表项)
  • 同一运营商两台手机互相访问可能 hairpin 失败(NAT 不支持回环)

工程影响:

  1. 同运营商两个手机直连可能反而比走 TURN 慢,因为运营商 hairpin 不可靠。
  2. UDP NAT 表项 30 秒回收 → 必须发心跳保活。WebRTC 自带的 STUN consent freshness 每 5 秒一次,正好压制这个问题。
  3. 关掉省电模式 / 后台进程时表项更短,需要监听 pageshow / freeze 事件主动 ICE Restart。

4. 企业防火墙的三种"恶意度"

等级行为WebRTC 应对
1 级(90%)开放 80/443 TCP,封 UDPTURN over TLS 443
2 级(8%)开放 80/443,深度包检测 SNITURN-TLS 走真实域名,避免 SNI 异常
3 级(< 2%)TLS 中间人代理(自签 CA)几乎无解,建议提示用户切换网络

4.1 检测当前网络等级

启动时跑一次"网络体检",决定走哪条路:

async function probeNetwork() {
const tests = [
{ url: 'stun:stun.example.com:3478', label: 'stun-udp' },
{ urls: 'turn:turn.example.com:3478?transport=udp', username, credential, label: 'turn-udp' },
{ urls: 'turn:turn.example.com:3478?transport=tcp', username, credential, label: 'turn-tcp' },
{ urls: 'turns:turn.example.com:443?transport=tcp', username, credential, label: 'turn-tls' },
];

const results = await Promise.all(tests.map(async (server) => {
const pc = new RTCPeerConnection({ iceServers: [server], iceTransportPolicy: 'all' });
pc.addTransceiver('audio');
await pc.setLocalDescription();

return new Promise((resolve) => {
const seen = new Set();
const timeout = setTimeout(() => { pc.close(); resolve({ label: server.label, types: [...seen] }); }, 5000);
pc.onicecandidate = ({ candidate }) => {
if (!candidate) {
clearTimeout(timeout); pc.close();
resolve({ label: server.label, types: [...seen] });
return;
}
seen.add(candidate.type);
};
});
}));

return results;
}

// 上报到日志,统计各级网络分布
console.log(await probeNetwork());
// [
// { label: 'stun-udp', types: ['host', 'srflx'] }, ← 1 级以下
// { label: 'turn-udp', types: ['host', 'srflx', 'relay'] },← UDP 通
// { label: 'turn-tcp', types: ['host', 'relay'] }, ← UDP 不通但 TCP 通
// { label: 'turn-tls', types: ['host', 'relay'] }, ← 2/3 级,靠 TLS
// ]

线上把这份诊断埋点上报,能精准看出"丢失"用户卡在哪一层网络。

4.2 SSL 拦截(3 级)的特征

公司在出口部署 SSL 解密代理时:

  • 客户端看到的证书 issuer 是公司自签 CA
  • TURN-TLS 握手时浏览器会拒绝(除非系统装了那张 CA)
  • 即使装了 CA,代理重新打包的 TLS 流可能改写 ALPN,TURN 协议层失败

这种环境基本无解。一些做法:

  • 配合 IT:在代理白名单里把 TURN 域名加上"绕过解密"
  • 提示用户切到手机热点
  • 极端情况降级到电话拨入(PSTN 网关)

5. IPv6 双栈

国内运营商 IPv6 普及率已超过 70%(2025 年),但企业网络很多仍是 v4-only,CDN/TURN 也不全部双栈。三种典型场景:

客户端 A客户端 B是否能直连备注
v4v4经典路径
v6v6性能最好,无 NAT
v4v6必须中继
v4-only TURNv6 客户端TURN 必须双栈

5.1 配置 coturn 双栈

listening-ip=0.0.0.0
listening-ip=::
relay-ip=0.0.0.0
relay-ip=::
external-ip=203.0.113.10
external-ip=2001:db8::1

DNS 同时配 A / AAAA 记录,浏览器会同时探测两条路径并取最快的(Happy Eyeballs,RFC 8305)。

5.2 客户端 IPv6 候选

启用 IPv6 后浏览器会同时收集 v4 和 v6 的 host / srflx 候选。收集时间会变长——每个 STUN 服务器都要在两个协议族上各跑一次。

// 限制为 v4-only(特殊场景,如某些老 SFU 不支持 v6)
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun4.example.com' }],
// 没有官方 API 强制禁 v6,只能不配 v6 STUN
});

6. 网络切换:4G ↔ Wi-Fi

移动场景核心痛点。切换瞬间发生的事:

  1. 操作系统给应用一个新的 IP(默认路由变化)。
  2. 旧 IP 上的 socket 还在,但发包黑洞。
  3. ICE 检查失败 → iceConnectionState = disconnected → failed
  4. 不做 ICE Restart 的话,通话直接挂。
// iOS / Android WebView / 桌面浏览器都支持 NetworkInformation API
navigator.connection?.addEventListener('change', () => {
if (pc.connectionState === 'connected' || pc.connectionState === 'disconnected') {
// 短延迟,等 OS 稳定
setTimeout(() => pc.restartIce(), 1500);
}
});

// 兜底:监听 ICE failed
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'failed') pc.restartIce();
};

iOS Safari 的 NetworkInformation API 不完整,靠 online/offline 事件不够。最稳的兜底是 iceConnectionState = failed 触发 restart。

6.1 切换期间的体验优化

ICE Restart 至少需要 1-3 秒,期间用户看到画面卡死。三种处理:

  1. 静音不停画:保留最后一帧让用户感知"在重连"。
  2. 音频优先:Restart 期间降级到只有音频,画面恢复后再升码率。
  3. 预热第二条链路:高级方案,同时维护两个 PC(4G + Wi-Fi 各一),切换时只换主链路指针。代价是带宽 ×2,仅适合极端高可用场景。

绝大多数产品做第 1 种就够。

7. 移动端的端口寿命与心跳

WebRTC 自带 STUN Consent Freshness(RFC 7675):每 5 秒发一次 STUN Binding 给对端,确保路径仍然通。这同时也给 NAT 表项续命。

但有两种情况心跳失效:

  • 应用切到后台,浏览器节流定时器(Chrome 后台 1 分钟一次)
  • 屏幕锁定时 iOS Safari 直接挂起整个 WebView

后台时 NAT 表项过期 → 回到前台后必然 failed → 必须 restart。监听 visibilitychange

document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && pc.connectionState === 'connected') {
// 检查实际是否还能通:拿一份 stats,看 RTT 是否合理
pc.getStats().then(stats => {
const pair = [...stats.values()].find(r =>
r.type === 'candidate-pair' && r.nominated && r.state === 'succeeded'
);
if (!pair || Date.now() - pair.lastPacketReceivedTimestamp > 5000) {
pc.restartIce();
}
});
}
});

8. 企业代理与 HTTP CONNECT

少数极端环境只允许走 HTTP/HTTPS 代理。WebRTC 标准里 TURN 不支持 HTTP CONNECT 代理。变通方案:

  • Chrome 启动参数 --proxy-server + --force-webrtc-ip-handling-policy=default_public_interface_only 让 WebRTC 也走代理
  • 部分企业部署"WebRTC 网关",把流量转成 WebSocket 包传出
  • LiveKit / Twilio 等商用方案有专门的"代理友好"模式

这类需求 5 年前还有很多,现在基本都靠 TURN-TLS-443 解决了。如果客户提出"我们公司必须走 HTTP 代理",先确认 443 真的封死再考虑代理方案。

9. 完整的"分层降级"建链策略

生产代码不能只配一个 TURN,必须分层降级。优先级从高到低:

const iceServers = [
// 第 1 层:直连(host 候选)—— 最快,0 中继成本
{ urls: 'stun:stun.example.com:3478' },

// 第 2 层:UDP 中继 —— 大多数 NAT 后能用
{ urls: 'turn:turn-cn.example.com:3478?transport=udp', username, credential },
{ urls: 'turn:turn-us.example.com:3478?transport=udp', username, credential },

// 第 3 层:TCP 中继 —— UDP 被防火墙的环境
{ urls: 'turn:turn-cn.example.com:3478?transport=tcp', username, credential },

// 第 4 层:TLS/443 —— 严格企业网络
{ urls: 'turns:turn-cn.example.com:443?transport=tcp', username, credential },
{ urls: 'turns:turn-us.example.com:443?transport=tcp', username, credential },
];

const pc = new RTCPeerConnection({
iceServers,
iceTransportPolicy: 'all',
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
});

浏览器会并行尝试所有 server,按优先级和到达顺序选 selected pair。第一个能用的胜出。

10. 调试工具速查

工具用途在哪用
chrome://webrtc-internals看候选、selected pair、ICE 状态Chrome / Edge
about:webrtc同上Firefox
turnutils_natdiscovery测自己的 NAT 行为服务端 / 用户机器
turnutils_uclient测 TURN 是否可用服务端
trickle-ice 网页一键看候选类型用户浏览器
tcpdump 'udp port 3478 or tcp port 5349'抓 STUN/TURN 包服务端
mtr &lt;peer-ip>看链路质量服务端

11. 反模式

反模式后果替代
只支持 v4部分 v6-only 环境失败TURN 双栈
4G/Wi-Fi 切换重建 PCDTLS 重新握手,体验差restartIce
后台时主动 close PC切回前台要全流程重建用 visibilitychange + restartIce
用 STUN 探测 NAT 类型决定是否走 TURNNAT 行为可能突变(运营商重启边界)永远配 TURN,让 ICE 自动选
一个 TURN 实例打天下跨大洲延迟高多区域部署 + 客户端按地理选
不上报真实 selected pair 类型出问题不知道用户卡在哪层通话结束上报 candidateType
TURN 长期凭据被滥用当代理短期 HMAC 凭据

12. 权威资料