Rubyだけで簡易パケットキャプチャツール作ってみた

公開:2021/02/22
更新:2021/02/23
3 min読了の目安(約3400字TECH技術記事

動作しているところ

概要

libpcap等のCのライブラリを使わずに、Rubyだけで簡単なパケットキャプチャツールを作ってみました。

https://github.com/kuredev/simple_capture
https://rubygems.org/gems/simple_capture

できること

以下のプロトコルに対応し(少な!)、コンソール上に表示します。

  • Ethernet(14Byte)
  • IPv4
  • ICMP(Echo/Reply)

作った動機

  • Rubyとネットワークプログラミングの勉强
  • 最近、Rubyのソケットプログラミングを色々といじくっていて[1][2][3]、ある程度使い方が分かってきたので、アプリぽいものを作ってみたかった
  • 既存のパケットキャプチャライブラリを調べたところ、メジャーなものはpacketfuのようで、これが内部で利用しているpcaprubはNative Extensionで、実際にキャプチャする部分はlibpcapを用いているため[4]、Rubyのソケットプログラミングだけで実装してみたかった

動作確認環境

  • Ruby 3.0
  • Amazon Linux2(Arm64)

Ruby / Linux なら大体動くと思います(多分)

使い方

ライブラリをインストールします。

% gem install simple_capture

以下のように実行ファイルを作成します。
SimpleCapture::Capture クラスにキャプチャするインターフェースの名前を渡して、 run します。

test.rb
require "simple_capture"

cap = SimpleCapture::Capture.new("eth1")
cap.run

実行します。RAWソケットを用いるため、root権限が必要です。
終了はCtrl+Cで終了してください。

% sudo ruby test.rb 

技術的なメモ

いくつか要点となる部分だけメモしておきます:

ソケット

本プログラムではパケットのヘッダ部から取得する必要があるため、Rawソケットでソケットを準備します。

simple_capture/capture.rb#L51

socket = Socket.new(Socket::AF_PACKET, Socket::SOCK_RAW, ETH_P_ALL)

受信データの取り扱い

パケットを受信すると、データが文字列として取得されます[5]。エンコーディングは #<Encoding:ASCII-8BIT> となっており、今回の対象はヘッダデータが主となりますため、そのまま出力しても何も分かりません(汗)

mesg, _ = socket.recvfrom(1500)
pp mesg	
出力結果
"\n" +
"\xF0\x9D\xDD\xB5\xAC\n" +
"+8=\xBB\n" +
"\b\x00E\x00\x00\x1DQb\x00\x00\xFF\x01\x04:\xAC\x1F\v\xF4\xAC\x1F\x02\x11\x00\x00\xF2t\r\x8A\x00\x01\x00"

文字列は1文字1Byteであり、1Byteは16進数でいうところの2桁です。WireSharkを見慣れている方であれば、キャプチャしたデータ部の左側画面の2桁ずつが上記の1文字とイメージすると分かりやすいかと思います。

これを扱うため、bytesメソッドで数値の配列に変換してみます。

mesg.bytes
結果
[10,240,157,221,181,172,10,43,56,61,187,10,8,0,69,0,0,29,81,98,0,0,255,1,4,58,172,31,11,244,172,31,2,17,0,0,242,116,13,138,0,1,0]

10進数だと分かりづらいですが、これを16進数に変換すると[6]WireShark等のキャプチャツールで確認できるデータ部の表示と同じものが得られます。
以下の例では最初の要素が "a" ですが、Ethernetヘッダは先頭が宛先MACアドレスであるため、宛先MACアドレスの先頭が "0A" であると分かります。

mesg.bytes.map { |byte| byte.to_s(16) }
16進数
["a","f0","9d","dd","b5","ac","a","2b","38","3d","bb","a","8","0","45","0","0","1d","8f","14","0","0","ff","1","c6","87","ac","1f","b","f4","ac","1f","2","11","0","0","f2","63","d","9b","0","1","0"]

あとは、ヘッダの仕様に合わせてデータを分解していけば、パケットを解釈していくことが出来ます。

参考文献/リンク

パケットキャプチャツールの自作は以下にサンプルプログラムが掲載されており、大いに参考にさせて頂きました。

https://amzn.to/2ZD49z0

ソケット周りの仕様の解説は以下の本が最も詳しかったです。少し古い本なので、サンプルプログラムはそのまま動かないかもですが、さすが評判なだけはありました。

https://amzn.to/37ASkOi

サンプルコードが多く、参考にさせて頂きました。

https://www.geekpage.jp/programming/linux-network/book/
脚注
  1. 【Ruby】ソケットをプロミスキャスモードにする - Qiita https://qiita.com/kure/items/8126b2e3cb0bc2d6fdf2 ↩︎

  2. 【Ruby】シンプルなPingクライアントを作ってみた - Qiita https://qiita.com/kure/items/2bd68ad810ac291c3365 ↩︎

  3. https://kure.hatenablog.jp/archive/category/setsocopt ↩︎

  4. このへん https://github.com/pcaprub/pcaprub/blob/c66e6aa2c047a4bc63abb4424596b3ef91d86b4f/ext/pcaprub_c/pcaprub.c#L600 ↩︎

  5. https://docs.ruby-lang.org/ja/latest/method/Socket/i/recvfrom.html ↩︎

  6. pack/unpack を使えばもっとスマートに出来るのかもですが、イマイチやり方がよく分からなかった… ↩︎