スイッチングハブ

本章では、簡単なスイッチングハブの実装を題材として、Ryuによるアプリケーションの実装方法を解説していきます。

スイッチングハブ

世の中には様々な機能を持つスイッチングハブがありますが、ここでは次のような単純な機能を持ったスイッチングハブの実装を見てみます。

  • ポートに接続されているホストのMACアドレスを学習し、MACアドレステーブルに保持する
  • 学習済みのホスト宛のパケットを受信したら、ホストの接続されているポートに転送する
  • 未知のホスト宛のパケットを受信したら、フラッディングする

このようなスイッチをRyuを使って実現してみましょう。

OpenFlowによるスイッチングハブ

OpenFlowスイッチは、Ryuの様なOpenFlowコントローラからの指示を受けて、次のようなことを行えます。

  • 受信したパケットのアドレスを書き換えたり、指定のポートから転送
  • 受信したパケットをコントローラへ転送(Packet-In)
  • コントローラから転送されたパケットを指定のポートから転送(Packet-Out)

これらの機能を組み合わせ、スイッチングハブを実現することが出来ます。

まずは、Packet-Inの機能を利用したMACアドレスの学習です。コントローラは、Packet-Inの機能を利用し、スイッチからパケットを受け取る事が出来ます。受け取ったパケットを解析し、ホストのMACアドレスや接続されているポートの情報を学習することが出来ます。

学習の後は受信したパケットの転送です。パケットの宛先MACアドレスが学習済みのホストのものか検索します。検索結果によって次の処理を実行します。

  • 学習済みのホストの場合…Packet-Outの機能で、接続先のポートからパケットを転送
  • 未知のホストの場合…Packet-Outの機能でパケットをフラッディング

これらの動作を順を追って図とともに説明します。

  1. 初期状態

    フローテーブルが空の初期状態です。

    ポート1にホストA、ポート4にホストB、ポート3にホストCが接続されているものとします。

    _images/fig19.png
  2. ホストA→ホストB

    ホストAからホストBへのパケットが送信されると、Packet-Inメッセージが送られ、ホストAのMACアドレスがポート1に学習されます。ホストBのポートはまだ分かっていないため、パケットはフラッディングされ、パケットはホストBとホストCで受信されます。

    _images/fig26.png

    Packet-In:

    in-port: 1
    eth-dst: ホストB
    eth-src: ホストA
    

    Packet-Out:

    action: OUTPUT:フラッディング
    
  3. ホストB→ホストA

    ホストBからホストAにパケットが返されると、フローテーブルにエントリを追加し、またパケットはポート1に転送されます。そのため、このパケットはホストCでは受信されません。

    _images/fig36.png

    Packet-In:

    in-port: 4
    eth-dst: ホストA
    eth-src: ホストB
    

    Packet-Out:

    action: OUTPUT:ポート1
    
  4. ホストA→ホストB

    再度、ホストAからホストBへのパケットが送信されると、フローテーブルにエントリを追加し、またパケットはポート4に転送されます。

    _images/fig45.png

    Packet-In:

    in-port: 1
    eth-dst: ホストB
    eth-src: ホストA
    

    Packet-Out:

    action: OUTPUT:ポート4
    

次に、実際にRyuを使って実装されたスイッチングハブのソースコードを見ていきます。

Ryuによるスイッチングハブの実装

スイッチングハブのソースコードは、Ryuのソースツリーにあります。

ryu/app/example_switch_13.py

OpenFlowのバージョンに応じて、他にもsimple_switch.py(OpenFlow 1.0)、simple_switch_12.py(OpenFlow 1.2)がありますが、ここではOpenFlow 1.3に対応した実装を見ていきます。

短いソースコードなので、全体をここに掲載します。

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet


class ExampleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(ExampleSwitch13, self).__init__(*args, **kwargs)
        # initialize mac address table.
        self.mac_to_port = {}

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # install the table-miss flow entry.
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0, match, actions)

    def add_flow(self, datapath, priority, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # construct flow_mod message and send it.
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]
        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # get Datapath ID to identify OpenFlow switches.
        dpid = datapath.id
        self.mac_to_port.setdefault(dpid, {})

        # analyse the received packets using the packet library.
        pkt = packet.Packet(msg.data)
        eth_pkt = pkt.get_protocol(ethernet.ethernet)
        dst = eth_pkt.dst
        src = eth_pkt.src

        # get the received port number from packet_in message.
        in_port = msg.match['in_port']

        self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

        # learn a mac address to avoid FLOOD next time.
        self.mac_to_port[dpid][src] = in_port

        # if the destination mac address is already learned,
        # decide which port to output the packet, otherwise FLOOD.
        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            out_port = ofproto.OFPP_FLOOD

        # construct action list.
        actions = [parser.OFPActionOutput(out_port)]

        # install a flow to avoid packet_in next time.
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            self.add_flow(datapath, 1, match, actions)

        # construct packet_out message and send it.
        out = parser.OFPPacketOut(datapath=datapath,
                                  buffer_id=ofproto.OFP_NO_BUFFER,
                                  in_port=in_port, actions=actions,
                                  data=msg.data)
        datapath.send_msg(out)

それでは、それぞれの実装内容について見ていきます。

クラスの定義と初期化

Ryuアプリケーションとして実装するため、ryu.base.app_manager.RyuAppを継承します。また、OpenFlow 1.3を使用するため、OFP_VERSIONSにOpenFlow 1.3のバージョンを指定しています。

また、MACアドレステーブル mac_to_port を定義しています。

OpenFlowプロトコルでは、OpenFlowスイッチとコントローラが通信を行うために必要となるハンドシェイクなどのいくつかの手順が決められていますが、Ryuのフレームワークが処理してくれるため、Ryuアプリケーションでは意識する必要はありません。

class ExampleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(ExampleSwitch13, self).__init__(*args, **kwargs)
        # initialize mac address table.
        self.mac_to_port = {}

# ...

イベントハンドラ

Ryuでは、OpenFlowメッセージを受信するとメッセージに対応したイベントが発生します。Ryuアプリケーションは、受け取りたいメッセージに対応したイベントハンドラを実装します。

イベントハンドラは、引数にイベントオブジェクトを持つ関数を定義し、ryu.controller.handler.set_ev_clsデコレータで修飾します。

set_ev_clsは、引数に受け取るメッセージに対応したイベントクラスとOpenFlowスイッチのステートを指定します。

イベントクラス名は、ryu.controller.ofp_event.EventOFP+ <OpenFlowメッセージ名>となっています。例えば、Packet-Inメッセージの場合は、EventOFPPacketInになります。詳しくは、RyuのドキュメントAPIリファレンスを参照してください。ステートには、以下のいずれか、またはリストを指定します。

定義 説明
ryu.controller.handler.HANDSHAKE_DISPATCHER HELLOメッセージの交換
ryu.controller.handler.CONFIG_DISPATCHER SwitchFeaturesメッセージの受信待ち
ryu.controller.handler.MAIN_DISPATCHER 通常状態
ryu.controller.handler.DEAD_DISPATCHER コネクションの切断

Table-missフローエントリの追加

OpenFlowスイッチとのハンドシェイク完了後にTable-missフローエントリをフローテーブルに追加し、Packet-Inメッセージを受信する準備を行います。

具体的には、Switch Features(Features Reply)メッセージを受け取り、そこでTable-missフローエントリの追加を行います。

@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
    datapath = ev.msg.datapath
    ofproto = datapath.ofproto
    parser = datapath.ofproto_parser

# ...

ev.msgには、イベントに対応するOpenFlowメッセージクラスのインスタンスが格納されています。この場合は、ryu.ofproto.ofproto_v1_3_parser.OFPSwitchFeaturesになります。

msg.datapathには、このメッセージを発行したOpenFlowスイッチに対応するryu.controller.controller.Datapathクラスのインスタンスが格納されています。

Datapathクラスは、OpenFlowスイッチとの実際の通信処理や受信メッセージに対応したイベントの発行などの重要な処理を行っています。

Ryuアプリケーションで利用する主な属性は以下のものです。

