めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Dockerのネットワーク管理とnetnsの関係

RHEL7RC+EPEL版Dockerの前提で解説します。RHEL7RCを最小構成で入れて、次の手順でDockerを導入します。

# yum -y install bridge-utils net-tools
# yum -y install http://download.fedoraproject.org/pub/epel/beta/7/x86_64/epel-release-7-0.1.noarch.rpm
# yum -y install docker-io
# systemctl enable docker.service

Dockerが設定するiptablesの内容を見るために(見やすくするために)、firewalldを停止した上でdockerサービスを起動します。

# systemctl stop firewalld.service
# systemctl mask firewalld.service
# systemctl restart docker.service

まずは普通の説明

dockerサービスを起動すると、コンテナ接続用の仮想ブリッジ「docker0」が用意されます。

# brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.56847afe9799	no		

# ifconfig docker0
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.42.1  netmask 255.255.0.0  broadcast 0.0.0.0
        inet6 fe80::5484:7aff:fefe:9799  prefixlen 64  scopeid 0x20<link>
        ether 56:84:7a:fe:97:99  txqueuelen 0  (Ethernet)
        RX packets 10956  bytes 600362 (586.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 19642  bytes 27720858 (26.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

このブリッジは、物理NICとは接続していないので、外部ネットワークとコンテナが直接に通信するためのものではありません。iptablesを見ると次のようになっています。若干、変なルール設定ですが、少なくとも、docker0に接続したコンテナから外部ネットワークへは、IPマスカレードで出ていけるようになっています。

# iptables-save | grep -v "^#"
*nat
:PREROUTING ACCEPT [1:100]
:INPUT ACCEPT [1:100]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -d 172.17.0.0/16 -j MASQUERADE
COMMIT
*filter
:INPUT ACCEPT [133:10308]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [72:9600]
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
COMMIT

CentOS6のイメージをダウンロードして、コンテナを起動してみます。

# docker pull centos
# docker run -it centos /bin/bash

コンテナ内部にはeth0があって、後でみるように、これはブリッジ「docker0」に接続されています。docker0と同じサブネットのIPが割り当てられており、docker0にpingが届きます。IPマスカレードでインターネットまで出て行くこともできます。

bash-4.1# ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 26:B7:4A:7F:01:BC  
          inet addr:172.17.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::24b7:4aff:fe7f:1bc/64 Scope:Link
          UP BROADCAST RUNNING  MTU:1500  Metric:1
          RX packets:7 errors:0 dropped:2 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:558 (558.0 b)  TX bytes:648 (648.0 b)

bash-4.1# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.42.1     0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

bash-4.1# yum -y install iputils traceroute
bash-4.1# ping -c1 172.17.42.1
PING 172.17.42.1 (172.17.42.1) 56(84) bytes of data.
64 bytes from 172.17.42.1: icmp_seq=1 ttl=64 time=0.104 ms

--- 172.17.42.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.104/0.104/0.104/0.000 ms

bash-4.1# traceroute -I 8.8.8.8
traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  172.17.42.1 (172.17.42.1)  0.096 ms  0.018 ms  0.012 ms
 2  192.168.122.1 (192.168.122.1)  0.234 ms  0.212 ms  0.204 ms
...(中略)...
15  209.85.243.156 (209.85.243.156)  72.423 ms  71.965 ms  71.918 ms
16  209.85.244.25 (209.85.244.25)  90.231 ms  89.823 ms  89.788 ms
17  * * *
18  google-public-dns-a.google.com (8.8.8.8)  70.944 ms  70.928 ms  71.182 ms

コンテナを起動したまま、別端末からホストLinuxに再ログインして、ブリッジdocker0を見るとコンテナ内のeth0の片割れのvethが接続されています。

# brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.56847afe9799	no		veth452d

# ifconfig veth452d
veth452d: flags=67<UP,BROADCAST,RUNNING>  mtu 1500
        inet6 fe80::874:87ff:fe84:6aa2  prefixlen 64  scopeid 0x20<link>
        ether 0a:74:87:84:6a:a2  txqueuelen 1000  (Ethernet)
        RX packets 4479  bytes 310641 (303.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7727  bytes 10625737 (10.1 MiB)
        TX errors 0  dropped 1 overruns 0  carrier 0  collisions 0

veth(Virtual Ethernet)は、クロスケーブルで直結された仮想NICのペアを作るLinuxの機能で、ポンチ絵にするとこんな状態になります。

ちなみに、コンテナ内のIPアドレスがどのように設定されているか気になるかも知れません。

「DHCPじゃないの?」

と思ったあなた、

残念。。。。。。OpenStackの勉強のし過ぎです。。。。。。。

コンテナの中で動いているのは、bashだけです。どこにもDHCPクライアントはありません。

bash-4.1# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 05:45 ?        00:00:00 /bin/bash
root        79     1  0 06:13 ?        00:00:00 ps -ef

実は、このアドレスは、Dockerがコンテナを作成した際にコンテナの中に入り込んで、独自にIPアドレスを設定しています。もう少し正確にいうと、このコンテナの「ネットワークネームスペース(netns)」に入り込んで設定しています。

コンテナのnetnsに潜り込む

それでは、ちょっとしたお遊びで、ホストLinuxからコンテナ内のnetnsに潜り込んでみましょう。コンテナは、プロセスの実行環境を分離する機能ですが、「ネットワーク環境の分離=netns」「ファイルシステムの分離=mntns」「プロセステーブルの分離=pidns」のように分離するリソースごとに別々のネームスペース機能で分離しています。したがって、ホストLinuxからコンテナ内のnetnsに潜り込むと、ファイルシステムやプロセステーブルはコンテナの外側(ホストLinux)なのに、ネットワーク設定だけコンテナの中が見える、という面白い状態になります。

netnsを操作する定番ツールはipコマンドですが、このコンテナのnetnsをipコマンドで管理できるようにちょっとした仕込みをします。まず、コンテナ内で起動しているbashのホストLinux上でのPIDを調べます。dockerデーモンの子プロセスのbashを探せばOKです。

# ps -ef | grep "docker -[d]"
root      9498     1  0 13:17 ?        00:00:02 /usr/bin/docker -d
# ps -ef | grep bash | grep 9498
root     10170  9498  0 18:00 pts/2    00:00:00 /bin/bash

このプロセスの/proc情報の中に、このプロセスが属するnetnsを操作するFDへのリンクがあります。

# ls -l /proc/10170/ns/net
lrwxrwxrwx. 1 root root 0  4月 24 18:01 /proc/10170/ns/net -> net:[4026532160]

/var/run/netns/以下からシンボリックリンクを張ると、ipコマンド管理下のnetnsとして認識されます。

# ln -s /proc/10170/ns/net /var/run/netns/hoge
# ip netns
hoge

ここまでくれば、Neutronのデバッグで鍛えたip netnsを駆使してやり放題ですね。このnetns内部でbashを起動します。

# ip netns exec hoge bash

次のようにコンテナ内のネットワーク環境が丸見えです。

# ifconfig eth0
eth0: flags=67<UP,BROADCAST,RUNNING>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 0.0.0.0
        inet6 fe80::8874:a6ff:fe1c:1b2a  prefixlen 64  scopeid 0x20<link>
        ether 8a:74:a6:1c:1b:2a  txqueuelen 1000  (Ethernet)
        RX packets 8  bytes 648 (648.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.42.1     0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

あくまで、netnsだけを切り替えているので、ネットワーク以外の環境(ファイルシステムやプロセステーブル)は、ホストLinuxと同じ状態である点に注意してください。bashを終了すると、ホストLinuxのネットワーク環境に戻ります。

# exit

Dockerがコンテナを作るタイミングで、ホストLinux側から次のような操作をして、コンテナ内のIPアドレスを設定しているものと考えられます。

・ホストLinux側でvethペアを作成する。
・その片割れをコンテナのnetnsに突っ込んで、コンテナからeth0として見えるようにする。
・コンテナのnetnsに入り込んで、コンテナ内のeth0にIPアドレスをセットする。

コンテナにNICを追加する

前述の手法を応用すると、起動中のコンテナに対して、ホストLinux上でvethペアを作成して、後からコンテナにNICを追加することができてしまう気もします。論より証拠で、実際にやってみましょう。

目標はこんな感じです。ブリッジ「br0」が宙に浮いていますが、これを物理NICに接続すれば、コンテナと外部ネットワークを直結することが可能になります。

まずブリッジ「br0」を作成します。

# brctl addbr br0
# ip link set br0 up
# ip addr add 192.168.200.1/24 dev br0

続いて、vethのペアを作成します。ここでは、[veth-host]----[veth-guest]という名前で作成した上で、ホスト側(veth-host)をbr0に接続します。

# ip link add name veth-host type veth peer name veth-guest
# ip link set veth-host up
# brctl addif br0 veth-host

# brctl show br0
bridge name     bridge id               STP enabled     interfaces
br0             8000.b638185b3372       no              veth-host

# ifconfig veth-host
veth-host: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether b6:38:18:5b:33:72  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# ifconfig veth-guest
veth-guest: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether d6:52:68:0a:1d:0e  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

この時点ではゲスト側のveth-guestも見えていますが、次のコマンドでコンテナのnetnsに突っ込むと、ホストからは見えなくなります。

# ip link set veth-guest netns hoge
# ifconfig veth-guest
veth-guest: error fetching interface information: Device not found

逆にコンテナ側で、veth-guestが見えるようになっています。

bash-4.1# ifconfig veth-guest
veth-guest Link encap:Ethernet  HWaddr D6:52:68:0A:1D:0E  
          BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

続いて、コンテナ内のNICの設定を行います。これも、先ほどの「ip netns exec」を駆使してホスト側で行います。まず、コンテナ内でのデバイス名を「eth1」に変更します。

# ip netns exec hoge ip link set veth-guest name eth1

「eth1」に対するIPの設定を行います。ルーティングテーブルも変更して、eth1側をデフォルトゲートウェイにします。

# ip netns exec hoge ip addr add 192.168.200.101/24 dev eth1
# ip netns exec hoge ip link set eth1 up
# ip netns exec hoge ip route delete default
# ip netns exec hoge ip route add default via 192.168.200.1

これで完成です。ホストからpingも通ります。

# ping -c1 192.168.200.101
PING 192.168.200.101 (192.168.200.101) 56(84) bytes of data.
64 bytes from 192.168.200.101: icmp_seq=1 ttl=64 time=0.082 ms

--- 192.168.200.101 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.082/0.082/0.082/0.000 ms

コンテナ内では次のように設定されています。

bash-4.1# ifconfig eth1
eth1      Link encap:Ethernet  HWaddr D6:52:68:0A:1D:0E  
          inet addr:192.168.200.101  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::d452:68ff:fe0a:1d0e/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 b)  TX bytes:648 (648.0 b)

bash-4.1# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.200.1   0.0.0.0         UG    0      0        0 eth1
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
192.168.200.0   0.0.0.0         255.255.255.0   U     0      0        0 eth1

すばらしい。。。。。

ちなみにコンテナを停止すると、後から追加したvethペアもうまく消してくれるようです。ただし、/var/run/netns以下のシンボリックリンクが残るので、これは手で削除する必要があります。これは、「ip netns」で操作するためだけに必要なので、上記の設定が終わったタイミングですぐに削除しても構わないでしょう。

で。。。。

実は、上記の作業を全部やってくれるシェルスクリプト(pipework)がすでに公開されてます。興味のある方は、スクリプトの中身を読み解いてみてください。