Cloudflare TURN 实战:WebRTC 凭证管理与 ICE 重启的 5 个关键点
2026/9/15 13:43:24 网站建设 项目流程

Cloudflare TURN 实战:WebRTC 凭证管理与 ICE 重启的 5 个关键点

【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills

凌晨三点,值班被叫醒:一通 48 小时的长通话,中继流量在整点归零,ICE 状态卡死在 failed。排查半天,原因并不玄乎——TURN 凭证刚好过期。Cloudflare 临时凭证的 TTL 上限就是 172800 秒,到期那一刻,连接没有缓冲、没有告警,直接断。

开源仓库 skills(Skills Catalog for Codex)里的 Cloudflare TURN 生产实现模式,把凭证管理、ICE 重启、连接排查这条链路的坑全踩过了。这里按一条主线拆给你:先建判断框架,再跑通最小链路,然后处理掉线恢复,最后过一遍上线检查。

⚠️ 30 秒决策地图:端口与传输策略怎么选

写代码之前先花 30 秒定策略,后面所有代码都只是把策略翻译成配置:

场景端口优先级iceTransportPolicy为什么这么选
视频会议 / P2P 通话3478/udp 打头all直连零成本、延迟最低,中继只当兜底
公司网封了 UDP3478/tcpallTCP 握手机率比 UDP 穿透高
严格企业防火墙443、5349(turns)all443 基本不会被拦,TLS 还顺带加密
IoT / 弱网设备3478/udp 打头relay连通性优先于成本,强制走中继
屏幕共享等多路流任意all + max-bundle多路流聚合成一条通道,省开销

这里有个坑:API 返回的 URL 列表里混着:53:80两个地址,它们在非浏览器端合法可用,但 Chrome / Firefox 会直接拦截 53 端口流量——relay 候选悄悄缺失,控制台没有任何报错。所以过滤必须放在服务端做,浏览器端只负责用。

成本上多一句:搭配 Cloudflare Calls SFU 使用时 TURN 免费,单独用按 $0.05/GB 出站计费,这也是iceTransportPolicy默认用all而非relay的原因之一。

最小可运行链路:从建 Key 到浏览器拿到 iceServers

后端签发:密钥永远留在服务端

先创建 TURN Key,拿到只在创建时返回一次的密钥:

POST /accounts/{account_id}/calls/turn_keys # body: { "name": "prod-turn" } # 响应里的 key 字段只在创建时出现,必须当场存进 secrets

注意key字段不会二次下发,丢了只能删掉重建。

然后是 Worker 侧的签发逻辑,核心是"代签 + 过滤"两件事:

