手を動かして学ぶネットワーク実験環境入門

これは、「フィヨルドブートキャンプ Part 2 Advent Calendar 2021」25日目の記事です。

adventar.org

adventar.org

はじめに

今年は、仮想環境のコンテナ周りを学習していましたが、仮想環境のコンテナを隔離する技術のNamespaceについて知ることができました。 そのNemespaceのうちの一つにNetwork Namespaceというコンテナのネットワーク環境を隔離するLinuxカーネルの機能があります。

ある日、このNetwork Namespaceを使ってTCP/IPのネットワークを手を動かして学ぶ方法が書いてある Linuxで手を動かして学ぶTCP/IPネットワーク入門 という 書籍を読んで実際にネットワークを作ってみました。すると、書籍を読んで理解した気になるのとは違って、実際に手を動かすことで、 ネットワークのパケットやルーターの動きなどをより深く理解できるようなりました。また、ネットワーク関連のコマンドやiptablesコマンドなどのLinuxコマンドにも 触ることになったので、Linuxを慣れ親しむ上でも有益になりましたし、ネットワーク技術を今後学ぶ上での知見(ヒント)を多少得ることができました。

f:id:mh_mobile:20211225111826j:plain

www.amazon.co.jp

この書籍では、ルーターやブリッジ、Source NATやDestination NATなどのネットワーク技術について、Network Namespaceを使って仮想ネットワークを作ることを通して、手を動かしながら学ぶことができます。そこで、これらの個別の要素を組み合わせることで、Dockerのデフォルトのbridgeネットワークのような仮想ネットワークを作る方法について共有したいと思います。

自作するネットワーク環境の構成

Linux VM上のDockerをインストールすると、decker0という名称の仮想ブリッジが作成されます。 また、この時、複数のDockerコンテナを起動した場合、コンテナの仮想インターフェースがLinux VM上の仮想ブリッジに接続されます。

例えば、3つコンテナを起動してみます。

$ docker run -it -d --name c1 nicolaka/netshoot
$ docker run -it -d --name c2 nicolaka/netshoot
$ docker run -it -d --name c3 nicolaka/netshoot

そうすると、docker0のブリッジ以外に vethc881c6avethaaf049aveth6a8c5f9といった仮想インターフェースを確認できます。

$  ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:87:21:22 brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.24/24 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 63744sec preferred_lft 63744sec
    inet6 fda6:a15e:b806:8445:5054:ff:fe87:2122/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591952sec preferred_lft 604752sec
    inet6 fe80::5054:ff:fe87:2122/64 scope link
       valid_lft forever preferred_lft forever
35: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:e7:ab:b9:da brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:e7ff:feab:b9da/64 scope link
       valid_lft forever preferred_lft forever
81: vethc881c6a@if80: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 0e:48:32:47:5b:ab brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::c48:32ff:fe47:5bab/64 scope link
       valid_lft forever preferred_lft forever
83: vethaaf049a@if82: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 2a:62:d0:50:13:ba brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::2862:d0ff:fe50:13ba/64 scope link
       valid_lft forever preferred_lft forever
85: veth6a8c5f9@if84: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether ca:a2:94:e1:5c:c6 brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::c8a2:94ff:fee1:5cc6/64 scope link
       valid_lft forever preferred_lft forever

docker0の仮想ブリッジの設定を確認すると、これらの仮想インターフェースがブリッジに接続されているのが確認できます。

$ brctl show docker0
bridge name bridge id       STP enabled interfaces
docker0     8000.0242e7abb9da   no      veth6a8c5f9
                            vethaaf049a
                            vethc881c6a

また、コンテナの仮想インターフェースのIPアドレスは、以下のコマンドで、172.17.0.2172.17.0.3172.17.0.4が割り当てられています。

$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' c1
172.17.0.2
$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' c2
172.17.0.3
$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' c3
172.17.0.4

このネットワーク環境の構成をネットワーク図にすると、以下のような感じになると思います。 172.17.0.0/16 がコンテナのセグメントで、192.168.64.0/24がおそらくMacのホストとLinux VM間のセグメントになるかと思います。 このネットワーク図のrouterのホストに該当するのは、Linux VMで、wanに該当するのがMacのホストを想定しています。

ここで、Multipassを使って、別のLinux VMを起動すると、192.168.64.0/24のセグメント内のIPアドレス(例えば、192.168.64.21/24)がLinux VMの インターフェースに割り当てられます。これらのLinux VMもまた、bridge100という名称の仮想ブリッジに接続されているのでしょう。

f:id:mh_mobile:20211225195026p:plain

前置が長くなりましたが、上記のネットワーク図のような構成をNetwork Namespaceを使ってLinux VM上に構築して、ネットワークの学習をしたいというのが 本記事の目的になります。

実際に作成するネットワーク構成は以下になります。

f:id:mh_mobile:20211225195042p:plain

br0docker0のような仮想ブリッジで、c1c2c3がDockerコンテナのように仮想ブリッジに接続するホスト(Network Namespace)になります。 これらのc1c2c3のホストからwanのホストに通信するためには、router のホストでiptablesを使ってSource NAT(IPマスカレード)を定義し、 プライベートアドレスからグローバルアドレスにIPアドレスの変換を行います。

f:id:mh_mobile:20211225205629p:plain

参考までに、Dockerでは、iptablesのnatテーブルに 172.17.0.0/16のSource NATが定義されています。

sudo iptables  -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !localhost/8          ADDRTYPE match dst-type LOCAL

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

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

また、wan側のホストからc2のホストに対して通信するために、router のホストでiptablesを使ってDestination NATを定義します。

f:id:mh_mobile:20211225205834p:plain

同様に、Dockerで -p 80:8080のようにポートを指定して起動した場合は、ホストの80ポートをc2のホストの80ポートに転送する Destination NATが定義されます。

