iptables / nftables / NAT 끝판정리

croc100·2026년 5월 15일

network

목록 보기
2/2
post-thumbnail

docker run -p 했을 때 무슨 일이 벌어지는가?

들어가며

해당 글은 리눅스 네트워크 기본기와 도커 사용 경험이 있는 초중급자를 위해 작성된 글 입니다
'docker run -p 8080:80 nginx 했더니 그냥 됐는데?' 에서 멈춰있는 사람을 위한 글
'그래서 그 -p 가 도대체 무슨 일을 하는것인가?' 를 따라가는 글이다
이미지 같은건 추후 추가할테니, 도식 텍스트로 우선 양해 바랍니다

들어가기 전에

'docker run -p 8080:80 nginx' 한 번이라도 쳐본적이 있다면, 의문이 든 적이 있을것이다

 - 도커 컨테이너는 컨테이너만의 네트워크를 가진다는데, 
   어떻게 호스트의 8080 포트로 들어오면 컨테이너의 80 포트로 들어가는것인가?
 - 호스트의 방화벽은 그대로인데, 도커가 알아서 포워딩을 해주는것인가?
 - iptables -L 했더니 처음 보는 DOCKER 같은 체인이 잔뜩 생겨있는데, 
   이건 누가 만든것인가?

이 글은 위 의문들을 [iptables의 NAT 테이블을 직접 까보는 방식]으로 답하는 글이다
보너스로, iptables의 후속인 nftables도 같이 본다

1. iptables란 무엇인가? .araboja

iptables는 [리눅스 커널의 패킷 필터링 프레임워크인 netfilter]를 사용자가 제어하기 위한 CLI 도구이다

  • 즉 [iptables ≠ 방화벽 그 자체]가 아닌, [iptables = 방화벽을 설정하는 도구]가 정확한 표현이다
  • 실제로 패킷을 까보고 결정을 내리는것은 커널 안에서 동작하는 netfilter다

지금까지의 리눅스 네트워크 관리의 사실상 표준이었고, 1998년부터 사용되어왔다

  • 너무 오래 살아남은 탓에, 후술할 nftables로 점차 대체되는 추세이지만, 아직도 현역이다

iptables의 구조 (테이블 + 체인)

iptables를 까려면, 먼저 [테이블(table)]과 [체인(chain)]이라는 두 개념을 알아야 한다

a. 테이블 (Table) - 어떤 종류의 작업을 할 것인가

  • filter : 패킷을 허용할것인가 막을것인가 (방화벽의 본체)
  • nat : 패킷의 출발지/목적지 주소를 바꿀것인가 (도커가 주로 쓰는 그것)
  • mangle : 패킷을 세부적으로 조작 (TTL, TOS 등)
  • raw : 연결 추적(connection tracking)을 거치지 않는 특수 처리

b. 체인 (Chain) - 그 작업을 패킷 흐름의 어느 시점에 할 것인가

  • PREROUTING : 패킷이 들어와서 [라우팅 결정되기 전]
  • INPUT : 라우팅 결과, 이 호스트가 목적지인 패킷
  • FORWARD : 라우팅 결과, 이 호스트를 지나가는 패킷
  • OUTPUT : 이 호스트에서 새로 생성된 패킷
  • POSTROUTING : 패킷이 나가기 직전, [라우팅 결정 이후]

테이블과 체인의 관계를 표로 정리하면

PREROUTINGINPUTFORWARDOUTPUTPOSTROUTING
filterOOO
natOOO
mangleOOOOO
rawOO
  • 즉 [어떤 테이블이 어떤 체인을 가질수 있는가]가 위 표인것이고,
  • 도커가 가장 많이 건드리는 자리는 [nat 테이블의 PREROUTING 체인]이다 (외부에서 들어오는 패킷의 목적지를 바꾸기 위해)

여기서 잠깐.

'그래서 패킷이 들어왔을 때, 도대체 어떤 순서로 체인을 거치는것인가?'

들어와서 [이 호스트에 머무는 패킷]의 흐름 :
 PREROUTING -> (라우팅 결정) -> INPUT -> 로컬 프로세스 -> OUTPUT -> POSTROUTING

들어와서 [이 호스트를 지나가는 패킷(=포워딩)]의 흐름 :
 PREROUTING -> (라우팅 결정) -> FORWARD -> POSTROUTING

위 표에서 [nat가 PREROUTING과 POSTROUTING을 가진 이유]가 여기서 나온다
패킷이 들어오자마자(=라우팅 결정 전) 목적지를 바꿔야 [원하는 곳으로 라우팅]이 가능하고,
패킷이 나가기 직전(=라우팅 결정 후) 출발지를 바꿔야 [응답이 돌아올 길]이 막히지 않는다

