Trema/Pioでパケットを作ろう(1)

trema/pio でいろんなパケットを作ってみよう! という話です。
Trema の packet generate/parse の機能が Pio として分離されています。こいつを使えば任意のパケットの生成・送受信機能を作れる。はず。ということで作ってみました。可変長フィールドのある物にいきなり手をつけるのは大変そうなので、最も単純であろう UDP についてどんな感じでやれるのか、作るに当たってどういうことに気をつければ良いのか、という話を記憶に残っている範囲で書いてみます。

道具

  • trema, pio は gem 版のみ使用 (ソースからビルドした物は今回使っていません)
    • trema/0.4.3
    • pio/0.4.2
  • ruby/2.0.0p247 on Ubuntu 13.04 (64bit)

最初は ruby1.8.7 な環境使ってたんですが、最近 Trema が 2.0 でも動くようになったということ知ったので、rvm + ruby2.0 な環境に全部移行する方向。

作ってみた物と動作

かなりこなれていないお試しプログラムなのでとりあえず gist に貼り付けておきます。

  • simple-router + udp echo server function using Pio
    • packetgen.rb : パラメタに従ってIPパケットを生成します。
    • packetread.rb : (packetgen.rb で生成した)バイナリを読んでフィールドを表示します。
    • pio-l4hdr.rb : IP Packet, UDP Header 関連
    • simple-router-echosrv.rb : simple-router を改造して udp echo server 機能を追加 (ついでに ARP 処理周りも Pio::Arp で置き換えてある)

packetgen/read は単純にテスト用のツールです。二つで一組。simple-router-echosrv.rb は Trema controller app. です。simple-router は自身のもつ interface に対する arp request/reply, icmp request/reply に対応することができるようになっています。今回はそれに加えて udp echo requet/reply にも応答するようになっています。

Ubuntu であれば echoping というツールで udp echo request を送信することができます。Windows だと EchoTool | TCP, UDP Echo server and client for Windows とか。EchoTool は echo server としても動作させられるので意外と重宝しました。

stereocat@lambda00 /cygdrive/c/bin
$ ./echotool.exe 192.168.1.83 /p udp

Hostname 192.168.1.83 resolved as 192.168.1.83

Reply from 192.168.1.83:7, time 22 ms OK
Reply from 192.168.1.83:7, time 24 ms OK
Reply from 192.168.1.83:7, time 19 ms OK
Reply from 192.168.1.83:7, time 21 ms OK
Reply from 192.168.1.83:7, time 24 ms OK

Statistics: Received=5, Corupted=0, Lost=0
stereocat@lambda00 /cygdrive/c/bin
$
  • simple-router-echosrv の設定と動作
stereocat@oftest06:~/training/packetgen$ cat echoserver.conf
# -*- coding: utf-8 -*-

$interface = [
  {
    :port => 2,
    :hwaddr => "54:52:00:01:00:02",
    :ipaddr => "192.168.1.83",
    :masklen => 24
  },
  {
    :port => 1,
    :hwaddr => "54:52:00:01:00:01",
    :ipaddr => "192.168.11.10",
    :masklen => 24
  }
]

$route = [
  {
    :destination => "0.0.0.0",
    :masklen => 0,
    :nexthop => "192.168.1.1"
  }
]
stereocat@oftest06:~/training/packetgen$
stereocat@oftest06:~/training/packetgen$ trema run simple-router-echosrv.rb
[start]
[switch_ready] 17
[features_reply] Datapath ID: 0x11
Port no: 2
  Hardware address: 00:0c:29:ea:f4:d4
  Port name: eth2
Port no: 65534
  Hardware address: 00:0c:29:ea:f4:ca
  Port name: ovsbr0
Port no: 1
  Hardware address: 00:0c:29:ea:f4:ca
  Port name: eth1
[handle_arp_request] 17
[handle_ipv4], 17
  src mac      : 00:02:2a:ea:a3:c9
  dst mac      : 54:52:00:01:00:02
  ether type   : 0800
  version      : 4
  header length: 20 (octets)
  tos          : 0
  total length : 50 (octets)
  identificat  : 25995
  flags        : 0
  frag offset  : 0
  ttl          : 128
  protocol     : 17
  checksum     : 517c
  src addr     : 192.168.1.16
  dst addr     : 192.168.1.83
  options      :
  valid?       : true
  src port     : 63988
  dst port     : 7
  total length : 30 (octets)
  checksum     : a07f
  payload      : UDP echo from lambda00
  valid?       : true
[send_echo_reply]
[make_echo_packet]
src mac  : 54:52:00:01:00:02
dst mac  : 00:02:2a:ea:a3:c9
src addr : 192.168.1.83
dst addr : 192.168.1.16
src port : 7
dst port : 63988
payload  : UDP echo from lambda00
(以下略)

