LoginSignup
10
9

More than 3 years have passed since last update.

プロトコルを自作し、RustでTCPサーバーを作成してWiresharkで解析する

Last updated at Posted at 2020-11-25

概要

昨今はオープンに設計されたHTTPなどのプロトコルを、オープンソースとして開発されたサーバやライブラリを用いて利用するため、独自にプロトコルを設計して開発するという機会はほぼなくなりました。
しかし、低レイヤーで何が起きているか何ができるのかを理解するには、独自プロトコルを設計して開発してみるという体験が一番良いと思っています。
そこで、社内の若手エンジニア向けに書いた記事を再編集して公開します。

TCPの基礎とWiresharkの基礎については、以前書いた記事WiresharkではじめるTCP超基礎入門を見てもらえると嬉しいです。

何をやるか

以下のことをやっていきます。

  1. 独自プロトコルを設計する
  2. Rustで独自プロトコルを処理できるサーバを作成する
  3. Wiresharkで独自プロトコルを解析するためのスクリプトを作成する
  4. telnetでサーバと通信し、Wiresharkで通信内容を解析する

1. 独自プロトコルを設計する

ごく簡単なものを設計します。
色々ツッコミどころがある上に意味もないプロトコルですが気にしないでください。

ヘッダー構造

データ位置(bit) 意味
0 - 7 コマンドID 1
8 - 15 コマンドID 2
16 - .. データ

コマンドID 1

コマンド 動作
<0x30> 固定文字列を返す。コマンドID 2の値によって返す文字列が変わる
<0x30>以外 受信データをechoする

コマンドID 2

コマンド 動作
<0x30> <0x30 0x30 0x6f 0x72 0x65 0x67 0x61 0x20 0x67 0x61 0x6e 0x64 0x61 0x6d 0x75 0x20 0x64 0x61 0x21 0xOD 0xOA> を返す
<0x30>以外 <0x30 0x2D 0x68 0x65 0x6c 0x6c 0x6f 0x20 0x77 0x6f 0x72 0x6c 0x64 0x21 0x0D 0x0A> を返す

データ

ヘッダーを除いた受信データ

2. Rustで独自プロトコルを処理できるサーバを作成する

Rustのインストール方法は下記を参照してください。

使用するTCPサーバのコードは下記です。

動作確認済みのRustのバージョンは下記になります。

$ cargo --version
cargo 1.47.0-nightly (51b66125b 2020-08-19)

ビルド〜実行はtcpserverディレクトリ以下で下記コマンドを実行してください。

$ cargo run

IO多重化とタスクシステム

IO多重化とタスクシステムはtokioを使用しています。

