コンテンツにスキップ

カスタムプロトコルのデコード

独自の、あるいは社内向けのソケットプロトコルをキャプチャしたものの、組み込みのフォーマットでは認識できず、ただのバイトの山だけが残ってしまった。カスタムデコードは、そんなときに短いスクリプトで読み方をツールに教える機能です。連続したバイトストリームを個々のメッセージに切り出し、プロトコルヘッダーを取り除き、必要に応じて解凍したら、あとはツールの自動構造化認識に任せられます。

このページは、デコーダーを自分で書きたい方に向けたものです。関数の書き方、呼び出されるタイミング、戻り値の意味を説明します。


1. これは何か: フレーム単位のデコードフック

Section titled “1. これは何か: フレーム単位のデコードフック”

あなたはデコードフックを書き、ツールはそこへコネクションのバイトを渡しながら、繰り返しこう問いかけます。「ここから始めて、完全なメッセージを 1 つ切り出せますか?」あなたはメッセージを 1 つ切り出し、何バイト使ったかを報告します。ツールは残りのバイトで再び問いかけ、これをストリームがすべて切り出されるまで繰り返します。

ネットワークフレームワークでバイトを蓄積していくデコーダーを書いたことがあれば、考え方は同じです。連続したバイトストリームを渡され、メッセージの境界を自分で決めるわけです。重要な取り決めが 2 つあります。

  • 方向は気にしなくてよい: コネクションの送信ストリーム受信ストリームは、それぞれ別々にあなたのフックを通ります。切り出された各メッセージには、ツールが自動で方向のラベルを付けます。スクリプトは「どう切り出すか」だけを扱えばよいのです。
  • 切り出した後の表示は扱わなくてよい: あなたが push したメッセージはすべて、もう一度自動認識を通ります。中に protobuf、JSON、plist が入っていれば、それがさらに構造化された形へ解析されます。あなたが担うのは、フレーム分割、ヘッダーの除去、解凍だけです。

デコーダー全体は 1 つの関数です。

function decode(buf, out) {
// buf: 現在パース待ちのバイトストリーム; out: 出力コレクター; 戻り値: 今回消費したバイト数
}

buf(入力): パース待ちのバイトストリーム

  • 型は ArrayBuffer。その中身は「前回消費を終えた位置から現在の末尾まで」の残りバイトすべてです。
  • プロパティ buf.byteLength: 今この瞬間に残っているバイト数(呼び出しごとに増えることがあります。メッセージを切り出せるだけ揃っているかは、これで判断します)。
  • ⚠️ これは生のバイトバッファであって Uint8Array ではないため、バイトを直接インデックスできません(buf[0]undefined を返します)。バイトを読むには、セクション 3 の組み込みヘルパー(u8 / u16be / sub …)を使うか、new Uint8Array(buf)new DataView(buf) で自分でビューをかぶせてください。

out(出力): 出力コレクター

  • 型は素の配列です。
  • メソッド out.push(メッセージのバイト): メッセージを 1 つ押し出します。1 回の呼び出しで複数 push しても構いません。
  • 「メッセージのバイト」に使えるのは、組み込みヘルパー(sub(...)gunzip(...) など)が返す ArrayBuffer、または文字列です。それ以外の型は無視されます。

戻り値: 消費したバイト数

  • 型は整数(number)で、buf先頭から使ったバイト数です。
  • > 0: 先頭からこのバイト数を消化してメッセージを 1 つ切り出した → ツールはそれらを破棄し、残りのバイトで再びあなたを呼び出します。
  • 0 / return なし / 負数: 「まだ先頭に完全なメッセージがない」または「これ以上切り出せない」の意味 → ツールはループを止め、残りのバイトを生データとして表示します。
  • 戻り値はバッファ長の範囲内に自動的にクランプされます(オーバーランを防ぐため)が、実際に使った本当のバイト数を返してください。

呼び出されるタイミング(ライフサイクル)

Section titled “呼び出されるタイミング(ライフサイクル)”
  • ツールはコネクションの送信ストリームに 1 回、受信ストリームに 1 回、それぞれパスを実行します。各パスでは decodeループで呼び出され、そのつど残りの未消費バイトが渡されます。あなたはメッセージを 1 つ切り出して消費バイト数を返し、これを 0 を返すまで繰り返します。
  • メッセージは、消費が確定して初めて現れます: あなたが push したメッセージは、あわせて > 0 を返したときにだけ表示されます。push しても 0 を返すとループは止まり、その push は破棄されます。ですからメッセージの push は、必ず「何バイト使ったか」を返すこととセットにしてください。
  • ループが終わった後も、消費されずに末尾に残ったバイトは失われません。1 つの生データのかたまりとして表示されます(たとえば、不完全なメッセージの後半部分など)。
  • 各ストリーム(方向)には、独立したまっさらなスクリプト環境が与えられます。送信ストリームと受信ストリームが互いに干渉することはありません。
  • 呼び出しをまたいで状態(カウンター、直前のメッセージの型など)を保持したいときは、変数を decode外側で宣言してください。それらは同じストリーム内で呼び出しをまたいで保持され、方向が変わるか再デコードするとリセットされます。
  • デコードはすでにキャプチャ済みのデータに対して実行され、何度でも再実行できます。スクリプトを編集して保存し、もう一度クリックすれば、同じコネクションを新しいルールで再デコードできます。
  • スクリプトが例外を投げたり、1 回の実行がタイムアウトしたりした場合は「今回は何も消費しなかった」として扱われ、残りのバイトは生データとして表示されます。そしてキャプチャはデータを失わずそのまま続きます
  • つまり、壊れたスクリプトが最悪でも起こせるのはデコードの失敗にとどまり、生のバイトが残るだけで、コネクションをクラッシュさせることはありません。安心して編集してください。
  • しかもエラーはもみ消されません: 例外やタイムアウトは、結果の上にあるデバッグ出力パネルに表示され(セクション 5 参照)、送信 と受信 のどちらのストリーム由来かがラベルで示されます。それをたどれば、そのまま修正へ行けます。

