こんな夜中にOpenFlowでネットワークをプログラミング!

第5回NOXを使って独自のOpenFlowコントローラを実装してみよう!

はじめに

前回は、オープンソースとして提供されているNOX(OpenFlowコントローラ)とOpenFlowSwitch(OpenFlowスイッチ)を用いて、OpenFlowが動作する環境を構築しました。OpenFlowの特長は、NOXにロードするNOXモジュール(前回は単に「モジュール」と表記)をユーザが実装することで、OpenFlowスイッチの動作を自由にカスタマイズできる点にあります。

今回は、簡単なNOXモジュールの作成方法を説明し、NOXモジュールに記述されたロジックどおりにOpenFlowスイッチが動作することを示します。また、OpenFlowコントローラとOpenFlowSwitch間でやりとりされるOpenFlowプロトコルや、NOX上で発生するイベントについても説明します。

NOXとNOXモジュールの関係

NOXは起動時にNOXモジュールを読み込み、そのモジュールに実装されたロジックに従って動作します。ユーザが独自にNOXモジュールを開発すれば、従来とはまったく異なる動作をするネットワークを作ることも可能です。

NOXは、OpenFlowSwitchとNOX間でやりとりされる複雑なOpenFlowプロトコルを隠蔽し、ユーザがNOXモジュールを簡単に実装できる環境を提供します。NOXは、⁠新規OpenFlowスイッチの発見」「未知のパケット受信」を検知するとイベントを発生させ、事象の発生をユーザに通知します。このしくみにより、ユーザはイベントに対する処理を記述するだけで簡単にOpenFlowコントローラを実装できます。

開発するNOXモジュールの仕様

今回開発するNOXモジュールの仕様は、わかりやすさを考慮して次のようにします。

  • OpenFlowスイッチが受信したパケットがARPパケットの場合、FLOOD処理(後述)を行う
  • OpenFlowスイッチが受信したIPv4パケットの送信先IPアドレスがサーバ1と一致、もしくはサーバ2と一致する場合、FLOOD処理を行う
  • 上記の2パターンに当てはまらない場合、OpenFlowスイッチは受信したパケットを破棄する
  • NOXが制御するOpenFlowスイッチは1台とする。2台以上検知した場合は、エラーをコンソールに表示する。新しく検知されたスイッチは一切管理せず、存在しないものとして扱う

FLOOD処理はブロードキャストに似ていますが、通常のブロードキャストとは異なり、ブロードキャストストームなどのループに起因する問題が生じないように配慮してブロードキャストします。今回はサーバが2台しかないため、FLOOD処理を行うとサーバ1から送信されたパケットはサーバ2へ、サーバ2から送信されたパケットはサーバ1へ転送されます。

開発に用いる言語はPythonです。前述の仕様からわかるように、従来のネットワークとは違って、MACアドレスを一切参照せずにパケットを転送させます。

開発したモジュールは、前回構築した環境図1で動作させます。必要に応じて前回を参照し、環境を構築してください。

図1 OpenFlowの基本構成
図1 OpenFlowの基本構成

OpenFlowプロトコルとイベントの関係

ここからは、OpenFlowプロトコルで定義されているメッセージやNOXが発生させるイベントについて、基本的な処理を例にシーケンス図を用いて具体的に説明します。

OpenFlowコネクションの確立図2は、OpenFlowコントローラが起動し、NOXがOpenFlowスイッチから接続要求を受け、OpenFlowのコネクションが確立するまでの関係を示したシーケンス図です。OpenFlowコネクションが確立すると、OpenFlowスイッチとOpenFlowコントローラが能力情報を交換し、本格的にメッセージをやりとりする前準備が完了します。処理の詳細を以下に示します。①~⑧の番号は図2の番号と対応しています。
図2 OpenFlowのコネクション確立時のシーケンス
図2 OpenFlowのコネクション確立時のシーケンス
  • ① NOXを起動する。NOXは6633番ポートでOpenFlowスイッチからの接続要求を待つ
  • ② OpenFlowSwitchがNOXの6633番ポートに接続し、TCPコネクションを確立する[1]
  • ③ NOXからOpenFlowSwitchに対してHelloメッセージを送信する。Helloメッセージは、TCPコネクションの確立直後に送受信する必要があるメッセージ
  • ④ NOXからOpenFlowSwitchに対してFeaturesRequestメッセージを送信し、OpenFlowスイッチが有する能力(対応しているOpenFlowプロトコルのバージョンなど)の取得を試みる
  • ⑤ NOXからHelloメッセージを受信したOpenFlowSwitchは、NOXに対してHelloメッセージを返送する
  • ⑥ NOXからFeaturesRequestメッセージを受信したOpenFlowSwitchは、NOXに対してFeaturesReplyメッセージを返送し、OpenFlowSwitchが対応するOpenFlowプロトコルのバージョンやオプションへの対応状況を通知する
  • ⑦ ③から⑥までが完了すると、OpenFlowコネクションが確立し、新規にOpenFlowスイッチが検出されたと判断される。NOXはDatapathJoinイベントを発行し、新スイッチを検出したことをユーザに通知する[2]
  • ⑧ OpenFlowスイッチを初めて検出した場合、NOXモジュールはOpenFlowスイッチの識別に必要なDatapathIdを保存する。保存された値と異なるDatapathIdを検出した場合は、スイッチを複数検出したと判断し、仕様に従ってエラーをコンソールに表示する

