Ubuntu Weekly Recipe

第804回mininetでお手軽ネットワークテスト環境を構築する

3月も半ばになり、暖かい日も増えてきました。これだけ暖かくなってくると、ちょっとしたアプリで少し特殊なネットワークフレームを流したり、普段使わないネットワークプロトコルを試したくなりますよね。でも本番環境でそれをやってしまうと、変質者としてしかるべき場所に通報されてしまいます。そこで今回は他人に迷惑をかけずに隔離されたネットワークテスト環境を構築できるmininetを使って、お縄にかからないようにしてみましょう。

Open vSwitchとネットワーク名前空間で気軽にテスト環境を構築する

Linuxカーネルにはネットワーク名前空間(netns)という機能があります。これはホストや他のコンテナから隔離された環境でネットワークインターフェースを作成し、操作できるようになる仕組みで、特にLinuxのコンテナ系ツールで使っている基礎技術のひとつです。

Ubuntuだとip netnsコマンドなどでネットワーク名前空間を管理できますし、たとえばループしたインターフェース間での性能テストみたいなことも可能になります。ip netnsコマンドを用いたネットワーク名前空間の使い方は「LXCで学ぶコンテナ入門」の第6回Linuxカーネルのコンテナ機能[5]でも紹介されています。気になる方はそちらも参照してください。

ip netnsは便利ではあるものの、複雑な設定が必要になると、ひたすらオプションを並べた複雑なコマンドを複数回入力しなくてはなりません。特別なツールをインストールしなくても良いという利点はあるものの、お手軽とはほど遠い状況です。もう少し簡単にテスト環境を作るツールがほしいところ。

そこで登場するのが今回紹介するmininetとなります。mininetはPython製のツールで、ネットワーク名前空間とOpen vSwitchを用いて、さまざまなトポロジーのネットワーク構成を構築できます[1]。たとえばホスト2台とそれらが繋がったスイッチ1台の単純な構成から、スイッチごとに複数のホストが接続されそれらのスイッチが簡易的なルーターで繋がっているような複雑な構成までお手の物です。

さらにmininetはPythonモジュールでもあるため、任意のネットワーク構成を構築するためのPythonスクリプトとして記述も可能です。もし複雑な構成を試したくなった場合は、その構成情報をPythonで記述していくことになるでしょう。

ちなみにOpen vSwitchは仮想スイッチ機能を実現するソフトウェアとなります。mininetでは主にOpenFlowコントローラーとして使っており、Open vSwitchの代わりにRyuなど他のOpenFlowコントローラーも使えるようです。

mininetは、このあたりのネットワーク名前空間やOpenFlow/Open vSwitchの使い方がわからなくても、気軽にちょっとした隔離ネットワークをソフトウェアだけで構築できるという点で大変便利なツールなのです。

mininetのインストール

さっそくmininetをインストールしましょう。いくつかの方法が存在しますが、Ubuntuの場合は次の2種類が主な手段となります。

仮想マシンイメージは、mininetが提供するUbuntuベースのイメージとなっています。mininet本体だけでなく、mininet用のWiresharkなどもインストールされており、とりあえず使ってみるだけならOSを問わないのでおすすめです。ただしイメージ自体はOVF(Open Virtualization Format)ファイルとして提供されています。任意の仮想マシンシステムで使うためにはOVFのインポートから始めないといけない点に注意してください。

mininet自体はUbuntu/Debianの公式リポジトリにもパッケージが存在します。幸か不幸かmininetの2024年3月時点での最新版は2021年2月にリリースされた「2.3.0」であり、Ubuntu 22.04 LTSであればこの最新版のパッケージが使えます。もし既存の環境で使いたいなら、パッケージ版のほうがお手軽でしょう。ただしmininet用のWiresharkは入っていないので、そちらは別途インストールする必要があります。

ところでmininetが仮想マシンイメージを推奨する理由のひとつが、⁠隔離性」にあります。mininetはネットワーク名前空間を使ってはいますが、すべてが隔離されているわけではありません。また一度にひとつのmininetプロセスしか起動できないのも注意点のひとつです。つまり複数の異なる構成を持ったネットワークを同時に利用したい場合は、どうしても仮想マシンで複数のインスタンスを立てて、それぞれ別のmininet設定を流し込まないといけないのです。

