컴퓨터공학 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 타임아웃보다 짧게 잡는다.

확인 문제

  1. NAPT 가 공인 IP 하나로 여러 내부 호스트를 지원할 수 있는 이유는?
  2. NAT 장비가 IP 주소를 바꾸면 TCP 체크섬도 다시 계산해야 하는 이유는?
  3. RFC 4787 이 권장하는 UDP 매핑 타이머의 최솟값과 권장값은?
  4. 집 공유기의 WAN 주소가 100.64.12.34 일 때 포트 포워딩이 동작하지 않는 이유는?
  5. “NAT 뒤에 있으니 안전하다”는 말이 틀린 이유를 한 문장으로 쓰라.

풀이

  1. 내부 (IP, 포트) 쌍마다 서로 다른 외부 포트를 할당해 표에 기억하므로, 응답의 목적지 포트만 보고 원래 호스트를 찾을 수 있기 때문이다.
  2. TCP 체크섬이 출발지·목적지 IP 를 담은 의사 헤더까지 포함해 계산되기 때문이다.
  3. 2분 미만이면 안 되고, 5분 이상을 권장한다.
  4. 그 주소는 RFC 6598 공유 대역으로, 통신사 NAT 뒤에 있다는 뜻이다. 바깥 연결은 통신사 NAT 에서 먼저 막히므로 집 공유기까지 오지 않는다.
  5. 외부 연결이 막히는 건 매핑이 없어서 생기는 부수 효과일 뿐, 명시적 접근 통제 정책이 아니기 때문이다.

더 읽을거리 (References)