Cloudflare TURN 实战指南:WebRTC 中继接入、50 分钟凭证续期与 ICE 重启排障
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
如果你上线过 WebRTC 通话应用,大概都收到过这种工单:"我电脑上好好的,到公司就没声了"。Cloudflare TURN 就是为这类问题准备的逃生通道——运行在 Cloudflare 全球 anycast 网络(310+ 城市,同一 IP 由就近机房应答,但不覆盖中国网络)上的托管中继。读完这篇,你能直接落地一套完整的 TURN 接入:Key 创建、凭证签发、端口取舍、续期与重启,一步不缺。
直连为什么会断:企业网络挡不住的东西
WebRTC 默认先尝试 P2P 直连,但有三类场景会把它拦死:
- 对称型 NAT:NAT 给每个目的方向分配不同映射,双方都猜不到对方的公网地址;
- 企业防火墙:整段封掉 UDP,或者不放行 3478 这类 WebRTC 常用端口;
- 运营商级 NAT 加网络切换:手机从 WiFi 切到蜂窝,公网 IP 变了,原本走通的路径瞬间失效。
直连走不通时,TURN 顶上:双方把流量都交给中继转发,牺牲一点时延换"一定能通"。所以 TURN 不是首选路径,而是兜底——这个定位直接决定了你后面写配置的方式。
端到端走通一次接入:从 Key 创建到 iceServers 交付
第一步:用 Cloudflare API 建一把 TURN Key
所有端点都需要带 "Calls Write" 权限的 API Token,Base URL 为https://api.cloudflare.com/client/v4:
POST /accounts/{account_id}/calls/turn_keys Content-Type: application/json { "name": "prod-turn-key" }响应里有uid、name、created、modified和key。注意一点:key(真正的密钥)只在创建时返回一次,拿到就存进密钥保管处,丢了只能删了重建。
后续管理靠四个操作:GET /accounts/{account_id}/calls/turn_keys(列出全部)、GET .../turn_keys/{key_id}(单个详情)、PUT .../turn_keys/{key_id}(改名)、DELETE .../turn_keys/{key_id}(删除)。
让 Worker 来签发临时凭证
⚠️ 密钥永远不要进浏览器。正确链路是:浏览器请求你自己的后端,后端(一个 Cloudflare Worker)持密钥去调凭证生成端点,只把临时凭证吐回去:
POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/generate Authorization: Bearer {key_secret} Content-Type: application/json { "ttl": 86400 }响应里和你有关的是三个字段:iceServers.urls(STUN 加多协议 TURN 地址的混合列表)、username(形如1738035200:user123)、credential(Base64 编码的 HMAC)。
Worker 侧的环境变量里,TURN_KEY_ID不敏感,可以放 wrangler.jsonc 的vars;TURN_KEY_SECRET用wrangler secret put TURN_KEY_SECRET单独注入。生产环境还可以绑定一个 KV 命名空间(例如CREDENTIALS_CACHE)做凭证缓存。
把 53 端口挡在服务端
生成端点返回的 urls 里夹着turn:turn.cloudflare.com:53?transport=udp和turn:turn.cloudflare.com:80?transport=tcp这类地址。它们对非浏览器客户端没问题,但Chrome 和 Firefox 会硬拦 53 端口的流量——浏览器侧不会报错,只会静默连不上。所以过滤必须放在服务端、在返回之前完成:
const clean = raw.urls.filter(u => !u.includes(':53')); return Response.json({ iceServers: [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: clean, username: raw.username, credential: raw.credential } ] });剩下的地址在浏览器里的尝试顺序:
3478/udp——首选,延迟最低;3478/tcp——UDP 被封时的回退;5349/tcp(turns:)——企业防火墙场景最可靠;443/tcp(turns:)——备选 TLS 端口,防火墙友好。
STUN(stun:stun.cloudflare.com:3478)始终保留:它负责发现公网候选,直连失败时再由 TURN 接管。两者一起塞进RTCPeerConnection,让 ICE 协商自己择优:
const res = await fetch('/api/turn-credentials'); const { urls, username, credential } = await res.json(); const iceServers = [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls, username, credential, credentialType: 'password' } ]; const pc = new RTCPeerConnection({ iceServers });让长连接活下去:50 分钟续期 + ICE 重启
凭证提前一分钟续
TTL 上限是172800 秒(48 小时),超过会被 API 直接拒绝;仓库示例里常用 3600 秒。凭证一过期连接就断,所以长通话要有续期定时器,推荐间隔是ttl * 1000 - 60000——提前 1 分钟留出安全窗口:
const refreshEvery = ttl * 1000 - 60000; // ttl=3600 → 50 分钟 setInterval(async () => { const cfg = pc.getConfiguration(); cfg.iceServers = await fetchFreshServers(); pc.setConfiguration(cfg); }, refreshEvery);有个坑:setConfiguration()不会触发 ICE 重启,它只是把连接上的 iceServers 换掉。如果连接已经坏了,光换凭证没用,得配合下一节的重启流程。
服务端还可以再垫一层缓存:一个持有凭证的 Manager 类,未过期就直出缓存,过期才去打生成端点。三个细节别漏:expiresAt = now + ttl*1000 - 60000;53 端口的过滤在写入缓存时做一次;ttl > 172800的防御性校验与 API 约束保持一致。需要立刻掐掉某个会话时,调POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/revoke(body 传{"username": "..."},返回 204):计费立即停止,活跃连接数秒内断开。
状态到 'failed' 时,光打日志不算处理
需要触发 ICE 重启的场景有四个:TURN 服务器维护(Cloudflare 网络上偶尔发生)、anycast 路由调整、超过 1 小时的长会话做凭证刷新、连接失败。状态进failed或disconnected时依次做四件事:
pc.addEventListener('iceconnectionstatechange', async () => { if (['failed', 'disconnected'].includes(pc.iceConnectionState)) { await refreshCreds(pc); // 1. 先换新凭证 pc.restartIce(); // 2. 触发重启 const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); // 3. 生成带 iceRestart 的 offer // 4. 通过信令通道把 offer 发给对端 } });把disconnected也纳入恢复条件很关键——移动网络切换经常先表现为 disconnected,只盯failed会漏掉一半场景。
排障:流量到底走的直连还是中继
排查"通话为什么没走中继"时,盯三个信号:
icecandidate事件:记candidate.type(host/srflx/relay)和candidate.protocol,确认有没有出现 relay 候选;iceconnectionstatechange:跟踪checking → connected → completed(或failed)的流转;getStats()里的candidate-pair报告:selected为 true 的条目就是当前真正在用的那条路径:
pc.getStats().forEach(r => { if (r.type === 'candidate-pair' && r.selected) { console.log('selected path:', r.protocol); } });如果连接建立得慢,按这个顺序查:候选收集是否完整、客户端到 Cloudflare 边缘的网络延迟、防火墙有没有放行 3478 / 5349 / 443;企业网络里可以直接改用 443 上的 TURN over TLS。
踩坑与限额速查
高频错误对照
| ❌ 你写成了 | ✅ 应该这样 |
|---|---|
ttl: 604800(7 天) | ttl: 86400(24 小时);超 48 小时直接拒 |
硬编码turn:141.101.90.1:3478 | 用域名turn:turn.cloudflare.com:3478;IP 变更有 14 天通知期 |
浏览器端保留:53URL | 服务端过滤!u.includes(':53') |
| 凭证到期不续 | setInterval提前 1 分钟刷新 |
failed时只打日志 | 刷新凭证 +restartIce() |
| TURN_KEY_SECRET 塞进前端 | 服务端生成凭证,客户端只打你的接口 |
单分配限额(按用户,不是按账户)
| 维度 | 阈值 | 触线后果 |
|---|---|---|
| 新唯一 IP 数 | >5 个/秒 | 丢包 |
| 包速率 | 入/出 5-10k pps | 丢包 |
| 数据速率 | 入/出 50-100 Mbps | 丢包 |
出现高丢包时,先对这三行自查一遍,再怀疑网络质量。
收尾:成本、安全与网络边界
成本怎么算:搭配 Cloudflare Calls SFU(选择性转发单元,托管媒体流转发服务)使用时 TURN 免费——SFU 需要时自动启用,客户端不用手动编排两者协调。否则按出站流量$0.05/GB计费。这也解释了为什么省成本要默认iceTransportPolicy: 'all'(先试直连,失败才中继),只有 IoT 这类要连通性可预期的场景才强制'relay';屏幕共享则用bundlePolicy: 'max-bundle'把多路媒体流聚合到单条传输上降开销。
上线前安全清单:
- 凭证只在服务端生成,密钥绝不下发
TURN_KEY_SECRET放 wrangler secrets,不进vars- TTL ≤ 预期会话时长(且 ≤ 48 小时)
- 凭证生成端点做限流
- 签发前先做客户端认证
- 保留凭证吊销 API,应对会话被攻陷
- 不硬编码 IP;确有白名单需求就配 DNS 监控
- 浏览器客户端过滤 53 端口
严格防火墙环境可以对turn.cloudflare.com白名单化这些地址:IPv4141.101.90.1/32、162.159.207.1/32,IPv62a06:98c1:3200::1/128、2606:4700:48::1/128。⚠️ 这批 IP 可能提前 14 天通知后变更:用dig turn.cloudflare.com A/dig turn.cloudflare.com AAAA定期核对,配自动告警,14 天内更新白名单。
网络边界:
- IPv6:客户端到 TURN 这一段 IPv4/IPv6 都支持,但中继地址只分配 IPv4(不支持 RFC 6156),TCP 中继(RFC 6062)同样不支持——IPv6 客户端能接入,中继出去的流量仍走 IPv4;
- TLS:1.1/1.2/1.3 都受支持。TLS 1.3 推荐套件
AEAD-AES128-GCM-SHA256、AEAD-AES256-GCM-SHA384、AEAD-CHACHA20-POLY1305-SHA256;TLS 1.2 推荐ECDHE-ECDSA-AES128-GCM-SHA256、ECDHE-RSA-AES128-GCM-SHA256等。
延伸阅读
- api.md:凭证生成/吊销 API、Key 管理、TypeScript 类型与 TTL 约束
- configuration.md:Worker 搭建、wrangler.jsonc、环境变量、IP 白名单
- gotchas.md:常见错误、限额细节与安全检查清单
- patterns.md:凭证缓存、ICE 重启、调试事件的完整示例
- SKILL.md 的网络连通性决策树中,WebRTC 实时通信场景对应
turn/与realtime-sfu/、realtimekit/模块
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考