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 に貼り付けておきます。
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 依存関係と同等)とかがだいたいこんな感じ。(ちょっと古いかも)
- 共通して使われるデータ型: ethernet-header, mac-address, ip-address
- 共通して使われるデータ型のラッパー: ip(ipv4-address), mac
- 特定プロトコル用のラッパー: lldp, arp
- 特定プロトコルのデータ型: lldp/*.rb arp/*.rb
みたいな感じですかね。(←ホントか?)
データ型については、コレはほとんどそのまま 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 できないといけないしね。なんだ。単純にちゃんとモックとスタブ作れって話ですね。