ip로 직접 만들어보는 네트워크 네임스페이스와 브리지 네트워크
컨테이너 네트워크 기초 2편

들어가며: ip와 네트워크 네임스페이스

컨테이너 네트워크 기초 1편에서는 리눅스 네임스페이스 중 하나인 UTS 네임스페이스에 대해서 소개했습니다. UTS 네임스페이스는 네트워크 상에서 고유한 이름을 나타내는 호스트네임을 격리시켜주는 역할을 합니다. 이번 글에서는 네트워크 네임스페이스를 소개합니다. UTS 네임스페이스는 비교적 간단합니다만, 네트워크 네임스페이스는 프로세스 간에 네트워크 환경을 격리할 수 있는 매우 강력한 기능들을 제공합니다.

먼저 첫 번째 예제에서는 네트워크 네임스페이스의 기본적인 사용법을 알아보고 가상 이더넷 인터페이스(veth)를 활용해 네트워크들을 직접 연결해봅니다. 두 번째 예제에서는 브리지 네트워크를 구성해서 네트워크 네임스페이스 간에, 혹은 호스트(디폴트 네트워크 네임스페이스)와 네트워크 네임스페이스 간에 통신하는 법을 소개합니다. 마지막으로 NAT를 통해 네트워크 네임스페이스 안에서 인터넷에 연결하는 법을 소개합니다. 처음 보면 조금 어려울 수 있습니다만, 브리지와 NAT를 사용한 네트워크 구성 기법은 도커Docker에서 사용하는 방법과 거의 똑같기 때문에 리눅스와 도커 네트워크를 좀 더 깊이 이해하는 데 도움이 될 것입니다.

이번 글도 시리즈 1편에서 베이그런트와 ubuntu/bionic64 박스로 셋업한 환경에서 진행합니다. 작업 환경 구축에 대한 더 자세한 정보는 아래 글들을 참고해주세요.

44BITS 소식과 클라우드 뉴스를 전해드립니다. 지금 5,000명 이상의 구독자와 함께 하고 있습니다 📮

컨테이너의 네트워크 환경을 격리해주는 네트워크 네임스페이스

UTS 네임스페이스는 비교적 간단히 테스트해볼 수 있습니다만, 네트워크 네임스페이스는 조금 더 복잡합니다.

먼저 네트워크 네임스페이스를 다루기 위한 스위스 아미 나이프를 하나 꺼내보겠습니다. 바로 ip입니다. 아직 리눅스에서 네트워크 환경을 확인할 때 ifconfig에 손이 먼저 나가는 분들이 계실지도 모르지만, 현재는 ip 명령어가 네트워크를 상태를 확인하고 제어하는 표준적인 명령어입니다. 심지어 ip에는 네트워크 네임스페이스를 다루는 기능이 기본적으로 내장되어있습니다.*

*

먼저 ip link로 현재 시스템의 네트워크 디바이스들을 확인해보겠습니다.

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 02:1d:a1:81:8c:fd brd ff:ff:ff:ff:ff:ff
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:30:8b:a6:52 brd ff:ff:ff:ff:ff:ff

1번 lo는 루프백 인터페이스, 2번 enp0s3은 가상 머신에 할당된 네트워크 인터페이스입니다. 5번 docker0는 도커가 설치되면서 설치된 기본 브리지입니다.*

* 이 글에서는 docker0에 대해서 거의 다루지 않습니다. docker0에 대한 내용은 시리즈 다음 편에서 알아볼 예정입니다.

가상 머신의 네트워크 어댑터. Mac Address로 enp0s3과 같은 것을 확인할 수 있습니다

호스트와 컨테이너를 연결하는 가상 인터페이스 만들기

먼저 첫 번재 예제로 호스트와 (네트워크 네임스페이스만 격리된) 컨테이너*를 연결하는 가상 인터페이스를 만들어보겠습니다. 리눅스에서는 이 가상 인터페이스를 veth(Virtual Ethernet Device)라고 부르며 ip 명령어로 생성하는 것이 가능합니다. veth는 항상 쌍으로 만들어집니다. 여기서는 이름을 veth0과 veth0로 붙여주겠습니다.

