Skip to content
Project Jelly Blog
Go back

컨테이너의 네트워크는 어떻게 연결되는가 — 빈 네임스페이스에서 인터넷까지

Edit page

Table of contents

Open Table of contents

1. 컨테이너 안에서 네트워크가 되는 건 당연한가?

컨테이너 안에서 ping 8.8.8.8을 치면 응답이 온다. curl로 외부 API를 호출할 수도 있다. 너무 자연스러워서 의문을 가지지 않는다.

하지만 컨테이너의 네트워크는 호스트와 다르다. 컨테이너는 네트워크 네임스페이스라는 커널 기능으로 격리된 네트워크 공간에서 실행된다. 이 공간은 생성 직후 완전히 비어 있다. 인터페이스도, IP도, 라우팅 테이블도 없다.

그렇다면 누군가가 이 빈 공간에 네트워크를 연결해주고 있다는 뜻이다.

이 글은 그 연결 과정을 손으로 직접 해보는 것이다.

이 글에서는 명령어의 예약어와 우리가 정한 이름을 구분하기 위해, 직접 짓는 이름은 의도적으로 튀게 지었다. banana(네임스페이스), wire-host, wire-banana(veth pair)는 전부 아무 이름이나 가능하다. 반면 type veth, peer name, netns예약어다.

2. 빈 네임스페이스 — 아무것도 없는 상태

네트워크 네임스페이스를 하나 만든다.

ip netns add banana
#           ~~~~~~
#           우리가 정한 이름. 아무거나 가능.

ip netns add는 커널에 새 네트워크 스택을 만들고, /var/run/netns/bananabind mount한다. 프로세스가 없어도 사라지지 않는다.

안에 들어가서 상태를 확인하면:

ip netns exec banana ip link show
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN

ip netns exec banana ip route show
# (아무것도 없음)

ip netns exec banana ping -c 1 8.8.8.8
# connect: Network is unreachable

lo만 있고 DOWN이다. 라우팅도 비어 있다. 어디에도 패킷을 보낼 수 없다.

ip netns exec banana bash를 치면 이 격리된 네트워크 안에서 bash가 실행된다. SSH 같은 원격 접속이 아니라, 커널의 setns() 시스템 콜로 네트워크 네임스페이스 포인터만 교체한 것이다. 파일시스템이나 PID는 호스트와 동일하고, 네트워크 스택만 다른 것을 바라보게 된다.

3. veth pair — 호스트와 네임스페이스를 잇는 통로

빈 방에 바깥과 연결되는 통로를 뚫어야 한다. 이때 쓰는 것이 veth pair다.

ip link add wire-host type veth peer name wire-banana
#           ~~~~~~~~~      ~~~~ ~~~~      ~~~~~~~~~~~
#           우리가 정한 이름  예약어         우리가 정한 이름

wire-hostwire-banana 두 가상 인터페이스가 쌍으로 생성된다. 파이프처럼 연결되어, 한쪽에 패킷을 넣으면 반대쪽에서 나온다. MAC 주소는 커널이 자동으로 만든다.

한쪽을 네임스페이스로 옮긴다.

ip link set wire-banana netns banana
ip link set wire-host up

wire-banana는 호스트에서 사라지고 banana 안에서만 보인다. wire-host는 호스트에 남는다. 통로는 생겼지만 IP가 없으니 통신은 안 된다.

4. IP 할당 — 양쪽에 주소를 붙이기

네트워크 네임스페이스가 격리되어 있으므로, 호스트쪽과 네임스페이스 안쪽에서 각각 설정해야 한다.

# ── 호스트에서 ──
ip addr add 10.200.0.1/24 dev wire-host

wire-host10.200.0.1/24를 붙이면, 주소가 생기고 커널이 10.200.0.0/24 → wire-host 라우팅을 자동 추가한다.

# ── 네임스페이스 안에서 ──
ip netns exec banana ip addr add 10.200.0.2/24 dev wire-banana
ip netns exec banana ip link set wire-banana up
ip netns exec banana ip link set lo up

wire-banana10.200.0.2/24를 붙이면, 마찬가지로 주소와 10.200.0.0/24 → wire-banana 라우팅이 자동 추가된다.

