WebRTC 앱이 내 Wi-Fi에서는 잘 되는데, 호텔에서 전화하는 그 한 고객에게만 실패합니다. P2P 미디어에는 양쪽을 잇는 경로가 필요합니다. 양쪽 모두 엄격한 NAT 뒤에 있거나 방화벽이 UDP를 버리면 경로가 없고, 통화는 그냥 연결되지 않습니다. 아무도 오류를 보지 못하고, 끝없는 "연결 중…"만 보입니다.
TURN 서버는 바로 그런 통화를 위한 대체 경로입니다. 양쪽이 미디어를 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 방화벽 설정 방법
댓글
아직 댓글이 없습니다. 첫 번째가 되세요.