ユーザは、⑧の処理を自身で実装する必要があります。DatapathIdは各OpenFlowスイッチが生成するユニークな値で、OpenFlowスイッチからOpenFlowコントローラに送信されるメッセージには一部の例外を除いてDatapathIdが付加されます。OpenFlowコントローラはDatapathIdを見ることで、パケットを送信したOpenFlowスイッチを識別できます。また、DatapathIdはOpenFlowスイッチが有するMACアドレスを基に生成されるため、重複する心配はありません。

パケット転送処理

続いて、図3を用いて、OpenFlowSwitchが未知のパケットを受信したときにNOXからの指示に従ってパケットを転送する処理(第2回で紹介した制御方式2)について以下に説明します。

図3 パケット転送処理時のシーケンス図
図3 パケット転送処理時のシーケンス図
  • ① サーバ1はサーバ2に対してPingパケットを送信する
  • ② 未知のパケットを受信したOpenFlowSwitchは、受信したパケットを自身のバッファに格納する。そして、PacketInメッセージをNOXに対して送信し、パケットの制御方法を問い合わせる
  • ③ NOXはPacketInイベントを発行し、OpenFlowSwitchが新しいパケットを受信したことをNOXモジュールに通知する
  • ④ NOXモジュールは先に示した仕様に従い、パケットをFLOOD処理するべきか、破棄するべきか判断する
  • ⑤ NOXモジュールがパケットをFLOOD処理すると判断した場合、NOXに対してFLOOD指示を発行する
  • ⑥ NOXはOpenFlowSwitchに対してFlowModifyメッセージを発行し、スイッチにFLOODを行う制御ルールを書き込む
  • ⑦ OpenFlowSwitchはNOXから書き込まれた制御ルールに従い、バッファに保存されたパケットをFLOOD処理する(結果としてサーバ2が接続されている物理ポートからパケットが送出される)
  • ⑧ OpenFlowSwitchからサーバ2にPingパケットが転送される

ユーザは、④と⑤の部分を自身で実装する必要があります。

NOXモジュールの実装

それでは、新しくNOXモジュールを作成してみましょう。NOXに新しい名前のモジュールを追加することは可能ですが、その方法は若干複雑です。そこで、今回はpyswitch.pyの中身を書き換えることで新モジュールを作成します。pyswitch.pyを書き換える前にバックアップをしておきましょう。図1のサーバ3にユーザhogehogeでログインし、以下のコマンドを実行してください。

$ cd /home/hogehoge/nox/build/src/nox/coreapps/examples/
$ cp pyswitch.py pyswitch.py.ori

これでpyswitchモジュールを複製できました。pyswitch.pyはpythonで記述されているため、テキストエディタで書き換え可能です。pyswitch.pyを開きリスト1。のソースコードを上書きで書き込んでください。その後、次のコマンドでNOXを起動させます。すでにNOXが起動中の場合は、NOXを停止してから起動してください。

リスト1 pyswitch.pyに書き込むソースコード
# -*- coding: utf-8 -*-

from nox.lib.core import *
from nox.lib.packet.arp import arp
from nox.lib.packet.ethernet import ethernet
from nox.lib.packet.ipv4 import ipv4
from nox.lib.packet.packet_utils import *

import logging

logger = logging.getLogger('nox.coreapps.examples.pyswitch')

HOST1_IPADDR = "192.168.0.1"  # host1のIPアドレス
HOST2_IPADDR = "192.168.0.2"  # host2のIPアドレス
CACHE_TIME = 10               # OpenFlowスイッチが制御ルールをキャッシュする時間(秒)