これはパッケージ版をインストールする場合も同様です。お試しで使うなら、ホストに直接インストールするのもアリではあるものの、本格的に使うなら任意の仮想マシンに閉じ込めてしまうのが良いでしょう。ちなみにコンテナ上で動かすというものひとつの案ではありますが、Open vSwitchを動くようにするのはいろいろと手間がかかります[2]。よって結局のところは何らかの方法でUbuntuの仮想マシンを用意した上で、そこのパッケージ版のmininetをインストールすることをおすすめします。

今回はLXD/Incusで、次のようにUbuntu 22.04 LTSの仮想マシンインスタンスを作って試しました。

$ lxc launch ubuntu:22.04 mininet
$ lxc shell mininet
$ sudo -i -u ubuntu

仮想マシンでもホストでも良いので、次のようにmininetパッケージをインストールしてください。

$ sudo apt install mininet

これでmininetの準備完了です。

mininetを試してみる

mininetでは「mn」コマンドでインスタンスを起動し、そこに指定したオプションによってネットワークトポロジーを設定できます。たとえばmn --helpを実行してみましょう。

$ mn --help
Usage: mn [options]
(type mn -h for details)

(中略)
  --topo=TOPO           linear|minimal|reversed|single|torus|tree[,param=value
                        ...] minimal=MinimalTopo linear=LinearTopo
                        reversed=SingleSwitchReversedTopo
                        single=SingleSwitchTopo tree=TreeTopo torus=TorusTopo

もっとも気にするであろうオプションがこの--topoです。これによってネットワーク構成を設定できます。何も指定しなければ、minimalが指定されます。これは指定した数のホストを1台のスイッチに接続するトポロジーで、ホストの数も指定しなければ2台のホストが作られます。

実際に試してみましょう。mnを実行するには管理者権限が必要です。

$ sudo mn
*** No default OpenFlow controller found for default switch!
*** Falling back to OVS Bridge
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1)
*** Configuring hosts
h1 h2
*** Starting controller

*** Starting 1 switches
s1 ...
*** Starting CLI:
mininet>

Open vSwitch(OVS Bridge)を利用しネットワークを構築し、ホスト2台(h1とh2)にスイッチ1台(s1)を作成、接続していることがわかります。最後にCLIを起動して止まっています。

このCLIを操作して、このネットワーク上にパケットを流していくことになります。またhelpで使い方を表示し、exitでmininet環境を終了します。mininet環境を終了すると、作成した仮想ネットワークは削除されるので注意してください。

試しにノードの情報を表示するコマンドをいくつか実行してみましょう。

ノードの一覧
mininet> nodes
available nodes are:
h1 h2 s1

ノード間の接続
mininet> links
h1-eth0<->s1-eth1 (OK OK)
h2-eth0<->s1-eth2 (OK OK)

各ノードのネットワーク情報
mininet> net
h1 h1-eth0:s1-eth1
h2 h2-eth0:s1-eth2
s1 lo:  s1-eth1:h1-eth0 s1-eth2:h2-eth0

IPアドレスも含めた表示
mininet> dump
<Host h1: h1-eth0:10.0.0.1 pid=5260>
<Host h2: h2-eth0:10.0.0.2 pid=5262>
<OVSBridge s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=5267>

起動時に表示された情報の通り、2台のホストと1台のスイッチが繋がっています。またホストh1にはIPアドレス「10.0.0.1」が、ホストh2にはIPアドレス「10.0.0.2」が割り振られていることもわかります。

ホストの詳細なネットワーク設定は、ipコマンドでも表示できます。

mininet> h1 ip addr
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: h1-eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 9e:2e:22:20:20:a2 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.1/8 brd 10.255.255.255 scope global h1-eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::9c2e:22ff:fe20:20a2/64 scope link
       valid_lft forever preferred_lft forever
