Trema/Pioでパケットを作ろう(3)
まあ、細かいこと考えてたなかなか先進まないのでやれそうなことやってしまえ、ってことで、TCP packet generator/parser を作ってみました。やってみたメモ。
- コードは Gist へ。
Pioのクラスとファイルの分割
パケットを作ろう(1) で "データとラッパーで分ける" って書いたんだけどまだきれいに分けられていません。クラス・モジュールの構造として、というのと、ファイルの分割を考えないといけない。いろいろ書いてみたけどファイルサイズ的にもうしんどい。
これPio::Tcp
, Pio::Udp
みたいな形でモジュール分割しておけばよかったなあ。その当たりの整理と、あわせてフォルダとかを分けるか。
IPv4
FrameHeader
(data)Frame
(data)Header
(data)
Tcp
OptionalTLV
(data)- tcp option classes (data)
Datagram
(data)
Udp
Datagram
(data)
ってあたりだろうか。Header class に payload field があって実際にはデータも含んじゃってるってあたりがわかりにくいなあ…というのがあるので wrapper の名前は変えるべきだろう。Pio::Lldp
とかは (pio)/lib/lldp.rb
に wrapper class が置いて在るみたいなので、今の "Datagram" class はそっちに変えるべきか。
みたいな感じか。まあ追々ね。
TCP packet generator/parser 実装上のアレコレ
Option
TCP Option も TLV である程度定型であればまだしも、Type だけしかない(EOL, NO-OP) とか, Type と Length だけある ( Sack permitted, Partial order connection permitted) とか、いくつかバリエーションがあってどうにか吸収してやらないといけない。結局共通するフィールドが type しかないので、length/value については別途クラスつくって choice してやるようにしています。
Header Length
TCP ヘッダにある長さの情報はヘッダ長 (data offset) だけで、データ(payload)長を明示的に計算できる情報はないんだよね。TCP datagram 全体の長さかあるいは payload (data) の長さが分かれば、:read_length
で読み込むというのがやれるのだけど、ないので、data offset の後全体を読み込むってのをやらんといかん。
Read Options and TCP payload
ってことで、data offset から後を tcp payload として読んでやらないといけないのだけど。TCP option 読み込むのも結構厄介でね。以下の条件を満たさないといけない。
- EOL までで読むのを止める
- data offsetまで読み込む(読み捨てる)
- Option list が EOL で終わっていない場合がある。
- EOL が Option list の中間に挟まっている可能性がある。
ということでこういう感じにしてみた。
class TcpHeader < BinData::Record endian :big ## 略 ## array :optional_tlv, :type => :tcp_optional_tlv, :onlyif => :has_options?, :read_until => lambda { @readbytes ||= header_length_in_bytes - 20 @readbytes = @readbytes - element.bytesize element.end_of_option_list? or @readbytes <= 0 } skip :length => lambda { @readbytes or 0 } rest :payload def has_options? data_offset > 5 end ## 略 ## end
skip
:length
で指定したバイト数だけ読み飛ばすことができる。:optional_tlv
が在る場合は、:read_until
で読んだオプションのサイズをカウント(カウントダウン)しておく。この数字が残っていれば末尾に EOL padding があるか option list の中間に EOL が入っている状況で、いずれもそこから後は読み飛ばしていい。- もし option がなければ
:optional_tlv
自体を含めないようにする。(:onlyif
で data offset が 5(=20byte), つまり option がない場合にはこのフィールドを含めない。これをやらないと、payload の最初の1バイトを:optional_tlv
で読んじゃってたので。
rest
- 「残り全部」。入力が Stream になってないと使えない…とかあったはず。
- 最初、
count_bytes_remaining
(Stream の残りのバイト数を出す) ってのでやろうとしたんだけど、どうもコレ思ったように動かなかったんだよね…。
Generate options
オプション生成は、symbol table から type value 出してあとは引数全部そのまま渡すだけです。なんかもうちょいうまくかけないもんかねえこれ。ホントは wrapper 側で EOL やら NO-OP やら自動的に埋めてくれるようにするのがいいんだろうけどね。
options = [ { :mss => { :segment_size => 1460 } }, :noop, { :wsopt => { :shift_count => 8 } }, :noop, :noop, :sackp ] tdgm = Pio::TcpDatagram.new( # 略 :optional_tlv => options, :payload => payload, )
いまのところ、EOL(0), NO-OP(1), MSS(2), Windows Scale Option(3), Sack Permitted(4), SACK(5) までは実際のデータ(パケットキャプチャしたデータ)を元にデータ作れるってのが見えてます。他のオプションは、一応書いてあるけど未テスト。
simple-router + TCP echo server
Pio::TcpServer
で、なんちゃって tcp state machine を実装してみた。が、seq/ack の計算とか session close 処理とかもろもろ怪しいです…けどまあ気にしない方向で。一応 tcp echo server としては動きます。
Pio は packet generate/parse の機能だとするならこういう機能は Pio ではないよな…。
- TCP SYN from client
[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 : 52 (octets) identificat : 13999 flags : 2 frag offset : 0 ttl : 128 protocol : 6 checksum : 4061 src addr : 192.168.1.16 dst addr : 192.168.1.83 options : valid? : true src port : 51279 dst port : 7 seq number : 64adb478 ack number : 00000000 data offset : 8 ctrl flag : 000010 window size : 2000 checksum : e9df urg pointer : 0000 options : 2, 1, 3, 1, 1, 4 payload : valid? : true
Checksum Calculation
ヘッダフィールド値を wrapper 側で個別にクラス変数として持って計算している。が、これは別に、checksum = 0 な data object をつくって to_binary_s
して、binary string に対して計算してやっても別にかまわないのだよな。アクセサならほとんど委譲しちゃうだろうし。
いま router-utils.rb
を元に書いたところがあるのと、疑似ヘッダ部分の計算を加えてやらなければいけないのとで、こういう書き方になってる。が、checksum 計算についてはバイトオーダーと 16bit 単位での区切りが合っていれば計算順序は問わないし、疑似ヘッダも別途 data class 作ってやってもいいんだよね。そうすると checksum 計算ルーチンは一本化できる…か? Checksum validation も含めて考えないとイカンけど。