Cloudflare TURN 生产级实现模式:WebRTC 中 TURN 凭证管理、ICE 重启与连接调试实战
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
Cloudflare TURN(Traversal Using Relays around NAT)是运行在 Cloudflare 全球 anycast 网络(310+ 城市,不含中国网络)上的托管中继服务,用于在 NAT 或防火墙阻断了 WebRTC 客户端与 SFU 之间的直连时,作为流量中继点保证通话可用。本文基于仓库中 patterns.md 的实现模式,结合同模块的 api.md、configuration.md 与 gotchas.md,完整讲解从浏览器端 ICE 服务器配置、端口选择、凭证刷新缓存,到 ICE 重启、调试排查的生产级实现方案,读完即可直接落地一套健壮的 TURN 接入代码。
实现前的两个前置条件
在动手写客户端代码之前,需要先完成两项基础设施准备:
- 创建 TURN Key:调用 Cloudflare API 创建 TURN 密钥,参考 api.md#create-turn-key。所有 API 端点都需要具备 "Calls Write" 权限的 Cloudflare API Token,Base URL 为
https://api.cloudflare.com/client/v4。 - 配置 Worker 服务:实现一个用于签发临时凭证的后端 Worker,参考 configuration.md#cloudflare-worker-integration。
创建 TURN Key
POST /accounts/{account_id}/calls/turn_keys Content-Type: application/json { "name": "my-turn-key" }响应中包含uid(密钥标识)、key(实际密钥,仅在创建时返回,必须立即保存)、name、created与modified(ISO 8601 时间戳)。此外还支持以下管理操作:
GET /accounts/{account_id}/calls/turn_keys:列出所有 TURN Key;GET /accounts/{account_id}/calls/turn_keys/{key_id}:获取单个 Key 详情;PUT /accounts/{account_id}/calls/turn_keys/{key_id}:更新名称;DELETE /accounts/{account_id}/calls/turn_keys/{key_id}:删除 Key。
Worker 集成要点
Worker 侧需要把密钥放入环境变量与 secrets,见 configuration.md:
# .env CLOUDFLARE_ACCOUNT_ID=your_account_id CLOUDFLARE_API_TOKEN=your_api_token TURN_KEY_ID=your_turn_key_id TURN_KEY_SECRET=your_turn_key_secretwrangler.jsonc 中,非敏感的TURN_KEY_ID可放在vars,敏感密钥通过wrangler secret put TURN_KEY_SECRET单独注入;生产环境还可绑定CREDENTIALS_CACHEKV 命名空间做凭证缓存。Worker 收到浏览器请求后,调用凭证生成端点把临时凭证返回给客户端,并在返回前过滤掉浏览器不可用的 53 端口 URL(详见下文端口策略)。
浏览器端基础 TURN 配置
WebRTC 通过RTCIceServer描述 ICE 服务器。实现时从自己的后端拉取临时凭证,并始终叠加一个公开 STUN 服务器:
interface RTCIceServer { urls: string | string[]; username?: string; credential?: string; credentialType?: "password" | "oauth"; } async function getTURNConfig(): Promise<RTCIceServer[]> { const response = await fetch('/api/turn-credentials'); const data = await response.json(); return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: [ 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp', 'turns:turn.cloudflare.com:443?transport=tcp' ], username: data.username, credential: data.credential, credentialType: 'password' } ]; } // Use in RTCPeerConnection const iceServers = await getTURNConfig(); const peerConnection = new RTCPeerConnection({ iceServers });这里的关键设计是:STUN(stun:stun.cloudflare.com:3478)负责发现公网候选,TURN(turn:/turns:)负责在直连失败时中继流量,两者以iceServers数组同时交给RTCPeerConnection,由 ICE 协商自动择优。
端口选择策略:UDP 优先,TLS 兜底
浏览器客户端推荐的端口尝试顺序为:
- 3478/udp:首选,延迟最低;
- 3478/tcp:UDP 被封禁网络的回退方案;
- 5349/tls:企业防火墙场景最可靠;
- 443/tls:备用 TLS 端口,防火墙友好。
必须避免端口 53——Chrome 和 Firefox 会拦截该端口的流量。因此生产代码中应当对服务端返回的 URL 列表做过滤与排序:
function filterICEServersForBrowser(urls: string[]): string[] { return urls .filter(url => !url.includes(':53')) // Remove port 53 .sort((a, b) => { // Prioritize UDP over TCP over TLS if (a.includes('transport=udp')) return -1; if (b.includes('transport=udp')) return 1; if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1; if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1; return 0; }); }为什么需要过滤:凭证生成 API 的响应中会包含turn:turn.cloudflare.com:53?transport=udp与turn:turn.cloudflare.com:80?transport=tcp这类地址(见 api.md#response-schema 的完整响应示例)。虽然它们在非浏览器客户端可用,但在浏览器端会静默失败,因此过滤逻辑应当放在服务端完成,而不是依赖浏览器端(见 gotchas.md#using-port-53-in-browsers)。
凭证生命周期管理:刷新与缓存
Cloudflare TURN 临时凭证的 TTL 上限为48 小时(172800 秒),超过会被 API 拒绝;默认值视 API 而定,仓库示例中常用 3600 秒。凭证到期后连接会中断,因此长通话必须实现"刷新 + 缓存"两条链路。
会话中凭证刷新(Credential Refresh)
setConfiguration()可以更新iceServers,但它不会触发 ICE 重启;如果连接已经失败,必须与restartIce()配合:
async function refreshTURNCredentials(pc: RTCPeerConnection): Promise<void> { const newCreds = await fetch('/turn-credentials').then(r => r.json()); const config = pc.getConfiguration(); config.iceServers = newCreds.iceServers; pc.setConfiguration(config); // Note: setConfiguration() does NOT trigger ICE restart // Combine with restartIce() if connection fails } // Auto-refresh before expiry setInterval(async () => { await refreshTURNCredentials(peerConnection); }, 3000000); // 50 minutes if TTL is 1 hour刷新时机以 TTL 为基准:ttl * 1000 - 60000(提前 1 分钟)是 gotchas.md 给出的推荐刷新间隔。
凭证缓存模式(TURNCredentialsManager)
为避免每个客户端每次都打到rtc.live.cloudflare.com生成端点,服务端可用TURNCredentialsManager在内存中缓存未过期的凭证,并在本地校验 TTL 上限:
class TURNCredentialsManager { private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null; async getCredentials(keyId: string, keySecret: string): Promise<RTCIceServer[]> { const now = Date.now(); if (this.creds && this.creds.expiresAt > now) { return this.buildIceServers(this.creds); } const ttl = 3600; if (ttl > 172800) throw new Error('TTL max 48hrs'); const res = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ttl }) } ); const data = await res.json(); const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53')); this.creds = { username: data.iceServers.username, credential: data.iceServers.credential, urls: filteredUrls, expiresAt: now + (ttl * 1000) - 60000 }; return this.buildIceServers(this.creds); } private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] { return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const } ]; } }注意三个细节:缓存有效期比 TTL 提前 1 分钟(- 60000)预留刷新窗口;过滤 53 端口在缓存写入时一次性完成;ttl > 172800的防御性校验与 API 侧的约束保持一致(api.md#credential-constraints 明确 API 会拒绝超过 172800 秒的请求)。
凭证生成端点的请求/响应契约:
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)。若需立即终止某会话,可调用POST .../credentials/revoke(Authorization: Bearer {key_secret},body 传{"username": "..."}),返回 204,计费立即停止,活跃连接在数秒内断开。
ICE 重启模式:网络变化与凭证过期的恢复手段
在网络切换、TURN 服务器维护或凭证过期后,iceconnectionstatechange事件会进入failed状态。生产级实现应在此时依次完成:刷新凭证 →restartIce()→ 重新创建带iceRestart: true的 offer → 通过信令通道发送给对方:
pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed') { console.warn('ICE connection failed, restarting...'); // Refresh credentials await refreshTURNCredentials(pc); // Trigger ICE restart pc.restartIce(); const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); // Send offer to peer via signaling channel... } });需要触发 ICE 重启的场景(gotchas.md#ice-restart-required-scenarios):
- TURN 服务器维护(Cloudflare 网络上偶尔发生);
- 网络拓扑变化(anycast 路由调整);
- 长会话(>1 小时)中的凭证刷新;
- 连接失败(
iceConnectionState === 'failed')。
更稳妥的做法是把failed和disconnected两种状态都纳入恢复条件,防止移动网络切换时掉线(对应 gotchas.md 中的完整示例)。
常见用例与 Cloudflare Calls SFU 集成
根据业务对连通性与效率的取舍,通过iceTransportPolicy和bundlePolicy控制 ICE 行为:
// Video conferencing: TURN as fallback const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' }; // IoT/predictable connectivity: force TURN const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' }; // Screen sharing: reduce overhead const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' });- 视频会议用
'all':先尝试 P2P 直连,失败才走中继; - IoT 等对可预测性要求高的场景用
'relay':强制全部流量经 TURN 中继,连通性可预期; - 屏幕共享用
'max-bundle':把多路媒体流聚合到单条传输通道,降低开销。
如果使用 Cloudflare Calls SFU,TURN 会在需要时自动启用,客户端无需手动编排 TURN 与 SFU 的协调:
const session = await callsClient.createSession({ appId: 'your-app-id', sessionId: 'meeting-123' });值得注意的成本信息(gotchas.md#cost-optimization):与 Cloudflare Calls SFU 搭配使用时 TURN 免费,否则按 $0.05/GB 出站流量计费。
调试 ICE 连通性
借助三个事件/API 观察 ICE 过程:
pc.addEventListener('icecandidate', (event) => { if (event.candidate) { console.log('ICE candidate:', event.candidate.type, event.candidate.protocol); } }); pc.addEventListener('iceconnectionstatechange', () => { console.log('ICE state:', pc.iceConnectionState); }); // Check selected candidate pair const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'candidate-pair' && report.selected) { console.log('Selected:', report); } });icecandidate:观察候选的type(host/srflx/relay)与protocol,确认是否出现了 relay 候选;iceconnectionstatechange:追踪checking → connected → completed(或failed)状态流转;getStats()中的candidate-pair报告:selected为 true 的条目即当前实际选中的候选对,可用于判断流量到底走的直连还是 TURN 中继。
若连接建立缓慢,gotchas.md#issue-slow-connection-establishment 建议检查:候选收集是否完整、到 Cloudflare 边缘的网络延迟、防火墙是否放行 WebRTC 端口(3478、5349、443),以及企业网络是否应改用 443 端口上的 TURN over TLS。
限额、常见错误与安全检查清单
单分配限额
以下限制是按用户分配而非账户级(gotchas.md#limits-per-turn-allocation):
| 维度 | 限额 | 超限后果 |
|---|---|---|
| 唯一 IP 数 | >5 个新 IP/秒 | 丢包 |
| 包速率 | 入/出 5-10k pps | 丢包 |
| 数据速率 | 入/出 50-100 Mbps | 丢包 |
高频错误对照
| 错误 | 正确做法 |
|---|---|
ttl: 604800(7 天) | 改用ttl: 86400(24 小时),超 48h API 直接拒绝 |
硬编码 IPturn:141.101.90.1:3478 | 用 DNS 域名turn:turn.cloudflare.com:3478,IP 变化有 14 天通知期 |
浏览器端保留:53端口 URL | 服务端过滤!url.includes(':53') |
| 凭证到期不做刷新 | setInterval提前 1 分钟刷新 |
| 只打日志不重启 | failed/disconnected时刷新凭证 +restartIce() |
| 把 TURN_KEY_SECRET 放客户端 | 只在服务端生成凭证,客户端请求/api/turn-credentials |
安全清单
- 凭证只在服务端生成,绝不下发密钥;
TURN_KEY_SECRET放在 wrangler secrets,不放进vars;- TTL ≤ 预期会话时长(且 ≤ 48 小时);
- 凭证生成端点做限流;
- 签发凭证前先做客户端认证;
- 为被攻陷的会话提供凭证吊销 API;
- 不硬编码 IP,或建立 DNS 监控;
- 浏览器客户端过滤 53 端口。
企业防火墙的 IP 白名单
严格防火墙环境可对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 天内更新白名单(configuration.md#ip-allowlisting)。
部署边界:IPv6 与 TLS
- 客户端到 TURN:IPv4/IPv6 均支持;但中继地址只分配 IPv4(不支持 RFC 6156),TCP 中继(RFC 6062)也不支持——IPv6 客户端可以接入,中继流量仍走 IPv4;
- TLS 版本: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等套件(详见 configuration.md#tls-configuration)。
进一步阅读
- TURN API 参考:凭证生成/吊销 API、Key 管理、TypeScript 类型与 TTL 约束;
- TURN 配置指南:Worker 搭建、wrangler.jsonc、环境变量、IP 白名单;
- TURN 陷阱与排查:常见错误、限额、安全检查清单;
- TURN 服务概览:服务地址、端口清单与快速开始;
- 在 SKILL.md 的网络连通性决策树中,WebRTC 实时通信场景对应的正是
turn/与realtime-sfu/、realtimekit/模块。
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考