ICE-Lite로 NAT 뚫기 — STUN/TURN 없이 가능?

kodeholic·2026년 3월 27일

SFU 해부학

목록 보기
2/5

SFU 해부학 시리즈 #2

WebRTC 튜토리얼을 펼치면 첫 장에 반드시 나오는 문장이 있다.

"STUN 서버와 TURN 서버를 준비하세요."

그런데 이상한 점이 있다. Discord, LiveKit, mediasoup — 이름 있는 SFU 기반 서비스들의 아키텍처 문서를 보면, TURN 서버가 없다. STUN도 별도로 운영하지 않는다. 수백만 사용자가 접속하는 서비스인데 어떻게 그게 가능한가?

답을 이해하려면 STUN/TURN이 왜 필요한지부터 짚어야 한다. 그리고 그 "왜"가 SFU 환경에서는 성립하지 않는다는 걸 보면, 답은 자연스럽게 나온다.


1. NAT 뒤에서는 못 찾는다

집이든 회사든, 대부분의 기기는 공유기 뒤에 있다. 내 PC의 IP는 192.168.0.10 같은 사설 주소다. 인터넷 너머의 누군가가 이 주소로 직접 패킷을 보낼 수 없다. 공유기가 사설 주소를 공인 주소로 바꿔주는데, 이 동작을 NAT(Network Address Translation)라고 한다.

  [내 PC]                [상대방 PC]
  192.168.0.10            192.168.1.20
      |                       |
  +--------+             +--------+
  | 공유기  |             | 공유기  |
  | (NAT)  |             | (NAT)  |
  +--------+             +--------+
      |                       |
  공인 IP                  공인 IP
  1.2.3.4                 5.6.7.8

  ※ 내가 먼저 밖으로 보내면 NAT가 구멍을 뚫어준다
  ※ 하지만 상대방이 내 사설 IP로 직접 보낼 방법은 없다

내가 먼저 외부 서버에 요청을 보내면, NAT가 "192.168.0.10:5000 → 1.2.3.4:30000" 같은 매핑을 만들어둔다. 응답은 이 매핑을 따라 들어온다. 하지만 상대방이 먼저 나에게 보내려면? 매핑이 없으니 NAT가 패킷을 버린다.

P2P 화상통화에서 양쪽 다 NAT 뒤에 있으면, 서로가 서로를 찾을 수 없다. 이 문제를 업계에서는 NAT traversal — "NAT을 뚫는 것"이라고 부른다. STUN과 TURN은 이걸 해결하는 도구다.


2. STUN과 TURN — 각각 뭘 해주는가

STUN은 간단하다. "내 공인 IP가 뭔지 알려줘."

  [내 PC] ---질문---> [STUN 서버]
          <--응답---
          "너의 공인 주소는 1.2.3.4:30000이야"

내 PC는 자기가 192.168.0.10이라는 것만 안다. STUN 서버에 물어보면, NAT가 바깥에 내놓은 공인 주소를 알 수 있다. 이걸 상대방에게 시그널링으로 전달하면, 상대방이 그 주소로 패킷을 보낼 수 있다.

문제는 이것만으로 안 되는 경우가 있다는 거다. NAT 종류에 따라 "내가 먼저 보낸 적 없는 IP에서 온 패킷은 다 버리겠다"는 정책(Symmetric NAT)이 걸려있으면, STUN으로 알아낸 주소가 무용지물이 된다.

이때 등장하는 게 TURN이다. "직접 연결이 안 되면 내가 중계해줄게."

  [내 PC] <----> [TURN 서버] <----> [상대방 PC]

  ※ 모든 미디어가 TURN 서버를 경유
  ※ 대역폭 비용이 TURN 서버에 집중된다

TURN은 확실한 해결책이다. 하지만 대가가 크다. 화상회의의 모든 영상/음성 트래픽이 TURN 서버를 거치면, 그 서버의 대역폭 비용이 곧 서비스 비용이 된다. 720p 영상 하나에 1.5Mbps, 10명 회의에서 양방향이면 수십 Mbps가 한 세션에서 TURN을 관통한다. 세션이 수천 개라면? TURN 서버 비용만으로 사업성이 흔들릴 수 있다.


3. SFU가 바꾸는 전제

P2P에서 STUN/TURN이 필요한 이유를 다시 보자.

  • 양쪽 다 NAT 뒤에 있다
  • 서로의 공인 주소를 모른다
  • NAT가 모르는 IP에서 온 패킷을 버린다