* 컨테이너를 한 마디로 정의하자면 루트 파일 시스템, 네트워크, 자원 관리, 리눅스 네임스페이스 등 다양한 격리 기술이 얼버무려진 특별히 실행된 프로세스입니다. 네트워크 네임스페이스만 격리된 프로세스도 낮은 수준으로 격리된 컨테이너라고 부를 수 있습니다. 이 글에서 이런 맥락에서 컨테이너를 용어를 사용합니다.

$ ip link add veth0 type veth peer name veth1
Default 네트워크 네임스페이스에 veth0과 veth1을 추가한 상태

이번에는 ip -br a 명령어로 veth0과 veth0이 생성된 것을 확인해봅니다. 이름에서 @ 뒤의 부분은 목적지를 의미합니다.

# ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
enp0s3           UP             02:1d:a1:81:8c:fd <BROADCAST,MULTICAST,UP,LOWER_UP>
docker0          DOWN           02:42:30:8b:a6:52 <NO-CARRIER,BROADCAST,MULTICAST,UP>
veth1@veth0      DOWN           86:b9:5a:db:f0:4a <BROADCAST,MULTICAST,M-DOWN>
veth0@veth1      DOWN           76:e0:cf:50:1b:2c <BROADCAST,MULTICAST,M-DOWN>

여기서 근본적인 질문을 하나 던져보겠습니다. 이 인터페이스 목록에 출력된다는 것은 무엇을 의미할까요?

이전 글에서 간단히 알아보았습니다만, 모든 리눅스의 프로세스들은 기본적으로 init(PID 1) 프로세스의 (디폴트) 네임스페이스를 공유합니다. 즉, 여기에 보이는 장비들은 디폴트 네트워크 네임스페이스에 속해있는 네트워크 디바이스의 목록들입니다.

자, 그럼 새로운 네트워크 네임스페이스를 만들어보겠습니다. 네트워크 네임스페이스는 ip 명령어의 netns 서브 커맨드로 제어할 수 있습니다. add는 네임스페이스를 생성하고, list는 네임스페이스를 보여줍니다. direct_netns라는 네트워크 네임스페이스를 추가합니다.

$ ip netns add direct_netns
$ ip netns list
direct_netns

파일을 사용해서 네임스페이스를 영속화하는 unshare보다 직관적이고 좋네요.*

* ip netns를 사용하면 /var/run/netns/ 아래에 네트워크 네임스페이스가 영속화됩니다. 네트워크 네임스페이스 별 설정은 /etc/netns 아래에 저장합니다.

direct_netns 네트워크 네임스페이스를 추가

ip netns에는 exec라는 서브 커맨드가 있습니다. 마치 docker exec처럼 특정 네트워크 네임스페이스에서 프로세스를 실행시키는 기능입니다. direct_netns 네임스페이스에서 ip --br link를 실행해 네트워크 디바이스 목록을 확인해봅니다.

# ip netns exec direct_netns ip --br link
lo               DOWN           00:00:00:00:00:00 <LOOPBACK>

루프백 디바이스 하나만 출력되는 것을 확인할 수 있습니다.

격리된 네트워크 네임스페이스에서 nginx 서버 실행하고 동작 확인

chroot를 사용하면 프로세스가 바라보는 루트 디렉터리를 변경할 수 있습니다. 이것만으로도 초보적인 수준이지만 그럴듯한 컨테이너를 만들어볼 수 있습니다.

chroot와 마찬가지로 ip로 네트워크 네임스페이스를 격리해서 프로세스를 실행한다면 이것도 또 다른 컨텍스트에서 보자면 컨테이너라고 할 수 있지 않을까요? 격리된 네임스페이스가 실제로 잘 동작하는지 확인하기 위해서 direct_netns 네임스페이스에서 nginx를 실행하고 동작을 확인해보겠습니다.

