Open vSwitchとRyuを使った最も単純なOpenFlowネットワークの構築


はじめに

この記事はドワンゴ Advent Calendar 2014の22日目の記事です。22日目の今回はOpen vSwitchとRyuを使ったOpenFlowネットワークの構築方法を紹介します。
今回の記事、ドワンゴ Advent Calendarですが、私の現在や過去の業務に密接にOpenFlowが結びついていたわけでもなく、本当に単なる私の趣味によるものであるということを最初に書いておきます。

OpenFlowとは

SDN(Software Defined Network)という単語を耳にする機会が最近多くなってきたとは感じないでしょうか。SDNというのは今までネットワーク機器が行っていたパケットの経路の制御をソフトウェアで行い、柔軟に制御できるようにすることです。そのソフトウェアはプログラミングすることができ、ネットワークを流れるデータの特性に合わせた制御も可能になります。そのSDNを実現するための技術として注目をされているのがOpenFlowです。

OpenFlowは、ネットワーク機器の制御を行うためのプロトコルでOpen Networking Foundationが中心となり策定を進めています。OpenFlowスイッチ、OpenFlowコントローラー、そしてそれらのやりとりに使われるOpenFlowプロトコルによって構成されています。

  • OpenFlowスイッチ
    • PCや他のネットワーク機器とつながるスイッチ。一般的なスイッチとは異なり、OpenFlowコントローラーにパケットの制御方法を問い合わせし様々な動作をする。一般的にはハードウェアにより提供されるが、Open vSwitchなどのソフトウェア実装もある。
  • OpenFlowコントローラー
    • OpenFlowスイッチよりパケットの情報を受け取り、経路を決定するソフトウェア。

OpenFlowの基本的な動きは「OpenFlowスイッチに流れてきたパケット情報をOpenFlowコントローラーに問い合わせ、パケットの内容によって、パケットを制御をする」という流れです。パケットの内容で確認し判定条件として利用できる部分はヘッダフィールドと呼ばれ下記のようなものがあります。

  • スイッチの入力ポート
  • 送信元MACアドレス
  • 宛先MACアドレス
  • Ethernetタイプ
  • VLAN ID
  • VLAN 優先度
  • 送信元IPアドレス
  • 宛先IPアドレス
  • IPプロトコル番号
  • ToSビット
  • 送信元ポート番号
  • 宛先ポート番号

ヘッダフィールドを条件として、下記操作を1つもしくは複数指示することができます。

  • Forward
    • パケットを指定ポートへ転送する
  • Enqueue
    • 指定のキューに入れる
  • Drop
    • パケットを破棄
  • Modify-Field
    • 指定のフィールドを書き換え

これらの条件判定をパケットが送られてくるごとにされることなく、1度問い合わせた内容はOpenFlowスイッチのフローテーブルという場所に格納されます。フローテーブルに無い条件のパケットのみがOpenFlowコントローラーに問い合わせが行われます。

これがOpenFlowの動きのすべてで、OpenFlowの細かい動きはすべてOpenFlowコントローラー内で動作するプログラムで定義することができます。

OpenFlowを利用したネットワークを構築するには

必要になるのはOpenFlowコントローラーとOpenFlowスイッチです。OpenFlowコントローラーはオープンソースで提供されているものもありますが、OpenFlowスイッチはハードウェアで提供されているもの(NEC UNIVERGE PFシリーズなど)を揃えようとすると手軽に手を出せる価格のものはありませんし、個人で購入するのも難しいでしょう。

そこでOpenFlowスイッチをソフトウェアとして実装されているものを利用するのが現実的です。今回はOpenFlowスイッチの代表的な実装のOpen vSwitch、また、OpenFlowコントローラーはPythonでコントローラーを実装することができるRyuというフレームワークを利用しOpenFlowネットワークを構築する方法を紹介します。

今回行うこと

今回はOpenFlowスイッチとなるマシンとOpenFlowコントローラーを同一のマシンに用意します。さらに仮想マシンだけがアクセスできる内部ネットワークに、内部ネットワークのみにつながっているマシンを用意し、ホストマシンから内部ネットワークのみにつながっているマシンに対してOpenFlowスイッチを経由してアクセスできるようにします。それぞれ仮想マシンはCentOS 6.5、ホストマシンはMac OS X 10.10を利用しました。

dwango_adventcalendar2014_1

各マシンの準備

