2014年1月23日木曜日

packメソッドについて補足

router-utilsのソースを読んでいて、最も難解なのがpackメソッドでした。
補足などと言えるほど理解していないかもですが、確認したことをメモしておきます。

例えば"abc123"という文字列をunpack("C*")すると[97, 98, 99, 49, 50, 51]という配列が返ってきます。
配列の数字は文字コードのようです。aの文字コードは97、1の文字コードは49です。

(pack.rb)
p "abc123".unpack("C*")]

$ ruby pack.rb
[97, 98, 99, 49, 50, 51]

次に、unpack("C*")の後にpack("C*").unpack("n*")を追加すると、配列をバイナリにし、その後ビッグエンディアンの16bit 符号なし整数にします。

p "abc123".unpack("C*").pack("C*").unpack("n*")

$ ruby pack.rb
[24930, 25393, 12851]

これはどうなったのかというと、配列の1つ目の24930は97と98をくっつけたものです。

(10進数)→(2進数)
24930→110000101100010

97→1100001
98→1100010

桁を揃えて結合すると同じ値になります

24930→0110000101100010
97,98→0110000101100010

99と49、50と51も同様です。

packのテンプレートの"C"が8bit 符号なし整数なので、丁度配列2つ分です。

続いて、テンプレート文字の"n"は何なのか。
16進数の0xffをpackしてみます。

p [0xff].pack("n*")

$ ruby pack.rb
"\000\377"

0xffの8進数表現になりました。

他の数も試してみます。
p [0x01ff].pack("n*")
p [0x0102ff].pack("n*")

$ ruby pack.rb
"\001\377"
"\002\377"

どうやら8ビットずつを8進数で表示したようです。
0x0102ffは後ろから16ビットだけが変換されたようで、数字が大きいと切られてしまうみたいですね。

router-utilsのソースで見てみます。
例えばICMPEchoReplyクラスの↓この辺など。

124      words = @payload.pack( "C*" ).unpack( "n*" )
125      words.each do | each |
126        @checksum = get_checksum( @checksum, each )
127      end

呼び出し元にさかのぼっていくと元データはmessage.dataです。
message.dataはネットワークに流れるデータなのでバイナリなのでしょう。
unpack("C*")で8bit 符号なし整数の配列にしています。

152      payload = message.data.unpack( "C*" )[ offset .. message.data.length - 1 ]
153      icmp = ICMPEchoReply.new( payload )

その後、(124行目)ICMPEchoReplyクラスでpack("C*")を実行して再びバイナリに変換し、unpack("n*")でビッグエンディアンの16bit 符号なし整数に変換しています。
IPヘッダやICMPヘッダのチェックサム(get_checksumメソッドで計算)は16ビットずつ計算していくので、unpack("n*")で16bitに変換して配列をループで処理しているようです。

以上になります。

2014年1月22日水曜日

Tremaを試す ~simple-ruter その9~

今回はrouter-utilsのソースを前回の続きから見ていきます。

 72  class IPPacket
 73    attr_accessor :id, :protocol, :daddr, :saddr, :payload
 74  
 75    def initialize options
 76      @id = options[ :id ]
 77      @protocol = options[ :protocol ]
 78      @daddr = options[ :daddr ]
 79      @saddr = options[ :saddr ]
 80      @payload = options[ :payload ]
 81      @tot_len = 20 + payload.length
 82    end
 83  
 84    def pack
 85      csum = get_checksum( 0, 0x4500 )
 86      header = [ 0x45, 0x00 ] # Version, IHL, ToS
 87  
 88      csum = get_checksum( csum, @tot_len )
 89      header += [ @tot_len >> 8, @tot_len & 0xff ] # len
 90  
 91      csum = get_checksum( csum, @id )
 92      header += [ @id >> 8, @id & 0xff ] # ID
 93  
 94      csum = get_checksum( csum, 0x4000 )
 95      header += [ 0x40, 0x00 ] # Flags, Frag offset
 96  
 97      csum = get_checksum( csum, 0x40 * 0x100 + @protocol )
 98      header += [ 0x40, @protocol ] # ttl, protocol
 99  