属性名 説明
id 接続しているOpenFlowスイッチのID(データパスID)です。
ofproto

使用しているOpenFlowバージョンに対応したofprotoモジュールを示します。OpenFlow 1.3 の場合は下記になります。

ryu.ofproto.ofproto_v1_3

ofproto_parser

ofprotoと同様に、ofproto_parserモジュールを示します。OpenFlow 1.3 の場合は下記になります。

ryu.ofproto.ofproto_v1_3_parser

Ryuアプリケーションで利用するDatapathクラスの主なメソッドは以下のものです。

send_msg(msg)

OpenFlowメッセージを送信します。msgは、送信OpenFlowメッセージに対応したryu.ofproto.ofproto_parser.MsgBaseのサブクラスです。

スイッチングハブでは、受信したSwitch Featuresメッセージ自体は特に使いません。Table-missフローエントリを追加するタイミングを得るためのイベントとして扱っています。

def switch_features_handler(self, ev):
# ...

    # install the table-miss flow entry.
    match = parser.OFPMatch()
    actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                      ofproto.OFPCML_NO_BUFFER)]
    self.add_flow(datapath, 0, match, actions)

Table-missフローエントリは、優先度が最低(0)で、すべてのパケットにマッチするエントリです。このエントリのインストラクションにコントローラポートへの出力アクションを指定することで、受信パケットが、すべての通常のフローエントリにマッチしなかった場合、Packet-Inを発行するようになります。

すべてのパケットにマッチさせるため、空のマッチを生成します。マッチはOFPMatchクラスで表されます。

次に、コントローラポートへ転送するためのOUTPUTアクションクラス(OFPActionOutput)のインスタンスを生成します。出力先にコントローラ、パケット全体をコントローラに送信するためにmax_lenにはOFPCML_NO_BUFFERを指定しています。

注釈

コントローラにはパケットの先頭部分(Ethernetヘッダー分)だけを送信させ、残りはスイッチにバッファーさせた方が効率の点では望ましいのですが、Open vSwitchのバグを回避するために、ここではパケット全体を送信させます。このバグはOpen vSwitch 2.1.0で修正されました。

最後に、優先度に0(最低)を指定してadd_flow()メソッドを実行してFlow Modメッセージを送信します。add_flow()メソッドの内容については後述します。

Packet-inメッセージ

未知の宛先の受信パケットを受け付けるため、Packet-Inイベントのハンドラを作成します。

@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
    msg = ev.msg
    datapath = msg.datapath
    ofproto = datapath.ofproto
    parser = datapath.ofproto_parser

# ...

OFPPacketInクラスのよく使われる属性には以下のようなものがあります。

属性名 説明
match ryu.ofproto.ofproto_v1_3_parser.OFPMatchクラスのインスタンスで、受信パケットのメタ情報が設定されています。
data 受信パケット自体を示すバイナリデータです。
total_len 受信パケットのデータ長です。
buffer_id 受信パケットがOpenFlowスイッチ上でバッファされている場合、そのIDが示されます。バッファされていない場合は、ryu.ofproto.ofproto_v1_3.OFP_NO_BUFFERがセットされます。

MACアドレステーブルの更新

def _packet_in_handler(self, ev):
# ...

    # get the received port number from packet_in message.
    in_port = msg.match['in_port']

    self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

    # learn a mac address to avoid FLOOD next time.
    self.mac_to_port[dpid][src] = in_port

# ...

OFPPacketInクラスのmatchから、受信ポート(in_port)を取得します。宛先MACアドレスと送信元MACアドレスは、Ryuのパケットライブラリを使って、受信パケットのEthernetヘッダから取得しています。

取得した送信元MACアドレスと受信ポート番号で、MACアドレステーブルを更新します。

複数のOpenFlowスイッチとの接続に対応するため、MACアドレステーブルはOpenFlowスイッチ毎に管理するようになっています。OpenFlowスイッチの識別にはデータパスIDを用いています。

転送先ポートの判定

