独自の dissector を作成:Wireshark Lua の強力な機能解説

はい、承知いたしました。「独自の dissector を作成:Wireshark Lua の強力な機能解説」と題した、WiresharkのLua dissector開発に関する詳細な解説記事を作成します。約5000語を目標に、Lua dissectorの基礎から応用、実践的なコード例までを網羅します。


独自の dissector を作成:Wireshark Lua の強力な機能解説

はじめに

ネットワーク通信の解析は、システムのデバッグ、セキュリティ分析、パフォーマンス最適化など、多岐にわたる分野で不可欠なスキルです。その中心的なツールの一つが、世界中で広く利用されているネットワークプロトコルアナライザ、Wiresharkです。Wiresharkは、ネットワーク上を流れるパケットをキャプチャし、それぞれのパケットを構成するプロトコルに応じて構造化された形式で表示する能力を持っています。

この「構造化された表示」を実現しているのが、「Dissector(ディセクター)」と呼ばれるWiresharkの内部コンポーネントです。Dissectorは、特定のプロトコルのフォーマットを理解し、パケットのバイナリデータを解析ツリー(Protocol Tree)として人間が読める形式に変換します。Wiresharkには標準で多くのプロトコルに対応するDissectorが内蔵されていますが、世の中には標準化されていない独自プロトコルや、特定の用途に特化したカスタムプロトコルが数多く存在します。また、既存プロトコルであっても、特定のコンテキストや追加情報を解析に含めたい場合があります。

このような場合に必要となるのが、独自のDissectorを開発することです。WiresharkのDissectorは、主にC言語で開発されていますが、C言語での開発はコンパイルが必要であり、Wiresharkの内部APIを深く理解する必要があるため、比較的学習コストが高いという側面があります。

そこで、Wiresharkが提供しているのがLuaスクリプトによるDissector開発機能です。Luaは軽量で組み込みに適したスクリプト言語であり、その柔軟性と動的な性質から、WiresharkのDissectorやその他のプラグイン開発に非常に適しています。Lua Dissectorは、Wiresharkを再コンパイルすることなく、スクリプトファイルを配置するだけでロードして利用できるため、開発サイクルが早く、試行錯誤が容易です。

本記事では、WiresharkのLua Dissector開発に焦点を当て、その強力な機能と開発手法を詳細に解説します。独自プロトコルを解析したい、特定のプロトコル解析にカスタマイズを加えたい、あるいは単にWiresharkの内部動作に興味がある、といった方々にとって、Lua Dissectorは非常に有用なツールとなるでしょう。記事を通して、Lua Dissectorの基本構造から、パケットデータの読み込み、解析ツリーの構築、さらには高度な機能(ポートマッピング、サブDissector、設定項目、状態管理など)までを、実践的なコード例と共に習得することを目指します。

Wireshark Dissectorの基礎

独自Dissectorの開発に入る前に、WiresharkにおけるDissectorの役割と仕組みについて理解しておくことが重要です。

プロトコルスタックとDissector

ネットワーク通信は通常、プロトコルスタック(例:TCP/IPモデルやOSI参照モデル)に従って階層化されています。イーサネットフレームの中にIPパケットがあり、そのIPパケットの中にTCPまたはUDPセグメントがあり、その中にアプリケーション層のデータがある、といった具合です。

Wiresharkは、キャプチャした生バイト列を受け取ると、まず一番下の層(物理層に近い部分、例えばEthernetフレームやRadio Tapヘッダなど)のDissectorを呼び出します。このDissectorは、自身のプロトコルのヘッダを解析し、そのヘッダ内に含まれる情報(例:Ethernet Typeフィールド、IP Protocolフィールド、TCP/UDPポート番号など)から、次に解析すべき上位プロトコルを特定します。そして、その上位プロトコルに対応するDissectorを呼び出し、解析の処理をバトンタッチします。このプロセスがプロトコルスタックを上にたどっていく形で繰り返され、最終的にアプリケーション層のデータまで解析が進みます。

各Dissectorの主な役割は以下の通りです。

  1. プロトコルヘッダの解析: パケット内の該当プロトコル部分のバイナリデータを読み込み、フィールドごとに意味を解釈します。
  2. 解析ツリーの構築: 解析したフィールド名、値、オフセット、長さなどをWiresharkの解析ツリーに追加し、視覚的に分かりやすく表示します。
  3. 上位プロトコルの特定と呼び出し: ヘッダ情報(例:ポート番号)に基づいて、次に呼び出すべき上位プロトコルのDissectorを特定し、そのDissectorに制御を渡します(これを「ディスパッチ」と呼びます)。

ポート番号/プロトコルタイプに基づくディスパッチ

Wiresharkが上位プロトコルDissectorを呼び出す際に最もよく利用されるのが、「Dissector Table」という仕組みです。Dissector Tableは、特定のプロトコルヘッダ内の値(例:TCP/UDPポート番号、Ethernet Type値、IP Protocol値など)をキーとして、対応するDissectorをルックアップするために使用されます。

例えば、IP DissectorはIPヘッダの「Protocol」フィールド(TCPなら6、UDPなら17など)を読み込み、この値をキーとして「ip.proto」という名前のDissector Tableを検索します。見つかったDissector(例:TCP DissectorやUDP Dissector)を、残りのパケットデータ(IPヘッダより後の部分)を引数として呼び出します。

同様に、TCP DissectorやUDP Dissectorは、TCP/UDPヘッダの「Source Port」または「Destination Port」フィールドの値をキーとして、「tcp.port」または「udp.port」という名前のDissector Tableを検索し、対応するアプリケーション層プロトコルのDissectorを呼び出します。

独自のプロトコルを特定のポート番号(またはEthernet Typeなど)で実行する場合、Lua Dissectorから対応するDissector Tableに自作Dissectorを登録することで、Wiresharkに自動的に自身のDissectorを呼び出させることができます。

サブDissector

Dissectorは、解析の一部を別のDissectorに委ねることができます。これを「サブDissector」の呼び出しと呼びます。例えば、ある独自プロトコルのメッセージペイロード部分が、実は別の標準プロトコル(例:JSON、XML、Protobufなど)でエンコードされている場合、そのペイロード部分を標準プロトコルDissectorに解析させることができます。これにより、独自のDissectorが全てのプロトコルフォーマットを理解する必要がなくなり、コードの modularity が向上します。

Lua Dissectorは、既存の(C言語で書かれた)Dissectorや、他のLua DissectorをサブDissectorとして呼び出すことができます。

Dissectorのライフサイクル

Wiresharkがパケットをキャプチャまたはファイルから読み込むと、各パケットに対して以下のプロセスが実行されます。

  1. 最初のDissector呼び出し: Wiresharkは、設定に基づいてそのパケットタイプ(例:Ethernet)に対応する最初のDissectorを呼び出します。
  2. 解析処理: 呼ばれたDissectorは、パケットデータ(生のバイト列)を引数として受け取り、自身のプロトコル部分を解析し、解析ツリーを構築します。
  3. 上位Dissectorへのディスパッチ: プロトコルヘッダ情報に基づいて、次に呼び出すべきDissectorを特定し、残りのパケットデータを引数としてそのDissectorを呼び出します。
  4. 繰り返し: 上記2-3のプロセスがプロトコルスタックの最上位まで繰り返されます。
  5. ポスト解析: 全ての通常Dissectorの実行が完了した後、「frame.end」Dissector Tableに登録された「ポストDissector」が実行されることがあります。これは、複数のプロトコルやパケットにまたがる情報を処理する場合などに利用されます。

Lua Dissectorは、この解析パイプラインに沿って動作します。Luaスクリプトで定義されたDissectorは、Wiresharkの起動時にロードされ、適切なDissector Tableに登録されていれば、対応するパケットが到着した際にWiresharkから呼び出されます。

Wireshark Lua 環境のセットアップ

Lua Dissectorを開発するためには、まずWiresharkがLuaをサポートしてビルドされていることを確認し、Luaスクリプトを実行できる環境を整える必要があります。

WiresharkのインストールとLuaサポート確認

最新版のWiresharkを公式サイトからダウンロードしてインストールしてください。ほとんどの場合、公式バイナリにはLuaサポートが含まれています。
インストール後、Wiresharkを起動し、「ヘルプ」→「Wiresharkについて」を選択します。開いたウィンドウの「バージョン情報」タブに「Compiled with Lua X.Y.Z」のような表記があれば、Luaが有効になっています。もし表示がなければ、Luaサポートを有効にしてビルドされたWiresharkを入手する必要があります(通常は公式配布版で問題ありません)。

