振り分けスイッチ試作


概要

二台の PC を使って下図のような環境を作成する。 二台の Open vSwitch (による Open Flow switch)を二本の線で接続し、一方に HTTP アクセスを通し、もう一方には HTTP 以外のトラフィックを通す、というもの。 下図右端の PC がインターネット向けに通信するときに、フローによって経路を振り分けられるように振る舞う。

コントローラは今回 DPID=1111 スイッチ側に入れてある。 つまり 1111 スイッチについてはコントローラとの通信は 127.0.0.1 (local interface)を対象に行う。 aaaa スイッチについてはコントローラホストと TCP 通信が必要。 なお 1111 も aaaa スイッチも共に Internet へのアクセスをもっているが、これは必須ではない。


準備

何らかの振り分け処理などを行うなら、NOX コントローラプログラムではそのパケットが「どのスイッチ」の「どのポート」から届いたかを知る必要がある場合が多い。 それらを識別するために必要な情報について示す。

datapath ID (DPID) 調査

異なるホストで動作する NOX に対して Open vSwitch の openflowd を起動する際には、以下のようにして対象ホストを指定する。
# ovs-openflowd netdev@br0 --ports=eth0,eth2 tcp:192.168.12.96
この時、NOX 側にメッセージが出る。 関係ありそうなものを以下に抜粋。

01843|openflow|DBG:Passive tcp interface received connection 
01846|openflow|DBG:stream: negotiated OpenFlow version 0x01 (we support versions 0x01 to 0x01 inclusive, peer no later than version 0x01)
01847|nox|DBG:Success sending in 'sending switch config'
01850|nox|DBG:Success sending in 'receiving features reply'
01855|nox|DBG:Success receiving in 'receiving features reply'
01858|nox|DBG:Success sending in 'receiving ofmp capability reply'
01863|nox|DBG:Success receiving in 'receiving ofmp capability reply'
01864|nox|DBG:Datapath 002320f65dd8 sent error in response to capability reply, assuming no management support
 <<< Datapath 初登場
01867|nox|DBG:No switch auth module registered, auto-approving switch
01868|nox|DBG:Registering switch with DPID = 2320f65dd8
01869|nox.coreapps.examples.pyswitch|INFO:Switch 2320f65dd8 has joined the network 
datapath ID の固定
しかし datapath ID は openflow daemon を起動する度に毎回異なる値になる。
Jul 25 09:31:43 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320c8ab0b
Jul 25 09:35:32 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320f65dd8
Jul 25 09:44:46 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320be98f5
Jul 25 10:25:32 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320a1b5d0
Jul 25 10:26:21 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320add0d9
さすがにこれでは困る場合が多いだろう。
(スイッチごとに特有の処理がない pyswitch のようなケースでは問題がないが、今回はだめ)

が、大丈夫。datapath ID は openflowd の起動時にオプションとして渡して固定できる。
# ovs-openflowd netdev@br0 --datapath-id=000000001000000d --ports=eth0,eth2 tcp:192.168.12.96
とすると、openflowd 側のログには以下のように出る。
Jul 25 10:45:17 localhost ovs-openflowd: 00003|ofproto|INFO|using datapath ID 0000002320969302
Jul 25 10:45:17 localhost ovs-openflowd: 00004|ofproto|INFO|datapath ID changed to 000000001000000d
となる。

なおマニュアルによると、--datapath が与えられなかった場合は MAC アドレスを使うとある、が、そうなってない。
ともあれ NOX 側のメッセージにもこの指定した値(1000000d)でちゃんと DPID として出ているので良いだろう。

注意:openflowd は(マニュアルにもあるが) --datapath-id=000000000000100d のように前方ゼロも 省略できない。 しかし NOX はログメッセージには 100d のように詰めて出す場合が結構あって、ちょっと対称性がない。
(例えば joined メッセージには Registering switch with DPID = 100d などと出す。)

ポート番号調査

ovs-ofctl show コマンドによって、どの NIC が OpenFlow ネットワーク上では、つまり NOX 管理情報としては何番のポートとして認識されているかを知ることが出来る。

DPID="1111" 側:
#  ovs-ofctl show br0
OFPT_FEATURES_REPLY (xid=0x1): ver:0x1, dpid:0000000000001111 << 以下の出力は抜粋 
n_tables:2, n_buffers:256
features: capabilities:0x87, actions:0xfff
 1(eth1): addr:00:50:43:00:e1:3b, config: 0, state:0 << eth1 を 1 番ポートとして認識。以下同様。 
 2(eth3): addr:00:50:43:00:e3:ea, config: 0, state:0
 3(eth0): addr:00:50:43:00:e3:47, config: 0, state:0
 LOCAL(br0): addr:6e:36:f7:64:22:64, config: 0x1, state:0x1