양쪽이 같은 서브넷이어야 커널이 상대를 직접 연결된 이웃으로 보고 패킷을 보낼 수 있다.

/24를 안 붙이면 어떻게 되는가

/24 없이 IP만 붙이면 커널은 /32(자기 자신만)로 처리한다. 서브넷 라우팅이 생성되지 않아 상대를 찾을 수 없고, default route 등록도 거부된다(Error: Nexthop has invalid gateway).

/24는 “같은 네트워크가 10.200.0.0~255 범위”라고 커널에 알려주는 것이다.

banana 안에서 할당한 10.200.0.2는 호스트에서 ip addr show로 보이지 않는다. 격리되어 있기 때문이다. 하지만 ping 10.200.0.2는 된다. 라우팅 경로가 있고 ARP로 MAC을 찾을 수 있으면 통신은 가능하다. “보이는 것”과 “도달 가능한 것”은 다르다.

여기까지 하면 호스트와 네임스페이스는 통신할 수 있다. 다만 이 경로만으로는 인터넷까지 나가지는 못한다.

ip netns exec banana ping -c 1 10.200.0.1
# 64 bytes from 10.200.0.1: icmp_seq=1 ttl=64 time=0.071 ms  ✅

ip netns exec banana ping -c 1 8.8.8.8
# connect: Network is unreachable  ❌

banana의 라우팅에 10.200.0.0/24 경로밖에 없기 때문이다.

5. default route — 외부로 가는 길을 알려주기

ip netns exec banana ip route add default via 10.200.0.1

banana의 라우팅 테이블에 “나머지 전부 → 10.200.0.1로 보내라”를 추가한다.

default via 10.200.0.1은 **“목적지를 10.200.0.1로 바꿔라”가 아니라 “10.200.0.1에게 대신 전달해달라고 보내라”**다. 패킷에는 두 겹의 주소가 있다. L3(IP)는 최종 목적지를, L2(MAC)는 바로 다음 장비를 가리킨다. L3는 편지봉투의 주소, L2는 다음 우체국까지 들고 갈 배달부다. 편지 주소는 안 바뀌고, 배달부만 구간마다 바뀐다.

banana에서 ping 8.8.8.8을 치면 실제로 일어나는 일
1. L3 헤더: 출발지 10.200.0.2, 목적지 8.8.8.8
2. 라우팅 조회: "8.8.8.8? default via 10.200.0.1"
   → 10.200.0.1은 "다음 배달부"일 뿐, 목적지를 바꾸는 게 아님
3. ARP로 10.200.0.1의 MAC을 찾음
4. L2 프레임: 도착 MAC = wire-host의 MAC
   L3는 그대로: 출발지 10.200.0.2, 목적지 8.8.8.8
5. wire-banana → veth pair → wire-host 도착

하지만 default route를 추가해도 외부 인터넷은 아직 안 된다.

6. 왜 외부 통신이 안 되는가

패킷이 호스트에 도착하면, 호스트 커널은 자신의 라우팅 테이블을 본다. 호스트에는 우리가 만들지 않은 기존 라우팅이 이미 있다.

호스트의 라우팅 테이블:
  default via 10.0.1.1 dev ens5       ← 원래 있던 것 (OS/DHCP)
  10.200.0.0/24 dev wire-host         ← 우리가 만든 것

호스트 커널은 “8.8.8.8? default → ens5”로 판단한다. wire-hostens5를 연결한 적은 없다. 호스트가 원래 갖고 있던 default route가 패킷을 ens5로 보내는 것이다. 이때도 패킷의 L3 목적지(8.8.8.8)는 바뀌지 않고, L2(MAC)만 다음 홉의 것으로 새로 만들어진다.

단, 커널은 기본적으로 자기 것이 아닌 패킷을 전달하지 않는다. IP 포워딩을 켜야 한다.

echo 1 > /proc/sys/net/ipv4/ip_forward

여기까지 하면 패킷은 인터넷으로 나간다. 하지만 응답이 돌아오지 않는다.

패킷이 ens5(172.16.93.17)를 통해 나갈 때 출발지 IP가 10.200.0.2다. 사설 IP이므로 외부에서 응답을 돌려보낼 수 없다.