Luaスクリプトの配置場所

Wiresharkは、特定のディレクトリに配置されたLuaスクリプトを起動時に自動的に探し、ロードします。このディレクトリの場所はオペレーティングシステムやWiresharkのバージョンによって異なります。

Wireshark内で「ヘルプ」→「Wiresharkについて」→「フォルダ」タブを開くと、「Personal Lua Plugins」や「Global Lua Plugins」などの項目があり、そのパスが表示されます。

  • Personal Lua Plugins: ユーザー固有のDissectorを配置する場所です。通常は $HOME/.wireshark/plugins/lua (Linux/macOS) または %APPDATA%\Wireshark\plugins\lua (Windows) のようなパスになります。開発中はここにスクリプトを置くのが便利です。
  • Global Lua Plugins: システム全体で利用されるDissectorを配置する場所です。Wiresharkのインストールディレクトリ内の plugins/lua サブディレクトリなどになります。

開発するLuaスクリプト(例: myproto.lua)を、これらのフォルダのいずれかに配置します。通常は「Personal Lua Plugins」フォルダを使用するのが推奨されます。

Wireshark設定でのLuaスクリプト有効化

Lua Dissectorをロードするためには、Wiresharkの設定でLuaサポートが有効になっている必要があります。
「編集」→「設定」を開き、「Protocols」→「Lua」を選択します。
ここで「Enable Lua support」のチェックボックスがオンになっていることを確認してください。通常はデフォルトでオンになっています。

設定変更後、Wiresharkを再起動すると、指定したフォルダに配置されたLuaスクリプトが自動的にロードされ、エラーがなければDissectorが利用可能になります。

開発環境

Lua Dissector開発には、特別なIDEは必須ではありませんが、シンタックスハイライトやコード補完機能を持つテキストエディタがあると便利です。VS Code, Sublime Text, Atom, Notepad++ など、多くのエディタがLuaのサポートを提供しています。

デバッグに関しては、Wireshark自体に組み込まれたLuaデバッガは限定的です。Luaスクリプト内で print() 関数を使ってデバッグ情報を標準出力(Wiresharkのコンソールウィンドウやstderr)に出力するのが一般的な手法です。Windows版Wiresharkには通常、インストールディレクトリに WiresharkPortable.exerunshark.exe といった実行ファイルが含まれており、これらを使用するとコンソールウィンドウが開かれ、Luaスクリプトの出力やエラーメッセージが表示されます。Linux/macOSでは、ターミナルからwiresharkコマンドを実行することでコンソール出力を見ることができます。

最小限のLua Dissector

それでは、最も基本的なLua Dissectorを作成してみましょう。ここでは、UDPポート番号12345番で動作する非常に単純な独自プロトコル「MyProto」を想定します。このプロトコルは、単にいくつかのフィールドを持つヘッダと、それに続くペイロードがあるという構造とします。

Lua Dissectorの基本構造

Lua Dissectorスクリプトは、通常以下の要素を含みます。

  1. プロトコル定義: Proto.new() 関数を使って、新しいプロトコルオブジェクトを作成します。ここでプロトコル名と略称を定義します。
  2. プロトコルフィールド定義: ProtoField.new() 関数を使って、そのプロトコルに含まれる各フィールド(例:整数、文字列、バイト列など)を定義します。これらのフィールドは、Wiresharkの解析ツリーに表示される項目に対応します。
  3. Dissector関数の実装: プロトコルオブジェクトの dissector メソッドに、実際の解析処理を行うLua関数を代入します。この関数は、Wiresharkからパケットデータやその他の情報と共に呼び出されます。
  4. Dissectorの登録: 作成したDissectorを、適切なDissector Table(例:「udp.port」)に登録し、特定の条件(例:ポート番号)で呼び出されるようにします。

最小限のコード例

UDPポート12345番で受信したパケットを「MyProto」として解析する最小限のDissectorコードです。

“`lua
— 1. プロトコル定義
local myproto = Proto.new(“MyProto”, “My Custom Protocol”)

— 2. プロトコルフィールド定義 (必須ではないが、解析ツリー表示に必要)
— ここでは例として、単純なバイト列ペイロード用のフィールドを定義
local pf_payload = ProtoField.bytes(“myproto.payload”, “Payload”)
myproto.fields = { pf_payload }

— 3. Dissector関数の実装
function myproto.dissector(tvb, pinfo, tree)
— pinfo: Packet Info オブジェクト (フレーム番号、時間、プロトコル情報などを含む)
— tree: TreeItem オブジェクト (解析ツリーのルートノード)
— tvb: Tvb オブジェクト (現在のプロトコルのバイト列データ)

-- パケット情報カラムにプロトコル略称を表示
pinfo.cols.protocol:set(myproto.name)

-- 現在のプロトコル用のサブツリーを作成
local subtree = tree:add(myproto, tvb, 0, tvb:len())

-- パケット全体をペイロードとして表示 (例)
-- ProtoFieldを使ってツリーにフィールドを追加
subtree:add(pf_payload, tvb, 0, tvb:len())

-- 必要に応じて、ここで上位プロトコルDissectorを呼び出すことも可能だが、
-- 今回は最上位プロトコルとして扱うため呼び出さない。

end

— 4. Dissectorの登録
— UDPポート12345番のパケットをこのDissectorで処理するように登録
local udp_port_table = DissectorTable.get(“udp.port”)
udp_port_table:add(12345, myproto)

print(“MyProto Dissector loaded.”) — ロード確認用デバッグ出力
“`

コードの説明:

  • Proto.new("MyProto", "My Custom Protocol"): 新しいプロトコルオブジェクトを作成します。最初の引数はプロトコル略称(パケット一覧のカラムなどに表示)、2番目は詳細なプロトコル名です。
  • ProtoField.bytes("myproto.payload", "Payload"): バイト列を表す新しいプロトコルフィールドを定義します。最初の引数はフィールドのエイリアス(フィルタリングなどに使用)、2番目は解析ツリーに表示されるラベルです。他に ProtoField.uint8, ProtoField.int16, ProtoField.string など、様々なデータ型に対応するフィールド定義関数があります。
  • myproto.fields = { pf_payload }: 定義したフィールドをプロトコルオブジェクトに関連付けます。これにより、Wiresharkはこれらのフィールドがこのプロトコルに属することを認識し、フィルタリングなどで利用できるようになります。
  • function myproto.dissector(tvb, pinfo, tree) ... end: これが実際の解析処理を行う関数です。myproto.dissector に代入することで、この関数がWiresharkから呼び出されるようになります。
    • tvb: 後述するTvb(Tagged Value Buffer)オブジェクトです。解析対象のバイト列へのアクセスを提供します。
    • pinfo: Pinfo(Packet Information)オブジェクトです。現在のパケットに関する様々な情報(フレーム番号、タイムスタンプ、プロトコルカラム情報など)を含みます。
    • tree: TreeItemオブジェクトです。解析ツリーのルートノードを表します。解析結果をツリーに追加するために使用します。
  • pinfo.cols.protocol:set(myproto.name): パケット一覧表示の「Protocol」カラムに、このプロトコルの略称(”MyProto”)を表示するように設定します。
  • local subtree = tree:add(myproto, tvb, 0, tvb:len()): 解析ツリーにこのプロトコル用の新しいノード(サブツリー)を追加します。最初の引数はプロトコルオブジェクト自身、次の引数tvbは解析対象のバイト列、0はオフセット(開始位置)、tvb:len()は長さです。これにより、このツリーノードがパケット中のどのバイト範囲に対応するかがWiresharkに示されます。
  • subtree:add(pf_payload, tvb, 0, tvb:len()): 作成したサブツリーノードに、先ほど定義した pf_payload フィールドを追加します。このフィールドは、tvbのオフセット0から長さtvb:len()までのバイト列に対応します。
  • DissectorTable.get("udp.port"): 「udp.port」という名前のDissector Tableを取得します。
  • udp_port_table:add(12345, myproto): 取得したテーブルに、キー12345(ポート番号)と、値としてmyproto Dissectorオブジェクトを登録します。これにより、UDPパケットで宛先または送信元ポートが12345の場合、このmyproto.dissector関数が呼び出されるようになります。
  • print("MyProto Dissector loaded."): LuaスクリプトがWiresharkによって正常にロードされたかどうかを確認するための簡単なデバッグ出力です。コンソールウィンドウに表示されます。

