Cloudflare TURN 实战指南:WebRTC 中继接入、50 分钟凭证续期与 ICE 重启排障
2026/9/15 19:14:32 网站建设 项目流程

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" }

响应里有uidnamecreatedmodifiedkey。注意一点: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 的varsTURN_KEY_SECRETwrangler secret put TURN_KEY_SECRET单独注入。生产环境还可以绑定一个 KV 命名空间(例如CREDENTIALS_CACHE)做凭证缓存。

把 53 端口挡在服务端

生成端点返回的 urls 里夹着turn:turn.cloudflare.com:53?transport=udpturn: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 } ] });

剩下的地址在浏览器里的尝试顺序:

  1. 3478/udp——首选,延迟最低;
  2. 3478/tcp——UDP 被封时的回退;
  3. 5349/tcp(turns:)——企业防火墙场景最可靠;
  4. 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 小时的长会话做凭证刷新、连接失败。状态进faileddisconnected时依次做四件事:

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/32162.159.207.1/32,IPv62a06:98c1:3200::1/1282606: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-SHA256AEAD-AES256-GCM-SHA384AEAD-CHACHA20-POLY1305-SHA256;TLS 1.2 推荐ECDHE-ECDSA-AES128-GCM-SHA256ECDHE-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),仅供参考

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

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

立即咨询