도커가 -p 옵션으로 만드는것은 PREROUTING 쪽 (외부->컨테이너) 룰이고,
컨테이너가 외부로 나갈 때 적용되는것은 POSTROUTING 쪽 (컨테이너->외부) 룰이다

2. nftables란 무엇인가? .araboja

nftables는 [iptables의 공식 후속작]이다. 2014년 리눅스 커널 3.13에 정식 도입되었다

  • iptables 시대에 누적되어왔던 문제 (각 프로토콜별 별도 도구 - iptables/ip6tables/arptables/ebtables -, 룰 갱신 비효율, 표현력 부족 등)를 한 번에 해결하기 위해 만들어졌다
  • 명령어는 nft이고, iptables보다 문법이 깔끔하며 성능도 좋다

iptables vs nftables 차이

iptablesnftables
등장1998년2014년
명령어iptables / ip6tables / arptables / ebtables 따로nft 하나로 통합
룰 추가룰 하나 추가할 때마다 전체 룰셋 재로딩원자적(atomic) 부분 갱신 가능
문법옵션 폭격 (-A, -p, -j ...)더 사람이 읽기 쉬운 표현식 기반
성능룰이 많아질수록 선형적으로 느려짐룰이 많아도 성능 유지 (set / map 자료구조 활용)
  • 그런데 왜 아직 iptables를 본문에서 메인으로 다루는것인가?
  • 답은 [현장이 아직 iptables 중심이기 때문]이다
  • 특히 [도커]는 아직 기본 백엔드로 iptables를 사용한다

여기서 잠깐.

'iptables 명령어 쳤더니 잘만 동작하던데, 우리 서버는 그럼 iptables 쓰는것인가?'

답: 모를수도 있다.

최근 배포판(Ubuntu 22.04+, RHEL 8+, Debian 11+ 등)에서는 
`iptables` 명령어가 사실 [iptables-nft] 라는 호환 레이어로 동작하는 경우가 많다
즉 명령어는 iptables를 치지만, 내부적으로는 nftables 백엔드가 룰을 처리하고 있다는것

확인 방법 :
 $ iptables --version
 iptables v1.8.x (nf_tables)        <- nftables 백엔드로 동작 중
 iptables v1.8.x (legacy)           <- 진짜 legacy iptables 백엔드

[ (nf_tables) ]가 붙어있다면, 호환 레이어로 동작하는것이다
이 경우, 'iptables -L'로 본 룰이 'nft list ruleset'에는 다른 모양으로 보일수 있다
(서로 다른 family로 들어가있을 수 있기 때문)

이래서 디버깅할 때 사람들이 미친다

3. NAT는 도대체 무엇인가? .araboja

NAT(Network Address Translation)는 단어 그대로 [네트워크 주소를 번역(=변환)하는 기술]이다

  • 패킷이 흘러가는 도중에 [출발지 IP] 또는 [목적지 IP]를 다른 값으로 갈아끼우는 작업이다

NAT는 크게 두 종류로 나뉜다 :

a. DNAT (Destination NAT) - 목적지 변환

외부에서 들어오는 패킷의 [목적지 주소]를 내부 주소로 바꾸는것

  • "내 호스트 IP의 8080 포트로 온 패킷은, 내부 컨테이너 IP의 80 포트로 보낸다" 가 정확히 이것
  • docker run -p 8080:80이 만드는 룰의 정체이다

b. SNAT / MASQUERADE (Source NAT) - 출발지 변환

내부에서 외부로 나가는 패킷의 [출발지 주소]를 외부 IP로 바꾸는것

  • "컨테이너의 IP(172.17.0.x)는 외부에서 라우팅이 안 되니까, 호스트의 IP로 바꿔서 내보낸다"가 정확히 이것
  • 컨테이너가 인터넷에 접속할 수 있는 이유이다
  • SNAT는 변환할 IP를 명시적으로 지정하고, MASQUERADE는 [인터페이스의 현재 IP를 자동으로 사용]한다. 호스트 IP가 동적으로 바뀔수 있는 환경(DHCP 등)에서 MASQUERADE가 유리하다

요약 :

  • DNAT : 외부 -> 내부 (들어올 때 목적지 갈아끼우기)
  • SNAT : 내부 -> 외부 (나갈 때 출발지 갈아끼우기)
  • 도커는 둘 다 쓴다

4. 도커는 네트워크를 어떻게 구성하는가?

본격적으로 docker run -p 추적에 들어가기 전에, 도커가 호스트 네트워크에 무엇을 만들어놓는지 한번 정리하자

a. docker0 브릿지

도커 데몬이 시작되면, 호스트에 [docker0] 라는 리눅스 브릿지가 자동 생성된다

  • 기본 IP 대역은 보통 172.17.0.0/16
  • ip a 명령으로 확인 가능하다
  • 이 브릿지는 [컨테이너들이 서로 통신하고, 외부로 나가는 통로] 역할을 한다

