你的 WebRTC 應用程式在自己的 Wi-Fi 下正常,偏偏在那位從飯店打來的客戶那裡失敗。點對點媒體需要雙方之間有一條路徑;當雙方都在嚴格 NAT 之後,或者防火牆丟棄 UDP,就沒有路徑,通話根本連不上。誰也看不到錯誤訊息——只有一個沒完沒了的「連線中…」。
TURN 伺服器就是這類通話的備援方案:雙方把媒體串流送給它,由它轉送。如果你在應用程式裡加入視訊或語音、為團隊運行 Matrix 或 Jitsi,或者幫遊戲做語音聊天,你就需要一台。coturn 是標準的開源實作,而且很輕巧。
選哪個方案
TURN 對 CPU 和記憶體的消耗很小。它消耗的是頻寬:每條中繼串流進出各經過伺服器一次,共兩次。我們方案的流量在 1 Gbit/s 下不限量,所以問題在於同時有多少通話在中繼,而不是要付多少錢。
| 同時中繼的通話數 | 方案 |
|---|---|
| 幾通——一個團隊、一個小應用程式 | Nano-IP:2 vCPU、1 GB、15 GB(每月 $8) |
| 幾十通 | Micro-IP:2 vCPU、2 GB、25 GB(每月 $10) |
| 更多,或把 TURN 放在應用程式伺服器旁邊 | Small-IP:4 vCPU、4 GB、35 GB(每月 $16) |
必須是有獨立 IPv4 的方案——TURN 的核心就是大量連接埠上的入站連線,而 NAT 方案從外部只接受 SSH。在我們的獨立 IP 方案上,公用位址直接設定在伺服器的網路介面上,所以 coturn 不需要 external-ip 這一行就能找到它。把伺服器放在離使用者近的地方:伺服器位於歐盟,共有兩個地點,下單時可看到哪裡有空位。如果你在意,可以用加密貨幣付款,無需 KYC。
需要開放的連接埠:UDP 和 TCP 3478,中繼媒體用的 UDP 49152–65535,如果加 TLS 再開 TCP 5349。
安裝與設定
在 Ubuntu 24.04 或 Debian 12 上以 root 身分執行:
apt install -y coturn
ip -4 addr # 網卡上應該看得到你的公用 IP
用一組新的共享密鑰寫入設定:
SECRET=$(openssl rand -hex 32)
cat > /etc/turnserver.conf <<EOF
listening-port=3478
fingerprint
use-auth-secret
static-auth-secret=$SECRET
realm=turn.example.com
min-port=49152
max-port=65535
no-cli
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
EOF
echo "$SECRET" # 後端需要這個值
systemctl restart coturn
ufw allow 22/tcp && ufw allow 3478 && ufw allow 49152:65535/udp && ufw enable
別省略 denied-peer-ip 這幾行。少了它們,任何持有有效憑證的人都能藉你的中繼存取私有位址——包括伺服器自身內部一側的服務。我們驗證過:有了這些行,往私有位址的中繼會被以「403 Forbidden IP」拒絕。
會過期的憑證
使用 use-auth-secret 時,coturn 接受包含到期時間的使用者名稱,以及作為該名稱 HMAC 的密碼。它們由你的後端產生;密鑰從不離開伺服器端。用 Node.js:
const crypto = require("crypto");
// 臨時 TURN 使用者名稱/密碼(coturn 透過 use-auth-secret 支援的 TURN REST API 方案)
function turnCredentials(secret, user, ttlSeconds = 3600) {
const username = `${Math.floor(Date.now() / 1000) + ttlSeconds}:${user}`;
const credential = crypto.createHmac("sha1", secret).update(username).digest("base64");
return { username, credential };
}
console.log(JSON.stringify(turnCredentials(process.env.TURN_SECRET, "alice")));
在瀏覽器裡向後端要一組新憑證,交給 RTCPeerConnection:
const { username, credential } = await fetch("/api/turn-credentials").then((r) => r.json());
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:turn.example.com:3478" },
{
urls: ["turn:turn.example.com:3478?transport=udp", "turn:turn.example.com:3478?transport=tcp"],
username,
credential,
},
],
});
測試
在伺服器上,用 openssl 依同樣的方案產生(得到的密碼與 Node.js 程式碼完全一致——我們比對過):
U="$(( $(date +%s) + 3600 )):test"
P=$(echo -n "$U" | openssl dgst -binary -sha1 -hmac "$SECRET" | base64)
turnutils_uclient -y -n 10 -u "$U" -w "$P" YOUR_SERVER_IP
最後你希望看到「Total lost packets 0」。密碼錯誤時,用戶端會停在「Cannot complete Allocation」——這表示伺服器在正常工作。
我們也用真正的 Chromium 驗證過:設定 iceTransportPolicy: "relay" 並使用新憑證時,瀏覽器從伺服器拿到了 relay 候選;密碼錯誤時一個都沒有。想用同樣方法測試你的設定,可以暫時打開這個選項——它會強制所有通話走 TURN。
需要時再加 TLS
有些網路會封鎖 UDP,對一般 TCP 也很挑剔;以 TLS 承載的 TURN 能穿過其中更多網路。為 turn.example.com 申請憑證(certbot 就行)。憑證檔案只有 root 能讀,而 coturn 以 turnserver 使用者身分執行,所以要給 coturn 一份它自己的副本:
mkdir -p /etc/coturn
cp /etc/letsencrypt/live/turn.example.com/fullchain.pem /etc/letsencrypt/live/turn.example.com/privkey.pem /etc/coturn/
chown turnserver: /etc/coturn/*.pem && chmod 640 /etc/coturn/privkey.pem
cat >> /etc/turnserver.conf <<'EOF'
tls-listening-port=5349
cert=/etc/coturn/fullchain.pem
pkey=/etc/coturn/privkey.pem
EOF
systemctl restart coturn && ufw allow 5349/tcp
在 urls 清單中加入 turns:turn.example.com:5349?transport=tcp。憑證效期很短,所以請把複製步驟放進 certbot 的 deploy hook——否則 TLS 會在第一次續期時悄悄失效。
值得知道
- TURN 不會把糟糕的連線變好。它多加了一跳,所以要把伺服器放在需要它的人附近。
- 如果你運行 SFU——例如 Jitsi 的 videobridge——那是另一個元件。TURN 幫助的是無法直接連到它的用戶端。
- 整個安全模型就是那組密鑰。不要放進前端,一旦外洩就立刻更換。
相關內容
- Jitsi Meet VPS:在自己的伺服器上視訊通話
- 自架 Matrix Synapse:Matrix 通話同樣需要 TURN 伺服器
- 獨立 IP 網站 VPS:負責發放憑證的那個應用程式
- 如何設定 ufw 防火牆
評論
目前還沒有評論。歡迎成為第一位。