你的 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 防火墙
评论
暂无评论。来做第一个吧。