export default { async fetch(req: Request, env: Env) { const res = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${env.TURN_KEY_ID}/credentials/generate`, { method: 'POST', headers: { Authorization: `Bearer ${env.TURN_KEY_SECRET}` }, body: JSON.stringify({ ttl: 3600 }) } ); const { iceServers: s } = await res.json(); // 浏览器会静默丢弃 :53 的地址,服务端先过滤再下发 const urls = s.urls.filter(u => !u.includes(':53')); return Response.json({ urls, username: s.username, credential: s.credential }); } };

细节在expiresAt的取法上:缓存写入时留 1 分钟提前量(ttl * 1000 - 60000),给刷新窗口留余地,别等凭证真过期了才动手。完整端点契约见 凭证 API 参考,需要缓存未过期凭证可参考 Worker 集成配置。

浏览器端:iceServers 的正确写法

浏览器只做一件事——从自己的后端拿现成的 iceServers:

const r = await fetch('/api/turn-credentials'); const { urls, username, credential } = await r.json(); const iceServers = [ { urls: 'stun:stun.cloudflare.com:3478' }, // STUN 只负责发现公网候选 { urls, username, credential, credentialType: 'password' } // TURN 负责直连失败时兜底 ]; const pc = new RTCPeerConnection({ iceServers });

关键设计是 STUN 和 TURN 同时给:STUN 便宜快,TURN 兜底,由 ICE 协商自动择优。千万别在前端拼 key secret 直接打生成端点——密钥一旦进浏览器 bundle,等于公开。

连接失效与恢复:48 小时掉线、ICE failed、网络切换

凭证过期、ICE 进入 failed、移动网络切换,三种诱因,恢复路径只有一条:换凭证 → 重启 ICE → 重发 offer。分开处理只会写出三份重复逻辑。

TURN 凭证 48 小时过期怎么办:提前一分钟热替换

// 到期前热替换 iceServers,而不是等断了再补救 const fresh = await fetch('/api/turn-credentials').then(r => r.json()); const cfg = pc.getConfiguration(); cfg.iceServers = [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: fresh.urls, username: fresh.username, credential: fresh.credential, credentialType: 'password' } ]; pc.setConfiguration(cfg);

这里有个最反直觉的点:setConfiguration()只换配置,不触发 ICE 重启。连接还活着时它静默生效;连接已经 failed 时它什么都不改变——必须配合下一节的重启组合拳。定时刷新间隔用ttl * 1000 - 60000,TTL 是 1 小时就每 50 分钟跑一次。

ICE 重启组合拳,以及如何确认流量真的走了中继

// failed 和 disconnected 都触发,防移动网络切换时的假性掉线 pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState !== 'failed' && pc.iceConnectionState !== 'disconnected') return; await refreshCreds(pc); // 先换新凭证 const offer = await pc.createOffer({ iceRestart: true }); // 带重启标志重建 offer await pc.setLocalDescription(offer); signal.send(offer); // 经信令通道发给对端 });

注意顺序不能换:先换凭证再重启,否则 ICE 拿着过期凭证重试,白烧一次协商。

恢复后想确认流量到底走直连还是中继,看选中候选对即可:

const stats = await pc.getStats(); stats.forEach(rep => { // selected 为 true 的候选对才是当前实际承载流量的 if (rep.type === 'candidate-pair' && rep.selected) { console.log('当前选中:', rep.localCandidateId); } });

如果候选对里迟迟不出现 relay 类型,多半是 53 端口过滤漏了或者防火墙没放行 3478/5349/443。48 小时掉线的完整排查思路见 常见陷阱与排查,更完整的恢复流程变体见 实现模式参考。

✅ 上线前加固:生产检查清单

单分配限额与高频错误对照

限额是按用户分配而非账户级,超了不报错、只丢包:

维度限额超限后果
新 IP 出现速率每秒 >5 个丢包
包速率入/出 5–10k pps丢包
数据速率入/出 50–100 Mbps丢包

高频翻车姿势与修正:

错误写法后果修正
ttl: 604800申请 7 天凭证API 直接拒绝改成 86400,48 小时是硬顶
iceServers 里写死 IPIP 变更(提前 14 天通知)后全线掉线turn.cloudflare.com域名
53 端口 URL 原样下发浏览器 relay 候选静默缺失服务端过滤后再返回
状态 failed 只打日志长通话无法自救刷新凭证 + 带 iceRestart 重建 offer
密钥硬编码进前端凭证生成端点被任意刷只有后端能调用生成端点

防火墙白名单与协议边界

严格防火墙环境需要给turn.cloudflare.com配白名单时,用dig turn.cloudflare.com A/dig turn.cloudflare.com AAAA定期核对,IP 变更有 14 天通知窗口,监控脚本要在这个窗口内更新白名单,细节在 配置指南。

协议边界两条,写进架构文档里免得以后背锅:

  • 中继分配的地址只有 IPv4(无 RFC 6156),IPv6 客户端能接入但中继流量走 IPv4;TCP 中继(RFC 6062)同样不支持
  • TLS 1.1/1.2/1.3 全支持,企业侧优先放行 443 与 5349 上的 TURN over TLS

上线前把这份清单过一遍:

  • 密钥只存 wrangler secrets,签发端点仅后端可达
  • 签发凭证前先做客户端认证,端点有限流
  • TTL ≤ 48 小时,且覆盖预期通话时长
  • 浏览器端 URL 已过滤 53 端口
  • 被攻陷会话可走吊销端点(计费即停、连接数秒内断开)
  • 用了 IP 白名单就有 DNS 监控,14 天窗口内能更新
  • 单用户中继流量预算在 5–10k pps / 50–100 Mbps 内
  • 搭配 Calls SFU 的场景确认走了免费 TURN 路径

延伸阅读

  • TURN 服务概览:地址与端口清单
  • Calls SFU 模块:TURN 与 SFU 自动协调
  • RealtimeKit 集成参考

【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询