OpenFlowコントローラーおよびOpenFlowスイッチの役割をするマシンを以後ofnw01と呼び、内部ネットワーク内の仮想マシンをinternal01と呼びます。構成ですが、2台のマシンに共通している部分は下記のとおりです。

  • OS
    • CentOS 6.6 x86_64 minimal
  • メモリ
    • 1GB
  • HDD
    • 20GB
  • CPUコア数
    • 1

2台のマシンで異なるのはNICの構成です。

  • ofnw01
    • NIC1 : ホストオンリーネットワーク、プロミスキャスモードをすべて許可
    • NIC2:内部ネットワーク、プロミスキャスモードをすべて許可
    • NIC3:NAT(インターネットに接続するため)
  • internal01
    • NIC1:内部ネットワーク
    • NIC2:NAT(インターネットに接続するため)

NATのNICについてはOSインストール後にDHCPでIPアドレスを取得するようにし、インターネットに接続できるようにしておきます。2つのマシンが用意できたら下記コマンドを実行し、OSを最新の状態にアップデートしておきます。

# yum update
# reboot

OpenFlowスイッチの準備

ofnw01にまずOpenFlowスイッチとして動作するOpen vSwitchをインストールします。今回は最も手軽に導入することができる方法としてRedHat系OS向けのOpenStackリポジトリであるRDOを利用します。

# yum install http://rdo.fedorapeople.org/openstack-icehouse/rdo-release-icehouse.rpm
# yum  install openvswitch
# service openvswitch start
# chkconfig openvswitch on

上記のコマンドを実行するだけで導入することができ、非常に手軽です。執筆時点では2.1.3がインストールされました。

#  ovs-vsctl show
6d7c9a72-02a7-41cb-abb8-f11bccad3e62
ovs_version: 
"2.1.3"

次にOpen vSwitchでブリッジの設定を行います。このとき内部ネットワークとホストオンリーネットワークで利用するNICにはIPアドレスが振られていないことを確認したうえで作業を行います。下記コマンドでbr0ブリッジを新規作成し、eth0とeth1をbr0ブリッジにポートとして登録します。

# ovs-vsctl add-br br0
# ovs-vsctl add-port br0 eth0
# ovs-vsctl add-port br0 eth1

図にすると下記のようなイメージになります。

dwango_adventcalendar2014_2

この状態でbr0はスイッチングハブとして動作していますので、内部ネットワークとホストオンリーネットワークは疎通している状態になっています。それを確認するために、internal01のeth0に対してホストオンリーネットワーク内で利用できるIPアドレスを付与し、ホストマシンと疎通が取れるか確認します。

internal01の/etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=static
HWADDR=08:00:27:40:52:0D
IPADDR=172.16.0.200
NETMASK=255.255.255.0

上記設定を確認し、ホストマシンからinternal01へpingをしてみます。

$ ping 172.16.0.200
PING 172.16.0.200 (172.16.0.200): 56 data bytes
64 bytes from 172.16.0.200: icmp_seq=0 ttl=64 time=2.424 ms
64 bytes from 172.16.0.200: icmp_seq=1 ttl=64 time=2.436 ms
64 bytes from 172.16.0.200: icmp_seq=2 ttl=64 time=3.052 ms
64 bytes from 172.16.0.200: icmp_seq=3 ttl=64 time=0.877 ms

試しにofnw01のopenvswitchを止めてみると疎通しなくなるのがわかると思います。

これでOpenFlowスイッチとして利用するブリッジの準備は完了です。

OpenFlowコントローラーの準備

