2021年度版、TSharkを使ってリアルタイムパケットモニタリング
2013年に以下の記事を書いた。この時はJSONではなく、XMLフォーマットを用いてリアルタイムパケットモニタリングを実現したが、最近では jsonが隆盛してきているため、jsonフォーマットでのリアルタイムパケットモニタリングを行いたい。また、Rubyのバージョンも 3.0 になり、Thread を使うより Ractor を使うほうがナウというもの。
JSON版パケットモニタリングコードサンプル
require 'open3' require 'json' def main cmd = [ %q(C:\Program Files\Wireshark\tshark.exe), '-i', '5', '-Y', 'tcp', '-T', 'json' ] r1 = Ractor.new(cmd) do |command| _, stdout, stderr, _ = Open3.popen3(*command) puts stderr.gets json_str = '' while line = stdout.gets json_str += line next unless line =~ /^ },$/ json_str = json_str&.chomp&.delete_suffix(',')&.delete_prefix('[')&.lstrip json_data = JSON.parse(json_str) process_json(json_data) json_str = '' end end r2 = Ractor.new do gets end Ractor.select(r1, r2) end def process_json(json_data) payload = json_data['_source']['layers']['tcp']['tcp.payload'] p payload&.split(':')&.size.to_i end main
コード解説
main() メソッドが メイン部分、tsharkからJSONデータを受け取り、パースし、process_json() メソッド に jsonデータを渡すを繰り返す。 process_json() メソッドが jsonデータを加工して処理する部分
コマンド生成
5行目~10行目は tshark.exe のコマンドを作成している部分。
cmd = [ %q(C:\Program Files\Wireshark\tshark.exe), '-i', '5', '-Y', 'tcp', '-T', 'json' ]
-i
は イーサネットインターフェースの番号を指定するオプション。数字を指定するが、この数字はどのように選べばいいか?というと、tshark.exe -D
とするとインターフェースの一覧が表示されるので、その頭の番号を指定するとよい。私の環境では 5番目のインターフェースがインターネットにつながるイーサネットカードであったのでそれを選択している。
-Y
は モニタリングしたパケットをフィルタするためのオプション。-f
もフィルタするためのオプションであるが、こちらはキャプチャする前にフィルタするためのオプション(キャプチャフィルタ)で -Y
はキャプチャした後表示をフィルタするためのオプション(ディスプレイフィルタ)になる。Wiresharkを使っていると馴染みがあるのはディスプレイフィルタとなると思うので、今回は -Y
を使用している。 (キャプチャフィルタの構文がわからないわけじゃないんだからね!!)
-T
は 出力フォーマットのオプションで、 json
を指定すると JSONで出力される。
Ractor
Ractor の技術は今回の tshark のモニタリングの本筋からは逸れるので軽く説明する。(というかちゃんと理解できてない)
Ruby にはもともと Thread という並列処理機構があったが、GILの関係でCPUのコア数をフルに活用できていなかった。その制約を取っ払う目的で作られたのが Ractor だと思っている。Ruby で真の並列処理を行いたいのであれば Ractor を使おう。というのが最近のトレンドな気がする。
以下に Ractor のサンプルコードを示す。
a = "hello" r1 = Ractor.new(a) { |a_| sleep 5 a_ } r2 = Ractor.new { gets } _, msg = Ractor.select(r1, r2) puts msg
上記の例では、2つの Ractor を生成し、 Ractor.select
でどちらかが終了するのを待ち受けている。 Ractor の最後の結果が msg に入る。 5秒何も入力しなかったら "hello" が表示され、5秒以内に何か入力するとその文字列が出力される。 Ractor.new(a) { |a_| ~ }
のように new に変数を与えるとそれを Ractor 内部で使用することができる。基本的に Ractor のスコープを超えて変数は参照できないので、引数で渡す必要がある。(そうせずに値を渡す方法もあるようだが、ここでは割愛。)
JSON パース
13行目で tshark を実行し、14行目で tsharkから標準エラー出力に出力されるモニタ情報を一行だけ取得し表示してる。一応これで、tsharkが正常に起動していることを確認できる(かな?)
tsharkから出力されるJSONテキストの形式は 配列がルートとなり、その中にパケット情報がオブジェクトとしてパックされている。以下、パケット例
[ { "_index": "packets-2021-11-04", "_type": "doc", "_score": null, "_source": { "layers": { "frame": { "frame.interface_id": "0", "frame.interface_id_tree": { "frame.interface_description": "イーサネット" }, "frame.encap_type": "1", ... }, "eth": { ... }, }, "ip": { "ip.version": "4", "ip.hdr_len": "20", ... }, "tcp": { "tcp.srcport": "55291", "tcp.dstport": "443", "tcp.port": "55291", "tcp.port": "443", ... }, "tls": { "tls.record": { "tls.record.content_type": "23", ... } } } } }, { "_index": "packets-2021-11-04", "_type": "doc", "_score": null, "_source": { "layers": { "frame": { ... } } } }, { ... } ...
抽象化すると [ { パケット1 }, { パケット2 }, { パケット3 }, ... { パケットn } ]
という構造になる。tsharkが起動されモニタリングが開始されると [
から出力され、パケット1, パケット2, .. と出力されていき、tsharkがモニタリングを終了するときに ]
が出力され JSONデータとして完成する。リアルタイムでモニタリングするには配列ノードは無視して、中の各パケットオブジェクトを逐次取得して処理しなければならない。戦略としては オブジェクトのお尻の }
がくるまで gets で取得して頭の無駄な文字をトリミングしてJSONオブジェクトとする方法を考える。それが 16行目 ~ 20行目の部分になる。
while line = stdout.gets json_str += line next unless line =~ /^ },$/ json_str = json_str&.chomp&.delete_suffix(',')&.delete_prefix('[')&.lstrip
16行目の while line = stdout.gets
で1行ずつ取得する。
17行目で取得した1行を json_str に結合する json_str が最終的に JSON文字列となり JSON.parse() に渡される
18行目で取得した1行がオブジェクトのお尻かどうかをチェックする。今のところ tshark はパケットオブジェクトのお尻として },
を1行で出力してくれるようなので、これを基準にお尻かどうか判断している。
20行目で取得した json_str を成形する。
- まず、
chomp
で無駄な末尾改行を取り除く - 次に、
delete_suffix(',')
で無駄なカンマを取り除く - 続いて、
delete_prefix('[')
で頭の[
を取り除く - 最後に、頭の空白を取り除く
以下、成形の具体例となる。
もし、tsharkが起動して一発目のパケットであれば以下のような構造になっているはずである。
[\n ␣␣{\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣},\n
なので、①末尾の改行を取り除き
[\n ␣␣{\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣},
次に、②末尾のカンマを取り除き
[\n ␣␣{\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣}
③ 頭の [
を取り除き
\n ␣␣{\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣}
④ 頭の空白・改行を取り除くことで、正しいJSONデータが取得できる。
{\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣␣␣~なにかデータ~\n ␣␣}
2回目以降のパケットでは③の [
を取り除く部分が不要だがRubyでは無駄に処理が動くだけで入力文字列がそのまま出力されるので問題なく動作する。
JSONデータを処理
21行目で成形したJSONデータをパースした後、process_json() メソッドにそのデータを渡し処理する。
def process_json(json_data) payload = json_data['_source']['layers']['tcp']['tcp.payload'] p payload&.split(':')&.size.to_i end
今回は tcp のペイロードを取得し、そのサイズを出力するようにした。JSONデータのどのフィールドにどのデータがあるかは 一度 JSONデータを出力してみないとわからないので、そのあたりは筋力が必要になるだろう。
JSONデータの調べ方
パケットにどのようなフィールドがあるかを調べるのに、tsharkを動かして、コマンドプロンプトでいちいちJSONを見ていたのでは埒が明かないときもあるだろう。そういう時は Wiresahrkに頼ろう。 Wiresharkの [ファイル(F)] → [エキスパートパケット解析] → [JSONとして] を選択すると取得したパケットをJSONで出力することが可能だ。出力方法には様々なオプションがあるので、ある程度フィルタリングした後出力し、好きなエディタでフィールドをチェックするといいだろう。