概要
昨今はオープンに設計されたHTTPなどのプロトコルを、オープンソースとして開発されたサーバやライブラリを用いて利用するため、独自にプロトコルを設計して開発するという機会はほぼなくなりました。
しかし、低レイヤーで何が起きているか何ができるのかを理解するには、独自プロトコルを設計して開発してみるという体験が一番良いと思っています。
そこで、社内の若手エンジニア向けに書いた記事を再編集して公開します。
TCPの基礎とWiresharkの基礎については、以前書いた記事WiresharkではじめるTCP超基礎入門を見てもらえると嬉しいです。
何をやるか
以下のことをやっていきます。
- 独自プロトコルを設計する
- Rustで独自プロトコルを処理できるサーバを作成する
- Wiresharkで独自プロトコルを解析するためのスクリプトを作成する
- 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スクリプトを読み込ませる方法は下記です。
- Wiresharkを起動し、メニューから
Wireshark > About Wireshark
を選択してAbout画面を表示する - About画面の
Folders
を選択し、Personal Lua Plugins
をダブルクリックすると、ディレクトリが存在していれば開き、存在していなければ作成するかどうか聞いてくるので作成する -
Personal Lua Plugins
ディレクトリの中にluaスクリプトをコピーする - Wiresharkのメニューから
Analyze > Reload Lua Plugins
を実行
スクリプトがちゃんと読み込まれたか確認するのは下記方法です。
- Wiresharkのメニューから
Analyze > Enabled Protocols
を選択 - Searchに
lessen
と入力 -
LESSEN
プロトコルが表示されるか確認
ヘッダー解析
こちらも単純に先頭から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
なので、この通信をフィルタします。
Protocol
列に独自プロトコルである LESSEN
が表示されているのが確認できます。
次に解析結果を見てみます。
Lessen Protocol
というツリーが表示され、プロトコルの解析結果が表示されていることが確認できます。
今回サーバーに送ったコマンドは、
意味 | 値 |
---|---|
コマンドID 1 | 0 |
コマンドID 2 | 1 |
データ | hoge¥r¥n |
ですので、この情報が正しく解析できていることが分かります。
また、レスポンスも解析されています。
5. まとめ
いかがでしたでしょうか。
プロトコルというのは決まりごとで出来ていて、決まりごとを実装することで通信や解析ができることが分かると思います。
この通信の仕様をTCP上に作ること自体は難しいことではないことも理解できたと思います(ただし実運用に耐えうる仕様を作るのは難しい)。
HTTPとWebサーバーも基本的にこのようにして作成されています。なんだか身近になった気がすると思います、たぶん。
皆さんもぜひ独自プロトコルを作ってインターネッツライフをエンジョイしてみてはいかがでしょうか。