今回は、iproute2 の ip-netns(8) を使わずに、Linux の Network Namespace を操作する方法について書いてみる。 目的は、namespaces(7) について、より深い理解を得ること。
使った環境は次のとおり。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS" $ uname -r 5.4.0-1043-gcp
もくじ
- もくじ
- 下準備
- 前提知識
- unshare(1) / nsenter(1) / mount(8) を使って操作する
- unshare(2) / setns(2) / mount(2) を使って操作する
- 参考
下準備
あらかじめ、必要なパッケージをインストールしておく。
$ sudo apt-get update $ sudo apt-get -y install iproute2 util-linux gcc
前提知識
Linux の namespaces(7) は、プロセスが利用するリソースを分離するための仕組み。 典型的には、Linux のコンテナ仮想化を実現するために用いられている。 今回はタイトルに Network Namespace と入れたものの、分離できるのは何も Network に限らない。
プロセスが利用している Namespace の情報は procfs
から /proc/<pid>/ns
で確認できる。
現在のプロセスであれば、自身の pid を確認するまでもなく /proc/self/ns
を見れば良い。
$ ls -alF /proc/self/ns total 0 dr-x--x--x 2 amedama amedama 0 May 21 12:41 ./ dr-xr-xr-x 9 amedama amedama 0 May 21 12:41 ../ lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 cgroup -> 'cgroup:[4026531835]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 ipc -> 'ipc:[4026531839]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 mnt -> 'mnt:[4026531840]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 net -> 'net:[4026531992]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 pid -> 'pid:[4026531836]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 pid_for_children -> 'pid:[4026531836]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 user -> 'user:[4026531837]' lrwxrwxrwx 1 amedama amedama 0 May 21 12:41 uts -> 'uts:[4026531838]'
これらのファイルの実体はシンボリックリンクで、参照先として表示されている謎の数字は inode 番号を示している。
つまり、Namespace は inode 番号が識別子になっている。
上記であれば、/proc/self/ns/net
がプロセスが利用している Network Namespace の識別子を表している。
$ file /proc/self/ns/net /proc/self/ns/net: symbolic link to net:[4026531992] $ stat -L /proc/self/ns/net File: /proc/self/ns/net Size: 0 Blocks: 0 IO Block: 4096 regular empty file Device: 4h/4d Inode: 4026531992 Links: 1 Access: (0444/-r--r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2021-05-21 12:42:07.565311760 +0000 Modify: 2021-05-21 12:42:07.565311760 +0000 Change: 2021-05-21 12:42:07.565311760 +0000 Birth: -
unshare(1) / nsenter(1) / mount(8) を使って操作する
さて、前提知識の確認が終わったところで、実際に ip-netns(8) を使わずに Network Namespace を操作してみよう。 まずは、ip-netns(8) 以外のコマンドラインツールで操作する方法を試す。
新しく namespaces(7) を作るコマンドとしては unshare(1) が使える。
--net
オプションを指定すると、コマンドで新たに起動するプロセスが利用する Network Namespace を確保できる。
以下では新しい Network Namespace を使って bash(1) を起動している。
$ sudo unshare --net bash
起動したシェルで確認すると、たしかに /proc/<pid>/ns
以下のファイルの inode 番号が変わっていることが分かる。
# file /proc/self/ns/net /proc/self/ns/net: symbolic link to net:[4026532254]
ip-link(8) を使ってみるデバイスの状況を確認すると、DOWN したループバックデバイスしか無いことが分かる。 どうやら、ちゃんと Network Namespace が新しく作られたようだ。
# ip link show 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
ただ、この状況で ip-netns(8) を使ってみても何も表示されない。 新しく Network Namespace ができたというのに、どうしてだろう。
# ip netns list
というのも、実は ip-netns(8) の list サブコマンドは、/var/run/netns
以下にあるファイルを見ているだけに過ぎない。
上記で何も表示されないということは、ここに何もファイルがないということ。
# ls /var/run/netns
たしかに何も表示されない。 そもそも、ip-netns(8) を使ったことがない環境であれば、ディレクトリすらできていないことだろう。
ここでおもむろに /var/run/netns
以下にファイルを作って、/proc/self/ns/net
を --bind
オプションつきでマウントしてみよう。
# touch /var/run/netns/example # mount --bind /proc/self/ns/net /var/run/netns/example
すると、ip-netns(8) の list サブコマンドに、作ったファイルと同じ内容が見られる。
# ip netns list
example
上記は、ちゃんと ip-netns(8) から使うことができる。 一旦、unshare(1) で作ったシェルのプロセスから抜けて、ip-netns(8) の exec サブコマンドを実行してみよう。
# exit $ sudo ip netns exec example bash -c 'ls -alF /proc/self/ns/net' lrwxrwxrwx 1 root root 0 May 21 12:49 /proc/self/ns/net -> 'net:[4026532254]'
上記から、ちゃんと使えることがわかる。 というのも、これは実のところ ip-netns(8) が内部的にやっているのとほぼ同じことをやっているため。
先ほど /var/run/netns
以下に作ったファイルは nsenter(1) から利用することもできる。
このコマンドは既存の Namespace に切り替えるために用いる。
--net
オプションにファイルを指定して、シェルを起動してみよう。
$ sudo nsenter --net=/var/run/netns/example bash
起動したシェルから確認すると、ちゃんと Namespace が切り替わっていることがわかる。
# ls -alF /proc/self/ns/net lrwxrwxrwx 1 root root 0 May 21 12:53 /proc/self/ns/net -> 'net:[4026532254]'
ちなみに、ip-netns(8) から利用するときには mount(8) を使わなくてもシンボリックリンクを張るだけで代用できる。
次のように、$$
を使って自身の pid を置換しつつ、Namespace を表したファイルからシンボリックリンクを張ってみよう。
# ln -s /proc/$$/ns/net /var/run/netns/symlink
起動したシェルから抜けた上で確認すると、ちゃんと ip-netns(8) のリストに表示されると共に、使えることがわかる。
# exit $ ip netns list symlink example $ sudo ip netns exec example bash -c 'ls -alF /proc/self/ns/net' lrwxrwxrwx 1 root root 0 May 21 12:58 /proc/self/ns/net -> 'net:[4026532254]'
このテクニックは Docker や Mininet などが作る Network Namespace を ip-netns(8) から操作したいときにも有効。
unshare(2) / setns(2) / mount(2) を使って操作する
さて、ip-netns(8) 以外のコマンドラインツールから操作できることがわかったところで、続いてはシステムコールを使ってみる。 というか、先ほど使った一連のコマンドラインツールも、内部的にはこれらの API を叩いていた。
早速だけど、以下にサンプルコードを示す。 このサンプルコードでは、次のような処理をしている。
- unshare(2) で Network Namespace を新しく作る
- mount(2) で
/proc/self/ns/net
を/var/run/netns
以下にsyscall-example
という名前でマウントする /proc/self/ns/net
の中身を表示する
#define _GNU_SOURCE #include <sched.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mount.h> int main(int argc, char *argv[]) { if (unshare(CLONE_NEWNET) < 0) { fprintf(stderr, "Failed to create a new network namespace: %s\n", strerror(errno)); exit(-1); } const char *netns_path = "/var/run/netns/syscall-example"; const int fd = open(netns_path, O_RDONLY | O_CREAT | O_EXCL, 0); if (fd < 0) { fprintf(stderr, "Cannot create namespace file \"%s\": %s\n", netns_path, strerror(errno)); return EXIT_FAILURE; } close(fd); const char *proc_path = "/proc/self/ns/net"; if (mount(proc_path, netns_path, "none", MS_BIND, NULL) < 0) { fprintf(stderr, "Failed to bind %s -> %s: %s\n", proc_path, netns_path, strerror(errno)); } const char *cmd = "file"; char* const args[] = {"file", "/proc/self/ns/net", NULL}; if (execvp(cmd, args) < 0) { fprintf(stderr, "Failed to exec \"%s\": %s\n", cmd, strerror(errno)); exit(-1); } return EXIT_SUCCESS; }
上記に nsadd.c
という名前をつけてビルドする。
$ gcc -o nsadd.o nsadd.c
実行すると、/proc/self/ns/net
が新しい識別子になっていることがわかる。
$ sudo ./nsadd.o
/proc/self/ns/net: symbolic link to net:[4026532315]
ip-netns(8) からも、ちゃんと使える。
$ ip netns list syscall-example symlink example $ sudo ip netns exec syscall-example bash -c 'ls -alF /proc/self/ns/net' lrwxrwxrwx 1 root root 0 May 21 13:10 /proc/self/ns/net -> 'net:[4026532315]'
続いては、上記で作った Network Namespace を示すファイルを利用するサンプルコード。 次のような処理をしている。
/var/run/netns
以下のファイルを open(2) で開く- 上記で得られたファイルディスクリプタを setns(2) に渡して Namespace を切り替える
/proc/self/ns/net
の中身を表示する
#define _GNU_SOURCE #include <sched.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(int argc, char *argv[]) { const char *mounted_path = "/var/run/netns/syscall-example"; const int fd = open(mounted_path, O_RDONLY | O_CLOEXEC); if (fd < 0) { fprintf(stderr, "Cannot open mounted path\"%s\": %s\n", mounted_path, strerror(errno)); return EXIT_FAILURE; } if (setns(fd, CLONE_NEWNET) < 0) { fprintf(stderr, "failed to setup the network namespace \"%s\": %s\n", mounted_path, strerror(errno)); close(fd); return EXIT_FAILURE; } const char *cmd = "file"; char* const args[] = {"file", "/proc/self/ns/net", NULL}; if (execvp(cmd, args) < 0) { fprintf(stderr, "Failed to exec \"%s\": %s\n", cmd, strerror(errno)); exit(-1); } return EXIT_SUCCESS; }
上記に nsexec.c
という名前をつけてビルドする。
$ gcc -o nsexec.o nsexec.c
実行すると、ちゃんと Network Namespace が切り替わっていることがわかる。
$ sudo ./nsexec.o
/proc/self/ns/net: symbolic link to net:[4026532315]
いじょう。
参考
- 作者:もみじあめ
- 発売日: 2020/02/29
- メディア: Kindle版