次にOpenFlowコントローラーを準備します。今回はOpenFlowスイッチを構築したofnw01上でOpenFlowコントローラーを動作させます。今回利用するOpenFlowコントローラーはRyu(http://osrg.github.io/ryu/)を利用します。RyuはPythonでOpenFlowコントローラーを実装することができるフレームワークです。まずはRyuを利用するためにPythonのパッケージマネージャーであるpipのインストール後にRyuのインストールを行います。

まずpipを利用するためにepelリポジトリを有効にします。

# yum install epel-release

次にpipとRyuのビルドに必要なものをインストールし、最後にpipを使いRyuをインストールします。

# yum install python-pip python-devel gcc gcc-c++
# pip install ryu
# pip install --upgrade netaddr

これでRyuを利用する環境の構築は完了です。

次にRyuでスイッチングハブの役割およびトラフィックモニターを行うこちら(http://osrg.github.io/ryu-book/ja/html/traffic_monitor.html)のサンプルを利用します。このサンプルをほとんど利用しているのですが、少し修正をいれてOpenFlowスイッチがOpenFlowコントローラーに接続したときにログ出力を行うようにしたのが下記のコードです。

from operator import attrgetter
from ryu.app import simple_switch_13
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, DEAD_DISPATCHER, CONFIG_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.lib import hub
 
class SimpleMonitor(simple_switch_13.SimpleSwitch13):
    def __init__(self, *args, **kwargs):
        super(SimpleMonitor, self).__init__(*args, **kwargs)
        self.datapaths = {}
        self.monitor_thread = hub.spawn(self._monitor)
 
    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
        def switch_features_handler(self, ev):
        self.logger.info('switch joind: datapath: %016x' % ev.msg.datapath.id)
 
    @set_ev_cls(ofp_event.EventOFPStateChange,
                [MAIN_DISPATCHER, DEAD_DISPATCHER])
    def _state_change_handler(self, ev):
        datapath = ev.datapath
        if ev.state == MAIN_DISPATCHER:
            if not datapath.id in self.datapaths:
                self.logger.debug('register datapath: %016x', datapath.id)
                self.datapaths[datapath.id] = datapath
        elif ev.state == DEAD_DISPATCHER:
            if datapath.id in self.datapaths:
                self.logger.debug('unregister datapath: %016x', datapath.id)
                del self.datapaths[datapath.id]
 
 
    def _monitor(self):
        while True:
            for dp in self.datapaths.values():
                self._request_stats(dp)
            hub.sleep(10)
 
 
    def _request_stats(self, datapath):
        self.logger.debug('send stats request: %016x', datapath.id)
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        req = parser.OFPFlowStatsRequest(datapath)
        datapath.send_msg(req)
        req = parser.OFPPortStatsRequest(datapath, 0, ofproto.OFPP_ANY)
        datapath.send_msg(req)
 
 
    @set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
    def _flow_stats_reply_handler(self, ev):
        body = ev.msg.body
        self.logger.info('datapath         '
                         'in-port  eth-dst           '
                         'out-port packets  bytes')
        self.logger.info('---------------- '
                         '-------- ----------------- '
                         '-------- -------- --------')
        for stat in sorted([flow for flow in body if flow.priority == 1],
                           key=lambda flow: (flow.match['in_port'],
                                             flow.match['eth_dst'])):
            self.logger.info('%016x %8x %17s %8x %8d %8d',
                             ev.msg.datapath.id,
                             stat.match['in_port'], stat.match['eth_dst'],
                             stat.instructions[0].actions[0].port,
                             stat.packet_count, stat.byte_count)
 
 
    @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
    def _port_stats_reply_handler(self, ev):
        body = ev.msg.body
        self.logger.info('datapath         port     '
                         'rx-pkts  rx-bytes rx-error '
                         'tx-pkts  tx-bytes tx-error')
        self.logger.info('---------------- -------- '
                         '-------- -------- -------- '
                         '-------- -------- --------')
        for stat in sorted(body, key=attrgetter('port_no')):
            self.logger.info('%016x %8x %8d %8d %8d %8d %8d %8d',
                             ev.msg.datapath.id, stat.port_no,
                             stat.rx_packets, stat.rx_bytes, stat.rx_errors,
                             stat.tx_packets, stat.tx_bytes, stat.tx_errors

上記のsimple_monitor.pyの準備ができたらOpenFlowコントローラーを起動します。

# ryu-manager simple_monitor.py
loading app simple_monitor.py
loading app ryu.controller.ofp_handler
instantiating app simple_monitor.py of SimpleMonitor
instantiating app ryu.controller.ofp_handler of OFPHandler

上記のように表示されます。これでOpenFlowコントローラーは起動している状態です。

次にOpen vSwitchで作ったbr0をOpenFlowスイッチとして動作させ、上記のOpenFlowコントローラーと接続します。

# ovs-vsctl set bridge br0 protocols=OpenFlow13
# ovs-vsctl set bridge br0 other-config:datapath-id=0000000000000001
# ovs-vsctl set-controller br0 tcp:127.0.0.1

上記ではまずbr0をOpenFlow1.3で動作させ、datapath-idというOpenFlowスイッチに対する識別子を1に定義、そして最後に127.0.0.1のOpenFlowコントローラーへ接続しています。接続を行うとOpenFlowコントローラー(simple_monitor.py)側で下記のように出力され、OpenFlowスイッチが接続されたことが確認できます。

switch joind: datapath: 0000000000000001
packet in 1 08:00:27:6b:7d:b4 0a:00:27:00:00:01 4294967294
packet in 1 0a:00:27:00:00:01 08:00:27:6b:7d:b4 2
packet in 1 08:00:27:6b:7d:b4 0a:00:27:00:00:01 4294967294
packet in 1 08:00:27:6b:7d:b4 0a:00:27:00:00:01 4294967294
datapath         in-port  eth-dst           out-port packets  bytes
---------------- -------- ----------------- -------- -------- --------
0000000000000001        2 08:00:27:6b:7d:b4 fffffffe        6      408
0000000000000001 fffffffe 0a:00:27:00:00:01        2        5      778
datapath         port     rx-pkts  rx-bytes rx-error tx-pkts  tx-bytes tx-error
---------------- -------- -------- -------- -------- -------- -------- --------
0000000000000001        1        0        0        0       11     1410        0
0000000000000001        2      566    55462        0      315    45936        0
0000000000000001 fffffffe      326    46702        0      566    55462        0

念のためこの状態でホストからinternal01へも問題なく疎通することを確認します。

$ ping 172.16.0.200
PING 172.16.0.200 (172.16.0.200): 56 data bytes
64 bytes from 172.16.0.200: icmp_seq=0 ttl=64 time=1.616 ms
64 bytes from 172.16.0.200: icmp_seq=1 ttl=64 time=0.913 ms
64 bytes from 172.16.0.200: icmp_seq=2 ttl=64 time=0.687 ms
64 bytes from 172.16.0.200: icmp_seq=3 ttl=64 time=0.746 ms

動作としては従来のスイッチングハブと何も変わらないのでなんとも言えない気持ちになりますが、この疎通はOpenFlowプロトコルに基づいた処理によって疎通しています。例えば、simple_monitor.pyではトラフィック情報を定期的に表示していますが、OpenFlowではトラフィック情報の取得を行うプロトコルが予め規定されているため、このようなことも簡単に実現することが可能です。これを利用すれば、トラフィックの統計情報を見て定期的に経路を変更するといった動きも可能です。

見苦しい言い訳

ここまで読んでいただいた方の中で「OpenFlowスイッチの代わりにLinuxが動くマシンが必要なんて、消費電力も高いしポートを増やすのにNICをどんどん追加しなければならないじゃないか。全然手軽じゃない。」と思われた方もいると思います。私もそう思います。

実は「BUFFALOの安い無線LANルーターでLinuxを動かし、そこでOpen vSwitchを使いOpenFlowスイッチとして動作させる、そしてOpenFlowコントローラーはRaspberryPi上で動かす」ということを当初やりたかったのですが、単純に間に合いませんでした…。(Open vSwitchは入ったし、OpenFlow Controllerとの連携もできたんですが、ポート間が疎通しなかったんです…) このあたりに関しては後日ちゃんと書かせていただきたい所存です。これが実現できればOpenFlowスイッチが安価、低消費電力、現実的なポート数での運用が可能です。また、今回これを読んでOpenFlowである必然性やうまみを感じ取れた方は少ないと思います。OpenFlowを利用するうまみは柔軟な経路制御であると思っているので、OpenFlowスイッチとOpenFlowコントローラーがひとつの構成ではどうしてもただ複雑になっているという印象が強くなるかと思います。

言い訳をたくさん書きましたが、今後は下記のようなことの実現を目指していきたいと思っています。

  • BUFFALOの安い無線LANルーターでのOpenFlowスイッチの構築
  • OpenFlowコントローラーによるトラフィック統計情報を利用した経路制御
  • OpenFlowコントローラーによるLoad Balancerの実装
  • OpenFlowコントローラーおよびOpenFlowスイッチの冗長化
  • OpenFlowコントローラープログラムのテスト

とりあえずBUFFALOのルーターは4台も買ってしまったので、まずはOpenFlowスイッチ化を冬休みの宿題にします。

おわりに

今回はVirtualBox上に構築したOpenFlowスイッチとOpenFlowコントローラーを使いOpenFlowの基本的な動作について紹介をしました。SDNやOpenFlowと聞いて何か複雑な仕組みではないだろうかと思っていた方がこれを読んで動きは非常に単純であるということと、検証する環境もそれほど時間がかからず構築できるということを体験していただけたのであれば、それ以上にありがたいことはありません。

You may also like...