디폴트 네트워크 네임스페이스에 lo(루프백) 인터페이스가 있는 것처럼, direct_netns 네임스페이스에도 lo(루프백) 인터페이스가 있었습니다. 이 두 인터페이스는 독립되어있습니다. 여기에 착안해 가설을 세워봅니다.

direct_netns에서 nginx 서버를 실행하면,

  1. 디폴트 네트워크 네임스페이스에서는 127.0.0.1로 접근할 수 없고,
  2. direcct_netns에서 127.0.0.1로는 접근이 가능하다.

물론 반대도 마찬가지입니다. 정말로 이렇게 동작할까요? 한 번 확인해보겠습니다.

먼저 direct_netns의 루프백 인터페이스를 동작시켜야합니다.

$ ip netns exec direct_netns ip link set dev lo up
$ ip netns exec direct_netns ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>

루프백 인터페이스의 상태가 DOWN에서 UNKNOWN이 되었습니다(이게 동작 상태입니다 😅).

direct_netns 네트워크 네임스페이스의 lo 인터페이스를 활성화

그 다음으로 nginx 패키지를 설치해줍니다. 이 예제에서는 chroot를 사용하지 않기 때문에 nginx는 호스트에 바로 설치해줍니다.*

* 여기서 chroot를 사용하지 않는다는 의미는 디폴트 네트워크 네임스페이스와 direct_netns 네트워크 네임스페이스 간에 파일 시스템을 공유한다는 의미입니다.

$ apt update
$ apt install nginx-core

다음 명령어로 nginx가 동작하고 있지 않은 것을 확인합니다.

$ curl 127.0.0.1
curl: (7) Failed to connect to 127.0.0.1 port 80: Connection refused

혹시 nginx가 이미 실행중이라면 systemctl stop nginx, systemctl disable nginx 명령어를 실행시켜 서비스를 중지시켜줍니다. ps aux | grep nginx로 프로세스를 확인 후 모두 종료해주세요. 위와 같이 Connection refused가 나오는 것을 확인합니다.

다음 명령어로 nginx를 direct_netns의 포그라운드에 실행합니다.

$ ip netns exec direct_netns nginx -g 'daemon off;'

다른 SSH 창을 하나 더 열어서 디폴트 네트워크 네임스페이스 환경에서 127.0.0.1로 Nginx 서버에 접근할 수 있는지 확인해봅니다.

$ curl 127.0.0.1
curl: (7) Failed to connect to 127.0.0.1 port 80: Connection refused

여전히 Connection refused가 나오는 것을 확인할 수 있습니다. 그렇다면 이번에는 direct_netns 네트워크 네임스페이스에서 같은 명령을 실행해보겠습니다.

$ ip netns exec direct_netns curl 127.0.0.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

디폴트 네트워크 네임스페이스와 direct_netns 네임스페이스에서 curl 127.0.0.1의 실행 결과가 다르다는 것으로 명령어를 실행할 때 다른 루프백 인터페이스에 접속하려는 것을 알 수 있습니다. 앞에서 세운 1번과 2번 모두 만족하는 것을 알 수 있습니다.

실제로 netstat을 사용해보면 두 네트워크 네임스페이스의 리슨중인 포트가 다른 것을 알 수 있습니다.

$ netstat -nat | grep LISTEN
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN

$ ip netns exec direct_netns netstat -nat | grep LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp6       0      0 :::80                   :::*                    LISTEN

가상 인터페이스 veth0, veth1에 연결하고 동작 확인하기

veth1을 direct_netns에 셋업하고 veth0과 veth1을 연결

앞에서 veth0, veth1 인터페이스를 만들어놓기만 하고 사용하지는 않았습니다. 현재 veth0(@veth1)veth1(@veth0) 인터페이스는 모두 디폴트 네임스페이스에 있습니다. 디폴트 네트워크 네임스페이스와 direct_netns 네임스페이스를 연결하기 위해, veth1을 direct_netns 네임스페이스로 옮겨보겠습니다.

$ ip link set veth1 netns direct_netns

