LinuxのNetns/veth/Bridge/NATで仮想ネットワーク構築

この記事は OIC ITCreate Club Advent Calendar 2018 15 日目が空いてたので、そのつもりで書いている記事です。 https://adventar.org/calendars/3072

Linux には、以下のような機能が標準で実装されており、簡単に使用可能です。 Cisco のような機器には性能が及ばないですが、一通りの基本的な名ネットワークはもちろん、更に高度なネットワークも気軽に組むことが出来ます。 これらの機能を組み合わせて Docker 等のコンテナエンジンのネットワーク技術は構築されています。

  • iptables
    • ファイアウォール・NAT(SNAT/DNAT/Masquerade)等の機能を提供
    • 主に、IP と TCP/UDP 等のプロトコルに対して設定が書ける
    • iptables コマンドで操作が可能
  • Bridge
    • L2 なブリッジインターフェースを作成する機能
    • 複数のネットワークインターフェースを、仮想的に L2 スイッチに繋げたような動作を行う
    • brctl コマンドで操作が可能
  • Network Namespace
    • ルーティングテーブル・インターフェース等が分離された環境を複数作るための機構
    • VRF のような機能を提供する
    • ip netns サブコマンドで操作が可能
  • veth
    • 自由に作成できる仮想的なインターフェースで、ネームスペース間やホストマシン等への接続が可能
    • 作成すると、互いに接続されたインターフェース 2 つが生成される
    • ip link add ip link set サブコマンドで操作が可能

この記事では、上の技術を組み合わせて、1 台のマシンの中でネットワークを仮想的に構築する手順を説明します。

完成図

Namespace を 2 つ作り、Bridge 経由でそれぞれの Namespace・ホストマシンのインターフェイスと疎通が取れるようにします。そして、Namespace の内部から、ホストマシンの iptables を経由して NAT を通した外部との通信も行えるようにします。

1 台のマシンの内部で、仮想的にネットワークを構築するところがポイントです。

有ると良い知識

  • SSH
  • Linux の基礎的なコマンド操作
  • IP/Static Route
  • Bridge

サーバを建てる

今回は さくらのクラウド を使用して一番安いプランで雑にサーバを立てました。 SSH 経由で操作を行います。

  • CPU: 1
  • RAM: 1GB
  • OS: Ubuntu 16.04
  • Kernel: 4.4.0-116

ネットワークを構築する

依存パッケージのインストール

Bridge を操作する brctl コマンドを使用するため、パッケージのインストールを行います。

ubuntu@namespace:~$ sudo -s
[sudo] password for ubuntu:
root@namespace:~# apt install bridge-utils

Namespace の作成

コンテナに見立てた host1 host2 2 つの Namespace を作成します。 最後に Namespace が作成できているかを確認します。

root@namespace:~# ip netns add host1
root@namespace:~# ip netns add host2
root@namespace:~# ip netns ls
host2
host1

仮想リンクの作成

仮想的な LAN ケーブル・ポートを表す veth を作成します。 作成すると、互いに仮想的に LAN ケーブルで接続されたインターフェース 2 つが生成される感じです。

リンクを作る前に、初期状態を確認しておきます。

root@namespace:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group 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 group default qlen 1000
    link/ether 9c:a3:ba:30:9e:18 brd ff:ff:ff:ff:ff:ff

ip コマンドで veth を作る場合、2 回登場する name の後にインターフェイスの名前を指定します。 どちらが先でも構いません。

root@namespace:~# ip link add name veth1 type veth peer name veth1-br
root@namespace:~# ip link add name veth1-h1 type veth peer name veth2-br
root@namespace:~# ip link add name veth1-h2 type veth peer name veth3-br

作成が終わったので、確認します。 3 個の veth を作成し、それぞれ 2 つの IF を持つので、全部で 6 つのリンクが登場します。

root@namespace:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group 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 group default qlen 1000
    link/ether 9c:a3:ba:30:9e:18 brd ff:ff:ff:ff:ff:ff