ロードとテスト方法

  1. 上記のコードを myproto.lua のような名前で保存します。
  2. 保存したファイルを、Wiresharkの「Personal Lua Plugins」フォルダに配置します。
  3. Wiresharkを起動します。コンソールウィンドウ(あれば)やWiresharkのLuaコンソール(「ツール」→「Lua」→「Lua Console」)に「MyProto Dissector loaded.」と表示されていれば、スクリプトは正常にロードされています。エラーが表示される場合は、スクリプトの構文や配置場所を確認してください。
  4. テスト用のパケットをキャプチャまたは用意します。例えば、nc -u 127.0.0.1 12345 でローカルホストのUDPポート12345に短いメッセージを送信するなどの方法があります。
  5. Wiresharkでキャプチャを開始し、UDPポート12345への(またはからの)パケットを生成します。
  6. キャプチャされたパケットリストを確認します。ポート12345のUDPパケットの「Protocol」カラムが「MyProto」と表示され、下の解析ツリーに「My Custom Protocol」というノードとその子ノードとして「Payload」が表示されていれば成功です。

この最小限のDissectorは、パケット全体を単一のペイロードとして表示するだけですが、Lua Dissectorの基本的な構造とWiresharkへの組み込み方法を理解するのに役立ちます。次章以降では、実際のプロトコル構造を解析するためのTvbオブジェクトとTreeItemオブジェクトの詳細について解説します。

PacketBytes (Tvb) オブジェクトの詳細

Dissectorの核心は、パケットのバイナリデータを読み取り、構造を解釈することです。Lua Dissectorでは、このバイナリデータへのアクセスは Tvb(Tagged Value Buffer)オブジェクトを通じて行われます。Dissector関数に渡される tvb 引数がこれに該当します。

Tvbオブジェクトは、解析対象となっているパケットの特定のバイト範囲(現在のプロトコルに対応する部分)へのビューを提供します。生のメモリバッファそのものではなく、オフセットと長さを持つ参照のようなものです。これにより、データのコピーを最小限に抑えつつ、効率的にパケットデータを扱うことができます。

バイト列の操作とデータの読み込み

Tvbオブジェクトは、様々なデータ型の値を指定したオフセットから読み込むための豊富なメソッドを提供します。オフセットは、現在の tvb が指す範囲の開始位置を基準とした相対オフセットです(通常は0から始まります)。

主な読み込みメソッド:

  • tvb:len(): 現在の tvb が指すバイト範囲の全長を返します。
  • tvb:reported_length(): 元のパケットデータ(例えば、キャプチャされた全体)における、この tvb の範囲の報告された長さを返します。断片化されたパケットなどでは tvb:len() と異なる場合があります。
  • tvb:int(offset, length [, base]): 指定されたオフセットから指定された長さのバイト列を、符号付き整数として読み込みます。長さは1, 2, 3, 4, 8バイトを指定できます。base は数値表示の基数(例: BASE_DEC (10進数), BASE_HEX (16進数))を指定しますが、通常はProtoField定義で指定するためここでは不要です。
  • tvb:uint(offset, length [, base]): 同様に、符号なし整数として読み込みます。
  • tvb:int8(offset), tvb:uint8(offset): 1バイトの符号付き/なし整数を読み込みます。
  • tvb:int16(offset), tvb:uint16(offset): 2バイトの符号付き/なし整数を読み込みます。
  • tvb:int24(offset), tvb:uint24(offset): 3バイトの符号付き/なし整数を読み込みます。
  • tvb:int32(offset), tvb:uint32(offset): 4バイトの符号付き/なし整数を読み込みます。
  • tvb:int64(offset), tvb:uint64(offset): 8バイトの符号付き/なし整数を読み込みます。

エンディアンの指定

数値データを読み込む際、バイトオーダー(エンディアン)を指定する必要があります。Wireshark Lua Dissectorでは、メソッド名にサフィックスを付けることでエンディアンを指定します。

  • リトルエンディアン (Little Endian, LE): メソッド名に _le を付けます。例: tvb:uint16_le(offset)
  • ビッグエンディアン (Big Endian, BE): メソッド名に _be を付けます。例: tvb:uint32_be(offset)
  • ネットワークバイトオーダー (Network Byte Order, BEと同じ): サフィックスなし、または _be を付けます。TCP/IP関連プロトコルでは通常こちらです。例: tvb:uint16(offset) または tvb:uint16_be(offset)

独自のプロトコルがどのエンディアンを使用しているかを確認し、適切なメソッドを選択してください。

文字列とバイト列の読み込み

  • tvb:string(offset, length [, encoding]): 指定されたオフセットから指定された長さのバイト列を文字列として読み込みます。ヌル終端文字列の場合は length を省略できます。encoding(例: ENC_ASCII, ENC_UTF_8)を指定できますが、ProtoFieldで指定するのが一般的です。
  • tvb:bytes(offset, length): 指定されたオフセットから指定された長さのバイト列を ByteArray オブジェクトとして返します。このオブジェクトはさらに .bytes().tonumber() などのメソッドを持つことができます。長さは省略可能で、その場合ヌル終端までを読み込みます。
  • tvb:raw_bytes(offset, length): 同様にバイト列を返しますが、これはパケット全体の生バイト列に対するオフセットと長さで指定します。通常は tvb:bytes() を使用します。

部分Tvbの取得

あるプロトコル部分が別のプロトコル(サブDissector)によって解析される場合、その部分に対応する新しい Tvb オブジェクトを作成してサブDissectorに渡すのが一般的です。

  • tvb:range(offset, length): 現在の tvb 内の指定されたオフセットから指定された長さの範囲に対する新しい Tvb オブジェクト(ビュー)を作成して返します。この操作はコピーを伴わないため非常に効率的です。lengthを省略した場合、指定オフセットから現在のtvbの末尾まで全てを含みます。

バイト列の確認とエラーハンドリング

Dissectorは、パケットデータが予期した長さや構造を持たない場合を適切に処理する必要があります。特に、指定したオフセットや長さが現在の tvb の範囲を超えているかをチェックすることが重要です。範囲外アクセスを試みると、Luaエラーが発生し、解析が中断されます。

tvb:len()tvb:reported_length() を使って、読み込みを行う前にオフセットと長さが有効な範囲内にあるかを確認します。

“`lua
local offset = 0
local header_len = 8 — 想定するヘッダ長

— ヘッダ長が tvb の残り長さより短いかチェック
if tvb:len() < offset + header_len then
— エラー処理: パケットが短すぎる
— pinfo.expert にエラー情報を追加することも可能 (後述)
pinfo.cols.info:set(“Malformed packet: header truncated”)
— ツリーにも表示する (省略)
return — 解析を中断
end

— ここから安全にヘッダフィールドを読み込める
local field1 = tvb:uint32(offset)
local field2 = tvb:uint32(offset + 4)
offset = offset + header_len

— ペイロードがあるかチェック
if tvb:len() > offset then
— ペイロードの解析またはサブDissector呼び出し
local payload_tvb = tvb:range(offset) — 残り全てをペイロードとする
— … payload_tvb を使った処理 …
end
“`

Tvb オブジェクト使用例(拡張MyProto)

MyProtoに、メッセージタイプ (1バイト符号なし整数)、メッセージID (4バイトビッグエンディアン符号なし整数)、ペイロード長 (2バイトビッグエンディアン符号なし整数) のヘッダを追加し、その後に指定された長さのペイロードが続く構造を想定します。