$  docker run -it -d -p 80:80 --name c2 nicolaka/netshoot
$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !localhost/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16        anywhere
MASQUERADE  tcp  --  172.17.0.3           172.17.0.3           tcp dpt:http-alt

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere
DNAT       tcp  --  anywhere             anywhere             tcp dpt:http to:172.17.0.3:8080

仮想環境(Linux VM)の準備

MacLinux VM上でネットワーク環境の構築を行います。 Mac上でLinux VMを動かす方法としては、VirtualBoxを使った方法が有名だと思いますが、 Intel MacmacOS Montereyで動作しなかったり、M1 Macに対応していないため、別の方法を 使うのが良いかと思います。

手軽にLinux VMを動作させる方法には、主にMultipass と Limaの2つがあります。今回は、私がよく使うMultipass を使った方法について書きます。

multipass.run

github.com

ちなみに、Mac上のLinux VMについては、以下の記事も参考になるかと思います。

tech.mirrativ.stream

Multipassを使った仮想環境の準備

HomeBrewでインストール

$ brew install --cask multipass

UbuntuVMの起動

$ multipass launch 20.04 --name tcpip --mem 5GB --disk 50GB
Launched: tcpip

*1

UbuntuVMにログイン

$ multipass shell tcpip
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-91-generic aarch64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Dec 25 13:45:11 JST 2021

  System load:             0.22
  Usage of /:              2.6% of 48.29GB
  Memory usage:            3%
  Swap usage:              0%
  Processes:               110
  Users logged in:         0
  IPv4 address for enp0s1: 192.168.64.24
  IPv6 address for enp0s1: fda6:a15e:b806:8445:5054:ff:fe87:2122


0 updates can be applied immediately.

IPフォワーディングの有効化

$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

Network Namespaceを使ったネットワーク環境の構築

Dockerのインストール

Dockerコンテナを活用するため、パッケージマネージャーのapt経由でDockerをインストールします。

$ sudo apt update
$ sudo apt install docker.io

Docker用のパッケージインストール後に、sudo なしでdockerコマンドを実行できるように、ユーザーをdockerグループ に追加します。

$ sudo gpasswd -a $USER docker

追加後は、一旦設定を有効化するために、multipassのLinux VMからログアウトし、再ログインしてください。

スクリプトの構築手順

Network Namespaceの作成

前述した各ホストのNetwork Namespaceを作成します。

$ sudo ip netns add ns1
$ sudo ip netns add ns2
$ sudo ip netns add ns3
$ sudo ip netns add router
$ sudo ip netns add wan

仮想インターフェースの作成

ペアの仮想インターフェースを作成します。 例えば、ns1の仮想インターフェースns1-veth0ns1-veth0とペアになる仮想ブリッジbr0に接続する仮想インターフェースns1-brが 作成されます。

$ sudo ip link add ns1-veth0 type veth peer name ns1-br0
$ sudo ip link add ns2-veth0 type veth peer name ns2-br0
$ sudo ip link add ns3-veth0 type veth peer name ns3-br0
$ sudo ip link add gw-veth1 type veth peer name wan-veth0

仮想インターフェースをNetwork Namespaceに割り当て

作成したペアの仮想インターフェースを各ホストのNetwork Namespaceに設定します。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set ns2-veth0 netns ns2
$ sudo ip link set ns3-veth0 netns ns3
$ sudo ip link set ns1-br0 netns router
$ sudo ip link set ns2-br0 netns router
$ sudo ip link set ns3-br0 netns router
$ sudo ip link set gw-veth1 netns router
$ sudo ip link set wan-veth0 netns wan

ブリッジの作成

routerのNetwork Namespace上に仮想ブリッジbr0を作成します。 また、仮想ブリッジに、ns1ns2ns3のペアとなる仮想インターフェースを割り当てます。

$ sudo ip netns exec router ip link add dev br0 type bridge
$ sudo ip netns exec router ip link set ns1-br0 master br0
$ sudo ip netns exec router ip link set ns2-br0 master br0
$ sudo ip netns exec router ip link set ns3-br0 master br0

仮想インターフェースの有効化

デフォルトで仮想インターフェースのステータスがDOWNになっているため、ステータスをUPに変更します。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec ns1 ip link set lo up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up
$ sudo ip netns exec ns2 ip link set lo up
$ sudo ip netns exec ns3 ip link set ns3-veth0 up
$ sudo ip netns exec ns3 ip link set lo up
$ sudo ip netns exec router ip link set ns1-br0 up
$ sudo ip netns exec router ip link set ns2-br0 up
$ sudo ip netns exec router ip link set ns3-br0 up
$ sudo ip netns exec router ip link set br0 up
$ sudo ip netns exec router ip link set lo up
$ sudo ip netns exec router ip link set gw-veth1 up
$ sudo ip netns exec wan ip link set wan-veth0 up
$ sudo ip netns exec wan ip link set lo up

仮想インターフェースにIPアドレスやルーティングの割り当て

各ホストのNetwork Namespaceの仮想インターフェースに対して、アドレスの設定を行います。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec ns1 ip route add default via 192.0.2.254
$ sudo ip netns exec ns1 ip link set ns1-veth0 address 00:00:5E:00:53:01

$ sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0
$ sudo ip netns exec ns2 ip route add default via 192.0.2.254
$ sudo ip netns exec ns2 ip link set ns2-veth0 address 00:00:5E:00:53:02

$ sudo ip netns exec ns3 ip address add 192.0.2.3/24 dev ns3-veth0
$ sudo ip netns exec ns3 ip route add default via 192.0.2.254
$ sudo ip netns exec ns3 ip link set ns3-veth0 address 00:00:5E:00:53:03

$ sudo ip netns exec router ip address add 192.0.2.254/24 dev br0
$ sudo ip netns exec router ip address add 203.0.113.254/24 dev gw-veth1

$ sudo ip netns exec wan ip address add 203.0.113.1/24 dev wan-veth0
$ sudo ip netns exec wan ip route add default via 203.0.113.254

ルーターのIPフォワーディングの有効化

