TenForward

技術ブログ。はてなダイアリーから移転しました

ip netns コマンドが意外にきめ細やかにコンテナを作ってくれる

(2016-07-26: 誤記修正しました "setns -> nsenter")

お手軽にシェルスクリプトなんかでコンテナを作る場合の強い味方といえば

  • util-linux の unshare/nsenter コマンド
  • iproute2 の ip netns コマンド

が代表的でしょう。"ip netns" は Network Namespace 作ってコマンド実行するだけの単純なコマンドかと思ったら、中では意外に色々細かくやってくれていることがわかったのでちょっと紹介しておきます。

以下は iproute2 4.2.0 で試しています。

/var/run/netns

まずよく知られているのが /var/run/netns 以下に Namespace 名のファイルを作ってくれることですね。これは、Namespace 内で動いているプロセスがなくなったら Namespace が消滅してくれるのを防ぐためです。だって ip netns add が終了したら、Namespace 内にはプロセスいなくなりますから。

まずはこの辺りで /var/run/netns を MS_SHARED にして、自身を bind mount して、ココをマウントポイントにしています。

   636        while (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, NULL)) {
        :(snip)
   644               /* Upgrade NETNS_RUN_DIR to a mount point */
   645               if (mount(NETNS_RUN_DIR, NETNS_RUN_DIR, "none", MS_BIND, NULL)) {
        :(snip)
   651        }
   (ip/ipnetns.cより)

そして

   667        /* Bind the netns last so I can watch for it */
   668        if (mount("/proc/self/ns/net", netns_path, "none", MS_BIND, NULL) < 0) {
   (ip/ipnetns.cより)

ip netns add コマンドのプロセスディレクトリの ns/net ファイルを /var/run/netns 以下に bind mount していますね。このように Namespace を示す特殊なファイルを残したままにしておけば Namespace が消えません。

Namespace と veth 作成

ここで Network Namespace として "netns01" を作成し、ホストとコンテナ間は veth インターフェースを作成します。

# export NETNS="netns01"
# export VETH="veth0"

それでは Network Namespace を作りましょう。

# ip netns add $NETNS
# ip netns list
netns01

簡単ですね。

それでは veth インターフェースを作りましょう。

# ip link add name $VETH-host type veth peer name $VETH-ns
# ip link set $VETH-ns netns $NETNS
# ip addr add 10.10.10.10/24 dev $VETH-host
# ip link set $VETH-host up
# ip netns exec $NETNS ip addr add 10.10.10.11/24 dev $VETH-ns
# ip netns exec $NETNS ip link set $VETH-ns up

ホスト側は "veth-host"、コンテナ側は "veth-ns" という名前のペアを作りました。それぞれアドレスを与えて、インターフェースを up します。

では、この作成した Namespace で bash を実行してコンテナを作成してみましょう。

# ip netns exec $NETNS /bin/bash
# echo $$
11945
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
11: veth0-ns@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ba:aa:99:70:46:ab brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.10.10.11/24 scope global veth0-ns
       valid_lft forever preferred_lft forever
    inet6 fe80::b8aa:99ff:fe70:46ab/64 scope link 
       valid_lft forever preferred_lft forever

"veth0-ns" インターフェースがあって、ちゃんとアドレスが割りあたってますね。まあ、これはそうなるようにコマンド実行していますので当たり前です。

コンテナ内の sysfs

ここで、Namespace 内で /sys 以下を見てみましょう。

# ls /sys/class/net -l
合計 0
lrwxrwxrwx 1 root root 0  7月 26日  20:01 lo -> ../../devices/virtual/net/lo/
lrwxrwxrwx 1 root root 0  7月 26日  20:01 veth0-ns -> ../../devices/virtual/net/veth0-ns/

ちゃんと Namespace 内のインターフェースのみ見えてますね。これは、この辺りの処理ですね。

    66        if (unshare(CLONE_NEWNS) < 0) {
    67               fprintf(stderr, "unshare failed: %s\n", strerror(errno));
    68               return -1;
    69        }
    70        /* Don't let any mounts propagate back to the parent */
    71        if (mount("", "/", "none", MS_SLAVE | MS_REC, NULL)) {
    72               fprintf(stderr, "\"mount --make-rslave /\" failed: %s\n",
    73                      strerror(errno));
    74               return -1;
    75        }
    76        /* Mount a version of /sys that describes the network namespace */
    77        if (umount2("/sys", MNT_DETACH) < 0) {
    78               fprintf(stderr, "umount of /sys failed: %s\n", strerror(errno));
    79               return -1;
    80        }
    81        if (mount(name, "/sys", "sysfs", 0, NULL) < 0) {
    82               fprintf(stderr, "mount of /sys failed: %s\n",strerror(errno));
    83               return -1;
    84        }
    (lib/namespace.cより)
  • 66行目で Mount Namespace を作ってます
  • 71行目で他の Mount Namespace に自身の Mount が伝播しないようにしています
  • 77行目で現在の /sys を umount して
  • 81行目で sysfs を改めて mount します

まあ、当たり前といえば当たり前ですが、ちゃんとここまで面倒見ててくれたんですね、ip netns。

/etc/netns

そして、以上の辺りのソースをつらつらと眺めていたら、気になる関数を見つけました。

    86        /* Setup bind mounts for config files in /etc */
    87        bind_etc(name);
    (lib/namespace.cより)

これを見てみると

    35               snprintf(netns_name, sizeof(netns_name), "%s/%s", etc_netns_path, entry->d_name);
    36               snprintf(etc_name, sizeof(etc_name), "/etc/%s", entry->d_name);
    37               if (mount(netns_name, etc_name, "none", MS_BIND, NULL) < 0) {
    (lib/namespace.cより)

"etc_netns_path" は /etc/netns/[namespace名] が入っていますので、その下のファイルを作成した Namespace 内に bind mount してくれるようです。

早速、ホスト上で /etc/netns/netns01/{hosts,resolv.conf} を作ってみました。

# cat /etc/netns/$NETNS/hosts 
10.10.10.10	host01
10.10.10.11	ns01
# cat /etc/netns/$NETNS/resolv.conf 
nameserver 8.8.8.8

以上を準備してから、再度コンテナを作成します。

# ip netns exec $NETNS /bin/bash
# cat /etc/resolv.conf 
nameserver 8.8.8.8
# cat /etc/hosts
10.10.10.10	host01
10.10.10.11	ns01

見事に作成した Namespace で実行されるコンテナでは準備したファイルが見えていますね。こんなとこまで面倒見てくれてたとは。(ちゃんと man ip-netns に書いてあるやん)

ip netns コマンドのソース、シンプルでコメントもちゃんと書かれてあって読みやすいのでコンテナ作成の勉強になる気がします。