NAT (Network Address Translation)

Linux 커널의 NAT는 별도 마법이 아니라 nf_natnf_conntrack가 결합된 상태 기반 주소 변환(Address Translation) 경로입니다. 이 문서는 SNAT·DNAT·MASQUERADE·REDIRECT, hairpin NAT, helper/ALG, CGNAT, NAT64와 NPTv6의 차이, nftables flowtable 가속, 튜닝 포인트와 실제 장애 분석 절차를 공식 문서와 커널 소스 기준으로 다시 정리합니다.

전제 조건: 네트워크 스택(Network Stack), 라우팅(Routing), Netfilter 문서를 먼저 읽으세요. NAT는 라우팅 앞뒤 어느 훅에서 어떤 필드가 바뀌는지 이해하지 못하면 규칙이 맞아 보여도 실제 패킷(Packet) 경로를 잘못 해석하기 쉽습니다.
일상 비유: 이 개념은 건물 대표 번호와 교환실과 비슷합니다. 외부에서는 대표 번호 하나만 보지만, 교환실은 내부 방 번호와 통화 상태를 기억해 두고 올바른 방으로 연결합니다. NAT도 첫 패킷에서 변환 표를 만들고, 이후 패킷은 그 표를 따라갑니다.

핵심 요약

  • conntrack — 같은 흐름인지 판별하고 원본/응답 튜플을 기억하는 상태 엔진입니다.
  • NAT 바인딩 — 첫 패킷에서 선택된 주소·포트 변환 결과입니다.
  • SNAT — 출발지 주소와 포트를 바꿔 외부로 나갈 수 있게 합니다.
  • DNAT — 목적지 주소와 포트를 바꿔 내부 서비스로 포워딩합니다.
  • Hairpin NAT — 내부 클라이언트가 외부 공개 주소로 같은 내부 서버에 접근할 때 필요한 역경로 유지 기법입니다.

단계별 이해

  1. 첫 패킷을 찾기
    룰셋이 아니라 “어떤 훅에서 최초 바인딩이 만들어졌는가”를 먼저 봅니다.
  2. 라우팅 전후를 나누기
    PREROUTING의 DNAT는 라우팅 결정에 영향을 주고, POSTROUTING의 SNAT는 출력 경로 직전에 적용됩니다.
  3. conntrack 상태를 확인하기
    후속 패킷은 규칙을 다시 찾지 않으므로, 실패 원인은 룰보다 상태 테이블과 타이머(Timer)에 있는 경우가 많습니다.
  4. 대칭 경로를 검증하기
    hairpin, 멀티홈, 컨테이너(Container) 환경에서는 응답이 반드시 같은 번역기를 거쳐 돌아오도록 설계해야 합니다.
문서 범위: 이 페이지(Page)는 Linux 메인라인의 nf_nat, nf_conntrack, nftables/iptables NAT 경로를 중심으로 설명합니다. RFC 4787·5382·5508·6888·6146·6296은 상호 운용성 요구사항과 번역 모델을 설명하는 기준선으로 사용합니다. 중요한 점은 NAT는 방화벽(Firewall) 그 자체가 아니라 주소 변환이라는 점입니다. 외부 노출 정도와 허용 여부는 별도 필터 규칙과 라우팅 정책이 결정합니다.

NAT 개요

NAT는 IPv4 주소 부족 때문에 널리 쓰였지만, 커널 관점에서 더 본질적인 정의는 패킷 헤더의 주소·포트 필드를 한 지점에서 다른 값으로 안정적으로 재기록하고, 응답 경로에서 그 역변환을 보장하는 상태 기계입니다. Linux에서는 이 상태 기계를 nf_conntrack가 유지하고, 실제 필드 수정을 nf_nat가 수행합니다.

중요한 오해 하나를 먼저 지우면 좋습니다. NAT는 흔히 “외부에서 못 들어오게 하니까 보안”으로 설명되지만, 그 효과는 대개 기본 드롭 정책과 상태 기반 필터가 함께 있을 때 생깁니다. NAT만 있다고 해서 보안 정책이 자동으로 완성되지는 않습니다. 반대로 포트 포워딩과 hairpin NAT처럼, NAT는 오히려 외부 노출 경로를 만드는 도구이기도 합니다.

형태무엇을 바꾸는가주로 쓰는 훅대표 용도실무 메모
SNAT 출발지 IP와 포트 POSTROUTING 사설망의 인터넷 접속 고정 공인 IP 또는 공인 IP 풀을 명시적으로 운용할 때 적합합니다.
MASQUERADE 출발지 IP와 포트 POSTROUTING 동적 WAN 주소 환경 출구 인터페이스의 현재 주소를 자동 사용합니다. PPPoE·DHCP uplink에 많이 씁니다.
DNAT 목적지 IP와 포트 PREROUTING, OUTPUT 포트 포워딩, 서비스 공개 PREROUTING DNAT는 라우팅 결정 전에 일어나므로 목적지 변화가 경로 선택에 직접 반영됩니다.
REDIRECT 목적지 IP를 로컬 호스트로 PREROUTING, OUTPUT 투명 프록시, 로컬 프록시 강제 DNAT의 특수형입니다. 목적지를 “현재 장비 자신”으로 바꿉니다.
NETMAP / prefix 대역 또는 프리픽스 PREROUTING, POSTROUTING 1:1 대역 매핑(Mapping), 프리픽스 교체 결정적 매핑을 원할 때 유용합니다. IPv6에서는 NPTv6 논의와 이어집니다.
첫 패킷만 룰을 본다: nftables NAT 문서가 명시하듯 stateful NAT는 흐름의 첫 번째 패킷으로 바인딩을 만들고, 이후 패킷은 conntrack에 저장된 NAT 정보를 사용합니다. 그래서 “규칙을 고쳤는데 이미 열려 있는 연결은 왜 안 바뀌지?”라는 현상은 정상입니다.

NAT 아키텍처와 커널 경로

NAT의 핵심은 “룰 평가”와 “패킷 재기록”을 분리하는 데 있습니다. 첫 패킷에서 nf_nat_setup_info()가 새 튜플을 고르고 reply 방향 튜플을 바꿉니다. 그 다음 패킷부터는 nf_nat_packet()이 이미 저장된 상태만 보고 헤더를 수정합니다. NAT는 매 패킷마다 규칙 전체를 재평가하는 시스템이 아닙니다.

새 패킷 도착 NEW 흐름, 아직 NAT 바인딩 없음 conntrack lookup / create nf_conntrack_in() NAT rule lookup 첫 패킷만 NAT chain 평가 nf_nat_setup_info() 고유 튜플 선택, reply tuple 변경 라우팅 결정 POSTROUTING SNAT 또는 OUTPUT/PREROUTING DNAT conntrack confirm 해시 테이블에 상태 확정 후속 패킷 nf_nat_packet()으로 바로 재기록 conntrack에 저장되는 것 Original tuple: 192.168.10.20:53000 → 198.51.100.10:443 Reply tuple: 198.51.100.10:443 → 203.0.113.10:40001 상태 비트 IPS_SRC_NAT / IPS_DST_NAT IPS_SRC_NAT_DONE / IPS_DST_NAT_DONE IPS_CONFIRMED 핵심 의미 첫 패킷에서만 새 바인딩을 만듭니다. 그 다음부터는 규칙 검색이 아니라 상태 기반 재기록입니다. 응답의 역변환도 reply tuple 덕분에 자동으로 일어납니다.
/* net/netfilter/nf_nat_core.c, 개념 파악용 축약 */
if (nf_ct_is_confirmed(ct))
    return NF_ACCEPT;

nf_ct_invert_tuple(&curr_tuple,
                   &ct->tuplehash[IP_CT_DIR_REPLY].tuple);
get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);

if (!nf_ct_tuple_equal(&new_tuple, &curr_tuple)) {
    nf_ct_invert_tuple(&reply, &new_tuple);
    nf_conntrack_alter_reply(ct, &reply);
    ct->status |= (maniptype == NF_NAT_MANIP_SRC) ?
                  IPS_SRC_NAT : IPS_DST_NAT;
}

if (ct->status & statusbit)
    verdict = nf_nat_manip_pkt(skb, ct, mtype, dir);
소스 레벨 해석: nf_nat_setup_info()는 아직 confirm되지 않은 연결에서만 NAT 정보를 세팅하고, reply tuple을 바꿔 응답 역변환을 준비합니다. 이후 nf_nat_packet()은 이미 설정된 상태 비트를 보고 실제 헤더를 수정합니다.

SNAT와 MASQUERADE

SNAT는 출발지 주소와 포트를 바꿔서 내부 호스트 여러 대가 외부와 통신하도록 만드는 가장 흔한 NAT 형태입니다. 리눅스에서 보통 POSTROUTING 훅에 두며, nftables manpage 기준으로 snatpostroutinginput nat chain에서 유효합니다. 실무에서는 거의 항상 POSTROUTING을 봅니다.

항목SNATMASQUERADE
외부 주소 지정 규칙에 공인 IP 또는 IP 풀을 직접 적음 출구 인터페이스의 현재 주소를 자동 사용
적합한 환경 고정 공인 IP, CGNAT, 로드 분산형 주소 풀 DHCP, PPPoE, 빈번한 WAN 주소 변경
추가 특징 persistent, 포트 범위, 풀 운용이 용이 인터페이스 다운/주소 제거 이벤트와 연동해 stale mapping 정리에 유리
주의할 점 공인 IP 변경 시 규칙을 직접 바꿔야 함 대규모 고정 주소 풀 운용에는 덜 명시적

커널 소스 nf_nat_masquerade.c를 보면 MASQUERADE는 NETDEV_DOWN와 주소 제거 이벤트를 감시해 해당 인터페이스에 묶인 conntrack 엔트리를 정리합니다. 그래서 주소가 바뀌는 uplink에서는 단순 SNAT보다 운용상 안전합니다.

table inet nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;

    # 고정 공인 IP 풀 사용
    oifname "wan0" ip saddr 10.10.0.0/16 \
      snat to 203.0.113.10-203.0.113.13 persistent,fully-random

    # 동적 주소 uplink
    oifname "ppp0" ip saddr 192.168.0.0/24 masquerade
  }
}

persistent는 가능한 한 같은 내부 클라이언트에 같은 외부 매핑을 유지하려는 플래그이고, fully-random은 포트 선택을 완전히 무작위화합니다. nftables manpage에 따르면 커널 5.0 이상에서는 randomfully-random이 같은 의미입니다.

포트 고갈: SNAT는 “외부 포트 공간”을 소비합니다. 하나의 공인 IPv4 주소와 하나의 전송 프로토콜 조합이 제공하는 동시 매핑 수는 유한하므로, 고밀도 환경에서는 주소 풀 확장, 포트 블록 설계, per-subscriber 제한을 함께 고려해야 합니다.

포트 할당 내부 동작: get_unique_tuple()

NAT 바인딩이 만들어질 때 커널이 실제로 하는 일은 충돌하지 않는 유일한 튜플을 찾는 것입니다. 이 과정의 핵심 함수가 get_unique_tuple()이며, nf_nat_core.c에 위치합니다. 이 함수는 사용자가 지정한 범위(주소 풀, 포트 범위) 안에서 기존 conntrack 엔트리와 충돌하지 않는 새 튜플을 선택합니다.

/* net/netfilter/nf_nat_core.c — get_unique_tuple() 개념 축약 */
static void
get_unique_tuple(struct nf_conntrack_tuple *tuple,
                 const struct nf_conntrack_tuple *orig_tuple,
                 const struct nf_nat_range2 *range,
                 struct nf_conn *ct,
                 enum nf_nat_manip_type maniptype)
{
    /* 1단계: 원래 tuple이 범위에 속하면 그대로 사용 시도 */
    if (in_range(orig_tuple, range)) {
        if (!nf_nat_used_tuple(orig_tuple, ct)) {
            *tuple = *orig_tuple;
            return;
        }
    }

    /* 2단계: 범위 내에서 사용되지 않은 조합 탐색 */
    find_best_ips_proto(zone, tuple, range, ct, maniptype);

    /* 3단계: 프로토콜별 포트/ID 선택 */
    if (!(range->flags & NF_NAT_RANGE_PROTO_RANDOM_ALL))
        if (!nf_nat_used_tuple(tuple, ct))
            return;