mininet> h2 ip addr
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: h2-eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 62:c8:b1:39:10:e0 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.2/8 brd 10.255.255.255 scope global h2-eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::60c8:b1ff:fe39:10e0/64 scope link
       valid_lft forever preferred_lft forever

mininetでは「ホスト名 コマンド」を実行することで、mininetが動いているマシンにインストールされている任意のコマンドを、指定したホストのネットワーク名前空間内部で実行できます。今回は「ip addr」を実行しましたがbashなど他のコマンドも利用可能です。

ちなみにホストから見ると次のようになります。

$ ip addr
(中略)
23: s1-eth1@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ovs-system state UP group default qlen 1000
    link/ether da:18:8b:07:1a:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::d818:8bff:fe07:1a02/64 scope link
       valid_lft forever preferred_lft forever
24: s1-eth2@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ovs-system state UP group default qlen 1000
    link/ether 46:c7:13:95:e0:2a brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::44c7:13ff:fe95:e02a/64 scope link
       valid_lft forever preferred_lft forever
25: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 7e:23:05:a8:6e:6e brd ff:ff:ff:ff:ff:ff
26: s1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 1a:cf:12:d1:66:49 brd ff:ff:ff:ff:ff:ff

スイッチ(s1)のインターフェースがホストから見えているようです。実はホストノードはネットワーク名前空間に閉じ込められますが、スイッチ用のネットワーク自体は別途設定をしない限りはネットワーク名前空間を作りません。

ホストノード間のpingを実行するにはpingallコマンドやpingfullコマンドが便利です。これらは単に表示される情報が異なるだけとなります。

mininet> pingall
*** Ping: testing ping reachability
h1 -> h2
h2 -> h1
*** Results: 0% dropped (2/2 received)

mininet> pingallfull
*** Ping: testing ping reachability
h1 -> h2
h2 -> h1
*** Results:
 h1->h2: 1/1, rtt min/avg/max/mdev 0.388/0.388/0.388/0.000 ms
 h2->h1: 1/1, rtt min/avg/max/mdev 0.010/0.010/0.010/0.000 ms

もちろん普通のpingコマンドも使えます。

mininet> h1 ping -c 3 h2
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.140 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.098 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.097 ms

--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2031ms
rtt min/avg/max/mdev = 0.097/0.111/0.140/0.020 ms

専用のターミナルを起動する

mininetでネットワークを構築したあとは、個々のホストノードで端末を立ち上げてそれぞれのホストノードで異なるコマンドを実行したいことがあります。一番手っ取り早いのは「h1 xterm」などと実行し、ホストノードの名前空間の中で任意の端末エミュレーターを起動することです。しかしながらサーバー上で動くかす場合は、そもそもGUIの端末エミュレーターもデスクトップ環境も入っていないことが多いでしょう。

そこでmnexecコマンドを用いて、任意のホストノードの名前空間でシェルを立ち上げる方法を紹介しましょう。

まずはdumpコマンドで各ノードのPIDを確認しておきます。

mininet> dump
<Host h1: h1-eth0:10.0.0.1 pid=6185>
<Host h2: h2-eth0:10.0.0.2 pid=6187>
<OVSBridge s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=6192>

ここでh1がPID=6185、h2がPID=6187になっていることがわかります。ホスト上でプロセスリストを表示すると次のようになっています。

$ ps -fe -q 6185,6187
UID          PID    PPID  C STIME TTY          TIME CMD
root        6185    6177  0 16:28 pts/2    00:00:00 bash --norc --noediting -is mininet:h1
root        6187    6177  0 16:28 pts/3    00:00:00 bash --norc --noediting -is mininet:h2

--norcによりbashrcなどを読み込まないモードとなり、--noeditingによってインタラクティブモードでreadlineライブラリを使わずに処理することになります。-isはインタラクティブモードを矯正しつつ、コマンド入力をパイプで受け取るような場合に指定します。つまりこれらによりmininet内部で任意のプログラムの実行をbashに指示できるようになっているのです。

たとえば前述した「h1 ip addr」なども実際はこのbashプロセスに対して「ip addr」コマンドを実行するように指示しています。その結果、⁠ip addr」コマンドはh1のbashの子プロセスとして起動するのです。