7. MASQUERADE — 출발지 주소 변환

이 문제를 해결하는 것이 iptablesMASQUERADE다. 패킷이 외부로 나가기 직전에 출발지 IP를 호스트의 외부 IP로 바꿔치기한다.

iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o wire-host -j MASQUERADE
iptables -A FORWARD -i wire-host -j ACCEPT
iptables -A FORWARD -o wire-host -m state --state RELATED,ESTABLISHED -j ACCEPT

-j MASQUERADE는 “나가는 인터페이스에 붙어있는 IP를 알아서 써라”는 뜻이다. 172.16.93.17은 규칙 어디에도 적지 않았다. ens5에 붙어있는 기존 IP를 자동으로 쓴다.

ip netns exec banana ping -c 1 8.8.8.8
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=8.35 ms

인터넷이 된다. 집에서 쓰는 공유기의 NAT와 동일한 원리다.

8. 전체 명령어 한눈에

# 1. 빈 네트워크 네임스페이스 생성
ip netns add banana

# 2. veth pair 생성 + 한쪽을 네임스페이스로 이동
ip link add wire-host type veth peer name wire-banana
ip link set wire-banana netns banana
ip link set wire-host up

# 3. IP 할당 — 호스트쪽
ip addr add 10.200.0.1/24 dev wire-host

# 4. IP 할당 — 네임스페이스 안쪽
ip netns exec banana ip addr add 10.200.0.2/24 dev wire-banana
ip netns exec banana ip link set wire-banana up
ip netns exec banana ip link set lo up
ip netns exec banana ip route add default via 10.200.0.1

# 5. 외부 통신을 위한 포워딩 + NAT
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o wire-host -j MASQUERADE
iptables -A FORWARD -i wire-host -j ACCEPT
iptables -A FORWARD -o wire-host -m state --state RELATED,ESTABLISHED -j ACCEPT

# 6. 네임스페이스 안에서 bash 실행
ip netns exec banana bash

9. 사라지는 것, 남는 것

네트워크 네임스페이스와 veth는 커널 객체다. 네임스페이스를 삭제하면 안의 인터페이스도 함께 사라지고, veth는 한쪽을 지우면 반대쪽도 자동으로 정리된다. 반면 iptables 규칙은 별개다. 네임스페이스나 인터페이스가 삭제되어도 규칙은 남는다. 명시적으로 삭제하지 않으면 재부팅할 때까지 유지된다.

10. 컨테이너 네트워크는 조립된 결과다

네트워크 네임스페이스의 네트워크는 처음부터 존재하는 기능이 아니다. 비어 있는 공간에 통로(veth)와 주소(IP), 경로(라우팅), 변환(NAT)을 차례로 붙여서 만든 결과다. 컨테이너 안에서 네트워크가 “그냥 되는” 것처럼 보이는 건, 이 과정을 누군가가 자동으로 해주고 있기 때문이다.

이 글에서 다룬 것은 “연결”이지 “보안”이 아니다. ip_forward를 켜고 FORWARD를 허용한 순간, 네임스페이스에서 호스트의 서비스나 내부 네트워크에도 접근할 수 있게 된다. 인터페이스가 서로 “안 보이는” 것은 보안 경계가 아니다. 실제 컨테이너 환경에서는 호스트쪽 iptables로 허용할 트래픽을 명시적으로 제한해야 한다.

이 글에서 다룬 veth + NAT은 네임스페이스를 연결하는 여러 방법 중 하나다. 여러 네임스페이스를 같은 서브넷으로 묶는 bridge, 커널이 제공하는 macvlan, ipvlan, vxlan 같은 다른 부품도 있다. 하지만 어떤 조합이든 해결해야 하는 문제는 동일하다 — 격리된 공간에 통로를 만들고, 주소를 붙이고, 경로를 알려주는 것.


Edit page
Share this post on:

Previous Post
설정 한 줄 바꾸는 데 왜 이렇게 무거운가 — Helm에서 ArgoCD로, 관심의 윈도우를 나눈 이야기
Next Post
Docker로 만들면 어디서든 된다? — GPU로 드러나는 컨테이너 추상화의 경계