“`lua
— 1. プロトコル定義 (変更なし)
local myproto = Proto.new(“MyProto”, “My Custom Protocol”)

— 2. プロトコルフィールド定義 (ヘッダフィールドを追加)
local pf_msg_type = ProtoField.uint8(“myproto.msg_type”, “Message Type”)
local pf_msg_id = ProtoField.uint32(“myproto.msg_id”, “Message ID”, base.HEX) — IDは16進数表示
local pf_payload_len = ProtoField.uint16(“myproto.payload_len”, “Payload Length”)
local pf_payload = ProtoField.bytes(“myproto.payload”, “Payload Data”) — バイト列フィールド

myproto.fields = {
pf_msg_type,
pf_msg_id,
pf_payload_len,
pf_payload
}

— 3. Dissector関数の実装
function myproto.dissector(tvb, pinfo, tree)
pinfo.cols.protocol:set(myproto.name)

local offset = 0
local header_len = 1 + 4 + 2 -- 1(type) + 4(id) + 2(length) = 7 bytes

-- ヘッダ長のチェック
if tvb:len() < header_len then
    pinfo.cols.info:set("MyProto: Malformed packet (header truncated)")
    local subtree = tree:add(myproto, tvb, 0, tvb:len()) -- ツリーは追加しておく
    subtree:add(tvb, 0, tvb:len()):append_text(" [Malformed Header]") -- エラー表示
    return -- 解析中断
end

-- プロトコル全体のサブツリーを作成
local subtree = tree:add(myproto, tvb, 0, tvb:len())

-- ヘッダフィールドの解析とツリーへの追加
local msg_type = tvb:uint8(offset)
subtree:add(pf_msg_type, tvb, offset, 1, msg_type) -- 値を明示的に渡すことも可能
offset = offset + 1

local msg_id = tvb:uint32_be(offset) -- ビッグエンディアンで読み込み
subtree:add(pf_msg_id, tvb, offset, 4, msg_id)
offset = offset + 4

local payload_len = tvb:uint16_be(offset) -- ビッグエンディアンで読み込み
subtree:add(pf_payload_len, tvb, offset, 2, payload_len)
offset = offset + 2

-- パケット情報カラムに主要な情報を表示
pinfo.cols.info:set(string.format("MyProto: Type=%d, ID=0x%X, PayloadLen=%d",
                                  msg_type, msg_id, payload_len))

-- ペイロードの解析とツリーへの追加
local payload_offset = offset
local actual_payload_len = tvb:len() - payload_offset

-- 報告されたペイロード長と実際のパケット長を比較
if actual_payload_len < payload_len then
    -- ペイロードが報告された長さより短い (truncated)
    pinfo.cols.info:append(" [Payload Truncated]")
    subtree:add(pf_payload, tvb, payload_offset, actual_payload_len):append_text(" [Truncated, Expected " .. payload_len .. " bytes]")
    -- 必要であれば、残りの部分も表示ツリーに追加するが、解析としては中断
elseif actual_payload_len > payload_len then
    -- ペイロードが報告された長さより長い (extra data)
    pinfo.cols.info:append(" [Extra Data]")
    subtree:add(pf_payload, tvb, payload_offset, payload_len) -- 報告された長さだけ表示
    local extra_tvb = tvb:range(payload_offset + payload_len)
    -- エラーツリーを追加して余分なデータを表示
    subtree:add(extra_tvb, 0, extra_tvb:len()):append_text(" [Extra Data]")
else -- actual_payload_len == payload_len
    -- ペイロード長が一致
    if payload_len > 0 then
         subtree:add(pf_payload, tvb, payload_offset, payload_len)
    end
end

-- ここでメッセージタイプなどに基づいてサブDissectorを呼び出すことも可能
-- (例: メッセージタイプ1の場合はXMLとして解析するなど)
-- if msg_type == 1 then
--     local xml_dissector = Dissector.get("xml")
--     if xml_dissector then
--         -- XMLデータ部分に対する新しい Tvb オブジェクトを作成し、サブツリーノードを渡す
--         local xml_tvb = tvb:range(payload_offset, payload_len)
--         xml_dissector:call(xml_tvb, pinfo, subtree)
--     end
-- end

end

— 4. Dissectorの登録 (変更なし)
local udp_port_table = DissectorTable.get(“udp.port”)
udp_port_table:add(12345, myproto)

print(“Extended MyProto Dissector loaded.”)
“`

この拡張例では、ヘッダの各フィールドを読み込み、その値を解析ツリーに追加しています。また、ペイロード長フィールドの値に基づいてペイロード部分を特定し、実際のパケット長との整合性をチェックしつつ表示しています。tvb:uint32_be() のようにエンディアンを指定して数値を読み込んでいる点に注目してください。

このように、Tvb オブジェクトのメソッドを使って、パケットの生バイト列からプロトコル構造に従って必要なデータを読み出すのが、Dissectorの解析処理の基本的なステップです。

TreeItem オブジェクトの詳細

Dissector関数に渡される tree 引数は TreeItem オブジェクトです。これは、Wiresharkの解析ツリー(Protocol Tree)における現在のプロトコルノード、またはその親ノードを表します。Dissectorは、解析したプロトコルフィールドをこの TreeItem オブジェクトの子ノードとして追加することで、解析結果を視覚的に構造化して表示します。

解析ツリーへのフィールド追加

TreeItem:add() メソッドは、解析ツリーに新しいノードを追加するための主要な方法です。最も一般的な使い方は、ProtoFieldオブジェクトと Tvb オブジェクトを使って、パケット中の特定のバイト範囲に対応するフィールドをツリーに追加することです。

“`lua
— サブツリーノード (myproto ツリーなど)
local subtree = …

— ProtoField オブジェクト
local pf_field = …

— tvb オブジェクト
local tvb = …

— 指定した ProtoField と tvb の特定の範囲 (offset, length) に対応するノードを追加
subtree:add(pf_field, tvb, offset, length)
“`

この形式では、Wiresharkは tvb の指定された範囲から自動的にProtoFieldで定義された型の値を読み込み、ツリーノードに表示します。

値を明示的に指定して追加

場合によっては、パケットデータから直接読み込むのではなく、計算された値やデコードされた値をツリーに表示したいことがあります。その場合は、add() メソッドに値も引数として渡すことができます。

“`lua
local calculated_value = … — 計算した値

— 値を明示的に渡してツリーに追加 (tvb, offset, length も指定可能だが、値が優先される)
subtree:add(pf_field, tvb, offset, length, calculated_value)

— 値だけを指定し、バイト範囲を指定しない (フィールドがパケット中の特定範囲に対応しない場合)
subtree:add(pf_field, calculated_value)
“`

値を明示的に渡す場合でも、tvb, offset, length を指定することで、そのツリーノードがパケット中のどのバイト範囲に対応するかをWiresharkに示すことができます。これにより、ツリーでノードを選択した際に、対応するバイト列が「バイトビュー」ペインでハイライトされるようになります。これはデバッグや解析において非常に役立ちます。

サブツリーの作成

プロトコル構造が階層的である場合(例:ヘッダと複数のサブ構造、またはメッセージリスト)、ツリー内にサブツリーを作成して表示を整理することができます。add() メソッドは、追加された新しい TreeItem オブジェクトを返すため、それに対してさらに add() を呼び出すことでサブツリーを構築できます。

“`lua
— メインのプロトコルノード
local proto_tree = tree:add(myproto, tvb, 0, tvb:len())

— ヘッダ用のサブツリーを作成
local header_subtree = proto_tree:add(“Header”, tvb, 0, header_len)
— ヘッダフィールドを header_subtree に追加
header_subtree:add(pf_field1, tvb, 0, 4)
header_subtree:add(pf_field2, tvb, 4, 2)

— ペイロード用のサブツリーを作成
local payload_subtree = proto_tree:add(“Payload”, tvb, header_len, payload_len)
— ペイロードフィールドを payload_subtree に追加
payload_subtree:add(pf_payload_data, tvb, header_len, payload_len)
“`

add() メソッドの最初の引数には、ProtoFieldオブジェクトの代わりに文字列(ツリーノードのラベルとして表示される)を指定することもできます。これは、特定のプロトコルフィールドに対応しない、構造的なノード(例:「Header」、「Items List」など)を作成する場合に便利です。

ツリーノードの表示をカスタマイズ

TreeItem:add() には様々なオーバーロードがあり、表示を細かく制御できます。

  • tree:add(proto_field, tvb, offset, length, value): 値を明示的に指定。
  • tree:add(proto_field, value): 値のみ指定(バイト範囲なし)。
  • tree:add(label_string, tvb, offset, length): ラベル文字列を指定してサブツリーを作成。
  • tree:add(label_string, value): ラベル文字列と値を指定(バイト範囲なし)。