20: veth1-br@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether fa:e7:d1:31:fc:9b brd ff:ff:ff:ff:ff:ff
21: veth1@veth1-br: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 32:41:0e:b8:25:7c brd ff:ff:ff:ff:ff:ff
22: veth2-br@veth1-h1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 32:e1:04:80:40:d1 brd ff:ff:ff:ff:ff:ff
23: veth1-h1@veth2-br: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 52:71:2c:36:be:d4 brd ff:ff:ff:ff:ff:ff
24: veth3-br@veth1-h2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether d2:bf:05:28:7a:bf brd ff:ff:ff:ff:ff:ff
25: veth1-h2@veth3-br: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether f6:2b:d9:be:0e:f5 brd ff:ff:ff:ff:ff:ff

仮想リンクと Namespace を結びつける

作成した仮想リンクは、以下のように Namespace と紐付けることが必要です。

  • Veth の veth1-h1 は Namespace の host1
  • Veth の veth1-h2 は Namespace の host2

実行後に link 一覧を確認すると、Namespace と紐づけた 2 つの IF が無くなっていることが分かります。

root@namespace:~# ip link set veth1-h1 netns host1
root@namespace:~# ip link set veth1-h2 netns host2
root@namespace:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group 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 group default qlen 1000
    link/ether 9c:a3:ba:30:9e:18 brd ff:ff:ff:ff:ff:ff
20: veth1-br@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether fa:e7:d1:31:fc:9b brd ff:ff:ff:ff:ff:ff
21: veth1@veth1-br: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 32:41:0e:b8:25:7c brd ff:ff:ff:ff:ff:ff
22: veth2-br@if23: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 32:e1:04:80:40:d1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
24: veth3-br@if25: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether d2:bf:05:28:7a:bf brd ff:ff:ff:ff:ff:ff link-netnsid 1

無くなった IF はどこに行ったかと言うと、 host1 host2 Namespace 配下に移動しました。 ip netns exec [Namespace名] サブコマンドの後に任意のコマンドを記述すると、その Namespace の内部から実行される機能が有ります。これを使用してリンクの一覧を表示します。すると、先程移動したリンクが出てきます。

root@namespace:~# ip netns exec host1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
23: veth1-h1@if22: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 52:71:2c:36:be:d4 brd ff:ff:ff:ff:ff:ff link-netnsid 0

Bridge の作成・接続

Linux 上で動作する、L2 スイッチである bridge を作成します。 先程導入したパッケージにbrctlが含まれているので、コマンドを実行して作成します。

作成すると、それぞれのインターフェイスが接続されていることが分かると思います。

root@namespace:~# brctl addbr br0
root@namespace:~# brctl addif br0 veth1-br
root@namespace:~# brctl addif br0 veth2-br
root@namespace:~# brctl addif br0 veth3-br
root@namespace:~#
root@namespace:~# ip link show dev br0
26: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 32:e1:04:80:40:d1 brd ff:ff:ff:ff:ff:ff
root@namespace:~# brctl show br0
bridge name     bridge id               STP enabled     interfaces
br0             8000.32e1048040d1       no              veth1-br
                                                        veth2-br
                                                        veth3-br

IF へアドレスの付与

仮想インターフェイスにまだ IP アドレスが振られていないので、通信を行うことが出来ないです。ホストマシンで動作する veth1 ・Namespace 内で動作する veth1-h1 veth-h2 に対して、順に IP アドレスを付与しようと思います。

root@namespace:~# ip addr add 10.0.0.254/24 dev veth1
root@namespace:~# ip netns exec host1 ip addr add 10.0.0.1/24 dev veth1-h1
root@namespace:~# ip netns exec host2 ip addr add 10.0.0.2/24 dev veth1-h2
root@namespace:~#
root@namespace:~# ip netns exec host1 ip addr show dev veth1-h1
23: veth1-h1@if22: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 52:71:2c:36:be:d4 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.1/24 scope global veth1-h1
       valid_lft forever preferred_lft forever

IF の起動

仮想インターフェイス・ブリッジ同士の結線が完了しました。 ですが、ip linkから確認すると、全てステータスがDOWNとなっています。 1 つ 1 つ起動させていきます。