パケットの転送を行うために、routerとなるNetwork Namespace上で、net.ipv4.ip_forwardの カーネルパラメータを1に設定します。

$ sudo ip netns exec router sysctl net.ipv4.ip_forward=1

ルーターループバックアドレスの転送設定の有効化

routerのNetwork Namespaceからlocalhost経由でホストに転送するために使用します。

$ sudo ip netns exec router sysctl net.ipv4.conf.all.route_localnet=1

ただ、正直この設定がよく理解できていません。

*2

route_localnet - BOOLEAN Do not consider loopback addresses as martian source or destination while routing. This enables the use of 127/8 for local routing purposes. default FALSE

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

iptablesのSource NAT(IPマスカレード)の設定

routerのホスト側からwan側のネットワークにパケットを転送するために、Source NATを使って、 プライベートアドレスからグローバルアドレスにIPアドレスの変換を行います。

$ sudo ip netns exec router iptables -t nat  \
      -A POSTROUTING \
      -s 192.0.2.0/24 \
      -o gw-veth1 \
      -j MASQUERADE

iptablesDestination NATの設定

routerのwan側やlocalhostから内部のホストns2の80ポートにパケットを転送するために、Destination NATを使って IPアドレスの変換を行います。

$ sudo ip netns exec router iptables -t nat \
      -A PREROUTING \
      --dst 203.0.113.254 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

$ sudo ip netns exec router iptables -t nat \
      -A OUTPUT \
      --dst 203.0.113.254 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

$ sudo ip netns exec router iptables -t nat \
      -A OUTPUT \
      --dst 127.0.0.1 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

$ sudo ip netns exec router iptables -t nat \
      -A POSTROUTING \
      -p tcp \
      --dst 192.0.2.2 \
      --dport 80 \
      -j SNAT \
      --to-source 203.0.113.254

スクリプトの動作検証

スクリプト全文 (シェルスクリプト)

create_docker_like_bridge_network.sh というスクリプトとして保存します。

#!/usr/bin/env bash

# Network Namespaceの作成
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip netns add ns3
sudo ip netns add router
sudo ip netns add wan

# 仮想インターフェースをNetwork Namespaceに割り当て
sudo ip link add ns1-veth0 type veth peer name ns1-br0
sudo ip link add ns2-veth0 type veth peer name ns2-br0
sudo ip link add ns3-veth0 type veth peer name ns3-br0
sudo ip link add gw-veth1 type veth peer name wan-veth0

# 仮想インターフェースをNetwork Namespaceに割り当て
sudo ip link set ns1-veth0 netns ns1
sudo ip link set ns2-veth0 netns ns2
sudo ip link set ns3-veth0 netns ns3
sudo ip link set ns1-br0 netns router
sudo ip link set ns2-br0 netns router
sudo ip link set ns3-br0 netns router
sudo ip link set gw-veth1 netns router
sudo ip link set wan-veth0 netns wan

# ブリッジの作成
sudo ip netns exec router ip link add dev br0 type bridge
sudo ip netns exec router ip link set ns1-br0 master br0
sudo ip netns exec router ip link set ns2-br0 master br0
sudo ip netns exec router ip link set ns3-br0 master br0

# 仮想インターフェースの有効化

## ns1
sudo ip netns exec ns1 ip link set ns1-veth0 up
sudo ip netns exec ns1 ip link set lo up

## ns2
sudo ip netns exec ns2 ip link set ns2-veth0 up
sudo ip netns exec ns2 ip link set lo up

## ns3
sudo ip netns exec ns3 ip link set ns3-veth0 up
sudo ip netns exec ns3 ip link set lo up

## router
sudo ip netns exec router ip link set ns1-br0 up
sudo ip netns exec router ip link set ns2-br0 up
sudo ip netns exec router ip link set ns3-br0 up
sudo ip netns exec router ip link set br0 up
sudo ip netns exec router ip link set lo up
sudo ip netns exec router ip link set gw-veth1 up

## wan
sudo ip netns exec wan ip link set wan-veth0 up
sudo ip netns exec wan ip link set lo up

# 仮想インターフェースにIPアドレスやルーティングの割り当て

## ns1
sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
sudo ip netns exec ns1 ip route add default via 192.0.2.254
sudo ip netns exec ns1 ip link set ns1-veth0 address 00:00:5E:00:53:01

## ns2
sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0
sudo ip netns exec ns2 ip route add default via 192.0.2.254
sudo ip netns exec ns2 ip link set ns2-veth0 address 00:00:5E:00:53:02

## ns3
sudo ip netns exec ns3 ip address add 192.0.2.3/24 dev ns3-veth0
sudo ip netns exec ns3 ip route add default via 192.0.2.254
sudo ip netns exec ns3 ip link set ns3-veth0 address 00:00:5E:00:53:03

## router
sudo ip netns exec router ip address add 192.0.2.254/24 dev br0
sudo ip netns exec router ip address add 203.0.113.254/24 dev gw-veth1

## wan
sudo ip netns exec wan ip address add 203.0.113.1/24 dev wan-veth0
sudo ip netns exec wan ip route add default via 203.0.113.254

# ルーターのIPフォワーディングの有効化
sudo ip netns exec router sysctl net.ipv4.ip_forward=1

# ルーターのループバックアドレスの転送設定の有効化
sudo ip netns exec router sysctl net.ipv4.conf.all.route_localnet=1

# iptablesのSource NAT(IPマスカレード)の設定
sudo ip netns exec router iptables -t nat  \
      -A POSTROUTING \
      -s 192.0.2.0/24 \
      -o gw-veth1 \
      -j MASQUERADE

# iptablesのDestination NATの設定
sudo ip netns exec router iptables -t nat \
      -A PREROUTING \
      --dst 203.0.113.254 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

sudo ip netns exec router iptables -t nat \
      -A OUTPUT \
      --dst 203.0.113.254 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