宛先MACアドレスが、MACアドレステーブルに存在する場合は対応するポート番号を、見つからなかった場合はフラッディング(OFPP_FLOOD)を出力ポートに指定したOUTPUTアクションクラスのインスタンスを生成します。

def _packet_in_handler(self, ev):
# ...

    # if the destination mac address is already learned,
    # decide which port to output the packet, otherwise FLOOD.
    if dst in self.mac_to_port[dpid]:
        out_port = self.mac_to_port[dpid][dst]
    else:
        out_port = ofproto.OFPP_FLOOD

    # construct action list.
    actions = [parser.OFPActionOutput(out_port)]

    # install a flow to avoid packet_in next time.
    if out_port != ofproto.OFPP_FLOOD:
        match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
        self.add_flow(datapath, 1, match, actions)

# ...

宛先MACアドレスが見つかった場合は、OpenFlowスイッチのフローテーブルにエントリを追加します。

Table-missフローエントリの追加と同様に、マッチとアクションを指定してadd_flow()を実行し、フローエントリを追加します。

Table-missフローエントリとは違って、今回はマッチに条件を設定します。今回のスイッチングハブの実装では、受信ポート(in_port)と宛先MACアドレス(eth_dst)を指定しています。例えば、「ポート1で受信したホストB宛」のパケットが対象となります。

今回のフローエントリでは、優先度に1を指定しています。値が大きいほど優先度が高くなるので、ここで追加するフローエントリは、Table-missフローエントリより先に評価されるようになります。

前述のアクションを含めてまとめると、以下のようなエントリをフローテーブルに追加します。

ポート1で受信した、ホストB宛(宛先MACアドレスがB)のパケットを、ポート4に転送する

ヒント

OpenFlowでは、NORMALポートという論理的な出力ポートがオプションで規定されており、出力ポートにNORMALを指定すると、スイッチのL2/L3機能を使ってパケットを処理するようになります。つまり、すべてのパケットをNORMALポートに出力するように指示するだけで、スイッチングハブとして動作するようにできますが、ここでは各々の処理をOpenFlowを使って実現するものとします。

フローエントリの追加処理

Packet-Inハンドラの処理がまだ終わっていませんが、ここで一旦フローエントリを追加するメソッドの方を見ていきます。

def add_flow(self, datapath, priority, match, actions):
    ofproto = datapath.ofproto
    parser = datapath.ofproto_parser

    # construct flow_mod message and send it.
    inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                         actions)]
# ...

フローエントリには、対象となるパケットの条件を示すマッチと、そのパケットに対する操作を示すインストラクション、エントリの優先度、有効時間などを設定します。

スイッチングハブの実装では、インストラクションにApply Actionsを使用して、指定したアクションを直ちに適用するように設定しています。

最後に、Flow Modメッセージを発行してフローテーブルにエントリを追加します。

def add_flow(self, datapath, priority, match, actions):
# ...
    mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                            match=match, instructions=inst)
    datapath.send_msg(mod)

Flow Modメッセージに対応するクラスはOFPFlowModクラスです。OFPFlowModクラスのインスタンスを生成して、Datapath.send_msg() メソッドでOpenFlowスイッチにメッセージを送信します。

OFPFlowModクラスのコンストラクタには多くの引数がありますが、多くのものは大抵の場合、デフォルト値のままで済みます。かっこ内はデフォルト値です。

datapath

フローテーブルを操作する対象となるOpenFlowスイッチに対応するDatapathクラスのインスタンスです。通常は、Packet-Inメッセージなどのハンドラに渡されるイベントから取得したものを指定します。

cookie (0)

コントローラが指定する任意の値で、エントリの更新または削除を行う際のフィルタ条件として使用できます。パケットの処理では使用されません。

cookie_mask (0)

エントリの更新または削除の場合に、0以外の値を指定すると、エントリのcookie値による操作対象エントリのフィルタとして使用されます。

table_id (0)

操作対象のフローテーブルのテーブルIDを指定します。

command (ofproto_v1_3.OFPFC_ADD)

どのような操作を行うかを指定します。

