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

# 概要

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

TCPの基礎とWiresharkの基礎については、以前書いた記事[WiresharkではじめるTCP超基礎入門](https://qiita.com/edi_t/items/7dcd6724f26d8ac513c4)を見てもらえると嬉しいです。

# 何をやるか

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

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のインストール方法は下記を参照してください。

https://www.rust-lang.org/tools/install

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

https://github.com/lunoir/tcp-lessen/tree/master/lessen5/tcpserver

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

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

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

```sh
$ cargo run
```

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

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

```rust
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バイト分取得してチェックします。

```rust
// 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で独自プロトコルを解析するためのスクリプトを作成する

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

https://github.com/lunoir/tcp-lessen/tree/master/lessen5/dissector

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

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

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

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

![スクリーンショット 2020-09-25 16.20.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538525/c0edf949-3f42-2fb4-eb0b-9a38dc863e3a.png)


## ヘッダー解析

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

```lua
    -- サブツリーとして追加
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538525/7881b096-df01-d688-25b4-2b2b540f57b9.png)

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

![スクリーンショット 2020-11-25 15.09.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538525/bceee5fc-aa21-f753-bb38-1e0032f36969.png)

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

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

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

![スクリーンショット 2020-11-25 15.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538525/fdebd625-2308-7c07-ba6d-fbdf9ee1499a.png)

# 5. まとめ

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

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