NAT 뒤의 두 컴퓨터는 어떻게 연결되나 — STUN과 홀펀칭

네트워크5분

집이나 사무실 컴퓨터에는 바깥에서 부를 수 있는 주소가 없다. 공유기 뒤에 숨어 있기 때문이다. 그래서 공유기 뒤의 두 컴퓨터는 서로에게 먼저 연결할 수 없다.

이 글은 그 문제를 어떻게 푸는지 정리한다. 순서는 NAT → STUN → 홀펀칭 → 중계 서버다.

공유기는 나간 것만 기억한다

집에 컴퓨터가 여러 대 있어도 바깥에 보이는 주소는 하나뿐이다. 공유기가 주소를 바꿔치기해서 내보낸다.

내 컴퓨터가 보냄:   192.168.0.5 의 40000번 →  구글
공유기가 바꿔서:     1.2.3.4 의 54321번    →  구글

공유기가 적어둠:    54321번으로 오는 답장 = 192.168.0.5 의 40000번

메모는 나갈 때만 만들어진다. 그래서 바깥에서 갑자기 뭔가 들어오면 공유기는 집 안 누구에게 줘야 할지 모른다. 그냥 버린다. 메모를 만드는 방법은 내가 먼저 나가는 것 하나뿐이고, 메모는 공유기가 관리하므로 바깥에서는 손댈 수 없다.

내 바깥 주소는 내가 모른다

공유기가 54321번을 골랐다는 걸 내 컴퓨터는 모른다. 공유기가 알려주지 않기 때문이다.

그래서 바깥에 있는 서버에 물어본다. 아무 메시지나 하나 보내면 이렇게 답해준다.

너는 나한테 1.2.3.4의 54321번으로 보인다.

이 역할을 하는 서버를 STUN이라고 부른다. 하는 일은 이것뿐이다. 거울 같은 것이다.

주의할 점이 하나 있다. STUN은 물어본 사람에게만 답한다. 상대에게 알려주지 않는다. 알아낸 주소를 상대에게 전달하는 건 다른 일이다.

STUN을 정의한 RFC 8489도 이 경계를 첫 문장에서 그어 둔다.

STUN is not a NAT traversal solution by itself. Rather, it is a tool to be used in the context of a NAT traversal solution.

거울 노릇만 하고 빠진다. 그 주소를 상대에게 옮기는 일, 동시에 두드리는 일은 전부 다른 단계다.

서로 동시에 두드린다

이제 A와 B가 각자의 바깥 주소를 알았다. 서로 알려주고 연결을 시도한다.

A가 B에게 보낸다.

A쪽 공유기를 나갈 때  →  메모 생김          → A쪽 문 열림
B쪽 공유기에 도착     →  메모 없음          → 버려짐

A가 보낸 것은 B에게 도착하지 못한다. 하지만 A쪽 공유기에 메모가 생겼다.

B가 A에게 보낸다.

B쪽 공유기를 나갈 때  →  메모 생김          → B쪽 문 열림
A쪽 공유기에 도착     →  아까 만든 메모 있음 → 통과

이제 서로 통한다.

첫 번째 메시지는 상대에게 닿으려고 보내는 것이 아니다. 내 쪽 문을 열려고 보내는 것이다. 그래서 양쪽이 다 보내야 한다. 한쪽만 보내면 영원히 안 된다.

이 방식을 홀펀칭(구멍 뚫기)이라고 부른다.

안 되는 경우도 있다

공유기 중에는 보내는 상대가 바뀔 때마다 번호를 새로 고르는 것이 있다.

그러면 STUN에게 물어봤을 때 들은 번호(54321)와, 실제로 상대에게 보낼 때 쓰는 번호가 다르다. 상대에게 알려준 번호가 틀린 번호가 되는 것이다. 이런 공유기 뒤에서는 홀펀칭이 실패한다.

처음 만나는 자리가 필요하다

여기서 문제가 하나 더 있다. A와 B는 서로의 주소를 어떻게 주고받나? 아직 연결이 없는데 주소를 알려줄 방법이 없다.

그래서 양쪽이 다 접속할 수 있는 중계 서버를 하나 둔다. 둘 다 그곳으로 먼저 나간다. 나가는 연결이니까 공유기가 막지 않는다. A가 접속해 기다리고, B가 접속해 “A에게 이 주소를 전해달라”고 부탁하면 중계 서버가 전달하고, 둘이 동시에 두드린다(홀펀칭). 성공하면 직접 통신하고, 실패하면 계속 중계 서버를 거친다.

뚫고 나서도 중계 서버는 남는다

여기가 흔히 오해하는 부분이다. 직접 연결이 되면 중계 서버가 필요 없어질 것 같지만 그렇지 않다. 세 가지 이유가 있다.

기다리는 쪽은 계속 접속해 있어야 한다. A의 주소를 아무도 모르기 때문에, 누군가 A를 찾으려면 중계 서버를 통할 수밖에 없다. A가 중계 서버와의 연결을 끊으면 아무도 A를 찾을 수 없다.

뚫어놓은 구멍은 저절로 막힌다. 공유기의 메모에는 유효기간이 있다. 보통 30초에서 몇 분 사이다. 조용하면 지워진다. 그래서 양쪽이 주기적으로 아무 메시지나 보내서 메모를 새로 고쳐야 한다.

여기서 방향을 헷갈리기 쉽다. 내 쪽 메모는 내가 보내야 갱신된다. 상대가 보내주는 걸 받는 것으로는 안 된다. 그래서 양쪽이 각자 보낸다.

주소가 바뀌면 다시 알려야 한다. 노트북을 와이파이에서 휴대폰 테더링으로 바꾸면 바깥 주소가 달라진다. 새 주소를 알리는 통로가 또 중계 서버다.

동시에 두드려야 한다는 규칙도 근거가 있다. NAT 통과 방식을 정리한 RFC 5128은 이 절차를 이렇게 적는다.

Suppose A starts sending UDP messages to B’s public endpoint, and simultaneously relays a request through server S to B, asking B to start sending UDP messages to A’s public endpoint.

한쪽만 보내는 순간에는 그 쪽 문만 열린다. 상대가 뒤따라 보내지 않으면 열린 문은 아무 소용이 없다. 그래서 순서가 아니라 동시성이 조건이다.

그래서 무엇을 확인하나

중계 서버를 직접 운영한다면 이 글에서 나온 규칙이 그대로 용량 산정 기준이 된다.

대역폭이 아니라 동시 접속 수를 본다. 직접 경로가 뚫린 뒤에도 트래픽만 빠질 뿐 접속은 그대로 남는다. 기다리는 쪽의 주소이자 구멍을 다시 여는 통로이기 때문이다. 서버 사양을 대역폭으로 잡으면 실제 한계에 못 미친다.

클라이언트 쪽 유지 신호(keepalive) 주기를 공유기 매핑 유지 시간보다 짧게 잡는다. 30초에서 몇 분 사이로 다양하므로, 가장 짧은 값을 기준으로 잡아야 아무 공유기에서도 안전하다.

어떤 공유기 뒤에서는 홀펀칭 자체가 안 된다는 걸 전제로 설계한다. 상대가 바뀔 때마다 번호를 새로 고르는 공유기가 있고, 그 비율을 0으로 만들 방법은 없다. 그래서 중계 서버는 실패했을 때의 대안이 아니라 처음부터 전량을 버틸 수 있어야 하는 경로다.

References