sudo ip netns exec router iptables -t nat \
      -A OUTPUT \
      --dst 127.0.0.1 \
      -p tcp \
      --dport 80 \
      -j DNAT \
      --to-destination 192.0.2.2:80

sudo ip netns exec router iptables -t nat \
      -A POSTROUTING \
      -p tcp \
      --dst 192.0.2.2 \
      --dport 80 \
      -j SNAT \
      --to-source 203.0.113.254

スクリプトの実行

作成済みの`create_docker_like_bridge_network.sh`を実行します。

$ chmod + x create_docker_like_bridge_network.sh
$ ./create_docker_like_bridge_network.sh
net.ipv4.ip_forward = 1
net.ipv4.conf.all.route_localnet = 1

作成済みのNetwork Namespaceの確認

$ ip netns ls
wan (id: 4)
router (id: 3)
ns3 (id: 2)
ns2 (id: 1)
ns1 (id: 0)
ns1の確認
$ sudo ip netns exec ns1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
4: ns1-veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff link-netns router
    inet 192.0.2.1/24 scope global ns1-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::8014:38ff:fea0:f294/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec ns1 ip route
default via 192.0.2.254 dev ns1-veth0
192.0.2.0/24 dev ns1-veth0 proto kernel scope link src 192.0.2.1
ns2の確認
$ sudo ip netns exec ns2 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
6: ns2-veth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff link-netns router
    inet 192.0.2.2/24 scope global ns2-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::ecb7:c5ff:fe7a:ccad/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec ns2 ip route
default via 192.0.2.254 dev ns2-veth0
192.0.2.0/24 dev ns2-veth0 proto kernel scope link src 192.0.2.2
ns3の確認
$ sudo ip netns exec ns3 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
8: ns3-veth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:03 brd ff:ff:ff:ff:ff:ff link-netns router
    inet 192.0.2.3/24 scope global ns3-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::e4f8:aeff:fed1:8039/64 scope link

$ sudo ip netns exec ns3 ip route
default via 192.0.2.254 dev ns3-veth0
192.0.2.0/24 dev ns3-veth0 proto kernel scope link src 192.0.2.3
routerの確認
$ sudo ip netns exec router ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 42:07:e4:03:4a:a1 brd ff:ff:ff:ff:ff:ff
    inet 192.0.2.254/24 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::4007:e4ff:fe03:4aa1/64 scope link
       valid_lft forever preferred_lft forever
3: ns1-br0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether be:4a:4e:40:6e:e1 brd ff:ff:ff:ff:ff:ff link-netns ns1
    inet6 fe80::bc4a:4eff:fe40:6ee1/64 scope link
       valid_lft forever preferred_lft forever
5: ns2-br0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether 42:07:e4:03:4a:a1 brd ff:ff:ff:ff:ff:ff link-netns ns2
    inet6 fe80::4007:e4ff:fe03:4aa1/64 scope link
       valid_lft forever preferred_lft forever
7: ns3-br0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether 8a:87:6d:1d:78:f5 brd ff:ff:ff:ff:ff:ff link-netns ns3
    inet6 fe80::8887:6dff:fe1d:78f5/64 scope link
       valid_lft forever preferred_lft forever
10: gw-veth1@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 4e:2f:cb:84:ee:a2 brd ff:ff:ff:ff:ff:ff link-netns wan
    inet 203.0.113.254/24 scope global gw-veth1
       valid_lft forever preferred_lft forever
    inet6 fe80::4c2f:cbff:fe84:eea2/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec router ip route
192.0.2.0/24 dev br0 proto kernel scope link src 192.0.2.254
203.0.113.0/24 dev gw-veth1 proto kernel scope link src 203.0.113.254
wanの確認
$ sudo ip netns exec wan ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
9: wan-veth0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 26:f9:80:46:ed:0e brd ff:ff:ff:ff:ff:ff link-netns router
    inet 203.0.113.1/24 scope global wan-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::24f9:80ff:fe46:ed0e/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec wan ip route
default via 203.0.113.254 dev wan-veth0
203.0.113.0/24 dev wan-veth0 proto kernel scope link src 203.0.113.1

疎通確認

ローカルホストからwan側への疎通
$ sudo ip netns exec ns1 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.170 ms

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

 $ sudo ip netns exec ns2 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.103 ms

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

$ sudo ip netns exec ns3 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.154 ms

--- 203.0.113.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.154/0.154/0.154/0.000 ms
wan側からホストへの疎通

wan側からホスト側のns2(192.0.2.2)への疎通ができているかを確認するために、 ns2のNetwork Namespaceに接続し、netcatコマンドで80番ポートを待ち受けしておきます。

$ sudo ip netns exec ns2 bash
root@tcpip:/home/ubuntu# nc -lvn 80
Listening on 0.0.0.0 80

同様に、wan側のNetwork Namespaceに接続します。(別ターミナルを使います)

$ sudo ip netns exec wan curl  203.0.113.254

上記の実行後に、ns2の標準出力に、接続受信の出力が表示されたことを確認できます。

root@tcpip:/home/ubuntu# nc -lvn 80
Listening on 0.0.0.0 80
Connection received on 203.0.113.254 60020
GET / HTTP/1.1
Host: 203.0.113.254
User-Agent: curl/7.68.0
Accept: */*

また、routerのlocalhost経由のアクセスでも、ns2のホストにアクセスできることが確認できます。

$ sudo ip netns exec router curl localhost
$ root@tcpip:/home/ubuntu# nc -lvn 80
Listening on 0.0.0.0 80
Connection received on 203.0.113.254 36414
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.68.0
Accept: */*

[発展編]Dockerコンテナを使ったネットワーク環境の構築

Network Namespaceを使ってネットワーク環境を構築することができました。 さらに、既存のDockerコンテナのNetwork Namespaceを 活用することで、Dockerコンテナをベースとしたネットワーク環境も構築することができます。