また、TreeItem オブジェクト自体には、ノードのテキストを操作したり、追加情報を表示したりするメソッドがあります。

  • tree_item:append_text(text): ノードの既存のテキストに文字列を追加します。エラー情報などを追記するのに便利です。
  • tree_item:add_packet_field(proto_field): 指定したProtoFieldに対応する値を、このノードのテキストに追加して表示します。
  • tree_item:add_expert_info(expert_info_object): このノードにエキスパート情報(エラー、警告など)を関連付けます(後述のExpertInfo参照)。

ProtoFieldの表示オプション

ProtoFieldの定義時に指定するオプションは、TreeItem:add() でそのフィールドが表示される際の挙動に影響します。

  • abbrev (文字列): フィールドの短縮名。フィルタリングなどに使用されます (myproto.msg_type == 1など)。
  • name (文字列): 解析ツリーに表示されるフィールドのラベル。
  • type (数値): フィールドのデータ型 (ftypes.UINT8, ftypes.UINT32, ftypes.BYTES, ftypes.STRINGなど)。
  • display (数値): 数値フィールドの表示形式 (base.DEC, base.HEX, base.OCT, base.DEC_HEX, base.HEX_DECなど)。
  • strings (テーブル): 数値に対応する文字列マッピング。例えば、メッセージタイプ1を”Request”、2を”Response”と表示したい場合に使います。{ [1] = "Request", [2] = "Response" } のようなテーブルを渡します。
  • base (数値): display と同様に数値表示形式を指定できます。displaybaseはどちらかを使います。
  • mask (数値): ビットマスクを指定します。フィールド値の一部だけを抽出して表示したい場合に利用できます。
  • blurb (文字列): フィールドの短い説明。Wiresharkのステータスバーなどに表示されることがあります。

例:メッセージタイプフィールドに文字列マッピングを適用

“`lua
— ProtoField 定義時に strings マッピングを追加
local msg_type_strings = {
[1] = “Request”,
[2] = “Response”,
[3] = “Error”
}
local pf_msg_type = ProtoField.uint8(“myproto.msg_type”, “Message Type”, nil, msg_type_strings) — nil は base/display

— Dissector 関数内
— …
local msg_type = tvb:uint8(offset)
subtree:add(pf_msg_type, tvb, offset, 1) — strings マッピングは ProtoField に定義されているため、add() では値だけ渡す必要はない
offset = offset + 1
— ツリーには “Message Type: 1 (Request)” のように表示される
“`

TreeItem:add() と ProtoField を適切に使用することで、複雑なプロトコル構造も分かりやすく階層化して表示することができます。特に、ProtoField の stringsdisplay オプションを活用することで、バイナリ値を人間が理解しやすい形式で表示できます。

高度な機能とテクニック

これまでの基本を踏まえ、より実用的で高度なLua Dissector開発に必要な機能を解説します。

Port/Protocol Mapping (DissectorTable)

前述の通り、Dissector Tableは、特定のプロトコル情報(ポート番号、Ethernet Typeなど)に基づいて適切なDissectorを呼び出すためのメカニズムです。Lua Dissectorを作成したら、必ず適切なDissector Tableに登録する必要があります。

  • UDP/TCP ポート: アプリケーション層プロトコルのDissectorは、「udp.port」または「tcp.port」テーブルに登録されます。キーはポート番号です。

    lua
    local udp_table = DissectorTable.get("udp.port")
    udp_table:add(12345, myproto) -- UDPポート12345に登録
    udp_table:add(54321, myproto) -- 複数のポートに同じDissectorを登録可能
    local tcp_table = DissectorTable.get("tcp.port")
    tcp_table:add(8888, myproto) -- TCPポート8888に登録

  • Ethernet Type: EthernetフレームのPayload Typeを示すフィールドです。「ethertype」テーブルに登録します。キーはEthernet Typeの数値です(例: IPは0x0800)。

    lua
    local ethertype_table = DissectorTable.get("ethertype")
    ethertype_table:add(0xABCD, myproto) -- EtherType 0xABCD に登録

  • IP Protocol: IPパケットのProtocolフィールド(IPv4)またはNext Headerフィールド(IPv6)を示す値です。「ip.proto」テーブルに登録します。キーはプロトコル番号です(例: TCPは6, UDPは17)。

    lua
    local ip_proto_table = DissectorTable.get("ip.proto")
    ip_proto_table:add(123, myproto) -- IPプロトコル番号123に登録

  • Dissector Tableのエントリを削除: 開発中にDissectorを一時的に無効にしたい場合などに使用できます。

    lua
    udp_table:remove(12345) -- UDPポート12345の登録を削除

  • Dissector Tableのエントリをリスト: 現在テーブルに登録されているエントリを確認できます。

    lua
    local udp_table = DissectorTable.get("udp.port")
    for key, dissector in pairs(udp_table) do
    print("UDP Port:", key, "->", dissector.name)
    end

  • Dissector Tableの取得: 特定のテーブル名が分からない場合、「Dissector Tables」テーブルを使って検索できます。

    lua
    for table_name, table_obj in pairs(DissectorTables) do
    print("Available Dissector Table:", table_name)
    end

Sub-Dissectors

自身のDissectorの一部として、他のDissectorを呼び出すことができます。これは、ペイロード部分が別の既存プロトコルでエンコードされている場合などに非常に有効です。

他のDissectorを呼び出すには、Dissector.get() または Dissector Table を使って対象のDissectorオブジェクトを取得し、その call() メソッドを使用します。

“`lua
— 例: MyProtoのペイロードがXMLである場合
local xml_dissector = Dissector.get(“xml”) — “xml” Dissector を名前で取得

if xml_dissector then
— ペイロード部分に対応する新しい Tvb オブジェクトを作成
local payload_tvb = tvb:range(payload_offset, payload_len)

-- XML Dissectorを呼び出す。新しい Tvb、現在の pinfo、XML解析結果を追加するツリーノードを渡す。
xml_dissector:call(payload_tvb, pinfo, subtree) -- subtreeは MyProto のペイロードノードなど

end
“`

dissector:call() メソッドは、通常3つの引数を取ります: tvb (解析対象のバイト列), pinfo (パケット情報), tree (解析結果を追加するツリーノード)。

特定の条件(例:ヘッダ内のタイプフィールドの値)によって呼び出すサブDissectorを切り替えたい場合は、Dissector Tableの機能と組み合わせて使用します。

“`lua
— ProtoField で Dissector Table を参照するように定義
— 例: ペイロードタイプに応じて呼び出すDissectorを切り替えるテーブル
local payload_type_dissector_table = DissectorTable.new(“myproto.payload_type”, “MyProto Payload Type”)

local pf_payload_type = ProtoField.uint8(“myproto.payload_type”, “Payload Type”, nil, nil, nil, “Table \”myproto.payload_type\””) — blurb にテーブル名を示唆

— payload_type_dissector_table に各種ペイロードタイプと対応するDissectorを登録 (例: XML, JSON)
— local xml_dissector = Dissector.get(“xml”)
— if xml_dissector then
— payload_type_dissector_table:add(1, xml_dissector) — Type 1 は XML
— end
— local json_dissector = Dissector.get(“json”)
— if json_dissector then
— payload_type_dissector_table:add(2, json_dissector) — Type 2 は JSON
— end

— Dissector関数内
— … ヘッダ解析で msg_type を取得 …
local msg_type = tvb:uint8(offset_msg_type)
subtree:add(pf_msg_type, tvb, offset_msg_type, 1, msg_type)
— … ペイロード部分の tvb (payload_tvb) を作成 …

— msg_type の値に対応する Dissector をテーブルから取得して呼び出す
local payload_dissector = payload_type_dissector_table:get_dissector(msg_type)
if payload_dissector then
payload_dissector:call(payload_tvb, pinfo, subtree) — 同じ subtree に追加
else
— 対応する Dissector が見つからない場合の処理 (例: ペイロードをバイト列として表示)
subtree:add(pf_payload, payload_tvb, 0, payload_tvb:len()):append_text(” [Unknown Payload Type]”)
end
``
このパターンは、TLV (Type-Length-Value)構造などでType値によってValue部分の解釈が変わる場合に非常に強力です。ProtoField定義で
baseオプションにbase.DISSECTOR_TABLEを指定し、stringsオプションの代わりにDissector Table名を渡すことで、WiresharkのUIでフィールド値に対応するDissectorを確認・変更できるようになりますが、Lua Dissectorからプログラム的に呼び出す場合はDissectorTable:get_dissector()`を使用するのが一般的です。