class pyswitch(Component):
  """ユーザが独自に定義したNOXモジュール"""

  switch_dpid = None          # OpenFlowスイッチのDatapathId

  def __init__(self, context):  ―――①
    Component.__init__(self, context)

  def install(self):  ―――②
    """NOXに対してNOXモジュールで定義したイベント関数を登録する関数"""
    self.register_for_datapath_join(self.datapath_join_event)
    self.register_for_packet_in(self.packet_in_event)

  def getInterface(self):  ―――③
    return str(pyswitch)

  def datapath_join_event(self, dpid, stats):  ―――④
    """DatapathIdイベントハンドラ"""
    logger.info('新しいOpenFlowスイッチ(dpid=%x)を検出しました' % dpid)

    if self.switch_dpid == None:
      self.switch_dpid = dpid
    else:
      logger.info('Error: 2つ以上のOpenFlowスイッチが検出されました')

  def packet_in_event(self, dpid, inport, reason, len, buffer_id, etherframe):  ―――⑤
    """PacketInイベントハンドラ"""
    logger.info('物理ポート番号(inport=%d)からパケットを受信しました', inport)

    packet = etherframe.next

    if isinstance(packet, arp):
      logger.info('ARPパケットをFLOODする制御ルールを書き込みます')
      self.write_flooding_rule(etherframe, dpid, buffer_id, inport)

    elif isinstance(packet, ipv4):
      dstip_str = ip_to_str(packet.dstip)    # パケットから送信先IPアドレスを取得し文字列に変換

      if dstip_str == HOST1_IPADDR or dstip_str == HOST2_IPADDR:
        logger.info('パケットをFLOODする制御ルールを書き込みます')
        self.write_flooding_rule(etherframe, dpid, buffer_id, inport)
      else:
        logger.info('パケットを破棄します')

    else:
      logger.info('パケットを破棄します')

    return CONTINUE

  def write_flooding_rule(self, etherframe, dpid, buffer_id, inport):  ―――⑥
    """OpenFlowスイッチにFLOOD処理を行うように制御ルールを書き込む"""
    flow = extract_flow(etherframe)
    flow[core.IN_PORT] = inport
    actions = [[openflow.OFPAT_OUTPUT, [0, openflow.OFPP_FLOOD]]]
    self.install_datapath_flow(dpid, flow, 0, CACHE_TIME,
        actions, buffer_id, openflow.OFP_DEFAULT_PRIORITY)

  def getFactory():  ―――⑦
    class Factory:
    def instance(self, context):
      return pyswitch(context)
    return Factory()
$ cd /home/hogehoge/nox/build/src
$ ./nox_core -v -i ptcp:6633 pyswitch

NOXが正しく動作しているかPingコマンドで確認してみます。サーバ1から次のコマンドを実行してください。

$ ping 192.168.0.2

同様にサーバ2から次のコマンドを実行してください。応答があれば、成功です。

$ ping 192.168.0.1

ソースコードの説明

コメントやログ出力をソースコードに適宜挿入しているため、ある程度は処理の流れを理解できると思います。ここでは、コメントからはわかりにくい個所について補足します。

pyswitchクラスの__init__ 関数(リスト1-は、NOXモジュールが初期化されるときに呼ばれる関数です。

pyswitchクラスのinstall関数は、NOXモジュールが受信したいイベントをNOXに登録する関数です。この関数では、NOXモジュールに対してDatapathJoinイベントとPacketInイベントを通知するように登録処理を行っています。

datapath_join_event関数には、図2の⑧の処理が実装されています。1つ目のOpenFlowスイッチを検出したときは、DatapathIdをメンバ変数に保存します。2つ以上のスイッチを検出した場合は、エラーをコンソールに出力します。

packet_in_event関数には、図3の④と⑤の処理を実装しています。受信したパケットがARPパケットの場合は、FLOOD処理を行う制御ルールをwrite_fl ooding_rule 関数でOpenFlowスイッチに書き込みます。受信したパケットがIPv4で、かつ送信先IPアドレスがhost1、もしくは、host2と同じ場合も同様にFLOOD処理を行う制御ルールを書き込みます。それ以外のパケットを受信した場合はとくに何も処理せず、単にコンソールに「パケットを破棄します」と表示します(何も処理しないと、結果としてパケットは破棄されます⁠⁠。

write_fl ooding_rule関数は複雑なので、細かく説明します。まず、OpenFlowスイッチが受信したパケット(etherframe)から第1回で説明した12タプルの情報(ただし、物理ポート番号を除く)をextract_flow関数で抽出し、連想配列であるflowに格納します。さらに、パケットを受信したときに経由した物理ポート番号(inport)をflowに格納します。続いて、連想配列であるactionsの設定を行います。OFPAT_OUTPUTはパケットを転送するという意味で、OFPP_FLOODはFLOOD処理を行うことを意味します。0の個所には本来パケットの最大サイズを指定します(今回はとくに気にしないでください⁠⁠。最後に、install_datapath_flow関数でOpenFlowスイッチに制御ルールを書き込みます。書き込まれた制御ルールは、CACHE_TIMEで指定した秒数だけ有効です。dpidはDatapathIdを表します。また、buffer_idはOpenFlowスイッチ内でバッファされているパケットを識別するための値です。OFP_DEFAULT_PRIORITYは、各制御ルールに設定可能な優先度のデフォルト値です。

getFactory関数やpyswitchクラスのgetInterface関数は、NOXモジュールがロードされるときに呼び出される関数です。これらの関数は、すべてのモジュールで同じような実装になります。とくに詳細を気にする必要はありません。

まとめ

今回は、NOXモジュールの実装方法について説明しました。従来のネットワークとは違った、MACアドレスを参照しない通信を体感いただけたと思います。次回は、より高度なNOXモジュールの作成方法を説明する予定です。

おすすめ記事

記事・ニュース一覧