OFPT_GET_CONFIG_REPLY (xid=0x3): frags=normal miss_send_len=0

DPID="aaaa" 側:
# ovs-ofctl show br0
OFPT_FEATURES_REPLY (xid=0x1): ver:0x1, dpid:000000000000aaaa
n_tables:2, n_buffers:256
features: capabilities:0x87, actions:0xfff
 1(eth3): addr:00:0e:0c:b1:1e:30, config: 0, state:0x1  << これだけ 0x1 なのは何??
 2(eth0): addr:00:0e:0c:b1:1d:34, config: 0, state:0
 3(eth2): addr:00:0e:0c:64:dc:b9, config: 0, state:0
 LOCAL(br0): addr:52:64:d6:ba:42:3c, config: 0x1, state:0x1
OFPT_GET_CONFIG_REPLY (xid=0x3): frags=normal miss_send_len=0

設計

単なる MAC learning bridge である pyswitch に、どのようにして「振り分け」の機能を追加するか検討する

経路選択

経路選択のルールとしては、
・DPID=1111 は、port# 2 と (3+1) の単位で学習する
 ・つまり inport を 2 あるいは (3+1) のどちらかで覚える
 ・output が (3+1) になった場合は、それが port 80 に関係すれば 3 へ、それ以外なら 1 へ出す
・DPID=aaaa についても同様
といったところ。
(なお後に port 80 だけでなく 443, 25 も上側の経路を流れるように追加した。)

見方としては、二つのスイッチがあり、左側が Internet へ接続されており、右側が PC につながれている「だけ」のものと見なせば良い。

両スイッチとも、各ポートごとに MAC アドレスを laerning する普通のブリッジとして機能する。
が、実はこのうち、両者を接続する V1, V2 ポートは二線からなる仮想ポートであり、線の一方は特定のトラフィック(port=80)だけを通し、それ以外は他方の線に流すようにされている。
V1 = (3) + (1) であり、V2 = (2) + (1) である。

が、MAC アドレスの learning は V1, V2 の仮想ポート単位でやる点に注意。このあたり間違った設定をするとループする。 模式的に学習テーブルを書くと下図のようになる。

つまり、パケットが入ってきたときは、source アドレスを見て MAC テーブルに登録するが、その際、入力ポートとしては DPID=1111 なら 2 あるいは V1 を登録する。つまり入力ポートなら 3 or 1 が V1 に集約して登録する。
パケットを出すときは、destination アドレスを見て MAC テーブルにあれば、その出力ポートにのみパケットを出力するが、その際、DPID=1111 なら登録されている出力ポートが V1 なら、実際の出力ポートとして 3 or 1 のどちらに出すかを(学習テーブルではなく)振り分けルールに従って決定する。
フローエントリに登録する際はそのようにして振り分けられた後の、実際の出力ポートで登録すればよい。(下図:1111側のみ)

このロジックを実現する。

部品確認

install_datapath_flow( ) による送信

http://noxrepo.org/~yapkke/doc/classnox_1_1lib_1_1core_1_1Component.html
を見ながら、機能部品について検討。
install_datapath_flow() がデータ送信を含んでいることに注意。
仕様をhttp://noxrepo.org/~yapkke/doc/classnox_1_1lib_1_1core_1_1Component.html で見ると、

nox::lib::core::Component::install_datapath_flow ( self,
 	dp_id,
 	attrs,
 	idle_timeout,
 	hard_timeout,
 	actions,
 	buffer_id = None,
 	priority = openflow.OFP_DEFAULT_PRIORITY,
 	inport = None,
 	packet = None	
)

となっている。 buffer_id, packet が None なら送信しない、ということらしい。 今回は普通に「フローエントリを登録しつつ」「対象ポートに出力」するだけなので、これはこのまま使える。

send_openflow( ) による FLOOD

元の pyswitch には、もし「未知の MAC source addr をもつ」パケットの場合、openflow.OFPP_FLOOD を使って inst.send_openflow( ) で FLOOD 出力するコードがある。 しかし振り分けポートへの出力は FLOOD で両側ポートに出すわけにはいかない。 振り分けポートの両方に二重にパケットを出してしまうことになるから。
仕様は、

def nox::lib::core::Component::send_openflow ( self,
 	dp_id,
 	buffer_id,
 	packet,
 	actions,
 	inport = openflow.OFPP_CONTROLLER
)

となっている。 さてこの actions は list of actions or dp port to send out of だそうで、 openflow.OFPP_FLOOD ではなく、OFPAT_OUTPUTを入れてやれば特定ポートにのみ出力することができるらしい。

つまり ypswitch では openflow.OFPP_FLOOD がそのまま入っていた。
	inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)