    /* 4단계: 포트 범위를 순회하며 비충돌 포트 확보 */
    find_proto_unique(tuple, range, ct, maniptype);
}

이 알고리즘의 핵심 포인트는 다음과 같습니다.

단계동작실패 시
원본 튜플 유지 변환 없이 원래 주소/포트가 범위 안이고 미사용이면 그대로 사용 다음 단계로 진행
IP 선택 find_best_ips_proto()가 범위 내에서 IP 선택. NF_NAT_RANGE_PERSISTENT이면 내부 주소의 해시(Hash)로 결정적 선택 범위 내 IP 순환
포트 선택 프로토콜별 unique_tuple() 콜백(Callback)으로 포트/ID를 선택. fully-random이면 prandom_u32() 기반 범위 소진 시 실패
충돌 검사 nf_nat_used_tuple()이 기존 conntrack과 비교. 5-tuple 전체가 겹치지 않으면 통과 다른 포트로 재시도
실무 관찰: persistent 플래그를 사용하면 같은 내부 클라이언트가 같은 외부 IP에 매핑되므로 웹 서비스의 IP 기반 세션 유지에 유리합니다. 반면 fully-random은 보안 측면에서 포트 예측을 어렵게 하므로, 두 요구사항이 충돌하면 주소는 persistent, 포트는 random 조합이 합리적입니다.

nf_nat_used_tuple()은 conntrack 해시 테이블(Hash Table)에서 reply 방향 충돌을 검사합니다. 이 검사가 중요한 이유는 두 개의 서로 다른 내부 연결이 같은 외부 5-tuple을 공유하면 응답 역변환이 모호해지기 때문입니다. 포트 고갈이 발생하는 근본 원인도 여기에 있습니다. 범위 안의 모든 포트가 이미 다른 연결에 사용 중이면 get_unique_tuple()이 실패하고, 해당 패킷은 NAT를 받지 못합니다.

/* net/netfilter/nf_nat_core.c — nf_nat_used_tuple() 개념 */
static bool
nf_nat_used_tuple(const struct nf_conntrack_tuple *tuple,
                   const struct nf_conn *ignored_conntrack)
{
    /* reply 방향의 역튜플을 만들어서 해시 테이블에서 검색 */
    struct nf_conntrack_tuple_hash *found;
    struct nf_conntrack_tuple reply;

    nf_ct_invert_tuple(&reply, tuple);
    found = nf_conntrack_find_get(net, zone, &reply);

    if (found) {
        /* 이미 다른 연결이 이 튜플을 쓰고 있음 → 충돌 */
        if (nf_ct_tuplehash_to_ctrack(found) != ignored_conntrack)
            return true;
    }
    return false;
}

DNAT, REDIRECT, 그리고 hairpin NAT

DNAT는 목적지 주소와 포트를 바꿔 패킷을 내부 서버나 다른 경로로 보냅니다. 가장 전형적인 예는 포트 포워딩입니다. nftables 기준으로 dnatredirectpreroutingoutput nat chain에서 유효합니다. 로컬 프로세스(Process)가 생성한 패킷을 다른 로컬 소켓(Socket)으로 돌리는 경우에는 OUTPUT REDIRECT가 자연스럽습니다.

table inet nat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;

    iifname "wan0" tcp dport 443 dnat to 192.168.10.30:8443
    iifname "wan0" tcp dport 22 redirect to :2222
  }

  chain output {
    type nat hook output priority -100; policy accept;

    tcp dport 853 redirect to :10053
  }
}

REDIRECT는 DNAT의 특수형이며 목적지 주소를 현재 로컬 장비로 바꿉니다. 커널 소스 nf_nat_redirect.c는 이 경로가 NF_INET_PRE_ROUTINGNF_INET_LOCAL_OUT에서만 사용되어야 함을 분명히 드러냅니다.

실무에서 자주 놓치는 부분은 hairpin NAT입니다. 내부 클라이언트가 외부 공개 주소(203.0.113.10:443)로 같은 내부 서버(192.168.10.30:8443)에 접근하면 DNAT만으로는 끝나지 않는 경우가 많습니다. 서버가 클라이언트에게 직접 응답해 버리면 경로가 비대칭이 되고, 클라이언트는 “내가 접속한 외부 주소”가 아니라 “내부 서버 주소”에서 답이 돌아와 세션이 깨질 수 있습니다. 그래서 hairpin에서는 반환 경로를 번역기 자신으로 묶기 위한 추가 SNAT/masquerade가 흔히 필요합니다.

내부 클라이언트 192.168.10.20 공개 주소로 접속 시도 NAT 게이트웨이 LAN: 192.168.10.1 / WAN 공인 주소: 203.0.113.10 PREROUTING DNAT LAN 방향 추가 SNAT 내부 서버 192.168.10.30:8443 포트 포워딩 대상 dst=203.0.113.10:443 DNAT → 192.168.10.30:8443 reply가 반드시 게이트웨이로 되돌아와야 함 클라이언트는 계속 203.0.113.10과 통신한다고 인식합니다 hairpin에서 DNAT만 적용하면 서버가 클라이언트에게 직접 답해 비대칭 경로가 생길 수 있으므로, 보통 LAN 방향 SNAT/masquerade를 같이 둡니다.
hairpin 요구사항: RFC 4787, RFC 5382, RFC 5508은 각각 UDP, TCP, ICMP 계열에서 hairpin 지원을 중요 요구사항으로 다룹니다. Linux에서도 hairpin 여부는 “NAT가 가능한가”보다 “DNAT와 반환 경로용 SNAT가 함께 설계됐는가”에 달려 있습니다.

conntrack과 NAT 상태

NAT를 이해할 때 가장 중요한 자료구조는 struct nf_conn입니다. 여기에 원본 방향 tuple, reply 방향 tuple, 상태 비트, 필요시 NAT 확장과 helper 확장이 연결됩니다. NAT가 “연결 기반”처럼 보이는 이유는 실제로 이 상태가 모든 후속 패킷의 판단 기준이 되기 때문입니다.

커널/사용자 지표의미현재 문서 기본값/성격왜 중요한가
nf_conntrack_buckets conntrack 해시 버킷 수 메모리 기준으로 계산, 초기 netns에서만 변경 가능 충돌과 탐색 비용에 직접 영향이 있습니다.
nf_conntrack_count 현재 할당된 흐름 수 읽기 전용(Read-Only) 테이블 포화 임박 여부를 보는 1차 지표입니다.
nf_conntrack_max 허용 최대 흐름 수 기본값은 nf_conntrack_buckets 초과 시 새 연결이 실패합니다.
nf_conntrack_tcp_timeout_established ESTABLISHED TCP 타임아웃 432000초 (5일) 긴 유휴 TCP 세션 유지 비용을 좌우합니다.
nf_conntrack_udp_timeout 일반 UDP 타임아웃 30초 짧은 UDP 흐름은 빨리 비우지만, NAT keepalive가 없는 애플리케이션에는 짧을 수 있습니다.
nf_conntrack_udp_timeout_stream 양방향 UDP 타임아웃 120초 RTP·QUIC 같은 장시간 UDP에서 체감에 직접 연결됩니다.
nf_flowtable_tcp_timeout flowtable의 TCP fastpath 타임아웃 30초 오프로드 경로에서 얼마 동안 캐시(Cache)를 유지할지 결정합니다.
nf_flowtable_udp_timeout flowtable의 UDP fastpath 타임아웃 30초 짧은 burst 트래픽에서 오프로드 hit 비율에 영향이 큽니다.

여기서 흥미로운 점은 RFC 4787이 일반 UDP NAT 매핑 타이머에 대해 “2분 미만으로 끝나면 안 된다”고 요구한다는 점입니다. 반면 현재 Linux nf_conntrack_udp_timeout 기본값은 30초입니다. 이는 리눅스 기본값이 곧바로 CPE/CGN 상호 운용성 요구사항을 만족한다는 뜻이 아님을 보여줍니다. 인터넷 경계 NAT 장비로 쓸 때는 역할에 맞게 타임아웃을 직접 조정해야 합니다. 이 문장은 RFC와 커널 문서를 함께 읽은 해석입니다.

# 현재 사용량과 한계
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

# 상태 보기
conntrack -L -o extended
conntrack -E

# 특정 흐름만 보기
conntrack -L -p tcp --orig-src 192.168.10.20 --dport 443
룰 변경과 기존 세션: NAT 규칙을 수정해도 이미 확정된 흐름은 예전 바인딩을 계속 쓸 수 있습니다. 문제를 재현할 때는 관련 conntrack 엔트리를 지우거나 새 연결로 다시 시작해야 합니다.

NAT helper와 ALG

FTP, SIP, 일부 레거시 제어 프로토콜처럼 “제어 채널 안에 다음 데이터 채널 주소와 포트가 실려 오는” 프로토콜은 NAT에 불리합니다. NAT는 L3/L4 헤더만 바꿔서는 충분하지 않고, 애플리케이션 페이로드(Payload)를 읽어 RELATED 흐름을 예상해야 합니다. 이 역할이 conntrack helper입니다.

현재 nftables 모델은 iptables 시절의 “자동으로 붙어주는 helper” 관성보다 훨씬 명시적입니다. nftables manpage도 helper는 ct helper set으로 연결에 명시적으로 붙여야 하며, conntrack lookup 뒤에서만 유효하다고 설명합니다. 즉, helper는 가능한 한 좁은 범위에 붙여야 합니다.

table inet helpers {
  ct helper ftp-standard {
    type "ftp" protocol tcp;
  }

  chain prerouting {
    type filter hook prerouting priority filter; policy accept;
    ip daddr 192.168.10.30 tcp dport 21 ct helper set "ftp-standard"
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related accept
    ip daddr 192.168.10.30 ct helper "ftp" tcp dport 1024-65535 accept
  }
}
보안상 주의: nftables helper 문서는 helper를 특정 daddr와 서비스에 엄격히 묶지 않으면 원치 않는 RELATED 포트 개방 효과가 생길 수 있다고 경고합니다. TLS로 보호되는 제어 채널에서는 helper가 페이로드를 읽을 수 없으므로, “helper가 안 먹는다”보다 “암호화(Encryption) 때문에 원래 불가능하다”가 정확한 설명일 때가 많습니다.

더 긴 설명은 conntrack 헬퍼 & ALG 문서를 참고하세요. NAT 문서에서는 helper를 “예외적 필요”로 보는 편이 정확합니다. 현대 프로토콜은 가능하면 애플리케이션 계층 NAT 의존을 줄이는 방향으로 설계됩니다.

RFC 동작 모델: mapping, filtering, hairpin

“full cone”, “restricted cone”, “symmetric NAT” 같은 표현은 여전히 현장에서 자주 쓰이지만, RFC 4787 이후 문서는 이를 mapping behavior, filtering behavior, hairpin behavior, timer behavior로 더 정밀하게 분해합니다. Linux NAT를 이 별명 하나로 규정하려고 하면 거의 항상 과도한 단순화가 됩니다.

RFC 요구사항핵심 내용Linux NAT를 읽을 때의 의미
RFC 4787 REQ-1 / RFC 5382 REQ-1 UDP와 TCP에서 Endpoint-Independent Mapping 요구 외부 목적지가 달라도 같은 내부 endpoint에 같은 외부 매핑을 유지하는 방향이 상호 운용성에 유리합니다.
RFC 4787 REQ-8 / RFC 5382 REQ-3 투명성이 중요하면 Endpoint-Independent Filtering 권고 필터링은 NAT 그 자체보다 방화벽 정책과 더 강하게 연결됩니다. NAT 규칙만 보고 외부 허용 범위를 단정하면 안 됩니다.
RFC 4787 REQ-9 / RFC 5382 REQ-8 / RFC 5508 REQ-7 UDP·TCP·ICMP hairpin 지원 포트 포워딩을 내부에서도 같은 공개 주소로 쓰려면 hairpin 경로를 설계해야 합니다.
RFC 4787 REQ-5 UDP 매핑 타이머는 일반적으로 2분 미만이면 안 됨 Linux 기본 UDP timeout은 이보다 짧을 수 있으므로, CPE/CGN 역할이면 운영자가 조정해야 합니다.
RFC 5508 ICMP Query/Error도 NAT traversal과 hairpin 고려 필요 PMTUD와 에러 전달 문제를 “그냥 ICMP 막힘”으로 뭉개면 원인 분석이 막힙니다.