SFU는 이 전제를 근본적으로 바꾼다. 서버가 공인 IP에 있다.

  [내 PC]                      [SFU 서버]
  192.168.0.10                  203.0.113.50
      |                             |
  +--------+                   (공인 IP, 고정)
  | 공유기  |                   (포트도 고정)
  | (NAT)  |
  +--------+
      |
  공인 IP
  1.2.3.4

  ※ 내가 SFU에 먼저 패킷을 보낸다
  ※ NAT가 매핑을 만든다 (1.2.3.4:30000 → 203.0.113.50)
  ※ SFU의 응답은 이 매핑을 따라 자연스럽게 들어온다

한쪽이 공인 IP에 고정되어 있으면:

  • 내가 먼저 서버에 보내면 된다. 클라이언트가 SFU에 패킷을 보내는 순간, NAT가 매핑을 만든다. SFU의 응답은 그 매핑을 타고 들어온다. Symmetric NAT여도 상관없다 — 내가 먼저 보낸 바로 그 서버에서 응답이 오니까.
  • SFU의 주소를 알아낼 필요가 없다. 서버 주소는 이미 알려져 있다. STUN으로 "상대방 공인 주소"를 찾을 필요가 없다.
  • 중계가 필요 없다. 직접 연결이 항상 성공하니까 TURN이 필요 없다.

P2P의 난제였던 "양쪽 다 NAT 뒤" 상황이 SFU에서는 "한쪽은 공인 IP"로 바뀐다. 이것만으로 STUN 서버 별도 운영과 TURN 서버 비용이 사라진다.


4. ICE-Lite — "나는 여기 있다" 한마디

ICE(Interactive Connectivity Establishment)는 WebRTC의 연결 수립 절차다. Full ICE에서는 양쪽이 할 일이 많다.

  Full ICE 절차:

  [클라이언트 A]                    [클라이언트 B]
       |                                |
  1. STUN으로 내 공인 주소 수집         1. 같은 과정
  2. 후보(candidate) 목록 생성         2. 같은 과정
     - host (사설 IP)                    - host
     - srflx (STUN 반사 주소)            - srflx
     - relay (TURN 중계 주소)            - relay
  3. 후보 목록을 상대에게 전달          3. 같은 과정
  4. 모든 조합에 대해 연결 테스트       4. 같은 과정
     (connectivity check)
  5. 가장 좋은 경로 선택                5. 합의

후보 수집, 교환, 조합별 연결 테스트... 이 과정에 수백 밀리초에서 수 초가 걸린다. P2P에서는 필요한 과정이다. 양쪽 다 어디에 있는지 모르니까.

ICE-Lite는 서버가 "나는 후보 수집도, 연결 테스트도 할 필요 없다"고 선언하는 것이다. SFU는 자기 주소를 이미 알고 있고, 공인 IP에 고정되어 있으니까 후보가 딱 하나 — 자기 자신이다.

  ICE-Lite 절차:

  [클라이언트]                      [SFU (ICE-Lite)]
       |                                |
  1. SFU 주소는 이미 알고 있음          서버: "내 주소는 203.0.113.50:10000"
  2. 클라이언트가 STUN Binding            (후보 1개, 수집 과정 없음)
     Request를 SFU에 직접 보냄           |
       |----STUN Binding Request------->|
       |<---STUN Binding Response-------|
       |     "너의 공인 주소는             ※ SFU가 직접 응답
       |      1.2.3.4:30000이야"          ※ 별도 STUN 서버 불필요
       |                                |
  3. 연결 수립 완료                     응답만 하면 끝

핵심을 정리하면:

  • SFU가 STUN 서버 역할을 겸한다. 클라이언트가 보낸 STUN Binding Request에 SFU가 직접 응답해서 클라이언트의 공인 주소를 알려준다. 별도 STUN 서버가 필요 없다.
  • 후보가 1개이므로 조합 테스트가 없다. 연결 시간이 눈에 띄게 줄어든다.
  • TURN이 구조적으로 불필요하다. "서버가 공인 IP" 전제가 유지되는 한, 직접 연결이 실패할 이유가 없다.

5. 그래서 TURN은 정말 안 쓰나?

한 가지 예외가 있다. 기업 방화벽이 UDP 자체를 차단하는 환경이다.