Dockerコンテナのネットワークを構築する方法として有名なのは、Docker Composeだと思います。 ただし、ネットワークの学習を目的とする上では、Namework NamespaceのコマンドをYAMLの設定ファイルに記述できる tinetという便利なツールを使うことができます。

*3

github.com

tinetのリポジトリには以下のように書かれています。

TiNET is network emulator environment for network function developer, routing software developer and networking educator. this is very simple tool that generate just shell script to construct virtual network.

DeepLで翻訳すると、以下のようになります。ということで、ネットワーク屋の人以外にも、 ネットワーク学習の教育向けにも活用できるのかと思います。

TiNETは、ネットワーク機能開発者、ルーティングソフトウェア開発者、 ネットワーク教育者向けのネットワークエミュレータ環境です。

また、tinetの作者の方のツイートですが、参考になるかと思います。

github.com

ホスト用のDockerコンテナのイメージ作成

各ネットワークユーティリティ用のコマンドをインストールしたコンテナイメージとnginxをインストールしたコンテナイメージの 2つを作成します。

ネットワークユーティリティ用のコマンドをインストールしたコンテナイメージ

Dockerfile_network という名称のDockerfileを作成します。

FROM ubuntu

RUN apt-get update && \
    apt-get install -y iputils-ping && \
    apt-get install -y iptables && \
    apt-get install -y vim && \
    apt-get install -y iproute2 && \
    apt-get install -y tcpdump && \
    apt-get install -y netcat && \
    apt-get install -y curl

Dockerfileを指定し、netutilsという名称のコンテナイメージを作成します。

$ docker build -f Dockerfile_network -t netutils .
Sending build context to Docker daemon  31.74kB
Step 1/2 : FROM ubuntu
 ---> d5ca7a445605
Step 2/2 : RUN apt-get update &&     apt-get install -y iputils-ping &&     apt-get install -y iptables &&     apt-get install -y vim &&     apt-get install -y iproute2 &&     apt-get install -y tcpdump &&     apt-get install -y netcat &&     apt-get install -y curl

nginxをインストールしたコンテナイメージ

上記のコンテナイメージをベースに作成します。 ここでは、Dockerfile_nginxという名称のDockerfileを作成します。

FROM netutils

RUN apt-get update && \
    apt-get install -y nginx

RUN echo "Hello Docker nginx container!" >  /var/www/html/index.nginx-debian.html

ENTRYPOINT /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

上記設定により、コンテナ起動時にENTRYPOINT命令にて 80ポートでnginxが起動されます。 また、疎通確認のために、nginxのHTTPサーバが返すhtmlファイルに対して、 Hello Docker nginx container!の文字列を返すように設定しています。

次に、Dockerfileを指定し、netutilsという名称のコンテナイメージを作成します。

$ docker build -f Dockerfile_nginx -t mynginx .

tinetのインストール

Intel Macを使っている場合

tinetのリポジトリのREADMEに記載のQuick Install通りに実行するとインストールされます。

$ curl -Lo /usr/bin/tinet https://github.com/tinynetwork/tinet/releases/download/v0.0.2/tinet
chmod +x /usr/bin/tinet
tinet --version

M1 Macを使っている場合

READMEに記載のQuick Install通りに実行しても、インストールに失敗します。おそらく curlでダウンロードしたバイナリが x86-64アーキテクチャであるため だと思います。

$ sudo file /usr/bin/tinet
/usr/bin/tinet: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=jrOQWb6v99DQpWDhefnP/nkrgg-bbZ4nDD4NHSrOM/nEL8ZQgelBKEONQiNAIx/Ql1cZnv4FmBvVWzIiIFU, not stripped

そのため、READMEに記載のBuildの手順に従ってビルドを実行します。 ただし、READMEに記載されているgolang:1.12のバージョンでは、ビルドに失敗するため、 回避策として golang:1.17を指定します。

$ git clone https://github.com/tinynetwork/tinet tinet && cd $_
$ docker run --rm -i -t -v $PWD:/v -w /v golang:1.17 go build
$ sudo mv tinet /usr/local/bin

上記を実行すると、バイナリがarm64で作成されます。

$ sudo file /usr/local/bin/tinet
/usr/local/bin/tinet: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, Go BuildID=LdJoSueuwc0karvXq8PX/cNk60_xkM-SFqx0DM3mt/a7XFmysuOm95ONK51OlB/kriiaVa9SJwzq5fFkrNt, not stripped

スクリプトの構築手順

Network Namespaceのネットワーク構築を使ったスクリプトを利用しますが、スクリプトの設定をtinetの設定ファイルの YAMLファイルに記述します。

この設定ファイルには、主に以下のような3つの設定項目があります

  • nodes 各ホストのDockerコンテナのイメージや仮想インターフェースを定義します。

  • node_configs 各ホスト上でコマンドを実行し、仮想インターフェースやルーティングテーブルなどを設定します。

  • test ネットワーク検証用のテストコマンドを記述します。

nodes項目の定義

各ホストのノードのコンテナイメージや仮想インターフェースを定義します。 ここでは、各ホストでnetuitlsのコンテナイメージを指定しますが、ns2のホストのみwan側からの疎通確認のために、 nginxをインインストール済みのmynginxのコンテナイメージを指定します。

