2012年4月4日水曜日

小野秀貴: OpenFlow Controllerを用いてマルチホーム環境を活用する

はじめに

前回はOpenFlowスイッチの実装であるOpen vSwitchを動かしてみまし
た。今回はOpenFlowコントローラを導入して、Open vSwitchと接続
して制御してみます。

OpenFlowコントローラにはNOX, Tremaなどいくつかのオープンソース
実装があります。今回はおもにNECで開発されているTremaを利用して
みます。Rubyで容易に記述できるらしいということから選んでみました。

今回の目標は、
「マルチホーム環境でopenvswitchの配下にあるクライアントからの
  上流へのアクセス時に、特定のTCPポート(今回はSMTP=25)の通信
  だけ異なるrouterを経由するようにする」
ということです。

前回はOpen vSwitch単体動作で上記を実現しましたが、今回はOpenFlow
コントローラ経由でこれを実現したいと思います。



◆OpenFlowコントローラTrema導入

まずはTremaの導入に必要なものをインストールします。
 # aptitude install gcc make ruby ruby-dev irb file libpcap-dev libsqlite3-dev rubygems rake arping
なおarpingはTrema導入には必要ないですが、今回利用するためイン
ストールしています。

今回、Tremaはgemを利用してインストールします。
ただ、そのまま試してみるとrake1.8がないというエラーになるため
以下の対処をしておきます。
 # cd /usr/bin
 # ln -s rake rake1.8
ようやくtremaのインストールです。
 # gem install trema
これにはOpenBlockS 600Dでは30分以上を要しました。

2012年3月11日時点ではtrema 0.2.2.1がインストールされました。
 # gem list trema
 *** LOCAL GEMS ***

 trema (0.2.2.1)
実行ファイルtremaは/var/lib/gems/1.8/binにインストールされるので
ここにPATHを通しておきます。
 # PATH=/var/lib/gems/1.8/bin:$PATH
れで準備は完了です。



TremaでOpen vSwitchを制御

まずはTremaを起動してみます。
Tremaにはいくつかサンプルが附属しています。
L2スイッチ機能を実現したlearning_switchというサンプルを試して
みることにします。
 # cd /var/lib/gems/1.8/gems/trema-0.2.2.1/src/examples/learning_switch
 # trema run learning-switch.rb
tremaのswitch_managerが起動して、OpenFlowプロトコルのデフォルトTCPポート
である6633でOpenFlowスイッチからの接続を待っている状態になります。
 # lsof -i:6633
 COMMAND     PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
 switch_ma 32074 root    5u  IPv4 11451496      0t0  TCP *:6633 (LISTEN)
通常はOpenFlowスイッチとOpenFlowコントローラは別ホストですが、
今回は同一ホスト(OpenBlockS 600D)でOpen vSwitchとTrema両者を実行します。
なお、tremaを起動したコンソールはアタッチされたままなので、別
コンソールでOpen vSwitchの作業を行います。

Open vSwitchは起動している状態になっているとします。
Open vSwitchからOpenFlowコントローラ(Trema)への接続はovs-vsctlの
set-controllerコマンドで行います。同一ホスト内なのでlocalhost(127.0.0.1)
に接続します。
 # ovs-vsctl set-controller br0 tcp:127.0.0.1
なお、コントローラからの離脱は
 # ovs-vsctl del-controller br0
で行います。

前回と同様に以下のようなネットワークを構成します。


tremaとopenvswitchはobs600-3で動いていて、openvswitchはtremaに
接続している状態になっているとします。

ここでobs600-1上でrouter-1へのpingを実行します。
 obs600-1# ping -c 1 192.168.100.1