説明
OFPFC_ADD 新しいフローエントリを追加します
OFPFC_MODIFY フローエントリを更新します
OFPFC_MODIFY_STRICT 厳格に一致するフローエントリを更新します
OFPFC_DELETE フローエントリを削除します
OFPFC_DELETE_STRICT 厳格に一致するフローエントリを削除します

idle_timeout (0)

このエントリの有効期限を秒単位で指定します。エントリが参照されずにidle_timeoutで指定した時間を過ぎた場合、そのエントリは削除されます。エントリが参照されると経過時間はリセットされます。

エントリが削除されるとFlow Removedメッセージがコントローラに通知されます。

hard_timeout (0)

このエントリの有効期限を秒単位で指定します。idle_timeoutと違って、hard_timeoutでは、エントリが参照されても経過時間はリセットされません。つまり、エントリの参照の有無に関わらず、指定された時間が経過するとエントリが削除されます。

idle_timeoutと同様に、エントリが削除されるとFlow Removedメッセージが通知されます。

priority (0)

このエントリの優先度を指定します。値が大きいほど、優先度も高くなります。

buffer_id (ofproto_v1_3.OFP_NO_BUFFER)

OpenFlowスイッチ上でバッファされたパケットのバッファIDを指定します。バッファIDはPacket-Inメッセージで通知されたものであり、指定するとOFPP_TABLEを出力ポートに指定したPacket-OutメッセージとFlow Modメッセージの2つのメッセージを送ったのと同じように処理されます。commandがOFPFC_DELETEまたはOFPFC_DELETE_STRICTの場合は無視されます。

バッファIDを指定しない場合は、OFP_NO_BUFFERをセットします。

out_port (0)

OFPFC_DELETEまたはOFPFC_DELETE_STRICTの場合に、対象となるエントリを出力ポートでフィルタします。OFPFC_ADD、OFPFC_MODIFY、OFPFC_MODIFY_STRICTの場合は無視されます。

出力ポートでのフィルタを無効にするには、OFPP_ANYを指定します。

out_group (0)

out_portと同様に、出力グループでフィルタします。

無効にするには、OFPG_ANYを指定します。

flags (0)

以下のフラグの組み合わせを指定することができます。

説明
OFPFF_SEND_FLOW_REM このエントリが削除された時に、コントローラにFlowRemovedメッセージを発行します。
OFPFF_CHECK_OVERLAP OFPFC_ADDの場合に、重複するエントリのチェックを行います。重複するエントリがあった場合にはFlow Modは失敗し、エラーが返されます。
OFPFF_RESET_COUNTS 該当エントリのパケットカウンタとバイトカウンタをリセットします。
OFPFF_NO_PKT_COUNTS このエントリのパケットカウンタを無効にします。
OFPFF_NO_BYT_COUNTS このエントリのバイトカウンタを無効にします。

match (None)

マッチを指定します。

instructions ([])

インストラクションのリストを指定します。

パケットの転送

Packet-Inハンドラに戻り、最後の処理の説明です。

宛先MACアドレスがMACアドレステーブルから見つかったかどうかに関わらず、最終的にはPacket-Outメッセージを発行して、受信パケットを転送します。

def _packet_in_handler(self, ev):
# ...

    # construct packet_out message and send it.
    out = parser.OFPPacketOut(datapath=datapath,
                              buffer_id=ofproto.OFP_NO_BUFFER,
                              in_port=in_port, actions=actions,
                              data=msg.data)
    datapath.send_msg(out)

Packet-Outメッセージに対応するクラスはOFPPacketOutクラスです。

OFPPacketOutのコンストラクタの引数は以下のようになっています。

datapath

OpenFlowスイッチに対応するDatapathクラスのインスタンスを指定します。

buffer_id

OpenFlowスイッチ上でバッファされたパケットのバッファIDを指定します。バッファを使用しない場合は、OFP_NO_BUFFERを指定します。

in_port

パケットを受信したポートを指定します。受信パケットでない場合は、OFPP_CONTROLLERを指定します。

actions

アクションのリストを指定します。

data