100      csum = get_checksum( csum, @saddr.to_i >> 16 )
101      csum = get_checksum( csum, @saddr.to_i & 0xffff )
102      csum = get_checksum( csum, @daddr.to_i >> 16 )
103      csum = get_checksum( csum, @daddr.to_i & 0xffff )
104      header += [ csum >> 8, csum & 0xff ] # checksum
105      header += @saddr.to_a + @daddr.to_a
106  
107      header.pack( "C*" ) + @payload.pack
108    end
109  end
110  
111  class ICMPPacket
112    attr_reader :payload, :length
113  
114    def initialize type, code, payload
115      @type = type
116      @code = code
117      @payload = payload
118      @length = 4 + payload.length
119    end
120  
121    def pack
122      @checksum = get_checksum( 0, @type * 0x100 + @code )
123  
124      words = @payload.pack( "C*" ).unpack( "n*" )
125      words.each do | each |
126        @checksum = get_checksum( @checksum, each )
127      end
128  
129      [ @type, @code, @checksum ].pack( "C2n" ) + @payload.pack( "C*" )
130    end
131  end
132  
133  class ICMPEchoReply < ICMPPacket
134    def initialize payload
135      super( 0x00, 0x00, payload )
136    end
137  end
138  


72~82行目
IPパケットを作るクラスです。
73行目はアクセサの宣言をしています。

75行目のinitializeで引数を変数にセットしています。
@idには分割したIPパケットを復元する時に使用される識別子が入ります。
@protocolにはICMPやTCPといったをトランスポート層のプロトコルを識別する番号が入ります。
@daddrには宛先のIPアドレスが入ります。
@saddrには送信元のIPアドレスが入ります。
@payloadにはペイロード(転送したいデータ)が入ります。
@tot_lenにはIPヘッダーのバイト長の20と、ペイロードの長さを足した値が入ります。

84~109行目
IPパケットを作るメソッドです。
csum変数にはIPパケットのチェックサムが入ります。
header変数にはIPヘッダのフォーマットに合わせてデータを追加していきます。

86行目はバージョンとヘッダ長、サービス種別をheader変数に追加しています。
IPv4の場合、バージョンには固定で4が入ります。
ヘッダ長にはIPv4ヘッダの固定長部分の長さである20バイトを4バイトで割った数の5が入ります。
サービス種別はこのプログラムでは0x00が固定でセットされています。

89行目はデータ全長をheader変数に追加しています。
ここでは先頭から8け桁取り出すために8ビット右シフトし、末尾から8ビット取り出すために0xffで論理積をしています。
なぜこんなことをしているのか疑問でしたが、どうやらpackメソッドを使うのに8ビットずつの配列にする必要があるためだと思います。

91行目は識別子をheader変数に追加しています。
95行目はフラグと断片位置をheader変数に追加しています。
このプログラムではIPパケットを分割禁止にしています。
98行目はttlとプロトコルをheader変数に追加しています。
このプログラムではttlは64がセットされています。

100行目から104行目は、宛先IPアドレスと送信元IPアドレスからチェックサムの計算を行っています。
IPヘッダのフォーマットでは、チェックサムはプロトコルの次のフィールドなので、header変数にチェックサムの値を追加する前にチェックサムより後ろのフィールドの値でチェックサムの計算をしています。
104行目はチェックサムをheader変数に追加しています。
105行目は宛先IPアドスと送信元IPアドレスをheader変数に追加しています。

107行目でheader変数をバイナリに変換し、@payload変数をpackメソッドで変換して追加します。

111~119行目
ICMPパケットを作るクラスです。
112行目はアクセサの宣言をしています。

114行目のinitializeで引数を変数にセットしています。
@typeには機能コードが入ります。
@codeには詳細な機能コードが入ります。
@payloadにはペイロード(転送したいデータ)が入ります。
@lengthにはICMPヘッダーのバイト長の4と、ペイロードの長さを足した値が入ります。

121~131行目
ICMPパケットを作るメソッドです。
@checksum変数にはチェックサムの値が入ります。

122行目の@checksum変数はget_checksumメソッドで@type変数と@code変数からチェックサムを求めています。
カッコ内の0x100は@type変数の値を256倍して16bitの先頭8bitに@type変数の値がくるようにします。
そこに@code変数の値を足し、こちらは末尾8bitが@code変数の値がくるようにします。

