MininetでL2/L3ネットワークを作るときのTips

## はじめに

[Mininet: An Instant Virtual Network on Your Laptop (or Other PC) - Mininet](http://mininet.org/) というツールをご存知でしょうか? Linux OS 上に仮想ネットワークをつくるツールで、SDN (OpenFlow) とかが流行っていた時期にその開発をデスクトップ上でできるということでよく使われていました。いまは P4 開発の文脈とかでも見かけるかな。……みたいな流れで紹介すると SDN 的な開発をするために使うものなのかと思ってしまうかもしれませんが、Mininet 自体はそうした使い方 ”も” できるというだけです。ここでは、Mininet で普通の L2 (Ethernet), TCP/IP なネットワークをつくってみる際の Tips をいくつか紹介します。

いきなり Tips だけ紹介されてもねーという感じかもしれませんが、ほぼ個人的な備忘録な位置づけです。一般的な L2/L3 として Mininet ができることを知りたい、普通のネットワーク上で試したいことがある人向けに残しておきます。

[2021-10-02] RPF (Reverse path filter) 設定について追記

## ルータをつくる

ルータというか、ルータっぽく動くノードというか。これはオフィシャルにサンプルがあります。

- [mininet/linuxrouter.py at master · mininet/mininet](https://github.com/mininet/mininet/blob/master/examples/linuxrouter.py)

```python
from mininet.node import Node

class LinuxRouter( Node ):
    "A Node with IP forwarding enabled."

    # pylint: disable=arguments-differ
    def config( self, **params ):
        super( LinuxRouter, self).config( **params )
        # Enable forwarding on the router
        self.cmd( 'sysctl net.ipv4.ip_forward=1' )

    def terminate( self ):
        self.cmd( 'sysctl net.ipv4.ip_forward=0' )
        super( LinuxRouter, self ).terminate()
```

やっていることは単純で、ノードを作ったとき、その netns のなかで `sysctl net.ipv4.ip_forward` を有効にするだけ。これで ipv4 の転送が有効になるので、あとは static route を設定すれば静的経路制御が使えるルータとして動きます。

## VLANが使えるノードとスイッチをつくる

これもオフィシャルにサンプルがあります。

- [mininet/vlanhost.py at master · mininet/mininet](https://github.com/mininet/mininet/blob/master/examples/vlanhost.py)

ありますが、もうずっと長いこと ifconfig 頼みなままで、うーんちょっとイマドキ感がない…などと思ったり。結局やり方だけ見て別につくってしまっていた。

VLAN の場合作らないといけないものがいくつかあるので、まとめた環境を作ってみましょう。こんな感じの L2 ネットワークを作ってみましょう。

![Sample L2 network](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/f1ce5109-ae22-b21f-aec8-280604772712.png)

上の図のネットワークがこんな感じのスクリプトで作れます。

```python
#!/usr/bin/python2

from mininet.topo import Topo
from mininet.net import Mininet
from mininet.log import setLogLevel, info
from mininet.cli import CLI
from mininet.node import Node, OVSBridge

class LinuxHost(Node):
    def delete_intf_ip(self, intf_name, ip_addr, prefix_length):
        return self.cmd("ip addr del %s/%s dev %s" % (ip_addr, prefix_length, intf_name))

    def add_sub_interface(self, parent_intf_name, sub_intf_name, ip_addr, vlan):
        self.cmd("ip link add link %s name %s type vlan protocol 802.1Q id %d" % (
            parent_intf_name, sub_intf_name, vlan
        ))
        self.cmd("ip addr add %s dev %s" % (ip_addr, sub_intf_name))
        self.cmd("ip link set %s up" % sub_intf_name)

class VlanOVSBridge(OVSBridge):
    def add_l2_access_vlan_config(self, intf_name, vlan_id):
        opt_str = "set Port %s vlan_mode=access tag=%d" % (intf_name, vlan_id)
        return self.vsctl(opt_str)

    def add_l2_trunk_vlans_config(self, intf_name, vlan_ids_str):
        opt_str = "set Port %s vlan_mode=trunk trunks=%s" % (intf_name, vlan_ids_str)
        return self.vsctl(opt_str)

class NetworkTopo(Topo):
    def build(self, **_opts):
        h1 = self.addHost('h1', cls=LinuxHost)
        h2 = self.addHost('h2', cls=LinuxHost)
        h3 = self.addHost('h3', cls=LinuxHost)
        s1 = self.addSwitch('s1', cls=VlanOVSBridge)
        s2 = self.addSwitch('s2', cls=VlanOVSBridge)
        self.addLink(h1, s1, intfName1='h1-eth0', intfName2='s1-eth1')
        self.addLink(h2, s1, intfName1='h2-eth0', intfName2='s1-eth2')
        self.addLink(h3, s2, intfName1='h3-eth0', intfName2='s2-eth3')
        self.addLink(s1, s2, intfName1="s1-eth0", intfName2='s2-eth0')

def config_hosts(net):
    # hosts
    h1 = net.get('h1')
    h1.intf('h1-eth0').setIP('192.168.10.1/24')
    h2 = net.get('h2')
    h2.intf('h2-eth0').setIP('192.168.20.2/24')
    h3 = net.get('h3')
    intf = h3.intf('h3-eth0')
    h3.delete_intf_ip(intf.name, intf.ip, intf.prefixLen)
    h3.add_sub_interface('h3-eth0', 'h3-eth0.10', '192.168.10.3/24', 10)
    h3.add_sub_interface('h3-eth0', 'h3-eth0.20', '192.168.20.3/24', 20)
    # switches
    s1 = net.get('s1')
    s1.add_l2_trunk_vlans_config('s1-eth0', '10,20')
    s1.add_l2_access_vlan_config('s1-eth1', 10)
    s1.add_l2_access_vlan_config('s1-eth2', 20)
    s2 = net.get('s2')
    s2.add_l2_trunk_vlans_config('s2-eth0', '10,20')
    s2.add_l2_trunk_vlans_config('s2-eth3', '10,20')

def run():
    topo = NetworkTopo()
    net = Mininet(topo=topo, controller=None)
    net.start()
    config_hosts(net)
    CLI( net )
    net.stop()

if __name__ == '__main__':
    setLogLevel('info')
    run()
```

前提

- Linux で vlan tag を扱う場合は 802.1Q カーネルモジュールを有効化しておく必要があります。(ここでは触れていません）

ノードの設定 (`LinuxHost`)

- アクセスポートにつなぐ場合は通常通り
- トランクポートにする場合は、ノード側にサブインタフェースを設定します。
    - Mininet では、インタフェース作成時にインタフェースにIPアドレスが適当に振られてしまうので、untagged な通信が必要なら IP 変更、不要なら削除しておきます (`h3.delete_intf_ip`)
    - サブインタフェースを作ったときは `up` が必要になるので注意 (作っただけだと `down` 状態)

スイッチの設定 (`VlanOVSBridge`)

- OVS のインタフェース設定をいじります。
- Trunk vlans は 文字列で渡しますが、Cisco 等で使われる "1-3,5" のような連続する VLAN ID をまとめて設定する書き方はできなかったはず。

## スイッチでSTPをつかう

Open vSwitch (OVS) の Spanning-tree サポートについて:

- per-vlan STP は OVS ではサポートされていません。複数の tree instance を持つことはできず、単一の tree となります。
- STP としては 802.1D と RSTP (802.1w) が使えるようです。
    - OVS で RSTP 動かしてみたことがないのでこの記事中では触れません。

802.1D STP を有効化する場合、 `addSwitch` するときに `stp=True` を渡せばOKです。上にあげたスクリプトで STP を有効にするならこんな感じ。

```python
class NetworkTopo(Topo):
    def build(self, **_opts):
        s1 = self.addSwitch('s1', cls=VlanOVSBridge, stp=True)
```

ブリッジプライオリティも設定できます。

- マニュアル参照: [http://www.openvswitch.org/support/dist-docs/ovs-vsctl.8.txt](http://www.openvswitch.org/support/dist-docs/ovs-vsctl.8.txt)
- priority 未指定(デフォルト)だと 0x8000 (32768)

```python
class VlanOVSBridge(OVSBridge):
    def add_bridge_priority_config(self, priority):
        opt_str = "set Bridge %s other_config:stp-priority=%s" % (self.name, priority)
        return self.vsctl(opt_str)

def config_hosts(net):
    s1 = net.get('s1')
    s1.add_bridge_priority_config(0) # primary root
    s2 = net.get('s2')
    s2.add_bridge_priority_config(0x1000) # secondary root
```

## 簡易的なサーバをつくる

各ノード (= netns) の中でプロセスをバックグラウンド実行すると、簡易的なサーバとして動作させられます。

(例1) http server っぽいやつ

```python
def config_hosts(net):
    h1 = net.get('h1')
    h1.cmd('python3 -m http.server 8080 -d /path/to/document-root &')
```

(例2) パケットフィルタ

```python
def config_hosts(net):
    h1 = net.get('h1')
    h1.cmd('sh /path/to/pf.sh')
```

```bash:pf.sh
#!/bin/bash

# clear all
iptables -F
# set policy
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# INPUT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
```

## Mininet CLI の外からもノード内操作ができるノードをつくる

Mininet 使ったことのある方はわかると思いますが、Mininet で複数のノードを操作するときは `xterm` コマンドを使って、複数のノード (ノードとして動く netns) の中でターミナルを動かしてそこで操作しましょう、というスタイルなんですよね。これを使うには X が手元で動いていないといけないし、動かせたとしても xterm はちょっと使い勝手が悪い。Mininet CLI 自体は python で動いているので複数のターミナルから attach するようになっていない。

ノード自体は netns として動いているので、別に Mininet CLI からじゃなくても netns として操作できればいいわけです。が、Mininet 内部で [unshare(1)](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/unshare.2.html) をつかって namespace を作っているので ip コマンド (iproute2) で見える状態になっていません。では `ip netns` で見える状態にするには? というと `/var/run/netns` にシンボリックリンクが必要だと。なのでそれを作ってしまえばよい。

- [ip-netns - process network namespace management at Linux.org](https://www.linux.org/docs/man8/ip-netns.html)
- [Mininet のネットワーク周りの実装 | CUBE SUGAR STORAGE](https://momijiame.tumblr.com/post/88568203016/mininet)

```python
from mininet.node import Node

class LinuxHost(Node):
    def config(self, **params):
        super(LinuxHost, self).config(**params)
        self.cmd('mkdir -p /var/run/netns')
        # add network namespace link to allow access namespace from external of mininet-CLI
        self.cmd('ln -s /proc/%d/ns/net /var/run/netns/%s' % (self.pid, self.name))

    def terminate(self):
        # clear network namespace link
        self.cmd('rm /var/run/netns/%s' % self.name)
        super(LinuxHost, self).terminate()
```

上に上げた VLAN ネットワークで試すとこうなります。Mininet CLI の `sh` は shell command の実行なので、Mininet CLI の外から `ip netns exec` で内部操作と同等のことができるようになっていることがわかります。

```
*** Starting CLI:
mininet> nodes
available nodes are: 
h1 h2 h3 s1 s2
mininet> sh ip netns list
h1 (id: 0)
h2 (id: 1)
h3 (id: 2)
mininet>
mininet> h1 ping -c1 192.168.10.3
PING 192.168.10.3 (192.168.10.3) 56(84) bytes of data.
64 bytes from 192.168.10.3: icmp_seq=1 ttl=64 time=0.435 ms

--- 192.168.10.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.435/0.435/0.435/0.000 ms
mininet>
mininet> sh ip netns exec h1 ping -c1 192.168.10.3
PING 192.168.10.3 (192.168.10.3) 56(84) bytes of data.
64 bytes from 192.168.10.3: icmp_seq=1 ttl=64 time=0.348 ms

--- 192.168.10.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.348/0.348/0.348/0.000 ms
mininet>
```

## 非対称ルーティングが起きる構成では Reverse path filter を設定する

[2021-10-02] 追記

参照

- [戻り経路フィルタ (Reverse Path Filtering)](https://linuxjf.osdn.jp/JFdocs/Adv-Routing-HOWTO/lartc.kernel.rpf.html)
- [When RHEL has multiple IPs configured, only one is reachable from a remote network. Or why does RHEL ignore packets when the route for outbound traffic differs from the route of incoming traffic? - Red Hat Customer Portal](https://access.redhat.com/solutions/53031)

検証用あるいはトレーニング用に仮想ネットワークをつくる場合、あえて非対称ルーティングが発生する構成にすることがあるかもしれません。Linux kernel ではそうした構成で「出入りの方向が一致しないパケットを許容するかどうか」を設定できます。許容する場合には `sysctl net.ipv4.conf.all.rp_filter = 2` (Loose mode) に設定します。(ディストリビューションによってデフォルト設定が異なるかもしれないので注意してください。)

```py
import subprocess

def run():
    subprocess.call(["sysctl", "-w", "net.ipv4.conf.all.rp_filter=2"])
    subprocess.call(["sysctl", "-w", "net.ipv4.conf.default.rp_filter=2"])
    # 省略
```

## 注意事項

### OVS Bridge の名前と DPID 指定

Mininet で OVS を使用するとき、デフォルトではスイッチ名を "アルファベット + 数字" の形にする必要があります。数字ナシの名前を設定すると "Unable to derive default datapath ID - please either specify a dpid or use a canonical switch name such as s23." というエラーメッセージが出力されます。

これは OVS を使うときに (OpenFlowで使われる) Datapath ID (DPID) を指定する必要があるためで、[デフォルトでは名前に含まれている数字をもとに DPID を設定するようになっています](https://github.com/mininet/mininet/blob/master/mininet/node.py#L913-L929)。 `addSwitch` するときに `dpid` オプションで別途 DPID が指定できて、このオプションを入れている場合はアルファベットのみのスイッチ名を指定することができます。

### namespace で何がどこまで分けられているか

Mininet の場合、network namespace (netns) **だけ** 分離していて、ほかの namespace については特に分割していません。なので、たとえば全てのノード上で実行されているプロセスが見えます。そのあたりコンテナのように分離されているわけではないので気をつけてください。

スイッチについては namespace をわける・わけないというオプションがあったはずです…がちゃんと見てないです。デフォルトではスイッチは OVS 上の Bridge として作成されており、特に namespace を分離して作られていなかったと思います。

## おわりに

いくつか挙げましたが、あとはこれらの要素を組み合わせることで、基本的な L2/L3 ネットワークをつくれます。

- OVS as Simple L2 Switch
  - 802.1D STP (OVS的には RSTP もいけるはず)
  - VLAN access/trunk
- 静的経路制御のできるルータ
    - VLAN (sub interface)
    - iptables とか組み合わせれば L4 firewall あたりまで拡張可能なはず
- ノード
    - VLAN (sub-interface)
    - サーバプロセスをバックグラウンド実行しておけば L4 についてもある程度はフォローできる

これくらいの範囲内で試したいことであれば、mininet が割とお手軽にいろいろ試行錯誤できる環境を提供してくれるでしょう。まあ [mininet api](http://mininet.org/api/annotated.html) とかある程度使い方を覚えないといけないという面倒臭さはありますが……。