パケットのバイナリデータを指定します。buffer_idにOFP_NO_BUFFERが指定された場合に使用されます。OpenFlowスイッチのバッファを利用する場合は省略します。

スイッチングハブの実装では、buffer_idにPacket-Inメッセージのbuffer_idを指定しています。Packet-Inメッセージのbuffer_idが無効だった場合は、Packet-Inの受信パケットをdataに指定して、パケットを送信しています。

これで、スイッチングハブのソースコードの説明は終わりです。次は、このスイッチングハブを実行して、実際の動作を確認します。

Ryuアプリケーションの実行

Mininetからxtermを起動するため、mnコマンドによりMininet環境を起動します。

構築する環境は、ホスト3台、スイッチ1台のシンプルな構成です。

mnコマンドのパラメータは、以下のようになります。

パラメータ 説明
topo single,3 スイッチが1台、ホストが3台のトポロジ
mac なし 自動的にホストのMACアドレスをセットする
switch ovsk Open vSwitchを使用する
controller remote OpenFlowコントローラは外部のものを利用する
x なし xtermを起動する

実行例は以下のようになります。

$ sudo mn --topo single,3 --mac --switch ovsk --controller remote -x
*** Creating network
*** Adding controller
Unable to contact the remote controller at 127.0.0.1:6633
*** Adding hosts:
h1 h2 h3
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1) (h3, s1)
*** Configuring hosts
h1 h2 h3
*** Running terms on localhost:10.0
*** Starting controller
*** Starting 1 switches
s1
*** Starting CLI:
mininet>

実行するとデスクトップPC上でxtermが5つ起動します。それぞれ、ホスト1~3、スイッチ、コントローラに対応します。

スイッチのxtermからコマンドを実行して、使用するOpenFlowのバージョンをセットします。ウインドウタイトルが「switch: s1 (root)」となっているものがスイッチ用のxtermです。

まずはOpen vSwitchの状態を見てみます。

switch: s1:

# ovs-vsctl show
fdec0957-12b6-4417-9d02-847654e9cc1f
Bridge "s1"
    Controller "ptcp:6634"
    Controller "tcp:127.0.0.1:6633"
    fail_mode: secure
    Port "s1-eth3"
        Interface "s1-eth3"
    Port "s1-eth2"
        Interface "s1-eth2"
    Port "s1-eth1"
        Interface "s1-eth1"
    Port "s1"
        Interface "s1"
            type: internal
ovs_version: "1.11.0"
# ovs-dpctl show
system@ovs-system:
        lookups: hit:14 missed:14 lost:0
        flows: 0
        port 0: ovs-system (internal)
        port 1: s1 (internal)
        port 2: s1-eth1
        port 3: s1-eth2
        port 4: s1-eth3
#

スイッチ(ブリッジ)s1ができていて、ホストに対応するポートが3つ追加されています。

次にOpenFlowのバージョンとして1.3を設定します。

switch: s1:

# ovs-vsctl set Bridge s1 protocols=OpenFlow13
#

空のフローテーブルを確認してみます。

switch: s1:

# ovs-ofctl -O OpenFlow13 dump-flows s1
OFPST_FLOW reply (OF1.3) (xid=0x2):
#

ovs-ofctlコマンドには、オプションで使用するOpenFlowのバージョンを指定する必要があります。デフォルトはOpenFlow10です。

スイッチングハブの実行

準備が整ったので、Ryuアプリケーションを実行します。

ウインドウタイトルが「controller: c0 (root)」となっているxtermから次のコマンドを実行します。

controller: c0:

# ryu-manager --verbose ryu.app.example_switch_13
loading app ryu.app.example_switch_13
loading app ryu.controller.ofp_handler
instantiating app ryu.app.example_switch_13 of ExampleSwitch13
instantiating app ryu.controller.ofp_handler of OFPHandler
BRICK ExampleSwitch13
  CONSUMES EventOFPPacketIn
  CONSUMES EventOFPSwitchFeatures