명령어가 조금씩 길어집니다만, 의미는 간단합니다. 네트워크 디바이스 veth0의 네트워크 네임스페이스를 direct_netns로 지정한다는 의미입니다. 이제 디폴트 네임스페이스와 direct_netns의 네트워크 디바이스 목록을 출력해봅니다.

$ ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
enp0s3           UP             02:1d:a1:81:8c:fd <BROADCAST,MULTICAST,UP,LOWER_UP>
docker0          DOWN           02:42:30:8b:a6:52 <NO-CARRIER,BROADCAST,MULTICAST,UP>
veth0@if13       DOWN           76:e0:cf:50:1b:2c <BROADCAST,MULTICAST>

$ ip netns exec direct_netns ip -br link
lo               DOWN           00:00:00:00:00:00 <LOOPBACK>
veth1@if14       DOWN           86:b9:5a:db:f0:4a <BROADCAST,MULTICAST>

veth0은 그대로 디폴트 네트워크 네임스페이스에 있고, veth1은 direct_netns 네트워크 네임스페이스로 이동한 것을 알 수 있습니다. 단, 이 장치들은 아직 연결이 되어있지 않습니다(status: DOWN). 인터페이스를 동작시키기에 앞서서 먼저 IP를 할당해주겠습니다.

$ ip a add 10.200.0.2/24 dev veth0
$ ip netns exec direct_netns ip a add 10.200.0.3/24 dev veth1

veth0에 고정 IP 10.200.0.2, veth1에는 고정 IP 10.200.0.3를 부여하였습니다.

아직 (디폴트 네임스페이스에서) 10.200.0.3에 ping을 보내도 응답이 오지 않습니다. direct_netns 네임스페이스에서 10.200.0.2로 핑을 보내면 Network is unreachable 메시지를 출력하며 실패합니다.

$ ping 10.200.0.3
PING 10.200.0.3 (10.200.0.3) 56(84) bytes of data.
^C
--- 10.200.0.3 ping statistics ---
63 packets transmitted, 0 received, 100% packet loss, time 64372ms

$ ip netns exec direct_netns ping 10.200.0.2
connect: Network is unreachable

veth0과 veth1 간에 통신을 하려면 링크를 up 상태로 만들어줘야합니다. ip link set 명령어로 veth0과 veth1 모두 up 상태로 만들어줍니다.

veth0과 veth1에 IP를 부여하고 활성화
$ ip link set dev veth0 up
$ ip netns exec direct_netns ip link set dev veth1 up

$ ip -br link | grep veth0
veth0@if13       UP             76:e0:cf:50:1b:2c <BROADCAST,MULTICAST,UP,LOWER_UP>
$ ip netns exec direct_netns ip -br link | grep veth1
veth1@if14       UP             86:b9:5a:db:f0:4a <BROADCAST,MULTICAST,UP,LOWER_UP>

다시 양쪽에서 ping을 보내봅니다.

$ ping 10.200.0.3
PING 10.200.0.3 (10.200.0.3) 56(84) bytes of data.
64 bytes from 10.200.0.3: icmp_seq=1 ttl=64 time=0.032 ms
64 bytes from 10.200.0.3: icmp_seq=2 ttl=64 time=0.034 ms
^C
--- 10.200.0.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2103ms
rtt min/avg/max/mdev = 0.032/0.034/0.036/0.001 ms

$ ip netns exec direct_netns ping 10.200.0.2
PING 10.200.0.2 (10.200.0.2) 56(84) bytes of data.
64 bytes from 10.200.0.2: icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from 10.200.0.2: icmp_seq=2 ttl=64 time=0.033 ms
^C
--- 10.200.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms
rtt min/avg/max/mdev = 0.027/0.031/0.034/0.005 ms

양쪽다 정상적으로 동작하는 것을 확인할 수 있습니다. 이제 두 네트워크 네임스페이스가 연결도 되어있고, IP도 할당 되어있기 때문에 직접 통신하는 것이 가능합니다. 다시 direct_netns에서 Nginx를 실행하고 디폴트 네임스페이스에서 접속 가능한지 확인해보겠습니다. 이 때 veth1에 할당된 10.200.0.3 IP를 사용합니다.

