[CS300 #146] NAT — 주소를 바꿔 쓰는 장비가 연결을 기억하는 방법
컴퓨터공학 300 주제 시리즈의 146번째 글이다. 전체 지도는 여기.
한 줄 요약
NAT(Network Address Translation)는 패킷이 경계를 지날 때 IP 주소(와 포트)를 바꿔 쓰고, 그 대응을 표에 기억해 돌아오는 응답을 원래 주인에게 되돌려 준다.
왜 필요한가
IPv4 주소는 32비트, 약 43억 개다. 집과 회사의 수많은 기기에 공인 주소를 하나씩 줄 수는 없다. 그래서 내부에서는 사설 주소(RFC 1918)를 쓰고, 경계 장비 하나가 공인 주소 하나로 바꿔 내보낸다. 오늘날 대부분의 가정용 공유기, 클라우드의 NAT 게이트웨이, 쿠버네티스의 kube-proxy 가 모두 NAT 를 한다.
NAT 는 편리한 만큼 문제의 원인도 된다. “밖에서 안으로 연결이 안 된다”, “오래 놀던 연결이 갑자기 끊긴다”, “서버 로그에 클라이언트 IP 가 전부 같은 주소로 찍힌다” 같은 일은 대부분 NAT 가 범인이다.
핵심 개념
기본 NAT 와 NAPT
RFC 3022 는 전통적 NAT 를 두 가지로 설명한다.
- 기본 NAT: IP 주소만 1:1 로 바꾼다. 내부 호스트 수만큼 공인 주소가 필요하다.
- NAPT(Network Address Port Translation): IP 와 함께 TCP/UDP 포트까지 바꾼다. 공인 주소 하나를 수많은 내부 호스트가 나눠 쓴다. 흔히 “NAT” 라고 하면 이것이다. 리눅스에서는 “masquerade” 라고도 부른다.
내부 10.0.0.10:51000 ---> [NAPT 장비] ---> 203.0.113.5:40001 ---> 서버 198.51.100.20:443
변환 표
(10.0.0.10, 51000, TCP) <-> (203.0.113.5, 40001, TCP)
서버 응답 198.51.100.20:443 -> 203.0.113.5:40001 ---> 표를 보고 10.0.0.10:51000 으로 되돌림
변환할 때는 IP 헤더 체크섬과 TCP/UDP 체크섬도 새로 계산해야 한다. TCP/UDP 체크섬이 IP 주소를 포함한 의사 헤더(pseudo-header)까지 덮기 때문이다.
SNAT 와 DNAT
| 종류 | 바꾸는 것 | 대표 용도 |
|---|---|---|
| SNAT (출발지 NAT) | 나가는 패킷의 출발지 | 사설망의 인터넷 접속 |
| DNAT (목적지 NAT) | 들어오는 패킷의 목적지 | 포트 포워딩, 로드 밸런싱, 쿠버네티스 Service |
포트 포워딩은 “공인 주소의 8443 포트로 오면 내부 서버의 443 으로” 같은 정적 DNAT 규칙이다.
연결 추적과 타임아웃
NAT 장비는 변환 표 항목을 언제 지울지 정해야 한다. TCP 는 FIN·RST 를 보고 끝을 알 수 있지만, 아무 패킷 없이 조용한 연결이나 UDP 는 시간으로 판단할 수밖에 없다. IETF 는 NAT 동작 권고를 따로 냈다.
- UDP(RFC 4787): 매핑 타이머는 2분보다 짧으면 안 되고, 5분 이상을 권장한다.
- TCP(RFC 5382): 수립된 연결의 유휴 타임아웃은 2시간 4분보다 짧으면 안 된다.
현실의 장비가 이 권고보다 짧게 잡는 경우가 있다. 그래서 DB 커넥션 풀이나 SSH 세션이 한동안 놀다가 “갑자기 끊기는” 일이 생긴다. 해결책은 응용이나 TCP keepalive 로 주기적으로 패킷을 흘려 항목을 살려 두는 것이다.
매핑 방식과 NAT 통과
RFC 4787 은 NAT 가 외부 포트를 어떻게 정하는지 분류한다.
- 엔드포인트 독립 매핑(Endpoint-Independent Mapping): 내부 (IP, 포트) 하나는 목적지가 어디든 같은 외부 (IP, 포트)로 바뀐다. 이 방식을 권장한다.
- 주소·포트 의존 매핑: 목적지마다 다른 외부 포트를 쓴다. 흔히 “symmetric NAT” 라 부르며, P2P 통신이 어렵다.
화상 통화나 게임처럼 NAT 뒤 두 기기가 직접 통신하려면 STUN(RFC 8489)으로 자기의 바깥 주소를 알아내고, 안 되면 TURN 같은 중계 서버를 쓴다.
통신사 NAT(CGN)
공인 주소가 더 부족해지자 통신사가 가입자들에게도 사설 주소를 주고 한 번 더 NAT 를 하는 경우가 생겼다. 이를 위해 100.64.0.0/10 공유 대역이 따로 정해졌다(RFC 6598). 집 공유기의 WAN 주소가 이 대역이면 이중 NAT 뒤에 있다는 뜻이고, 포트 포워딩이 동작하지 않는다.
NAT 는 방화벽이 아니다
NAT 뒤 장비는 밖에서 먼저 연결할 수 없으니 “보호받는 것처럼” 보인다. 하지만 이는 부수 효과일 뿐 보안 정책이 아니다. 접근 통제는 방화벽 규칙으로 명시해야 한다. IPv6 는 주소가 충분해 NAT 없이 쓰는 것이 기본이므로, 이 착각을 그대로 가져가면 위험하다.
직접 해 보기
NAPT 변환 표를 흉내 내는 작은 시뮬레이터다. 외부 포트를 할당하고, 응답을 되돌리고, 유휴 시간이 지난 항목을 지운다. 주소는 문서용·사설 대역이다.
import itertools
class NAPT:
def __init__(self, public_ip, idle_timeout):
self.public_ip = public_ip
self.idle = idle_timeout
self.ports = itertools.count(40000)
self.out = {} # (내부ip, 내부포트, proto) -> [외부포트, 마지막사용시각]
self.back = {} # (외부포트, proto) -> (내부ip, 내부포트)
def outbound(self, src_ip, src_port, proto, now):
key = (src_ip, src_port, proto)
if key not in self.out: # 엔드포인트 독립 매핑
p = next(self.ports)
self.out[key] = [p, now]
self.back[(p, proto)] = (src_ip, src_port)
self.out[key][1] = now
return self.public_ip, self.out[key][0]
def inbound(self, dst_port, proto, now):
self.expire(now)
return self.back.get((dst_port, proto)) # 없으면 None = 버림
def expire(self, now):
for key, (p, last) in list(self.out.items()):
if now - last > self.idle:
del self.out[key]; del self.back[(p, key[2])]
nat = NAPT("203.0.113.5", idle_timeout=300) # UDP 권고: 5분 이상
print(nat.outbound("10.0.0.10", 51000, "udp", now=0))
print(nat.outbound("10.0.0.11", 51000, "udp", now=1)) # 같은 내부 포트, 다른 호스트
print(nat.outbound("10.0.0.10", 51000, "udp", now=2)) # 같은 매핑 재사용
print("응답(t=100):", nat.inbound(40000, "udp", now=100))
print("모르는 포트:", nat.inbound(45555, "udp", now=100))
print("응답(t=500):", nat.inbound(40001, "udp", now=500)) # 0.11 은 t=1 이후 조용함
print("응답(t=500):", nat.inbound(40000, "udp", now=500))
('203.0.113.5', 40000)
('203.0.113.5', 40001)
('203.0.113.5', 40000)
응답(t=100): ('10.0.0.10', 51000)
모르는 포트: None
응답(t=500): None
응답(t=500): None
마지막 두 줄을 보라. 두 내부 호스트 모두 300초 넘게 아무것도 보내지 않았으므로 항목이 지워졌고, 서버가 늦게 보낸 응답은 버려진다. 응용 입장에서는 “응답이 오지 않는 타임아웃”으로 보인다. 같은 내부 포트 51000 을 쓰는 두 호스트가 서로 다른 외부 포트를 받은 것도 NAPT 의 핵심이다.
현업에서는
- 리눅스 NAT 는 netfilter 의 연결 추적(conntrack)을 바탕으로 한다. 트래픽이 많은 노드에서 conntrack 표가 가득 차면 새 연결이 조용히 버려진다. 커널 로그의 “table full, dropping packet” 메시지가 신호다.
- 쿠버네티스 Service 의 ClusterIP 는 kube-proxy 가 만든 DNAT 규칙으로 실제 파드 IP 로 바뀐다. 파드가 클러스터 밖으로 나갈 때는 노드 IP 로 SNAT 된다. 홈랩 k3s 클러스터에서 외부 서버 로그에 노드 주소만 찍히는 이유다.
- 로드 밸런서 뒤 서버가 진짜 클라이언트 IP 를 알아야 하면 L7 에서는
X-Forwarded-For헤더, L4 에서는 PROXY 프로토콜, 쿠버네티스에서는externalTrafficPolicy: Local같은 방법을 쓴다. - 오래 유지되는 연결(DB 풀, gRPC, 웹소켓)은 keepalive 주기를 경로 위 가장 짧은 NAT 타임아웃보다 짧게 잡는다.
확인 문제
- NAPT 가 공인 IP 하나로 여러 내부 호스트를 지원할 수 있는 이유는?
- NAT 장비가 IP 주소를 바꾸면 TCP 체크섬도 다시 계산해야 하는 이유는?
- RFC 4787 이 권장하는 UDP 매핑 타이머의 최솟값과 권장값은?
- 집 공유기의 WAN 주소가
100.64.12.34일 때 포트 포워딩이 동작하지 않는 이유는? - “NAT 뒤에 있으니 안전하다”는 말이 틀린 이유를 한 문장으로 쓰라.
풀이
- 내부 (IP, 포트) 쌍마다 서로 다른 외부 포트를 할당해 표에 기억하므로, 응답의 목적지 포트만 보고 원래 호스트를 찾을 수 있기 때문이다.
- TCP 체크섬이 출발지·목적지 IP 를 담은 의사 헤더까지 포함해 계산되기 때문이다.
- 2분 미만이면 안 되고, 5분 이상을 권장한다.
- 그 주소는 RFC 6598 공유 대역으로, 통신사 NAT 뒤에 있다는 뜻이다. 바깥 연결은 통신사 NAT 에서 먼저 막히므로 집 공유기까지 오지 않는다.
- 외부 연결이 막히는 건 매핑이 없어서 생기는 부수 효과일 뿐, 명시적 접근 통제 정책이 아니기 때문이다.
더 읽을거리 (References)
- RFC 3022, Traditional IP Network Address Translator (Traditional NAT): https://www.rfc-editor.org/rfc/rfc3022.html
- RFC 4787, NAT Behavioral Requirements for Unicast UDP: https://www.rfc-editor.org/rfc/rfc4787.html
- RFC 5382, NAT Behavioral Requirements for TCP: https://www.rfc-editor.org/rfc/rfc5382.html
- RFC 6598, IANA-Reserved IPv4 Prefix for Shared Address Space: https://www.rfc-editor.org/rfc/rfc6598.html