BRICK ofp_event
  PROVIDES EventOFPPacketIn TO {'ExampleSwitch13': set(['main'])}
  PROVIDES EventOFPSwitchFeatures TO {'ExampleSwitch13': set(['config'])}
  CONSUMES EventOFPErrorMsg
  CONSUMES EventOFPHello
  CONSUMES EventOFPEchoRequest
  CONSUMES EventOFPEchoReply
  CONSUMES EventOFPPortStatus
  CONSUMES EventOFPSwitchFeatures
  CONSUMES EventOFPPortDescStatsReply
connected socket:<eventlet.greenio.base.GreenSocket object at 0x7f1239937a90> address:('127.0.0.1', 37898)
hello ev <ryu.controller.ofp_event.EventOFPHello object at 0x7f1239927d50>
move onto config mode
EVENT ofp_event->ExampleSwitch13 EventOFPSwitchFeatures
switch features ev version=0x4,msg_type=0x6,msg_len=0x20,xid=0xea43ed30,OFPSwitchFeatures(auxiliary_id=0,capabilities=79,datapath_id=1,n_buffers=256,n_tables=254)
move onto main mode

OVSとの接続に時間がかかる場合がありますが、少し待つと上のように

connected socket:<....
hello ev ...
...
move onto main mode

と表示されます。

これで、OVSと接続し、ハンドシェイクが行われ、Table-missフローエントリが追加され、Packet-Inを待っている状態になっています。

Table-missフローエントリが追加されていることを確認します。

switch: s1:

# ovs-ofctl -O openflow13 dump-flows s1
OFPST_FLOW reply (OF1.3) (xid=0x2):
 cookie=0x0, duration=105.975s, table=0, n_packets=0, n_bytes=0, priority=0 actions=CONTROLLER:65535
#

優先度が0で、マッチがなく、アクションにCONTROLLER、送信データサイズ65535(0xffff = OFPCML_NO_BUFFER)が指定されています。

動作の確認

ホスト1からホスト2へpingを実行します。

  1. ARP request

    この時点では、ホスト1はホスト2のMACアドレスを知らないので、ICMP echorequestに先んじてARP requestをブロードキャストするはずです。このブロードキャストパケットはホスト2とホスト3で受信されます。

  2. ARP reply

    ホスト2がARPに応答して、ホスト1にARP replyを返します。

  3. ICMP echo request

    これでホスト1はホスト2のMACアドレスを知ることができたので、echo requestをホスト2に送信します。

  4. ICMP echo reply

    ホスト2はホスト1のMACアドレスを既に知っているので、echo replyをホスト1に返します。

このような通信が行われるはずです。

pingコマンドを実行する前に、各ホストでどのようなパケットを受信したかを確認できるようにtcpdumpコマンドを実行しておきます。

host: h1:

# tcpdump -en -i h1-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h1-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes

host: h2:

# tcpdump -en -i h2-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h2-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes

host: h3:

# tcpdump -en -i h3-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h3-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes

それでは、最初にmnコマンドを実行したコンソールで、次のコマンドを実行してホスト1からホスト2へpingを発行します。

mininet> h1 ping -c1 h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_req=1 ttl=64 time=97.5 ms

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

ICMP echo replyは正常に返ってきました。

まずはフローテーブルを確認してみましょう。

switch: s1:

# ovs-ofctl -O openflow13 dump-flows s1
OFPST_FLOW reply (OF1.3) (xid=0x2):
 cookie=0x0, duration=417.838s, table=0, n_packets=3, n_bytes=182, priority=0 actions=CONTROLLER:65535
 cookie=0x0, duration=48.444s, table=0, n_packets=2, n_bytes=140, priority=1,in_port=2,dl_dst=00:00:00:00:00:01 actions=output:1
 cookie=0x0, duration=48.402s, table=0, n_packets=1, n_bytes=42, priority=1,in_port=1,dl_dst=00:00:00:00:00:02 actions=output:2
#

Table-missフローエントリ以外に、優先度が1のフローエントリが2つ登録されています。

  1. 受信ポート(in_port):2, 宛先MACアドレス(dl_dst):ホスト1 →動作(actions):ポート1に転送
  2. 受信ポート(in_port):1, 宛先MACアドレス(dl_dst):ホスト2 →動作(actions):ポート2に転送

