상태: 실측 (2026-11-01). 가정용 공유기 뒤의 TT Lab 클러스터에서 잰 값입니다. 공유기·클러스터·네트워크 설정은 하나도 바꾸지 않았고, 읽기와 임시 파드만 썼습니다. 주소와 도메인은 문서용 예시(192.0.2.0/24, 203.0.113.0/24,
app.example.com)로 바꿔 적었고, 시간과 횟수는 실제 값입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 원인을 확인하지 못한 해석은 「추정」 으로 표시합니다.
목차
1. 결론부터
공유기 뒤에 웹 서비스를 열고 도메인을 붙였습니다. 휴대폰 데이터로 열면 잘 열리는데, 같은 와이파이에 붙은 노트북에서는 열리지 않습니다. 서버도 멀쩡하고 DNS 도 맞습니다. 이것이 헤어핀 NAT 문제입니다.
- 원인은 공유기입니다. 사설망 안의 클라이언트가 공인 주소로 보낸 요청을 공유기가 다시 사설망 안으로 돌려보내 주어야 하는데(헤어핀), 그 처리를 하지 않거나 목적지만 바꾸면 연결이 맺어지지 않습니다.
- 헤어핀 NAT 는 목적지(DNAT)와 함께 출발지(SNAT)도 바꿉니다. 그래야 서버의 답이 공유기를 거쳐 돌아오고, 공유기가 주소를 되돌려 클라이언트에게 넘길 수 있습니다. NAT 규격은 이것을 「반드시 지원하라(MUST)」 고 합니다.
- 고칠 곳을 바꿀 수 없다면 이름을 바꿉니다. 가장 흔한 답은 사설망 안에서만 같은 도메인이 내부 주소를 가리키게 하는 분할 DNS 입니다. 기기 몇 대라면 hosts 파일로도 됩니다.
- 실측(설정 변경 없음): 노트북·클러스터 노드·파드 세 곳에서 공인 주소로 접속하면 3번 모두 5초 시간 초과였고, 같은 이름으로 내부 주소에 바로 가면 200 이었습니다. 바깥 네 지점 가운데 세 곳은 200 을 받았습니다.
쿠버네티스 안의 같은 모양의 문제(파드가 자기 Service 로 요청하는 경우)는 2편에서 봅니다.
2. 무엇이 문제인가
집이나 작은 사무실의 공유기는 보통 공인 주소 하나를 갖고, 안쪽 기기들에게는 사설 주소를 나눠 줍니다. 바깥에서 들어오는 요청을 안쪽 서버로 보내려면 포트 포워딩을 겁니다. 「공인 주소의 443 으로 오는 것은 서버 192.0.2.10 의 443 으로」 같은 규칙입니다. 공유기는 들어온 패킷의 목적지 주소를 서버 주소로 바꿉니다. 이것을 DNAT(Destination NAT)라고 합니다.
바깥 손님에게는 이것으로 충분합니다. 서버의 답은 기본 경로를 따라 공유기로 가고, 공유기는 기록해 둔 연결 정보로 출발지를 공인 주소로 되돌려 손님에게 보냅니다. 손님은 공인 주소로 보냈고 공인 주소에서 답을 받았으니 문제가 없습니다.
문제는 손님이 같은 사설망 안에 있을 때입니다. 노트북(192.0.2.20)이 app.example.com 을 찾으면 공인 DNS 는 공인 주소(203.0.113.7)를 알려 줍니다.
노트북은 그 주소로 요청을 보내고, 요청은 기본 게이트웨이인 공유기로 갑니다. 여기서 공유기가 할 수 있는 일은 셋입니다.
- 아무것도 하지 않는다. 사설망 쪽에서 들어온 패킷에는 포트 포워딩을 적용하지 않습니다. 공유기는 그 주소를 자기 것으로 보고 받지만 443 에서 듣는 것이 없으니 버리거나 거절합니다.
- 목적지만 바꾼다(DNAT). 아래 그림의 경우입니다.
- 목적지와 출발지를 모두 바꾼다(DNAT + SNAT). 이것이 헤어핀 NAT 입니다.
목적지만 바꾸면
공유기가 목적지만 서버 주소로 바꿔 보내면, 서버가 받은 패킷의 출발지는 노트북의 사설 주소(192.0.2.20)입니다. 서버가 보기에 노트북은 같은 망의 이웃이므로 답을 공유기로 보내지 않고 노트북에게 바로 보냅니다. 답 패킷의 출발지는 서버 주소(192.0.2.10)입니다.
노트북은 203.0.113.7 의 443 과 연결을 맺으려고 SYN 을 보냈습니다. 그런데 돌아온 SYN-ACK 의 출발지는 192.0.2.10 입니다. 노트북의 TCP 는 이것을 자기가 연 연결의 답으로 맞춰 볼 수 없으므로 모르는 패킷으로 처리합니다(보통 RST 로 거절합니다). 노트북은 기다리던 답을 끝내 받지 못하고 SYN 을 몇 번 더 보낸 뒤 시간 초과로 끝납니다. 공유기도 답이 자기를 지나가지 않았으므로 주소를 되돌려 줄 기회가 없습니다.
요점은 요청과 답이 다른 길로 다닌다는 것입니다. NAT 는 한 연결의 두 방향이 모두 자기를 지나야 주소를 바꿨다가 되돌릴 수 있습니다.
출발지도 바꾸면: 헤어핀 NAT
공유기가 목적지와 함께 출발지도 자기 주소로 바꾸면(②), 서버는 요청이 공유기에서 왔다고 보고 답을 공유기로 보냅니다(③). 공유기는 연결 기록대로 두 주소를 되돌려, 출발지 203.0.113.7 로 노트북에게 넘깁니다(④). 노트북은 기다리던 주소에서 답을 받았으므로 연결이 맺어집니다. 패킷이 공유기에 들어왔다가 머리핀처럼 꺾여 같은 쪽으로 나가므로 헤어핀이라고 부릅니다. 루프백 NAT, NAT 리플렉션도 같은 기능을 가리키는 이름입니다.
대가도 있습니다. 서버 쪽에서는 사설망 안의 모든 접속이 공유기 하나에서 온 것처럼 보입니다. 접속 기록으로 사내 사용자를 구분할 수 없고, 출발지 주소로 거는 접근 제한이나 요청 수 제한이 사내 사용자 전체를 한 사람으로 셉니다. 사내 트래픽이 공유기를 두 번 지나므로 공유기의 처리량도 씁니다.
규격은 무엇을 요구하나
NAT 의 동작을 정한 IETF 문서는 헤어핀을 선택 사항으로 두지 않습니다.
- RFC 4787 (UDP) REQ-9: NAT 는 헤어핀을 반드시 지원해야 하고, 헤어핀으로 돌려보낸 패킷의 출발지는 「외부 주소와 포트」(External source IP address and port)여야 합니다.
- RFC 5382 (TCP) 7.2절과 REQ-8: TCP 에서도 같은 요구입니다.
그림에서는 이해하기 쉽도록 출발지를 공유기의 사설 쪽 주소(192.0.2.1)로 그렸지만, 규격이 요구하는 모양은 출발지가 공인 주소와 바깥에서 쓰는 포트로 보이는 것입니다. 어느 쪽이든 답이 공유기로 돌아온다는 핵심은 같습니다. 직접 리눅스로 공유기를 만들 때 흔히 쓰는 「사설망 출발, 서버 도착이면 MASQUERADE」 규칙은 사설 쪽 주소로 바꾸는 방식입니다. 실제 가정용 공유기 가운데는 규격과 달리 헤어핀을 지원하지 않거나 끌 수 있게 둔 제품이 있습니다(이번에 잰 공유기도 2절의 1이나 2에 해당했습니다. 어느 쪽인지는 5절에서 봅니다).
3. 해결책 넷
공유기를 고칠 수 있으면 그것이 가장 깔끔하지만, 늘 그럴 수는 없습니다. 방법은 크게 넷이고, 실제로는 둘 이상을 섞어 씁니다.
| 방법 | 하는 일 | 언제 쓰나 | 단점 |
|---|---|---|---|
| 분할 DNS | 사설망 안의 DNS 가 같은 이름에 내부 주소를 답한다 | 사내 기기·클러스터가 많고 DNS 서버를 직접 운영할 때 | 안과 밖의 답이 달라 「DNS 가 맞는지」 확인할 때 헷갈린다. 내부 주소가 바뀌면 두 곳을 고쳐야 한다. DoH 처럼 사내 DNS 를 건너뛰는 기기에는 듣지 않는다 |
| hosts 파일 | 기기 하나에서 이름을 내부 주소로 고정한다 | 기기 몇 대, 급할 때 | 기기마다 손으로 고쳐야 하고 잊힌다. 노트북을 들고 나가면 바깥에서 그 이름이 안 열린다 |
| 공유기 설정 | 공유기의 헤어핀(NAT 루프백) 기능을 켠다 | 공유기가 지원하고 설정을 바꿀 수 있을 때 | 제품마다 이름과 위치가 다르고 아예 없기도 하다. 서버에는 사내 접속이 공유기 주소로 보인다 |
| 바깥 경유 | 사내 기기가 바깥의 프록시·터널·VPN 을 거쳐 공인 주소로 들어온다 | 바깥 손님이 보는 길을 그대로 시험해야 할 때 | 느리고 비용이 들며, 바깥 장비가 하나 더 고장 날 수 있다 |
몇 가지를 덧붙입니다.
- 분할 DNS 는 TLS 와 잘 맞습니다. 이름이 그대로이므로 인증서의 이름과 SNI·Host 헤더가 바뀌지 않습니다. 내부 주소를 IP 로 직접 치면 인증서 이름이
맞지 않지만, 분할 DNS 와 hosts 는 이 문제가 없습니다. 하나의 명령에서만 잠깐 바꿔 보려면
curl --resolve app.example.com:443:192.0.2.10이 같은 일을 합니다. - 분할 DNS 는 「바깥이 된다」를 보장하지 않습니다. 사내에서 잘 열린다고 바깥에서도 열리는 것은 아닙니다. 바깥 상태는 바깥에서 봐야 합니다(5절).
- 공유기 설정은 이번에 확인하지 않았습니다. 설정을 바꾸지 않는다는 원칙 때문에, 이 공유기에 헤어핀을 켜는 메뉴가 있는지와 켰을 때의 동작은 미실측입니다.
4. 측정 방법
설정을 바꾸지 않고 「같은 이름으로, 공인 주소와 내부 주소 각각에 접속」 하는 것을 세 자리에서 비교했습니다.
| 자리 | 이름을 어떻게 푸나 |
|---|---|
| 노트북(macOS, 사설망) | hosts 파일이 이름을 내부 주소로 고정해 두었다. 공인 주소 쪽은 --resolve 로 강제 |
| 클러스터 노드(우분투, 사설망) | hosts 파일에 오히려 공인 주소가 적혀 있다. 내부 주소 쪽을 --resolve 로 강제 |
| 클러스터 안의 임시 파드 | 클러스터 DNS(CoreDNS)의 hosts 플러그인이 이름에 내부 주소를 답한다(분할 DNS). 공인 주소 쪽은 TCP 연결만 시도 |
명령의 모양은 이렇습니다. 시도마다 5초 제한을 두었습니다.
# 공인 주소로 (DNS 를 건너뛰고 공인 주소를 강제, 이름은 그대로)
curl -sv --max-time 5 --resolve app.example.com:443:203.0.113.7 https://app.example.com/
# 내부 주소(게이트웨이)로
curl -sv --max-time 5 --resolve app.example.com:443:192.0.2.10 https://app.example.com/
바깥 대조는 여러 나라에서 HTTP 요청을 보내 주는 공개 점검 서비스(check-host.net 의 HTTP 점검)로 네 지점에서 했습니다.
5. 실측 결과
| 자리 | 공인 주소로 | 내부 주소로 |
|---|---|---|
| 노트북 | 3/3 시간 초과(5.00~5.01초, 연결 단계에서 멈춤). 포트 80 도 시간 초과 | 3/3 HTTP 200, 연결 2.8~4.0ms, 전체 23~28ms |
| 클러스터 노드 | 3/3 시간 초과(5.00초). hosts 의 공인 주소를 따르는 기본 접속도 시간 초과 | 3/3 HTTP 200, 전체 24~31ms |
| 임시 파드 | TCP 연결 시간 초과(5.0초) | 이름이 내부 주소로 풀려 /healthz 200(136ms, 첫 요청이라 TLS 연결 포함) |
| 바깥 네 지점 | 세 지점 HTTP 200(0.78~1.05초), 한 지점 「Connection reset by peer」 | 해당 없음 |
바깥에서는 열리고 안에서는 공인 주소로 열리지 않는다는 헤어핀 문제의 모양이 그대로 나왔습니다. 2026-10 의 장애 기록에 「사내 헤어핀은 막힘」 이라고 적어 두었는데, 2026-11-01 에도 그대로였습니다. 바깥 한 지점의 연결 거절은 다른 세 지점이 정상이어서 그 지점 쪽 사정으로 보지만 원인은 확인하지 못했습니다(추정).
몇 가지를 더 쟀습니다.
- 공유기가 공인 주소를 직접 가집니다. 사설망에서 공인 주소로 traceroute 를 하면 첫 홉이 그 공인 주소 자신이었고, ping 은 노트북에서 0.65~1.5ms, 노드에서 0.37~0.50ms 로 3/3 답했습니다. 패킷은 공유기까지 가고, 공유기는 그 주소를 자기 것으로 압니다. 통신사의 이중 NAT(CGNAT) 같은 다른 층이 끼어 있지 않다는 뜻입니다.
- 시간 초과였지 거절이 아니었습니다. 공인 주소의 443·80 에서 RST 도 ICMP 오류도 받지 못했습니다. 2절의 세 경우 가운데 「아무것도 하지 않는다(버린다)」 와 「목적지만 바꾼다」 는 클라이언트에게 똑같이 시간 초과로 보입니다.
- 둘 중 어느 쪽인지는 가리지 못했습니다(미실측). 목적지만 바꾸는 공유기라면 게이트웨이 쪽에 노트북 주소에서 온 SYN 이 도착해야 합니다. 게이트웨이
노드의 흐름 기록(Hubble)과 연결 추적 표를 보았지만, 성공한 내부 접속도 이 두 곳에 L4 기록이 남지 않아(게이트웨이가 L7 프록시로 받는 구조) 판별에
쓸 수 없었습니다. 서버 쪽에서
tcpdump를 떠야 가릴 수 있는데, 관리자 권한이 필요해 이번에는 하지 않았습니다. - 노드에서 잰 「내부 주소」 는 사설망을 건너지 않았습니다. 노드에서 게이트웨이 주소로 간 요청은 그 노드 자신의 L7 프록시가 처리한 기록이 남았습니다. Cilium 이 노드의 호스트 네임스페이스에서 LoadBalancer 주소로 가는 연결을 노드 안에서 바로 처리하기 때문입니다. 반면 노트북에서 간 요청은 게이트웨이 주소를 맡은 노드에 노트북 주소로 도착한 기록이 남았습니다. 「내부 주소로 된다」 를 확인할 때는 클러스터 밖의 사내 기기에서 재야 사설망 구간까지 확인됩니다.
6. TT Lab 에서 있었던 일
일반화해서 적으면 이렇습니다.
- 사내에서만 서비스 도메인이 열리지 않았습니다. 바깥 손님은 문제가 없었으므로, 처음에는 사내 기기의 DNS 나 브라우저 캐시를 의심했습니다. 실제로는 공유기가 헤어핀을 하지 않았습니다.
- 사내 시험이 바깥 상태를 알려 주지 못했습니다. 바깥 접속이 정말로 끊긴 장애가 났을 때, 사내에서는 평소에도 안 열리던 이름이라 「안 된다」 가
새 정보가 되지 못했습니다. 반대로 사내에서 내부 주소로 「된다」 도 바깥을 보장하지 않습니다. 그 뒤로 확인은 두 갈래로 나눕니다. 안은
--resolve로 게이트웨이 주소에 직접, 밖은 바깥 점검 서비스로 봅니다. - 해결책을 자리마다 다르게 썼습니다. 클러스터 안에는 CoreDNS 의
hosts플러그인으로 분할 DNS 를 두었습니다(파드에서 이름이 내부 주소로 풀린 것이 이것입니다). 개발용 노트북은 hosts 파일로 고정했습니다. 공유기 설정은 건드리지 않았습니다. - 잊힌 hosts 파일이 하나 있었습니다. 한 노드의 hosts 파일에는 이름이 오히려 공인 주소로 적혀 있어서, 그 노드에서 도메인으로 하는 접속은 언제나 실패했습니다. hosts 파일은 「기기마다 손으로, 그리고 잊힌다」 는 단점이 그대로 드러난 경우입니다.
클러스터 안에서 자기 공인 주소로 나가는 길이 막히면 cert-manager 의 HTTP-01 자체 점검처럼 「바깥에서는 되는데 안에서 확인만 실패하는」 일이 생깁니다. 이것은 cert-manager 는 어떻게 인증서를 만들고 갈아 끼우나에서 다뤘습니다.
7. 한계
- 반복 수가 적습니다. 자리마다 3회, 바깥은 네 지점 1회씩입니다.
- 공유기 한 대입니다. 공유기가 「버리는지」 「목적지만 바꾸는지」 는 가리지 못했고(5절), 헤어핀을 켜는 설정이 있는지와 켰을 때의 동작, 켰을 때 서버가 보는 출발지 주소는 재지 않았습니다(미실측).
- IPv4 만 쟀습니다. 서비스 이름에 IPv6 주소가 없어서 IPv6 의 경우는 재지 않았습니다(미실측).
- 바깥 경유(프록시·터널·VPN)는 비교표에만 적었고 재지 않았습니다(미실측).
8. 정리: 「안에서만 안 돼요」 를 들으면
| 확인 | 이유 |
|---|---|
| 바깥에서 먼저 연다(휴대폰 데이터, 바깥 점검 서비스) | 바깥이 되고 안이 안 되면 헤어핀일 가능성이 크다 |
안에서 --resolve 이름:443:내부주소 |
이것이 되면 서버는 정상이고 길이 문제다 |
| 안에서 공인 주소로 시간 초과인지 | 거절이 아니라 시간 초과면 공유기가 버리거나 목적지만 바꾸는 경우 |
| 해결은 이름 쪽에서 | 분할 DNS(여럿), hosts(몇 대), 공유기 기능이 있으면 켜기 |
| 잊힌 hosts 파일 | 한 기기에서만 다르게 되면 hosts 부터 본다 |
| 확인용 기기의 자리 | 클러스터 노드에서 잰 「된다」 는 사설망을 안 건넜을 수 있다 |
티티 한마디. 우리 집 주소로 편지를 부쳤는데 우체국이 「같은 동네네」 하고 옆집 아저씨 이름으로 답장을 바로 넣어 주면, 저는 모르는 사람 편지라고 버려요. 우체국이 답장도 꼭 우체국 도장으로 찍어 보내 줘야 제가 받아요. 그게 안 되면 그냥 집 안에서는 「옆방!」 하고 부르면 돼요(분할 DNS)!