124行目は@payload変数の値をバイナリにし、unpackで16bitごとの配列にします。
その次のループでチェックサムを計算します。

129行目でICMPヘッダと@payload変数をバイナリにします。

133~137行目
ICMPリプライのパケットを作るクラスです。
ICMPPacketクラスを継承しています。

superでスーパークラスであるICMPPacketクラスのinitializeメソッドを呼び出します。
1つ目の引数はICMPのタイプでエコー応答の0、2つ目の引数はコードでエコー応答は固定で0が入ります。

今回はここまで。
router-utilsはあと少しで終わりです。

2014年1月21日火曜日

VMwareで仮想環境を作ってみる

今までVirtualBoxを使っていましたが、試しにVMwareを使ってみようと思います。
使うのはVMware Player 6です。

http://www.vmware.com/jp/products/player/

個人利用は無償だそうです。
とりあえずダウンロード。

インストールはウィザードに従ってすんなり終了しました。

今度、ゲストOSを作ってみます。

Tremaを試す ~simple-ruter その8~

今回ばrouter-utilsのソースを前回の続きから全体の半分ぐらいまで見ていきます。

以下のソースはrouter-utils.rbからの抜粋です

 33  class ARPPacket
 34    attr_accessor :type, :tha, :sha, :tpa, :spa
 35  
 36    def initialize type, tha, sha, tpa, spa
 37      @type = type
 38      @tha = tha
 39      @sha = sha
 40      @tpa = tpa
 41      @spa = spa
 42    end
 43  
 44    def pack
 45      eth_header = EthernetHeader.new( @tha, @sha, 0x0806 )
 46  
 47      # arp
 48      arp = [ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, @type ]
 49      arp += @sha.to_a + @spa.to_a + @tha.to_a + @tpa.to_a
 50  
 51      while arp.length < 46 do
 52        arp += [ 0x00 ]
 53      end
 54  
 55      eth_header.pack + arp.pack( "C*" )
 56    end
 57  end
 58  
 59  class ARPRequest < ARPPacket
 60    def initialize sha, tpa, spa
 61      tha = [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]
 62      super( 1, tha, sha, tpa, spa )
 63    end
 64  end
 65  
 66  class ARPReply < ARPPacket
 67    def initialize tha, sha, tpa, spa
 68      super( 2, tha, sha, tpa, spa )
 69    end
 70  end
 71  


33~43行目
ARPパケットを作るクラスです。
34行目はアクセサの宣言をしています。

36行目のinitializeで引数を変数にセットしています。
@typeにはARPリクエスト、ARPリプライといったARPパケットのタイプがセットされます。
@thaには宛先のmacアドレスが入ります。
@shaには送信元のmacアドレスが入ります。
@tpaには宛先のIPアドレスが入ります。
@spaには送信元のIPアドレスが入ります。

44~56行目
packメソッドではARPパケットのフォーマットに合わせてデータを並べていきます。
まずはeth_header変数にEthernetHeaderオブジェクトをセットしてイーサネットヘッダーを用意します。

arp変数には、ARPパケットフォーマットにしたがって、制御情報やプロトコル、ARPパケットのタイプを配列にセットします。
次いでarp変数に送信元macアドレス、送信元IPアドレス、宛先macアドレス、宛先IPアドレスをセットします。

51行目のループは、イーサネットフレームのデータ部分の長さは最低でも46バイトなので、arp変数の長さが46バイトになるまで0x00を追加していきます。
最後にeth_header変数をpackメソッドでバイナリにしたデータと、arp変数をpack変数でバイナリにしたデータをくっつけます。

59~64行目
ARPリクエストのパケットを作るクラスです。
APRPacketクラスを継承しています。

initializeメソッドで、tha変数にmacアドレスをセットしています。
ARPリクエストなので、macアドレスには「FF:FF:FF:FF:FF:FF」が入ります。

superでスーパークラスであるARPPacketクラスのinitializeメソッドを呼び出します。
一つ目の引数はARPパケットのタイプなので、ARPリクエストは1を指定します。

66~70行目
ARPリプライのパケットを作るクラスです。
ARPPacketクラスを継承しています。

superでスーパークラスであるARPPacketクラスのinitializeメソッドを呼び出します。
一つ目の引数はARPパケットのタイプなので、ARPリプライは2を指定します。