정리하면, Linux NAT를 “기본적으로 port-restricted cone NAT다” 같은 한 줄로 정의하는 것은 정확하지 않습니다. 외부에서 관찰되는 동작은 NAT 바인딩 정책, 필터 규칙, helper 사용 여부, hairpin 설계, 타이머 설정이 합쳐져 결정됩니다.

CGNAT (Carrier-Grade NAT)

CGNAT는 가정용 공유기의 NAT를 그대로 크게 키운 것이 아니라, 수많은 가입자가 한정된 공인 IPv4 주소 공간(Address Space)을 공유하도록 만드는 통신사업자급 상태 시스템입니다. 그래서 단순한 address rewrite보다 공정성(Fairness), 로그량, 포트 할당, 메모리 예산, 장애 반경이 핵심 문제가 됩니다.

RFC 6598은 CGN과 CPE 사이에서 사용할 공유 주소 공간으로 100.64.0.0/10을 예약했습니다. 이 공간은 RFC 1918 사설 주소와 비슷하게 외부 라우팅 대상이 아니지만, 목적이 다릅니다. 통신사업자 내부에서 CPE와 CGN 사이 구간을 번호 매기기 위한 별도 공간으로 이해하는 편이 정확합니다.

CGN 운영 포인트RFC 6888 관점실무 의미
주소 풀링 기본은 paired pooling 권고 가능하면 가입자당 같은 외부 IP를 유지해 애플리케이션 혼란과 로그 복잡도를 줄입니다.
포트 제한 가입자별 외부 포트 수 제한 필요 한 가입자가 포트를 독식해 전체 CGN을 고갈시키지 않게 합니다.
상태 메모리 메모리 사용량 제한과 속도 제한 필요 대규모 봇 감염이나 비정상 burst가 전체 장비를 무너뜨리지 않게 합니다.
로그 목적지 주소/포트 로깅은 가능한 한 피해야 함 개인정보와 저장 비용 모두 커지므로, 필요한 최소 정보만 남기는 것이 원칙입니다.
포트 할당 알고리즘 포트 효율, 로그 최소화, 예측 난이도 사이 균형 결정적 포트 블록은 로그를 줄이지만 포트 활용률과 분산성에 trade-off가 있습니다.
명시적 제어 가입자가 매핑을 제어할 프로토콜 필요 PCP 같은 외부 매핑 제어 체계가 없으면 일부 응용 프로그램이 급격히 불리해집니다.
CGNAT는 “큰 NAT”가 아니다: 가정용 기본값을 그대로 확대하면 곧바로 로그 폭증, 포트 경쟁, 특정 가입자 폭주, 관측 부재 문제가 드러납니다. CGN 설계는 NAT 기능보다 자원 예산과 운영 자동화가 더 큰 주제입니다.

NAT64와 NPTv6

IPv6 전환 문맥에서 “NAT”라는 단어는 NAT44 하나만 가리키지 않습니다. RFC 6146의 stateful NAT64는 IPv6 클라이언트와 IPv4 서버를 이어 주는 주소·프로토콜 번역기이고, RFC 6296의 NPTv6는 IPv6-to-IPv6 프리픽스 번역기입니다. 둘은 목표도, 상태 모델도, 운용상의 함정도 다릅니다.

항목NAT44Stateful NAT64NPTv6
주소 패밀리 IPv4 ↔ IPv4 IPv6 ↔ IPv4 IPv6 ↔ IPv6
상태 보통 stateful stateful stateless
핵심 테이블 conntrack + NAT binding BIB + session table 프리픽스 매핑 규칙
포트 변환 필요 시 수행 주로 수행 하지 않음
RFC 포인트 4787/5382/5508/6888 6146 6296
운영 핵심 주소 절약, 포워딩, hairpin IPv6 전용 클라이언트의 IPv4 접근 멀티홈, 프리픽스 교체, 상태 없는 주소 독립성
IPv6 클라이언트 2001:db8:1::20 Stateful NAT64 BIB: TCP / UDP / ICMP Query Session table IPv6 주소와 IPv4 transport address를 매핑 IPv4 서버 198.51.100.40:443 내부 IPv6 프리픽스 fd00:10::/48 NPTv6 상태 없음, 포트 변환 없음 checksum-neutral prefix swap 주로 멀티홈·프리픽스 독립성 문제를 해결 외부 IPv6 프리픽스 2001:db8:200::/48

RFC 6146는 stateful NAT64가 Endpoint-Independent Mapping을 사용하고 TCP, UDP, ICMP Query마다 별도 BIB와 session table을 가진다고 설명합니다. 또 hairpin을 고려하며, end-to-end IPsec과는 기본적으로 양립하지 않는다고 적시합니다. 즉 NAT64는 단순한 dnat/snat 규칙의 변형이 아니라, 별도의 주소 패밀리 변환 모델입니다.

반면 RFC 6296의 NPTv6는 상태가 없고 checksum-neutral하며, 포트를 건드리지 않습니다. 그래서 NAT44보다 훨씬 단순하지만, 보안 기능을 제공하는 것은 아닙니다. RFC 6296도 NPTv6만으로 방화벽이 되는 것은 아니며, 필요한 경우 별도 필터링이 병행되어야 한다고 읽는 편이 맞습니다.

실무 구분: 이 페이지의 중심은 메인라인 Netfilter NAT입니다. RFC 6146 수준의 NAT64는 별도의 BIB/session 모델을 요구하므로, 단순 NAT44 문법과 같은 수준으로 생각하면 구현과 운영 요구사항을 과소평가하게 됩니다.

Stateless NAT와 prefix rewrite

stateful NAT가 conntrack을 기반으로 흐름 상태를 유지하는 반면, stateless NAT는 패킷마다 규칙대로 헤더를 바꾸고 별도 상태를 유지하지 않습니다. nftables NAT 위키가 강조하듯, 이런 방식은 1:1 변환이나 매우 통제된 환경에서는 빠르고 단순할 수 있지만, 일반적인 인터넷 경계 NAT로는 함정이 많습니다.

핵심 제약은 conntrack을 끄고 사용해야 한다는 점입니다. 공식 문서는 notrack를 raw 우선순위(Priority) 또는 그보다 이른 훅에 배치하지 않으면 conntrack lookup이 먼저 일어나므로 stateless 설계가 성립하지 않는다고 설명합니다.

table inet rawnat {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    tcp dport 443 ip daddr set 192.0.2.10 tcp dport set 8443 notrack
  }
}
권장 범위: stateless NAT는 “상태가 없어서 빠르다”보다 “상태가 없어서 안전장치도 없다”가 먼저 떠올라야 합니다. 역방향 경로, ICMP 오류, PMTUD, 다중 세션 충돌까지 모두 운영자가 직접 책임져야 하므로, 정말 1:1 결정적 변환이 필요한 경우에만 제한적으로 사용하는 편이 좋습니다.

nf_flowtable과 NAT fastpath

nf_flowtable은 “conntrack을 버리는 기능”이 아니라, 한 번 conntrack/NAT로 검증된 흐름을 더 짧은 경로로 전달하는 fastpath입니다. 커널 문서가 분명히 적듯 첫 패킷은 기존 IP forwarding path를 통과해야 하고, flowtable 엔트리는 보통 첫 응답 패킷을 본 뒤 만들어집니다. 이후 패킷은 ingress 훅에서 flowtable lookup을 하고, hit이면 neigh_xmit()로 바로 전송할 수 있습니다.

여기서 중요한 사실은 flowtable 엔트리도 NAT 구성을 저장한다는 점입니다. 따라서 flowtable hit 경로에서도 SNAT/DNAT는 계속 적용됩니다. 이것이 “NAT가 있으면 fastpath를 못 탄다”는 단순화가 틀린 이유입니다. 다만 fragment, helper, 복잡한 예외 경로, 미지원 드라이버는 여전히 classic path에 남을 수 있습니다.

첫 패킷 classic path 진입 PREROUTING → routing → FORWARD conntrack / NAT / policy 검사 첫 응답 패킷 flowtable entry 생성 가능 후속 패킷 ingress lookup으로 fastpath Software flowtable ingress에서 tuple lookup NAT config 포함, TTL/hoplimit 감소 hit 시 neigh_xmit()로 classic path 우회 fragment처럼 transport header가 없으면 lookup 불가 Hardware offload NIC가 지원하면 flowtable flags offload 사용 conntrack 출력에서 [OFFLOAD], [HW_OFFLOAD] 확인 복잡한 rules/helper/tunnel은 종종 미오프로드 상세 한계는 드라이버와 NIC capability에 좌우
table inet fastpath {
  flowtable f {
    hook ingress priority 0;
    devices = { lan0, wan0 };
    flags offload;
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related flow offload @f accept
    tcp dport { 80, 443 } accept
    udp dport 443 accept
  }
}
관측 포인트: 커널 문서 기준으로 소프트웨어 fastpath는 [OFFLOAD], 하드웨어 오프로드는 [HW_OFFLOAD] 태그로 conntrack 출력에서 확인할 수 있습니다. 더 깊은 내용은 Netfilter Flowtable 문서를 참고하세요.

성능과 용량 설계

NAT 성능 문제는 대개 “룰이 많아서 느리다”보다 다음 네 축에서 터집니다. 상태 수, 포트 수, 경로 대칭성, 예외 프로토콜입니다. 근거 없는 Mpps 숫자를 외우기보다, 어떤 자원이 먼저 고갈되는지 모델링하는 편이 훨씬 실전적입니다.

병목(Bottleneck) 후보전형적 증상첫 확인 항목대응 방향
conntrack 테이블 포화 새 연결만 실패, 기존 연결은 유지 nf_conntrack_count, nf_conntrack_max 버킷/최대치 조정, timeout 재설계, 비정상 흐름 차단
외부 포트 고갈 특정 공인 IP에서만 신규 세션 실패 SNAT 주소 풀, 가입자당 포트 사용량 공인 IP 풀 확장, deterministic port block 설계, per-user quota
비대칭 라우팅 요청은 나가는데 응답이 INVALID 또는 미도달 ip route get, ECMP/정책 라우팅, firewall node 수 왕복이 같은 번역기를 지나게 설계
helper/ALG 일부 레거시 프로토콜만 비정상 helper attach 여부, 제어 채널 암호화 여부 helper 범위 축소, 애플리케이션 설계 변경
PMTU/MSS 문제 큰 패킷만 멈춤, HTTPS 일부 응답만 끊김 ICMP frag-needed, TCP MSS, 터널(Tunnel) 중첩 여부 MSS clamp, ICMP 경로 보존, MTU 재설계
오프로드 미적중 평소보다 CPU 사용 급증, 특정 플로우만 느림 [OFFLOAD]/[HW_OFFLOAD], fragment 여부 flowtable 대상 흐름 선별, 미지원 프로토콜 분리

운영 튜닝의 기본 원칙은 단순합니다. 세션 수를 예측하고, 세션당 메모리와 타이머를 계산하고, 주소/포트 풀을 그보다 넉넉하게 설계하면 됩니다. 반대로 “일단 nf_conntrack_max만 크게 올리자” 접근은 메모리 압박과 GC 지연(Latency)을 다른 형태로 되돌려 받을 가능성이 큽니다.

디버깅(Debugging) 절차

NAT 문제를 디버깅할 때는 항상 질문 순서를 고정하는 편이 좋습니다. 1) 룰이 맞는가, 2) 첫 패킷이 어떤 체인에서 어떤 verdict를 받는가, 3) conntrack 엔트리가 어떤 tuple로 확정됐는가, 4) 응답이 같은 번역기를 지나오는가. 이 순서를 지키면 문제 공간이 빠르게 줄어듭니다.

# 1. 현재 룰셋과 체인 우선순위 확인
nft list ruleset

# 2. 실제 첫 패킷 추적
nft monitor trace

# 3. conntrack 상태/이벤트 확인
conntrack -L -o extended
conntrack -E

# 4. 경로 확인
ip route get 198.51.100.10 from 192.168.10.20 iif lan0