$ curl 10.200.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

접근이 가능합니다!

네트워크 네임스페이스 netns1과 nets2를 만들고, veth2와 veth3로 연결

브리지 네트워크

브리지는 데이터링크(L2) 계층의 장비로 네트워크 세그먼트를 연결해주는 역할을 합니다. 브리지는 물리 장비나 소프트웨어로 구성할 수 있습니다. ip 명령어를 사용하면 veth 가상 인터페이스 뿐만 아니라, 가상 브리지를 만드는 것도 가능합니다. 브리지라는 이름이 실제로 쓰이기 때문에 브리지라고 사용하고 있습니다만, 가상 스위치라고 생각해도 무방합니다. 첫 번째 예제에서는 veth로 네트워크 네임스페이스에 직접 연결해봤다면, 이번 예제에서는 브리지를 통해 네트워크 네임스페이스(컨테이너)들을 연결해보겠습니다.

브리지 네트워크1: 브리지로 컨테이너들 연결하기

이번에는 veth를 만들고 한쪽은 브리지에 연결하고, 한 쪽은 컨테이너에 연결해보겠습니다. 이번 예제에서 만들고자 하는 네트워크 구성도는 다음과 같습니다.*

* 구성도에서는 네트워크 네임스페이스를 netns1, netns2로 표기하였습니다만, 본문에서는 container4, container5라는 이름을 사용하고 있습니다.

브리지를 사용한 네트워크 구성도. br0와 netns1, netns2를 연결

우선은 브리지를 생성하고 활성화해줍니다. 브리지 이름은 br0를 사용합니다.

$ ip link add br0 type bridge
$ ip link set br0 up

먼저 container4라는 네트워크 네임스페이스를 하나 만들어줍니다. brid4, veth4 쌍으로 된 가상 인터페이스들을 생성합니다. veth4를 container4 네트워크 네임스페이스에 연결하고, ip(10.201.0.4)를 할당해줍니다. 마지막으로 container4 네트워크 네임스페이스의 lo와 veth4와 디바이스를 활성화합니다. 한 번 더 설명했습니다만, 이미 앞에서 해본 작업들입니다.

$ ip netns add container4
$ ip link add brid4 type veth peer name veth4
$ ip link set veth4 netns container4
$ ip netns exec container4 ip a add 10.201.0.4/24 dev veth4
$ ip netns exec container4 ip link set dev lo up
$ ip netns exec container4 ip link set dev veth4 up

여기서부터는 처음하는 작업입니다. 디폴트 네임스페이스에 있는 brid4 가상 인터페이스를 br0에 연결해줍니다. 그리고 brid4 인터페이스를 활성화합니다.

$ ip link set brid4 master br0
$ ip link set dev brid4 up
brid4와 veth4를 연결

어렵지 않죠? 그럼 같은 작업을 한 번 더해보겠습니다. 이번에는 container5 네트워크 네임스페이스를 만들어줍니다. 다음 내용은 생략하겠습니다. 앞에서 실행한 모든 명령어에서 4만 5로 바뀌었습니다.

$ ip netns add container5
$ ip link add brid5 type veth peer name veth5
$ ip link set veth5 netns container5
$ ip netns exec container5 ip a add 10.201.0.5/24 dev veth5
$ ip netns exec container5 ip link set dev lo up
$ ip netns exec container5 ip link set dev veth5 up
$ ip link set brid5 master br0
$ ip link set dev brid5 up

이것으로 디폴트 네트워크 네임스페이스와 container4 네트워크 네임스페이스, container5 네트워크 네임스페이스를 브리지로 연결했습니다.

br0에 netns1, netns2를 연결한 네트워크 구성도

그럼 이제 container4와 container5 사이에 ping을 시도해보겠습니다. 이에 앞서서 container4의 veth4에서 자신의 ip에 대해서 ping을 보내보겠습니다.