Preferences (設定項目)

Lua Dissectorにユーザーが設定可能なパラメータ(例:デフォルトポート番号、デバッグ出力レベルなど)を持たせたい場合があります。Wiresharkは、Luaスクリプトが Prefs.new() を使って設定項目を定義すると、それをWiresharkの「設定」ダイアログに自動的に追加してくれます。

“`lua
— Prefs オブジェクトを作成 (プロトコル略称を渡す)
local myproto_prefs = Prefs.new(“MyProto”, myproto)

— 設定項目を定義
local pref_default_port = myproto_prefs:uint(“default_port”, “Default UDP Port”, 12345) — unsigned integer
local pref_debug_enabled = myproto_prefs:bool(“debug_enabled”, “Enable Debugging”, false) — boolean
local pref_protocol_version = myproto_prefs:enum(“protocol_version”, “Protocol Version”, 1,
{[1] = “Version 1”, [2] = “Version 2”}, “Select the protocol version”) — enumeration

— Dissector 関数内での設定値の利用
function myproto.dissector(tvb, pinfo, tree)
— 設定値を取得
local current_default_port = pref_default_port.value
local is_debug_enabled = pref_debug_enabled.value
local selected_version = pref_protocol_version.value

-- 例: デバッグが有効ならメッセージを表示
if is_debug_enabled then
    print("MyProto Dissector: Debugging enabled for frame", pinfo.number)
end

-- 例: 設定されたポートでなければ解析をスキップ (DissectorTableで既にフィルタされているが、念のため)
-- if pinfo.dest_port ~= current_default_port and pinfo.src_port ~= current_default_port then
--     return
-- end

-- 例: プロトコルバージョンによって解析ロジックを変える
if selected_version == 1 then
    -- Parse as Version 1
elseif selected_version == 2 then
    -- Parse as Version 2
end

-- ... rest of dissector logic ...

end
“`

Prefs.new(name, proto) でPrefsオブジェクトを作成し、:uint(), :int(), :bool(), :string(), :enum() などのメソッドで設定項目を定義します。これらのメソッドは、定義された設定項目に対応する内部的なオブジェクトを返します。設定値を取得するには、そのオブジェクトの .value プロパティにアクセスします。

Wiresharkの「設定」ダイアログには、「Protocols」の下にLua Dissectorの設定項目が表示され、ユーザーがGUIから値を変更できます。

Protocol States (Pinfo オブジェクト)

TCPストリームなど、複数のパケットにまたがって状態を維持する必要があるプロトコルを解析する場合、Pinfo オブジェクトが提供する proto_data フィールドを利用できます。pinfo.proto_data は、現在のプロトコルディスパッチコンテキストに紐付けられたLuaテーブルです。同じTCPストリームや同じコネクションに属する後続のパケットの解析時には、同じ pinfo.proto_data テーブルが再利用されます(ただし、新しいパケットごとに新しいPinfoオブジェクトが作成されます)。

“`lua
— Dissector 関数内
function myproto.dissector(tvb, pinfo, tree)
— pinfo.proto_data はフレームごとに初期化されるテーブル
— ストリームやコネクションベースの状態管理には pinfo.proto_data に追加のキーが必要
— 最も一般的なのは、プロトコルオブジェクト自体をキーとして使うこと
local state = pinfo.proto_data[myproto]

-- 状態が初期化されていない場合
if not state then
    state = {}
    state.message_count = 0
    state.first_timestamp = pinfo.abs_ts
    pinfo.proto_data[myproto] = state -- 状態を pinfo.proto_data に保存
end

-- パケットごとの処理で状態を更新
state.message_count = state.message_count + 1

-- 状態情報に基づいて解析を行う、または表示に含める
pinfo.cols.info:append(string.format(" (Msg #%d)", state.message_count))

-- ... rest of dissector logic ...

-- TCPストリームの再構成を扱う場合
-- WiresharkはTCPストリームを再構成してDissectorを呼び出す機能を持つ
-- その場合、pinfo.desegment_len や pinfo.desegment_offset が利用可能
-- Lua Dissectorは基本的に再構成後の完全なデータストリームに対して実行されることを想定
-- ただし、断片化されたパケットを受信した場合、tvb:len() < tvb:reported_length() のような状況が発生しうる

-- 例: 長さフィールドに基づいてデータの区切りをWiresharkに指示
-- (これはストリームベースプロトコルで重要)
-- assuming 'payload_len' is read from the header
local bytes_processed = offset + payload_len
local bytes_remaining = tvb:len() - bytes_processed

if bytes_remaining > 0 then
    -- まだデータが残っている場合、後続のデータを同じDissectorで処理させる
    -- pinfo.desegment_len = -1 は残りの全てを後続のパケットと連結する
    -- pinfo.desegment_offset = offset は連結開始位置
    pinfo.desegment_offset = bytes_processed
    pinfo.desegment_len = -1 -- All remaining data belongs to the next segment
else
    -- このパケットでメッセージが完結した場合
    pinfo.desegment_offset = pinfo.desegment_offset + bytes_processed
    pinfo.desegment_len = 0 -- Current segment finished
end

-- Desegmentation flags are crucial for Wireshark to correctly reassemble stream data
-- If a message spans multiple packets, you need to tell Wireshark how much more data is needed
-- If tvb:len() is less than the expected message length (header + payload_len),
-- you might need to set pinfo.desegment_len to the amount of *missing* data,
-- and pinfo.desegment_offset to where the current tvb ends.

-- More advanced desegmentation:
-- local expected_len = header_len + payload_len
-- if tvb:len() < expected_len then
--     -- Tell wireshark how much data is missing
--     pinfo.desegment_len = expected_len - tvb:len()
--     pinfo.desegment_offset = tvb:len() -- Next data starts after current tvb
-- else
--     -- The current tvb contains at least one full message
--     local processed_len = header_len + payload_len
--     if tvb:len() > processed_len then
--         -- There's more data in this packet, belonging to the next message
--         pinfo.desegment_len = tvb:len() - processed_len -- Pass remaining data
--         pinfo.desegment_offset = processed_len
--     else
--         -- This packet ends exactly at the end of the message
--         pinfo.desegment_len = 0 -- No remaining data for the next call from this tvb
--         pinfo.desegment_offset = tvb:len()
--     end
-- end

end
“`

pinfo.proto_data は強力ですが、使用には注意が必要です。これは特定のプロトコルレイヤおよびコネクション/ストリームに対してローカルな状態を保持します。異なるプロトコルや異なるストリームの状態は、別のエントリ(例: pinfo.proto_data[another_proto])として管理する必要があります。

pinfo.desegment_len および pinfo.desegment_offset は、Wiresharkにストリームデータのセグメント化/再構成の情報を伝えるために重要です。これは特にTCPのようなストリーム指向のプロトコルで、一つのアプリケーションメッセージが複数のTCPセグメントに分割されたり、一つのTCPセグメントに複数のアプリケーションメッセージが含まれたりする場合に必要になります。

Post-Dissectors

通常のDissectorがプロトコルスタックを上方向に解析するのに対し、ポストDissectorは全ての通常のDissectorが実行されたに呼び出されます。これは、「frame.end」Dissector Tableに登録されます。ポストDissectorは、パケット全体の解析結果に基づいて追加の処理を行いたい場合に役立ちます。

