Trema/Pioでパケットを作ろう(2)
続きです。
IP, UDP の話
Checksum 計算
Gist みればわかると思いますが、simple-router の中に router-utils.rb ってのがあるんですね。これの checksum 計算用のルーチンを丸パクリです。チェックサム計算のコード自体はなんか ruby っぽくないので、まだリファクタリングの余地があると思いますが。
チェックサム計算についてはなかなかわかりやすい資料がないんですよねえ…。Web上の資料としては以下の物が参考になりました。
- TCP/IP通信プログラミング Ver.2
- Blog Alpha Networking: IP、TCP/UDP チェックサムについての覚え書き
- チェックサム計算。最後はやっぱり手計算で検算してたりするので…。
- IP チェックサムの秘密
- なぜ "1 の補数和の 1 の補数" なのか? という話。
- 基礎から学ぶWindowsネットワーク:第10回 IPパケットの構造とIPフラグメンテーション (2/3) - @IT
- 基礎から学ぶWindowsネットワーク:第13回 データグラム通信を実現するUDPプロトコル (3/4) - @IT
今回何を間違えたって、udp checksum の処理内容を間違えてました。具体的には、udp checksum って "疑似ヘッダ+UDPヘッダ+データ(payload)" に対してチェックサム取らなきゃいけないんだけど、疑似ヘッダ+データ に対してだけチェックサム計算をしていた(UDPヘッダを含めるのを忘れていた)というところでした。
Wireshark で checksum validation をする
UDP の checksum validation は optional なので、というのもあるのでしょうが、Wireshark ではデフォルトでは udp checksum validation は無効になっています(下図参照)。有効にする方法をメモっておきます。
メニュー [Edit]-[Preferences]
"Protocols"-"UDP"
"Validate the UDP checksum if possible:" にチェックを入れます。
checksum が "validation disabled" から "Correct" に変わりました。Checksum 計算が間違えてると赤字で警告されます。
Cisco tcp/udp small servers
Echo server は実は Cisco のルーティング機能を持つ機器(L3SW/Router)であれば使うことができたりしますね。
デフォルトでは disable になっているので
conf t service udp-small-servers service tcp-small-servers
するだけです。echo/discard/chargen (tcp/udp), daytime(tcp) が使えるようになる。とはいえ、コレは本当に server が動くだけで、どういうデータを受け取ったかとかの情報とかわからないんだよね…。こっちが作ったパケットに不備があるとかエラーがある場合はただ応答がないのでうーんと言う感じ。届いているかどうかすらわからないから結局パケットキャプチャ大会だったし。
結局どう使ったかというと、echoping で Cisco に udp echo おくって、成功例のパケットキャプチャ取って、request/reply のパケットフォーマットを確認したとかその程度です。
最小フレームサイズ
イーサネットでは最小フレームサイズが 60 byte (FCS 除く)ってことになってるので、UDP datagram + IP Header + Ethernet frame header を足して 64 byte に満たない場合は padding する必要があります。
- IPv4Frame
def to_binary padcount = 0 if @packet.total_length < MIN_PACKET_LEN padcount = MIN_PACKET_LEN - @packet.total_length end @frame_hdr.to_binary_s + @packet.to_binary + ( "\000" * padcount ) end
イーサネットヘッダは 14byte あるので(802.1Qナシの場合) ip packet で 46 byte 必要って事になります。
StringIO
Trema::PacketIn::data はそのままだと(binary)Stringを返します。で、ひとつの String を分割して BinData::read させる場合、IO class 同等の機能で読めるようにしておく必要があります。
BinData だとフィールド宣言時に :read_length
して長さを指定してやればその分のデータを読んでくれるのですが、ethernet header は固定長フィールドで payload の長さの情報がありません。
で、読み込む長さを指定できないので、いまは Ethernet Header とその payload ってことで分割して読んでます。が…ここで通常の String だと、常にアタマからよむわけです。要は ethernet header のあと、ip packet のところから読んで欲しいのに ethernet header から読んじゃうわけです。read
するときはいったん読んだ次のところから読んで欲しい。
- IPv4Frame
def self.read io io = StringIO.new(io, 'r') if String === io frame = IPv4FrameHeader.read io ippct = IPv4Packet.read io IPv4Frame.new( :frame_header => frame, :packet => ippct ) end
ってことで String が引数として渡された場合は StringIO に変換して使うようにしてあります。
- イーサネット - Wikipedia
- ASCII.jp:Ethernetのフレーム構造を理解しよう (1/3)|入門Ethernet
- Ethernet LAN - イーサネットフレームのフォーマットとMACアドレス
…とはいえ、元の packet_in.data の長さ自体はわかるわけで、その辺はもうちょっと工夫してやれば、一発で ethernet header + payload を処理する方法とかありそうな気がするんだが。まあもうちょっと考える。