$ ip netns exec container4 ping 10.201.0.4
PING 10.201.0.4 (10.201.0.4) 56(84) bytes of data.
64 bytes from 10.201.0.4: icmp_seq=1 ttl=64 time=0.013 ms
64 bytes from 10.201.0.4: icmp_seq=2 ttl=64 time=0.025 ms
^C
--- 10.201.0.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms
rtt min/avg/max/mdev = 0.013/0.020/0.025/0.007 ms

당연히 잘 됩니다. 그럼 container4에서 container5의 veth5 ip에 ping을 보내면 어떻게 될까요?

$ ip netns exec container4 ping 10.201.0.5
PING 10.201.0.5 (10.201.0.5) 56(84) bytes of data.
^C
--- 10.201.0.5 ping statistics ---
7 packets transmitted, 0 received, 100% packet loss, time 6475ms

잘 동작하지 않습니다. 한 번에 잘 될 수도 있습니다만, 제가 사용중인 베이그런트 ubuntu/bionic64 박스에서는 의도한 대로 동작하지 않습니다.

이건 리눅스의 방화벽이라고 할 수 있는 iptables 설정 때문입니다. 먼저 iptable -L로 FORWARD 규칙을 확인해보겠습니다.

$ iptables -L | grep FORWARD
Chain FORWARD (policy DROP)

FORWARD 규칙 값이 DROP으로 되어있습니다. 이 값을 ACCEPT로 변경해주겠습니다. (만약 바로 ping이 보내졌다면 이 값이 이미 ACCEPT일 것입니다)

$ iptables --policy FORWARD ACCEPT
$ iptables -L | grep FORWARD
Chain FORWARD (policy ACCEPT)

이제 다시 ping을 보내봅니다.

$ ip netns exec container4 ping 10.201.0.5
PING 10.201.0.5 (10.201.0.5) 56(84) bytes of data.
64 bytes from 10.201.0.5: icmp_seq=1 ttl=64 time=0.039 ms
64 bytes from 10.201.0.5: icmp_seq=2 ttl=64 time=0.043 ms
^C
--- 10.201.0.5 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1007ms
rtt min/avg/max/mdev = 0.039/0.041/0.043/0.002 ms

아주 잘 동작합니다! 반대도 한 번 확인해보겠습니다.

$ ip netns exec container5 ping 10.201.0.4
PING 10.201.0.4 (10.201.0.4) 56(84) bytes of data.
64 bytes from 10.201.0.4: icmp_seq=1 ttl=64 time=0.039 ms
64 bytes from 10.201.0.4: icmp_seq=2 ttl=64 time=0.043 ms
^C
--- 10.201.0.4 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1134ms
rtt min/avg/max/mdev = 0.039/0.041/0.043/0.002 ms

마찬가지로 의도한 대로 동작하는 것을 확인할 수 있습니다. 디폴트 네임스페이스에서 컨테이너 네임스페이스로 ping을 보내는 경우는 어떨까요?

$ ping 10.201.0.4
PING 10.201.0.4 (10.201.0.4) 56(84) bytes of data.
^C
--- 10.201.0.4 ping statistics ---
5 packets transmitted, 0 received, 100% packet loss, time 4239ms

이건 또 동작하지 않네요. 동작하지 않는 이유는 디폴트 네트워크 네임스페이스에서 10.201.0.4 IP를 어디에서 찾아야할지 모르기 때문입니다. route 명령어를 실행해봅니다.

$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3

br0에 아이피와 브로드캐스트 IP를 셋업합니다.*

* 조금 어려울 수 있습니다만, brd의 다음 값으로 +-를 넣어주기도 합니다. +를 넣으면 앞의 어드레스 값에서 서브넷 마스크의 모든 값을 1로 처리합니다. 즉, 10.201.0.255를 넣은 것과 같습니다. -를 넣어주면 전부 비트값을 0으로 처리합니다.