3. 組み込みヘルパー(スクリプト内で直接使える)

Section titled “3. 組み込みヘルパー(スクリプト内で直接使える)”

よくある処理、すなわちバイトの読み取り、整数の読み取り、テキストへの変換、解凍は、すべて組み込みで用意されているので、自分で作り直す必要はありません。

分類 シグネチャ 説明
サブスライスを取る sub(buf, off[, len]) off から切り出す(len を省くと末尾まで)、ArrayBuffer を返す
整数を読む u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) ビッグエンディアン / リトルエンディアンで符号なし整数を読む
テキストに変換 hex(buf) / ascii(buf) 16 進文字列に変換 / テキストとして読み取る
圧縮を検出 gzipMagic(buf) gzip かどうか(真偽値を返す)
解凍 gunzip / inflate / unzstd / lz4dtx (buf) 解凍する。できない場合はエラーにせず入力をそのまま返す
デバッグ log(...args) / console.log(...args) デバッグパネルへ出力(セクション 5 参照)

標準的なバイト読み書きの機能(DataViewUint8Array など)もそのまま使えます。


① 長さ接頭辞 [4 バイトのビッグエンディアン長][ペイロード]、独自プロトコルで最もよくある形です。

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // 長さヘッダーがまだ届ききっていない、待つ
const total = 4 + u32be(buf, 0) // メッセージ全体 = 4 バイトのヘッダー + ペイロード
if (buf.byteLength < total) return 0 // メッセージ全体がまだ届ききっていない、待つ
out.push(sub(buf, 4, total - 4)) // ヘッダーを取り除き、ペイロードを自動認識へ渡す
return total // このメッセージを消費し、次の切り出しへ進む
}

② 区切り文字 / 行単位、改行などのマーカーでフレームを切り出します。

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // まだ改行がない、待つ
out.push(sub(buf, 0, i)) // この行を push(改行は含めない)
return i + 1 // 改行もあわせて消費する
}

③ まとめて一括処理、ストリーム全体を 1 つのメッセージとして扱います。たとえば全体を解凍する場合です。

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // gzip なら解凍する
return buf.byteLength // すべて消費、ループはすぐ終わる
}

④ 型で振り分け、ヘッダーに型フィールドがあり、型ごとに処理を分けます。

function decode(buf, out) {
if (buf.byteLength < 4) return 0
const total = 4 + u32be(buf, 0)
if (buf.byteLength < total) return 0
const type = u8(buf, 4) // 先頭バイトがメッセージの型
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // 型 2 は圧縮されている
return total
}

エディターには、これらのパターンをそろえたすぐ使えるテンプレートがあります。1 つ挿入して、いくつか数字を調整すれば動きます。

カスタムデコーダーのエディター: decode(buf, out) スクリプトを書く。「テンプレートを挿入」で、長さ接頭辞 / マジックシグネチャ / 区切り文字 / 固定長 / gzip / 型で振り分け のすぐ使える骨組みが用意され、関数のクイックリファレンスも付属


  • 書いて保存する: デコーダーエディターで作成し、名前を付けて保存します。テンプレートを挿入して始められ、組み込み関数のクイックリファレンスもすぐ手元にあります。
  • 適用する: 意味がつかめないコネクションを右クリックし、「デコード方法を指定」→ 自分のデコーダー を選ぶと、そのコネクションの送受信トラフィック全体が即座にあなたのルールで切り出されて表示されます。
  • デバッグと変数の確認: decode の中では log(...)console.log(...) で任意の値を出力できます。バイト数、型フィールド、hex(sub(buf, 0, 8)) などです。出力は「デコード方法を指定」の結果のにある「デバッグ出力」パネルに現れ、各行に送信 と受信 のどちらのストリーム由来かがラベル付けされます。スクリプトのエラーやタイムアウトも同じパネルに、やはり方向のラベル付きで表示されます。こうして出力しては場所を突き止めるやり方は、当てずっぽうに編集するよりずっと速く進みます。

    サンドボックスには完全なブラウザ / Node の console はありません。このパネルにつながっているのは console.log と、それと同等の log だけです。TextDecoderfetchsetTimeout などは使えません。バイトをテキストとして読むには ascii(buf) を使ってください。

  • 編集してすぐ反映を見る: 再キャプチャは不要です。スクリプトを保存し、もう一度「デコード方法を指定」をクリックすれば、同じコネクションがその場で新しいルールにより再デコードされます。思いどおりに切り出せるまで、繰り返し調整できます。
  • 構造へ進む: デコードされた各断片は自動認識へ戻されるので、protobuf、JSON、plist はさらに構造化された形へ解析され、データの確認とデコードの複数のビューで見られます。
  • デコーダーは名前を付けてリストとして保管でき、いつでも追加・編集・削除できます。

  • 社内向けや独自のソケットプロトコル(よくある形は 長さ接頭辞 + protobuf / JSON / バイナリ)をキャプチャしたが、組み込みのフォーマットでは認識できないとき。デコーダーを書いて切り出し、読める構造へ復元します。
  • カスタムヘッダーに包まれた、あるいは 1 段圧縮を経たバイトのかたまりを、読める内容に戻したいとき。

