NET namespace
이전에 리눅스 컨테이너 (4)편에서는 프로세스 격리 기능 중 하나인 UTS namespace에 대해 살펴보았습니다. 리눅스 컨테이너에서 사용하는 여러 종류의 namespace 중 NET namespace는 컨테이너가 고유한 네트워크 인터페이스, IP 주소, 라우팅 테이블, 포트 등 네트워크 스택 전반을 독립적으로 소유하도록 하여, 네트워크 자원 측면에서의 완전한 격리를 제공합니다. 이를 통해 하나의 물리 서버에서 여러 컨테이너가 각각 자신만의 가상 네트워크 환경을 구성하고, 다른 컨테이너나 호스트와 충돌 없이 통신하거나 완전히 차단된 상태로 동작할 수 있습니다.
Network Interface
네트워크 인터페이스(Network Interface)는 컴퓨터 시스템이 외부 네트워크와 통신하기 위해 사용하는 가상의 통신 창구 또는 장치를 의미합니다. 쉽게 말해, 운영체제 안에서 네트워크를 통해 데이터를 주고받는 입출력 통로라고 할 수 있습니다.
리눅스와 같은 운영체제에서는 eth0, wlan0, lo와 같은 이름으로 네트워크 인터페이스가 표현되며, 각각의 인터페이스는 MAC 주소, IP 주소, MTU(최대 전송 단위) 등의 속성을 가지고 있습니다. 이러한 속성들은 해당 인터페이스가 데이터를 어떻게 송수신할지를 결정하는 중요한 기준이 됩니다.
하드웨어적으로는 실제 네트워크 카드(NIC, Network Interface Card)가 그 역할을 수행하지만, 현대의 운영체제에서는 이와 별개로 가상 인터페이스(Virtual Interface)도 함께 사용됩니다. 예를 들어, 루프백 인터페이스 lo 역시 시스템 내부 통신을 위한 가상의 네트워크 인터페이스입니다.
그러면 아래의 명령어로 직접 인터페이스 정보를 확인해보겠습니다.
$ ip link
과거에는 "ifconfig"를 사용했지만 이 명령어는 기능도 제한적이고 더 이상 유지보수되지 않아 현대 리눅스에서는 "ip" 명령어를 표준으로 사용합니다.
- 1번 lo는 루프백 인터페이스로, 시스템 내부에서 자기 자신과 통신할 때 사용되는 가상 네트워크 인터페이스입니다.
- 2번 enp0s1은 가상 머신에 할당된 네트워크 인터페이스로, PCI 버스 0번의 슬롯 1번에 연결된 Ethernet 장치를 의미합니다.
- 3번 docker0는 Docker가 컨테이너 간 네트워크 통신을 위해 자동으로 생성하는 가상 브리지 인터페이스입니다.
NET namespace 실습
1. veth 생성
먼저 가상 인터페이스를 만들어보겠습니다. 리눅스에서는 이 가상 인터페이스를 veth(Virtual Ethernet Device)라고 부르며 ip 명령어로 생성할 수 있습니다. veth는 항상 쌍(pair)으로 만들어집니다. 이름은 veth0과 veth1로 붙여주겠습니다.
$ sudo ip link add veth0 type veth peer name veth1
이어서 추가된 네트워크 인터페이스를 확인해보겠습니다.
$ ip -br link
아래 도식은 Docker bridge(docker0)를 제외한 현재 네트워크 인터페이스의 상태를 시각적으로 표현한 것입니다.
초록색 박스는 활성화된(UP) 인터페이스를, 회색 박스는 비활성화된(DOWN) 인터페이스를 나타냅니다.
이전 포스팅에서 살펴본 것처럼, 리눅스의 모든 프로세스는 기본적으로 init(PID 1) 프로세스의 네임스페이스를 공유합니다.
즉, 위에서 확인한 네트워크 디바이스 목록은 기본 네트워크 네임스페이스(Default NET Namespace)에 속한 장치들입니다.
2. NET namespace 생성
이제 새로운 네트워크 네임스페이스(Network Namespace)를 만들어보겠습니다.
NET namespace는 ip 명령어의 netns 서브 커맨드를 통해 생성하고 관리할 수 있습니다. 이름은 ns1로 붙여주겠습니다.
$ sudo ip netns add ns1
$ ip netns list
생성된 네임스페이스는 ip netns list 명령어를 통해 확인할 수 있으며, 이는 /var/run/netns/ 디렉터리 아래에 파일 형태로 저장되어 관리됩니다. 이 파일들은 각각의 네임스페이스에 접근하기 위한 핸들(handle) 역할을 합니다.
$ ls /var/run/netns/
다시 호스트 네트워크 namespace의 네트워크 인터페이스를 확인해보면 NET namespace를 생성하기 전과 아무런 차이가 없습니다.
ip netns에는 exec이라는 서브 커맨드가 있습니다.
이 명령은 마치 docker exec처럼, 특정 네트워크 네임스페이스 내에서 프로세스를 실행할 수 있게 해줍니다.
이를 활용해, 이제 ns1 네임스페이스 안에서 네트워크 디바이스 목록을 확인해보겠습니다.
$ sudo ip netns exec ns1 ip --br link
ns1에서는 lo(루프백 인터페이스) 하나만 출력되는 것을 확인할 수 있습니다.
3. NET namespace에서 프로세스 실행
이렇게 격리된 namespace에서 nginx를 실행하고 동작을 확인해보겠습니다. 그전에 DOWN 상태의 lo(루프백 인터페이스)를 활성화시켜보겠습니다.
$ sudo ip netns exec ns1 ip link set dev lo up
$ sudo ip netns exec ns1 ip --br link
lo(루프백 인터페이스)는 내부 전용 가상 네트워크 인터페이스이기 때문에 데이터 링크 계층(L2)가 존재하지 않기 때문에 활성화 시 UP 이 아닌 UNKNOWN으로 표시됩니다.
그 다음으로 nginx 패키지를 설치해주겠습니다. 이번 실습에서는 이전 포스팅에서 실습한 chroot를 사용하지 않아 호스트와 네임스페이스 간에 파일 시스템을 공유하므로 nginx는 호스트에 설치해주겠습니다.
$ sudo apt update
$ sudo apt install nginx-core
설치가 완료되면 잘 동작하는지 확인해주겠습니다.
$ curl 127.0.0.1
그 다음, nginx를 앞서 생성한 ns1 네임스페이스에서 실행시키기 위해 호스트에서는 서비스를 중지하고 비활성화시키겠습니다.
$ sudo systemctl stop nginx
$ sudo systemctl disable nginx
$ ps aux | grep nginx
$ curl 127.0.0.1
위와 같이 프로세스가 종료된 것을 확인 후 다시 연결 시도해보면 실패하는 것을 확인합니다.
그 다음 ns1에 nginx를 포그라운드로 실행시켜줍니다.
$ sudo ip netns exec ns1 nginx -g 'daemon off;'
이어서 새로운 터미널을 열어 호스트의 네트워크 네임스페이스에서와 ns1 네트워크 네임스페이스에서 각각 127.0.0.1로 nginx 서버에 접근할 수 있는지 확인해봅니다.
$ curl 127.0.0.1
$ sudo ip netns exec ns1 curl 127.0.0.1
이를 통해 호스트의 lo(루프백 인터페이스)와 ns1의 lo(루프백 인터페이스)가 서로 다른 lo(루프백 인터페이스)를 가진다는 것을 확인해볼 수 있습니다.
추가로 netstat을 사용해 두 네트워크 네임스페이스가 리슨중인 포트가 서로 다르다는 것을 확인해볼 수 있습니다.
$ sudo apt install net-tools -y
$ netstat -nat | grep LISTEN
$ sudo sudo ip netns exec ns1 netstat -nat | grep LISTEN
4. veth0, veth1 연결
이제 앞서 만들었던 veth(가상 인터페이스)를 활용해 격리된 두 네트워크 네임스페이스를 연결해보겠습니다.
우선 호스트 네트워크 네임스페이스에 있던 veth1을 n1 네임스페이스로 옮겨보겠습니다.
$ sudo ip link set veth1 netns ns1
$ ip -br link
$ sudo ip netns exec ns1 ip -br link
두 네임스페이스의 네트워크 디바이스 목록을 출력해보면 veth1이 ns1 네트워크 네임스페이스로 이동한 것을 알 수 있습니다.
그러나, 아직 이 장치들은 연결되지 않았습니다. 같은 네트워크 네임스페이스에 있을 때와는 달리 서로 다른 네트워크 네임스페이스에 있을 때에는 각각 독립된 네트워크 공간을 가지고 있어서, MAC 주소나 인터페이스 이름만으로는 상대 인터페이스를 식별하거나 통신할 수 없습니다. 따라서 IP 주소를 할당하고 라우팅 경로를 설정해야 논리적으로 상대를 찾고 통신할 수 있습니다.
veth0에는 고정 IP 10.100.0.2, veth1에는 고정 IP 10.100.0.3을 부여해주겠습니다.
$ sudo ip addr add 10.100.0.2/24 dev veth0
$ sudo ip netns exec ns1 ip addr add 10.100.0.3/24 dev veth1
IP가 제대로 할당되었는지도 확인해보겠습니다.
$ ip addr show veth0
$ sudo ip netns exec ns1 ip addr show veth1
그 다음 각 링크를 활성화시켜주겠습니다.
$ sudo ip link set dev veth0 up
$ sudo ip netns exec ns1 ip link set dev veth1 up
제대로 활성화가 되었는지도 확인해보겠습니다.
$ ip -br link
$ sudo ip netns exec ns1 ip -br link
그리고 원래는 라우팅 경로도 설정해주어야 하지만 같은 서브넷 대역의 IP를 설정하는 경우 라우팅 테이블이 자동으로 설정됩니다.
다음 명령어를 통해 라우팅 테이블도 확인해보겠습니다.
$ ip route
$ sudo ip netns exec ns1 ip route
이제 인터페이스 활성화도하고 IP 할당과 라우팅도 설정되었으니 양쪽에서 ping을 보내 연결을 확인해보겠습니다.
$ ping -c 3 10.100.0.3
$ sudo ip netns exec ns1 ping -c 3 10.100.0.2
양쪽 모두 정상적으로 동작하는 것을 확인할 수 있습니다. 이어서 이전 터미널에서 ns1에 포그라운드로 실행시켰었던 nginx에 할당된 IP를 사용해 호스트 네트워크 네임스페이스에서 연결 시도를 해보겠습니다. 만약 종료되었다면 다시 ns1에서 포그라운드로 nginx를 실행시켜주고 연결 시도를 하면됩니다.
$ curl 10.100.0.3
아주 잘 연결이 되는 것을 확인할 수 있습니다.
Bridge Network
브리지는 데이터 링크 계층(L2)에서 동작하는 장비로, 서로 다른 네트워크 세그먼트를 연결하는 역할을 합니다.
브리지는 물리적인 장비일 수도 있지만, 소프트웨어 기반으로도 구성할 수 있습니다. 리눅스에서는 ip 명령어를 통해 veth 가상 인터페이스뿐만 아니라, 가상 브리지도 생성할 수 있습니다. 여기서 말하는 "브리지"는 실제 네트워크 장비의 이름이기도 하지만, 개념적으로는 가상 스위치로 이해해도 무방합니다.
Bridge Network 실습
앞선 예제에서는 veth를 사용해 네트워크 네임스페이스에 직접 연결하는 방식을 실습해봤다면,
이번 예제에서는 가상 브리지를 통해 여러 네임스페이스(컨테이너)를 연결해보겠습니다.
1. 브리지 생성 및 활성화
우선 브리지를 생성하고 활성화하겠습니다.
$ sudo ip link add br0 type bridge
$ sudo ip link set br0 up
2. NET namespace 추가 구성
이어서 앞에서 실습한 명령어를 활용하여 다음과 같은 네트워크 구성도를 목표로 NET namespace를 하나 더 추가 구성해보겠습니다.
$ sudo ip netns add ns2
$ sudo ip netns exec ns2 ip link set dev lo up
$ sudo ip link add veth2 type veth peer name veth2-br
$ sudo ip link add veth3 type veth peer name veth3-br
$ sudo ip link set veth2 netns ns1
$ sudo ip link set veth3 netns ns2
$ sudo ip netns exec ns1 ip addr add 10.101.0.4/24 dev veth2
$ sudo ip netns exec ns2 ip addr add 10.101.0.5/24 dev veth3
$ sudo ip netns exec ns1 ip link set dev veth2 up
$ sudo ip netns exec ns2 ip link set dev veth3 up
3. 브리지 구성
그리고 veth를 브리지에 연결하고 활성화시킵니다.
$ sudo ip link set veth2-br master br0
$ sudo ip link set veth3-br master br0
$ sudo ip link set dev veth2-br up
$ sudo ip link set dev veth3-br up
4. 방화벽 설정
이제 ns1의 veth2에서 ns2의 veth3에 ping 시도를 해보겠습니다.
$ sudo ip netns exec ns1 ping -c 3 10.101.0.5
veth0과 veth1을 연결할 때와는 달리 ping이 실패하는것을 확인할 수 있습니다.
veth0과 veth1은 두 네임스페이스가 veth 쌍으로 직접 연결된 구성이고, veth2과 veth3는 브리지를 통해 간접적으로 연결된 구성입니다.
브리지를 사용하는 경우 트래픽은 브리지를 거치며 호스트를 경유하게 되므로, 리눅스 커널은 패킷 포워딩 기능을 수행하게 됩니다.
다음 명령어로 리눅스 방화벽의 패킷 포워딩 룰을 확인해보겠습니다.
$ sudo iptables -L | grep FORWARD
policy DROP 이란 호스트를 경유하는 모든 트래픽을 기본적으로 차단한다는 뜻입니다.
앞선 ping 테스트에서 이 정책 때문에 veth2와 veth3 사이에 연결이 실패했던 것입니다.
이 정책을 ACCEPT로 변경해주겠습니다.
$ sudo iptables --policy FORWARD ACCEPT
이제 다시 ping을 보내보겠습니다.
양쪽 모두 연결이 잘 되는 것을 확인할 수 있습니다.
5. 라우팅 설정
이번에는 호스트 네트워크 네임스페이스에서 ns1과 ns2로 ping 연결을 해보겠습니다.
$ ping -c 3 10.101.0.4
위와 같이 연결이 되지 않는 것은, 호스트 네트워크 네임스페이스에 10.101.0.4 주소에 대한 경로가 존재하지 않기 때문입니다.
라우팅 테이블을 확인해보겠습니다.
$ ip route
- default(어느 경로에도 해당하지 않을 때)로 향하는 트래픽은 192.168.64.1 게이트웨이를 통해서 나갑니다.
- 10.100.0.0/24로 향하는 트래픽은 veth0 인터페이스를 통해서 나갑니다.
ns1과 ns2로의 라우팅을 설정하기 위해, 브리지 인터페이스에 IP 주소를 할당하고 브로드캐스트 주소를 지정해주겠습니다.
(일반적으로 브로드캐스트 주소는 모든 호스트 비트를 1로 설정하여 사용하는데, brd의 파라미터로 +를 전달하게 되면 브로드캐스트 주소를 자동으로 계산합니다.)
$ sudo ip addr add 10.101.0.1/24 brd + dev br0
$ ip addr show br0
다시 라우팅 테이블을 확인해보겠습니다.
- 10.101.0.0/24로 향하는 트래픽은 br0 인터페이스를 통해서 나갑니다.
다시 호스트 네트워크 네임스페이스에서 ns1, ns2로 ping 연결을 해보겠습니다.
이제 정상적으로 ping이 보내집니다. 당연히 br0에 할당된 ip로도 ping 연결이 보내집니다.
6. NAT 구성
브리지 네트워크를 통해 default(호스트), ns1, ns2 네트워크 네임스페이스들의 인터페이스를 연결하는 것까지는 성공했지만, 아직 문제가 하나 남아있습니다. 현재 설정으로는 ns1, ns2에서 인터넷에 접속할 수가 없습니다.
$ sudo ip netns exec ns1 ping 8.8.8.8
$ sudo ip netns exec ns1 curl google.com
이를 해결하기 위해서는 NAT와 DNS 셋업을 추가적으로 해야합니다.
NAT를 설정하기에 앞서 호스트 네트워크 네임스페이스에는 default 라우팅이 설정되어 있었습니다. ns1과 ns2에서 규칙에 적용되지 않는 모든 트래픽을 처리할 수 있도록 default 라우팅을 br0으로 설정해줍니다.
$ sudo ip netns exec ns1 ip route add default via 10.101.0.1
ns1과 ns2의 라우팅 테이블을 확인해봅니다.
이어서 NAT 구성을 하기 위해서는 리눅스 커널의 IP 포워드 기능을 활성화해줘야 합니다.
$ sudo sysctl -w net.ipv4.ip_forward=1
그리고 리눅스 방화벽(iptables)에 NAT 규칙을 추가해줍니다.
$ sudo iptables -t nat -A POSTROUTING -s 10.101.0.0/24 -j MASQUERADE
다시 ns1과 ns2에서 외부 주소로 ping을 보내면 정상적으로 동작하는 것을 확인할 수 있습니다.
7. DNS 셋업
그러나 여전히 도메인은 동작하지 않습니다.
이는 네트워크 네임스페이스마다 DNS 설정이 독립적으로 적용되기 때문입니다.
따라서 /etc/netns/<네임스페이스 이름>/resolv.conf 파일을 생성하여 해당 네임스페이스 전용의 DNS 서버를 지정해줄 수 있습니다.
먼저 호스트 네트워크 네임스페이스의 resolv.conf 파일을 확인해보겠습니다.
$ cat /etc/resolv.conf
호스트 네트워크 네임스페이스는 127.0.0.53(루프백 주소)을 사용해 DNS를 처리하지만, ns1과 ns2와 같은 별도 네임스페이스에서는 해당 주소에 접근할 수 없습니다. 따라서 각 네임스페이스에는 외부 DNS 서버를 직접 명시해주어야 합니다.
$ sudo mkdir -p /etc/netns/ns1
$ echo "nameserver 8.8.8.8" | sudo tee /etc/netns/ns1/resolv.conf
$ sudo mkdir -p /etc/netns/ns2
$ echo "nameserver 8.8.8.8" | sudo tee /etc/netns/ns2/resolv.conf
DNS 셋업을 해주고 나면 도메인 요청도 잘 되는 것을 확인할 수 있습니다.
마치며
이번 글에서는 네트워크 인터페이스와 브리지에 대해 알아보고, ip netns 명령어를 활용해 NET 네임스페이스 기반의 간단한 격리 환경을 구성해보았습니다. 다음 글에서는 cgroup에 대해서도 실습해보겠습니다.
'Cloud' 카테고리의 다른 글
리눅스 컨테이너 (4) - namespace [UTS] (1) | 2025.06.09 |
---|---|
리눅스 컨테이너 (3) - namespace [PID] (2) | 2025.06.02 |
리눅스 컨테이너 (2) - chroot (1) | 2025.05.28 |
리눅스 컨테이너 (1) (2) | 2025.05.11 |