このときtremaからopenvswitchへはARP REPLY, ICMP ECHO REQUEST,
ICMP ECHO REPLYの3つのフローが登録されています。なお、ARP REQUEST
についてはtremaのlearning-switch.rbでは出力先のポートが未定
なためこの時点ではフローは登録されません。
 # ovs-ofctl dump-flows br0
 NXST_FLOW reply (xid=0x4):
  cookie=0x2, duration=4.156s, table=0, n_packets=1, n_bytes=98, priority=65535,icmp,in_port=2,vlan_tci=0x0000,dl_src=00:0a:85:04:93:99,dl_dst=00:00:00:00:00:01,nw_src=192.168.100.123,nw_dst=192.168.100.1,nw_tos=0,icmp_type=8,icmp_code=0 actions=output:1
  cookie=0x3, duration=4.146s, table=0, n_packets=1, n_bytes=98, priority=65535,icmp,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:0a:85:04:93:99,nw_src=192.168.100.1,nw_dst=192.168.100.123,nw_tos=0,icmp_type=0,icmp_code=0 actions=output:2
  cookie=0x1, duration=4.17s, table=0, n_packets=0, n_bytes=0, priority=65535,arp,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:0a:85:04:93:99,nw_src=192.168.100.1,nw_dst=192.168.100.123,arp_op=2 actions=output:2
これでlearning-switch.rbが期待通りに動作しているのが確認できました。

Trema確認中に少し気になった点があります。TremaはCtrl-Cで終了
するのですが、switch_managerプロセスが動作したままで、次回の
trema起動がうまくいかない状況になりました。とりあえずtrema run
の前にkillall switch_managerを行うことで暫定対処としました。



コントローラ用コード作成

今回の目的の
「マルチホーム環境でopenvswitchの配下にあるクライアントからの
  上流へのアクセス時に、特定のTCPポート(今回はSMTP=25)の通信
  だけ異なるrouterを経由するようにする」
を実現することを考えます。

基本的にはL2スイッチなのでtremaサンプルのlearning-switch.rbを
ベースにします。
上記を実現するためのコードを少し加えます(modified-learning-switch.rb)。
なお、既存部分で変更を加えていない箇所は掲載を省略しています。

 class ModifiedLearningSwitch < Trema::Controller
  add_timer_event :age_fdb, 5, :periodic
  add_timer_event :periodic_arp_request, 10, :periodic


  def start
    @fdb = FDB.new
    @target_routes = []
    @target_routes.push({ :route_dst => "192.168.100.1",
                          :route_dst_modified => "192.168.100.2",
                          :ip_src => "192.168.100.123",
                          :port => 25})
    periodic_arp_request
  end

  def packet_in datapath_id, message
    @fdb.learn message.macsa, message.in_port
    matched_route = nil

    @target_routes.each do |route|
      if message.tcp? &&
         message.tcp_dst_port == route[:port] &&
         message.ipv4_saddr.to_s == route[:ip_src] &&
         message.macda.to_s == route[:mac_dst]
        matched_route = route
      end
    end

    port_no = @fdb.port_no_of( message.macda )
    if matched_route.nil?
      if port_no
        flow_mod datapath_id, message, port_no
        packet_out datapath_id, message, port_no
      else
        flood datapath_id, message
      end
    else
      if port_no
        flow_mod_dstmac datapath_id, message, port_no, matched_route[:mac_dst_modified]
        packet_out_dstmac datapath_id, message, port_no, matched_route[:mac_dst_modified]
      else
        flood_dstmac datapath_id, message, matched_route[:mac_dst_modified]
      end
    end
  end

  ##############################################################################
  private
  ##############################################################################

  def periodic_arp_request
    @target_routes.each do |route|
      reply=`arping -0 -c 1 -w 100 #{route[:route_dst]}`
      if /from (\w{2}:\w{2}:\w{2}:\w{2}:\w{2}:\w{2}) \(([\d\.]+)\)/ =~ reply
        info "arping " + $1 + "=" + $2
        route[:mac_dst] = $1
      end
      reply=`arping -0 -c 1 -w 100 #{route[:route_dst_modified]}`
      if /from (\w{2}:\w{2}:\w{2}:\w{2}:\w{2}:\w{2}) \(([\d\.]+)\)/ =~ reply
        info "arping " + $1 + "=" + $2
        route[:mac_dst_modified] = $1
      end
    end
  end

  def flow_mod_dstmac datapath_id, message, port_no, new_mac_dst
    send_flow_mod_add(
      datapath_id,
      :match => ExactMatch.from( message ),
      :actions => [
                Trema::ActionSetDlDst.new( :dl_dst => Mac.new( new_mac_dst )),
                Trema::ActionOutput.new( :port => port_no )
        ]      
    )
  end

  def packet_out_dstmac datapath_id, message, port_no, new_mac_dst
    send_packet_out(
      datapath_id,
      :packet_in => message,
      :actions => [
                Trema::ActionSetDlDst.new( :dl_dst => Mac.new( new_mac_dst )),
                Trema::ActionOutput.new( :port => port_no )
        ]      
    )
  end

  def flood_dstmac datapath_id, message, new_mac_dst
    packet_out_dstmac datapath_id, message, OFPP_FLOOD, new_mac_dst
  end
 end