nodes:
- name: ns1
  image: netutils
  interfaces:
  - { name: ns1-veth0, type: direct, args: router#ns1-br0 }
- name: ns2
  image: mynginx
  interfaces:
  - { name: ns2-veth0, type: direct, args: router#ns2-br0 }
- name: ns3
  image: netutils
  interfaces:
  - { name: ns3-veth0, type: direct, args: router#ns3-br0 }
- name: router
  image: netutils
  interfaces:
  - { name: ns1-br0, type: direct, args: ns1#ns1-veth0 }
  - { name: ns2-br0, type: direct, args: ns2#ns2-veth0 }
  - { name: ns3-br0, type: direct, args: ns3#ns3-veth0 }
  - { name: gw-veth1, type: direct, args: wan#wan-veth0 }
- name: wan
  image: netutils
  interfaces:
  - { name: wan-veth0, type: direct, args: router#gw-veth1 }

node_configs項目の定義

node_configs:
- name: ns1
  cmds:
  - cmd: ip addr add 192.0.2.1/24 dev ns1-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns1-veth0 address 00:00:5E:00:53:01
- name: ns2
  cmds:
  - cmd: ip addr add 192.0.2.2/24 dev ns2-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns2-veth0 address 00:00:5E:00:53:02
- name: ns3
  cmds:
  - cmd: ip addr add 192.0.2.3/24 dev ns3-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns3-veth0 address 00:00:5E:00:53:03
- name: router
  cmds:
  - cmd: ip link add dev br0 type bridge
  - cmd: ip link set br0 up
  - cmd: ip link set ns1-br0 master br0
  - cmd: ip link set ns2-br0 master br0
  - cmd: ip link set ns3-br0 master br0
  - cmd: ip addr add 192.0.2.254/24 dev br0
  - cmd: ip addr add 203.0.113.254/24 dev gw-veth1
  - cmd: sysctl net.ipv4.ip_forward=1
  - cmd: sysctl net.ipv4.conf.all.route_localnet=1
  - cmd: >-
      iptables -t nat
      -A POSTROUTING
      -s 192.0.2.0/24
      -o gw-veth1
      -j MASQUERADE
  - cmd: >-
      iptables -t nat
      -A PREROUTING
      --dst 203.0.113.254
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A OUTPUT
      --dst 203.0.113.254
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A OUTPUT
      --dst 127.0.0.1
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A POSTROUTING
      -p tcp
      --dst 192.0.2.2
      --dport 80
      -j SNAT
      --to-source 203.0.113.254
- name: wan
  cmds:
  - cmd: ip addr add 203.0.113.1/24 dev wan-veth0
  - cmd: ip route add default via 203.0.113.254

test項目の定義

ここでは、単純にホスト側からwan側への疎通用のテストコマンドを定義しています。

test:
  cmds:
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns1 (192.0.2.1)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns1 ping -c 1 203.0.113.1
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns2 (192.0.2.2)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns2 ping -c 1 203.0.113.1
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns3 (192.0.2.3)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns3 ping -c 1 203.0.113.1

スクリプトの動作検証

スクリプトの設定ファイル全文 (yaml)

docker_bridge_like_network_spec.yamlという名称でスクリプトを実行します。

---
nodes:
- name: ns1
  image: netutils
  interfaces:
  - { name: ns1-veth0, type: direct, args: router#ns1-br0 }
- name: ns2
  image: mynginx
  interfaces:
  - { name: ns2-veth0, type: direct, args: router#ns2-br0 }
- name: ns3
  image: netutils
  interfaces:
  - { name: ns3-veth0, type: direct, args: router#ns3-br0 }
- name: router
  image: netutils
  interfaces:
  - { name: ns1-br0, type: direct, args: ns1#ns1-veth0 }
  - { name: ns2-br0, type: direct, args: ns2#ns2-veth0 }
  - { name: ns3-br0, type: direct, args: ns3#ns3-veth0 }
  - { name: gw-veth1, type: direct, args: wan#wan-veth0 }
- name: wan
  image: netutils
  interfaces:
  - { name: wan-veth0, type: direct, args: router#gw-veth1 }
node_configs:
- name: ns1
  cmds:
  - cmd: ip addr add 192.0.2.1/24 dev ns1-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns1-veth0 address 00:00:5E:00:53:01
- name: ns2
  cmds:
  - cmd: ip addr add 192.0.2.2/24 dev ns2-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns2-veth0 address 00:00:5E:00:53:02
- name: ns3
  cmds:
  - cmd: ip addr add 192.0.2.3/24 dev ns3-veth0
  - cmd: ip route add default via 192.0.2.254
  - cmd: ip link set ns3-veth0 address 00:00:5E:00:53:03
- name: router
  cmds:
  - cmd: ip link add dev br0 type bridge
  - cmd: ip link set br0 up
  - cmd: ip link set ns1-br0 master br0
  - cmd: ip link set ns2-br0 master br0
  - cmd: ip link set ns3-br0 master br0
  - cmd: ip addr add 192.0.2.254/24 dev br0
  - cmd: ip addr add 203.0.113.254/24 dev gw-veth1
  - cmd: sysctl net.ipv4.ip_forward=1
  - cmd: sysctl net.ipv4.conf.all.route_localnet=1
  - cmd: >-
      iptables -t nat
      -A POSTROUTING
      -s 192.0.2.0/24
      -o gw-veth1
      -j MASQUERADE
  - cmd: >-
      iptables -t nat
      -A PREROUTING
      --dst 203.0.113.254
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A OUTPUT
      --dst 203.0.113.254
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A OUTPUT
      --dst 127.0.0.1
      -p tcp
      --dport 80
      -j DNAT
      --to-destination 192.0.2.2:80
  - cmd: >-
      iptables -t nat
      -A POSTROUTING
      -p tcp
      --dst 192.0.2.2
      --dport 80
      -j SNAT
      --to-source 203.0.113.254
- name: wan
  cmds:
  - cmd: ip addr add 203.0.113.1/24 dev wan-veth0
  - cmd: ip route add default via 203.0.113.254
test:
  cmds:
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns1 (192.0.2.1)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns1 ping -c 1 203.0.113.1
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns2 (192.0.2.2)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns2 ping -c 1 203.0.113.1
  - cmd: echo "=========================================="
  - cmd: echo "Connectivity test from ns3 (192.0.2.3)"
  - cmd: echo "=========================================="
  - cmd: docker exec ns3 ping -c 1 203.0.113.1

スクリプトの実行

docker_bridge_like_network_spec.yaml の設定ファイルを指定し、tinetのupコマンドとconfコマンドを実行します。 upコマンドはDockerコンテナの起動を行います。confコマンドは、起動中のDockerコンテナに対して仮想インターフェースの 設定などを行います。

これらを同時に実行するための upconf コマンドもあります。

*4

tinet upコマンドの実行

upコマンドでコンテナを起動します。

$ tinet up -c  docker_bridge_like_network_spec.yaml | sudo sh -x
+ docker run -td --net none --name ns1 --rm --privileged --hostname ns1 -v /tmp/tinet:/tinet netutils
+ mkdir -p /var/run/netns
+ docker inspect ns1 --format {{.State.Pid}}
+ PID=24154
+ ln -s /proc/24154/ns/net /var/run/netns/ns1
+ docker run -td --net none --name ns2 --rm --privileged --hostname ns2 -v /tmp/tinet:/tinet mynginx
+ mkdir -p /var/run/netns
+ docker inspect ns2 --format {{.State.Pid}}
+ PID=24233
+ ln -s /proc/24233/ns/net /var/run/netns/ns2
+ docker run -td --net none --name ns3 --rm --privileged --hostname ns3 -v /tmp/tinet:/tinet netutils
+ mkdir -p /var/run/netns
+ docker inspect ns3 --format {{.State.Pid}}
+ PID=24311
+ ln -s /proc/24311/ns/net /var/run/netns/ns3
+ docker run -td --net none --name router --rm --privileged --hostname router -v /tmp/tinet:/tinet netutils
+ mkdir -p /var/run/netns
+ docker inspect router --format {{.State.Pid}}
+ PID=24387
+ ln -s /proc/24387/ns/net /var/run/netns/router
+ docker run -td --net none --name wan --rm --privileged --hostname wan -v /tmp/tinet:/tinet netutils
+ mkdir -p /var/run/netns
+ docker inspect wan --format {{.State.Pid}}
+ PID=24463
+ ln -s /proc/24463/ns/net /var/run/netns/wan
+ ip link add ns1-veth0 netns ns1 type veth peer name ns1-br0 netns router
+ ip netns exec ns1 ip link set ns1-veth0 up
+ ip netns exec router ip link set ns1-br0 up
+ ip link add ns2-veth0 netns ns2 type veth peer name ns2-br0 netns router
+ ip netns exec ns2 ip link set ns2-veth0 up
+ ip netns exec router ip link set ns2-br0 up
+ ip link add ns3-veth0 netns ns3 type veth peer name ns3-br0 netns router
+ ip netns exec ns3 ip link set ns3-veth0 up
+ ip netns exec router ip link set ns3-br0 up
+ ip link add gw-veth1 netns router type veth peer name wan-veth0 netns wan
+ ip netns exec router ip link set gw-veth1 up
+ ip netns exec wan ip link set wan-veth0 up
+ ip netns del ns1
+ ip netns del ns2
+ ip netns del ns3
+ ip netns del router
+ ip netns del wan

tinet confコマンドの実行

起動中のコンテナに対して、仮想インターフェースのアドレスやルーティングの設定などを行います。

$ tinet conf -c  docker_bridge_like_network_spec.yaml | sudo sh -x
+ docker exec ns1 ip addr add 192.0.2.1/24 dev ns1-veth0
+ docker exec ns1 ip route add default via 192.0.2.254
+ docker exec ns1 ip link set ns1-veth0 address 00:00:5E:00:53:01
+ docker exec ns2 ip addr add 192.0.2.2/24 dev ns2-veth0
+ docker exec ns2 ip route add default via 192.0.2.254
+ docker exec ns2 ip link set ns2-veth0 address 00:00:5E:00:53:02
+ docker exec ns3 ip addr add 192.0.2.3/24 dev ns3-veth0
+ docker exec ns3 ip route add default via 192.0.2.254
+ docker exec ns3 ip link set ns3-veth0 address 00:00:5E:00:53:03
+ docker exec router ip link add dev br0 type bridge
+ docker exec router ip link set br0 up
+ docker exec router ip link set ns1-br0 master br0
+ docker exec router ip link set ns2-br0 master br0
+ docker exec router ip link set ns3-br0 master br0
+ docker exec router ip addr add 192.0.2.254/24 dev br0
+ docker exec router ip addr add 203.0.113.254/24 dev gw-veth1
+ docker exec router sysctl net.ipv4.ip_forward=1
+ docker exec router sysctl net.ipv4.conf.all.route_localnet=1
+ docker exec router iptables -t nat -A POSTROUTING -s 192.0.2.0/24 -o gw-veth1 -j MASQUERADE
+ docker exec router iptables -t nat -A PREROUTING --dst 203.0.113.254 -p tcp --dport 80 -j DNAT --to-destination 192.0.2.2:80
+ docker exec router iptables -t nat -A OUTPUT --dst 203.0.113.254 -p tcp --dport 80 -j DNAT --to-destination 192.0.2.2:80
+ docker exec router iptables -t nat -A OUTPUT --dst 127.0.0.1 -p tcp --dport 80 -j DNAT --to-destination 192.0.2.2:80
+ docker exec router iptables -t nat -A POSTROUTING -p tcp --dst 192.0.2.2 --dport 80 -j SNAT --to-source 203.0.113.254
+ docker exec wan ip addr add 203.0.113.1/24 dev wan-veth0
+ docker exec wan ip route add default via 203.0.113.254

作成済みのDockerコンテナの確認

docker psコマンドを実行すると、各コンテナが起動していることを確認できます。

$ docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS     NAMES
7cfd223d072b   netutils   "bash"                   3 minutes ago   Up 3 minutes             wan
898004063e9a   netutils   "bash"                   3 minutes ago   Up 3 minutes             router
6f3a6412d755   netutils   "bash"                   3 minutes ago   Up 3 minutes             ns3
39da406646fc   mynginx    "/bin/sh -c '/usr/sb…"   3 minutes ago   Up 3 minutes             ns2
e5160a7057f6   netutils   "bash"                   3 minutes ago   Up 3 minutes             ns1

ns1の確認

$ docker exec -it ns1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ns1-veth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.0.2.1/24 scope global ns1-veth0
       valid_lft forever preferred_lft forever

$ docker exec -it ns1 ip route
default via 192.0.2.254 dev ns1-veth0
192.0.2.0/24 dev ns1-veth0 proto kernel scope link src 192.0.2.1

ns2の確認

$ docker exec -it ns2 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ns2-veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.0.2.2/24 scope global ns2-veth0
       valid_lft forever preferred_lft forever

$ docker exec -it ns2 ip route
default via 192.0.2.254 dev ns2-veth0
192.0.2.0/24 dev ns2-veth0 proto kernel scope link src 192.0.2.2

ns3の確認

$ docker exec -it ns3 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ns3-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.0.2.3/24 scope global ns3-veth0
       valid_lft forever preferred_lft forever

$ docker exec -it ns3 ip route
default via 192.0.2.254 dev ns3-veth0
192.0.2.0/24 dev ns3-veth0 proto kernel scope link src 192.0.2.3

routerの確認

 $ docker exec -it ns3 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ns3-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.0.2.3/24 scope global ns3-veth0
       valid_lft forever preferred_lft forever

$ docker exec -it ns3 ip route
default via 192.0.2.254 dev ns3-veth0
192.0.2.0/24 dev ns3-veth0 proto kernel scope link src 192.0.2.3

wanの確認

$ docker exec -it wan ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: wan-veth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ce:11:e6:78:60:57 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 203.0.113.1/24 scope global wan-veth0
       valid_lft forever preferred_lft forever


$ docker exec -it wan ip route
default via 203.0.113.254 dev wan-veth0
203.0.113.0/24 dev wan-veth0 proto kernel scope link src 203.0.113.1

疎通確認

ローカルホストからwan側への疎通
$ docker exec -i ns1 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.070 ms

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


$ docker exec -i ns2 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.065 ms

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

docker exec -i ns3 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.063 ms

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

tinet testを使っても検証できます。

tinet test -c docker_bridge_like_network_spec.yaml | sudo sh -x
+ echo ==========================================
==========================================
+ echo Connectivity test from ns1 (192.0.2.1)
Connectivity test from ns1 (192.0.2.1)
+ echo ==========================================
==========================================
+ docker exec ns1 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.058 ms

--- 203.0.113.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.058/0.058/0.058/0.000 ms
+ echo ==========================================
==========================================
+ echo Connectivity test from ns2 (192.0.2.2)
Connectivity test from ns2 (192.0.2.2)
+ echo ==========================================
==========================================
+ docker exec ns2 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.055 ms

--- 203.0.113.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.055/0.055/0.055/0.000 ms
+ echo ==========================================
==========================================
+ echo Connectivity test from ns3 (192.0.2.3)
Connectivity test from ns3 (192.0.2.3)
+ echo ==========================================
==========================================
+ docker exec ns3 ping -c 1 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.058 ms

--- 203.0.113.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.058/0.058/0.058/0.000 ms
wan側からホストへの疎通

wan側からホスト側のns2(192.0.2.2)への疎通ができているかを確認するために、 wan側から 203.0.113.254に対して、curlコマンドを実行します。

$ docker exec -i wan curl 203.0.113.254
Hello Docker nginx container!

上記コマンドで、ns2のコンテナ上で起動しているnginxからHTMLのレスポンスが返ってきたことを確認できます。 同様に、routerのコンテナ上からlocalhostに対してcurlコマンドを実行すると同様の結果が確認できます。

$ docker exec -i router curl localhost
Hello Docker nginx container!

おわりに

いかがだったでしょうか(笑)。 Linuxで手を動かして学ぶTCP/IPネットワーク入門 という書籍で作りながら学んだブリッジやNATなどの学習した個別の要素を組み合わせて、Dockerのようなブリッジネットワークを作ってみるのは面白そうだなと思い、この記事のテーマを思いつきました。

実際に作ってみることで、Dockerの裏側の仕組みや便利さをより一層実感できるようになりました。

また、Network NamespaceやDockerコンテナを活用して、手を動かしながら仮想ネットワークを構築することで、 書籍で理論を学ぶことと異なり、ネットワーク技術についてより深く理解できるようになった気がしています。

今後は、アプリケーションレベルでこのようなネットワーク実験環境を活用できないか模索したいともいます。 例えば、Web配信の技術の書籍に記載されているようなHTTPキャッシュ・リバースプロキシなどの動作も手元のローカル環境で手軽に学習できたらいいなと思います。

最後に、M1 MaxをMacBookも入手したということもあり(笑)、仮想環境を存分に使いこなしつつ、今後も作って壊してを高速に回して楽しんで学習していきたいと思います。

参考資料

ネットワークを学ぶ上で参考になる資料(読んで面白かった書籍など)もあわせて載せておきます。

gihyo.jp

www.ohmsha.co.jp

aws.amazon.com

gihyo.jp

Dockerのブリッジネットワークの構成については、以下の記事がめちゃくちゃ分かりやすいので、ぜひ読んでほしいと思います。

tech-lab.sios.jp

Dockerのiptabesの仕組みについては、Software Desigin 2021年9月号とSoftware Desigin 2021年10月号の体系的に学ぶDockerネットワークのしくみiptablesの回が参考 になるかと思います。

gihyo.jp

gihyo.jp

tinetを使う上では、以下のYouTube動画やtinetのリポジトリ上のexamplesにある設定例が参考になるかと思います。

www.youtube.com

github.com

参考コード

今回使ったスクリプトや設定ファイルは、以下のリポジトリにアップしています。

github.com

*1:nameオプションには、VMの名称を指定します

*2:networking - Port forward with iptables - Server Faultも参考にしています。

*3:tinetでは、ルーティングテーブルを変更するため、Dockerコンテナが特権モードで動作します。

*4:tinetのコマンドを実行すると、実行されたコマンドが標準出力に表示されます。