$ ip addr add 10.201.0.1/24 brd 10.201.0.255 dev br0
$ ip a show br0
9: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ae:42:11:fe:39:1d brd ff:ff:ff:ff:ff:ff
    inet 10.201.0.1/24 brd 10.201.0.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::4819:faff:fe7a:6743/64 scope link
       valid_lft forever preferred_lft forever

다시 route를 실행해봅니다.

$ route
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.201.0.0      0.0.0.0         255.255.255.0   U     0      0        0 br0

이제 10.201.0.0/24 IP 대역이 br0로 연결된 것을 확인할 수 있습니다. 다시 ping을 해보겠습니다.

$ ping 10.201.0.4
PING 10.201.0.4 (10.201.0.4) 56(84) bytes of data.
64 bytes from 10.201.0.4: icmp_seq=1 ttl=64 time=0.096 ms
64 bytes from 10.201.0.4: icmp_seq=2 ttl=64 time=0.041 ms
^C
--- 10.201.0.4 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1172ms
rtt min/avg/max/mdev = 0.041/0.068/0.096/0.028 ms

이제 정상적으로 ping이 보내지네요. 컨테이너에서 br0에 할당된 ip로도 ping을 보내봅니다.

$ ip netns exec container4 ping 10.201.0.1
PING 10.201.0.1 (10.201.0.1) 56(84) bytes of data.
64 bytes from 10.201.0.1: icmp_seq=1 ttl=64 time=0.069 ms
64 bytes from 10.201.0.1: icmp_seq=2 ttl=64 time=0.038 ms
^C
--- 10.201.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1014ms
rtt min/avg/max/mdev = 0.038/0.053/0.069/0.017 ms

역시 잘 동작합니다. 여기까지 브리지 네트워크를 직접 구축해보았습니다.

브리지 네트워크2: NAT 구성을 통해 인터넷에 연결

네트워크 네임스페이스에서 인터넷을 사용하기 위한 NAT 구성

브리지 네트워크를 사용해 default, container4, container5 네트워크 네임스페이스들의 인터페이스를 연결하는 것까지는 성공했습니다만, 아직 문제가 하나 남아있습니다. 지금 설정으로는 컨테이너 네임스페이스에서 인터넷에 접속할 수 없습니다.

$ ip netns exec container4 ping 8.8.8.8
connect: Network is unreachable
$ ip netns exec container4 curl google.com
curl: (6) Could not resolve host: google.com

이를 해결하기 위해서는 NAT와 DNS 셋업을 추가적으로 해야합니다. 차례대로 진행해보겠습니다. 먼저 앞서 디폴트 네임스페이스에서 route를 실행한 결과를 자세히 보면 default 목적지에 대한 규칙이 있었습니다. 하지만 컨테이너 네트워크 네임스페이스에는 이런 규칙이 없습니다.

$ route
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
...

$ ip netns exec container4 route
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.201.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth4

default는 따로 라우팅 규칙을 적용받지 않을 때 나머지 모든 ip에 대한 라우트를 처리합니다. 컨테이너 네트워크 네임스페이스에 default 규칙을 추가해줍니다.

$ ip netns exec container4 ip route add default via 10.201.0.1
$ ip netns exec container5 ip route add default via 10.201.0.1

이 규칙에 따라 다른 규칙이 적용되지 않는 경우 10.201.0.1으로 모든 트래픽이 보내집니다.

$ ip netns exec container4 route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         10.201.0.1      0.0.0.0         UG    0      0        0 veth4
10.201.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth4

NAT 셋업을 위해서는 리눅스의 IP 포워드 기능을 활성화해야합니다.

sysctl -w net.ipv4.ip_forward=1

그리고 iptables에 NAT 규칙을 추가해줍니다.

$ iptables -t nat -A POSTROUTING -s 10.201.0.0/24 -j MASQUERADE

다시 컨테이너 네트워크 네임스페이스에 외부 주소에 핑을 보내보면 정상 동작하는 것을 확인할 수 있습니다.

$ ip netns exec container4 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=33.1 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=32.0 ms
^C
--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3022ms
rtt min/avg/max/mdev = 32.082/33.327/34.273/0.835 ms