ざっくり動作を説明します。

まずは変更点として、periodic_arp_request methodで定期的にarp requestをrouter-1
とrouter-2に送信してMACアドレスを解決しておきます。これは処理するパケットのDST MAC
がrouter-1のMACにマッチした場合にrouter-2のMACに変更するということを実現する
ために必要になるためです。

openvswitchでフローが未解決パケットが到着したときには、OpenFlow プロト
コルでtremaに送信します。そしてtremaではpacket_inメソッドが呼ばれます。
learning-switch.rbではpacket_inでは通常は宛先MACアドレスを学習している
場合は、
 1. フローをOpenFlowスイッチに登録(flow_mod)
 2. 受信したパケットを該当ポートへ出力(packet_out)
という処理を行います。

今回の変更点として、特定の条件(ip_src = 192.168.100.123, dst_port = 25)
にマッチする場合は、
 1. フローをOpenFlowスイッチに登録
    ただし、DST MACアドレスをrouter-2のものに変更(flow_mod_dstmac)
 2. 受信したパケットを該当ポートへ出力
    ただし、DST MACアドレスをrouter-2のものに変更(packet_out_dstmac)
という処理を行うように修正しました。



動作実験

上記で作成したmodified-learning-switch.rbを実行します。
 # trema run modified-learning-switch.rb
Open vSwitchをTremaに接続します。
 # ovs-vsctl set-controller br0 tcp:127.0.0.1
obs600-1から外部のホスト(10.0.0.1)にsmtp接続します。
 obs600-1# telnet 10.0.0.1 25
 Connected to 10.0.0.1.
 Escape character is '^]'.
 220 smtp.example.org. ESMTP Postfix
このときのOpen vSwitchのフローの状態を確認します。
 # ovs-ofctl dump-flows br0
  - 省略 -
  cookie=0x9, duration=5.993s, table=0, n_packets=5, n_bytes=336, priority=65535,tcp,in_port=2,vlan_tci=0x0000,dl_src=00:0a:85:04:93:99,dl_dst=00:00:00:00:00:01,nw_src=192.168.100.123,nw_dst=10.0.0.1,nw_tos=16,tp_src=50729,tp_dst=25 actions=mod_dl_dst:00:00:00:00:00:02,output:1
  - 省略 -
dump-flowsでの確認結果には、mod_dl_dstが表示されています。
また、10.0.0.1のmailログでもrouter-2経由でアクセスされたログを
確認できので期待通りの動作を実現できました。



まとめ
今回はOpenFlowコントーラのTremaを利用し、Open vSwitchをTremaに
接続してOpenFlowプロトコルでフローが設定されることを確認しました。

また、Tremaのコードに修正を加えることで、特定の条件にマッチする場合
には異なるノードへ転送するようなフローをOpen vSwitchに設定する
ことができることを確認しました。

0 件のコメント:

コメントを投稿