# 5. 양쪽 인터페이스에서 동시에 관찰
tcpdump -ni lan0 host 192.168.10.30
tcpdump -ni wan0 host 203.0.113.10
증상유력 원인가장 먼저 볼 것
외부로는 나가지만 응답이 안 돌아옴 SNAT 누락, 역방향 경로 비대칭, upstream reverse path 문제 POSTROUTING hit 여부, reply tuple, 상류 라우팅
포트 포워딩은 외부에서 되는데 내부에서만 안 됨 hairpin용 SNAT/masquerade 누락 LAN→공개주소 트래픽의 DNAT 이후 반환 경로
유휴 후 UDP 세션이 끊김 timeout이 애플리케이션 keepalive보다 짧음 nf_conntrack_udp_timeout, keepalive 주기
FTP/SIP 같은 일부 프로토콜만 실패 helper 미부착 또는 암호화로 인한 ALG 불가 ct helper, RELATED 허용 규칙
규칙을 고쳐도 문제 재현이 그대로 기존 conntrack 엔트리가 계속 살아 있음 관련 엔트리 삭제 후 재시험
flow offload가 기대만큼 안 걸림 첫 응답 패킷 미관측, fragment, helper, 미지원 경로 conntrack 태그, nft flowtable 대상 조건
MSS/PMTU: NAT 자체가 MTU를 줄이는 것은 아니지만, 터널·TPROXY·오버레이(Overlay)와 겹치면 “작은 패킷은 되는데 큰 패킷만 멈춤” 현상이 흔합니다. 이때는 방화벽보다 먼저 PMTUD, ICMP frag-needed 전달, TCP MSS clamp를 점검하세요.

컨테이너와 네임스페이스(Namespace)에서의 NAT

컨테이너 환경의 NAT는 대개 “컨테이너 안”이 아니라 호스트 네임스페이스의 브리지(Bridge) 출구에서 일어납니다. Docker 기본 브리지 모델을 떠올리면 이해가 쉽습니다. 컨테이너는 veth로 브리지에 붙고, 호스트의 POSTROUTING MASQUERADE가 외부 통신을 담당합니다.

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    ip saddr 172.17.0.0/16 oifname "eth0" masquerade
  }
}

네트워크 네임스페이스를 쓰면 NAT 규칙과 conntrack 상태도 네임스페이스 경계와 함께 관리됩니다. 다만 실제로 어느 네임스페이스의 NAT가 적용되는지는 “패킷이 어느 네임스페이스의 훅을 지나는가”로 결정되므로, 브리지·veth·hostNetwork·CNI 정책을 같이 봐야 합니다. Kubernetes도 CNI에 따라 iptables/nftables NAT, eBPF 기반 SNAT, 또는 NAT 최소화 모델까지 다양합니다.

관련 학습: 컨테이너 NAT 자체보다 네임스페이스 경로가 더 헷갈리는 경우가 많습니다. 네트워크 네임스페이스Linux Containers 문서를 함께 보세요.

NAT와 터널의 상호작용

NAT가 터널(GRE, VXLAN, IPsec, WireGuard 등)과 만나면 어떤 헤더를 기준으로 NAT가 동작하는가가 핵심 문제가 됩니다. 터널 캡슐화(Encapsulation) 전에 NAT가 적용되는지, 캡슐화 후 외부 헤더에 NAT가 적용되는지에 따라 완전히 다른 경로가 만들어집니다.

시나리오 A: 내부 패킷에 NAT 후 터널 캡슐화 원본 패킷 src=10.0.1.5 dst=8.8.8.8 SNAT 적용 src→203.0.113.10 dst=8.8.8.8 터널 캡슐화 outer hdr + NAT된 inner WAN으로 전송 outer src/dst는 터널 endpoint 시나리오 B: 터널 캡슐화 후 외부 헤더에 NAT 원본 패킷 inner hdr 변경 없음 터널 캡슐화 outer src=10.0.0.1 dst=peer 외부 헤더 SNAT outer src→공인IP WAN으로 전송 inner는 원본 그대로 터널별 NAT 상호작용 핵심 GRE: L4 포트가 없어 conntrack이 GRE key/call-id로 흐름 구분 (nf_conntrack_proto_gre) VXLAN/GENEVE: 외부 UDP 헤더에 대해 일반 UDP NAT 적용 가능, inner는 별도 네임스페이스 IPsec ESP: 포트 없는 프로토콜이므로 NAT-Traversal(UDP 4500 캡슐화) 없이는 NAT 통과 불가 WireGuard: 이미 UDP 기반이므로 외부 헤더의 일반 NAT 통과에 제약 없음
터널 유형NAT 통과 가능 여부conntrack 처리주요 주의사항
GRE 제한적 (L4 포트 없음) nf_conntrack_proto_gre가 key/call-id로 추적 동일 NAT 뒤에 여러 GRE 터널이 있으면 call-id 충돌 위험. PPTP helper 필요 시 보안 함의 고려
VXLAN / GENEVE 양호 (외부 UDP 기반) 외부 UDP 헤더로 일반 conntrack 내부 패킷은 별도 bridge/namespace에서 처리. 외부 NAT가 source port를 바꾸면 ECMP 분산에 영향
IPsec ESP 직접 불가 → NAT-T 필요 ESP는 L4 포트 없음, NAT-T는 UDP 4500으로 캡슐화 IKE 협상에서 NAT 감지 시 자동으로 NAT-T 전환. IPsec & xfrm 문서 참고
WireGuard 양호 (UDP 기반) 외부 UDP 헤더로 일반 conntrack PersistentKeepalive로 NAT 매핑 유지. WireGuard 문서 참고
L2TP 양호 (UDP 1701 또는 IP 프로토콜 115) UDP 모드는 일반 conntrack, IP 프로토콜 모드는 제한 IPsec과 함께 쓸 때는 IPsec의 NAT-T 제약이 우선. L2TP 문서 참고

특히 GRE는 L4 포트가 없기 때문에 NAT에 본질적으로 불리합니다. 커널의 nf_conntrack_proto_gre는 GRE key와 call-id를 사용해 흐름을 구분하지만, 하나의 NAT 뒤에 여러 GRE 터널이 존재하면 충돌이 발생할 수 있습니다.

/* net/netfilter/nf_conntrack_proto_gre.c — GRE conntrack 튜플 추출 */
static bool gre_pkt_to_tuple(
    const struct sk_buff *skb,
    unsigned int dataoff,
    struct net *net,
    struct nf_conntrack_tuple *tuple)
{
    const struct pptp_gre_header *grehdr;

    /* GRE key 또는 call ID를 L4 포트 대신 사용 */
    tuple->src.u.gre.key = grehdr->call_id;
    tuple->dst.u.gre.key = grehdr->call_id;

    return true;
}
MTU와 MSS 클램핑: 터널 캡슐화는 패킷 크기를 늘리므로 경로 MTU가 줄어듭니다. NAT 환경에서 터널을 함께 사용할 때는 tcp-mss-clamp를 FORWARD 체인에 설정하지 않으면 "소량 데이터는 되는데 대량 전송만 멈추는" 전형적인 PMTUD 블랙홀 증상이 나타날 수 있습니다.
# 터널 환경에서 MSS 클램핑 예시
table inet mangle {
  chain forward {
    type filter hook forward priority mangle; policy accept;
    oifname "gre1" tcp flags syn tcp option maxseg size set rt mtu
    oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
  }
}

eBPF 기반 NAT

전통적인 Netfilter NAT는 conntrack 상태와 nf_nat 경로에 의존하지만, 최근에는 eBPF를 사용하여 NAT를 구현하는 접근법이 등장했습니다. 대표적으로 Cilium은 conntrack과 NAT 테이블을 BPF 맵으로 구현하고, TC 또는 XDP 훅에서 패킷 헤더를 직접 수정합니다.

측면Netfilter NATeBPF 기반 NAT
상태 저장 nf_conntrack 해시 테이블 BPF 해시 맵 (BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH)
훅 위치 Netfilter 훅 (PREROUTING, POSTROUTING 등) TC ingress/egress, XDP, cgroup socket
헤더 수정 nf_nat_manip_pkt() bpf_skb_store_bytes(), bpf_l3_csum_replace(), bpf_l4_csum_replace()
Helper/ALG conntrack helper 프레임워크 일반적으로 미지원 (애플리케이션 계층에서 해결)
성능 flowtable fastpath 가능 XDP에서는 드라이버 수준 처리로 높은 pps
디버깅 conntrack -L, nft monitor trace bpftool map dump, bpf_trace_printk()
생태계 범용, 모든 배포판 기본 지원 Cilium, Katran 등 특화 프로젝트

eBPF NAT의 핵심 아이디어는 BPF 맵을 conntrack 테이블 대용으로 사용하고, TC/XDP 프로그램에서 직접 L3/L4 헤더를 재기록하는 것입니다. 기본적인 SNAT를 BPF로 구현하면 다음과 같은 형태가 됩니다.

/* eBPF TC 프로그램에서의 SNAT 개념 예시 */
struct nat_entry {
    __be32 orig_src;
    __be16 orig_sport;
    __be32 nat_src;
    __be16 nat_sport;
};

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 65536);
    __type(key, struct flow_key);
    __type(value, struct nat_entry);
} nat_table SEC(".maps");

SEC("tc")
int snat_egress(struct __sk_buff *skb)
{
    struct nat_entry *entry;
    struct flow_key key = {};

    /* 5-tuple 추출하여 key 구성 */
    extract_flow_key(skb, &key);

    entry = bpf_map_lookup_elem(&nat_table, &key);
    if (!entry) {
        /* 새 흐름: NAT 매핑 생성 */
        struct nat_entry new_entry = {};
        allocate_nat_binding(&new_entry, &key);
        bpf_map_update_elem(&nat_table, &key, &new_entry, BPF_NOEXIST);
        entry = &new_entry;
    }

    /* IP 헤더의 source address 변경 + 체크섬 보정 */
    bpf_l3_csum_replace(skb, IP_CSUM_OFF, entry->orig_src, entry->nat_src, 4);
    bpf_skb_store_bytes(skb, IP_SRC_OFF, &entry->nat_src, 4, 0);

    /* L4 source port 변경 + 체크섬 보정 */
    bpf_l4_csum_replace(skb, L4_CSUM_OFF, entry->orig_sport, entry->nat_sport, 2);
    bpf_skb_store_bytes(skb, L4_SPORT_OFF, &entry->nat_sport, 2, 0);

    return TC_ACT_OK;
}
Cilium의 접근: Cilium은 이 방식을 확장하여 Kubernetes Pod 간 통신에서 kube-proxy를 대체합니다. Service의 ClusterIP를 backend Pod IP로 DNAT하는 과정을 eBPF 맵 기반으로 처리하므로, iptables 규칙이 Service 수에 비례해 늘어나는 문제를 피합니다. 더 자세한 내용은 BPF/eBPF/XDP 문서를 참고하세요.
trade-off: eBPF NAT는 높은 성능을 제공하지만, Netfilter의 성숙한 helper/ALG 생태계, nft monitor trace 수준의 내장 디버깅, 범용 배포판 호환성을 포기하는 대가가 있습니다. 범용 게이트웨이에서는 Netfilter NAT가 여전히 합리적이고, 대규모 컨테이너 환경에서 Service 라우팅 성능이 핵심이면 eBPF가 강점을 발휘합니다.

NAT 바인딩 생명 주기

NAT 바인딩은 생성부터 소멸까지 명확한 생명 주기를 가집니다. 이 주기를 이해하면 "규칙을 고쳤는데 왜 안 바뀌지", "타임아웃 전에 왜 끊기지", "왜 새 연결이 실패하지" 같은 실무 질문에 체계적으로 답할 수 있습니다.

1. 첫 패킷 도착 2. conntrack 엔트리 생성 unconfirmed list에 추가 3. NAT 바인딩 설정 nf_nat_setup_info() → reply tuple 변경 4. conntrack confirm 해시 테이블에 확정, IPS_CONFIRMED 5. 후속 패킷 처리 만료 경로 A. 타이머 만료 (timeout) B. TCP RST/FIN 관찰 C. conntrack -D 수동 삭제 D. MASQUERADE: 인터페이스 다운 이벤트 E. nf_conntrack_max 초과 시 early drop 바인딩 실패 경로 포트 범위 소진 → get_unique_tuple() 실패 conntrack_max 초과 → 엔트리 생성 불가 → 해당 패킷 DROP

