Tailscale과 내 미니 PC 동작 방식
들어가며: 나는 Tailscale 을 믿고만 썼다
집 미니 PC 로 셀프 호스팅 (서버를 직접 운영하는 것) 을 하면서, 접속 문제를 하나씩 해결해 나갔다.
- 처음엔 공유기 포트 포워딩 + ISP 공인 IP. 가장 단순하지만 자꾸 깨졌다.
- ISP 가 공인 IP 를 예고 없이 바꾸면, 도메인이 옛 IP 를 가리켜 블로그가 며칠간 외부에서 안 열렸다. 이건 DDNS 로 막았다 (10분마다 공인 IP 를 확인해 DNS 를 자동 갱신). 사고 기록은 집에서 내 서버에 도메인으로 접속이 안 되는 이유 (NAT Loopback) 에 있다.
- 그래도 내가 내 서버에 닿는 건 여전히 불편했다. 집에서는 NAT 헤어핀 (hairpin) 때문에 도메인으로는 접속이 안 돼
/etc/hosts를 손으로 토글하고, 카페에서는 또 다른 IP 를 쓰고, 라우터엔 22번 포트가 인터넷에 열려 있었다. 여기서 Tailscale 로 옮겼다.
그런데 솔직히 고백하면, 그때 나는 Tailscale 이 어떻게 동작하는지 하나도 몰랐다. 집에서 내 서버에 도메인으로 접속이 안 되는 이유 (NAT Loopback) 글에 이렇게까지 써놓고도 그랬다.
통신이 VPN 터널을 통해 직접 전달되므로 공유기의 NAT 를 거치지 않는다.
100.x.x.x, "mesh VPN", "coordination 서버" 같은 말을 뜻도 모른 채 사실처럼 적었던 것이다. 한마디로 Tailscale 을 블랙박스 로 쓰고 있었다. 이 글은 내가 믿고만 쓰던 그 블랙박스를 직접 열어본 기록이다. (NAT loopback 자체의 원리는 집에서 내 서버에 도메인으로 접속이 안 되는 이유 (NAT Loopback) 에서 다뤘으니 여기선 반복하지 않는다.)
그림으로 보는 세 가지 접근
말로 풀기 전에, 내가 거쳐온 세 방식을 그림으로 먼저 본다.
① 포트 포워딩 + 공인 IP (처음 방식, 단순하지만 가장 잘 깨짐)
flowchart TD
User["외부에서 접속"] -->|"공인 IP로"| Router["집 공유기<br/>포트 포워딩"]
Router -->|"22번 포트 전달"| MiniPC["미니 PC"]
Router -.-> Problem["문제: 공인 IP가 바뀌면 끊김<br/>집에서는 도메인 접속 불가 (NAT 헤어핀)<br/>22번 포트가 인터넷에 노출"]:::warn
classDef warn fill:#f8d7da,stroke:#dc3545,color:#721c24
② DDNS (공인 IP 가 바뀌어도 방문자 접속 유지)
flowchart TD
MiniPC["미니 PC"] -->|"10분마다 공인 IP 확인"| Check{"IP가 바뀌었나?"}
Check -->|"예"| Update["Porkbun DNS 자동 갱신"]
Check -->|"아니오"| Skip["그대로 둠"]
Update --> DNS["DNS는 늘 최신 공인 IP"]
Visitor["방문자"] -->|"도메인 조회"| DNS
DNS --> Good["공개 사이트 접속 유지<br/>(내 접근 문제는 그대로)"]:::ok
classDef ok fill:#d4edda,stroke:#28a745,color:#155724
③ Tailscale (내가 어디서든 내 서버에 닿는 문제를 해결)
flowchart TD
subgraph CP["Control plane (소개만)"]
Coord["coordination 서버"]
end
Laptop["노트북<br/>100.x 고정 주소"]:::ok
MiniPC["미니 PC<br/>100.x 고정 주소"]:::ok
Laptop -.->|"공개키 올리고 소개받기"| Coord
MiniPC -.->|"공개키 올리고 소개받기"| Coord
Laptop ==>|"직접 암호화 연결"| MiniPC
Laptop -.->|"직접 연결 실패 시"| DERP["DERP 중계<br/>(여전히 암호화)"]
DERP -.-> MiniPC
classDef ok fill:#d4edda,stroke:#28a745,color:#155724
①과 ②는 주로 "방문자가 내 사이트에 닿는" 공개 경로 이야기고, ③은 "내가 내 서버에 닿는" 경로를 바꿔놓은 것이다. 이제 ③의 속을 하나씩 열어본다.
큰 그림: 소개만 해주는 중매자 (control plane vs data plane)
Tailscale 을 이해하는 열쇠는 하나다. 연결을 주선하는 일 과 실제 데이터가 흐르는 일 이 분리돼 있다는 것.
데이팅 앱을 떠올려 보자. 앱은 두 사람을 소개하고 연락처를 건네줄 뿐, 둘의 통화에는 끼어들지 않는다. 소개가 끝나면 둘은 직접 통화한다.
- Control plane (제어 평면, 연결을 주선하는 층) = 중매 앱. Tailscale 의 coordination 서버 (
login.tailscale.com) 다. 내 기기들을 알고, 각 기기의 공개키 (public key) 와 접근 규칙을 나눠준다. 하는 일은 소개가 전부다. - Data plane (데이터 평면, 실제 트래픽이 흐르는 층) = 실제 통화. 일단 소개가 끝나면 두 기기는 자기들끼리 암호화된 회선으로 직접 주고받는다. Tailscale 서버는 이 트래픽을 보지도, 나르지도 않는다.
이 한 줄만 기억하면 된다. 회사 Tailscale 은 내 기기들을 소개해줄 뿐, 데이터는 내 기기끼리 곧장 간다. 그래서 빠르고 (멀리 우회하지 않음), 사적이다 (그들이 못 읽음).
이렇게 모든 기기가 중앙 서버를 거치지 않고 서로 직접 연결되는 구조를 mesh VPN (그물망 VPN) 이라고 부른다. 옛날 회사 VPN 처럼 트래픽이 한 서버로 모이는 hub-and-spoke (중앙 집중식) 와는 반대다.
데이터를 잠그는 자물쇠: WireGuard
앞에서 두 기기가 암호화된 회선으로 직접 주고받는다고 했다. 그럼 그 회선은 무엇으로 암호화할까? WireGuard 라는 경량 VPN 프로토콜이다. 공개키 암호 (public-key cryptography) 를 쓴다.
자물쇠 비유가 쉽다.
- 공개키 (public key) = 열린 자물쇠. 누구에게나 나눠준다. (중매자가 뿌린 게 이것이다.)
- 비밀키 (private key) = 그 자물쇠를 여는 유일한 열쇠. 기기 밖으로 절대 안 나간다.
내 노트북이 미니 PC 에 데이터를 보낼 때, 미니 PC 의 열린 자물쇠로 잠근다. 그걸 여는 건 미니 PC 의 비밀키뿐이다. 그래서 라우터도, ISP 도, 심지어 Tailscale 도 못 읽는다. Tailscale 은 열린 자물쇠 (공개키) 만 갖고 있었지, 여는 열쇠 (비밀키) 는 가진 적이 없다.
핵심 반전은 이것이다. 중매자는 모두를 소개해줄 수 있으면서도, 누구의 편지도 읽지 못한다. 안 읽는 게 아니라 못 읽는 것이다.
기술 메모: WireGuard 는 키 교환에 Curve25519, 패킷 암호화에 ChaCha20-Poly1305 를 쓴다. 둘 다 현대적이고 빠른 방식이다.
포트 하나 안 열고 어떻게 직접 연결될까: NAT traversal
내 미니 PC 도, 노트북도 집 공유기 (NAT) 뒤에 있다. 바깥에서는 공유기의 공인 IP 하나만 보이고, 안의 특정 기기를 직접 부를 수 없다. (이 NAT 의 구조와 hairpin 문제는 NAT (네트워크 주소 변환) 과 집에서 내 서버에 도메인으로 접속이 안 되는 이유 (NAT Loopback) 에서 자세히 다뤘다.)
예전 방식이 포트 포워딩이었다. "외부에서 22번으로 오는 요청은 미니 PC 로 보내라" 는 안내문을 공유기에 붙이는 방식이다. 손이 가고, 인터넷에 포트가 노출된다.
Tailscale 은 NAT traversal (NAT 통과), 일명 홀 펀칭 (hole punching) 을 쓴다. 원리는 이렇다.
- 공유기는 안에서 먼저 나간 연결의 응답은 통과시킨다. (피자를 주문하면, 배달이 돌아올 길을 잠깐 열어두는 것과 같다.)
- 그래서 coordination 서버가 양쪽에게 서로의 공인 주소를 알려주고, 두 기기가 동시에 바깥으로 패킷을 쏘게 만든다.
- 양쪽 공유기가 각각 "우리 집 기기가 먼저 나갔다" 하고 길을 열고, 두 흐름이 중간에서 만난다. 포트 포워딩 안내문이 전혀 필요 없다.
다시 강조하면, 중매자는 주소를 알려주고 타이밍만 맞춰줄 뿐, 실제 데이터는 직접 흐른다 (data plane).
직접 연결이 막히면: DERP
가끔 양쪽 공유기가 너무 빡빡해서 (엄격한 방화벽, 일부 회사·통신사 망) 홀 펀칭이 실패한다. 그럴 땐 DERP (Designated Encrypted Relay for Packets, 지정 암호화 패킷 중계 서버) 라는 중립 지대를 거친다. 양쪽이 여기에 (여전히 잠긴) 상자를 맡기고 찾아가는 방식이다. 중계 지점에서도 내용은 못 읽는다. 조금 느리지만 항상 연결된다.
안 변하는 주소: 100.x.x.x
내 옛 고생의 절반은 "주소가 자꾸 바뀐다" 였다. 공인 IP 는 ISP 가 멋대로 바꾸고, 집과 카페마다 경로가 달랐다.
Tailscale 은 각 기기에 100.x.y.z 형태의 고정 주소 를 준다. 물리 네트워크가 어떻게 바뀌든 이 주소는 그대로다. 그래서 공인 IP 가 뭔지 신경 끄고, 이 주소 (또는 MagicDNS 이름) 로만 닿으면 된다.
왜 하필
100.x일까?100.64.0.0/10은 RFC 6598 이 정의한 "공유 주소 공간 (shared address space)" 이라, 집 안 사설 IP (192.168.x.x등) 와 충돌하지 않도록 고른 것이다.
말로만 하지 않고, 직접 확인했다
글로 배운 걸 내 진짜 서버에서 확인해봤다. (ssh evan@evanshlee.com 으로 접속해 읽기 전용 명령만 실행했다.)
| 확인한 것 | 결과 | 무슨 뜻인가 |
|---|---|---|
| 공인 IP vs 공개 DNS | 둘 다 일치 (실제 값 가림) | DDNS 정상. 도메인이 옛 IP 를 안 가리킴 |
| DDNS 타이머 | 3분 전 실행, 10분 간격 | DDNS 가 살아서 공개 사이트를 지키는 중 |
| 노트북 ↔ 미니 PC | direct (중계 안 거침) |
data plane 이 정말 직접 연결 |
| 가장 가까운 DERP | Toronto, 3.5ms | 백업 중계가 살아있고 가까움 |
| NAT 종류 | MappingVariesByDestIP: false + UPnP |
"쉬운 NAT", 홀 펀칭이 잘 됨 |
| Tailscale IP | 100.x.x.x (개인 tailnet 주소) |
안 변하는 그 주소 |
| 서비스·가동 | nginx·api·ssh active, uptime 43일 | 서버 건강 |
(노트북과 미니 PC 가 지금은 같은 집 네트워크라 LAN 으로 직접 연결됐고, 떨어져 있을 땐 인터넷 너머로 홀 펀칭을 시도한다. 그래서 DERP 는 평소엔 거의 안 쓴다.)
그리고 제일 마음에 든 건 이것이다. 방금 확인에 쓴 그 SSH 접속 자체가 Tailscale 위를 달리고 있었다. 이 맥북과 미니 PC (둘 다 100.x 대역 tailnet 주소) 가 직접 이야기했고, 상태창에 그 링크가 direct 라고 찍혔다. Tailscale 을 들여다보는 데 쓴 도구가, 그 자체로 Tailscale 위에서 직접 (peer-to-peer) 돌고 있었던 것이다. 이보다 분명한 증거는 없다.
그래서 Tailscale 이 푼 것과, 안 푼 것
오해를 하나 풀고 싶다. 한동안 나는 "공인 IP 가 바뀌어 블로그가 죽은 문제" 까지 Tailscale 이 풀었다고 생각했다. 아니었다.
- 공개 사이트가 살아있는 이유 = DDNS. 방문자는 여전히 공인 경로로 들어온다. Tailscale 의
100.x는 사적인 주소라 외부인은 못 본다. (공개까지 Tailscale 로 하려면 Funnel 이라는 별도 기능이 필요한데, 나는 안 쓴다.) - 내가 어디서든 내 서버에 닿는 이유 = Tailscale. 고정
100.x주소 + 직접 암호화 링크. NAT 헤어핀도 사라지고, 라우터 22번 포트도 닫을 수 있었다.
두 경로를 한 그림에 겹쳐 보면 이렇게 갈린다.
flowchart TD
subgraph Me["나의 접속 (사적 경로)"]
MyDevice["내 노트북·폰<br/>어디서나"]
end
subgraph Guests["방문자 접속 (공개 경로)"]
Visitor["일반 방문자"]
end
MyDevice -->|"Tailscale 고정 100.x<br/>직접 암호화 연결"| MiniPC["미니 PC<br/>(블로그·SSH)"]
Visitor -->|"도메인 조회"| DNS["공개 DNS<br/>DDNS가 최신 IP 유지"]
DNS --> PubIP["공인 IP"]
PubIP -->|"공유기 포트 포워딩"| MiniPC
classDef priv fill:#d4edda,stroke:#28a745,color:#155724
classDef pubc fill:#cce5ff,stroke:#004085,color:#004085
class MyDevice priv
class Visitor,DNS,PubIP pubc
같은 미니 PC 로 가지만 길이 완전히 다르다. 내 접속은 Tailscale (사적 경로), 방문자는 공개 DNS 와 포트 포워딩 (공개 경로) 을 탄다. 두 도구, 두 역할이다.
마치며
블랙박스를 열기 전에도 Tailscale 은 잘 동작했다. 하지만 열어보고 나니 "왜 이게 필요한지, 언제 DERP 로 빠지는지, 무엇을 Tailscale 에 맡기면 안 되는지" 가 또렷해졌다. 도구를 믿는 것과 이해하는 건 다르다.
이번 Tailscale 글은 Claude Code 와 함께 작성했다. 내가 뭘 알고 있고 뭐가 부족한지 구분했다. draft 글을 확인하고 내가 읽기에 어색한 부분이 있다면 고쳤다. 내가 읽어도 모르는 부분이 있으면 다시 물어보고 알아내어 넣었다. 아직도 모르는 부분이 있을지도 모르지만, 100% 완성은 없기에 발행한다. 계속 고쳐보자.
관련 글
- 집에서 내 서버에 도메인으로 접속이 안 되는 이유 (NAT Loopback) - 집에서 도메인 접속이 안 되던 이유, 그리고 Tailscale 로 옮긴 계기
- 원격 서버의 localhost를 내 컴퓨터에서 사용하기 (SSH 포트 포워딩) - SSH 터널로 원격 개발 서버 접근
- NAT (네트워크 주소 변환) - NAT 기본 개념
References
- Tailscale - How Tailscale works - control/data plane, coordination 서버, NAT traversal, DERP
- WireGuard 백서 (PDF) - Curve25519 키 교환, ChaCha20-Poly1305 암호화
- RFC 6598 -
100.64.0.0/10공유 주소 공간 정의 - Tailscale - DERP - 중계 서버 동작