カスタムプロトコルのデコード
独自の、あるいは社内向けのソケットプロトコルをキャプチャしたものの、組み込みのフォーマットでは認識できず、ただのバイトの山だけが残ってしまった。カスタムデコードは、そんなときに短いスクリプトで読み方をツールに教える機能です。連続したバイトストリームを個々のメッセージに切り出し、プロトコルヘッダーを取り除き、必要に応じて解凍したら、あとはツールの自動構造化認識に任せられます。
このページは、デコーダーを自分で書きたい方に向けたものです。関数の書き方、呼び出されるタイミング、戻り値の意味を説明します。
1. これは何か: フレーム単位のデコードフック
Section titled “1. これは何か: フレーム単位のデコードフック”あなたはデコードフックを書き、ツールはそこへコネクションのバイトを渡しながら、繰り返しこう問いかけます。「ここから始めて、完全なメッセージを 1 つ切り出せますか?」あなたはメッセージを 1 つ切り出し、何バイト使ったかを報告します。ツールは残りのバイトで再び問いかけ、これをストリームがすべて切り出されるまで繰り返します。
ネットワークフレームワークでバイトを蓄積していくデコーダーを書いたことがあれば、考え方は同じです。連続したバイトストリームを渡され、メッセージの境界を自分で決めるわけです。重要な取り決めが 2 つあります。
- 方向は気にしなくてよい: コネクションの送信ストリームと受信ストリームは、それぞれ別々にあなたのフックを通ります。切り出された各メッセージには、ツールが自動で方向のラベルを付けます。スクリプトは「どう切り出すか」だけを扱えばよいのです。
- 切り出した後の表示は扱わなくてよい: あなたが push したメッセージはすべて、もう一度自動認識を通ります。中に protobuf、JSON、plist が入っていれば、それがさらに構造化された形へ解析されます。あなたが担うのは、フレーム分割、ヘッダーの除去、解凍だけです。
2. decode 関数: decode(buf, out)
Section titled “2. decode 関数: decode(buf, out)”デコーダー全体は 1 つの関数です。
function decode(buf, out) { // buf: 現在パース待ちのバイトストリーム; out: 出力コレクター; 戻り値: 今回消費したバイト数}入力・出力・戻り値とは何か
Section titled “入力・出力・戻り値とは何か”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 つの生データのかたまりとして表示されます(たとえば、不完全なメッセージの後半部分など)。
状態と冪等性
Section titled “状態と冪等性”- 各ストリーム(方向)には、独立したまっさらなスクリプト環境が与えられます。送信ストリームと受信ストリームが互いに干渉することはありません。
- 呼び出しをまたいで状態(カウンター、直前のメッセージの型など)を保持したいときは、変数を
decodeの外側で宣言してください。それらは同じストリーム内で呼び出しをまたいで保持され、方向が変わるか再デコードするとリセットされます。 - デコードはすでにキャプチャ済みのデータに対して実行され、何度でも再実行できます。スクリプトを編集して保存し、もう一度クリックすれば、同じコネクションを新しいルールで再デコードできます。
エラーで壊れることはない
Section titled “エラーで壊れることはない”- スクリプトが例外を投げたり、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 参照) |
標準的なバイト読み書きの機能(DataView、Uint8Array など)もそのまま使えます。
① 長さ接頭辞 [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 つ挿入して、いくつか数字を調整すれば動きます。

5. 使い方、デバッグの仕方
Section titled “5. 使い方、デバッグの仕方”- 書いて保存する: デコーダーエディターで作成し、名前を付けて保存します。テンプレートを挿入して始められ、組み込み関数のクイックリファレンスもすぐ手元にあります。
- 適用する: 意味がつかめないコネクションを右クリックし、「デコード方法を指定」→ 自分のデコーダー を選ぶと、そのコネクションの送受信トラフィック全体が即座にあなたのルールで切り出されて表示されます。
- デバッグと変数の確認:
decodeの中ではlog(...)やconsole.log(...)で任意の値を出力できます。バイト数、型フィールド、hex(sub(buf, 0, 8))などです。出力は「デコード方法を指定」の結果の上にある「デバッグ出力」パネルに現れ、各行に送信↑と受信↓のどちらのストリーム由来かがラベル付けされます。スクリプトのエラーやタイムアウトも同じパネルに、やはり方向のラベル付きで表示されます。こうして出力しては場所を突き止めるやり方は、当てずっぽうに編集するよりずっと速く進みます。サンドボックスには完全なブラウザ / Node の
consoleはありません。このパネルにつながっているのはconsole.logと、それと同等のlogだけです。TextDecoder、fetch、setTimeoutなどは使えません。バイトをテキストとして読むにはascii(buf)を使ってください。 - 編集してすぐ反映を見る: 再キャプチャは不要です。スクリプトを保存し、もう一度「デコード方法を指定」をクリックすれば、同じコネクションがその場で新しいルールにより再デコードされます。思いどおりに切り出せるまで、繰り返し調整できます。
- 構造へ進む: デコードされた各断片は自動認識へ戻されるので、protobuf、JSON、plist はさらに構造化された形へ解析され、データの確認とデコードの複数のビューで見られます。
- デコーダーは名前を付けてリストとして保管でき、いつでも追加・編集・削除できます。
6. どんなときに使うか
Section titled “6. どんなときに使うか”- 社内向けや独自のソケットプロトコル(よくある形は 長さ接頭辞 + protobuf / JSON / バイナリ)をキャプチャしたが、組み込みのフォーマットでは認識できないとき。デコーダーを書いて切り出し、読める構造へ復元します。
- カスタムヘッダーに包まれた、あるいは 1 段圧縮を経たバイトのかたまりを、読める内容に戻したいとき。
7. API リファレンス
Section titled “7. API リファレンス”デコードのエントリーポイント、組み込みヘルパー関数、実行環境をまとめたクイックリファレンスです。buf を取る関数はすべて、ArrayBuffer と文字列のどちらも受け付けます。
デコードのエントリーポイント
Section titled “デコードのエントリーポイント”decode(buf, out) → number| 目的 | デコーダー唯一のエントリーポイント、必ず実装すること。buf の先頭からメッセージを切り出し、out へ push し、今回消費したバイト数を返す。 |
buf |
ArrayBuffer、パース待ちの残りバイトストリーム。長さは buf.byteLength を使う。バイトを直接インデックスできないため、下記のヘルパーか new DataView(buf) / new Uint8Array(buf) を使う。 |
out |
Array、出力コレクター。out.push(bytes) でメッセージを 1 つ押し出す(bytes は ArrayBuffer または文字列。それ以外の型は無視される)。1 回で複数 push できる。 |
| 戻り値 | number、buf の先頭から消費したバイト数。> 0 で継続、0 / 負数 / 戻り値なし → 停止し、残りのバイトは生データとして表示される。 |
| 呼び出し規約 | コネクションの送信ストリームと受信ストリームにそれぞれ 1 パスずつ実行する。各パスでは繰り返し呼び出し、そのつど残りの未消費バイトを渡し、0 を返すまで続ける。 |
組み込みヘルパー関数
Section titled “組み込みヘルパー関数”サブスライスを取る
| シグネチャ | 戻り値 | 説明 |
|---|---|---|
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など)、DataView、JSON、Math、RegExp、Dateといった一般的な組み込みオブジェクトはすべて使えます。より低レベルなバイトの読み書きが必要なときはnew DataView(buf)/new Uint8Array(buf)がすぐ使えます。 - デバッグ:
log(...)/console.log(...)は「デバッグ出力」パネルへ出力します。スクリプトのエラーやタイムアウトもそこへ、送信↑と受信↓のどちらのストリーム由来かのラベル付きで表示されます。 - 提供されないもの: それ以外のブラウザ / Node 固有の機能、すなわち
log以外のconsoleメソッド、TextDecoder/TextEncoder、fetch、setTimeout、requireなどはすべて使えません。バイトをテキストとして読むにはascii(buf)を使ってください。 - 各
decode呼び出しには実行時間の上限があります。無限ループやタイムアウトは中断され「何も消費しなかった」として扱われるので、ツールがハングすることはありません。
プロキシキャプチャに戻る · 関連: データの確認とデコード · ローカル NIC キャプチャ