今回はここまでです。

Tremaを試す ~simple-ruter その7~

今回はrouter-utilsのソースを見ていきます。
前回見たrouter-utils.rbから抜粋しました

  1  require 'ipaddr'
  2  
  3  def get_checksum csum, val
  4    sum = ( ~csum & 0xffff ) + val
  5    while sum > 0xffff
  6      sum = ( sum & 0xffff ) + ( sum >> 16 )
  7    end
  8    ~sum & 0xffff
  9  end
 10  
 11  class IPAddr
 12    def to_a
 13      self.to_s.split( "." ).collect do | each |
 14        each.to_i
 15      end
 16    end
 17  end
 18  
 19  class EthernetHeader
 20    attr_accessor :macda, :macsa, :eth_type
 21  
 22    def initialize macda, macsa, eth_type
 23      @macda = macda
 24      @macsa = macsa
 25      @eth_type = eth_type
 26    end
 27  
 28    def pack
 29      ( @macda.to_a + @macsa.to_a + [ eth_type ] ).pack( "C12n" )
 30    end
 31  end
 32  

1行目
rubyの標準ライブラリのipaddrファイルを読み込み、IPAddrクラスを使えるようにします。

3~9行目
IPv4パケットのチェックサムを計算するメソッドです。
4行目は、引数のcsum変数の値を1の補数にし、0xffffをビット論理積で演算した結果に引数のvalの値を足してsum変数にセットしています。

5行目は、sum変数の値が0xffffを超えている間はループを繰り返します。
ループの中では、sum変数の値をビット論理積で16ビットにした値と、sum変数を右に16ビットシフトした値を足しています。
16ビット右シフトする事で、16ビットからあふれた値をチェックサムに足しています。

8行目でsum変数の値を1の補数にしています。

11~17行目
標準ライブラリのIPAddrクラスに定義を追加しています。
標準ライブラリのIPAddrクラスに定義を追加しています。
ここではto_aメソッドを追加しています。
このメソッドでは、IPAddrオブジェクトの値を文字列に変換し、ドットで分割して、10進数に変換した値を配列にして返します。

19~31行目
このメソッドではイーサネットヘッダーを構成します。
まずアクセサの宣言をして外部からアクセス出来るようにします。

initializeでは、オブジェクトを作る時に渡された引数を変数にセットします。
@macda変数には宛先のmacアドレスが入ります。
@macsa変数には送信元のmacアドレスが入ります。
@eth_typeにはパケットのプロトコルのタイプが入ります。例えばIPv4やARPなどです。

28行目以降で、packメソッドを定義しています。
@macda変数と@macsa変数をeth_type変数が含まれている配列に追加し、packメソッドでバイナリにしています。
Cは8bitの符号なし整数、nはネットワークバイトオーダーです。

短いですが、今日はここまでにします。

2014年1月19日日曜日

Tremaを試す ~simple-ruter その6~

今回はからrouter-utilsを見ていきいます。
このプログラムはルータに必要な機能を使えるようにします。

今回はこのプログラムに定義されているクラスとメソッドについて簡単に説明します。