WebRTC 미디어는 UDP로 전송된다. 그런데 일부 기업 네트워크는 보안 정책상 허용된 포트(80, 443) 외의 UDP를 전부 막아놓는다. 이 경우 SFU가 공인 IP에 있어도 UDP 패킷 자체가 방화벽에서 걸린다.

이런 환경에서의 대응 전략은 두 가지다:

  • TURN over TCP/TLS (443 포트): HTTPS와 같은 포트를 쓰므로 방화벽을 통과한다. 하지만 미디어를 TCP로 감싸니까 지연이 늘어난다.
  • SFU 자체를 443 포트에서 운영: UDP 443은 대부분의 방화벽에서 허용된다. QUIC이 UDP 443을 쓰기 때문에 이 포트를 막으면 웹 브라우징 자체가 느려지기 때문이다.

현실적으로 UDP를 완전 차단하는 네트워크는 전체의 5~8% 수준이다(Google WebRTC 팀의 2016년 발표 기준). 나머지 92~95%는 ICE-Lite만으로 연결에 문제가 없다.


한눈에 비교

항목P2P + Full ICESFU + ICE-Lite
STUN 서버필수 (별도 운영)불필요 (SFU가 겸함)
TURN 서버필요 (Symmetric NAT 대비)대부분 불필요
후보 수집양쪽 모두 (host/srflx/relay)서버: 1개, 클라이언트: 생략 가능
연결 테스트모든 후보 조합없음
연결 수립 시간수백ms ~ 수초수십ms
대역폭 비용TURN 경유 시 높음SFU 포워딩만
구현 복잡도높음낮음

공짜 점심의 조건

ICE-Lite는 마법이 아니다. "클라이언트가 접속할 수 있는 고정된 공인 주소가 있어야 한다"는 전제가 성립해야 한다.

가장 직관적인 구성은 SFU 서버에 공인 IP를 직접 붙이는 것이다. 클라우드 VM이든, 코로케이션 서버든 — 공인 IP에서 직접 리스닝하면 ICE-Lite가 동작한다.

하지만 꼭 서버에 공인 IP를 직접 붙여야 하는 건 아니다. 공유기 뒤에 서버를 두고, 포트포워딩으로 외부 포트를 서버로 연결해도 동일하게 동작한다. 클라이언트 입장에서는 공유기의 공인 IP:포트로 접속하는 것이고, 그 뒤에 NAT가 있는지 없는지는 알 필요가 없다.

  [클라이언트] ----> 공유기 (1.2.3.4:10000)
                       | 포트포워딩
                       v
                    [SFU 서버] (192.168.0.100:10000)

  ※ 클라이언트에겐 1.2.3.4:10000이 SFU 주소
  ※ ICE-Lite candidate에 공유기의 공인 IP를 설정하면 끝

ICE-Lite에서 서버가 클라이언트에게 알려주는 candidate 주소를 공유기의 공인 IP로 설정하기만 하면 된다. 사무실 공유기 뒤에 라즈베리파이 한 대를 놓고 SFU를 돌려도, 포트포워딩 하나면 외부에서 접속할 수 있다. 온프레미스 환경에서 별도의 공인 IP 없이 SFU를 운영할 수 있다는 뜻이다.

결국 핵심은 "서버가 공인 IP를 갖고 있느냐"가 아니라 "클라이언트가 도달할 수 있는 고정된 공인 주소가 있느냐"다. 직접 공인 IP든, 포트포워딩이든, 그 조건만 충족하면 STUN 서버 운영 비용 제로, TURN 서버 대역폭 비용 제로. SFU를 선택하는 순간 따라오는 구조적 이점이다.

다음 편에서는 연결이 수립된 뒤의 이야기를 다룬다. 브라우저와 SFU 사이에 미디어가 오가려면 암호화가 필요하다. DTLS로 키를 교환하고 SRTP로 미디어를 보호하는 과정, 그리고 그게 SFU 구현에서 왜 까다로운지를 해부한다.

다음 편: [SFU 해부학 #3] DTLS + SRTP — 미디어 암호화의 실제


참고 자료


이 글은 OxLens — Rust로 만드는 경량 SFU 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.

profile
Creator of 0xLENS — the world's first AI-native, Rust-built SFU with real-time PTT Floor Control. No fork. No framework. Just Rust and resolve.

0개의 댓글