use tokio::net::TcpListener;
use tokio::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut listner = TcpListener::bind("127.0.0.1:37564").await?;

    loop {
        let (mut socket, _) = listner.accept().await?;

        tokio::spawn(async move {
:

ヘッダー解析

単純に先頭から1バイトづつ、2バイト分取得してチェックします。

// Command analyze 1st
let commandx: &[u8] = &[buf[0]];
let command = std::str::from_utf8(commandx).unwrap();

eprintln!("command id is ; id = {:?}", command);

// Command analyze 2nd
let commandx2: &[u8] = &[buf[1]];
let command2 = std::str::from_utf8(commandx2).unwrap();

eprintln!("command2 id is ; id = {:?}", command2);

3. Wiresharkで独自プロトコルを解析するためのスクリプトを作成する

解析スクリプトは下記にあります。

短いので全コードを見せると下記のようになっています。

-- wireshark protocol analyzer for lessen protocol.

--プロトコル定義
lessen = Proto("lessen", "Lessen Protocol")

-- プロトコルフィールド定義
lessen_command_id_one = ProtoField.new("Lessen Protocol Command 1", "lessen.command_id_one", ftypes.STRING)
lessen_command_id_two = ProtoField.new("Lessen Protocol Command 2", "lessen.command_id_two", ftypes.STRING)
lessen_data = ProtoField.new("Lessen Protocol Data", "lessen.data", ftypes.STRING)

-- プロトコルフィールド定義
lessen.fields = {lessen_command_id_one, lessen_command_id_two, lessen_data}

-- Dissector定義
function lessen.dissector(buffer, pinfo, tree)
    -- 表示名
    pinfo.cols.protocol:set("LESSEN")

    -- データ長を計算
    local buflen = buffer:reported_length_remaining()
    local datalen = buflen - 2

    -- バッファ長がヘッダー長に満たない場合はスルー
    if buflen < 2 then
        return
    end

    -- サブツリーとして追加
    local subtree = tree:add(lessen, buffer())

    -- サブツリーにlessen_command_id_oneを追加
    subtree:add(lessen_command_id_one, buffer(0,1))
    -- サブツリーにlessen_command_id_twoを追加
    subtree:add(lessen_command_id_two, buffer(1,1))

    -- サブツリーにlessen_dataを追加
    if datalen > 0 then
        subtree:add(lessen_data, buffer(2,datalen))
    end
end

-- Lessen Protocolをポート番号と対応させる
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(37564, lessen)

Wiresharkにluaスクリプトを読み込ませる方法は下記です。

  1. Wiresharkを起動し、メニューからWireshark > About Wiresharkを選択してAbout画面を表示する
  2. About画面のFoldersを選択し、Personal Lua Pluginsをダブルクリックすると、ディレクトリが存在していれば開き、存在していなければ作成するかどうか聞いてくるので作成する
  3. Personal Lua Pluginsディレクトリの中にluaスクリプトをコピーする
  4. WiresharkのメニューからAnalyze > Reload Lua Pluginsを実行

スクリプトがちゃんと読み込まれたか確認するのは下記方法です。

  1. Wiresharkのメニューから Analyze > Enabled Protocols を選択
  2. Searchに lessen と入力
  3. LESSEN プロトコルが表示されるか確認

スクリーンショット 2020-09-25 16.20.31.png

ヘッダー解析

こちらも単純に先頭から1バイトつづ、2バイト分取得して解析結果に追加してます。

    -- サブツリーとして追加
    local subtree = tree:add(lessen, buffer())

    -- サブツリーにlessen_command_id_oneを追加
    subtree:add(lessen_command_id_one, buffer(0,1))
    -- サブツリーにlessen_command_id_twoを追加
    subtree:add(lessen_command_id_two, buffer(1,1))

4. telnetでサーバと通信し、Wiresharkで通信内容を解析する

telnetを起動し、コマンドを送ると応答が返ってきます。

01hoge
0-hello world!
00hoge
00orega gundam da!
11hoge
11hoge

Wiresharkの解析結果表示

下記コマンドの通信内容をWiresharkで見ます。

01hoge
0-hello world!

Portは 37564 なので、この通信をフィルタします。

スクリーンショット 2020-11-25 15.09.04.png

Protocol 列に独自プロトコルである LESSEN が表示されているのが確認できます。
次に解析結果を見てみます。

スクリーンショット 2020-11-25 15.09.17.png

Lessen Protocol というツリーが表示され、プロトコルの解析結果が表示されていることが確認できます。
今回サーバーに送ったコマンドは、

意味
コマンドID 1 0
コマンドID 2 1
データ hoge¥r¥n

ですので、この情報が正しく解析できていることが分かります。
また、レスポンスも解析されています。

スクリーンショット 2020-11-25 15.16.42.png

5. まとめ

いかがでしたでしょうか。
プロトコルというのは決まりごとで出来ていて、決まりごとを実装することで通信や解析ができることが分かると思います。
この通信の仕様をTCP上に作ること自体は難しいことではないことも理解できたと思います(ただし実運用に耐えうる仕様を作るのは難しい)。
HTTPとWebサーバーも基本的にこのようにして作成されています。なんだか身近になった気がすると思います、たぶん。

皆さんもぜひ独自プロトコルを作ってインターネッツライフをエンジョイしてみてはいかがでしょうか。

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9