Tremaでネットワーク機器プログラミング

2014年8月 2日 (土)

◆単純なブリッジに仮想インターフェースを追加【OpenFlow1.0をTremaの土管として使う】

Trema のプログラムにより、ICMP Echo に応答する仮想のインタフェーフェースを作成してみました。
以下の2つの理由により少ないコード数で実現することができました。
1. Trema の PacketIn メッセージには ARP の解析結果が含まれている。
(Trema の PacketIn http://rubydoc.info/github/trema/trema/master/Trema/PacketIn )
2. Trema の前提としている Pio ライブラリを使用することで ARP と ICMP Echo パケットのエンコード/デコードが簡単にできる。

※注意:不正なパケットを受信した場合の処理はあまり考えていません。

■確認環境
  ・OS: Lubuntu 13.04 Desktop (i386)
  ・Ruby 1.9.3p194
  ・Trema 0.4.7
  ・Pio 0.7.0
  ・bindata 2.1.0

■テスト構成
  OpenFlow コントローラである Trema 側のプログラムにより仮想のインターフェースには図のように MAC アドレスと IP アドレスを割り振ってあります。

Photo

■プログラムコード

以前の記事「◆ 単純なブリッジの実装その4~Open vSwitch対策~【OpenFlow1.0をTremaの土管として使う】」のコードを改造して作成しました。

require 'pio'

class VirtualIP1 < Controller
  MIN_PACKET_DATA_LEN = 60

  def start
    @port_list = [ 1, 2, 3 ]    # all port numbers

    @VIF_mac = Pio::Mac.new("02:00:00:00:00:01")
    @VIF_ipv4addr = Pio::IPv4Address.new("192.168.0.251")
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def flood( datapath_id, in_port, data )
    actions = [ ]
    @port_list.each do | port |
      if ( in_port != port )
        actions.push( SendOutPort.new( port ) )
      end
    end

    send_packet_out( datapath_id,
      :in_port => in_port,
      :data => data,
      :actions => actions )
  end

  def handle_icmpv4_echo_request( datapath_id, message )
    return if ( message.ipv4_daddr != @VIF_ipv4addr )

    begin
      icmpv4_echo_req = Pio::Icmp.read( message.data )
    rescue => ex
      return
    end

    icmpv4_echo_reply = Pio::Icmp::Reply.new(
      :destination_mac => message.macsa,
      :source_mac => message.macda,
      :ip_source_address => message.ipv4_daddr,
      :ip_destination_address => message.ipv4_saddr,
      :identifier => icmpv4_echo_req.icmp_identifier,
      :sequence_number => icmpv4_echo_req.icmp_sequence_number,
      :echo_data => icmpv4_echo_req.echo_data)

    flood( datapath_id, OFPP_NONE, icmpv4_echo_reply.to_binary)
  end

  def handle_arp( datapath_id, message )
    return if ( message.arp_tpa != @VIF_ipv4addr )

    begin
      arp_req = Pio::Arp.read( message.data )
    rescue => ex
      return
    end

    arp_reply = Pio::Arp::Reply.new(
      :source_mac => @VIF_mac,
      :destination_mac => message.macsa,
      :sender_protocol_address => message.arp_tpa,
      :target_protocol_address => message.arp_spa
    )

    flood( datapath_id, OFPP_NONE, arp_reply.to_binary )
  end

  def handle_virtualIF( datapath_id, message )
    if ( message.arp_request? )
      handle_arp( datapath_id, message )
    end

    if ( message.icmpv4_echo_request? )
      handle_icmpv4_echo_request( datapath_id, message )
    end
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    # padding. length of sending data must be >= 60
    data_len = message.data.length
    if ( data_len < MIN_PACKET_DATA_LEN )
      data = message.data + "\x00" * ( MIN_PACKET_DATA_LEN - data_len )
    else
      data = message.data
    end

    # Flooding
    flood( datapath_id, message.in_port, data )

    # Virutal Interface
    if ( message.macda == @VIF_mac or message.macda.broadcast? )
      handle_virtualIF( datapath_id, message )
    end

  end

end

青字のところ:フラッディング処理を関数にまとめました。
赤字のところ:今回の仮想インターフェース用に追加したコードです。ARP Request への応答、ICMP Echo Request への応答のコードを追加しています。
仮想インターフェースの MAC アドレスと IPアドレスは、あらかじめ @VIF_mac と @VIF_ipv4addr の変数に入れています。Pio のライブラリを使っているのは、Trema側のPacketIn メッセージで  Pio を使うようになっていたため合わせました。(どこかのバージョンで Pio を使うように Trema の実装が変更されたようです。)

作成したデータを送信する際にイーサネットのフレームとしての最少バイト数を確保するようにしないといけないのですが、to_binary メソッドを見るとそのための処理が入っていたので今回のコードでは何もしていません。
ARP: https://github.com/trema/pio/blob/develop/lib/pio/arp/format.rb
ICMP : https://github.com/trema/pio/blob/develop/lib/pio/icmp/format.rb

■テスト用仮想ネットワークのコード

trema run 実行時に -c オプションで渡す仮想ネットワークのコードを以下に示します:

vswitch ( "vswitch1" ) {
  datapath_id 0x01
}

1.upto(3) do | each |
    netns ( "vhost#{ each }" ) {
      ip "192.168.0.#{ each }"
      netmask "255.255.255.0"
    }

    link "vswitch1", "vhost#{ each }"
end

仮想ネットワークのコードでは Ruby のプログラムが使えるので、3つの仮想ホストの生成のコードをまとめるようにしています。仮想ホスト vhost1 ~ vhost3 に 192.168.0.1 ~ 192.168.0.3 の IP を割り当てています。
仮想ホストは netns を使ってネットワークネームスペースとして生成します(仮想ホスト上で ping などのコマンドを使いたいため)。

■テスト結果

(1) vhost1 にて ARP キャッシュを確認します。
起動直後なので何も入っておらず何も表示されません。

$ sudo ip netns exec vhost1 arp

(2) vhost1 から vhost2 へ ping を実行したのち、ARP キャッシュを確認します。
vhost2 の IP アドレスに対応した MAC アドレスが学習されたことが確認できます。

$ ip netns exec vhost1 ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_req=1 ttl=64 time=28.8 ms
64 bytes from 192.168.0.2: icmp_req=2 ttl=64 time=25.9 ms
64 bytes from 192.168.0.2: icmp_req=3 ttl=64 time=26.2 ms

--- 192.168.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 25.950/27.013/28.849/1.303 ms
$ $ sudo ip netns exec vhost1 arp アドレス               HWタイプ  HWアドレス         フラグ マスク インタフェース 192.168.0.2           ether    1e:29:d4:1a:c0:24   C                 trema0-1

(3) vhost1 から今回作成した仮想インターフェースに対して ping を実行したのち ARP キャッシュを確認します。

$ sudo ip netns exec vhost1 ping -c 3 192.168.0.251
PING 192.168.0.251 (192.168.0.251) 56(84) bytes of data.
64 bytes from 192.168.0.251: icmp_req=1 ttl=128 time=60.0 ms
64 bytes from 192.168.0.251: icmp_req=2 ttl=128 time=44.2 ms
64 bytes from 192.168.0.251: icmp_req=3 ttl=128 time=38.9 ms

--- 192.168.0.251 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 38.967/47.745/60.010/8.940 ms
$
$ sudo ip netns exec vhost1 arp アドレス               HWタイプ  HWアドレス         フラグ マスク インタフェース 192.168.0.251         ether    02:00:00:00:00:01   C                 trema0-1 192.168.0.2           ether    1e:29:d4:1a:c0:24   C                 trema0-1

ping にきちんと応答が返ってきているので当たり前ですが、ARP キャッシュには 192.168.0.251 に対して MAC アドレス 02:00:00:00:00:01 が学習されています。


参考: Pio について

概要は以下あたりが参考になります:
http://www.slideshare.net/ssuser6d53d5/trema-day4-pio-trema

ソースやサンプルなどはこちら:
https://github.com/trema/pio

2014年7月13日 (日)

◆ 単純なブリッジの実装その4~Open vSwitch対策~【OpenFlow1.0をTremaの土管として使う】

以前書いた記事『◆ 単純なブリッジの実装その3~より完璧な土管化~【OpenFlow1.0をTremaの土管として使う】』の保守リリースです。

Open vSwitch と接続した場合に、イーサネットの最低フレーム長に満たないデータが packet_in に入ってくる現象が発生し、そのまま転送しようとすると Trema が終了してしまうので対策することにしました。
こういうケースを想定した処理は堅牢なプログラムにするためには必要でしたね。

■発生環境
  ・OS: Lubuntu 13.04 Desktop (i386)
  ・Trema: 0.4.7
  ・OpenFlowスイッチ: Open vSwitch 1.9.0 (※同OS上に apt-get でインストール)
  ・仮想ホストとして、ネットワークネームスペース

※記事『◆ Lubuntu 13.04 に Open vSwitch をインストールした時のログ

 作成方法詳細は後述します。

■現象
  以下のメッセージを出力して Trema が停止する。

The length of the provided Ethernet frame is shorter than the minimum length of an Ethernet frame (= 64 bytes).

■原因
  送信データが、イーサネットの最小フレームサイズ(64バイト)に満たないため。

  ARP パケットを受け取った時に packet_in に入ってくる PacketIn メッセージの data のサイズが 42 バイトで渡ってくる。これをそのまま send_packet_out で送ろうとすると、
  42 + 4(FCS分) = 46 バイト
となり、64バイトに満たない。

■対策
  send_packet_out に渡すデータのデータ長の最低長が、64 から FCS分の4バイトを除いた 60 バイトとなるようにする。
  send_packet_out で送る data が 60 バイトに満たない場合、後ろを 0 で埋めて 60 バイトにする。

■ソースコード
赤字が変更箇所です。

class SimpleBridge4 < Controller
  MIN_PACKET_DATA_LEN = 60

  def start
    @port_list = [ 1, 2, 3 ]    # all port numbers
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    # padding. length of sending data must be >= 60
    data_len = message.data.length
    if ( data_len < MIN_PACKET_DATA_LEN )
      data = message.data + "\x00" * ( MIN_PACKET_DATA_LEN - data_len )
    else
      data = message.data
    end

    actions = [ ]
    @port_list.each do | port |
      if ( message.in_port != port )
        actions.push( SendOutPort.new( port ) )
      end
    end

    send_packet_out( datapath_id,
      :in_port => message.in_port,
      :data => data,
      :actions => actions )   end end

■再現環境

Photo_2

(1)再現環境の作り方
以下のコマンドを実行します。当然、すでに Open vSwitch がインストール済であることが前提です。

sudo ovs-vsctl add-br ofs1
sudo ovs-vsctl set-fail-mode ofs1 secure

sudo ip netns add vhost1
sudo ip link add name vlink1-0 type veth peer name vlink1-1
sudo ip link set vlink1-1 netns vhost1
sudo ip netns exec vhost1 ifconfig vlink1-1 up 192.168.0.1
sudo ip link set vlink1-0 up
sudo ovs-vsctl add-port ofs1 vlink1-0

sudo ip netns add vhost2
sudo ip link add name vlink2-0 type veth peer name vlink2-1
sudo ip link set vlink2-1 netns vhost2
sudo ip netns exec vhost2 ifconfig vlink2-1 up 192.168.0.2
sudo ip link set vlink2-0 up
sudo ovs-vsctl add-port ofs1 vlink2-0

sudo ovs-vsctl set-controller ofs1 tcp:127.0.0.1:6653
sudo ovs-vsctl set controller ofs1 connection-mode=out-of-band
sudo ovs-vsctl set controller ofs1 inactivity-probe=180

(2)現象を発生させるコマンド
192.168.0.2 への ARP キャッシュを消して、必ず ARP リクエストが生じるようにしてから ping を実行します。
あらかじめ Trema を実行させておきスイッチと接続が完了したことを確認した上で、以下のコマンドを実行します。1つ目のコマンド実行時に、ARPエントリがありません、といったメッセージがでるかもしれませんが構いません。

 sudo ip netns exec vhost1 arp -d 192.168.0.2
 sudo ip netns exec vhost1 ping -c 10 192.168.0.2

(3)再現環境の削除の仕方

 sudo ovs-vsctl del-br ofs1
 sudo ip netns delete vhost1
 sudo ip netns delete vhost2

以上

2014年7月 5日 (土)

◆ ポートベースVLANの作成【OpenFlow1.0をTremaの土管として使う】

[注意: 本記事は、OpenFlow の適切な使い方ではありません。単にパケットの出し入れの道具として利用しています。]

前回のブリッジのコードをもとにしてポートベースVLAN を実装し、動かしてみました。

■確認環境
  OS: Lubuntu 13.04 Desktop (i386)
  Trema: 0.4.7

■構成
  スイッチは、4ポート。
  ポート1,2で VLAN1 を構成。
  ポート3、4で VLAN2 を構成。

  各ポートにはホストを接続。

  Trema の仮想ネットワーク構成ファイル(h4s1.conf)は以下の通り:

vswitch ( "vswitch1" ) {
  datapath_id 0x01
}

vhost ( "vhost1" ) {
  mac "02:00:00:00:00:01"
  ip "192.168.1.1"
  netmask "255.255.255.0"
}

vhost ( "vhost2" ) {
  mac "02:00:00:00:00:02"
  ip "192.168.1.2"
  netmask "255.255.255.0"
}

vhost ( "vhost3" ) {
  mac "02:00:00:00:00:03"
  ip "192.168.1.3"
  netmask "255.255.255.0"
}

vhost ( "vhost4" ) {
  mac "02:00:00:00:00:04"
  ip "192.168.1.4"
  netmask "255.255.255.0"
}

link "vswitch1", "vhost1"
link "vswitch1", "vhost2"
link "vswitch1", "vhost3"
link "vswitch1", "vhost4"

図にすると:
Photo_2
※注意!!
Trema の仮想ネットワークではスイッチのポート番号と仮想ホストの対応を指定できません。上記の図における仮想ホストが接続されるポートのポート番号は今回テストした時にたまたまこうだったというだけで、必ずこうなるわけではありません。

■Trema のコード(PortBasedVLAN1.rb)
基本となるコードは前回のブリッジのコードです。

class PortBasedVLAN1 < Controller

  def start
    @port_list = [ 1, 2, 3, 4 ]    # all port numbers

    @port_list_of_vlan = {
      1 => [ 1, 2 ],
      2 => [ 3, 4 ]
    }

    @vlan_id_of_port = { }
    @port_list_of_vlan.each do | vlan_id, vlan_port_list |
      vlan_port_list.each do | port |
        @vlan_id_of_port[ port ] = vlan_id
      end
    end

    @port_of_mac = { }
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    learned_port = @port_of_mac[ message.macsa.to_i ]
    if learned_port == nil or learned_port != message.in_port
      info "MAC[%s] is found at Port #{ message.in_port }." % message.macsa.to_s
      @port_of_mac[ message.macsa.to_i ] = message.in_port
    end

    vlan_id = @vlan_id_of_port[ message.in_port ]
    vlan_port_list = @port_list_of_vlan[ vlan_id ]

    actions = [ ]
    vlan_port_list.each do | port |
      if ( message.in_port != port )
        actions.push( SendOutPort.new( port ) )
      end
    end

    send_packet_out( datapath_id,
      :in_port => message.in_port,
      :data => message.data,
      :actions => actions )
  end

end

緑のところ:
ポート数を4つに増やしています。

青いところ:
仮想ホストがどのポートにつながっているか分かるようにメッセージ出力するためのコードです。MACアドレスをキーにしたハッシュにポート番号を格納しています。受け取ったパケットが、まだ記録がないMAC から届いた場合か記録があってもポート番号に変更があった時にメッセージ出力するようにしています。

赤いところ:
start メソッド
VLAN ごとに含まれるポート番号の配列を @port_list_of_mac 変数にあらかじめ指定しています。VLAN ID をキーとしたハッシュです。
ポート番号から VLAN ID を求めるため @vlan_id_of_port のハッシュをあらかじめ計算しています。

packet_in メソッド
ポート番号から VLAN ID を求めて、その VLAN のポート番号の配列を求めています。
以前は、すべてのポートに対してパケットを送信していた箇所を、入力ポート番号が属する VLAN の各ポートにパケットを送信するように変更しています。

■テスト

trema を起動します。

$ trema run PortBasedVLAN1.rb -c h4s1.conf
Switch[0x0000000000000001] is up.

Lubuntu の network-monitor サービスを停止していないと、勝手にパケットが飛ぶので仮想ホスト以外のMACが検出されてメッセージが出力されますがこれらは無視してください。

別のターミナルウィンドウを開き、各仮想ホストからパケットを送ります:

$ trema send_packet -s vhost1 -d vhost2
$ trema send_packet -s vhost2 -d vhost3
$ trema send_packet -s vhost3 -d vhost4
$ trema send_packet -s vhost4 -d vhost1

そうすると trema を起動した画面に以下のように各仮想ホストのMACが検出されてどのポートにつながっているか確認できます:

MAC[02:00:00:00:00:01] is found at Port 3.
MAC[02:00:00:00:00:02] is found at Port 1.
MAC[02:00:00:00:00:03] is found at Port 2.
MAC[02:00:00:00:00:04] is found at Port 4.

上記のポートと仮想ホストの接続関係を前提に通信のテストを行います。

まず、テストの前に以下のコマンドで統計情報をクリアしておきます:

$ trema reset_stats

(1) テスト1: VLAN をまたがった通信
ポート3の vhost1 からポート1 の vhost2 へパケットを送り vhost2 側の受信パケットを確認してみます。

$ trema send_packet -s vhost1 -d vhost2
$ trema show_stats -r vhost2

ポート3 は VLAN2, ポート1 は VLAN1 に属し異なる VLAN なのでパケットが届かず vhost2 では何もパケットを受信していないので空行が表示されます。

(2) テスト2: VLAN 内の通信
ポート2 の vhost3 からポート1 の vhost2 へパケットを送り vhost2 側の受信パケットを確認してみます。

$ trema send_packet -s vhost3 -d vhost2
$ trema show_stats -r vhost2
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.1.2,1,192.168.1.3,1,1,50

VLAN1 内の通信なので受信パケットが vhost2 に届いています。

(3) その他のポートの組み合わせも試してみます

$ trema send_packet -s vhost3 -d vhost4
$ trema show_stats -r vhost4

$ trema send_packet -s vhost1 -d vhost4
$ trema show_stats -r vhost4
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.1.4,1,192.168.1.1,1,1,50

こちらも、VLAN内の通信となる場合だけパケットが受信できています。

以上です。

2014年6月29日 (日)

◆ 単純なブリッジの実装その3~より完璧な土管化~【OpenFlow1.0をTremaの土管として使う】

[2014/07/27 追記。イーサネットの最少パケットサイズを確保する修正をした記事を書きました。「◆ 単純なブリッジの実装その4~Open vSwitch対策~【OpenFlow1.0をTremaの土管として使う】」]

[注意: 本記事は、OpenFlow の適切な使い方ではありません。単にパケットの出し入れの道具として利用しています。]

以前書いた記事「◆ 単純なブリッジの実装その2~複数ポート対応~【OpenFlow1.0をTremaの土管として使う】」のコードを、パケットのデータを加工して流すことにも対応できるように修正してみます。

先の記事のコードでは、パケットを送付する際、Trema の send_packet_out に引数 :packet_in を使って受信メッセージをそのまま送信データとして渡しています。仕様を確認してみましょう。
http://rubydoc.info/github/trema/trema/master/Trema/Controller:send_packet_out

packet_in (PacketIn) — default: nil
— The PacketIn object received by packet_in handler. If this option is not nil, :buffer_id,
:data, and :in_port option is set automatically
according to the value of :packet_in.

:packet_in を使うと自動で :buffer_id, :in_port, :data を設定します。

OpenFlow では、コントローラからはパケットのデータそのものを渡すのではなくスイッチ側で管理している識別子 buffer_id を渡すという仕掛けがあります。コントローラでデータを加工して送付したい場合にはこの機能は不要です。
openflow 1.0.0 仕様書の "25.3.6 Send Packet Message" には以下の説明があります:
http://archive.openflow.org/documents/openflow-spec-v1.0.0.pdf

/* uint8_t data[0]; */ /* Packet data. The length is inferred
                          from the length field in the header.
                          (Only meaningful if buffer_id == -1.) */

このカッコ書きのところがポイントです。データに変更を加えたパケットを送付するには buffer_id を指定してはいけないということです。

Trema の send_packet_out の引数 :buffer_id を確認してみると:

:buffer_id (Number) — default: 0xffffffff
— The buffer ID assigned by the datapath.
If 0xffffffff, the frame is not buffered, and
the entire frame must be passed in :data.

:buffer_id はデフォルトのままで引数 :data に指定したデータを送付します。

以上から、:packet_in を使うのではなく、:data, :in_port を指定すれば良いということがわかります。

■対策後のコード
赤字のところが変更箇所です。

class SimpleBridge3 < Controller

  def start
    @port_list = [ 1, 2, 3 ]    # all port numbers
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    actions = [ ]
    @port_list.each do | port |
      if ( message.in_port != port )
        actions.push( SendOutPort.new( port ) )
      end
    end

    send_packet_out( datapath_id,
      :in_port => message.in_port,
      :data => message.data,

      :actions => actions )
  end

end

2013年11月 2日 (土)

◆ 単純なブリッジの実装その2~複数ポート対応~【OpenFlow1.0をTremaの土管として使う】

前回作成した簡単なブリッジは2ポートのみにしか対応していませんでしたが、今回は3ポート以上にも対応するように修正しました。

■Trema のソースコード

class SimpleBridge2 < Controller

  def start
    @port_list = [ 1, 2, 3 ]    # all port numbers
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    actions = [ ]
    @port_list.each do | port |
      if ( message.in_port != port )
        actions.push( SendOutPort.new( port ) )
      end
    end

    send_packet_out( datapath_id,
      :packet_in => message,
      :actions => actions )
  end

end

■解説

変更箇所を赤字にしています。

start

対象とするポート番号を持つ配列である @port_list には、上記コードでは例として1,2,3の3つのポート番号を指定しています。ここには3つでも4つでも対象のポート番号を指定できます。

packet_in

変数 actions は新規に追加した変数です。SendOutPort.new で生成した出力先ポート番号指定の配列です。パケットが入って来たポート(message.in_port) 以外の対象ポート番号すべてに出力するようにしています。

2013年10月24日 (木)

◆ 単純なブリッジの実装【OpenFlow1.0をTremaの土管として使う】

[2013/10/25 追記。テスト結果を加執しました]

前回の記事の方針をもとにして、2ポートのブリッジを作成しました。今回は何も機能がなくて、MAC の学習機能もフィルタリング機能もなく、リピーターと変わりません。

■Tremaのコード

class SimpleBridge < Controller

  def start
    @port_list = [ 1, 2 ]	# all port numbers
  end

  def switch_ready( datapath_id )
    info "Switch[0x%016x] is up." % datapath_id

    send_flow_mod_add( datapath_id, :priority => 100 )

    @port_list.each do | port |
      send_flow_mod_add( datapath_id,
        :match => Match.new( :in_port => port ),
        :actions => SendOutPort.new( OFPP_CONTROLLER ) )
    end
  end

  def switch_disconnected( datapath_id )
    info "Switch[0x%016x] is disconnected." % datapath_id
  end

  def packet_in( datapath_id, message )
    return unless message.reason == Trema::PacketIn::OFPR_ACTION

    if ( message.in_port == @port_list[0] )
      target_port = @port_list[1]
    else
      target_port = @port_list[0]
    end

    send_packet_out( datapath_id,
      :packet_in => message,
      :actions => SendOutPort.new( target_port ) )
  end

end

■解説

実現しているのは以下の機能:
(1) 1番ポートから入って来たパケットを2番ポートへ送る。
(2) 2番ポートから入って来たパケットを1番ポートへ送る。
それだけです。

start
対象とする2つの OpenFlow ポートの番号をインスタンス変数の配列 @port_listに格納しています。
ここを書き換えれば 1, 2 番以外のポートを対象にすることができます。

switch_ready
OpenFlowスイッチが接続してきたらフローエントリを登録します。
登録しているエントリは3つです。

・フローエントリ1
パケットを捨てる。

優先度(priority)を 100 と低くしています。これにより、次に示すフローエントリ2,3に合致しないパケットに対して適用されるフローエントリにしています。
つまり、対象とする2つの OpenFlow ポート以外のパケットは捨てるという動作を OpenFlow スイッチにさせています。
      
・フローエントリ2、および 3
対象とする2つの OpenFlow ポートに入ってきたパケットを OpenFlow コントローラに送る。

Trema のデフォルトでは優先度は 65535 でフローエントリを登録するので 100 より優先されます。
参照:http://rubydoc.info/github/trema/trema/master/Trema/Controller:send_flow_mod_add

    :priority (Number) ? default: 0xffff  - The priority level of the flow entry.


packet_in
1行目は、パケットが入ってきた理由が OFPR_ACTION (アクションにより届いたパケット)かどうかを判定しています。
フローエントリ2、3以外の理由でパケットが入ってきた場合は return で抜けて何もしない(=パケットを捨てる)ようしています。
これにより、フローエントリの登録が完了する前にOpenFlowスイッチが受信したパケットが要因で packet_in が呼ばれた場合でも何もしないようにしています。
前回の記事で考察したように、パケットのデータに欠けがないパケットのみを処理対象にするためにアクションにより届いたパケットのみを扱うようにしています。

2行目からは、入って来たポート番号を確認し、もう一方のポートにパケットを送りだしています。


では、実行してみます。

■実行環境
・OS: Lubuntu 13.04 Desktop (i386)
・Trema: 0.4.3
・Ruby: 1.9.3p194

■Trema の仮想ネットワークによる実行

(1) 仮想ネットワーク定義

vswitch("ofs1") {
  datapath_id "0x1"
}
netns("host1") {
  ip "192.168.0.1"
  netmask "255.255.255.0"
}
netns("host2") {
  ip "192.168.0.2"
  netmask "255.255.255.0"
}
link "host1", "ofs1"
link "host2", "ofs1"

(2) 実行前の下準備

Trema の仮想ネットワークに勝手にパケット流して邪魔するのでテストの間はネットワークマネージャを一時停止しておきます。

$ sudo service network-manager stop
network-manager stop/waiting

(3) Trema の起動

ここでは、Trema のコードのファイル名を SimpleBridge.rb とし、仮想ネットワーク定義のファイル名を vnet-2netns.conf としています。

$ trema run SimpleBridge.rb -c vnet-2netns.conf
Switch[0x0000000000000001] is up.

(4) ping の実行

別の端末を用意して、host1 から host2(192.168.0.2) へ ping をパケットサイズ1000で実行します。

$ sudo ip netns exec host1 ping -c3 -s1000 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 1000(1028) bytes of data.
1008 bytes from 192.168.0.2: icmp_req=1 ttl=64 time=81.8 ms
1008 bytes from 192.168.0.2: icmp_req=2 ttl=64 time=29.5 ms
1008 bytes from 192.168.0.2: icmp_req=3 ttl=64 time=28.7 ms

--- 192.168.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 28.799/46.737/81.846/24.828 ms

128バイトを超えるパケットもきちんと応答が返りました。テスト成功です。

(5) Trema の終了

以下のコマンドでTremaを終了させます:

$ trema killall

すると、trema run を実行していた端末では以下が出力されて trema が終了しています。

Switch[0x0000000000000001] is disconnected.

(6) 必要なら...ネットワークマネージャの再起動

以下のようにして再起動します:

$ sudo service network-manager start
network-manager start/running, process 313

上記の313の箇所はプロセスIDなので実行ごとに異なる値です。

最近のトラックバック

無料ブログはココログ