デコードのエントリーポイント、組み込みヘルパー関数、実行環境をまとめたクイックリファレンスです。buf を取る関数はすべて、ArrayBuffer と文字列のどちらも受け付けます。

デコードのエントリーポイント

Section titled “デコードのエントリーポイント”
decode(buf, out) → number
目的 デコーダー唯一のエントリーポイント、必ず実装することbuf の先頭からメッセージを切り出し、outpush し、今回消費したバイト数を返す。
buf ArrayBuffer、パース待ちの残りバイトストリーム。長さは buf.byteLength を使う。バイトを直接インデックスできないため、下記のヘルパーか new DataView(buf) / new Uint8Array(buf) を使う。
out Array、出力コレクター。out.push(bytes) でメッセージを 1 つ押し出す(bytesArrayBuffer または文字列。それ以外の型は無視される)。1 回で複数 push できる。
戻り値 numberbuf の先頭から消費したバイト数。> 0 で継続、0 / 負数 / 戻り値なし → 停止し、残りのバイトは生データとして表示される。
呼び出し規約 コネクションの送信ストリームと受信ストリームにそれぞれ 1 パスずつ実行する。各パスでは繰り返し呼び出し、そのつど残りの未消費バイトを渡し、0 を返すまで続ける。

サブスライスを取る

シグネチャ 戻り値 説明
sub(buf, off) ArrayBuffer off から末尾まで切り出す
sub(buf, off, len) ArrayBuffer off から len バイト切り出す。範囲外の off / len は自動でクランプされ、エラーにならない

整数を読む(符号なし。off が範囲外のときは 0 を返す)

シグネチャ 戻り値 説明
u8(buf, off) number 1 バイト読む
u16be(buf, off) / u16le(buf, off) number 2 バイト読む、ビッグエンディアン / リトルエンディアン
u32be(buf, off) / u32le(buf, off) number 4 バイト読む、ビッグエンディアン / リトルエンディアン

テキストに変換

シグネチャ 戻り値 説明
hex(buf) string 小文字の 16 進数に変換
ascii(buf) string テキストとして読み出す(ASCII / テキスト内容に適する)

圧縮 / 解凍(解凍できないときは入力をそのまま返す。1 回の解凍の上限は約 16 MB)

シグネチャ 戻り値 説明
gzipMagic(buf) boolean gzip のマジックナンバーで始まるかどうか
gunzip(buf) ArrayBuffer gzip の解凍
inflate(buf) ArrayBuffer zlib / deflate の解凍
unzstd(buf) ArrayBuffer zstd の解凍
lz4dtx(buf) ArrayBuffer ブロック LZ4 ストリームの解凍

デバッグ

シグネチャ 戻り値 説明
log(...args) (なし) 引数を 1 行に連結して「デバッグ出力」パネルへ出力する。ArrayBuffer は 16 進数で表示される
console.log(...args) (なし) log と同じ(console.log に慣れた人向けに便利)
  • 標準的な JavaScript: ArrayBuffer、型付き配列(Uint8Array など)、DataViewJSONMathRegExpDate といった一般的な組み込みオブジェクトはすべて使えます。より低レベルなバイトの読み書きが必要なときは new DataView(buf) / new Uint8Array(buf) がすぐ使えます。
  • デバッグ: log(...) / console.log(...) は「デバッグ出力」パネルへ出力します。スクリプトのエラーやタイムアウトもそこへ、送信 と受信 のどちらのストリーム由来かのラベル付きで表示されます。
  • 提供されないもの: それ以外のブラウザ / Node 固有の機能、すなわち log 以外の console メソッド、TextDecoder / TextEncoderfetchsetTimeoutrequire などはすべて使えません。バイトをテキストとして読むには ascii(buf) を使ってください。
  • decode 呼び出しには実行時間の上限があります。無限ループやタイムアウトは中断され「何も消費しなかった」として扱われるので、ツールがハングすることはありません。

プロキシキャプチャに戻る · 関連: データの確認とデコード · ローカル NIC キャプチャ