2055 字
10 分鐘

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 的傳輸udptcptls
ICE candidate protocol該 candidate 對外使用的 transport protocoludptcp
TURN URL 的 ?transport=你提供給 ICE agent 的候選連線方式udptcp

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 事故證據。
  • relayProtocolnull:瀏覽器可能沒有暴露這個欄位,或 stats 尚未完整;把 state、candidate type、時間和瀏覽器版本一併記錄,不能用空值硬推論 UDP。

RTCIceCandidatePairStats 的標準選擇關係是 transport.selectedCandidatePairId;MDN 也提醒 Firefox 的 candidate-pair.selected 是非標準屬性,不應只依賴它。這個差異能避免在 Chrome 上因為查不到 selected: true 而誤判沒有可用連線。

事故已 resolved,為什麼畫面還是慢?#

如果 Status 已經 resolved,但原本建立的通話仍顯示 local relay 是 TCP,這和該次事件的更新內容一致:既有連線不會自動換成新路徑。依你的應用程式能力選擇:

  1. 一般通話或短工作階段:讓使用者離開並重新加入,建立新的 ICE gathering。
  2. 需要維持 session:透過 signaling 執行 ICE restart,讓雙方重新交換 offer/answer。
  3. 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,並把短效 usernamecredential 傳給前端。概念上的後端呼叫如下:

Terminal window
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 變 TCPCloudflare Status、事件時間與瀏覽器 stats新連線測試;既有連線排程 reconnect/ICE restart
單一辦公室只有 TCPrelayProtocol、企業防火牆與出口政策確認 UDP 3478、TCP 3478/80、TLS 5349/443 的允許狀態
relayProtocol 是 UDP 仍然延遲RTT、packet loss、SFU/對端 path不要只重建 TURN credential,繼續查媒體 path
stats 沒有 selected pairICE 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

MDN:RTCIceCandidateStats relayProtocol

MDN:RTCIceCandidatePairStats

Cloudflare Realtime TURN TCP/UDP 怎麼查?從事故狀態到 WebRTC ICE Restart
https://laplusda.com/posts/cloudflare-realtime-turn-tcp-udp-diagnosis/
作者
Zero
發佈於
2026-09-08
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。