특히 confirm 전에 드롭되는 패킷은 conntrack 엔트리와 NAT 바인딩이 모두 사라집니다. 이것은 실제로 방화벽 정책이 FORWARD에서 패킷을 거부했을 때 의도적으로 발생하는 것이며, 바인딩이 누적되는 것을 막아 줍니다.

/* net/netfilter/nf_conntrack_core.c — confirm 전후의 차이 */

/* confirm 전: unconfirmed list에만 존재, 해시 미등록 */
static int __nf_conntrack_confirm(struct sk_buff *skb)
{
    struct nf_conn *ct = nf_ct_get(skb, &ctinfo);

    /* reply 방향 충돌이 있으면 confirm 실패 */
    if (nf_ct_is_dying(ct) || __nf_conntrack_confirm_conflict(ct))
        return NF_DROP;

    /* 해시 테이블에 양 방향 삽입 */
    __nf_conntrack_hash_insert(ct, hash, reply_hash);
    ct->status |= IPS_CONFIRMED;

    return NF_ACCEPT;
}
early drop과 GC: nf_conntrack_max에 도달하면 커널은 가장 덜 활발한 UNREPLIED 연결부터 제거하여 공간을 확보합니다. 이 early drop 메커니즘 때문에 max 값을 무작정 올리기보다 불필요한 연결의 타임아웃을 줄이는 것이 더 효과적인 경우가 많습니다.

NAT 로깅과 감사

법적 요구사항이 있는 환경(통신사업자, 공공기관)에서는 NAT 바인딩을 기록해야 할 수 있습니다. "누가 언제 어떤 외부 주소/포트를 사용했는가"를 사후에 추적하려면 최소한 타임스탬프, 내부 주소/포트, 외부 주소/포트, 프로토콜을 기록해야 합니다.

# nftables 로그 기반 NAT 감사
table inet nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oifname "wan0" ip saddr 10.0.0.0/8 log prefix "NAT-SNAT " group 1 \
      snat to 203.0.113.10-203.0.113.13
  }
}

# conntrack 이벤트 기반 로깅 (더 정확함)
conntrack -E -e NEW -o timestamp,id

# nfnetlink_log 그룹 수신
ulogd -c /etc/ulogd.conf
로깅 방식장점단점적합한 환경
nftables log 규칙 위치에서 즉시 기록, 유연한 prefix 매 패킷 로그 시 성능 영향, NAT 후 주소는 별도 캡처 필요 소규모, 특정 흐름만 감사
conntrack 이벤트 NEW/DESTROY 이벤트만 기록, 바인딩 생성/소멸 시점 정확 ulogd/별도 데몬 필요, 이벤트 유실 가능 중규모, CGN 감사
결정적 포트 블록 로그 없이도 역추적(Backtrace) 가능 (알고리즘으로 계산) 포트 효율 저하, 설계 복잡 대규모 CGN, 로그 저장 비용 절감
로그 폭증 주의: 모든 NAT 이벤트를 기록하면 수만 CPS 환경에서 로그만으로 디스크와 CPU가 포화될 수 있습니다. RFC 6888도 CGN에서 목적지까지 기록하는 것은 가능한 한 피해야 한다고 권고합니다. 결정적 포트 블록(deterministic port block allocation)은 로깅 없이도 "이 외부 IP:포트 범위는 어느 가입자의 것"인지 계산으로 역추적할 수 있어 대규모 환경에서 실용적입니다.

IPv6 환경에서의 NAT 필요성 재평가

IPv6의 설계 철학은 "NAT 없이 end-to-end 연결성을 회복"하는 것입니다. 하지만 실무에서는 IPv6에서도 NAT가 사용되는 경우가 있으며, 각각의 동기와 trade-off를 정확히 이해하는 것이 중요합니다.

IPv6 NAT 유형동기대안권고
NPTv6 (RFC 6296) 멀티홈 시 프리픽스 독립성, ISP 변경 시 내부 주소 유지 PI (Provider Independent) 주소, ULA + PA 듀얼 멀티홈이 꼭 필요하고 PI 주소를 받기 어려운 경우에 한정
Stateful NAT66 "IPv4처럼 숨기고 싶다" stateful 방화벽 (ip6tables/nftables filter) NAT가 보안을 제공한다는 것은 오해. 방화벽으로 충분
NAT64 (RFC 6146) IPv6-only 네트워크에서 IPv4 서비스 접근 듀얼스택, 464XLAT, MAP-E/MAP-T IPv6 전환기에 실용적. DNS64와 함께 사용
SIIT/MAP-E/MAP-T IPv4-as-a-Service over IPv6 인프라 듀얼스택 통신사업자의 IPv4 서비스 유지에 사용
핵심 원칙: IPv6에서 NAT를 도입하기 전에 "이 NAT가 해결하려는 문제를 방화벽이나 라우팅 정책으로 대체할 수 없는가"를 먼저 확인하세요. 대부분의 "주소를 숨기고 싶다" 요구사항은 stateful 필터 규칙으로 동일하게 달성할 수 있으며, end-to-end 연결성을 보존합니다. IPv6 라우팅과 주소 체계에 대해서는 IP (IPv4/IPv6) 문서를 참고하세요.

CGNAT: 포트 블록 할당과 RFC 6888

앞서 CGNAT의 운영 포인트를 개괄했지만, 실제 대규모 CGN을 설계할 때는 포트 블록 할당(Port Block Allocation, PBA) 전략이 성능, 로그 비용, 공정성 세 축을 동시에 결정합니다. 단순히 "포트를 랜덤으로 뿌리자"가 아니라, 가입자별로 미리 할당된 포트 범위 안에서만 NAT 바인딩을 만드는 방식입니다.

CGNAT 포트 블록 할당 (Deterministic PBA) 공인 IP 풀 203.0.113.1 ~ .4 (4개) 포트 범위 (per IP) 1024 ~ 65535 = 64512 포트 블록 크기 256 포트/블록 (가입자당) 수용 가입자 4 x 252 = 1008명 가입자별 포트 블록 배분 예시 (203.0.113.1) 가입자 A 포트 1024 ~ 1279 가입자 B 포트 1280 ~ 1535 가입자 C 포트 1536 ~ 1791 ... 포트 1792 ~ ... 역추적: 외부 IP 203.0.113.1 + 포트 1300 → 블록 (1280~1535) → 가입자 B (로그 불필요) 동적 포트 블록 할당 (Dynamic PBA) 가입자 D 초기: 블록 #0 (256 포트) 블록 소진 감지 사용률 >90% 도달 추가 블록 할당 블록 #1 (256 포트 추가) 최대 블록 제한 max 4블록 (1024 포트) PBA 방식 비교 Deterministic: 로그 불필요, 포트 낭비 가능 | Dynamic: 포트 효율 높음, 블록 할당/해제 로그 필요 두 방식 모두 RFC 6888 REQ-2(가입자별 제한)와 REQ-3(메모리 보호)를 만족시키는 방향으로 설계

RFC 6888은 CGN에 대해 다음과 같은 핵심 요구사항을 명시합니다. 이들은 단순한 권고가 아니라, 상호 운용성과 법적 추적 가능성을 위한 실질적 기준선입니다.

RFC 6888 요구사항내용Linux CGN 설계 시 의미
REQ-1 Endpoint-Independent Mapping (EIM) 지원 필수 persistent 플래그로 같은 내부 endpoint에 같은 외부 매핑 유지. nftables snat persistent 사용
REQ-2 가입자별 외부 포트 수 제한 필수 PBA로 블록 단위 제한 또는 connlimit으로 연결 수 상한. 하나의 봇넷 가입자가 전체 풀을 고갈시키는 것 방지
REQ-3 NAT 상태 테이블 메모리 보호 필수 nf_conntrack_max + 가입자별 conntrack limit. early drop 메커니즘이 최후 방어선
REQ-5 EIM 권장, Endpoint-Independent Filtering (EIF) 지원 시 기본은 끄기 EIF는 방화벽 정책으로 분리. NAT 규칙에서 EIF를 기본 활성화하면 보안 약화 위험
REQ-7 Hairpin 지원 필수 CGN 뒤의 가입자 간 통신에서도 hairpin 경로가 동작해야 함
REQ-9 로깅: 목적지까지 기록하는 것은 피해야 함 Deterministic PBA로 per-session 로그 제거. 블록 할당/해제만 기록하면 역추적 가능
REQ-10 가입자가 매핑을 제어할 프로토콜(PCP) 지원 권장 Port Control Protocol (RFC 6887)로 가입자가 명시적 포트 포워딩 요청 가능

Deterministic PBA의 핵심은 로깅 없이도 역추적이 가능하다는 점입니다. 외부 IP와 포트만 알면 어느 가입자에게 할당된 블록인지 산술적으로 계산할 수 있습니다. 이는 대규모 환경에서 per-session 로그의 저장 비용과 개인정보 문제를 동시에 해결합니다.

# Deterministic PBA 역추적 알고리즘 예시
def reverse_lookup(ext_ip, ext_port, block_size=256, port_start=1024):
    """외부 IP:포트 → 가입자 ID 역산"""
    block_index = (ext_port - port_start) // block_size
    subscriber_id = ip_to_pool_index(ext_ip) * blocks_per_ip + block_index
    return subscriber_id

# 예: 203.0.113.1:1300 → 블록 1 → 가입자 B
# (1300 - 1024) // 256 = 1 → 블록 #1 → 가입자 B
# Linux에서 CGNAT용 nftables 설정 예시
table ip cgnat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;

    # 가입자별 포트 범위를 nftables map으로 관리
    ip saddr 100.64.0.0/10 oifname "wan0" \
      snat to 203.0.113.1-203.0.113.4 persistent,fully-random

    # 가입자별 동시 연결 제한 (REQ-2)
    ip saddr 100.64.0.0/10 ct count over 1024 drop
  }
}

# conntrack 테이블 CGNAT 규모 조정
sysctl -w net.netfilter.nf_conntrack_max=2097152
sysctl -w net.netfilter.nf_conntrack_buckets=524288

# UDP 타이머: RFC 4787 요구 최소 2분
sysctl -w net.netfilter.nf_conntrack_udp_timeout=120
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=180

# TCP ESTABLISHED 타이머 축소 (5일은 CGN에 과도)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=7200
EIM과 게임/P2P: Endpoint-Independent Mapping이 없으면 WebRTC, 온라인 게임, P2P 파일 공유의 NAT traversal 성공률이 급격히 떨어집니다. CGNAT에서 EIM을 지원하지 않으면 STUN/TURN 트래픽이 폭증하고, 가입자 민원이 급증하는 패턴이 반복됩니다. Linux의 persistent 플래그는 EIM을 근사적으로 구현하지만, 완전한 EIM은 추가적인 매핑 정책 설계가 필요합니다.

NAT64/NPTv6 구현 상세: TAYGA, Jool, 464XLAT

앞서 NAT64와 NPTv6의 개념적 차이를 설명했지만, 실제 Linux에서 이를 구현하는 도구는 크게 세 가지입니다. TAYGA(사용자 공간(User Space) stateless NAT64), Jool(커널 모듈(Kernel Module) stateful NAT64), 그리고 464XLAT(CLAT + PLAT 조합)입니다. 각각의 아키텍처와 성능 특성이 다르므로 환경에 맞게 선택해야 합니다.

