SFU 해부학 시리즈 #3
지난 편에서 ICE-Lite로 클라이언트와 SFU 사이에 UDP 연결을 수립했다. 파이프가 뚫린 것이다.
그런데 이 파이프로 영상과 음성을 날것 그대로 보내면 어떻게 될까? 같은 와이파이 네트워크에 있는 누구나 패킷을 캡처해서 내용을 볼 수 있다. 카페에서 화상회의를 하면 옆자리 사람이 내 화면을 엿볼 수 있다는 뜻이다.
WebRTC는 미디어 암호화를 선택이 아닌 필수로 규정한다(RFC 8827). 브라우저가 암호화 없는 미디어 전송을 아예 허용하지 않는다. 이 암호화가 DTLS와 SRTP의 조합으로 이루어진다.
웹에서 암호화라면 TLS가 떠오른다. HTTPS가 바로 HTTP + TLS다. 검증된 기술이니 그대로 쓰면 될 것 같다.
문제는 TLS가 TCP 위에서 동작한다는 점이다. TLS는 바이트 스트림이 순서대로 빠짐없이 도착한다는 TCP의 보장을 전제로 설계되었다. 패킷 하나가 유실되면 뒤따르는 모든 패킷이 대기한다(Head-of-Line Blocking).
WebRTC 미디어는 UDP로 전송된다. 실시간 영상에서는 패킷 하나를 기다리느라 전체가 멈추는 것보다, 그 패킷을 버리고 다음 프레임으로 넘어가는 게 낫다. UDP는 순서 보장도, 재전송도 하지 않는다. TLS를 그대로 올릴 수 없다.
DTLS(Datagram Transport Layer Security)는 이 문제를 해결한다. TLS의 암호화 메커니즘을 그대로 가져오되, UDP 위에서 동작하도록 수정한 프로토콜이다. 핵심 차이는 간단하다:
정리하면: DTLS = TLS의 암호화 + UDP의 비순차 허용. "UDP 위의 TLS"라고 생각하면 된다.
ICE로 UDP 연결이 수립되면, 그 위에서 바로 DTLS 핸드셰이크가 시작된다. 목적은 하나다. 양쪽이 암호화에 쓸 키를 안전하게 합의하는 것.
[클라이언트] [SFU]
| |
|------- ClientHello ----------->| 1. "나는 이런 암호화를 지원해"
|<------ ServerHello ------------| 2. "이걸로 하자"
|<------ Certificate ------------| 3. 서버 인증서 전달
|<------ ServerHelloDone --------|
| |
|------- Certificate ----------->| 4. 클라이언트 인증서 전달
|------- ClientKeyExchange ----->| 5. 키 교환 재료 전달
|------- Finished -------------->| 6. "내 쪽 준비 완료"
|<------ Finished ---------------| 7. "서버도 완료"
| |
|====== 암호화 채널 수립 =========|
| |
| 이후: SRTP 키 추출 (DTLS-SRTP)|
웹 브라우저에서 HTTPS 사이트에 접속할 때 일어나는 TLS 핸드셰이크와 거의 동일한 흐름이다. 차이는 이게 UDP 위에서 일어난다는 것, 그리고 핸드셰이크가 끝나면 데이터를 직접 DTLS로 보내는 게 아니라, SRTP 암호화에 쓸 키를 뽑아낸다는 것이다.
이 키 추출 방식을 DTLS-SRTP(RFC 5764)라고 한다. DTLS 핸드셰이크에서 합의된 마스터 시크릿으로부터 SRTP 암호화 키와 인증 키를 유도한다. 키 교환은 DTLS가 하고, 실제 미디어 보호는 SRTP가 한다. 역할 분담이다.
키가 준비되었다. 이제 실제 미디어 패킷을 암호화할 차례다.
RTP(Real-time Transport Protocol)는 미디어 전송의 표준 프로토콜이다. 영상 프레임, 음성 샘플이 RTP 패킷에 담겨서 전송된다. SRTP(Secure RTP)는 이 RTP 패킷을 암호화한 버전이다.
SRTP의 구조에서 중요한 점이 하나 있다. 헤더는 암호화하지 않고, 페이로드만 암호화한다.
일반 RTP 패킷:
+------------------+----------------------+
| RTP Header | Payload (영상/음성) |
| (평문) | (평문) |
+------------------+----------------------+
SRTP 패킷:
+------------------+----------------------+-----------+
| RTP Header | Payload (영상/음성) | Auth Tag |
| (평문) | (암호화됨) | (무결성) |
+------------------+----------------------+-----------+
※ Header: SSRC, 시퀀스 번호, 타임스탬프, 페이로드 타입 등
※ Auth Tag: 헤더+페이로드 전체의 무결성을 검증하는 값
왜 헤더를 평문으로 두는가? 이유는 실용적이다:
정리하면: 영상/음성 내용은 절대 볼 수 없고, 메타데이터(누가 보내고, 몇 번째 패킷인지)는 볼 수 있되 SRTP 키를 모르는 제3자는 조작할 수 없다. SFU처럼 키를 가진 종단점은 헤더를 변경하고 Auth Tag를 다시 생성할 수 있다. 이게 SRTP의 보안 모델이다.
P2P에서는 간단하다. 양쪽이 DTLS 핸드셰이크를 한 번 하고, 그 키로 SRTP 암호화/복호화를 한다. 연결 하나, 키 하나.
SFU는 다르다. SFU는 모든 클라이언트와 각각 별도의 DTLS 세션을 유지한다.
[Client A] <=== DTLS #1 ===> +=========+
[Client B] <=== DTLS #2 ===> | SFU |
[Client C] <=== DTLS #3 ===> | |
[Client D] <=== DTLS #4 ===> +=========+
※ 4명이면 DTLS 세션 4개, SRTP 키 4쌍
※ 30명이면 DTLS 세션 30개, SRTP 키 30쌍
Client A가 보낸 SRTP 패킷을 B, C, D에게 전달하려면:
A의 영상 패킷 흐름:
[A] --SRTP(키A)--> [SFU] --복호화--> [평문 RTP]
|
+--SRTP(키B)-----> [B]
+--SRTP(키C)-----> [C]
+--SRTP(키D)-----> [D]
※ SFU 내부에서 평문 RTP가 잠깐 존재한다
※ 하지만 미디어를 디코딩(H.264 → 영상)하지는 않는다
SFU는 미디어 내용을 "보지" 않는다. 암호화를 풀어서 RTP 패킷을 꺼내지만, 그 RTP 패킷을 디코딩해서 영상으로 만들지는 않는다. 봉투를 열어서 다른 봉투에 넣는 것이지, 편지를 읽는 게 아니다.
이 과정이 SFU 구현에서 까다로운 이유:
| 항목 | P2P | SFU |
|---|---|---|
| DTLS 세션 수 | 1개 (상대방과) | N개 (참여자 수만큼) |
| SRTP 키 | 1쌍 | N쌍 |
| 복호화/재암호화 | 없음 | 매 패킷마다 |
| 미디어 내용 접근 | 양쪽만 가능 | SFU도 RTP 레벨 접근 (디코딩은 안 함) |
| 핸드셰이크 시점 | 연결 시 1회 | 참여자 입장마다 |
| 키 관리 복잡도 | 단순 | 참여자 입퇴장에 따라 동적 |
WebRTC에서 DTLS + SRTP는 선택사항이 아니다. 브라우저가 강제한다. 암호화 없는 RTP 전송을 시도하면 브라우저가 거부한다.
이건 SFU 개발자에게 피할 수 없는 숙제다. ICE-Lite로 연결을 수립하고, DTLS 핸드셰이크로 키를 교환하고, SRTP로 모든 패킷을 암호화/복호화해야 한다. 이 세 단계를 전부 구현해야 클라이언트의 첫 번째 영상 프레임이 상대방에게 도달한다.
지금까지 SFU의 연결 과정을 따라왔다. ICE-Lite로 UDP 파이프를 뚫고, DTLS로 키를 교환하고, SRTP로 미디어를 보호한다. 다음 편에서는 이 과정 전체를 조율하는 시그널링, 그리고 왜 SFU에서 전통적인 SDP 교환 대신 다른 방식을 택하는지를 다룬다.
다음 편: [SFU 해부학 #4] SDP-Free 시그널링 — SDP를 버린 이유
이 글은 OxLens — Rust로 만드는 경량 SFU 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.