となっていたところを、
	prt = inst.st[dpid][dstaddr]
	actions = [[openflow.OFPAT_OUTPUT, [0, prt[0]]]]
	inst.send_openflow(dpid, bufid, buf, actions, inport)
のようにしてやればいいか。
(つまりそのような別関数 myFlood(dpid, bufid, buf, vinport) を作る)

複数ポートに吐くのは actions を配列化して
actions = [[openflow.OFPAT_OUTPUT, [0, 1]], [openflow.OFPAT_OUTPUT, [0, 2]]]
のようにしてやる。

なお順繰りにやろうとして send_openflow( ) を同じバッファで二度呼んではいけない。
それをやって OpenFlow Error (type=1,	code=7) というのを出してしまった。
http://noxrepo.org/pipermail/nox-dev/2011-May/007507.html に記述あり。

実装

コード

振り分けコントローラ:panierswitch.py

実際に設置する場合は ./nox/build/src/nox/coreapps/examples 以下あたりに置けば良い。 同ディレクトリの meta.json へのエントリ追加も忘れず。 実際に末端につないだ PC から外部(インターネット)のホストに対してアクセスし、HTTP など指定されたトラフィックとそれ以外のトラフィックが振り分けられていることが確認できる。
(単純に NIC のランプでもよし、スイッチホスト上で tcpdump するもよし。)

解説

結局以下のような構造にした。
VPORTS と OPORTS は一つの「スイッチ構成およびトポロジと振り分けロジック」を納めた配列データから合成することが勿論可能なのだが、プログラムコードの分かりやすさのために今回は別のものとして書く。

まずスイッチとポートについて定義
# switch DPID
SW1 = int("1111", 16)  << DPID を手で書くと失敗するので定数定義
SW2 = int("aaaa", 16)

# aggregated virtual port
V1 = 5  << 仮想ポート番号として適当に与える(実際には datapath 内で一意で有りさえすればよい)
V2 = 7

# rule base << これ、あとでコードの中に書かれるので注意 
RULE80  = 80  # HTTP 
RULE443 = 443 # HTTPS
RULE25  = 25  # SMTP
ANY = -1      # no condition (catch all)

# map table : phy port -> vport << 物理ポートがどのように仮想ポートに束ねられているかを表現
VPORTS = { SW1: { 2:2, 3:V1, 1:V1 }, 
           SW2: { 3:3, 2:V2, 1:V2 } }

# outport check table, format= { vport: [[ RULE, outport ]] } << こちらは
         逆、ではないが、仮想ポートへの出力がどのように処理されるべきかルールと出力物理ポートを指定
OPORTS = { SW1: { 2:  [ [ ANY, 2 ] ],
                  V1: [ [ RULE80, 3 ], [ RULE443, 3 ],
                        [ RULE25, 3 ], [ ANY, 1 ] ] }, 
           SW2: { 3:  [ [ ANY, 3 ] ],
                  V2: [ [ RULE80, 2 ], [ RULE443, 2 ],
                        [ RULE25, 2 ], [ ANY, 1 ] ] } }

書き方としては、
3:  [ [ ANY, 3 ] ] は、
出力先が仮想ポート 3 (実際には物理ポート 3 に同じ)の場合は、全ての場合に物理ポート 3 に出力。
V2: [ [ RULE80, 2 ], [ ANY, 1 ] ] なら、
出力先が仮想ポート V2 (物理ポート 2 と 1 の二本からなる)の場合は、RULE80 にマッチすれば物理ポート 2 に出力。
それ以外の全ての場合に物理ポート 1 から出力。
となる。

RULE80 などのルールは getOutport( ) で参照される。ルールは柔軟性が必要なので関数内にハードコードしている。
    ops=OPORTS[dpid][vport] << 仮想ポート一つぶんの出力先候補リストを得る
    for (rule, outport) in ops:
        if rule == RULE80 and testTCPport(80, packet):
            return outport 
        elif rule == RULE443 and testTCPport(443, packet):
            return outport
        elif rule == RULE25 and testTCPport(25, packet):
            return outport 
        elif rule == ANY: # no condition match
            return outport 
            
Python 的には判定用の関数をリストに含ませておくのが良いのだろうが、今はバグ取りに見通しが良い方を採る。

この冒頭部分の定義を参照しながら、MAC 学習テーブルには(束ねられたポートの場合は)仮想ポート番号を学習させ、出力がそこを指す場合は、束ねられたどの経路を選ぶべきかを決める。 その出力時の選定ロジックが上記後半に示した記述で、これに関しては判定ルールを増やすときに素直に追加していけば良い。 最終的にスイッチの追加やポートの追加といった構成の変更に対してコードを直接変更する必要がない状況にはできた。


以上で冒頭に示したような特定トラフィックを別経路に回す振り分けスイッチが実現できた。



Yutaka Yasuda (yasuda [ at ] ylb.jp)