Trema で OpenFlow ネットワークプログラミング
はじめに
"ネットワークプログラミング" という言葉は、恐らくシステム屋さんにとって TCP/UDP あるいはIPといった L4, L3 の世界のプログラミングを想起させるのではないかと思います。ですが OpenFlow によって、そのレイヤが一気に L1 まで落ちました。つまり Layer-1 ︵物理層︶までがプログラマブルに扱える領域になったということです。 これは主に Ethernet とIPに限定されるものの、従来 L1 から L3 の領域はネットワーク屋さんの領分で L4 以上がシステム屋さん、あるいはアプリケーション屋さんの領分という暗黙の了解を OpenFlow が無くしてしまいました。 今日は OpenFlow ネットワークを制御するソフトウェア︵コントローラ︶を実装するためのフレームワーク "Trema" について、お話をさせていただきます。Trema とは何か?
3年くらい前から OpenFlow や SDN といった言葉がもてはやされ、その実何がどう上手くできるのか、モヤッとしててよくわからないバズワードとなって久しいです。更にここへきて、通信事業者などが中心となって、こうした技術を利用した、ファイヤーウォールや諸所の認証機能などのネットワーク機能を仮想化する NFV なるモノが打ち出されました。 厄介なバズワードがどんどん増え、加えて SDN と NFV を同一レベルのものとして扱う記事などによって混乱が進み、この話題に縁遠いエンジニアにとってはそろそろ訳がわからなくなってきてるんじゃないかと思います。 憶測ですが、こうした用語が次々と登場する背景にネットワーク屋さん︵とかく欧米圏︶の特徴に起因するところがあるのではないかと思っています。彼らは何にでも名前を付けたがります。仮にそれが別の言葉で一般的に認識されているモノや事象であっても、名前っぽい名前が無いと分かると、それらを一般化した総体として長々しい名前を付けます。そしてその名前の各単語の頭文字をくっつけてそいつを指すようにします。"Virtual eXtensible Local Area Network" で VXLAN、"Network Virtualization using Generic Routing Encapsulation" で NVGRE などなど。 実に普通です。GNU や Linux, YAML などといったネーミングと比較しても、システム屋さんとネットワーク屋さんの性質の違いが現れます。 閑話休題。 ﹁じゃあ Trema とか OpenFlow って何なんじゃ﹂という話ですが、ここでこれらの用語をざっくりと整理してみます。先の SDN, NFV 云々の話の流れで言うと、OpenFlow とは SDN を実現するプリミティブな部分の実装をサポートするモノで、Trema とは OpenFlow ネットワークを制御する OpenFlow コントローラと呼ばれるソフトウェアを実装するフレームワークになります。これをソフトウェアとの対比で表したものが下の表になります。 NFV => ある目的に特化したアプリケーション (Ruby on Rails 相当) SDN => アプリケーションを作るためのプログラミング言語 (Ruby 相当) OpenFlow => プログラミング言語を支える技術 (libc 相当) Trema => 標準Cライブラリ (libc) の実装の一つ (glibc 相当)何故 Trema なのか?
標準Cライブラリと一口に言っても、その実装は glibc の他にも BSD 系のモノや組み込み用に専科したものなど様々あります。 同じように、OpenFlow コントローラフレームワーク (と呼ばれているモノ、あるいはそう分類されるもの) には Trema 以外にも様々あります。主だった OpenFlow コントローラフレームワークの特徴をまとめてみました。 Trema : 開発・テスト・デバッグツールを自前で備えたオールインワンな OpenFlow コントローラフレームワーク。NEC のメンバーが中心となって開発を推進。 Floodlight : 米 Big Switch 社が開発する商用のオープンソースの OpenFlow コントローラ。高度にモジュール化された設計が特徴。 Ryu : OpenStack Neutron に Ryu プラグインがあり、Neutron と REST API を使って協調動作可能。NTT コミュニケーションズのメンバーが中心となって開発を推進。 OpenDaylight : OpenFlow 以外の SDN 技術をサポートする SDN コントローラ(これについては後述します)。大規模エンタープライズ向け。主に cisco などのネットワークベンダーが中心となって開発を推進。 次に、Trema が唯一と謳うオールインワンフレームワークを成す、テストフレームワークとテストツール、そして最近 Trema プロジェクト内部で話題のサブプロジェクト Trema::Pio についてご紹介します。テストフレームワーク‥仮想ネットワーク
ここで言う仮想ネットワークとは﹁物理の上に仮想ネットワークを構築して、マルチテナントなネットワークを作りましょ﹂的なモノではなく、一つのPC上で仮想的に任意のトポロジーのIPネットワークを構築して、そこに仮想的なホストを接続させる事が出来るモノのことを言っています。なので﹁ネットワークシュミレータ﹂といった方が意味的に近いのかもしれませんが、Trema グループがこれを﹁仮想ネットワーク﹂と呼んでいるので、ここでもそう呼びます。 Trema では、こうした仮想ネットワークの構築、及び仮想ネットワークに接続する仮想ホスト間でのパケット転送ができる環境を提供しています。これによって、物理的に OpenFlow ネットワークを用意しなくても、手元にあるPCで手軽に大規模かつ複雑な OpenFlow ネットワークを構築し、そこで自前のコントローラの動作をテストできます。 では実際に動かしながらこいつを見てみましょう。 まず、Trema を動かせる環境を用意します。GitHub の Trema リポジトリから最新のソースコードを取得しビルドします︵詳しくは、Trema の GitHub ページ などを参考に実施してください︶。 環境構築が済んだら、以下のコマンドで実際に動かしてみます。仮想ネットワークを含む Trema に対する操作は、リポジトリ配下の trema コマンドによって行われます。"trema --help" で使い方が確認できます。Trema のコントローラアプリケーションを起動するには、run コマンドの後ろにCもしくは Ruby で作成したコントローラアプリケーションのパスを指定します。
1 |
$ trema run |
今回は仮想ネットワークの動作を確認するだけなので、用意されているサンプルプログラムを実行します。ここではサンプルプログラム dumper (src/examples/dumper/dumper.rb) を使用します。dumper は、コントローラに通知される各種イベントに対して、イベントとイベントに付随するメタデータ (メッセージデータ) の中身を標準出力に書き出すコントローラアプリケーションです。
1 2 |
gakusei@debian:~$ cd file/trema gakusei@debian:~/file/trema$ sudo ./trema run src/examples/dumper/dumper.rb -c src/examples/dumper/dumper.conf |
仮想ネットワークを使用するには -c オプションに対して、Trema 仮想ネットワーク用の設定ファイルのパスを渡します。サンプルプログラム dumper と同じディレクトリに、仮想ネットワーク用の設定ファイル (dumper.conf) が置かれているのでこれを指定して Trema を起動します。尚、仮想ネットワークを使用する場合、TAP デバイスを作成するため root 権限を要求してくるため、root 権限を与えてやります。
ここで、仮想ネットワークの設定ファイルの中身を見てみます。
1 2 3 4 5 6 7 8 9 10 11 |
ohyama@gree:~/trema$ cd ohyama@gree:~$ cd trema ohyama@gree:~/trema$ nl -b a src/examples/dumper/dumper.conf 1 vswitch("dumper") { datapath_id "0xabc" } 2 3 vhost("host1") 4 vhost("host2") 5 6 link "dumper", "host1" 7 link "dumper", "host2" ohyama@gree:~/trema$ |
ここでは、二つの仮想ホストが一つの仮想スイッチに接続する仮想ネットワークを作る設定をしています。また見ての通り、Trema 仮想ネットワークの設定ファイルは Ruby の内部 DSL で記述されます。
dumper.conf には、vswitch, vhost, link の 3 種類のメソッドコールが記述されており、それぞれが「仮想スイッチ」「仮想ホスト」「仮想リンク」の定義になります。vswitch と vhost の引数でそれぞれ仮想スイッチ、仮想ホストにラベルをつけてやれます。またブロックで内部で、それぞれのメタ情報を設定してやることもできます。
vswitch では、OpenFlow スイッチを識別する 64 ビットの識別子 ( datapath_id ) を設定しています。サンプルでは何も指定していませんが、vhost に対して仮想ホストの IP アドレス、MAC アドレスを以下のように指定してやる事もできます。
1 2 3 4 5 6 7 8 9 10 11 |
vhost("host1") { ip "192.168.0.1" netmask "255.255.255.0" mac "00:00:00:00:00:01" } vhost("host2") { ip "192.168.0.2" netmask "255.255.255.0" mac "00:00:00:00:00:02" } |
作成した仮想ネットワークに対してトラフィックを流してやるには、trema コマンドのサブコマンドの一つ send_packets コマンドを使用します。Trema のリポジトリで "trema send_packets --help" と実行すると、send_packets コマンドの使用法を教えてくれます。
それでは試しに host1 からトラフィックを出して見たいと思います。trema を起動したのとは別のコンソールから、以下コマンドを実行してみてください。以下のコマンドで dumper.conf で定義した host1 から host2 に対して UDP パケットが送られます。
1 |
$ ./trema send_packets --source host1 --dest host2 --n_pkts 100 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
ohyama@gree:~$ /sbin/ifconfig eth0 Link encap:Ethernet HWaddr 08:00:27:ad:b9:3d inet addr:192.168.122.10 Bcast:192.168.122.255 Mask:255.255.255.0 inet6 addr: fe80::200:ff:fe10:1011/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:684 errors:0 dropped:0 overruns:0 frame:0 TX packets:437 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:72307 (70.6 KiB) TX bytes:77933 (76.1 KiB) Interrupt:11 Base address:0xe000 lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host UP LOOPBACK RUNNING MTU:16436 Metric:1 RX packets:65 errors:0 dropped:0 overruns:0 frame:0 TX packets:65 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:4600 (4.4 KiB) TX bytes:4600 (4.4 KiB) trema0-0 Link encap:Ethernet HWaddr 92:59:85:36:e0:9f UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) trema0-1 Link encap:Ethernet HWaddr e2:1a:1e:1e:7d:3f UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:100000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) trema1-0 Link encap:Ethernet HWaddr ee:23:3e:37:0e:14 UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) trema1-1 Link encap:Ethernet HWaddr f6:fb:1c:c8:ae:22 UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:100000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) ohyama@gree:~$ |
ここで、仮想スイッチと仮想ホストの実体 (プロセス) を ps コマンドで見て行きましょう。trema 関連のプロセスを確認できます。
1 2 3 4 5 6 7 8 9 |
ohyama@gree:~$ ps alx | head -1 && ps alx | grep trema F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND 0 0 1847 1232 20 0 25384 22800 ? S+ pts/0 0:02 ruby ././bin/trema run src/examples/dumper/dumper.rb -c src/examples/dumper/dumper.conf 5 0 1856 1 20 0 3956 1888 ? Ss ? 0:00 /home/gakusei/trema/objects/switch_manager/switch_manager --daemonize --port=6653 -- port_status::Dumper packet_in::Dumper state_notify::Dumper vendor::Dumper 5 0 1933 1 20 0 2600 744 ? Ss ? 2:59 /home/gakusei/trema/objects/phost/phost -i trema0-1 -p /home/gakusei/trema/tmp/pid -l /home/gakusei/trema/tmp/log -n host1 -D 5 0 1938 1 20 0 2600 744 ? Ss ? 2:59 /home/gakusei/trema/objects/phost/phost -i trema1-1 -p /home/gakusei/trema/tmp/pid -l /home/gakusei/trema/tmp/log -n host2 -D 5 0 1943 1 20 0 4268 1140 ? Ss ? 0:02 /home/gakusei/trema/objects/openvswitch/bin/ovs-openflowd --detach --out-of-band --fail=closed --inactivity-probe=180 --rate-limit=40000 --burst-limit=20000 --pidfile=/home/gakusei/trema/tmp/pid/open_vswitch.dumper.pid --verbose=ANY:file:dbg --verbose=ANY:console:err --log-file=/home/gakusei/trema/tmp/log/openflowd.dumper.log --datapath-id=0000000000000abc --unixctl=/home/gakusei/trema/tmp/sock/ovs-openflowd.dumper.ctl --ports=trema0-0,trema1-0 netdev@vsw_0xabc tcp:127.0.0.1:6653 0 1000 1961 1415 20 0 2844 676 - S+ pts/3 0:00 grep trema ohyama@gree:~$ |
Trema デバッグツール : Tremashark
仮想ネットワークについてかなり長々書いてしまいましたが、ここで Trema が提供するデバッグツール Tremashark について見てゆきます。 Tremashark のインストール方法については、@stereocat さんのブログ でハマり所とその対処も含め、詳しく解説してくれています。 Tremashark は、コントローラに到着する OpenFlow メッセージを解析し、Wireshark に出力する Trema のプラグインになります。Wireshark で読み込ませる為に、OpenFlow メッセージを一旦 libpcap ファイルフォーマットに変換するという処理を行なっています。また、OpenFlow メッセージだけではなく、コントローラが吐き出すログ出力も syslog 経由で同時に表示できる機構になっているみたいです︵http://www.slideshare.net/chibayasunobu/tremashark︶。 Tremashark の便利な所は、物理ネットワーク上の何処で何が、何処に対して、何を行ったのをがひと目で把握できるところにあります。 trema の run コマンドに -s オプションを指定することによって、Tremashark プラグインを有効化できます。以下が実行例になります。
1 |
$ ./trema run ./src/examples/learning_switch/learning-switch.rb -c ./src/examples/learning_switch/learning_switch.conf -s |
Trema::Pio について
最後にトレンディな話題として、地味ながら Trema で話題になっている Trema Pio について紹介したいと思います。 コントローラを開発していると、コントロールプレーンでパケットを生成してデータプレーンに流したいといった要求が出てきます。例えば、コントローラ側からデータプレーンのトポロジーを知りたいといった場合や、ネットワークにどんなIPを持ったホストが接続されているかを一定周期で知りたいといった場合、更にはホストの疎通を確認したい場合などなど。これらに対してこれまでは、Cで Ruby の拡張ライブラリを書く、あるいは以下のように気合で Ethernet フレームを作って送り出すといった荒業を繰り出すといった事をしてかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
LONG_MAX = ( 1 << 64 ) - 1 INT_MAX = ( 1 << 32 ) - 1 SHORT_MAX = ( 1 << 16 ) - 1 def header dpid, portnum, type sprintf( "%016x", dpid & LONG_MAX ) + sprintf( "%08x", portnum & INT_MAX ) + sprintf( "%04x", type & SHORT_MAX ) end def padding size '00' * size end def send_ether_flame dpid, port, type send_packet_out dpid, :actions => ActionOutput.new( :port => OFPP_FLOOD ), :data => [ header( dpid, port, type ) + create_payload( 50 ) ].pack( "H*" ) end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def get_dpid macsa, macda ( macda.to_s.split(':') + macsa.to_s.split(':') )[0..7].join.hex end def get_portnum macsa, macda ( macda.to_s.split(':') + macsa.to_s.split(':') )[8..11].join.hex end def packet_in dpid, msg if msg.eth_type == CONTROLLER_DEFINED_TYPE from_dpid = get_dpid( msg.macsa, msg.macda ) from_port = get_portnum( msg.macsa, msg.macda ) #...some processing... else #...some other processing... end end |
ここでは、packet_in オブジェクトの送信元と宛先アドレスフィールドをそれぞれマージしてバイナリに変換し、先頭 64 bit だけ取り出して、数値に戻すという処理をしなければなりません。メンテナンス性を著しく害した非常にダメな感じのやり方です。
加えて、こうしたオレオレなトラフィックをデータプレーンに流して、既存のネットワーク機器に入ったりなんかしちゃと、ビックリさせてエラーとか吐かせちゃうかもしれないので、ここは LLDP などの既存のプロトコルのパケットを生成し、また取得したパケットを解析できるようにしたいです。
そこで役に立つのが Trema::Pio です。例えば先程のコードを LLDP パケットを送り出すモノに書き換えたいと思います。 Trema Pio を使うと以下のようにしてできます。
1 2 3 4 5 6 7 |
require 'pio' def send_ether_flame dpid, port send_packet_out dpid, :actions => ActionOutput.new( :port => OFPP_FLOOD ), :data => Pio::Lldp.new( :dpid => dpid, :port_number => port ).to_binary end |
邪魔なオレオレコードを無くすことができました。受信側のパース処理においても同様です。
1 2 3 4 5 6 7 8 9 10 11 |
require 'pio' def packet_in dpid, msg if msg.lldp? lldp = Pio::Lldp.read msg.data #...some processing... else #...some other processing... end end |