# 메시 VPN이 직접 연결에 실패하는 지점 - NAT 종류와 릴레이 자가 호스팅
WireGuard 기반 메시 VPN을 깔면 모든 노드가 서로 직접 연결되는 그림을 기대합니다. 실제로는 일부 노드 쌍이 릴레이를 거치고, 그 쌍만 지연이 눈에 띄게 큽니다. 사무실 안에서는 잘되던 것이 재택 환경에서 안 되기도 합니다.
원인은 방화벽 설정이 아니라 NAT의 종류입니다. NAT는 다 같은 NAT가 아니고 어떤 종류는 구멍 뚫기가 원리적으로 어렵습니다. 이 글에서는 직접 연결이 성립하는 조건, 그것이 깨지는 지점, 그리고 마지막 수단인 릴레이를 직접 운영할 때 무엇이 필요한지 정리합니다.
# 1. WireGuard는 NAT 트래버설을 하지 않는다
먼저 층을 나눠야 합니다. WireGuard 자체는 UDP 위의 암호화 터널 프로토콜이고 "상대의 엔드포인트 주소"를 이미 알고 있다는 전제로 동작합니다. 양쪽이 모두 NAT 뒤에 있으면 서로의 공인 주소를 알 방법이 없고, WireGuard는 그것을 알아내 주지 않습니다.
메시 VPN 제품들이 WireGuard 위에 올리는 것이 이 부분입니다.
- 컨트롤 플레인: 노드 목록·공개키·후보 엔드포인트를 교환
- NAT 트래버설: 후보 주소로 동시에 패킷을 쏴 구멍을 뚫는 절차
- 릴레이: 위가 실패했을 때 대신 전달하는 서버
Tailscale 계열에서 이 릴레이를 DERP라고 부릅니다.
DERP (Designated Encrypted Relay for Packets) servers manage device connections and NAT traversal.
이름에 릴레이가 들어가지만 실제로는 두 역할을 겸합니다. 연결 협상 메시지를 주고받는 통로이자, 협상이 실패했을 때의 데이터 경로입니다. 협상 통로 역할 때문에 직접 연결이 잘되는 환경에서도 DERP는 항상 필요합니다. 릴레이를 통한 데이터 전송만 안 일어날 뿐입니다.
# 2. easy NAT과 hard NAT
직접 연결의 성패는 NAT가 매핑을 어떻게 만드느냐로 갈립니다. 핵심 구분은 하나의 내부 소켓이 목적지가 달라져도 같은 외부 포트를 쓰는가입니다.
- endpoint-independent mapping (easy NAT): 내부
192.168.1.10:41641에서 나가는 패킷은 목적지가 어디든 같은 외부 포트에 매핑됩니다. - endpoint-dependent mapping (hard NAT, symmetric NAT): 목적지마다 다른 외부 포트를 만듭니다.
이 차이가 STUN의 동작 여부를 결정합니다. STUN은 "외부 서버에 패킷을 보내고, 서버가 본 내 주소를 되돌려 받는" 방식으로 자기 공인 엔드포인트를 알아냅니다. easy NAT에서는 STUN 서버가 본 주소가 다른 상대에게도 그대로 유효합니다. hard NAT에서는 그렇지 않습니다.
if we use the same socket to send to
5.5.5.5:1234and7.7.7.7:2345, we'll end up with two different ports on 2.2.2.2, one for each destination. If you use the wrong port to talk back, you don't get through.
STUN 서버로 알아낸 포트는 STUN 서버와 대화할 때만 유효한 포트입니다. 그 포트를 상대에게 알려 줘도 상대가 보낸 패킷은 다른 매핑을 찾다가 버려집니다. 여기서 직접 연결이 무너집니다.
이것이 "방화벽을 다 열었는데도 릴레이를 탄다"의 정체입니다. 인바운드를 허용하고 말고의 문제가 아니라, 알려 줄 수 있는 주소가 애초에 존재하지 않는 상황입니다.
# 3. 포트를 추측하는 방법과 그 한계
한쪽만 hard NAT라면 우회가 가능합니다. hard NAT 쪽이 여러 소켓을 열어 두고 easy NAT 쪽이 포트를 추측해 쏘는 방식입니다. 무작위 추측이지만 생일 문제 덕분에 생각보다 효율이 좋습니다. 열린 포트가 256개일 때 확률은 이렇습니다.
| 프로브 횟수 | 성공 확률 |
|---|---|
| 174 | 50% |
| 256 | 64% |
| 1,024 | 98% |
| 2,048 | 99.9% |
2,048번이면 거의 확실합니다. 초당 수백 패킷이면 몇 초 안에 끝납니다.
문제는 양쪽 다 hard NAT일 때입니다. 이때는 서로 상대의 포트를 모르는 상태에서 양쪽 조합을 맞춰야 하므로 경우의 수가 곱해집니다.
For dual hard NATs, reaching 99.9% requires 170,000 probes from each side - roughly 28 minutes at standard rates.
28분입니다. 기술적으로 불가능하지는 않지만 연결을 맺는 시간으로는 쓸 수 없습니다. 양쪽이 모두 hard NAT면 사실상 직접 연결을 포기해야 합니다.
그리고 NAT 종류와 무관하게 막히는 경우도 있습니다.
some networks can break our connectivity much more directly than by having a difficult NAT
UDP를 통째로 막는 네트워크에서는 어떤 트래버설 기법도 의미가 없습니다. 기업 게스트 와이파이나 일부 공용 네트워크가 그렇습니다. 이 경우 TCP 443을 쓰는 릴레이만이 유일한 경로가 됩니다.
정리하면 릴레이가 필요한 조건은 셋입니다.
- 양쪽 모두 hard NAT
- UDP가 차단된 네트워크
- 직접 연결 협상이 끝나기 전의 초기 몇 백 밀리초
3번 때문에 정상적인 환경에서도 연결 초기에는 릴레이를 탑니다. 릴레이로 먼저 통신을 시작해 두고 뒤에서 직접 경로를 찾아 성공하면 조용히 갈아탑니다. "처음엔 느리다가 곧 빨라지는" 현상의 이유입니다.
# 4. 릴레이가 트래픽을 볼 수 없는 이유
릴레이를 거친다는 말이 곧 중간자가 생긴다는 뜻은 아닙니다.
Because Tailscale private keys never leave the local device that generated them, it's impossible for a DERP server to decrypt your traffic. A DERP server blindly forwards already-encrypted traffic from one device to another.
키가 단말을 떠나지 않으므로 릴레이는 암호문을 그대로 전달할 뿐입니다. 그래서 공용 릴레이를 쓰는 것 자체는 기밀성 문제가 아닙니다.
그럼에도 릴레이를 직접 운영하는 이유는 다른 데 있습니다.
- 지연. 릴레이 경로는 A → 릴레이 → B입니다. 릴레이가 지리적으로 멀면 왕복 시간이 크게 늘어납니다. 국내 노드끼리 통신하는데 릴레이가 해외에 있으면 수백 밀리초가 붙습니다.
- 대역폭. 릴레이를 타는 트래픽이 많으면 공용 릴레이의 처리량 제한에 걸립니다.
- 경로 통제. 트래픽이 어느 사업자의 회선을 지나는지 조직이 정해야 하는 경우.
- 폐쇄망. 인터넷에 나갈 수 없는 환경에서는 공용 릴레이 자체에 도달할 수 없습니다.
지연이 가장 실질적입니다. 다만 직접 연결이 잘되는 환경이라면 릴레이 성능은 거의 의미가 없습니다. 릴레이를 세우기 전에 먼저 확인할 것은 "우리 노드들이 실제로 얼마나 릴레이를 타고 있는가"입니다. 그 비율이 낮다면 자가 호스팅의 이득이 거의 없습니다.
# 5. 릴레이를 직접 운영할 때의 요구사항
여기서 아이러니한 제약이 나옵니다.
DERP servers require direct internet connectivity and must not be behind NAT devices (such as firewalls) or load balancers.
NAT 문제를 해결해 주는 서버 자신은 NAT 뒤에 있으면 안 됩니다. 로드 밸런서 뒤에 두는 것도 안 됩니다. 릴레이가 클라이언트를 출발지 주소로 식별하고 HTTP 업그레이드 프로토콜을 그대로 써야 하기 때문입니다. 클라우드에서 세운다면 공인 IP를 직접 붙인 인스턴스여야 하고 관리형 로드 밸런서 뒤에 숨기면 동작하지 않습니다.
나머지 요구사항은 이렇습니다.
| 항목 | 값 | 비고 |
|---|---|---|
| 포트 | HTTP 80, HTTPS 443, STUN 3478/UDP | 변경 시 DERPPort/STUNPort 지정 |
| 도메인 | 필요 | TLS 인증서 발급용 |
| ICMP | 인바운드·아웃바운드 허용 | 지연 측정에 사용 |
| RegionID | 900~999 | 사용자 지정 영역으로 예약된 범위 |
실행 자체는 단순합니다.
go install tailscale.com/cmd/derper@latest
# 도메인만 주면 443에서 TLS로 뜬다
sudo derper --hostname=derp.example.com --verify-clients
--verify-clients가 중요합니다. 이 플래그가 없으면 릴레이가 누구에게나 열립니다. 릴레이는 인증 없이 암호문을 전달하는 서버이므로, 공개해 두면 남의 트래픽 중계에 쓰일 수 있습니다. 이 플래그는 선택이 아니라 기본으로 켜야 합니다. 다만 클라이언트를 검증하려면 릴레이 서버 자신도 tailnet에 참여해 있어야 합니다.
그다음 네트워크 정책에 릴레이를 등록합니다.
{
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "self-seoul",
"Nodes": [
{
"Name": "900a",
"RegionID": 900,
"HostName": "derp.example.com",
"IPv4": "203.0.113.10"
}
]
}
}
}
}
OmitDefaultRegions가 정책적 선택 지점입니다. true로 두면 자체 릴레이만 씁니다. 폐쇄망이라면 그래야 하지만 자체 릴레이가 죽으면 릴레이가 필요한 노드 쌍은 통신이 끊깁니다. false로 두면 공용 릴레이가 폴백으로 남습니다. 릴레이를 단일 인스턴스로 운영하면서 기본 영역까지 꺼 두는 조합이 가장 위험합니다.
IPv4를 명시하는 것도 실무적으로 중요합니다. 생략하면 클라이언트가 DNS를 먼저 풀어야 하는데, VPN이 아직 안 붙은 상태에서 DNS가 안 되는 환경이면 릴레이에 접속하지 못합니다.
# 6. 감수해야 하는 제약
문서가 명시하는 한계가 있습니다.
Running your own DERP servers is an advanced operation that requires significant resources on your part to set up and maintain.
그리고 자체 릴레이는 기기 공유 기능을 지원하지 않고 컨트롤 플레인 최적화의 일부를 받지 못하며 특정 exit node 구성과 호환되지 않습니다. 기능이 줄어드는 대가로 경로를 얻는 구조입니다.
운영 비용도 과소평가하기 쉽습니다. 릴레이는 상태를 거의 갖지 않지만 가용성이 곧 연결성입니다. 릴레이가 죽으면 hard NAT 뒤 노드들이 서로 못 봅니다. 최소 두 대를 다른 장애 도메인에 두거나, 기본 영역을 폴백으로 남겨 두는 설계가 필요합니다.
# 7. 직접 확인하는 방법
먼저 자기 네트워크가 easy인지 hard인지 확인합니다.
tailscale netcheck
출력에서 볼 것은 목적지에 따라 매핑이 달라지는지를 알려 주는 항목입니다. 이 값이 참이면 hard NAT이고 3절의 조건에 해당합니다. UDP 자체가 안 되는지, 어느 릴레이가 가장 가까운지(지연 표)도 여기서 나옵니다.
그다음 실제 경로를 봅니다.
# 피어별로 direct인지 relay인지
tailscale status
# 특정 피어까지의 경로 - 릴레이로 시작해 직접으로 바뀌는 과정이 보인다
tailscale ping <peer>
tailscale status에서 direct 203.0.113.5:41641처럼 나오면 직접 연결, relay "sel"처럼 나오면 릴레이 경유입니다. tailscale ping은 몇 번 반복하면 릴레이 응답에서 직접 응답으로 전환되는 순간을 보여 줍니다. 계속 릴레이에 머문다면 직접 연결이 실패한 것입니다.
릴레이 비율을 세어 두면 자가 호스팅의 필요성을 숫자로 판단할 수 있습니다.
# 전체 피어 중 릴레이를 타는 비율
tailscale status | awk 'NF>3 {total++; if ($0 ~ /relay/) r++} END {printf "relay %d / %d\n", r, total}'
자체 릴레이를 세운 뒤에는 그것이 실제로 쓰이는지 확인합니다.
# 릴레이 서버가 응답하는지
curl -sI https://derp.example.com/derp | head -1
# 클라이언트가 인식한 릴레이 영역과 지연
tailscale netcheck | grep -A20 'DERP latency'
# 8. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| 일부 노드 쌍만 지연이 큼 | 양쪽 hard NAT으로 릴레이 경유 | netcheck로 확인, 릴레이 위치 조정 |
| 방화벽을 열었는데도 릴레이 | 알려 줄 공인 엔드포인트가 없음 | 포트 개방과 무관, NAT 종류 문제 |
| 연결 직후만 느림 | 협상 중 릴레이 사용 후 전환 | 정상 동작, 몇 초 후 직접 전환 확인 |
| 특정 네트워크에서만 전부 릴레이 | UDP 차단 | TCP 443 릴레이 경로 확보 |
| 자체 릴레이가 안 잡힘 | NAT·LB 뒤에 배치 | 공인 IP 직결 인스턴스로 이전 |
| 릴레이 접속만 실패 | DNS 미해결 | 정책에 IPv4 명시 |
| 릴레이 장애 시 통신 두절 | 기본 영역을 꺼 둠 | OmitDefaultRegions: false 또는 이중화 |
| 모르는 트래픽이 릴레이를 지남 | 클라이언트 검증 미설정 | --verify-clients |
# 9. 마무리
- 직접 연결의 성패는 방화벽이 아니라 NAT가 목적지별로 다른 매핑을 만드는지로 갈립니다. hard NAT에서는 STUN으로 알아낸 주소가 상대에게 무효합니다.
- 한쪽만 hard NAT면 포트 추측으로 몇 초 안에 뚫립니다. 양쪽 다 hard NAT면 99.9%에 도달하는 데 28분이 걸려 실용적이지 않습니다.
- 릴레이는 마지막 수단이자 협상 통로입니다. 직접 연결이 잘되는 환경에서도 릴레이 자체는 항상 필요합니다.
- 릴레이는 암호문만 전달하므로 공용 릴레이를 쓴다고 기밀성이 깨지지 않습니다. 자가 호스팅의 실익은 지연과 경로 통제입니다.
- 릴레이 서버는 NAT나 로드 밸런서 뒤에 두면 동작하지 않습니다. 그리고 릴레이 가용성이 곧 일부 노드 쌍의 연결성이므로, 단일 인스턴스 + 기본 영역 비활성화 조합은 피해야 합니다.
- 세우기 전에 먼저 릴레이 경유 비율을 재 봅니다. 그 값이 낮으면 자가 호스팅의 이득도 낮습니다.