(1)のエントリは2回参照され(n_packets)、(2)のエントリは1回参照されています。(1)はホスト2からホスト1宛の通信なので、ARP replyとICMP echo replyの2つがマッチしたものでしょう。(2)はホスト1からホスト2宛の通信で、ARP requestはブロードキャストされるので、これはICMP echo requestによるもののはずです。

それでは、example_switch_13のログ出力を見てみます。

controller: c0:

EVENT ofp_event->ExampleSwitch13 EventOFPPacketIn
packet in 1 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 1
EVENT ofp_event->ExampleSwitch13 EventOFPPacketIn
packet in 1 00:00:00:00:00:02 00:00:00:00:00:01 2
EVENT ofp_event->ExampleSwitch13 EventOFPPacketIn
packet in 1 00:00:00:00:00:01 00:00:00:00:00:02 1

1つ目のPacket-Inは、ホスト1が発行したARP requestで、ブロードキャストなのでフローエントリは登録されず、Packet-Outのみが発行されます。

2つ目は、ホスト2から返されたARP replyで、宛先MACアドレスがホスト1となっているので前述のフローエントリ(1)が登録されます。

3つ目は、ホスト1からホスト2へ送信されたICMP echo requestで、フローエントリ(2)が登録されます。

ホスト2からホスト1に返されたICMP echo replyは、登録済みのフローエントリ(1)にマッチするため、Packet-Inは発行されずにホスト1へ転送されます。

最後に各ホストで実行したtcpdumpの出力を見てみます。

host: h1:

# tcpdump -en -i h1-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h1-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
20:38:04.625473 00:00:00:00:00:01 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 10.0.0.2 tell 10.0.0.1, length 28
20:38:04.678698 00:00:00:00:00:02 > 00:00:00:00:00:01, ethertype ARP (0x0806), length 42: Reply 10.0.0.2 is-at 00:00:00:00:00:02, length 28
20:38:04.678731 00:00:00:00:00:01 > 00:00:00:00:00:02, ethertype IPv4 (0x0800), length 98: 10.0.0.1 > 10.0.0.2: ICMP echo request, id 3940, seq 1, length 64
20:38:04.722973 00:00:00:00:00:02 > 00:00:00:00:00:01, ethertype IPv4 (0x0800), length 98: 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 3940, seq 1, length 64

ホスト1では、最初にARP requestがブロードキャストされていて、続いてホスト2から返されたARP replyを受信しています。次にホスト1が発行したICMP echo request、ホスト2から返されたICMP echo replyが受信されています。

host: h2:

# tcpdump -en -i h2-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h2-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
20:38:04.637987 00:00:00:00:00:01 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 10.0.0.2 tell 10.0.0.1, length 28
20:38:04.638059 00:00:00:00:00:02 > 00:00:00:00:00:01, ethertype ARP (0x0806), length 42: Reply 10.0.0.2 is-at 00:00:00:00:00:02, length 28
20:38:04.722601 00:00:00:00:00:01 > 00:00:00:00:00:02, ethertype IPv4 (0x0800), length 98: 10.0.0.1 > 10.0.0.2: ICMP echo request, id 3940, seq 1, length 64
20:38:04.722747 00:00:00:00:00:02 > 00:00:00:00:00:01, ethertype IPv4 (0x0800), length 98: 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 3940, seq 1, length 64

ホスト2では、ホスト1が発行したARP requestを受信し、ホスト1にARP replyを返しています。続いて、ホスト1からのICMP echo requestを受信し、ホスト1にecho replyを返しています。

host: h3:

# tcpdump -en -i h3-eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on h3-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
20:38:04.637954 00:00:00:00:00:01 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 10.0.0.2 tell 10.0.0.1, length 28

ホスト3では、最初にホスト1がブロードキャストしたARP requestのみを受信しています。

まとめ

本章では、簡単なスイッチングハブの実装を題材に、Ryuアプリケーションの実装の基本的な手順と、OpenFlowによるOpenFlowスイッチの簡単な制御方法について説明しました。