Pio/Bindata の機能開発

Documents

何はなくとも BinData の使い方がわからないとさっぱりですよ。まず BinData のドキュメントを読みましょう。

あとは高宮さんのコレ

データとラッパーでわける

Pio は Bindata を使って作られていますが、基本的には、Bindata::Record あるいは Bindata::Primitive から派生させたデータを扱うクラスと、それに対する処理を実装したラッパー的なクラスから成り立っています。

Pio/0.2.4のファイル構成(原則1ファイル1クラスなのでコレがほぼ Class 依存関係と同等)とかがだいたいこんな感じ。(ちょっと古いかも)


みたいな感じですかね。(←ホントか?)

データ型については、コレはほとんどそのまま BinData 使ってるだけです。バイナリのパース(read/write) なんかコレにお任せ。その他周辺の API (to_hoge とか) についてはラッパーのほうでやります。

read/write を同時に作る

データ型とその操作のための wrapper を作っていくわけですが、read/write を同時に作っていくのがわかりやすいと思います。要は簡易テストをしながら作っていけって話なんですが。テストするためにはデータ(バイナリ)の生成とデータの読み取りができないといけないので。packetgen.rb/packetread.rb を作ったのはそういう理由からです。

  • データを生成する
    • packetgen.rb
    • od -x とかやってだいたいのデータができているかどうかを確認しましょう。
stereocat@oftest06:~/training/packetgen$ ruby packetgen.rb | od -x
0000000 5254 0100 0200 2004 449a 63cf 0008 0045
0000020 2100 c624 0040 1140 fb91 a8c0 5301 a8c0
0000040 6701 0700 aefd 0d00 4c54 6261 6463 0065
0000060 0000 0000 0000 0000 0000 0000
0000074
stereocat@oftest06:~/training/packetgen$
  • データを読み取る
    • packetread.rb
    • BinData read していくだけですが。
stereocat@oftest06:~/training/packetgen$ ruby packetgen.rb | ruby packetread.rb
* ipv4 frame
  src mac      : 04:20:9a:44:cf:63
  dst mac      : 54:52:00:01:00:02
  ether type   : 0800
* ipv4 packet
  version      : 4
  header length: 20 (octets)
  tos          : 0
  total length : 33 (octets)
  identificat  : 46629
  flags        : 2
  frag offset  : 0
  ttl          : 64
  protocol     : 17
  checksum     : 009c
  src addr     : 192.168.1.83
  dst addr     : 192.168.1.103
  options      :
  valid?       : true
* ipv4 payload (proto:17 datagram)
  src port     : 7
  dst port     : 64942
  total length : 13 (octets)
  checksum     : 544c
  payload      : abcde
  valid?       : true
stereocat@oftest06:~/training/packetgen$

最初、まずチェックできるようにするってところまで持っていくのが一つのマイルストーンかな…。Gist に貼り付けた物はいくつかのデータ型(クラス)が連動して動くようになっていますが、1クラス作るごとに read/write のコードをつくってチェックします。

コレがあると何が嬉しいかって、実際のパケットのデータとかでのデバッグや再現性のチェックがやりやすくなる…いや、ないとチェックのしようがない、の方が正しいか。例えばコントローラに組み込んでみててなんぞ Pio でエラー出て停まってしまうというときにどうするか。PacketIn instance に対して puts packet_in.data.unpack("H*") とかやれば実際とれてるデータがわかるわけですが、controller を使った動作って動的すぎて訳がわからんのです。こういうとき、reader が別にあれば

str = ["0000000100020018b9c14fc30800450000242a7140003f118ca4c0a80210c0a8015385f200070010a5986d6a335c5a5f546200000000000000000000"].pack("H*")
ipfrm = Pio::IPv4Frame.read str

みたいな感じで Pio 単体のチェックがやれるようになるわけです。同様に generator についても、パラメタ類を別途設定して同等のデータが生成できるかどうかとか、それで checksum 合ってるのかとか確認することができるわけです。……できるわけですって実際やってたんですが。

もちろん rspec 等でテストを作り込んでもいいんですが、それをやるにしても結局 read/write できないといけないしね。なんだ。単純にちゃんとモックとスタブ作れって話ですね。

まとめ

一般的な Pio 使った開発作業ってどんなものかって話を書いてみました。この辺は何というか…バイナリ慣れ、バイナリ操作慣れをしないとなかなかとっつきにくいです正直。

ちょっと長くなるので、個別のプロトコル(IP/UDP)についてはまた次回エントリで。