(router-utils.rb)
  1  require 'ipaddr'
  2  
  3  def get_checksum csum, val
  4    sum = ( ~csum & 0xffff ) + val
  5    while sum > 0xffff
  6      sum = ( sum & 0xffff ) + ( sum >> 16 )
  7    end
  8    ~sum & 0xffff
  9  end
 10  
 11  class IPAddr
 12    def to_a
 13      self.to_s.split( "." ).collect do | each |
 14        each.to_i
 15      end
 16    end
 17  end
 18  
 19  class EthernetHeader
 20    attr_accessor :macda, :macsa, :eth_type
 21  
 22    def initialize macda, macsa, eth_type
 23      @macda = macda
 24      @macsa = macsa
 25      @eth_type = eth_type
 26    end
 27  
 28    def pack
 29      ( @macda.to_a + @macsa.to_a + [ eth_type ] ).pack( "C12n" )
 30    end
 31  end
 32  
 33  class ARPPacket
 34    attr_accessor :type, :tha, :sha, :tpa, :spa
 35  
 36    def initialize type, tha, sha, tpa, spa
 37      @type = type
 38      @tha = tha
 39      @sha = sha
 40      @tpa = tpa
 41      @spa = spa
 42    end
 43  
 44    def pack
 45      eth_header = EthernetHeader.new( @tha, @sha, 0x0806 )
 46  
 47      # arp
 48      arp = [ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, @type ]
 49      arp += @sha.to_a + @spa.to_a + @tha.to_a + @tpa.to_a
 50  
 51      while arp.length < 46 do
 52        arp += [ 0x00 ]
 53      end
 54  
 55      eth_header.pack + arp.pack( "C*" )
 56    end
 57  end
 58  
 59  class ARPRequest < ARPPacket
 60    def initialize sha, tpa, spa
 61      tha = [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]
 62      super( 1, tha, sha, tpa, spa )
 63    end
 64  end
 65  
 66  class ARPReply < ARPPacket
 67    def initialize tha, sha, tpa, spa
 68      super( 2, tha, sha, tpa, spa )
 69    end
 70  end
 71  
 72  class IPPacket
 73    attr_accessor :id, :protocol, :daddr, :saddr, :payload
 74  
 75    def initialize options
 76      @id = options[ :id ]
 77      @protocol = options[ :protocol ]
 78      @daddr = options[ :daddr ]
 79      @saddr = options[ :saddr ]
 80      @payload = options[ :payload ]
 81      @tot_len = 20 + payload.length
 82    end
 83  
 84    def pack
 85      csum = get_checksum( 0, 0x4500 )
 86      header = [ 0x45, 0x00 ] # Version, IHL, ToS
 87  
 88      csum = get_checksum( csum, @tot_len )
 89      header += [ @tot_len >> 8, @tot_len & 0xff ] # len
 90  
 91      csum = get_checksum( csum, @id )
 92      header += [ @id >> 8, @id & 0xff ] # ID
 93  
 94      csum = get_checksum( csum, 0x4000 )
 95      header += [ 0x40, 0x00 ] # Flags, Frag offset
 96  
 97      csum = get_checksum( csum, 0x40 * 0x100 + @protocol )
 98      header += [ 0x40, @protocol ] # ttl, protocol
 99  
100      csum = get_checksum( csum, @saddr.to_i >> 16 )
101      csum = get_checksum( csum, @saddr.to_i & 0xffff )
102      csum = get_checksum( csum, @daddr.to_i >> 16 )
103      csum = get_checksum( csum, @daddr.to_i & 0xffff )
104      header += [ csum >> 8, csum & 0xff ] # checksum
105      header += @saddr.to_a + @daddr.to_a
106  
107      header.pack( "C*" ) + @payload.pack
108    end
109  end
110  
111  class ICMPPacket
112    attr_reader :payload, :length
113  
114    def initialize type, code, payload
115      @type = type
116      @code = code
117      @payload = payload
118      @length = 4 + payload.length
119    end
120  
121    def pack
122      @checksum = get_checksum( 0, @type * 0x100 + @code )
123  
124      words = @payload.pack( "C*" ).unpack( "n*" )
125      words.each do | each |
126        @checksum = get_checksum( @checksum, each )
127      end
128  
129      [ @type, @code, @checksum ].pack( "C2n" ) + @payload.pack( "C*" )
130    end
131  end
132  
133  class ICMPEchoReply < ICMPPacket
134    def initialize payload
135      super( 0x00, 0x00, payload )
136    end
137  end
138  
139  module RouterUtils
140    def create_arp_request_from interface, addr
141      arp = ARPRequest.new( interface.hwaddr, addr, interface.ipaddr )
142      arp.pack
143    end
144  
145    def create_arp_reply_from message, replyaddr
146      arp = ARPReply.new( message.macsa, replyaddr, message.arp_spa, message.arp_tpa )
147      arp.pack
148    end
149  
150    def create_icmpv4_reply entry, interface, message
151      offset = 14 + 20 + 4
152      payload = message.data.unpack( "C*" )[ offset .. message.data.length - 1 ]
153      icmp = ICMPEchoReply.new( payload )
154      ip_packet = IPPacket.new( :id => message.ipv4_id,
155                                :protocol => message.ipv4_protocol,
156                                :ttl => message.ipv4_ttl,
157                                :daddr => message.ipv4_saddr,
158                                :saddr => message.ipv4_daddr,
159                                :payload => icmp )
160      eth_header = EthernetHeader.new( entry.hwaddr, interface.hwaddr, 0x0800 )
161  
162      eth_header.pack + ip_packet.pack
163    end
164  end


