Ruby Advent Calendar 2021: パケットキャプチャ入門 with dRuby
訂正とお詫び (2022/3/5)
この記事の最後に「WiresharkがdRubyに対応していないためキャプチャできなかった」と結論づけているのですが、筆者の不見識でWiresharkにdRuby用のディセクタがビルトインされていることに気づいていませんでした。
申し訳ありません。
以下のように設定するとdRubyパケットをキャプチャすることが可能です。
Wireshark > Preferences > Protocols > DRb
ポート番号を指定する
以下は元の記事 (未編集) です↓
Ruby Advent Calendar 2021 17日目の記事です。
昨日は@okuramasafumiさんでした。
この記事は
2台の機器 (MacBookとThinkPad) がお互いに通信を行うような
dRubyスクリプトを書き、その実際の通信の様子をWiresharkで眺める
という小さな実験を行った際の記録です。
きっかけ
最近Wiresharkに入門したのですが、それと同時にdRubyにも興味が湧いて勉強していました。
ご存知dRubyは、ネットワーク越しにRubyオブジェクトを操作することができるフレームワークです。
HTTP通信と同じく、dRubyスクリプトを実行するときに機器間で実際に発生する通信は通常、TCP/IPプロトコルレイヤによって隠蔽されています
そこで機器間で送受信されたdRubyスクリプトが実際にパケットとして、あるいはアプリケーションデータ (※) としてネットワーク上をどんな風に流れているのか、その様子を見てみたいと思い立ち、今回の実験に至りました。
※ 実際にキャプチャした結果衝撃の事実に直面することになりました。むしろなぜこの時点で気づかなかった…
以下記事の内容に誤りなど技術的指摘がありましたら、お手数ですが教えてください。
動作環境
- Ruby 3.0.2 (dRuby 2.0.4)
- Wireshark 3.4.9 (macOS Intel 64-bit)
- MacBook (macOS 11.6) と ThinkPad (Ubuntu18.04) をそれぞれ自宅のWiFiネットワークに接続。
2台のPCのグローバルIPアドレスを調べたところ、
となっていました。
2台の機器は以下のように疎通確認済みです。
# ThinkPad $ ping6 2400: ... :4808 -c 3 PING6(56=40+8+8 bytes) 2400: ... :439c --> 2400: ... :4808 16bytes from 2400: ... :439c, icmp_seq=0 ttl=64 time=4.26ms 16bytes from 2400: ... :439c, icmp_seq=1 ttl=64 time=4.86ms 16bytes from 2400: ... :439c, icmp_seq=2 ttl=64 time=4.25ms --- 2400: ... :439c439c ping6 statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss
# MacBook $ ping6 2400: ... :439c -c 3 PING6(56=40+8+8 bytes) 2400: ... :4808--> 2400: ... :439c 16bytes from 2400: ... :4808, icmp_seq=0 hlim=64 time=94.183ms 16bytes from 2400: ... :4808, icmp_seq=1 hlim=64 time=116.538ms 16bytes from 2400: ... :4808, icmp_seq=2 hlim=64 time=34.263ms --- 2400: ... :4808 ping6 statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss
※ MacからThinkPadへの通信にやたら時間がかかっていますが、弊環境だと大体いつもThinkPadの方が通信速度が優秀なのです…
改めてdRubyについて
皆さんご存知、Rubyのための分散オブジェクトシステムです。
分散オブジェクト技術 【distributed object technology】
library drb
dRubyによって、Rubyオブジェクトをプロセスやネットワークを超えて操作することが可能になります。
作者である咳さんご自身がdRubyの基本的な使い方について紹介されているRubyKaigi 2016の登壇動画はこちらです。
簡単な動作例を以下に示します。
ターミナルを二つ開いてそれぞれirbを起動し、drbライブラリをrequire
します。
# ターミナル1 $ irb --simple-prompt >> require 'drb' => true
# ターミナル2 $ irb --simple-prompt >> require 'drb' => true
続いてターミナル1でFoo
クラスとインスタンスメソッドfrom
を定義します。
# ターミナル1 ?> class Foo ?> def from ?> "I'm from terminal 1!" ?> end >> end => :from
続いてターミナル1でDRb.start_service
を実行すると、サーバープロセスを起動することができます。
このとき、DRb.start_service
の第一引数にクライアントがアクセスするためのURI、第二引数にFoo
オブジェクトを渡します。
第二引数に渡したFoo
オブジェクトは他のプロセスから操作可能なオブジェクトとして公開されます。
# ターミナル1 >> foo = Foo.new >> puts foo #<Foo:0x00007f81149b7cf0> >> DRb.start_service("druby://localhost:8080", foo) # ローカルホストのポート番号8080で待ち受ける
これで、ターミナル2からターミナル1のFoo
オブジェクトに対してメソッド呼び出しができるようになります。
ターミナル2からFoo
オブジェクトに対してメソッド呼び出しするためには、まずターミナル2でDRbObject.new_with_uri
を実行します。
このとき、引数にターミナル1のサーバープロセスが待ち受けているURIを指定します。
# ターミナル2 >> foo = DRbObject.new_with_uri('druby://localhost:8080') >> puts foo #<Foo:0x00007f81149b7cf0>
こうすることによってターミナル2からターミナル1のFoo
オブジェクトの複製であるオブジェクト (※) を取得することができます。
※ この辺り難しいので詳しくは後述のdRuby Bookを参照
ターミナル2から変数foo
に格納されたオブジェクトに対してfrom
メソッドを実行すると…
# ターミナル2 >> foo.from => "I'm from terminal 1!"
ターミナル2にfrom
メソッドの返り値が表示されました!
ターミナル2から、ターミナル1で定義したFoo#from
のメソッド呼び出しに成功したということです。
逆に、ターミナル1からターミナル2にあるオブジェクトへの操作も試してみましょう。
# ターミナル2 ?> class Bar ?> def from ?> "I'm from terminal 2!" ?> end >> end => :from >> DRb.start_service("druby://localhost:8081", Bar.new) # ローカルホストのポート番号8081で待ち受ける
# ターミナル1 bar = DRbObject.new_with_uri('druby://localhost:8081') bar.from => "I'm from terminal 2!"
こちらももちろん成功します。楽しいですね!
ちなみにBar#from
の内容を"I'm from terminal 2!"
ではなくputs "I'm from terminal 2!"
にしたりすると、ターミナル2に"I'm from terminal 2!"
が表示され (ターミナル2の標準出力はターミナル2のままなので) 、ターミナル1にはnil
が返ります (puts
の返り値はnil
なので) 。
他にもdRubyは様々な特徴を備えています。
詳しくはdRubyによる分散・Webプログラミング(あるいは英語版・無料のThe dRuby Book)、そして「令和のdRuby Book」といった趣きのn月刊ラムダノート Vol.2, No.1(2020)の特集「#2 dRubyで楽しむ分散オブジェクト」もおすすめです。
ThinkPadとMacBook、それぞれの機器で実行するdRubyスクリプト
今回の実験では、次のようなプログラムを動作させることにしました。
- ThinkPadはサーバープロセスをポート番号8080で起動し、MacBookからアクセスすることができる空のKVS
{ }
(…という名の空ハッシュ)を公開する - MacBookはサーバープロセスをポート番号8081で起動し、ThinkPadからアクセスできるようにしておく
- MacBookがThinkPadの公開しているKVS
{ }
を取得する - MacBookが取得したKVSにレコード
{ "greeting" => "Hello from MacBook" }
を追加する - ThinkPadは自身が公開しているKVSにレコードが追加されたことを検知し、自身の標準出力にログを出力する
- MacBookが再びKVSにレコード
{ "stdout" => $stdout }
を追加する - ThinkPadは再び自身が公開しているKVSにレコードが追加されたことを検知し、自身の標準出力にログを出力する
- ThinkPadはKVSに追加されたレコードがttyに結合している場合、結合先にメッセージ
"Hello from ThinkPad"
を出力する
プログラムは次のようになります。
https://github.com/shioimm/til/tree/master/activities/learning_network_with_drbgithub.com
# thinkpad.rb require 'drb' kvs = {} # 新たに追加されたレコードを検証するために使用する last_kvs = {} # サーバープロセスをポート番号8080で起動して空のKVSを公開 DRb.start_service("druby://#{ENV['LOCAL_HOST_ADDRESS']}:8080", kvs) puts "Start server process on #{DRb.uri}" loop do if kvs.size > last_kvs.size puts "New record has been added:\n#{kvs}" new_values = (kvs.values - last_kvs.values) new_stdouts = new_values.select { |value| value.respond_to?(:tty?) && value.tty? } unless new_stdouts.empty? # 追加されたレコードがttyに結合している場合は"Hello from ThinkPad"を出力 puts "Greet to client: 'Hello from ThinkPad'" new_stdouts.each { |new_stdout| new_stdout.puts "Hello from ThinkPad" } end last_kvs = kvs.dup end sleep 1 end
# macbook.rb require "drb" # サーバープロセスをポート番号8081で起動 DRb.start_service("druby://#{ENV['LOCAL_HOST_ADDRESS']}:8081") puts "Start server process on #{DRb.uri}" # ThinkPadが公開しているKVSを取得 uri = "druby://#{ENV["REMOTE_HOST_ADDRESS"]}:8080" kvs = DRbObject.new_with_uri(uri) puts "[LOG] uri = #{uri}" puts "[LOG] kvs = DRbObject.new_with_uri(uri)" puts "[LOG] kvs:\n#{kvs}" # KVSにレコード { kvs["greeting"] => "Hello from MacBook" } を追加 kvs["greeting"] = "Hello from MacBook" puts "[LOG] kvs['greeting'] = 'Hello from MacBook'" puts "[LOG] kvs:\n#{kvs}" sleep 1 # KVSにレコード { kvs["stdout"] => $stdout } を追加 # この$stdoutはMacBook自身の標準出力 kvs["stdout"] = $stdout puts "[LOG] kvs['stdout'] = $stdout" puts "[LOG] kvs:\n#{kvs}" puts "[LOG] sleep"; sleep
あらかじめ環境変数LOCAL_HOST_ADDRESS
とREMOTE_HOST_ADDRESS
を各機器にセットしておきました。
ThinkPadのLOCAL_HOST_ADDRESS
はThinkPadのIPアドレス (末尾439c
)、REMOTE_HOST_ADDRESS
はMacBookのIPアドレス (末尾4808
) です。
MacBookにはその逆になります。
これらのプログラムをそれぞれMacBookとThinkPadに置き、それぞれ実行すると以下のようになります。
# ThinkPad $ ruby thinkpad.rb Start server process on druby://2400: ... :439c:8080
# MacBook $ ruby macbook.rb Start server process on druby://2400: ... :4808:8081 [LOG] uri = druby://2400: ... :439c:8080 [LOG] kvs = DRbObject.new_with_uri(uri) [LOG] kvs: {} [LOG] kvs['greeting'] = 'Hello from MacBook' [LOG] kvs: {"greeting"=>"Hello from MacBook"}
# ThinkPad ... New record has been added: {"greeting"=>"Hello from MacBook"}
# MacBook ... [LOG] kvs['stdout'] = $stdout [LOG] kvs: {"greeting"=>"Hello from MacBook", "stdout"=>#<DRb::DRbObject:0x00007fa55c8a57b0 @uri="druby://2400: ... :4808:8081", @ref=80>} [LOG] sleep
# ThinkPad ... New record has been added: {"greeting"=>"Hello from MacBook", "stdout"=>#<DRb::DRbObject:0x00007fa55c8a57b0 @uri="druby://2400: ... :4808:8081", @ref=80>}
# MacBook ... Hello from ThinkPad
無事にMacBookからThinkpadへ、ThinkPadからMacBookへ、お互い通信を行うことができました!
実際の通信の様子をWiresharkでキャプチャしてみる
上記のやりとりをキャプチャします。
WiresharkはこちらからMacBookへダウンロードし、インストール済みです。
(ThinkPad(Ubuntu)にもapt
を使ってインストールしたのですが、今回はクライアントであるMacBookでキャプチャしたファイルのみを確認します)
早速Wiresharkを開いてキャプチャを開始すると、次のような通信の様子が表示されました。
この状態で、それぞれの機器でプログラムを実行します。
# ThinkPad $ ruby thinkpad.rb Start server process on druby://2400: ... :439c:8080 New record has been added: {"greeting"=>"Hello from MacBook"} New record has been added: {"greeting"=>"Hello from MacBook", "stdout"=>#<DRb::DRbObject:0x00007fa55c8a57b0 @uri="druby://2400: ... :4808:8081", @ref=80>}
# MacBook $ ruby macbook.rb Start server process on druby://2400: ... :4808:8081 [LOG] uri = druby://2400: ... :439c:8080 [LOG] kvs = DRbObject.new_with_uri(uri) [LOG] kvs: {} [LOG] kvs['greeting'] = 'Hello from MacBook' [LOG] kvs: {"greeting"=>"Hello from MacBook"} [LOG] kvs['stdout'] = $stdout [LOG] kvs: {"greeting"=>"Hello from MacBook", "stdout"=>#<DRb::DRbObject:0x00007fa55c8a57b0 @uri="druby://2400: ... :4808:8081", @ref=80>} [LOG] sleep Hello from ThinkPad
プログラムの実行に成功したため、一旦ここでキャプチャを止めてファイルとして保存しました。
保存したファイルを開き、今回実験した内容を通信に絞り込むため次のような条件でフィルタをかけました。
tcp.port == 8080 || tcp.port == 8081
すると…
(上から下まで全部TCP)
(dRubyスクリプトが含まれているはずの) アプリケーションデータがキャプチャできていない…!?
…と、ここまで来て気づいたのですがWiresharkがキャプチャできるのはWireshark自身がサポートしているアプリケーションプロトコルのみなのでした。
残念ながらその中にdRubyは含まれていないため、キャプチャファイルではトランスポート層でやりとりしたTCPパケットの情報のみが表示されています。それはそう…!!
せっかくなので、dRubyならではのちょっと特徴的なパケットの様子を見てみたいと思います。
先ほど実験用プログラムの中で、ThinkPadのサーバープロセスのポート番号を8080、MacBookのサーバープロセスのポート番号を8081としました。
通信の始まりであるこの行では、ThinkPadの8080番に対して、MacBookの55094番から[SYN]を送るところから通信が始まっています。
この時、MacBookはクライアントとしてパケットを送信しているため、自身のサーバーのプロセスの8081番ではなくてエフェメラルポートである55094番を使用しています。
2行目ではThinkPadの8080番からMacBookの55094番に対して[ACK]が返っていることが確認できます。
この後順調に通信が進んでいくのですが、190行目で突然シーケンス番号0の新しい通信が開始されます。
見切れていますが送信元のIPアドレスの末尾が439c
、宛先のIPアドレスの末尾が4808
でポート番号が8081になっているので、これはThinkPadからMacBookのサーバーポート宛の通信であることがわかります。
(ThinkPadからMacBookへ"Hello from ThinkPad!"
と送信している分です)
今度はThinkPadがクライアントになっているので、送信元のポート番号として38272という番号が割り当てられています。
最後に、それぞれの端末から通信をCtrl+Cで切断した部分がこちらです。
シンプルなサーバー・クライアントシステムの場合、お互いが[FIN, ACK]と[ACK]を送り合っておしまいになると思うのですが、今回サーバープロセスとクライアントプロセスが2つずつあるため、通常の2倍の[FIN, ACK]と[ACK]が飛び交っていてちょっと面白いです。
まとめ
今回の実験の内容自体はdRuby的にもWireshark的にも最初の一歩を踏んだ程度のものではあるのですが、個人的にはパケットをキャプチャしてまじまじ眺める経験そのものが初めてだったためとても勉強になりました。
残念ながら送受信しているdRubyスクリプトそのものをアプリケーションデータとして確認することはできませんでした…が、実際にネットワークを超えて行くTCPパケットとしてのdRubyの姿を眺めることができて感動しました。
WiresharkもdRubyもまだまだ奥が深そうなので、引き続き楽しんでいきたいと思います。
以上、「Ruby Advent Calendar 2021: パケットキャプチャ入門 with dRuby」でした。
明日は@yancyaさんです。
それでは皆さん楽しいクリスマスをお過ごしください🎄🍰