하지만 여전히 도메인은 동작하지 않습니다.

$ ip netns exec container4 curl google.com
curl: (6) Could not resolve host: google.com

이는 네트워크 네임스페이스 별로 DNS 설정을 별도로 해야하기 때문입니다. /etc/netns/<NETNS> 디렉터리 아래에 resolv.conf를 만들어 해결할 수 있습니다.

$ mkdir -p /etc/netns/container4/
$ echo 'nameserver 8.8.8.8' > /etc/netns/container4/resolv.conf

다시 한 번 시도해보겠습니다.

$ ip netns exec container4 curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

도메인 요청도 잘 되는 것을 확인할 수 있습니다.

브릿지 네트워크3: 브리지 네크워크 구성부터 NAT 셋업까지 총 정리

지금까지 작업한 내용을 한 번에 정리해봅니다. 먼저 이미 만들어진 가상 장치들을 삭제하거나, 새로운 테스트 환경을 준비해주세요.

ip link delete brid4
ip link delete brid5
ip link delete br0
ip netns delete container4
ip netns delete container5

이제 아래 스크립트로 지금까지의 작업을 한꺼번에 실행할 수 있습니다.

# 브릿지 생성 및 셋업
ip link add br0 type bridge
ip link set br0 up
ip addr add 10.201.0.1/24 brd 10.201.0.255 dev br0
iptables --policy FORWARD ACCEPT

# container4 네트워크 네임스페이스 셋업
ip netns add container4
ip link add brid4 type veth peer name veth4
ip link set veth4 netns container4
ip netns exec container4 ip a add 10.201.0.4/24 dev veth4
ip netns exec container4 ip link set dev lo up
ip netns exec container4 ip link set dev veth4 up
ip link set brid4 master br0
ip link set dev brid4 up
ip netns exec container4 ip route add default via 10.201.0.1

# container5 네트워크 네임스페이스 셋업
ip netns add container5
ip link add brid5 type veth peer name veth5
ip link set veth5 netns container5
ip netns exec container5 ip a add 10.201.0.5/24 dev veth5
ip netns exec container5 ip link set dev lo up
ip netns exec container5 ip link set dev veth5 up
ip link set brid5 master br0
ip link set dev brid5 up
ip netns exec container5 ip route add default via 10.201.0.1

# NAT 및 DNS 셋업
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.201.0.0/24 -j MASQUERADE
mkdir -p /etc/netns/container4/
echo 'nameserver 8.8.8.8' > /etc/netns/container4/resolv.conf
mkdir -p /etc/netns/container5/
echo 'nameserver 8.8.8.8' > /etc/netns/container5/resolv.conf

여기서는 편의상 container4container5 네임스페이스만을 만들었습니다만, 컨테이너 네임스페이스를 추가하는 부분은 간단한 셸 스크립팅으로 확장할 수 있습니다.

마치며

여기까지 ip 명령어와 네트워크 네임스페이스에 대해서 알아보았습니다. 새삼 ip 명령어와 네트워크 네임스페이스가 얼마나 강력한지 느낄 수 있습니다. 실제로 이 기능들은 다양한 네트워크 시뮬레이션 및 테스트 작업에도 많이 사용됩니다. 무엇보다도 컨테이너를 만들 때 매우 중요한 역할을 합니다. 그런데 컨테이너를 만들 때 이런 복잡한 작업을 꼭 해야할까요?

다행히도 그렇지는 않습니다. 이미 도커를 사용하시는 분들은 이해하시겠지만, 네트워크 네임스페이스에 대한 처리는 도커가 컨테이너를 실행하거나 관리하는 동안 전부 알아서 해줍니다. 도커에는 네트워크에 대한 몇 가지 선택지가 있습니다만, 도커에서 기본적으로 사용하는 docker0 브리지 네트워크는 위에서 실습한 내용과 거의 같다고 봐도 무방합니다. 다음 글에서는 도커를 직접 사용해서 컨테이너 네트워크에 대해서 조금 더 알아보도록하겠습니다.