Trema でパケットを生成する話

 今回は Trema の小ネタです。Ruby 版の Trema にて、OpenFlow コントローラでパケットを生成してスイッチに送るという処理をやってみたいと思います。

 

 Trema は C で書かれたコアコンポーネントRuby から呼び出すというアーキテクチャになっています。C で記述したコントローラでは、Trema のコアコンポーネントが提供する API を直接叩けるので、凝った処理ができるのですが。Ruby では完結で柔軟な記述が出来る代わりにそれが難しいです。

 今回は Ruby 版の Trema において、コントローラにおいてパケットを生成してスイッチに送るといった、ちょっとだけ凝った処理を行うプログラムの紹介をします。尚 "パケットを送る" と言いましたが、厳密には "空のペイロードEthernet フレーム" を送ります。なのでここでは、パケットと Ethernet フレームを区別なく扱います。

 

 Ruby 版の Trema は、パケット本体を指定して、データプレーンにパケットを送る OpenFlow メッセージ (Send-Packet メッセージ) のインターフェイスは提供していますが、(ざっと見た限りでは)パケットの生成を支援する Ruby コードは見当たりません。

 ちなみに C 版のコントローラでパケットを生成してデータプレーンに流すには、以下のようにします。

 

static void
send_ether_frame( const uint8_t *macda, const uint8_t *macsa, uint16_t type, size_t buflen )
{
 /* Ethernet フレームの生成 */
  buffer *buf = alloc_buffer_with_length( buflen );
  buf->length = buflen;

 /* Ethernet ヘッダの設定 */
  ether_header_t *l2hdr = ( ether_header_t * ) buf->data;
  memcpy( l2hdr->macda, macda, ETH_ADDRLEN );
  memcpy( l2hdr->macsa, macsa, ETH_ADDRLEN );
  l2hdr->type = type;

 /* アクションリストの設定 */
  openflow_actions *actions = create_actions();
  append_action_output( actions, OFPP_FLOOD, 0 );

  /* Send-Packet メッセージの生成 */
  buffer *packet_out = create_packet_out( get_transaction_id(), UINT32_MAX, OFPP_NONE, actions, buf );

 /* Send-Packet メッセージの送信 */
  send_openflow_message( dpid, packet_out );

  free_buffer( buf );
}

 

 send_ether_frame は、中身が空の Ethernet フレームを生成してスイッチに送る関数です。

 フレームの生成にはまず、Trema が提供する alloc_buffer_with_length() を呼び出してメモリ領域を確保し、データの先頭を ether_header_t 型オブジェクトにキャストしてやって、Ethernet ヘッダを設定します。あとは、こいつを引数に (適当なアクションリストを設定して) create_packet_out() 関数を呼んでやれば、生成した Ethernet フレームをスイッチに送ることができます。

 それでは本題の、Ruby 版 Trema コントローラで、生成したパケット (Ethernet フレーム) をデータプレーンに流すコントローラを記述します。

 

# filename packet_generator.rb
class PacketGenerator < Controller
  def switch_ready dpid
    # generate ethernet frame and send it

  macda = Mac.new( 'FF:FF:FF:FF:FF:FF' )
  macsa = Mac.new( dpid & 0xffffffffffff )

    send_packet_out dpid,
      :actions => ActionOutput.new( :port => OFPP_FLOOD ),
      :data => make_ether_frame( macda.to_s, macsa.to_s, '0800', '00' * 50 )
  end

  def packet_in dpid, msg
    puts "[packet_in] (dpid:%016x) macsa:#{msg.macsa}, macda:#{msg.macda}" % dpid
  end

  private
  def make_ether_frame macda, macsa, type, payload
    header = macda.split(':').join + macsa.split(':').join + type

    [ header + payload ].pack "H*"
  end
end

 

 Send-Packet メッセージを送る API (send_packet_out) の :data パラメータに送信するパケット (Ethernet フレーム) を設定します。作成する Ethernet フレームは、make_ether_frame 関数で作成します。make_ether_frame 関数では、Ethernet ヘッダのパラメータとペイロードのデータを受け取り、Array#pack メソッドによってバイナリ変換します(個人的にはこの処理を隠すコードが欲しいです)。そうして作成した Ethernet フレームのバイナリデータを返します。

 ここでは、作成した Ethernet フレームのヘッダが正しく読まれる事を確認するだけなので、上位プロトコルを表す Ethernet-Type に 0800 (IPv4) を設定していますが、ペイロード部分には空の値 (00) を設定しています。ただし、仕様 (ieee-802.3) でフレームの最短長が 64 byte に設定されているので、50 byte のサイズのペイロードデータを設定しています。

 

 さてではこいつを動かしてみます。

 ノード間で作成したパケットを送り合い、お互いに受信したパケットの Ethernet ヘッダに設定した正しいデータが格納されているかを確認します。

 動作の確認は、例によって Trema ネットワークエミュレータを利用します。以下が、利用したネットワークエミュレータの設定ファイルです。

 

# filename: test.conf
vswitch("node01") { datapath_id "0x0001020304050607" }
vswitch("node02") { datapath_id "0x08090a0b0c0d0e0f" }

 

 作成したコントローラを実行すると次のような出力が得られます。

 

# trema run ./packet_generator.rb -c ./test.conf 
[packet_in] (dpid:08090a0b0c0d0e0f) macsa:02:03:04:05:06:07, macda:ff:ff:ff:ff:ff:ff
[packet_in] (dpid:0001020304050607) macsa:0a:0b:0c:0d:0e:0f, macda:ff:ff:ff:ff:ff:ff

 

 Switch-Ready イベントハンドラ作成した Ethernet フレームの MAC アドレスが、Packet-In メッセージハンドラでキチンと読めていますね。