get_checksumメソッド
IPv4パケットのチェックサムを計算する時に使用するメソッドです。

IPAddrクラス
1行目のrequireでipaddrを読み込む事で、標準ライブラリIPAddrクラス使えるようになっていますが、そのIPAddrクラスに定義を追加しています。

EthernetHeaderクラス
イーサネットヘッダーを構成するためのクラスです。

ARPPacketクラス
ARPパケットを構成するためのクラスです。

ARPRequestクラス
ARPリクエストのパケットを作るクラスです。
ARPPacketクラスを継承しています。

ARPReplyクラス
ARPリプライのパケットを作るクラスです。
ARPPacketクラスを継承しています。

IPPacketクラス
IPパケットを作るクラスです
ここではICMPのリプライパケットを送るのに使われています。

ICMPPacketクラス
ICMPパケットを構成するためのクラスです。

ICMPEchoReplyクラス
ICMPのリプライパケットを作るクラスです。
ICMPPacketクラスを継承しています。

RouterUtilsモジュール
create_arp_request_fromメソッドとcreate_arp_reply_fromメソッドとcreate_icmpv4_replyメソッドを定義しています。


今回は大まかな説明だけになります。
個人的にはsimple-router全体でこのソースが一番難しかったです。

次回以降、何度かに分けてこのソースを読んでいきます。

rubyのpackメソッドとは何だ!?

以前rubyのpackメソッドがわからないという記事を書きましたが、なんとなくわかった気がします。

packメソッドは「配列の内容をtemplateで指定された文字列にしたがってバイナリとしてパックした文字列を返す」という説明がどうにも理解できませんでした。
今でも怪しいですけど。

まあ、要するに文字や数字をバイナリに変換するメソッドと思っていいのでしょう。
templateは変換対象のデータが文字列なのか、16進数なのか等を示していると思います。

unpackはその逆で、バイナリデータを指定の文字や数字に変換します。
以下のプログラムでどう表示されるか試してみました。

(test_pack.rb)
p [0x41,0x42,0xE3,0x81,0x82].pack("C*")
print([0x41,0x61,0xE3,0x81,0x82].pack("C*"),"\n")
puts [0x41,0x61,0xE3,0x81,0x82].pack("C*")

p [0x41,0x61,0xE3,0x81,0x82].pack("C*").unpack("B*")
p ["Aaあ"].pack("A*").unpack("B*")
p ["0100000101100001111000111000000110000010"].pack("B*").unpack("B*")

puts ["0100000101100001111000111000000110000010"].pack("B*")


初めの3行はp、print、putsでどう表示されるかを確認します。
次の3行はpackメソッドで様々な形式のデータをバイナリに変換し、unpackメソッドでバイト表現にしています。

最後はバイト表現のデータをputsメソッドで表示しています。

実行すると、以下のように表示れました。
$ ruby test_pack.rb
"AB\343\201\202"
Aaあ
Aaあ
["0100000101100001111000111000000110000010"]
["0100000101100001111000111000000110000010"]
["0100000101100001111000111000000110000010"]
Aaあ


初めの3行は表示の確認です。
pだと「あ」が8進数で表示されています。
printとputsは文字で表示されています。

そもそも、なぜ文字で表示されるか?という疑問がありましたが、どうやら「p」や「print」「puts」メソッドが人が見やすいように変換しているようです。
ただし、「p」と「print」「puts」は変換の仕方が違うようで、見え方が違っています。

次にさまざまな形式のデータをバイナリに変換し、そのあとバイト表現にしています。
これは16進数、文字列、2進数と表現は違いますが実はすべて同じデータでした。
なので、2バイト表現した値はすべて同じになります。

最後のputsは2バイトで表現されたデータを表示しただけです。
putsメソッドで変換されて「Aaあ」と表示されました。

こんな具合に、いろいろなデータをバイナリに変換するためのメソッドだと理解しました。

間違っていないといいんですけど。