“`lua
— プロトコル定義は不要 (既存の解析結果を見るだけ)
local mypostdissector = Dissector.new(“mypostdissector”, “My Post-Dissector”)

function mypostdissector.dissector(tvb, pinfo, tree)
— この時点では、tvb はパケット全体、tree はフレームのルートノード
— pinfo には、これまでの Dissector が設定した情報(Expert Infoなど)が含まれている可能性がある

-- 例: 特定の条件を満たすパケットをマークする
-- プロトコルフィールドの値を取得するには pinfo.cinfo.columns や Field オブジェクトを使う
-- より簡単な方法は、Wiresharkの Field オブジェクトを使うこと
local myproto_msg_type_field = Field.new("myproto.msg_type") -- ProtoField の abbrev を使う

-- パケットリストのカラムに情報を追加することも可能
-- Column.add(name, column_type, field)
-- Column.add("My Status", Column.TYP_STRING, nil, "Initialized") -- 静的な文字列
-- Column.add("My Type", Column.TYP_NUMBER, myproto_msg_type_field) -- フィールド値をカラムに表示

-- 各パケットごとにこのポストDissectorが呼び出される
-- フレーム番号を取得: pinfo.number

-- 例: 特定のメッセージタイプ(例: Type=3 のエラーメッセージ)を検出したら、フレームにコメントを追加
local msg_type_value = myproto_msg_type_field() -- Fieldオブジェクトを関数として呼び出すと値を取得できる
if msg_type_value and msg_type_value == 3 then
    -- パケットリストのコメントカラムにテキストを追加
    pinfo.cols.info:append(" [MyProto Error Detected]")
    -- エキスパート情報としてエラーを記録
    pinfo.expert = Expert.new(Expert.PROTO_MALFORMED, Expert.SEVERITY_ERROR, "Detected MyProto Error Message")
end

-- 他のプロトコルのフィールド値を参照することも可能
-- local ip_src_field = Field.new("ip.src")
-- local ip_src_value = ip_src_field()
-- if ip_src_value then
--     print("Frame", pinfo.number, "from IP:", tostring(ip_src_value)) -- IPAddress オブジェクトなので文字列化が必要
-- end

-- 集計処理などを行う場合は、Luaスクリプト内でグローバルなテーブルに情報を蓄積する
-- global _G.mypost_stats = _G.mypost_stats or { error_count = 0 }
-- if msg_type_value and msg_type_value == 3 then
--     _G.mypost_stats.error_count = _G.mypost_stats.error_count + 1
--     print("Total MyProto errors:", _G.mypost_stats.error_count)
-- end

end

— frame.end テーブルに登録
DissectorTable.get(“frame.end”):add(mypostdissector)

print(“MyPostDissector loaded.”)
“`

ポストDissectorは、特定のDissector Tableに登録されるわけではないため、Dissectorオブジェクトを作成する際にDissector.new()を使用します。これは、DissectorTableに登録されるプロトコルオブジェクトとは少し異なります。DissectorTable.get("frame.end"):add(dissector_object) で登録します。

ポストDissector内では、他のDissectorが既に解析ツリーを構築し終えているため、Field.new(abbrev)() のようにFieldオブジェクトを使って任意のフィールドの値を取得できます。

Error Handling and Reporting (Expert Info)

Dissectorがパケットの構造に異常を見つけた場合(例: 長さフィールドが不正、必須フィールドが存在しないなど)、エラーや警告として報告することが重要です。Wiresharkではこれを「Expert Information」(エキスパート情報)として表示する仕組みがあります。

Lua Dissectorでは、Pinfo オブジェクトの expert フィールドに Expert オブジェクトを代入することで、エキスパート情報を報告できます。

“`lua
— Dissector 関数内
— … ヘッダ解析で payload_len を取得 …
local expected_payload_len = payload_len — from header
local actual_payload_len = tvb:len() – offset — remaining data length

if actual_payload_len < expected_payload_len then
— ペイロードが truncated されている場合
— 診断メッセージ (string) と、診断タイプ (PROTO_MALFORMED, CALL_DISSECTOR_ERRORなど), 重要度 (ERROR, WARNING, NOTE) を指定
local expert_msg = string.format(“Payload truncated: Expected %d bytes, got %d”, expected_payload_len, actual_payload_len)
— 新しい Expert オブジェクトを作成して pinfo.expert に代入
pinfo.expert = Expert.new(Expert.PROTO_MALFORMED, Expert.SEVERITY_WARN, expert_msg)
— あるいは、TreeItem にエキスパート情報を関連付ける
— local payload_tree = subtree:add(pf_payload, tvb, offset, actual_payload_len)
— payload_tree:add_expert_info(Expert.new(Expert.PROTO_MALFORMED, Expert.SEVERITY_WARN, expert_msg))

pinfo.cols.info:append(" [Payload Truncated]") -- パケットリストにも表示

-- 解析ツリーにもエラーノードを追加 (オプション)
-- subtree:add(tvb, offset, actual_payload_len):append_text(" [Payload Truncated]")

elseif actual_payload_len > expected_payload_len then
— ペイロードが長すぎる場合
local expert_msg = string.format(“Extra data after payload: Expected %d bytes, got %d”, expected_payload_len, actual_payload_len)
pinfo.expert = Expert.new(Expert.GENERIC, Expert.SEVERITY_NOTE, expert_msg) — Genericタイプ, Note重要度
pinfo.cols.info:append(” [Extra Data]”)

 -- 余分なデータ部分をツリーに表示し、エキスパート情報を関連付ける
 local extra_tvb = tvb:range(offset + expected_payload_len)
 local extra_tree = subtree:add(extra_tvb, 0, extra_tvb:len()):append_text(" [Extra Data]")
 extra_tree:add_expert_info(Expert.new(Expert.GENERIC, Expert.SEVERITY_NOTE, expert_msg))

end

— さらに、特定のバイト範囲にエラーを示すには TreeItem:add_expert_info() を使う
— 例: CRCフィールドが不正な場合
— local crc_tree_item = subtree:add(pf_crc, tvb, crc_offset, crc_len)
— if calculated_crc ~= packet_crc then
— crc_tree_item:add_expert_info(Expert.new(Expert.CHECKSUM_BAD, Expert.SEVERITY_ERROR, “CRC Mismatch”))
— pinfo.cols.info:append(” [Bad CRC]”)
— end
“`

Expert.new(etype, severity, text)Expert オブジェクトを作成します。
* etype: 診断タイプ (Expert.PROTO_MALFORMED, Expert.CHECKSUM_BAD, Expert.SEQUENCE_ERROR, Expert.GENERICなど、定義済み定数を使用)。
* severity: 重要度 (Expert.SEVERITY_ERROR, Expert.SEVERITY_WARN, Expert.SEVERITY_NOTE)。
* text: 診断メッセージとして表示される文字列。

pinfo.expert に代入すると、そのパケット全体に対するエキスパート情報として記録されます。TreeItem:add_expert_info() を使うと、解析ツリーの特定のノードにエキスパート情報を関連付けられます。Wiresharkの「エキスパート情報」ウィンドウでこれらの情報を確認できます。

Lua JIT (Just-In-Time) Compilation

WiresharkがLuaJITサポートを有効にしてビルドされている場合、Luaスクリプトの実行速度が大幅に向上する可能性があります。LuaJITは、Luaコードをオンザフライでマシンコードにコンパイルするため、特にループ処理などでパフォーマンスのボトルネックがある場合に効果的です。WiresharkのLua環境は、互換性のために標準LuaとLuaJITの両方で動作するように設計されていますが、可能な場合はLuaJITを利用すると良いでしょう。

Debugging Lua Dissectors

前述の print() 関数による出力は最も基本的なデバッグ手法ですが、いくつか追加のテクニックがあります。

  • Lua Console: Wiresharkの「ツール」→「Lua」→「Lua Console」を開くと、インタラクティブなLua環境が利用できます。ロードされているグローバル変数や関数を調べたり、簡単なコード片を実行してテストしたりできます。
  • エラーメッセージ: Luaスクリプトに構文エラーや実行時エラーがあると、Wiresharkのステータスバーに「Lua: … error」のようなメッセージが表示され、コンソールウィンドウに詳細なエラーバックトレースが出力されます。このバックトレースは、エラーが発生したファイル名と行番号を示してくれるため、問題特定の強い味方となります。
  • error() 関数: Luaの組み込み関数 error() を使うと、任意の場所でスクリプトの実行を中断し、エラーメッセージを発生させることができます。条件付きで実行を停止させたい場合に便利です。
  • 限定的なLuaデバッガ: Wiresharkには基本的なLuaデバッガ機能も組み込まれている場合がありますが、GUIデバッガのような高機能なものではなく、コマンドラインベースのことが多いです。利用可能な場合は、Wiresharkのドキュメントを参照してください。ただし、printデバッグの方が手軽でよく使われます。
  • ダンプ: パケットの特定のバイト列の内容を確認したい場合、tvb:bytes(offset, length):bytes() でバイト列を取得し、それを文字列化してprintで出力したり、解析ツリーにそのままバイト列フィールドとして追加したりするのが便利です。

効果的なデバッグのためには、エラーが発生しうる箇所に print 文をこまめに挿入し、変数の値や実行パスを確認することが重要です。

実践的なDissector開発のステップ