b. veth pair

컨테이너가 만들어지면, 도커는 [veth pair (가상 이더넷 쌍)]이라는 가상 케이블 한 쌍을 만든다

  • 한 쪽 끝은 컨테이너 내부의 eth0
  • 다른 한 쪽 끝은 호스트의 docker0 브릿지에 꽂힘
  • 이 한 쌍이 [컨테이너와 호스트를 잇는 가상 LAN 케이블]인것
   [   Container    ]                            [      Host      ]
       eth0 ---------- (veth pair) ---------- docker0 ---- (외부)
   (172.17.0.2/16)                          (172.17.0.1/16)

c. iptables 룰

그리고 도커는 자동으로 호스트의 iptables에 자기 전용 체인을 만든다 :

  • DOCKER : 도커가 자동 관리하는 메인 룰 영역
  • DOCKER-USER : 사용자가 추가하는 룰 (도커보다 먼저 처리됨, 도커가 안 건드림)
  • DOCKER-ISOLATION-STAGE-1, STAGE-2 : 컨테이너 간 격리 전용

이 셋([브릿지] + [veth] + [iptables 룰])이 [도커 네트워크의 본체]이다

5. 본론: docker run -p 8080:80 nginx 추적

자, 이제 위 개념 다 가지고 실제로 명령어를 친다고 가정하자

$ docker run -d -p 8080:80 --name web nginx

이 한 줄을 치면, 도커가 백그라운드에서 다음 작업을 수행한다 :

  1. nginx 컨테이너 생성
  2. 컨테이너에 IP 할당 (예: 172.17.0.2)
  3. veth pair 생성 -> docker0 브릿지에 연결
  4. iptables nat 테이블의 DOCKER 체인에 DNAT 룰 추가
  5. iptables nat 테이블의 POSTROUTING 체인에 MASQUERADE 룰 추가
  6. iptables filter 테이블의 FORWARD 체인에 허용 룰 추가

4, 5, 6번이 우리가 까볼 룰들이다

실제 룰 확인

호스트에서 iptables NAT 테이블을 까보자

$ sudo iptables -t nat -L -n -v

대략 이런 모양이 나온다 (필요한 부분만 발췌) :

Chain PREROUTING (policy ACCEPT)
target     prot opt source     destination
DOCKER     all  --  0.0.0.0/0  0.0.0.0/0    ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target       prot opt source         destination
MASQUERADE   all  --  172.17.0.0/16  0.0.0.0/0
MASQUERADE   tcp  --  172.17.0.2     172.17.0.2   tcp dpt:80

Chain DOCKER (2 references)
target     prot opt source         destination
RETURN     all  --  0.0.0.0/0      0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0      0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

읽어보자.

PREROUTING 체인 :

  • "들어온 패킷 중 목적지가 LOCAL(=이 호스트의 IP)이면, DOCKER 체인으로 점프해라"
  • 즉 외부에서 호스트의 어떤 포트로든 패킷이 들어오면, DOCKER 체인을 거치게 된다

DOCKER 체인 :

  • "TCP 8080 포트로 온 패킷은, 목적지를 172.17.0.2:80으로 바꿔라(DNAT)"
  • 이게 바로 [-p 8080:80]이 만드는 룰의 정체이다

POSTROUTING 체인 :

  • "172.17.0.0/16 (=컨테이너 네트워크)에서 나가는 패킷은 호스트 IP로 출발지 변환(MASQUERADE)해라"
  • 이게 [컨테이너가 인터넷에 접속할 수 있는 이유]이다

6. 패킷의 흐름 따라가기

위 룰들을 알고 나서, 외부에서 curl http://호스트IP:8080을 쳤을 때의 패킷 흐름을 따라가보자

[ 외부 클라이언트 ]  -- (호스트IP:8080) -->  [ 호스트의 eth0 ]
                                                    |
                                                    v
                                          [ PREROUTING (nat) ]
                                                    |
                                            DOCKER 체인으로 점프
                                                    |
                                                    v
                                       DNAT : 목적지를 172.17.0.2:80 으로 변경
                                                    |
                                                    v
                                            [ 라우팅 결정 ]
                                       (목적지 172.17.0.2 는 docker0 쪽 -> FORWARD)
                                                    |
                                                    v
                                           [ FORWARD (filter) ]
                                                    |
                                       DOCKER 체인의 ACCEPT 룰에 의해 통과
                                                    |
                                                    v
                                [ docker0 ] -> [ veth ] -> [ 컨테이너의 eth0:80 ]
                                                                  |
                                                            nginx 가 응답

핵심은 :

  • [목적지 변환(DNAT)이 PREROUTING에서 일어난다]
  • 변환 직후 [라우팅 결정]에서 "어, 이 패킷은 docker0 쪽으로 보내야겠네"가 결정된다
  • 그 다음에야 filter 테이블의 FORWARD가 통과 여부를 판단한다

