Open vSwitchのconntrack連携を試してみた(その3)

# 正直コネクショントラッキングとかしたくないですｗ

セッションテーブルが溢れてひどいことになるのが目に見えているじゃないですかorz

古くは2000年ごろから[ルータのCAMが溢れる問題で苦しんだり](http://www.janog.gr.jp/meeting/janog10/pdf/network-tanaka.pdf)、最近でも[古いOpen vSwitchでData Pathのフローテーブルが溢れる事象で苦しんだり](http://research.sakura.ad.jp/2011/10/11/hbstudy27/)(※)、なんと15年以上も同じ問題で地獄を見続けているわけです。

※ 補足: Open vSwitchは、バージョン2.3あたりから新規フローセットアップ性能がものすごく改善しており、この問題は現状クリアされています(DoSにもだいぶ強くなりました)。

# ということで今回は、

conntrackテーブルが溢れた時の安定性も含めて、性能面の評価もやってみようと思います。

前回までに、conntrackインテグレーション機能をサポートしたOpen vSwitchのインストールから、最小構成でのステートフルインスペクションの実装までやってみました。

- [Open vSwitchのconntrack連携を試してみた(その1)](http://qiita.com/jh1vxw/items/0d0e370fb1b374ab0c93)
- [Open vSwitchのconntrack連携を試してみた(その2)](http://qiita.com/jh1vxw/items/bfc3bb3d18a923990190)

今回は、仮想サーバホスティングにありがちなマルチテナント構成での実装を行ってみます。

# テスト構成

前回同様、2つのVMをOpen vSwitchに接続し、eth2を介して外部ノードを接続します(インターネット上のホストを想定)。

![a.png](https://qiita-image-store.s3.amazonaws.com/0/108990/40cd153d-e00d-850f-2318-d5874bb66b6e.png)

ここでは、以下のポリシーを実装します。

- VM1, VM2が発信するパケットに対し、MACスプーフィング、ARPスプーフィング、IPスプーフィング(※)を防止するフィルタを設定
- VM1が着信するパケットのうち、TCP 22番ポート宛のみ許可し、それ以外は許可しない
- VM2が着信するパケットのうち、TCP 80番ポート宛のみ許可し、それ以外は許可しない
- VM1, VM2いずれも、自身が発した通信の戻りパケットを自動的に許可する(ここでconntrackインテグレーション機能を活用し、ステートフルインスペクションを実装)

※ スプーフィングとは、他者のアドレスを偽装したパケットを送信する行為です。[共有ホスティングでは対策が必須となります](http://irs.ietf.to/past/docs_20080808/)。

# Step1. Open vSwitchのフローエントリを設定

conntrackの気持ちになって3時間ほど悩みまくった結果、以下の設定を編み出しました。
※ もっとスマートな方法があったら教えて下さい m(__)m

```
$ cat a.flow
# conntrack
priority=9900,ip,ct_state=-trk actions=ct(table=0)
priority=9800,ip,ct_state=+trk+est actions=normal
priority=9700,ip,ct_state=+trk+rel actions=normal

# packet filter to vm1
priority=6900,dl_dst=52:54:00:60:84:ff,tcp,tp_dst=22 actions=ct(commit),normal
priority=6800,dl_dst=52:54:00:60:84:ff,ip actions=drop

# packet filter to vm2
priority=5900,dl_dst=52:54:00:99:90:78,tcp,tp_dst=80 actions=ct(commit),normal
priority=5800,dl_dst=52:54:00:99:90:78,ip actions=drop

# in_port=2 (from vm1)
priority=2900,in_port=2,dl_src=52:54:00:60:84:ff,arp,arp_spa=192.168.0.1 actions=normal
priority=2800,in_port=2,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop
priority=2700,in_port=2,dl_src=52:54:00:60:84:ff,ip,nw_src=192.168.0.1 actions=ct(commit),normal

# in_port=3 (from vm2)
priority=1900,in_port=3,dl_src=52:54:00:99:90:78,arp,arp_spa=192.168.0.2 actions=normal
priority=1800,in_port=3,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop
priority=1700,in_port=3,dl_src=52:54:00:99:90:78,ip,nw_src=192.168.0.2 actions=ct(commit),normal

# default allow
priority=0 actions=normal

$ ovs-ofctl add-flows br0 a.flow
```

in_portはtapインターフェイスのofport番号を指定します。この環境では2と3が割り当てられていました。以下のように確認することができます。

```
$ ovs-vsctl get interface tap2 ofport   <= VM1側
2
$ ovs-vsctl get interface tap4 ofport   <= VM2側
3
```

正しく設定できると、以下のようになります。

```
[root@sl67-r3 ~]# ovs-ofctl dump-flows br0
NXST_FLOW reply (xid=0x4):
 cookie=0x0, duration=4.116s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=9900,ct_state=-trk,ip actions=ct(table=0)
 cookie=0x0, duration=4.115s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=9800,ct_state=+est+trk,ip actions=NORMAL
 cookie=0x0, duration=4.115s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=9700,ct_state=+rel+trk,ip actions=NORMAL
 cookie=0x0, duration=4.115s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=6900,tcp,dl_dst=52:54:00:60:84:ff,tp_dst=22 actions=ct(commit),NORMAL
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=5900,tcp,dl_dst=52:54:00:99:90:78,tp_dst=80 actions=ct(commit),NORMAL
 cookie=0x0, duration=4.115s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=6800,ip,dl_dst=52:54:00:60:84:ff actions=drop
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=5800,ip,dl_dst=52:54:00:99:90:78 actions=drop
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=2900,arp,in_port=2,dl_src=52:54:00:60:84:ff,arp_spa=192.168.0.1 actions=NORMAL
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=2700,ip,in_port=2,dl_src=52:54:00:60:84:ff,nw_src=192.168.0.1 actions=ct(commit),NORMAL
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=1900,arp,in_port=3,dl_src=52:54:00:99:90:78,arp_spa=192.168.0.2 actions=NORMAL
 cookie=0x0, duration=4.113s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=1700,ip,in_port=3,dl_src=52:54:00:99:90:78,nw_src=192.168.0.2 actions=ct(commit),NORMAL
 cookie=0x0, duration=4.114s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=2800,in_port=2,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop
 cookie=0x0, duration=4.113s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=1800,in_port=3,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop
 cookie=0x0, duration=4.113s, table=0, n_packets=0, n_bytes=0, idle_age=4, priority=0 actions=NORMAL
```

設定の意味は以下のとおりです。

- priority=9900: 最初に全てのIPパケットをconntrackインテグレーション機能の処理に回し、自テーブル(table=0)を再評価します。2回目の評価ではtrkフラグが付いているので、本ルール(ct_state=-trk)にはマッチせず、以降のルールの評価に進みます。
- priority=9800,9870: conntrackに載っているセッションにマッチするパケットを通しちゃいます。
- priority=6900: VM1宛ての22番ポートを許可しconntrackします。関連する通信は前のルール(9800,9870)で許可されます。
- priority=6800: その他のVM1宛のパケットを破棄します。
- priority=5900,5800: VM1と同様、VM2宛てパケットの処理です。
- priority=2900,2800: VM1が発するARPパケットをソースMACアドレス、ソースIPアドレス指定で許可し、その他のブロードキャスト、マルチキャストを破棄します。
- priority=2700: VM1が発するIPパケットをソースMACアドレス、ソースIPアドレス指定で許可し、conntrackします。
- priority=1900-1700: VM1と同様、VM2が発するパケットの処理です。

# Step2. 通信確認

以下のパターンで通信テストをし、通信成立(○)、不成立(×)が期待通りになることを確認できました。いい感じです。

| From        | To           | 通信内容          | 結果 |
|:-----------:|:------------:|:----------------:|:----:|
| VM1         | EXT          | 全て              | ○ |
| VM1         | VM2          | TCP 80番ポート宛て | ○ |
| VM1         | VM2          | 上記以外          | × |
| VM2         | EXT          | 全て              | ○ |
| VM2         | VM1          | TCP 22番ポート宛て | ○ |
| VM2         | VM1          | 上記以外           | × |
| EXT         | VM1          | TCP 22番ポート宛て | ○ |
| EXT         | VM1          | 上記以外           | × |
| EXT         | VM2          | TCP 80番ポート宛て | ○ |
| EXT         | VM2          | 上記以外           | × |

また、VM1,VM2においてMACアドレス、IPアドレスを別のものに変更(スプーフィング)した場合に、通信ができなくなることを確認しました。

# Step3. 性能測定

Open vSwitchを経由する通信をconntrackする場合としない場合で、どれほどパフォーマンスの差がでるかを測定します。ここでは、conntrackテーブルサイズを2,000万に拡張しました。

/etc/modprobe.d/conntrack.conf

```
options nf_conntrack hashsize=2500000
```

また、今回UDPパケットを用いてconntrackテーブルを埋め尽くす試験をするため、セッションが溜まるようにtimeoutを30秒から3,000秒に延長しておきました。sysctl -aで確認すると、以下のようになります。

```
net.netfilter.nf_conntrack_buckets = 2500096
net.netfilter.nf_conntrack_max = 20000000
net.netfilter.nf_conntrack_udp_timeout = 3000
```

これで準備ができました。

いつもこのようなテストをする際、パケットジェネレータとして[mzコマンド](http://www.perihel.at/sec/mz/)を使ってます。VM1にて以下のように実行し、VM2に向けてUDPパケットをバンバン送信します。

```
$ mz -c0 -A 192.168.0.1 -B 192.168.0.2 -t udp "sp=1-65535,dp=1-65535"
```

spとdpは送信元ポート番号と宛先ポート番号の範囲です。それぞれインクリメントしながらパケットを送出するため、全てのパケットがconntrack的に新規セッション扱いとなります。

以下のように、VM2にて受信パケットレートの推移をvnstatコマンドで確認します。

```
$ vnstat -i eth1 -l
Monitoring eth1...    (press CTRL-C to stop)

   rx:    24.68 Mbit/s 75200 p/s          tx:        0 kbit/s     1 p/s
```

conntrackしない場合と、conntrackする場合の性能差は以下のようになりました。

![b.png](https://qiita-image-store.s3.amazonaws.com/0/108990/53110a91-23eb-27f9-30af-4aba248ce05c.png)

conntrackしない場合に比べ、conntrackする場合は即座に42%程度まで性能低下します。
また、セッション数が増えるに従いconntrackテーブルの更新/検索コストが上がるため、2,000万エントリ時には22%程度まで低下することがわかりました。

# まとめ

本機能を用いることで実現したいことが実装できることがわかり、機能評価としては満足のいく結果となりました。また、エージング(負荷を継続的にかけて不安定にならないか)も行いましたが、今のところ問題ないようでした。

しかしながら、性能低下がだいぶ厳しいです。[弊社のクラウド](http://cloud.sakura.ad.jp/)では、[ロードバランサ](http://cloud-news.sakura.ad.jp/load-balancer/)や[ルータアプライアンス](http://cloud-news.sakura.ad.jp/vpc-router/)の性能目安を公開していますが、これらは全て仮想アプライアンスではなく、仮想スイッチ(Open vSwitch)のボトルネックによるものです。

Open vSwitchの限界性能をもとに、マルチテナントした場合に1ユーザあたりこれくらいは大丈夫だろう的な、ギリギリの値を絞り出しています。conntrackする場合は、これらの値の見直しが必要そうです(涙)

あともう一つ、conntrackではL2の情報が持てず、L3以上の情報(IPアドレスやポート番号)しか保持できないため注意が必要です。具体的には、異なるユーザが異なるVLAN上で同じIPアドレス帯(192.168.0.0/24など)を使用していた場合、あるユーザの通信によって生成されたconntrackエントリによって、別のユーザの通信が意図せず許可されてしまう可能性があります。これについては、zoneを指定することで解決できそうなので、折をみて試してみようと思います。

http://openvswitch.org/support/dist-docs/ovs-ofctl.8.html

```
       ct_zone=zone
              Matches  the  given  16-bit connection zone exactly. This repre-
              sents the most recent connection tracking context  that  ct  was
              executed  in.  Each  zone  is an independent connection tracking
              context, so if you wish to track the  same  packet  in  multiple
              contexts  then you must use the ct action multiple times. Intro-
              duced in Open vSwitch 2.5.
```

長々と最後までお付き合いいただきましてありがとうございましたｗ