Cloudflare Realtime TURN TCP/UDP 怎麼查?從事故狀態到 WebRTC ICE Restart
Cloudflare Realtime TURN 的視訊或語音突然變慢時,最容易出現的誤判是「TURN 掛了」或「一定是自己的防火牆」。實際上,連線可能仍然成功,只是 ICE 選了一條 TCP relay path;如果既有連線沒有重新協商,即使服務端已修復,它也可能繼續沿用原本的 TCP 路徑。
這篇文章聚焦一個具體而常被忽略的排錯順序:先查 Cloudflare Status,再用 RTCPeerConnection.getStats() 讀目前選中的 ICE candidate,最後才決定是等待、重新連線、做 ICE restart,還是檢查自己的網路與憑證。這不是把所有 TCP 連線都當成故障;TCP 是 Cloudflare Realtime TURN 支援的正式傳輸路徑,只是在即時媒體中通常要知道自己為什麼走到那裡。
2026-09-07 的 TURN 事件告訴我們什麼?
Cloudflare Status 曾記錄一個 Performance degradation affecting TURN service 事件:頁面在 2026-09-07 13:02 UTC 建立,13:52 UTC 標示 resolved;第一則更新則寫明相關效能問題自 2026-08-22 起被觀察到。事件描述指出,有相當一部分流量被 relay 到 TCP,而不是預期的 UDP;Chrome/WebRTC 使用者可能遇到延遲或連線變慢。修復後,新連線的影響已結束,但事件更新也提醒既有連線會繼續使用 TCP,直到 client 結束連線或重新啟動。
換算成台灣時間,這次 Status 頁面的調查到 resolved 約在 21:02–21:52;它不是整段效能問題的起始與持續時間。這個時間點只屬於該次事件的紀錄,不代表日後所有 TCP 都是 Cloudflare 故障。若你現在才看到延遲,先查看 Cloudflare Status 是否有新的 incident,再依下列方法檢查實際路徑。
先分辨三個不同的「協定」
TURN/WebRTC 的 protocol 很容易被混在一起。排錯時至少要分開看:
| 名稱 | 看的是哪一段 | 常見值 |
|---|---|---|
ICE candidate relayProtocol | 本機 client 到 TURN server 的傳輸 | udp、tcp、tls |
ICE candidate protocol | 該 candidate 對外使用的 transport protocol | udp 或 tcp |
TURN URL 的 ?transport= | 你提供給 ICE agent 的候選連線方式 | udp、tcp |
Cloudflare Realtime TURN 的主要 endpoint 包含 turn.cloudflare.com:3478 的 UDP 與 TCP,以及 TLS 的 5349;也提供 80/443 等 alternate port。Port 53 雖然出現在部分回傳的 iceServers 清單裡,但 Cloudflare 文件提醒許多瀏覽器和 ISP 會封鎖它,不應只依賴這個 alternate port。
其中最能回答「是不是透過 TURN 用 TCP 連出去」的是 local relay candidate 的 relayProtocol。如果選中的 local candidate 不是 relay,代表目前的連線沒有使用 TURN relay,這時應把問題轉到 direct/server-reflexive candidate、SFU 或對端網路,而不是繼續只看 TURN。
用 getStats() 找目前選中的 relay candidate
標準做法是先從 transport stats 讀 selectedCandidatePairId,再透過 candidate pair 的 localCandidateId 找到 local candidate。不要只搜尋第一筆 candidate-pair,因為報告裡可能同時存在尚未選用、已失敗或舊的候選配對。
async function inspectSelectedIcePath(peerConnection) { const stats = await peerConnection.getStats(); let selectedPair;
for (const report of stats.values()) { if (report.type === 'transport' && report.selectedCandidatePairId) { selectedPair = stats.get(report.selectedCandidatePairId); break; } }
if (!selectedPair) { return { state: 'no-selected-pair' }; }
const local = stats.get(selectedPair.localCandidateId); const remote = stats.get(selectedPair.remoteCandidateId);
return { state: selectedPair.state, nominated: selectedPair.nominated, localCandidateType: local?.candidateType ?? null, localProtocol: local?.protocol ?? null, relayProtocol: local?.relayProtocol ?? null, localTurnUrl: local?.url ?? null, remoteCandidateType: remote?.candidateType ?? null, remoteProtocol: remote?.protocol ?? null, };}
const path = await inspectSelectedIcePath(peerConnection);console.table(path);解讀時先看 localCandidateType:
relay+relayProtocol: 'udp':本機到 TURN 的 relay transport 是 UDP。relay+relayProtocol: 'tcp'或'tls':本機到 TURN 的 relay transport 不是 UDP,需再對照 Status、網路政策與部署時提供的 URLs。- 不是
relay:目前選中的 path 沒有透過 TURN;不要把它當成 TURN TCP 事故證據。 relayProtocol是null:瀏覽器可能沒有暴露這個欄位,或 stats 尚未完整;把state、candidate type、時間和瀏覽器版本一併記錄,不能用空值硬推論 UDP。
RTCIceCandidatePairStats 的標準選擇關係是 transport.selectedCandidatePairId;MDN 也提醒 Firefox 的 candidate-pair.selected 是非標準屬性,不應只依賴它。這個差異能避免在 Chrome 上因為查不到 selected: true 而誤判沒有可用連線。
事故已 resolved,為什麼畫面還是慢?
如果 Status 已經 resolved,但原本建立的通話仍顯示 local relay 是 TCP,這和該次事件的更新內容一致:既有連線不會自動換成新路徑。依你的應用程式能力選擇:
- 一般通話或短工作階段:讓使用者離開並重新加入,建立新的 ICE gathering。
- 需要維持 session:透過 signaling 執行 ICE restart,讓雙方重新交換 offer/answer。
- TURN credential 即將到期:先從後端取得新的短效 credential,再用
setConfiguration()更新 peer connection,接著依 signaling 流程重新協商。
最小的 ICE restart 示意如下,實際仍需要把 offer 傳給對端並套用 answer:
const offer = await peerConnection.createOffer({ iceRestart: true });await peerConnection.setLocalDescription(offer);
// 將 peerConnection.localDescription 送到你的 signaling server。// 對端回傳 answer 後,再呼叫 setRemoteDescription(answer)。Cloudflare Realtime FAQ 也建議 client 支援 ICE restart,因為 TURN allocation 可能在維護或網路拓撲變化時中斷。這是連線韌性的設計,不是只針對 2026-09-07 事件的臨時 workaround。
TURN URLs 與短效 credential 怎麼設計?
TURN key 是長期 secret,應留在 server;瀏覽器只收到後端產生的短效 credential。Cloudflare 官方示例用 generate-ice-servers 取得 iceServers,並把短效 username/credential 傳給前端。概念上的後端呼叫如下:
curl "https://rtc.live.cloudflare.com/v1/turn/keys/$TURN_KEY_ID/credentials/generate-ice-servers" \ --header "Authorization: Bearer $TURN_KEY_API_TOKEN" \ --header "Content-Type: application/json" \ --data '{"ttl": 3600}'前端只使用後端回傳的 iceServers:
const peerConnection = new RTCPeerConnection({ iceServers: [ { urls: [ 'stun:stun.cloudflare.com:3478', 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turn:turn.cloudflare.com:80?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp', 'turns:turn.cloudflare.com:443?transport=tcp', ], username: turn.username, credential: turn.credential, }, ],});不要把 TURN_KEY_API_TOKEN 放進瀏覽器,也不要為了「一定走 UDP」刪掉所有 TCP/TLS fallback。正確做法是按照使用者網路和安全政策保留可用候選,再用 stats 觀察實際被選中的 path。如果使用 trickle ICE,port 53 timeout 通常不會阻塞候選收集;不使用 trickle ICE 時,依 Cloudflare 文件考慮過濾 port 53 以免多等一次 timeout。
Credential 的 TTL 應長於預期工作階段,但不宜無限期。Cloudflare FAQ 說明 credential 最長 48 小時;長時間 allocation 應在到期前產生新 credential,並在連線中透過 setConfiguration() 配合重新協商。若 credential 過期,連線最後仍會被中斷,不能只靠 ICE restart 掩蓋 credential lifecycle 問題。
一份可放進監控的判斷表
| 觀察到的現象 | 先查什麼 | 下一步 |
|---|---|---|
| 大量使用者同時從 UDP 變 TCP | Cloudflare Status、事件時間與瀏覽器 stats | 新連線測試;既有連線排程 reconnect/ICE restart |
| 單一辦公室只有 TCP | relayProtocol、企業防火牆與出口政策 | 確認 UDP 3478、TCP 3478/80、TLS 5349/443 的允許狀態 |
relayProtocol 是 UDP 仍然延遲 | RTT、packet loss、SFU/對端 path | 不要只重建 TURN credential,繼續查媒體 path |
| stats 沒有 selected pair | ICE gathering、signaling、connectionState | 先修連線協商,不要從空 stats 推斷傳輸協定 |
| credential 到期後才斷線 | 後端 TTL、刷新時機與 setConfiguration() | 在預期最長 session 前取得新 credential |
常見問題
Q: TCP 一定代表 Cloudflare Realtime TURN 壞掉嗎?
A: 不一定。Cloudflare Realtime TURN 正式支援 UDP、TCP 和 TLS;TCP 可能是服務事件、企業網路限制或 ICE fallback 的結果。先讀 local relay candidate 的 relayProtocol,再把時間和 Status 事件對照。
Q: Status 顯示 resolved,為什麼不重新整理網頁就不會恢復?
A: 既有 ICE connection 可能會繼續使用原本的 TCP path。讓 client 結束並重建連線,或透過 signaling 做 ICE restart,才會觸發新的 candidate 選擇。服務已修復和你的現有 session 已換路徑,是兩件事。
Q: 可以把 TURN URL 只留 UDP 嗎?
A: 除非你完全控制所有使用者網路,否則不建議。保留 TCP/TLS fallback 能提高建立連線的成功率;如果你擔心 fallback 的效能,應記錄 relayProtocol 並依網路條件改善,而不是讓受限網路直接無法連線。
Q: getStats() 找不到 relayProtocol 怎麼辦?
A: 先確認 local candidate 的 candidateType 是否為 relay,再看瀏覽器是否支援該欄位。若未暴露,保留 selectedCandidatePairId、candidate type、connection state、瀏覽器版本和時間,並用服務端觀測補足;不能把 null 當成 UDP。
查證範圍:本文於 2026-09-08 檢查 Cloudflare Realtime TURN、credential、FAQ、Cloudflare Status incident,以及 MDN/WebRTC stats reference;事件後的 reconnection 和 ICE restart 建議為設計流程,未在本機建立實際 WebRTC 通話或呼叫 Cloudflare API。
參考資料:
Cloudflare Status:Performance degradation affecting TURN service
Cloudflare Realtime:TURN Service
Cloudflare Realtime:Generate Credentials
Cloudflare Realtime FAQ:TURN allocation disruption 與 ICE restart
回報錯字、失效連結,或告訴我你想看的延伸主題。