NAT64 패킷 변환 흐름 (Stateful) IPv6-only 클라이언트 src: 2001:db8:1::20 dst: 64:ff9b::198.51.100.40 DNS64 서버 AAAA 합성: 64:ff9b:: + IPv4 주소 NAT64 변환기 BIB + Session Table Well-Known Prefix: 64:ff9b::/96 IPv4 서버 198.51.100.40 변환 인지 불가 변환 전후 패킷 비교 NAT64 변환 전 (IPv6) IPv6 Header: src=2001:db8:1::20, dst=64:ff9b::c633:6428 TCP Header: sport=49152, dport=443 Next Header: 6 (TCP), Hop Limit: 64 NAT64 변환 후 (IPv4) IPv4 Header: src=203.0.113.10, dst=198.51.100.40 TCP Header: sport=30001, dport=443 Protocol: 6 (TCP), TTL: 63, DF=1 464XLAT 아키텍처 IPv4 애플리케이션 레거시 IPv4-only 소프트웨어 CLAT (클라이언트측) Stateless NAT46 IPv4→IPv6 변환 IPv6-only 전송 사업자 네트워크 순수 IPv6 경로 PLAT (사업자측) Stateful NAT64 IPv6→IPv4 변환 464XLAT: IPv4 앱 → CLAT(NAT46) → IPv6 전송 → PLAT(NAT64) → IPv4 인터넷 Android, iOS, Windows에서 CLAT 지원. 사업자 측 PLAT는 표준 NAT64와 동일
구현유형실행 공간상태특징주요 제한
TAYGA Stateless NAT64/SIIT 사용자 공간 (TUN 디바이스) Stateless 설정이 단순, 1:1 매핑에 적합 포트 공유 불가, TUN 경유로 성능 제한
Jool Stateful NAT64 / SIIT 커널 모듈 Stateful (BIB + session) RFC 6146 준수, 높은 성능, EAMT 지원 외부 커널 모듈, 커널 버전 호환성 관리 필요
464XLAT CLAT Stateless NAT46 클라이언트 디바이스 Stateless IPv4-only 앱의 IPv6 네트워크 사용 가능 CLAT 구현 필요 (clatd, Android native)
nftables prefix NAT NPTv6 커널 (Netfilter) Stateless 메인라인 지원, checksum-neutral IPv6→IPv6만, 포트 변환 없음
# Jool 커널 모듈 기반 Stateful NAT64 설정 예시
modprobe jool
jool instance add "nat64inst" --iptables --pool6 64:ff9b::/96

# IPv4 풀 설정
jool pool4 add --tcp 203.0.113.10 1024-65535
jool pool4 add --udp 203.0.113.10 1024-65535
jool pool4 add --icmp 203.0.113.10 0-65535

# BIB/Session 테이블 확인
jool bib display --tcp
jool session display --tcp

# DNS64: BIND 9 설정 예시
# named.conf에 추가:
# dns64 64:ff9b::/96 {
#     clients { any; };
#     mapped { !rfc1918; any; };
#     suffix ::;
# };
# TAYGA stateless NAT64 설정
# /etc/tayga.conf
# tun-device nat64
# ipv4-addr 192.168.255.1
# ipv6-addr 2001:db8:1::1
# prefix 64:ff9b::/96
# dynamic-pool 192.168.255.0/24

tayga --mktun
ip link set nat64 up
ip route add 192.168.255.0/24 dev nat64
ip route add 64:ff9b::/96 dev nat64

# NPTv6: nftables로 프리픽스 변환
table ip6 nptv6 {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    ip6 daddr 2001:db8:200::/48 \
      ip6 daddr set fd00:10::/48 notrack
  }
  chain postrouting {
    type filter hook postrouting priority srcnat; policy accept;
    ip6 saddr fd00:10::/48 \
      ip6 saddr set 2001:db8:200::/48 notrack
  }
}
DNS64 없이는 NAT64가 불완전: NAT64 변환기가 동작하더라도 IPv6-only 클라이언트는 IPv4 서버의 주소를 알 방법이 없습니다. DNS64 서버가 A 레코드를 AAAA 합성 레코드(예: 64:ff9b::198.51.100.40)로 변환해야 클라이언트가 NAT64 프리픽스를 사용해 접속을 시작할 수 있습니다. DNS64 없는 NAT64는 수동으로 합성 주소를 사용하는 매우 제한된 경우에만 동작합니다.

conntrack과의 깊은 연동: nf_nat_hook과 튜플 변환

NAT와 conntrack의 관계는 "conntrack이 상태를 저장하고 NAT가 헤더를 고친다" 수준보다 훨씬 깊습니다. 커널 내부에서 NAT는 conntrack의 튜플 변환 메커니즘을 직접 조작하며, 이 과정을 이해해야 zone 기반 NAT, 다중 NAT 체인, 복잡한 DNAT+SNAT 조합의 동작을 정확하게 예측할 수 있습니다.

conntrack 튜플 변환과 NAT 바인딩 과정 Original Tuple (ct->tuplehash[DIR_ORIGINAL]) src: 192.168.10.20:53000 dst: 198.51.100.40:443 proto: TCP, zone: 0 NAT 적용 후에도 이 tuple은 변하지 않음 Reply Tuple (ct->tuplehash[DIR_REPLY]) NAT 전: src=198.51.100.40:443, dst=192.168.10.20:53000 SNAT 후: src=198.51.100.40:443, dst=203.0.113.10:40001 nf_conntrack_alter_reply()가 reply tuple을 수정 응답 패킷이 이 tuple과 매치되면 역변환 적용 NAT nf_nat_manip_pkt() 내부 동작 1. 방향 판별 ORIGINAL 방향: NAT 적용 REPLY 방향: 역변환 적용 2. 프로토콜별 헤더 수정 l3proto->manip_pkt() — IP 헤더 l4proto->manip_pkt() — TCP/UDP/ICMP 3. 체크섬 보정 IP checksum 재계산 TCP/UDP pseudo-header 체크섬 갱신 Zone 기반 NAT: 동일 IP 주소가 다른 의미를 가질 때 Zone 1 (인터페이스 A) 10.0.0.5:80 → 내부 서버 A conntrack zone=1에서 추적 Zone 2 (인터페이스 B) 10.0.0.5:80 → 내부 서버 B conntrack zone=2에서 추적 같은 5-tuple이라도 zone이 다르면 별도 conntrack 엔트리 → 별도 NAT 바인딩 가능 nf_conntrack_tuple = {src_ip, src_port, dst_ip, dst_port, protocol, zone} zone이 포함되므로 동일 IP:포트라도 서로 다른 연결로 취급됨 → 멀티테넌트 NAT에 핵심
/* net/netfilter/nf_nat_core.c — nf_nat_manip_pkt() 개념 축약 */
static unsigned int
nf_nat_manip_pkt(struct sk_buff *skb,
                  struct nf_conn *ct,
                  enum nf_nat_manip_type mtype,
                  enum ip_conntrack_dir dir)
{
    struct nf_conntrack_tuple target;

    /* ORIGINAL 방향이면 NAT된 값 적용, REPLY면 역변환 */
    if (dir == IP_CT_DIR_ORIGINAL)
        target = ct->tuplehash[IP_CT_DIR_REPLY].tuple;
    else
        target = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple;

    /* L3 헤더 수정: IP 주소 변경 + IP 체크섬 */
    l3proto->manip_pkt(skb, &target, mtype);

    /* L4 헤더 수정: 포트 변경 + TCP/UDP 체크섬 */
    l4proto->manip_pkt(skb, ct, mtype, &target);

    return NF_ACCEPT;
}
# conntrack zone 설정: 멀티테넌트 NAT 환경
table inet zone_nat {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    iifname "tenant-a" ct zone set 1
    iifname "tenant-b" ct zone set 2
  }

  chain nat_prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    # 같은 외부 포트를 서로 다른 내부 서버로 DNAT
    ct zone 1 tcp dport 80 dnat to 10.1.0.10:80
    ct zone 2 tcp dport 80 dnat to 10.2.0.10:80
  }
}

# zone별 conntrack 확인
conntrack -L -z 1
conntrack -L -z 2
zone의 실전 활용: conntrack zone은 같은 물리 장비에서 여러 테넌트나 VRF의 NAT를 독립적으로 운용할 때 핵심입니다. zone 없이는 동일 5-tuple의 서로 다른 테넌트 트래픽이 같은 conntrack 엔트리를 공유하려 해서 NAT 충돌이 발생합니다. 커널 4.3 이후 ct zone set으로 raw 우선순위에서 zone을 지정할 수 있습니다.

Masquerade 내부 구현: 인터페이스 변경 감지

MASQUERADE는 단순히 "SNAT인데 IP를 자동으로 찾아주는 것"이 아닙니다. 커널의 nf_nat_masquerade.c인터페이스 다운과 주소 제거 이벤트를 감시하는 notifier를 등록하고, 해당 인터페이스에 묶인 conntrack 엔트리를 자동 정리합니다. 이 메커니즘이 MASQUERADE를 DHCP/PPPoE 환경에서 안전하게 만드는 핵심입니다.

/* net/netfilter/nf_nat_masquerade.c — 핵심 구조 축약 */

/* 1. 인터페이스 다운 이벤트 감시 */
static int masq_device_event(
    struct notifier_block *this,
    unsigned long event,
    void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);

    if (event == NETDEV_DOWN) {
        /* 이 인터페이스와 연결된 NAT 매핑 정리 시작 */
        nf_ct_iterate_cleanup_net(net, device_cmp,
                                   (void *)(long)dev->ifindex, 0, 0);
    }
    return NOTIFY_DONE;
}

/* 2. 주소 변경 이벤트 감시 */
static int masq_inet_event(
    struct notifier_block *this,
    unsigned long event,
    void *ptr)
{
    struct in_ifaddr *ifa = ptr;
    struct net_device *dev = ifa->ifa_dev->dev;

    /* 주소가 제거되면 해당 주소로 NAT된 연결 정리 */
    nf_ct_iterate_cleanup_net(net, masq_device_cmp,
                               (void *)(long)dev->ifindex, 0, 0);
    return NOTIFY_DONE;
}

/* 3. conntrack 엔트리 비교: 해당 인터페이스의 NAT인지 확인 */
static int device_cmp(
    struct nf_conn *ct,
    void *ifindex)
{
    const struct nf_conn_nat *nat = nfct_nat(ct);

    if (!nat)
        return 0;
    /* masquerade가 이 인터페이스를 통해 적용됐는지 확인 */
    return nat->masq_index == (int)(long)ifindex;
}
이벤트MASQUERADE 동작SNAT 동작실무 영향
인터페이스 다운 (NETDEV_DOWN) 해당 인터페이스의 모든 NAT conntrack 정리 변화 없음 (conntrack 유지) PPPoE 재연결 시 MASQUERADE는 자동으로 stale 매핑 제거
주소 제거 (NETDEV_CHANGEADDR) 해당 주소에 묶인 NAT conntrack 정리 변화 없음 (규칙의 고정 IP가 바뀌지 않으면) DHCP 갱신 시 MASQUERADE가 주소 변경을 자동 반영
새 패킷 도착 출구 인터페이스의 현재 주소를 실시간(Real-time)으로 조회 규칙에 명시된 고정 IP를 사용 MASQUERADE는 매 새 연결마다 현재 주소를 반영
성능 영향: MASQUERADE의 인터페이스 주소 조회는 새 연결마다 발생하므로, 수천만 CPS 환경에서는 SNAT에 비해 미미하지만 측정 가능한 오버헤드(Overhead)가 있을 수 있습니다. 그러나 대부분의 실무 환경에서 이 차이는 무시할 수준이며, 동적 주소 환경에서의 안전성이 훨씬 중요합니다. 진짜 성능 병목은 MASQUERADE vs SNAT 차이가 아니라 conntrack 테이블 크기와 포트 고갈입니다.

NAT 성능 최적화: conntrack 튜닝과 GRO/GSO

앞서 성능 설계의 개괄을 다뤘지만, 여기서는 구체적인 튜닝 파라미터와 GRO/GSO가 NAT 경로에 미치는 영향을 상세히 살펴봅니다. NAT 성능은 결국 conntrack 해시 효율, 메모리 예산, NIC 오프로드와의 상호작용 세 가지로 분해됩니다.

파라미터기본값권장 튜닝 방향계산 근거
nf_conntrack_buckets 메모리에 비례 (보통 65536) 예상 최대 연결 수의 1/4 ~ 1/2 평균 체인 길이 2~4가 목표. 체인이 길면 lookup O(n) 증가
nf_conntrack_max = nf_conntrack_buckets (커널 버전에 따라 다름) 예상 동시 세션 수의 1.5~2배 엔트리당 ~300바이트. 1M 엔트리 ≈ 300MB 메모리
nf_conntrack_tcp_timeout_established 432000초 (5일) CGN: 7200초 (2시간), 서버: 3600초 유휴 TCP가 테이블을 장기 점유하는 것을 방지
nf_conntrack_tcp_timeout_time_wait 120초 60초로 축소 가능 TIME_WAIT는 이미 종료된 연결이므로 빠른 회수가 유리
hashsize (모듈 파라미터) 시스템 메모리에 비례 부팅 시 options nf_conntrack hashsize=262144 런타임에 변경 불가(일부 커널), 부팅 시 설정 권장
# conntrack 해시 테이블 상태 확인
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_buckets

