Network Namespaceを使ってLinuxのルーティングテーブルを分離させる

# はじめに
わかった風な文章ですが、Network Namespaceやsystemd、PIDファイルの使い方など、色々と初めてのことなので、間違ってるかもしれません。ミスがあれば指摘いただけると幸いです。

# Linuxのルーティングテーブルについて
複数のNICを搭載しているLinuxでは、通常、すべてのNICが一つのルーティングテーブルに乗っています。

![netns_1.png](https://qiita-image-store.s3.amazonaws.com/0/157891/9bc2c86c-ca8c-c0e2-b7f6-67f6c1714a87.png)

Network Namespaceを使うことで、ネットワーク機器でいう所の**Virtual Router（VRF）**のように、一台のホスト内に複数のルーティングテーブルを定義し、NIC毎のルーティングテーブルを分離させる事ができます。

![netns_2.png](https://qiita-image-store.s3.amazonaws.com/0/157891/60e211ea-6635-a200-f154-90df1d4217e9.png)


## メリット
それぞれのNetwork Namespace上のトラフィックは完全に分離されます。（vethなど、内部でnamespace間を接続する技術を使わない限り。）

## 今回の目的
検証用ネットワークは、ルータやスイッチ、FWやLBといったネットワーク機器だけでなく、試験通信を行うために、送信元または送信先となるホストが必要です。（通信がPingやTracerouteなどで済む場合は、Network機器をそのままホストに使ってしまうこともありますが。）私の場合、それにLinuxを使ってます。

大雑把に書くと、次のような形になります。"Linux Host"と書かれているPCが、送信元や送信先となるホストです。（実際には２台とは限りませんし、仮想マシンだったりします。）

![netns_3.png](https://qiita-image-store.s3.amazonaws.com/0/157891/6cc49cfd-37fa-95e8-75af-c42d74d947e6.png)

このままではホストの操作が面倒なので、ホストにNICを増やして、管理接続用のセグメントを作って、そこからリモートで操作するようにします。また、ホストでは検証する内容に応じて色々なプログラムが必要になることがあるので、すぐにyum等でインストールができるようにインターネットにも繋がって欲しいです。

そこでこうなります。

![netns_4.png](https://qiita-image-store.s3.amazonaws.com/0/157891/1e3d26c5-2892-3a6a-e940-82aefd28276c.png)

こうなると、ホストには検証用ネットワーク以外に管理接続用ネットワーク（点線部分）が繋がってしまい、ホストのルーティングを考慮する必要が出てきて、場合によっては本来の目的である検証の品質が落ちてしまいます。また、検証に必要なネットワークアドレスが管理接続用と重複してしまうような場合は、管理接続用アドレスを変えなければいけない、ということも考えられます。これはかなり不便です。

そこで、ホストでNetwork Namespaceをつかって、次のようにネットワークを分離させることにしました。

![netns_5.png](https://qiita-image-store.s3.amazonaws.com/0/157891/ea32c380-39fc-6173-a8f1-e5727541cba3.png)

あまりわかりやすいイラストではない気もしますが、青い部分が検証用ネットワーク、赤い部分が管理接続用ネットワークです。このようにしておけば、検証用ネットワークと管理接続用ネットワークは完全に分離され（ルーティングテーブルが別々となる。アドレスが重複しても全く問題ない）、安全に、素早く検証が行えます。

# 構築する内容
 - Linux Hostに、二つのNetwork Namespaceを作成する
 - 検証用ネットワークのNamespace名を**default**、管理接続用ネットワークのNamespace名を**manage**とする
 - manageには、eth0 を割り当てる
 - 管理接続のため、eth0に対してssh接続ができるようにする
 - 設定は変更と削除が簡単に可能なように、スクリプトで動作できるようにする
 - ホストの起動時に自動的に実行するよう、systemdを使ってサービス化する

# 環境
　- CentOS7 (Kernel 3.10.0)
　- iproute2
　- sshd
　- dhclient (eth0のIPアドレスを設定する際に使用。静的アドレスの場合は不要）

# 参考情報
 - [Qiita:netnsを使ったホスト内ネットワーク構築
](https://qiita.com/_norin_/items/f2c1b0158ee34abcdd0c)
 - [Qiita:systemdでの自動起動設定
](https://qiita.com/shosan87/items/f65b43373b895edb2946)
 - [Qiita:bashでのPID取得方法まとめ($$、$PPID、$!、$BASHPID)](https://qiita.com/laikuaut/items/1daa06900ad045d119b4)
 - [めもめも：Systemd入門(4) - serviceタイプUnitの設定ファイル](http://enakai00.hatenablog.com/entry/20130917/1379374797)
 - [serverfault:How can I switch from a custom linux network namespace back to the default one?](https://serverfault.com/questions/622813/how-can-i-switch-from-a-custom-linux-network-namespace-back-to-the-default-one)
 - [stackexchange:how to find out namespace of a particular process?](https://unix.stackexchange.com/questions/113530/how-to-find-out-namespace-of-a-particular-process)
 - [man 8 ip-netns](http://man7.org/linux/man-pages/man8/ip-netns.8.html)

#　設定方法

## Network Namespace　作成・削除スクリプト
Namespaceを作成・削除するのに、次のようなスクリプトを作りました。

```bash:/usr/local/sbin/management_ns.sh
#!/bin/bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin

DEFAULT_NS=default
MANAGE_NS=manage

MANAGE_IF=eth0

DHCP_CMD=dhclient
DHCP_PID_FILE="/var/run/dhclient-$MANAGE_NS-$MANAGE_IF.pid"
DHCP_CMD_ARG="-pf $DHCP_PID_FILE"

SSHD_CMD=`which sshd`
SSHD_CONF="/etc/ssh/sshd_config"
SSHD_PID_FILE="/var/run/sshd-$MANAGE_NS-$MANAGE_IF.pid"
SSHD_OPTS="PidFile $SSHD_PID_FILE"

case $1 in
	"start")
		echo "create management namespace settings"
		ip netns add $MANAGE_NS
		ip link set $MANAGE_IF netns $MANAGE_NS up
		ip netns exec $MANAGE_NS $DHCP_CMD $DHCP_CMD_ARG
		ip netns exec $MANAGE_NS $SSHD_CMD -f $SSHD_CONF -o "$SSHD_OPTS"
		ln -s /proc/1/ns/net /var/run/netns/$DEFAULT_NS
		;;
	"stop")
		echo "delete management namespace settings"
		ip netns delete $MANAGE_NS
		kill -9 `cat $DHCP_PID_FILE`
		kill -9 `cat $SSHD_PID_FILE`
		rm $DHCP_PID_FILE
		rm $SSHD_PID_FILE
		rm /var/run/netns/default
		;;
	"status")
		echo "show management namespace settings"
		echo "-----[show netnses]-----"
		ip netns
		echo ""
		echo "-----[show manage namespace ip link]-----"
		ip netns exec $MANAGE_NS ip link
		echo ""
		echo "-----[show manage namespace ip addr]-----"
		ip netns exec $MANAGE_NS ip addr
		echo ""
		echo "-----[show manage namespace ip route]-----"
		ip netns exec $MANAGE_NS ip route
		echo ""
		echo "-----[show dhcpclient pid]-----"
		cat $DHCP_PID_FILE
		echo ""
		echo "-----[show sshd pid]-----"
		cat $SSHD_PID_FILE
		;;
esac
```

作成したら、実行権限を付与しておきます。

```bash
$ chmod u+x /usr/local/sbin/management_ns.sh
```

### スクリプトの使い方
- スクリプトの引数に"start"とつけると、Network Namespaceの作成を開始します。
    - "manage" Namespaceを作成し、eth0がそちらへ分離された後、dhclientで、eth0に設定するIPを待ち受けます。
    - 次に "manage" Namespace上で、sshdを起動します。これでSSHで接続することができます。
- スクリプトの引数に"stop"とつけると、作成したNamespaceを破棄し、起動したプロセス（dhcpclient, sshd)を終了させます。
- スクリプトの引数に"status"とつけると、このスクリプトで作成した各種の設定が表示されます。

## systemdにサービスとして登録
上記のスクリプトを、systemdで実行できるようにします。まず、以下のファイルを"/etc/systemd/system/management_ns.service" として作成します。

```text:/etc/systemd/system/management_ns.service
[Unit]
Description = create management network namespace.
After=networking.service.target

[Service]
ExecStart=/usr/local/sbin/management_ns.sh start
ExecStop=/usr/local/sbin/management_ns.sh stop
Type=Simple
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
```

次に、systemdの再読み込みをします。

```bash
$ systemctl daemon-reload
```

これで、　作成したserviceファイルがsystemdに読み込まれているはずなので、確認します。

```bash
$ systemctl list-unit-files | grep management_ns
management_ns.service                            disabled
```

ホストの起動時に自動実行させるように登録します。

```bash
$ systemctl enable management_ns.service

Created symlink from /etc/systemd/system/multi-user.target.wants/management_ns.service to /etc/systemd/system/management_ns.service.
```

即時実行する場合は、

```bash
$ systemctl start management_ns.service
```

実行状態で、元々のスクリプトに"status"引数をつけると、次のような感じで表示されます。

```bash
$ management_ns.sh status
show management namespace settings
-----[show netnses]-----
default
manage

-----[show manage namespace ip link]-----
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff

-----[show manage namespace ip addr]-----
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether xx:xx:xx:xx:xx:cf brd ff:ff:ff:ff:ff:ff
    inet 192.168.xxx.xx/24 brd 192.168.xxx.255 scope global dynamic eth0
       valid_lft 6501sec preferred_lft 6501sec

-----[show manage namespace ip route]-----
default via 192.168.xxx.xxx dev eth0 
192.168.xxx.0/24 dev eth0 proto kernel scope link src 192.168.xxx.xx 

-----[show dhcpclient pid]-----
1985

-----[show sshd pid]-----
1987

```

停止（Network Namespaceを削除し、元に戻す）場合は、systemctl stopを実行します。

```bash
$ systemctl stop management_ns.service
```

停止状態でstatusを確認すると、このような感じになります。

```bash
$ management_ns.sh status
show management namespace settings
-----[show netnses]-----

-----[show manage namespace ip link]-----
Cannot open network namespace "manage": No such file or directory

-----[show manage namespace ip addr]-----
Cannot open network namespace "manage": No such file or directory

-----[show manage namespace ip route]-----
Cannot open network namespace "manage": No such file or directory

-----[show dhcpclient pid]-----
cat: /var/run/dhclient-manage-eth0.pid: そのようなファイルやディレクトリはありません

-----[show sshd pid]-----
cat: /var/run/sshd-manage-eth0.pid: そのようなファイルやディレクトリはありません
```

# 解説
## Network Namespaceについて
Network Namespaceとは、プロセスが属するNetworkの名前空間です。個々のプロセスは必ずどこかのNetwork Namespaceに属しています。**/proc/(PID)/ns/net**で指すentryが、個々のプロセスの属するNetwork Namespaceを指しています。

```bash
$ ls -l /proc/1027/ns/net 
lrwxrwxrwx. 1 root root 0  3月 29 23:21 /proc/1027/ns/net -> net:[4026532166]
```

しかし、これを見ても具体的に何というNetwork Namespceを指してるのかはわかりません。各プロセスがどのNetwork Namespaceに属しているかを調べたい場合、次のコマンドで行えます。

**ip netns idenfity PID**

```bash
# ip netns idenfity の実行例

$ ip netns identify 1027
manage　

# PID 1027 のプロセスは　manage　というNetwork Namespaceに属していることがわかる
```


## Network Namespaceの継承
新しく生成されたプロセスのNamespaceは、基本的に、親プロセスと同じものを継承します。　LinuxではPID 1（initかsystemd）が全ての元となるプロセスなので、他のプロセスは、基本的にはこのプロセスのNetwork Namespaceを継承する形となります。属するNetwork Namespaceを継承せず、指定したい場合、次のコマンドで行えます。

**ip netns exec Namespace名 command**

```bash
# ip netns exec の実行例
# Network Namespace "default"で、"ping 1.1.10.1" というコマンドを実行する

$ ip netns exec default ping 1.1.10.1
PING 1.1.10.1 (1.1.10.1) 56(84) bytes of data.
64 bytes from 1.1.10.1: icmp_seq=1 ttl=255 time=1.31 ms
64 bytes from 1.1.10.1: icmp_seq=2 ttl=255 time=1.36 ms

```

## Network Namespaceの名前
新しいNetwork Namespaceを作成する際は、その名前をつける必要があります。それは良いのですが、実は、最初から存在するNetwork Namespaceには、名前が付いてません。

```bash
# 起動後にPID 1の所属するnamespace名を調べた結果

$ ip netns identify 1
$ 

# (何も出力されない）

```

通常、Network Namespace名は**/var/run/netns/**配下で管理されるのですが、デフォルトのNetwork Namespaceしかない場合、このディレクトリは空です。

例えば今回の場合、**manage**というNetwork Namespaceを作成して、その上でsshdを起動しています。その結果、sshによるログイン直後は、manage 上にいることになりますが、操作したい対象はデフォルトのNetwork Namespaceです。しかし、操作しようにも名前がなければ操作ができません。

良い方法が無いか調べたところ、こちらに良い方法が紹介されてました。

[serverfault:How can I switch from a custom linux network namespace back to the default one?](https://serverfault.com/questions/622813/how-can-i-switch-from-a-custom-linux-network-namespace-back-to-the-default-one)

> I found that you can return to the default network namespace with two simple commands:

> ```
  ln -s /proc/1/ns/net /var/run/netns/default
> ```

なるほどなと。これだけで元のNamespaceに**default**という名前をつけられるわけですね。

それにしても、この参考にした記事、やりたいことが全く同じでした。やっぱり誰もが考えるんだろうなぁ。

> My scenario is:
> 
> I want to run a SSH server in a separate network namespace (to keep the rest of the system unaware of the network connection, as the system is used for network testing), but want to be able to execute programs in the default network namespace via the SSH connection.


## SSHログイン直後のNetwork Namespace
すでに書いた通り、このスクリプトを使ったホストのeth0のIPアドレスにSSHでログインした場合、ログイン直後は、**manage**というNetwork Namespace内にいる形になります。これは、sshdを**manage**上で実行しており、そのsshdの子プロセスとしてbashが実行されるためです。

```bash
# eth0にssh接続した後に、pstreeで実行中のbashのツリーを見た結果

systemd(1)─┬─agetty(650)
         (省略)
           ├─sshd(2189)───sshd(2191)───bash(2193)───pstree(2241)
```

sshd(2189)が、manageにある場合、その子孫のプロセスとなる、ssh(2191), bash(2193), pstree(2241)もmanageに属することになります。この状態で、例えばyumコマンドを実行した場合、そのyumコマンドのプロセスも、manageに属することになります。結果、manage側の通信経路でインターネットへ繋がり、パッケージをダウンロードすることができます。

## Network Namespaceの移動
任意のNetwork Namespaceでプログラムを実行するには **ip netns exec namespace名 command**　という形で可能ですが、毎回このようなコマンドを実行するのは面倒です。

**ip netns exec namespace名 /bin/bash**
とすることで、指定のNetowrk Namespaceでbashが実行され、そのbashによるプロンプト（コマンド待ち受け状態）になります。以降のコマンドは全て、このbashの子プロセスとして実行されるため、移動先のNetwork Namespaceでコマンドを実行していることになります。

## プロンプトに現在のNetwork Namespcae名を表示する方法

前述の方法でNetwork Namespaceを移動していると、自分が今どのNamespaceにいるか分からなくなることがあります。その場合は、~/.bashrc に以下のような内容を追記して、プロンプトにNamespace名を表示させると便利です。

```bash:.barhrcに追記
# If bash execute in specific network-namespace, 
# then add namespace name in prompt.

NETNS=`ip netns identify $BASHPID`
if [ $NETNS ]; then
	PS1="[\u@\h \W]($NETNS)\\$ "
else
	PS1="[\u@\h \W]\\$ "
fi
```

bashでは、シェル変数 **BASHPID**で、自身のPIDを取得することができます。なので、**ip netns identify $BASHPID**というコマンドで、現在のNetwork Namespaceを取得できます。

このPS1の設定値はCentOS 7を元にしてるので、他のディストリビューションの方がそのまま使うと、プロンプトが大きく変わって胃しまうかもしれないので、気をつけてください。

こんな感じになります。

```bash

[user1@cent01 ~](manage)$　（manage にいる）
[user1@cent01 ~](manage)$ ip netns exec default /bin/bash (default に移動）
[user1@cent01 ~](default)$ (default にいる）

```

# その他
systemdのサービスのフォーマットとか、sshdの実行時はフルパスでないといけない事とか、-oオプションでコンフィグを上書きする方法とか、これで勉強になった内容は多いのですが、このページはここまでにします。