root@namespace:~# ip link set br0 up
root@namespace:~# ip link set veth1 up
root@namespace:~# ip link set veth1-br up
root@namespace:~# ip link set veth2-br up
root@namespace:~# ip link set veth3-br up
root@namespace:~# ip netns exec host1 ip link set veth1-h1 up
root@namespace:~# ip netns exec host2 ip link set veth1-h2 up
root@namespace:~#
root@namespace:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group 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 group default qlen 1000
    link/ether 9c:a3:ba:30:9e:18 brd ff:ff:ff:ff:ff:ff
20: veth1-br@veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP mode DEFAULT group default qlen 1000
    link/ether fa:e7:d1:31:fc:9b brd ff:ff:ff:ff:ff:ff
21: veth1@veth1-br: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:41:0e:b8:25:7c brd ff:ff:ff:ff:ff:ff
22: veth2-br@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP mode DEFAULT group default qlen 1000
    link/ether 32:e1:04:80:40:d1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
24: veth3-br@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP mode DEFAULT group default qlen 1000
    link/ether d2:bf:05:28:7a:bf brd ff:ff:ff:ff:ff:ff link-netnsid 1
26: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:e1:04:80:40:d1 brd ff:ff:ff:ff:ff:ff

内部の疎通確認

この状態で、ホストマシンに接続されている veth1 と、Namespace に接続されている veth1-h1 veth1-h2 は、同一の L2 スイッチ(Bridge)に接続され、同一のネットワークを持つ IP アドレスを付与しています。ですので、既に疎通が取れるはずです。確かめてみましょう。

root@namespace:~# ping -c 1 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.092 ms

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


root@namespace:~# ip netns exec host1 ping -c 1 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.069 ms

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


root@namespace:~# ip netns exec host2 ping -c 1 10.0.0.254
PING 10.0.0.254 (10.0.0.254) 56(84) bytes of data.
64 bytes from 10.0.0.254: icmp_seq=1 ttl=64 time=0.067 ms

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

それぞれが疎通を取ることが出来ました。

ここで、外部の IP アドレスに向けて疎通確認を行うと、リーチしないと怒られます。 Namespace 内のルーティングテーブルでは、標準でデフォルトルートが指定されていません。なので、10.0.0.0/24のネットワーク以外の行き先は現時点で不明なのでunreachableと表示されています。

root@namespace:~# ip netns exec host1 ping -c 1 8.8.8.8
connect: Network is unreachable

NAT の設定を行う

ホストマシン(10.0.0.254)で動作する iptables で NAT 機能を有効にして、Namespace 内からの通信を IP マスカレードで外に出れるように設定しましょう。

カーネル設定による IP フォワーディングの有効化・NAT テーブルの初期化・設定の追加・確認と順に行っていきます。

root@namespace:~# echo 1 > /proc/sys/net/ipv4/ip_forward
root@namespace:~# iptables --table nat --flush
root@namespace:~# iptables --table nat --append POSTROUTING --source 10.0.0.0/24 --jump MASQUERADE
root@namespace:~# iptables --table nat --list
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  10.0.0.0/24          anywhere

ルーティングの追加

上で設定したホストマシン(10.0.0.254)に対して、デフォルトルートを向けます。 ip コマンドを使用して、StaticRoute として指定しています。

root@namespace:~# ip netns exec host1 ip route add default via 10.0.0.254
root@namespace:~# ip netns exec host2 ip route add default via 10.0.0.254
root@namespace:~# ip netns exec host1 ip route
default via 10.0.0.254 dev veth1-h1
10.0.0.0/24 dev veth1-h1  proto kernel  scope link  src 10.0.0.1

外部との疎通の確認

すべての手順が完了したので、Namespace 内から疎通が取れるかを確認します。

root@namespace:~# ip netns exec host1 ping -c 1 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=120 time=16.6 ms

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


root@namespace:~# ip netns exec host2 ping -c 1 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=120 time=16.4 ms

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

まとめ

ルーターで設定するような項目は、主に Linux の標準的な機能だけで設定可能です。 気軽に使える機能なので、積極的に使っていきたいです。