# 해시 체인 분포 확인 (커널 디버그 정보)
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/net/nf_conntrack | wc -l

# 메모리 사용량 추정
# entries * ~300 bytes = 메모리
# 1,000,000 entries ≈ 300MB
# 2,000,000 entries ≈ 600MB

# 권장 CGN 튜닝 셋
sysctl -w net.netfilter.nf_conntrack_max=2097152
sysctl -w net.netfilter.nf_conntrack_buckets=524288
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=7200
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=60
sysctl -w net.netfilter.nf_conntrack_udp_timeout=120
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=180

# GC 관련 (커널 5.x+에서는 자동 관리가 대부분)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_close_wait=60
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_fin_wait=60

GRO/GSO와 NAT의 상호작용은 자주 간과되지만 성능에 큰 영향을 미칩니다. GRO(Generic Receive Offload)는 여러 패킷을 하나의 큰 skb로 합쳐서 상위 스택에 올리고, GSO(Generic Segmentation Offload)는 반대로 전송 시 큰 skb를 분할합니다. 문제는 NAT가 이 합쳐진/분할된 패킷의 어느 시점에서 체크섬(Checksum)을 보정하는가입니다.

오프로드NAT 경로에서의 동작성능 영향주의사항
GRO (수신) 합쳐진 skb에 대해 conntrack lookup 1회 lookup 횟수 감소로 성능 향상 서로 다른 conntrack 엔트리의 패킷이 합쳐지면 문제 발생 가능
GSO (송신) NAT 헤더 수정 후 GSO로 분할 큰 패킷 단위로 NAT 처리하므로 per-packet 비용 감소 flowtable 경로에서도 GSO 유지 가능
TSO (HW 송신) NIC가 세그먼트 분할 CPU 부하 최소화 HW offload NAT와 결합 시 최대 효율
LRO (HW 수신) NIC가 합침, 포워딩 환경에서는 GRO 권장 포워딩 시 LRO는 수신측 IP가 바뀌므로 비활성화 필요 ethtool -K eth0 lro off
LRO vs GRO: NAT/포워딩 장비에서는 LRO를 끄고 GRO만 사용하는 것이 안전합니다. LRO는 패킷을 하드웨어에서 합칠 때 로컬 소비를 가정하므로, 포워딩 경로에서 다시 분할해야 하는 오버헤드가 생깁니다. GRO는 이 문제를 인식하고 설계되었으므로 포워딩 호환성이 좋습니다.

Hairpin NAT: 내부 클라이언트에서 공인 IP로의 접근

앞서 hairpin NAT의 개념을 설명했지만, 실제 패킷 경로와 필요한 규칙을 구체적으로 추적하면 이해가 훨씬 깊어집니다. Hairpin NAT의 핵심 문제는 응답의 비대칭 경로입니다. DNAT만 적용하면 서버는 클라이언트에게 직접 응답하고, 클라이언트는 "내가 접속한 공인 IP가 아닌 내부 IP에서 답이 왔다"고 인식해 세션을 거부합니다.

실패 경로: DNAT만 적용 (hairpin SNAT 없음) 내부 클라이언트 192.168.10.20 게이트웨이 (NAT) LAN: .1 / WAN: 203.0.113.10 내부 서버 192.168.10.30:8443 src=.20 dst=203.0.113.10:443 DNAT: dst→.30:8443 서버가 클라이언트에 직접 응답! (src=.30, 게이트웨이 우회) 클라이언트: RST! "203.0.113.10이 아님" 성공 경로: DNAT + hairpin SNAT 적용 내부 클라이언트 192.168.10.20 게이트웨이 (NAT) LAN: .1 / WAN: 203.0.113.10 내부 서버 192.168.10.30:8443 src=.20 dst=203.0.113.10:443 DNAT+SNAT: src→.1 dst→.30:8443 서버→게이트웨이 (src=.30 dst=.1) 역변환: src=203.0.113.10:443 Hairpin NAT conntrack 엔트리 상세 conntrack 엔트리 (hairpin 경로) Original: src=192.168.10.20:53000 → dst=203.0.113.10:443 Reply: src=192.168.10.30:8443 → dst=192.168.10.1:53000 Status: IPS_SRC_NAT | IPS_DST_NAT | IPS_CONFIRMED DNAT(dst→.30:8443)과 SNAT(src→.1)이 동시에 적용된 상태 Split-horizon DNS: Hairpin을 피하는 대안 내부 DNS가 service.example.com → 192.168.10.30 직접 응답 (내부에서는 공인 IP 불필요) 외부 DNS는 동일 이름에 대해 203.0.113.10 응답 NAT 부하 없이 직접 통신 가능하므로, 관리 가능한 환경에서는 hairpin보다 권장
# Hairpin NAT 완전한 nftables 설정
table inet nat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    # 외부 + 내부 모두에서 공인 IP로 접근 시 DNAT
    tcp dport 443 ip daddr 203.0.113.10 dnat to 192.168.10.30:8443
  }

  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    # 외부 나가는 트래픽: 일반 masquerade
    oifname "wan0" masquerade

    # Hairpin: 내부→내부 트래픽에서 소스를 게이트웨이로 변경
    # (서버가 게이트웨이로 응답을 보내도록 강제)
    ip saddr 192.168.10.0/24 ip daddr 192.168.10.30 tcp dport 8443 \
      snat to 192.168.10.1
  }
}
Hairpin의 대가: hairpin SNAT를 적용하면 서버에서 볼 때 모든 클라이언트가 게이트웨이 IP에서 오는 것처럼 보입니다. 이는 서버 측 접근 로그에서 실제 클라이언트 IP를 잃는다는 의미입니다. X-Forwarded-For 같은 프록시 헤더를 쓸 수 없는 L4 NAT에서는 이를 피할 방법이 없습니다. 가능하다면 split-horizon DNS로 hairpin 자체를 불필요하게 만드는 것이 가장 깨끗한 해결책입니다.

nftables NAT 구문: map, vmap, 체이닝

nftables의 NAT는 단순한 snat to/dnat to를 넘어 map과 vmap을 활용해 동적이고 효율적인 NAT 규칙을 구성할 수 있습니다. 이는 수십~수백 개의 포트 포워딩 규칙이 필요한 환경에서 규칙 수를 극적으로 줄이고 lookup 성능을 개선합니다.

nftables map/vmap 기반 NAT 동작 패킷 도착 tcp dport = 8080 NAT map lookup dnat to tcp dport map @portfwd 결과: DNAT → 10.0.1.10:80 map vs vmap 비교 map (데이터 매핑) 키 → 값을 반환 map portfwd { type inet_service : ipv4_addr . inet_service } 8080 : 10.0.1.10 . 80 8443 : 10.0.2.20 . 443 dnat to tcp dport map @portfwd vmap (verdict 매핑) 키 → verdict (액션)을 반환 vmap action { type ipv4_addr : verdict } 10.0.1.0/24 : jump tenant_a_nat 10.0.2.0/24 : jump tenant_b_nat ip saddr vmap @action 런타임 동적 업데이트 nft add element inet nat portfwd { 9090 : 10.0.3.30 . 80 } 규칙 재로드 불필요 map 원소만 추가/삭제 기존 conntrack은 유지 새 연결부터 새 map 원소 적용
# map 기반 다중 포트 포워딩 (DNAT)
table inet nat {
  map portfwd {
    type inet_service : ipv4_addr . inet_service
    elements = {
      8080 : 10.0.1.10 . 80,
      8443 : 10.0.2.20 . 443,
      2222 : 10.0.3.30 . 22,
      3306 : 10.0.4.40 . 3306,
      5432 : 10.0.5.50 . 5432
    }
  }

  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    iifname "wan0" dnat to tcp dport map @portfwd
  }

  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oifname "wan0" masquerade
  }
}

# 런타임에 포트 포워딩 추가 (재로드 불필요)
nft add element inet nat portfwd { 9090 : 10.0.6.60 . 80 }
nft delete element inet nat portfwd { 3306 : 10.0.4.40 . 3306 }

# vmap 기반 테넌트별 NAT 체이닝
table inet multitenant {
  chain tenant_a_nat {
    snat to 203.0.113.10
  }

  chain tenant_b_nat {
    snat to 203.0.113.20
  }

  map tenant_dispatch {
    type ipv4_addr : verdict
    flags interval
    elements = {
      10.0.1.0/24 : jump tenant_a_nat,
      10.0.2.0/24 : jump tenant_b_nat
    }
  }

  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oifname "wan0" ip saddr vmap @tenant_dispatch
  }
}

# concatenated map: 소스 주소+포트로 복합 매핑
table inet complex_nat {
  map svc_map {
    type ipv4_addr . inet_service : ipv4_addr . inet_service
    elements = {
      192.168.1.0/24 . 80 : 10.0.1.10 . 8080,
      192.168.2.0/24 . 80 : 10.0.2.20 . 8080
    }
  }
}
map vs 다수의 규칙: 포트 포워딩 규칙이 100개라면, 개별 dnat 규칙 100개를 나열하는 것과 map 1개를 사용하는 것은 성능이 크게 다릅니다. map은 내부적으로 해시 테이블을 사용하므로 O(1) lookup이 가능하지만, 규칙 나열은 O(n) 순차 검색입니다. 또한 map의 원소는 룰셋 전체를 교체하지 않고도 동적으로 추가/삭제할 수 있어 운영상 유리합니다.

디버깅 & 트레이싱: ftrace와 nf_nat 추적

앞서 기본 디버깅 절차를 다뤘지만, 복잡한 NAT 문제에서는 nft monitor traceconntrack -E 이상의 도구가 필요합니다. 커널의 ftrace 인프라를 활용하면 nf_nat_manip_pkt(), nf_nat_setup_info() 같은 내부 함수의 호출을 직접 추적할 수 있습니다.

# === 1. nft monitor trace: 패킷별 체인 통과 추적 ===
# 추적 대상 패킷 마킹
table inet trace_debug {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    ip saddr 192.168.10.20 tcp dport 443 meta nftrace set 1
  }
}

# 실시간 모니터링
nft monitor trace

# 출력 예시:
# trace id abc123 inet trace_debug prerouting packet:
#   iif "lan0" ether saddr ... ip saddr 192.168.10.20
#   ip daddr 203.0.113.10 tcp dport 443
# trace id abc123 inet nat prerouting rule
#   dnat to 192.168.10.30:8443 (verdict accept)

# === 2. conntrack 이벤트 모니터링 ===
# 새 연결과 소멸 이벤트만 추적
conntrack -E -e NEW,DESTROY -p tcp --orig-dst 203.0.113.10

# 타임스탬프와 ID 포함
conntrack -E -o timestamp,id

# 특정 NAT 바인딩 확인
conntrack -L -p tcp --reply-src 203.0.113.10 -o extended

# === 3. ftrace를 이용한 커널 함수 추적 ===
# nf_nat 관련 함수 추적 설정
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo function > current_tracer
echo 'nf_nat_setup_info nf_nat_packet nf_nat_manip_pkt' > set_ftrace_filter
echo 1 > tracing_on

# 추적 결과 확인
cat trace_pipe | head -100

# 특정 프로세스/CPU만 추적
echo 0 > tracing_on
echo function_graph > current_tracer
echo 'nf_nat_*' > set_ftrace_filter
echo 1 > tracing_on

# === 4. perf를 이용한 NAT 경로 프로파일링 ===
# NAT 관련 함수의 CPU 소비 비율
perf top -g --filter 'nf_nat'
perf record -g -p $(pgrep ksoftirqd) -- sleep 10
perf report --sort comm,dso,symbol