Lua Dissectorを実際に開発する際の一般的なステップをまとめます。

  1. プロトコル仕様の理解: まず、解析したいプロトコルの仕様書やドキュメントを徹底的に読み込み、メッセージ構造、フィールドの型、エンディアン、長さ、意味などを正確に把握します。仕様書がない場合は、実際の通信パケットをキャプチャし、バイナリデータから構造を推測するリバースエンジニアリングが必要になります。
  2. 解析対象パケットの準備: 仕様を理解したら、そのプロトコルの実際の通信パケットをキャプチャしたpcap(またはpcapng)ファイルを用意します。様々な種類のメッセージや異常なケース(パケットが短すぎる、長すぎる、フィールド値が不正など)を含むサンプルパケットがあると、開発とテストが効率的に行えます。
  3. プロトコルとフィールドの定義: Luaスクリプトの冒頭で、Proto.new() でプロトコルを定義し、仕様に従って必要なすべてのフィールドを ProtoField.new() で定義します。フィールド名、略称、型、表示形式、文字列マッピングなどを正確に設定します。
  4. Dissector関数の骨格作成: myproto.dissector = function(tvb, pinfo, tree) ... end という形でDissector関数の基本的な構造を作成します。pinfo.cols.protocol の設定や、ルートとなるサブツリーの作成を行います。
  5. ヘッダ解析の実装: パケットの先頭から順に、tvbオブジェクトのメソッド(uint8, uint16_be, stringなど)を使ってヘッダフィールドの値を読み込みます。読み込んだ値は subtree:add() を使って解析ツリーに追加します。読み込みと同時に、オフセットを適切に進めていきます。
  6. エラーチェックと報告: 各フィールドを読み込む前に、tvb:len() などを使って、読み取りに必要なバイト数が現在の tvb の範囲内に収まっているかをチェックします。範囲外アクセスの可能性がある場合は、エラーとして報告し、解析を中断します。また、フィールド値の妥当性チェック(例: 長さフィールドの値が大きすぎるなど)を行い、不正な場合はエキスパート情報を記録します。
  7. 可変長部分/ペイロードの解析: ヘッダの解析が終わったら、残りの部分(ペイロードや可変長フィールド)を処理します。ヘッダ内の長さフィールドやタイプフィールドの値に基づいて、ペイロードの範囲を特定したり、内容を解釈したりします。
  8. サブDissectorの活用: ペイロード部分が他のプロトコルである場合は、tvb:range() でその部分の Tvb オブジェクトを作成し、Dissector.get() や Dissector Table を使って対応するDissectorを取得して call() します。
  9. Dissectorの登録: 開発中のDissectorがどのような種類のトラフィックを解析すべきかに応じて、適切なDissector Table(udp.port, tcp.port, ethertype, ip.proto など)に登録します。ポート番号やプロトコルタイプをキーとして指定します。
  10. テストとデバッグ: 用意したサンプルパケットを使ってWiresharkでキャプチャファイルを開き、Dissectorが正しく動作するかを確認します。解析ツリーが仕様通りに表示されているか、フィールド値が正しいか、エラーが適切に報告されているかなどをチェックします。問題があれば、printデバッグやエラーメッセージを頼りにスクリプトを修正します。
  11. 高度な機能の追加: 必要に応じて、設定項目(Preferences)、状態管理(pinfo.proto_data)、ポストDissectorなどの高度な機能を導入します。
  12. コードの整理とドキュメンテーション: コードを整理し、可読性を高めます。コメントを追加して、特に複雑なロジックやプロトコルの特殊な部分について説明を記述します。

このステップを繰り返しながら、Dissectorの精度を高めていきます。最初から完璧なDissectorを目指すのではなく、最小限の機能から始めて、徐々に複雑な部分やエラー処理を追加していくのが効率的なアプローチです。

Lua Dissectorのパフォーマンス考慮

Lua DissectorはC Dissectorに比べて開発が容易ですが、パフォーマンスの面では一般的にC Dissectorの方が優れています。しかし、ほとんどのユースケースではLua Dissectorのパフォーマンスは十分であり、開発の容易さとのトレードオフとして許容されます。パフォーマンスがボトルネックとなる可能性がある場合は、以下の点に注意してください。

  • 効率的なTvb操作: tvb:range() はデータのコピーを作成しない効率的な方法です。不要なデータの読み込みやコピーは避けましょう。必要なバイトだけをピンポイントで読み込むようにします。
  • 不要な処理の削減: パケットの解析に必須でない処理(例: 詳細なデバッグ出力、複雑な計算など)は、パフォーマンスが問題になる場合に限定したり、設定項目(Preferences)でオン/オフを切り替えられるようにしたりすることを検討します。
  • ループ処理の最適化: 大量のデータをループ処理で解析する場合(例: 長大なリスト構造)、LuaJITが有効な環境であればパフォーマンスが改善される可能性があります。純粋なLuaのループ処理は、C言語に比べてオーバーヘッドが大きくなる傾向があります。
  • 正規表現の使用に注意: Luaのパターンマッチング機能や正規表現ライブラリ(もし使用する場合)は、複雑なパターンや大きな入力に対してパフォーマンスが高くない場合があります。固定フォーマットや単純な区切り文字に基づいた解析の方が一般的に高速です。
  • テーブルルックアップの効率: DissectorTableによるディスパッチは効率的ですが、自作のLuaテーブルで大量のルックアップを行う場合は、テーブル構造やキーの選択がパフォーマンスに影響する可能性があります。

多くの場合、Lua Dissectorのパフォーマンスは、ネットワークI/OやWireshark自体の処理(パケットキャプチャ、ファイル読み書き、UI表示など)に比べてボトルネックにならないことがほとんどです。したがって、まずは正確な解析ロジックを実装することに注力し、パフォーマンスが問題になった場合にこれらの最適化手法を検討するのが現実的です。

まとめと展望

本記事では、WiresharkのLua Dissector開発について、その基礎から実践的なテクニックまでを詳細に解説しました。

Lua Dissectorは、独自のカスタムプロトコルを解析したり、既存プロトコルの解析をカスタマイズしたりするための強力で柔軟なツールです。C言語でのDissector開発と比較して、コンパイル不要で容易に試行錯誤できる点、Lua言語の習得コストが低い点などが大きなメリットです。

Lua Dissector開発の要点:

  • Proto.new()ProtoField.new() でプロトコルとフィールドを定義する。
  • myproto.dissector 関数で実際の解析ロジックを実装する。
  • Tvb オブジェクトを使ってパケットのバイナリデータを読み込む(オフセット、長さ、エンディアンを指定)。
  • TreeItem:add() を使って解析結果を解析ツリーに構造化して表示する。
  • DissectorTable を使って、特定のプロトコル条件(ポート番号など)でDissectorが呼び出されるように登録する。
  • Dissector.get()dissector:call() で他のDissectorをサブDissectorとして呼び出す。
  • Prefs.new() でユーザー設定可能なパラメータを追加する。
  • pinfo.proto_datapinfo.desegment_len を使ってプロトコルの状態やセグメント化を管理する。
  • Expert.new()TreeItem:add_expert_info() でエラーや警告を報告する。
  • Field.new() で他のDissectorが解析したフィールドの値を取得する(特にポストDissectorで有用)。

独自のDissectorを作成する能力は、ネットワーク通信の深い理解とデバッグにおいて非常に価値があります。仕様が公開されていない独自プロトコルのリバースエンジニアリング、特定のアプリケーションの通信挙動分析、セキュリティ関連の調査など、様々なシナリオで役立ちます。

Lua Dissector開発のさらなる学習リソースとして、Wiresharkの公式WikiにあるLuaに関するドキュメントは非常に有用です。Lua APIリファレンスは、利用可能なすべてのオブジェクト、メソッド、定数について網羅的な情報を提供しています。また、Wiresharkのソースコードに含まれる標準のLua Dissectorスクリプト(plugins/luaディレクトリ以下)も、実際のコード例として参考になります。

ネットワークプロトコルは進化し続け、新しいカスタムプロトコルも日々生まれています。Lua Dissectorは、そのような変化に対応し、Wiresharkの解析能力を拡張するための強力な手段を提供してくれます。ぜひ、ご自身の解析ニーズに合わせて、独自のLua Dissector開発に挑戦してみてください。本記事が、その最初の一歩を踏み出す助けとなれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール