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

第6回NOXを使って独自のOpenFlowコントローラを実装してみよう!~パケットアウト編

はじめに

今回も、前回に引き続き、オープンソースで提供されているNOX(OpenFlowコントローラ)上で動作するNOXモジュールの実装方法を説明します。

オープンクラウドキャンパス

先に、8月2日に開催された「オープンクラウドキャンパス」という勉強会で、OpenFlowについて説明してきましたので、そのときの様子を紹介します。OpenFlowに取り組むNTTデータ、NEC、Citrix、Fusionの4社がそれぞれの視点で話をしています。他にMidonetやVyattaの紹介もありました。質疑応答の時間が十分にあったとは言えない状況でしたが、OpenFlowの本質は「プログラマブルである」など、深い指摘があったのが印象的でした。

最近は雑誌などでOpenFlowが取り上げられる機会が増えています。OpenFlowへの注目も高まっており、SDN(Software-Defi ned Network)技術を推進するONF(Open Networking Foundation)に参加する企業は、新たにIntel、Extream Networks、富士通、OpenFlowベンチャーなどが加わり、43社まで増えています(2011年8月12日現在⁠⁠。また、OpenFlowを用いて複数のデータセンターを仮想的に1つのデータセンターとして見るデータセンター間でライブマイグレーションを行うといった取り組みも行われています。

今回の開発で使用する構成

今回開発するNOXモジュールは、図1に示す構成で動作させます。これは第4回で構築した環境と同じですが、サーバ2のIPアドレスが異なっているので注意してください。

図1 OpenFlowの動作環境
図1 OpenFlowの動作環境

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

今回開発するNOXモジュールの仕様を以下に示します。OpenFlowらしく、一般的なネットワーク装置では実現できない仕様にします。

  • OpenFlowコントローラがOpenFlowスイッチを検出したとき、スイッチに接続されているサーバ1とサーバ2のMACアドレスを取得するため、スイッチからサーバに対してARP要求パケットを送信する
  • OpenFlowスイッチがサーバからARP応答パケットを受信した場合、ARP応答パケットからサーバのMACアドレスを取得し、スイッチの物理ポートの先に接続されているサーバのMACアドレスを取得する
  • NOXが制御するOpenFlowスイッチは1台とする。2台以上検知した場合は、エラーをコンソールに表示する。新しく検知されたスイッチは一切管理せず、存在しないものとして扱う
  • OpenFlowスイッチは、サーバからOpenFlowルータ宛のARP要求パケットを受信した場合、OpenFlowコントローラ内でARP応答パケットを生成し、サーバに返送する。OpenFlowルータ宛でない場合は、受信したARP要求パケットを破棄する
  • IPv4パケットを受信したとき、サーバ1とサーバ2のMACアドレスを取得できていない場合は受信したパケットを破棄する
  • IPv4パケットを受信し、かつサーバ1とサーバ2のMACアドレスが既知の場合、OpenFlowコントローラ内でパケットがOpenFlowルータを通過したのと同じようにMACアドレスを書き換え、コントローラからスイッチ経由でパケットを送出する
  • ARPでもIPv4パケットでもない場合は、パケットを破棄する

OpenFlowプロトコルとパケットアウトの関係

前回は、OpenFlowスイッチに制御ルールを書き込むことでパケットの転送を実現しました。今回は、OpenFlowコントローラ内で書き換えたパケットをスイッチ経由でサーバに対して送出する方法でパケットを転送します(パケットアウトとも呼びます⁠⁠。パケットアウトを行うときに利用するOpenFlowプロトコルのメッセージやイベントの関係を図2に示します。

図2 パケットアウト処理時のシーケンス図
図2 パケットアウト処理時のシーケンス図

パケットアウトを行う場合はOpenFlowスイッチに制御ルールを書き込まないため、同じパケットを受信した場合は再びOpenFlowコントローラにPacketInメッセージが送信されることに注意してください。

  • ① サーバ1はサーバ2に対してPingパケットを送信する
  • ② 未知のパケットを受信したOpenFlowSwitchは、受信したパケットを自身のバッファに格納する。そして、PacketInメッセージをNOXに対して送信し、パケットの制御方法を問い合わせる
  • ③ NOXはPacketInイベントを発行し、OpenFlowSwitchが新しいパケットを受信したことをNOXモジュールに通知する
  • ④ NOXモジュールは、OpenFlowスイッチが受信したパケットを必要に応じて書き換える(OpenFlowスイッチが受信したパケットは、PacketInメッセージやPacketInイベントを通してNOXモジュールまで届けられる)
  • ⑤ NOXモジュールは、書き換えたパケットを送信するようにNOXに対して指示する
  • ⑥ NOXは、コントローラから送信されたパケットをサーバ2に送信するようにOpenFlowSwitchに対して指示する
  • ⑦ OpenFlowSwitchは、コントローラから受信した書き換え済みのPingパケットをサーバ2に送信する

NOXモジュールの実装

それでは、NOXモジュールを作成しましょう。前回と同様にpyswitch.pyの中身を書き換えます。サーバ3にユーザhogehogeでログインし、次のコマンドでpyswitch.pyのバックアップをしましょう。

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

バックアップが終わったら、エディタでpyswitch.pyを開き、リスト1のソースコードを上書きで書き込みます。その後、以下のコマンドでNOXを起動してください(すでにNOXが起動中の場合は、停止してから起動します⁠⁠。

$ cd /home/hogehoge/nox/build/src
$ ./nox_core -v -i ptcp:6633 pyswitch

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

$ ping 192.168.0.2 (サーバ1から)
$ ping 192.168.0.1 (サーバ2から)

Pingに対して応答があれば、成功です。

リスト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.1.1"           # host2のIPアドレス
ROUTER_MACADDR1 = "00:00:00:00:00:01"  # routerのhost1側のMACアドレス
ROUTER_MACADDR2 = "00:00:00:00:00:02"  # routerのhost2側のMACアドレス
ROUTER_IPADDR1 = "192.168.0.10"        # routerのhost1側のIPアドレス
ROUTER_IPADDR2 = "192.168.1.10"        # routerのhost2側のIPアドレス
ZERO_MAC = "00:00:00:00:00:00"
BROADCAST_MAC = "FF:FF:FF:FF:FF:FF"
ARPOP_REQUEST = 1                      # ARPの要求オペレーションコード
ARPOP_REPLY = 2                        # ARPの応答オペレーションコード
CACHE_TIME = 10                        # OpenFlowスイッチが制御ルールをキャッシュする時間(秒)

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

  switch_dpid = None      # OpenFlowスイッチのDatapathId
  port1_macaddr = None    # port番号1に接続しているHostのMACアドレス
  port2_macaddr = None    # port番号2に接続しているHostのMACアドレス

  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 create_arp_request(self, srcmac, srcip, dstip): ――①
    """ARP要求を生成する"""
    arppkt = arp()
    arppkt.hwlen = 6
    arppkt.opcode = ARPOP_REQUEST
    arppkt.protolen = 4
    arppkt.hwsrc = octstr_to_array(srcmac)
    arppkt.protosrc = ipstr_to_int(srcip)
    arppkt.hwdst =octstr_to_array(ZERO_MAC)
    arppkt.protodst = ipstr_to_int(dstip)

    etherframe = ethernet(None, arp)
    etherframe.dst = octstr_to_array(BROADCAST_MAC)
    etherframe.src = octstr_to_array(srcmac)
    etherframe.type = ethernet.ARP_TYPE
    etherframe.next = arppkt

    return etherframe

  def reply_arp_packet(self, dpid, inport, etherframe, response_mac): ――②
    """ARP応答を返却する"""
    arppkt = etherframe.next

    # ARP応答のパケットを構築する
    arppkt.opcode = ARPOP_REPLY
    protodst = arppkt.protodst
    arppkt.hwdst = arppkt.hwsrc
    arppkt.protodst = arppkt.protosrc
    arppkt.hwsrc = octstr_to_array(response_mac)
    arppkt.protosrc = protodst

    # ethernetのフレームを構築する
    etherframe.dst = arppkt.hwdst
    etherframe.src = arppkt.hwsrc

    logger.info("ARP応答を返します %s => %s" % (mac_to_str(arppkt.hwsrc), mac_to_str(arppkt.hwdst)))
    self.send_openflow_packet(dpid, etherframe.tostring(), inport)

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

    if self.switch_dpid == None:
      self.switch_dpid = dpid

      logger.info('Host1にARP要求を送信します')
      arp_for_host1 = self.create_arp_request(ROUTER_MACADDR1, ROUTER_IPADDR1, HOST1_IPADDR)
      self.send_openflow_packet(dpid, arp_for_host1.tostring(), openflow.OFPP_ALL)

      logger.info('Host2にARP要求を送信します')
      arp_for_host2 = self.create_arp_request(ROUTER_MACADDR2, ROUTER_IPADDR2, HOST2_IPADDR)
      self.send_openflow_packet(dpid, arp_for_host2.tostring(), openflow.OFPP_ALL)
    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):
      arppkt = packet
      if arppkt.opcode == ARPOP_REQUEST:
        arp_dstip_str = ip_to_str(arppkt.protodst)
        if arp_dstip_str == ROUTER_IPADDR1:
          self.reply_arp_packet(dpid, inport, etherframe, ROUTER_MACADDR1)
        elif arp_dstip_str == ROUTER_IPADDR2:
          self.reply_arp_packet(dpid, inport, etherframe, ROUTER_MACADDR2)
        else:
          logger.info('パケットを破棄します')
      else:
        if inport == 1:
          self.port1_macaddr = mac_to_str(etherframe.src)
          logger.info('port1に接続されているMACアドレス=%s', self.port1_macaddr)
        elif inport == 2:
          self.port2_macaddr = mac_to_str(etherframe.src)
          logger.info('port2に接続されているMACアドレス=%s', self.port2_macaddr)

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

      if self.port1_macaddr == None or self.port2_macaddr == None:
        logger.info('Host情報取得が完了していないため、パケットを破棄します')
        return CONTINUE

      logger.info('IPv4です。パケットを転送します')

      if dstip_str != HOST1_IPADDR and dstip_str != HOST2_IPADDR:
        logger.info('送信先IPアドレスが異なるためパケットを破棄します')
        return CONTINUE

      outport = 2 if (inport == 1) else 1

      if dstip_str == HOST1_IPADDR:
        etherframe.src = octstr_to_array(ROUTER_MACADDR1)
        etherframe.dst = octstr_to_array(self.port1_macaddr)
        self.send_openflow_packet(dpid, etherframe.tostring(), outport)
        logger.info('パケットを転送します(outport=%d)', outport)
      else:
        etherframe.src = octstr_to_array(ROUTER_MACADDR2)
        etherframe.dst = octstr_to_array(self.port2_macaddr)
        self.send_openflow_packet(dpid, etherframe.tostring(), outport)
        logger.info('パケットを転送します(outport=%d)', outport)

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

    return CONTINUE

def getFactory():
  class Factory:
    def instance(self, context):
      return pyswitch(context)
  return Factory()

ソースコードの説明

以降、リスト1のソースコードについて説明していきますが、前回と重複する部分の説明は省略します。

pyswitchクラスのcreate_arp_request関数(リスト1-は、ARP要求パケットを生成する関数です。引数のsrcmac、srcip、dstipには、それぞれARPの送信元MACアドレス、送信元IPアドレス、送信先IPアドレスを指定する必要があります。本関数の戻り値は、生成されたARP要求パケットです。

reply_arp_packet関数は、サーバが送信したARP要求パケットからARP応答パケットを生成し、サーバにARP応答を送り返す関数です。response_macには、ARP要求に応答するノードが保持しているMACアドレスを指定します。

datapath_join_event関数は、OpenFlowスイッチを検出したときに呼ばれる関数です。OpenFlowの重複を検出した場合は、エラーをコンソールに表示し処理を終えます。1つ目のスイッチの検出である場合は、create_arp_request関数でARP要求パケットを生成し、生成したパケットをsend_openfl ow_packet関数でサーバに送信します。サーバにARP要求パケットを送信すると、サーバからARP応答パケットを受け取ることができるので、結果としてサーバのMACアドレスを取得できます。本関数は引数にopenfl ow.OFPP_ALLを指定しているため、スイッチのすべての物理ポートからパケットが送出されます。また、arp_for_host1.tostring()により生成したパケットを文字列に変換しています。文字列に変換されたパケットは、さらに本関数の中でバイナリに変換されNOXに渡されます。

packet_in_event関数は、OpenFlowスイッチが未知のパケットを受信したときに呼ばれる関数です。etherframe.nextは、ethernetフレームから上位のプロトコルを取得する処理です。

etherframe.nextにより取得したプロトコルがARPだった場合は、さらにARP要求か、ARP応答かにより実行する処理が変わります。ARP要求を受信した場合は、reply_arp_packet関数を呼び出し、サーバにARP応答を返却します。ARP応答を受信した場合は、ethernetフレームの送信元MACアドレスの値を取得し、サーバのMACアドレスを取得します。

プロトコルがIPv4の場合は、以下のいずれかの動作になります。OpenFlowコントローラがサーバのMACアドレス取得に失敗している場合は、受信したパケットを破棄します。MACアドレスの取得に成功している場合は、サーバ1からのパケットはサーバ2へ、サーバ2からのパケットはサーバ1へ転送します。もちろん、OpenFlowルータを経由しているので、パケットの発信元MACアドレスと送信先MACアドレスは書き変わります。

まとめ

今回は、前回に引き続きNOXモジュールの実装方法について説明しました。今回の例で物足りない方は、ルータのIPアドレス(192.168.0.10や192.168.1.10)宛にPingパケットを送信した場合に応答が返るように追加実装してみてください。

現在、OpenFlowはデータセンターへの適用を中心に検討されています。将来的には、データセンター間の連携やWANへの適用が期待されています。OpenFlowによりネットワーク機器を自由にコントロールできるようになりましたが、同時に限界を感じることもあります。OpenFlowを利用して従来のネットワークと真に異なる世界を実現するには、サーバ側のプロトコルスタックもプログラムで制御可能にするべきでしょう。⁠サーバからARPパケットが送信される」⁠デフォルトゲートウェイという概念が存在する」ことが、OpenFlowのプログラマビリティを損ねていると感じることもあります。

今回で筆者の執筆は終わりになります。お付き合いいただきありがとうございました。次回からは、オープンソースのOpenFlowコントローラ「Trema」を開発している方が継続して執筆する予定です。ご期待ください。

おすすめ記事

記事・ニュース一覧