여기서 잠깐.

'어, 근데 호스트의 eth0가 받은 패킷의 목적지는 [호스트 IP]였는데, 
 어떻게 INPUT으로 안 가고 FORWARD로 갔지?'

이게 NAT의 미친(?) 부분이다.

PREROUTING의 DNAT가 [목적지 IP를 172.17.0.2로 갈아끼운 직후], 
커널은 '이 패킷의 목적지가 어디지?' 를 다시 판단한다 (= 라우팅 결정)

이때 172.17.0.2는 호스트 IP가 아니라 docker0 너머의 IP니까, 
[INPUT으로 가는게 아닌, FORWARD로 가야 한다] 라는 결론이 나오는것이다

즉 라우팅 결정의 기준이 되는 IP는 [DNAT 적용 이후의 IP]이다
이 순서가 헷갈리면 도커 네트워크 디버깅이 끔찍해진다

7. DOCKER-USER 체인은 왜 있는가?

도커는 컨테이너를 띄울 때마다 DOCKER 체인에 룰을 자동으로 박는다

  • 즉 [DOCKER 체인은 사용자가 손대면 안 되는 영역]인것이다

근데 사용자가 "특정 IP에서 오는 트래픽은 무조건 막고 싶다" 같은 커스텀 룰을 추가하고 싶을 때, 어디에 넣어야 도커가 안 건드리는가?

  • 그 자리가 [DOCKER-USER] 체인이다

DOCKER-USER 체인은 :

  • FORWARD 체인에서 [도커 룰보다 먼저] 처리된다
  • 도커 데몬이 재시작되어도 룰이 살아남는다
  • 사용자가 자유롭게 룰을 추가/삭제할 수 있다
# 예: 192.168.1.100 에서 오는 패킷을 컨테이너로 가지 못하게 막기
$ sudo iptables -I DOCKER-USER -s 192.168.1.100 -j DROP
  • 도커를 운영하면서 방화벽 룰을 추가할 일이 생긴다면, [DOCKER 체인이 아닌 DOCKER-USER 체인]에 넣어라
  • 안 그러면 도커 재시작 / 컨테이너 재생성 시점에 룰이 날아가는 경우가 발생한다 (필자가 겪어본 경험이다..)

8. 그래서 nftables는 언제 쓰는것인가?

본문 내내 iptables를 봤는데, [그래서 nftables는 도대체 언제?] 라는 의문이 생길 수 있다

현재 시점에서 :

  • 도커는 [기본적으로 iptables 백엔드]를 사용한다 (최신 버전에서 nftables 백엔드 옵션이 추가되긴 했지만, 아직 기본값은 iptables)
  • 새로운 시스템을 처음부터 세팅한다면 nftables가 더 깔끔하지만,
  • [도커가 자동으로 만드는 룰]이 iptables 기반이라서, 도커 호스트 자체는 iptables 룰을 계속 보게 된다

즉 현실은 :

  • 도커가 깔린 호스트 -> 결국 iptables 룰 읽을 일이 많다
  • 도커 없는 일반 호스트 -> nftables로 가도 무방, 사실 추천

결론은 [iptables는 아직 안 죽었고, 한동안 안 죽을것]이다

마무리

docker run -p 8080:80 한 줄이 만드는 일을 정리하면 :

  1. 컨테이너 생성 + veth pair로 docker0에 연결
  2. PREROUTING -> DOCKER 체인에서 [호스트:8080 -> 컨테이너:80] DNAT 룰 추가
  3. POSTROUTING에서 [컨테이너 -> 외부] MASQUERADE 룰 추가
  4. FORWARD에서 해당 트래픽 ACCEPT 룰 추가

이 4개가 합쳐져서, 우리가 curl localhost:8080 을 쳤을 때 컨테이너의 nginx가 응답하는것이다

마법처럼 동작하는것 같던 [-p 옵션]의 정체는, 결국 [iptables 룰 몇 개]였다는것

도커 네트워크 디버깅을 하다가 막히면, 가장 먼저 까봐야 할 곳은

  • iptables -t nat -L -n -v
  • iptables -t filter -L -n -v

이 두 명령어이다. 컨테이너가 외부로 못나가거나, 외부에서 컨테이너로 못들어올 때, 위 두 명령어를 외운채로 까보면 90%는 답이 나온다


이렇게 [docker run -p가 도대체 무슨일을 하는것인가]에 대해 까보았습니다
앞으로 도커 네트워크 디버깅에서 막막함을 조금이나마 덜수 있길 바랍니다

사실 요즘은 그냥 docker-compose 쓰면 이런거 신경 안써도 된다.. 까지 신경써야 한다면 당신은 SE다

profile
DevOps / IaaS, SaaS provider

0개의 댓글