결론: 포트포워딩처럼 우리집 공유기에 구멍을 녡는 게 아니라, Tailscale이 운영하는 공개 서버가 중계해준다.
# ① 이 맥(tailnet 멤버)에서 조회
$ dig +short k3s-master.taildcdcee.ts.net
100.103.119.1 # tailnet 안에서만 유효
# ② 우리집 인터넷의 실제 공인 IP
$ curl -s ifconfig.me
124.80.125.211
# ③ 진짜 외부인이 보는 것과 동일하게, 공용 DNS에 직접 질의
$ dig +short k3s-master.taildcdcee.ts.net @8.8.8.8
103.84.155.153
103.84.155.217
①은 MagicDNS가 지름길로 사설 IP를 알려준 것 — 외부인은 이 경로를 못 탄다. ③이 진짜 외부인이 보는 값인데, 우리집 공인 IP도 사설 tailnet IP도 아닌 제3의 IP 대역이 나온다 — 이게 Tailscale이 직접 운영하는 Funnel 공개 엔드포인트다.
외부 클라이언트 ──(인바운드 TCP 연결)──▶ 우리집 공유기(공인 IP:포트) ──NAT──▶ 내부 서버
공유기가 밖에서 들어오는 연결을 직접 받아서 내부로 꽂아주는 구조. 진짜 공인 IP 필요(CGNAT이면 불가능), 공유기 관리자 권한 필요, 포트가 그대로 인터넷에 노출되어 포트스캔 봇의 대상이 됨.
k3s-master ──(아웃바운드 연결을 미리 맺어둠)──▶ Tailscale 공개 서버(103.84.x.x)
외부 클라이언트 ──(평범한 HTTPS)──▶ Tailscale 공개 서버 ──(그 터널로 릴레이)──▶ k3s-master
k3s-master는 인바운드 포트를 여는 게 아니라, 밖으로 나가는 연결만 미리 맺어둔다. 아웃바운드 연결은 거의 모든 공유기/방화벽이 기본 허용하므로 공유기 설정을 하나도 안 건드려도 된다. 그 결과 우리집 공인 IP(124.80.125.211)는 외부에 아예 노출되지 않고, CGNAT 환경이든 상관없이 동작한다.
답은 외부인이 tailnet 안으로 들어오는 게 아니기 때문이다.
<table header-row="true"><tr><td>층위</td><td>접속 주체</td><td>Tailscale 계정 필요?</td></tr><tr><td>tailnet mesh (100.x.x.x)</td><td>같은 계정에 로그인된 기기끼리</td><td>필요</td></tr><tr><td>Funnel URL (https://*.ts.net)</td><td>아무나</td><td>불필요</td></tr></table></td><td>아무나</td><td>불필요</td></tr></table>)
Funnel URL은 브라우저 입장에서 그냥 평범한 공인 웹사이트일 뿐이다 — tailnet 안으로 외부인을 "초대"하는 게 아니라, 우리 노드가 Tailscale의 공개 인프라를 빌려서 서비스 하나만 일반 웹사이트처럼 대신 내보내주는 것이 Funnel의 정체다.
$ tailscale funnel status # k3s-worker1/2에서
No serve config
Funnel/Serve 설정은 클러스터나 계정 단위가 아니라 각 노드의 tailscaled가 로컬로 들고 있는 상태라, k3s-master에서만 켤다면 딜 그 노드만 공개된다. 공개 가능한 포트 슬롯은 443/8443/10000 세 개로 고정되어 있다 — 지금은 443 슬롯 하나(localhost:80)만 쓰는 중.
포트 22(SSH)는 이 목록에 없으므로 인터넷에서 SSH로 접속하는 건 불가능하다.