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台のマシンの内部で、仮想的にネットワークを構築するところがポイントです。

ns diagram

有ると良い知識

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

サーバを建てる

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

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

netns 2018 12 20 14 52 23

ネットワークを構築する

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

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の標準的な機能だけで設定可能です。 気軽に使える機能なので、積極的に使っていきたいです。