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

続きです。

IP, UDP の話

Checksum 計算

Gist みればわかると思いますが、simple-router の中に router-utils.rb ってのがあるんですね。これの checksum 計算用のルーチンを丸パクリです。チェックサム計算のコード自体はなんか ruby っぽくないので、まだリファクタリングの余地があると思いますが。

チェックサム計算についてはなかなかわかりやすい資料がないんですよねえ…。Web上の資料としては以下の物が参考になりました。

今回何を間違えたって、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 で Ciscoudp 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 に変換して使うようにしてあります。

…とはいえ、元の packet_in.data の長さ自体はわかるわけで、その辺はもうちょっと工夫してやれば、一発で ethernet header + payload を処理する方法とかありそうな気がするんだが。まあもうちょっと考える。

その他

あとなんかあったっけ。

  • 802.1Q とか考えないとイカンかな
  • IP Packet の分割とかってどうすっかね
  • TCP もな
  • 名前とか API とか見直した方がよさそうなところが…