# === 5. dropwatch로 패킷 드롭 위치 추적 ===
dropwatch -l kas
# 또는 perf로 kfree_skb 트레이스포인트
perf record -e skb:kfree_skb -a -- sleep 5
perf script
도구용도장점오버헤드
nft monitor trace 패킷별 체인/규칙 통과 추적 nftables 네이티브, 쉬운 사용 마킹된 패킷에 대해 중간, 프로덕션 주의
conntrack -E conntrack 이벤트 (NEW/UPDATE/DESTROY) 바인딩 생성/소멸 시점 정확 낮음 (이벤트 기반)
ftrace function 커널 함수 호출 추적(Call Trace) nf_nat 내부 동작 가시화 높음, 프로덕션 비권장
ftrace function_graph 함수 호출 그래프와 소요 시간 성능 병목 함수 식별 매우 높음, 디버그 환경 전용
perf CPU 프로파일링(Profiling), 드롭 포인트 시스템 전체 성능 분석 샘플링 기반으로 상대적 낮음
dropwatch 패킷 드롭 위치 추적 "어디서 버려지는가" 직접 확인 낮음
tcpdump (양쪽) NAT 전후 패킷 비교 가장 기본적이고 확실한 검증 캡처 양에 비례
프로덕션 트레이싱 주의: nft monitor trace는 마킹된 패킷에 대해서만 동작하므로 범위를 좁히면 프로덕션에서도 사용 가능합니다. 반면 ftrace function_graph는 모든 NAT 패킷에 대해 오버헤드가 발생하므로 트래픽이 있는 환경에서는 성능 저하가 심합니다. 실무에서는 "tcpdump 양쪽 + conntrack -E + nft trace"의 3단 조합이 대부분의 문제를 해결합니다.

컨테이너/쿠버네티스 NAT: kube-proxy와 CNI

앞서 컨테이너 NAT의 기본을 다뤘지만, Kubernetes 환경에서의 NAT는 훨씬 복잡한 다층 구조를 가집니다. kube-proxy의 iptables 모드, IPVS 모드, 그리고 eBPF 기반 CNI(Cilium)가 각각 다른 방식으로 Service의 ClusterIP를 backend Pod IP로 변환합니다.

Kubernetes kube-proxy iptables 모드 NAT 경로 클라이언트 Pod 10.244.1.5 Service (ClusterIP) 10.96.0.100:80 KUBE-SERVICES chain PREROUTING/OUTPUT에서 진입 kube-proxy가 생성하는 iptables 체인 구조 KUBE-SVC-XXXXX Service별 체인 확률 기반 분산 (statistic mode random) KUBE-SEP-YYYYY Endpoint(Pod)별 체인 DNAT to 10.244.2.10:8080 KUBE-POSTROUTING MASQUERADE (필요 시) mark 0x4000/0x4000 체크 3가지 모드 비교 iptables 모드 기본 모드, 범용 호환 규칙 수 = O(Services x Endpoints) 대규모 클러스터에서 규칙 갱신 느림 확률 기반 로드밸런싱 (무상태) conntrack 기반 세션 유지 IPVS 모드 해시 기반 lookup: O(1) 다양한 스케줄링: rr, lc, sh, ... 대규모 Service에서 성능 우위 여전히 conntrack 사용 ipvsadm으로 상태 확인 가능 eBPF 모드 (Cilium) iptables/conntrack 완전 우회 가능 BPF 맵 기반: O(1) lookup socket-level LB로 DNAT 자체를 회피 XDP/TC 훅에서 조기 처리 hubble로 flow 가시성 제공 NodePort/LoadBalancer 외부 접근 시 추가 NAT externalTrafficPolicy: Cluster → 추가 SNAT (클라이언트 IP 손실) externalTrafficPolicy: Local → SNAT 없음 (클라이언트 IP 보존, 불균등 분산 가능)
# kube-proxy iptables 모드: 실제 생성되는 규칙 확인
iptables -t nat -L KUBE-SERVICES -n --line-numbers
iptables -t nat -L KUBE-SVC-XXXXX -n   # Service별 체인
iptables -t nat -L KUBE-SEP-YYYYY -n   # Endpoint별 체인

# 확률 기반 DNAT 규칙 (3개 backend 예시)
# -A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.33333
#     -j KUBE-SEP-AAA
# -A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000
#     -j KUBE-SEP-BBB
# -A KUBE-SVC-XXXXX -j KUBE-SEP-CCC

# KUBE-SEP-AAA 내부:
# -A KUBE-SEP-AAA -p tcp -j DNAT --to-destination 10.244.2.10:8080

# IPVS 모드 확인
ipvsadm -Ln
# TCP  10.96.0.100:80 rr
#   -> 10.244.2.10:8080   Masq    1      0          0
#   -> 10.244.3.11:8080   Masq    1      0          0

# Cilium eBPF 모드: Service 매핑 확인
cilium service list
cilium bpf lb list

# conntrack과 kube-proxy의 상호작용 디버깅
conntrack -L -d 10.96.0.100
# 참고: IPVS 모드에서도 conntrack은 사용됨 (MASQUERADE 등)
Kubernetes NAT 시나리오NAT 유형어디서 발생주의사항
Pod → ClusterIP Service DNAT (ClusterIP → Pod IP) PREROUTING 또는 OUTPUT (로컬 Pod) conntrack 기반 세션 어피니티가 필요하면 sessionAffinity: ClientIP 설정
Pod → 외부 인터넷 SNAT/MASQUERADE 노드의 POSTROUTING CNI에 따라 Pod IP가 직접 라우팅되면 SNAT 불필요
외부 → NodePort DNAT (NodeIP:NodePort → Pod IP) PREROUTING externalTrafficPolicy: Cluster이면 추가 SNAT으로 클라이언트 IP 손실
외부 → LoadBalancer LB → DNAT → Pod 외부 LB + 노드 PREROUTING 클라우드 LB는 보통 DSR 미지원, 이중 NAT 발생 가능
Pod → 같은 Service (hairpin) DNAT + 조건부 SNAT source Pod = destination Pod일 때 자기 자신으로 DNAT되면 hairpin 문제 재현
규칙 수 폭발: kube-proxy iptables 모드에서 Service 1000개 x Endpoint 10개 = 10,000개 이상의 iptables 규칙이 생성됩니다. 규칙 갱신 시 전체 체인을 원자적(Atomic)으로 교체하므로, 대규모 클러스터에서는 규칙 갱신만으로 수 초의 지연이 발생할 수 있습니다. 이것이 IPVS 모드나 eBPF 기반 CNI로 전환하는 주된 동기입니다.

NAT와 보안: 오해와 실제

NAT가 "보안을 제공한다"는 관념은 현장에서 가장 흔하고 위험한 오해 중 하나입니다. NAT는 주소 변환 도구이지 접근 제어(Access Control) 도구가 아닙니다. 이 섹션에서는 NAT의 보안 효과와 한계를 정확히 구분합니다.

NAT가 실제로 하는 것NAT가 하지 않는 것올바른 대안
내부 주소를 외부에서 직접 보지 못하게 함 포트 포워딩/DNAT을 설정하면 노출됨 방화벽 규칙으로 명시적 허용/거부
외부에서 먼저 연결을 시작하기 어렵게 함 (conntrack 의존) 상태 테이블이 있으면 응답 패킷은 통과함 ct state new 기반 필터로 방향 제어
내부 토폴로지(Topology)를 부분적으로 숨김 애플리케이션 레이어에서 내부 IP 노출 가능 (SIP, FTP 등) ALG/helper 제한, 프록시 사용
포트 스캔 대상을 줄임 (외부 포트만 노출) UPnP/PCP로 자동 포트 매핑이 활성화되면 무력화 UPnP 비활성화, PCP 정책 적용
# NAT가 아닌 방화벽으로 보안을 확보하는 올바른 패턴
table inet firewall {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iifname "lo" accept
    # 명시적으로 허용하는 서비스만
    tcp dport { 22, 80, 443 } accept
    icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert } accept
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    # 내부→외부는 허용
    iifname "lan0" oifname "wan0" accept
    # 포트 포워딩 대상만 허용
    iifname "wan0" oifname "lan0" ip daddr 192.168.10.30 tcp dport 8443 accept
  }
}

# NAT는 주소 변환 역할만
table inet nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oifname "wan0" masquerade
  }
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    iifname "wan0" tcp dport 443 dnat to 192.168.10.30:8443
  }
}
IPv6에서의 교훈: IPv6 환경에서 NAT 없이도 stateful firewall만으로 동일한 보안 수준을 달성할 수 있다는 사실이 "NAT가 보안을 제공한다"는 오해의 본질을 잘 드러냅니다. 보안은 접근 정책(누가 무엇에 접근할 수 있는가)이지, 주소 변환(어떤 주소를 어떤 주소로 바꾸는가)이 아닙니다.

NAT 장애 패턴과 해결 체크리스트

실무에서 반복되는 NAT 장애 패턴을 체계화하면 디버깅 시간을 크게 단축할 수 있습니다. 아래는 가장 흔한 10가지 패턴과 각각의 진단 순서입니다.

#패턴근본 원인진단 명령해결
1 새 연결만 실패, 기존은 정상 conntrack 테이블 포화 sysctl net.netfilter.nf_conntrack_count max 증가, timeout 축소, 불필요 트래픽 notrack
2 특정 외부 IP로만 SNAT 실패 해당 IP의 포트 고갈 conntrack -L --reply-src IP | wc -l IP 풀 확장, 가입자별 제한
3 규칙 변경 후 효과 없음 기존 conntrack 엔트리 유지 conntrack -D -p tcp --dport 443 관련 conntrack 삭제 후 재시험
4 내부에서 공인 IP 접근 불가 hairpin SNAT 누락 tcpdump -ni lan0 양방향 내부→내부 SNAT 추가 또는 split-horizon DNS
5 UDP 세션이 유휴 후 끊김 NAT 타이머 < 앱 keepalive sysctl net.netfilter.nf_conntrack_udp_timeout 타이머 증가 또는 앱 keepalive 단축
6 FTP active 모드 실패 helper 미부착 conntrack -L | grep helper ct helper set "ftp" 규칙 추가
7 큰 파일만 전송 멈춤 PMTUD 블랙홀 tcpdump -ni wan0 icmp TCP MSS clamp, ICMP 경로 보존
8 PPPoE 재연결 후 NAT 깨짐 SNAT의 고정 IP가 더 이상 유효하지 않음 ip addr show ppp0 MASQUERADE 사용으로 전환
9 Kubernetes Service 접속 간헐 실패 conntrack race, 동시 SYN 충돌 conntrack -S (drop/insert_failed 카운터) IPVS 모드 전환 또는 nf_conntrack_tcp_be_liberal=1
10 flowtable offload 적용 안 됨 첫 응답 미관측, fragment, helper conntrack -L | grep OFFLOAD offload 대상 조건 확인, helper 분리
# 종합 진단 스크립트
#!/bin/bash
echo "=== conntrack 상태 ==="
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
echo "사용률: $(( $(sysctl -n net.netfilter.nf_conntrack_count) * 100 / \
  $(sysctl -n net.netfilter.nf_conntrack_max) ))%"

echo ""
echo "=== conntrack 통계 (에러 카운터) ==="
conntrack -S

echo ""
echo "=== NAT 관련 conntrack 분포 ==="
echo "SRC_NAT: $(conntrack -L 2>/dev/null | grep -c SRC_NAT)"
echo "DST_NAT: $(conntrack -L 2>/dev/null | grep -c DST_NAT)"
echo "OFFLOAD: $(conntrack -L 2>/dev/null | grep -c OFFLOAD)"

echo ""
echo "=== 외부 IP별 포트 사용량 (상위 5개) ==="
conntrack -L 2>/dev/null | \
  grep -oP 'src=\K[0-9.]+' | sort | uniq -c | sort -rn | head -5

echo ""
echo "=== 최근 드롭된 conntrack 이벤트 ==="
dmesg | grep -i 'nf_conntrack: table full' | tail -5
체계적 접근: NAT 장애를 만나면 "1) conntrack 포화? 2) 포트 고갈? 3) 경로 비대칭? 4) 타이머?" 순서로 확인하세요. 이 네 가지를 먼저 배제하면 나머지는 대부분 규칙 오류나 helper 문제로 좁혀집니다. conntrack -Sdropinsert_failed 카운터가 0이 아니면 테이블 관련 문제를 강하게 의심해야 합니다.

참고자료