RETROSPECTIVE · 회고
STUN과 TURN, 그리고 TURN 계정을 코드에 박으면 안 되는 이유
Network
WebRTC 통화가 우리 집 와이파이에서는 되는데 특정 회사망이나 모바일 네트워크에서는 연결되지 않는 경우가 있었다. 원인은 NAT였다.
STUN: 내 공인 주소 알아내기
공유기 뒤에 있는 기기는 자신의 공인 IP/포트를 모른다. STUN 서버에 물어보면 바깥에서 보이는 주소를 알려주고, 이 주소를 ICE candidate로 상대에게 전달해 직접(P2P) 연결을 시도한다.
STUN으로 안 되는 경우
대칭형(symmetric) NAT는 목적지마다 다른 포트를 할당한다. STUN 서버에게 보인 주소와 상대에게 보이는 주소가 달라서 직접 연결이 실패한다. 이때 필요한 게 TURN이다.
TURN: 중계 서버
TURN 서버는 양쪽 트래픽을 대신 받아 전달한다. 연결은 거의 항상 되지만 모든 미디어가 서버를 거치므로 대역폭 비용이 든다. 그래서 ICE는 직접 연결 후보를 먼저 시도하고, 실패할 때만 TURN(relay) 후보를 쓴다.
new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "turn:turn.example.com:3478", username, credential },
],
});
TURN 계정을 클라이언트 코드에 고정하면
TURN 자격 증명은 구조상 브라우저에 전달될 수밖에 없다. 고정된 아이디/비밀번호를 번들에 넣으면 누구나 꺼내서 내 TURN 서버를 무료 릴레이로 쓸 수 있다. 공개 저장소라면 더 말할 것도 없다.
해결: 시간 제한 임시 자격 증명
coturn 같은 TURN 서버는 공유 비밀키 기반의 임시 자격 증명(TURN REST API 방식)을 지원한다. 서버가 로그인한 사용자에게만 짧은 유효기간의 계정을 발급한다.
// 서버 (Node.js)
import crypto from "crypto";
function issueTurnCredential(userId) {
const expiresAt = Math.floor(Date.now() / 1000) + 60 * 60; // 1시간
const username = `${expiresAt}:${userId}`;
const credential = crypto
.createHmac("sha1", process.env.TURN_SECRET)
.update(username)
.digest("base64");
return { username, credential };
}
TURN 서버에는 같은 비밀키를 설정해두고(use-auth-secret, static-auth-secret), 만료 시각이 지난 계정은 거절하게 한다. 비밀키는 서버에만 있으니 번들에서 꺼내갈 게 없다.