さて、mnexecコマンドはこのmnが作ったホストノードのbashプロセスと同じ名前空間に「アタッチ」するような形で動作します。

$ sudo mnexec -a 6185 bash
root@mininet:/home/ubuntu# ip addr
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: h1-eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether a2:c5:ae:b5:c9:49 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.1/8 brd 10.255.255.255 scope global h1-eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::a0c5:aeff:feb5:c949/64 scope link
       valid_lft forever preferred_lft forever

具体的には-a PIDで指定したPIDを元に、mininetが作ったホストノード用のネットワーク名前空間(とマウント名前空間)を検索し、そこの名前空間にアタッチした上で指定したコマンドを実行します。その結果、今回のようにbash上で実行した「ip addr」は、mininetの「h1 ip addr」のそれと同じ結果になるというわけです。

名前空間にアタッチしてコマンドを実行するというシンプルな作りであるため、もちろん他のシェルでも使えますし、任意のコマンドを実行可能です。また複数のシェルインスタンスを立ち上げて複雑なこともできるようになっています。たとえばmnexecでtcpdumpしつつ、他のmnexecで任意のネットワークプログラムを動かす、といった定番操作が考えられるでしょう。

トポロジーの設定について

mnコマンドでmininetを起動する際、トポロジーオプションを使うことで、複雑な構成を構築可能です。

たとえば何も指定しなければ、ホストノード1台とスイッチ1台になりますが、これをホストノードを10台にするには次のようにsingleトポロジーを指定します。

$ sudo mn --topo=single,k=10
*** No default OpenFlow controller found for default switch!
*** Falling back to OVS Bridge
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2 h3 h4 h5 h6 h7 h8 h9 h10
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1) (h3, s1) (h4, s1) (h5, s1) (h6, s1) (h7, s1) (h8, s1) (h9, s1) (h10, s1)
*** Configuring hosts
h1 h2 h3 h4 h5 h6 h7 h8 h9 h10
*** Starting controller

*** Starting 1 switches
s1 ...
*** Starting CLI:

mininet> links
h1-eth0<->s1-eth1 (OK OK)
h2-eth0<->s1-eth2 (OK OK)
h3-eth0<->s1-eth3 (OK OK)
h4-eth0<->s1-eth4 (OK OK)
h5-eth0<->s1-eth5 (OK OK)
h6-eth0<->s1-eth6 (OK OK)
h7-eth0<->s1-eth7 (OK OK)
h8-eth0<->s1-eth8 (OK OK)
h9-eth0<->s1-eth9 (OK OK)
h10-eth0<->s1-eth10 (OK OK)

何も指定しない場合のminimalトポロジーは、⁠single」「k=2」を指定した場合と同じです。singleトポロジーは「スイッチが1台で、任意の数のホストノードをkパラメーターで設定できる」トポロジーとなります。

単純にスイッチの台数を増やしたければ、linearトポロジーが使えます。

$ sudo mn --topo=linear,k=4,n=2
*** No default OpenFlow controller found for default switch!
*** Falling back to OVS Bridge
*** Creating network
*** Adding controller
*** Adding hosts:
h1s1 h1s2 h1s3 h1s4 h2s1 h2s2 h2s3 h2s4
*** Adding switches:
s1 s2 s3 s4
*** Adding links:
(h1s1, s1) (h1s2, s2) (h1s3, s3) (h1s4, s4) (h2s1, s1) (h2s2, s2) (h2s3, s3) (h2s4, s4) (s2, s1) (s3, s2) (s4, s3)
*** Configuring hosts
h1s1 h1s2 h1s3 h1s4 h2s1 h2s2 h2s3 h2s4
*** Starting controller

*** Starting 4 switches
s1 s2 s3 s4 ...
*** Starting CLI:

mininet> links
h1s1-eth0<->s1-eth1 (OK OK)
h1s2-eth0<->s2-eth1 (OK OK)
h1s3-eth0<->s3-eth1 (OK OK)
h1s4-eth0<->s4-eth1 (OK OK)
h2s1-eth0<->s1-eth2 (OK OK)
h2s2-eth0<->s2-eth2 (OK OK)
h2s3-eth0<->s3-eth2 (OK OK)
h2s4-eth0<->s4-eth2 (OK OK)
s2-eth3<->s1-eth3 (OK OK)
s3-eth3<->s2-eth4 (OK OK)
s4-eth3<->s3-eth4 (OK OK)

linearトポロジーは「スイッチの台数をkパラメーターで、個々のスイッチにつながるホストの台数をnパラメーターで設定し、スイッチを直列につなぐ」トポロジーです。

スイッチをツリー構造でつなげるには、その名の通りtreeトポロジーを使います。

$ sudo mn --topo=tree,depth=2,fanout=3
*** No default OpenFlow controller found for default switch!
*** Falling back to OVS Bridge
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2 h3 h4 h5 h6 h7 h8 h9
*** Adding switches:
s1 s2 s3 s4
*** Adding links:
(s1, s2) (s1, s3) (s1, s4) (s2, h1) (s2, h2) (s2, h3) (s3, h4) (s3, h5) (s3, h6) (s4, h7) (s4, h8) (s4, h9)
*** Configuring hosts
h1 h2 h3 h4 h5 h6 h7 h8 h9
*** Starting controller

*** Starting 4 switches
s1 s2 s3 s4 ...
*** Starting CLI:

mininet> links
s1-eth1<->s2-eth4 (OK OK)
s1-eth2<->s3-eth4 (OK OK)
s1-eth3<->s4-eth4 (OK OK)
s2-eth1<->h1-eth0 (OK OK)
s2-eth2<->h2-eth0 (OK OK)
s2-eth3<->h3-eth0 (OK OK)
s3-eth1<->h4-eth0 (OK OK)
s3-eth2<->h5-eth0 (OK OK)
s3-eth3<->h6-eth0 (OK OK)
s4-eth1<->h7-eth0 (OK OK)
s4-eth2<->h8-eth0 (OK OK)
s4-eth3<->h9-eth0 (OK OK)

ツリーの頂点にs1スイッチが存在し、そこからfanoutのパラメーターの数だけ子ノードを生やし、depthで指定した深さの終端ノードとして、ホストノードを構築します。つまりホストノードの数は「fanoutのdepth乗個」となるわけです。

最後のtorusトポロジーは2次元トーラスを構築するトポロジーです。mn --topo=torus,3,3のように後ろに次元数を指定します。ただし、ネットワークがループしてしまうためSTP(Spanning Tree Protocol)に対応したコントローラーを使う必要があります。

Pythonプログラムを使えば複雑化・自動化もできる

mininetは--topoオプションでトポロジーを設定できますが、それでも柔軟性に欠けると感じるかもしれません。また、ルーターノードを追加したり、物理ネットワークインターフェースをノードとして参加させたりしたいこともあるでしょう。

mininetは元々Python製のソフトウェアであることもあって、外部のプログラムから利用しやすいPython APIを用意しています。mnコマンド自体がこのAPIを利用しており、たとえばトポロジーの作り方などは/usr/lib/python3/dist-packages/mininet/topo.pyなどが参考になるはずです。

さらに、いくつかのよくある機能を実現するためのPythonプログラムは/usr/lib/python3/dist-packages/mininet/examples/にサンプルとしてインストールされています。そちらも参考にすると良いでしょう。ただしこれらのサンプルはうまく動かないことも多いです。

ちなみにmininetのAPIに対応し、より複雑なIPネットワークを構成するためのIPMininetというライブラリも存在します。定番のIPプロトコルをmininet内部に組み込みたい場合は、こちらも参考になるかもしれません。

mininetは、残念ながらアクティブにメンテナンスされているソフトウェアとは言えませんが、それでも必要な機能が十分に揃っています。ちょっとしたことをやるのであればこれでも問題ありませんし、多少複雑なこともPythonスクリプトを書けば実現できます。ちょっとしたテストを作るために、お手軽に環境を作りたい場合であれば、相応の役に立つはずです。

おすすめ記事

記事・ニュース一覧