iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Rust

30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊系列 第 22

Day 22: 使用 Rust 剖析 Zenoh:深入了解 Wireshark 插件 zenoh-dissector

  • 分享至 

  • xImage
  •  

使用 Rust 剖析 Zenoh:深入了解 Wireshark 插件 zenoh-dissector

Zenoh 專案的 Wireshark dissector 是現代系統程式設計的精彩範例,展現了如何用 Rust 實現安全又強大的網路分析工具,同時善用既有協定實作。本文筆者將探討 zenoh-dissector 插件的架構、實作方式與安裝流程。

什麼是 Zenoh Dissector?

zenoh-dissector 是用 Rust 編寫的 Wireshark 插件,能深入剖析 Zenoh 協定封包。它運用 Rust 的記憶體安全特性與既有的 zenoh-protocol crate,不僅易於維護,也可以高效分析 Zenoh 網路流量,無需自行重寫協定解析邏輯。

注意: 本插件需 Wireshark 4.4 版。若需舊版 Zenoh 協定(0.10.0 之前),可用 legacy Lua 插件。

安裝指南

先決條件

編譯 dissector 前需具備:

  • Rust 工具鏈:從 rustup.rs 安裝
  • Wireshark 4.4:及相關開發套件

各平台安裝方式:

Linux (Ubuntu):

sudo add-apt-repository -y ppa:wireshark-dev/stable
sudo apt update
sudo apt install -y wireshark-dev wireshark

macOS:

brew install --cask wireshark
# 編譯時需建立符號連結
ln -snf $(find /Applications/Wireshark.app/Contents/Frameworks -name "libwireshark.*.dylib" | tail -n 1) libwireshark.dylib
export WIRESHARK_LIB_DIR=$(pwd)

Windows (Chocolatey):

choco install -y wireshark

編譯與安裝

  1. 編譯插件:

    cargo build --release
    
  2. 安裝至 Wireshark:

    Linux:

    mkdir -p ~/.local/lib/wireshark/plugins/4.4/epan
    cp ./target/release/libzenoh_dissector.so ~/.local/lib/wireshark/plugins/4.4/epan/
    

    macOS:

    mkdir -p ~/.local/lib/wireshark/plugins/4.4/epan
    cp ./target/release/libzenoh_dissector.dylib ~/.local/lib/wireshark/plugins/4.4/epan/libzenoh_dissector.so
    

    Windows:

    $epan_dir = "$Env:APPDATA\Wireshark\plugins\4.4\epan"
    New-Item -ItemType Directory -Path $epan_dir -Force
    cp .\target\release\zenoh_dissector.dll $epan_dir
    

使用方法

  1. 啟動 Wireshark,並在 Help -> About Wireshark -> Plugins 驗證插件安裝情況
  2. 在網路介面啟動封包截取
  3. 透過 display filter:zenoh 篩選 Zenoh 流量
  4. dissector 會自動處理 7447/UDP 及 7447/TCP 預設埠號的流量

demo

架構設計:Rust 安全包住 C 的unsafe 邊界

此 dissector 採用典型的 Rust/C interoperability 架構,將功能分散至兩個 crate:

epan-sys:FFI 邊界

epan-sys crate 提供 Wireshark Enhanced Packet ANalyzer (EPAN) 函式庫的低階 unsafe FFI 介面,特色如下:

  • 利用 bindgen 自動由 Wireshark 的 C 標頭構建 Rust 函式簽名
  • 隔離所有 unsafe 代碼,最大限度縮小非安全範圍
  • 為安全與 Wireshark 互動打下基礎

如欲進一步瞭解 Rust/C FFI,請參見前文:Day 14: 在 Zenoh 中橋接 Rust 與 C — 第 1 部分:架構與程式碼生成

zenoh-dissector:高階安全邏輯

主 crate 包含所有安全、封包剖析邏輯:

  • 協定重用:充分利用官方 zenoh-protocolzenoh-codec crate
  • 無需重寫解析邏輯:避免重複 Zenoh 封包解析
  • 型別安全:善用 Rust 型別系統確保正確性

這種設計分離確保robustness: 將 unsafe 侷限於 FFI,其餘全部享有 Rust 的安全保證。

插件註冊:Wireshark 介面

註冊流程在 zenoh-dissector/src/lib.rs 透過可供 C 調用的導出函數完成:

版本握手

#[no_mangle]
#[used]
static plugin_want_major: std::ffi::c_int = 4;
#[no_mangle]
#[used]
static plugin_want_minor: std::ffi::c_int = 4;

註冊掛勾

plugin_register 提供兩個主要 callback:

  • register_protoinfo:註冊協定 ("Zenoh", "zenoh") 及所有顯示欄位
  • register_handoff:指派解析器指定埠號(TCP/UDP 7447)

協定狀態以 thread_local! 靜態變數 PROTOCOL_DATA 管理,全域狀態管理乾淨俐落。

巨集自動化欄位註冊

Rust 巨集基礎請見前文:Day 10: Rust Macro 熱身:從 macro_rules! 到 Derive Macro

其中的亮點是 zenoh-dissector/src/macros.rs 裡的巨集,能自動定義協定欄位:

Trait 註冊系統

impl_for_struct!impl_for_enum! 巨集自動生成兩個關鍵 trait 實作:

  1. Registration trait:定義 Wireshark 欄位如何註冊

    • generate_hf_map():建立 Wireshark 欄位註冊表
    • generate_subtree_names():產生 UI 分層欄位名
  2. AddToTree trait:定義解析資料如何顯示在 Wireshark dissection tree

    • add_to_tree():遞迴填充 UI tree 的協定資料

巨集架構深入探討

impl_for_struct! 巨集於 zenoh-dissector/src/macros.rs 具高度彈性,可處理多種欄位型態:

macro_rules! impl_for_struct {
    (
        struct $struct_name:ident {
            // 一般欄位 - 以文字呈現
            $($field_name:ident: $field_ty:ty,)*

            // 展開欄位 - 創建子樹
            $(#[dissect(expand)] $expand_name:ident: $expand_ty:ty,)*

            // 向量欄位 - 集合遍歷
            $(#[decode(vec)] $vec_name:ident: Vec<$vec_ty:ty>,)*

            // 選擇性欄位 - 條件性顯示
            $(#[dissect(option)] $skip_name:ident: Option<$option_ty:ty,)*

            // 列舉欄位 - 顯示 enum variant
            $(#[dissect(enum)] $enum_name:ident: $enum_ty:ty,)*
        }
    ) => { /* macro body */ }
}

產生的 Registration 實作:

impl Registration for $struct_name {
    fn generate_hf_map(prefix: &str) -> HeaderFieldMap {
        let mut hf_map = HeaderFieldMap::new()
        // 每個一般欄位建立文字條目
        $(
            .add(
                format!("{}.{}", prefix, stringify!{$field_name}),
                &stringify!{$field_name}.to_case(Case::Title),
                FieldKind::Text
            )
        )*
        // 向量欄位建立分支條目
        $(
            .add(
                format!("{}.{}", prefix, stringify!{$vec_name}),
                stringify!{$vec_ty},
                FieldKind::Branch
            )
        )*;

        // 遞迴擴展巢狀欄位定義
        $(
            hf_map.extend(<$vec_ty>::generate_hf_map(&format!("{prefix}.{}", stringify!{$vec_name})));
        )*

        hf_map
    }
}

產生的 AddToTree 實作:

impl AddToTree for $struct_name {
    fn add_to_tree(&self, prefix: &str, args: &TreeArgs) -> Result<()> {
        // 每個一般欄位加進 tree
        $(
            let hf_index = args.get_hf(&format!("{prefix}.{}", stringify!{$field_name}))?;
            unsafe {
                epan_sys::proto_tree_add_string(
                    args.tree,
                    hf_index,
                    args.tvb,
                    args.start as _,
                    args.length as _,
                    nul_terminated_str(&format!("{:?}", self.$field_name))?,
                );
            }
        )*

        // 向量欄位遍歷,每個元素加進 tree
        $(
            for item in NetworkMessageIter::new(self.reliability, self.$vec_name.as_slice()) {
                item.add_to_tree(&format!("{prefix}.{}", stringify!{$vec_name}), args)?;
            }
        )*

        // 展開欄位則委派給自身實作
        $(
            self.$expand_name.add_to_tree(
                &format!("{prefix}.{}", stringify!{$expand_name}),
                args
            )?;
        )*

        Ok(())
    }
}

範例

摘自 zenoh-dissector/src/zenoh_impl.rs:

impl_for_struct! {
    struct InitSyn {
        version: u8,
        whatami: WhatAmI,
        zid: ZenohId,
        resolution: Resolution,
        batch_size: BatchSize,
        ext_qos: Option<QoS>,
        ext_auth: Option<Auth>,
        // ... 更多 extension 欄位
    }
}

這樣僅用一行巨集呼叫就能自動生成約 80 行原本需手動寫的重複程式碼。

編譯期欄位產生

巨集系統在編譯期就建立 Rust struct 欄位與 Wireshark 顯示元素的映射:

  1. 欄位名稱轉換:如 batch_size 轉成 "zenoh.init.batch_size" 給 Wireshark
  2. 顯示名稱產生:如 batch_size 變成 UI 的 "Batch Size"
  3. 型態化處理:enum、option、vector 有對應處理邏輯
  4. 分層組織:巢狀結構對應 Wireshark 展開式樹狀介面

巨集編程力量

此方法帶來下列效益:

  • 零偏移:欄位定義永遠與協定同步
  • 一致性 UI:所有訊息類型相同顯示模式
  • 易擴展性:新增欄位型態只須擴充巨集模式
  • 效能佳:全部於編譯期產生,無 run-time 負擔

這種方式:

  • 保持程式 DRY 原則 (Don't Repeat Yourself)
  • 保持 dissector 與協定定義同步
  • 省去所有 Wireshark 整合 boilerplate

核心剖析邏輯

主要剖析流程於 dissect_maindissect_heur

1. 封包資料萃取

從 Wireshark 的 tvbuff_t 安全複製 raw bytes 至 Rust Vec<u8>

2. TCP 重組處理

能正確處理 TCP 流重組,檢查 can_desegment 並於訊息不完整時要求更多資料。

3. Zenoh 訊息解碼

利用 zenoh_codec::Decode trait 從 buffer 解析 TransportMessage,這是架構上的重大勝利,直接用 battle-tested 的實作。

4. UI Tree 填充

加進 tree 的方法皆用 epan-sys 函式(如 proto_tree_add_string, proto_item_add_subtree),構建 Wireshark 可展開樹狀介面。

5. 摘要字串產生

如 "7447 → 7447 [InitSyn, Frame(DATA)]" 給封包列表用的摘要說明。

從 Lua 到 Rust:重寫的理由

現在的 Rust 版本是舊 Lua 插件(v0.7.2-rc 分支)升級而來,這轉型彰顯了現代架構的優勢。

原始 Lua 作法

舊 Lua dissector (zenoh.lua, 約 1,500 行) 傳統 Wireshark plugin 方式:

手動協定解析:

function parse_zint(buf, bsize)
  local i = 0
  local val = 0
  repeat
    local tmp = buf(i, 1):uint()
    val = bit.bor(val, bit.lshift(bit.band(tmp, 0x7f), i * 7))
    i = i + 1
  until (bit.band(tmp, 0x80) == 0x00)
  return val, i
end

海量手動欄位定義:

-- 幾百行都在定義
proto_zenoh.fields.init_flags = ProtoField.uint8("zenoh.init.flags", "Flags", base.HEX)
proto_zenoh.fields.init_vmaj = ProtoField.uint8("zenoh.init.v_maj", "VMaj", base.u8)
proto_zenoh.fields.data_flags = ProtoField.uint8("zenoh.data.flags", "Flags", base.HEX)
-- ... 一百多個手動欄位定義

硬編碼訊息解讀:

function parse_msgid(tree, buf)
  local msgid = bit.band(buf(0, 1):uint(), 0x1f)
  if msgid == SESSION_MSGID.DECLARE then
    local subtree = tree:add("DECLARE (Zenoh Transport)")
    -- 手動子樹處理...
  elseif msgid == SESSION_MSGID.DATA then
    local subtree = tree:add("DATA (Zenoh Transport)")
    -- 更多手動處理...
  -- ... if-elseif 連串
end

Lua 版本的問題

  1. 協定重覆:dissector 跟協定解析各自實作,易出現維護困難與 bug
  2. 高維護成本:協定一變,欄位、解析、UI tree 都得手動調整
  3. 易誤:位元操作與手動 buffer 解析容易產生不易發現錯誤
  4. 版本漂移:dissector 很快就跟協定脫節
  5. 型別安全不足:Lua 動態型別,編譯期無法保證正確

Rust 重寫的優勢

直接重用協定、無需重寫:

// 用 zenoh-codec 解析
let messages = self.codec.read(&mut reader)?;
for message in messages {
    message.add_to_tree(tree_item);
}

巨集自動產生一致性:

impl_for_struct! {
    struct InitSyn {
        version: u8,
        whatami: WhatAmI,
        zid: ZenohId,
    }
}

自動同步:

  • 欄位定義自動跟 zenoh-protocol crate 同步
  • 解析邏輯一定保持一致
  • 新訊息型別不用額外 dissector code

記憶體安全與效能:

  • Rust 所有權系統杜絕 buffer overflow 與記憶體漏洞
  • 可用 ZSlice/ZBuf 零拷貝解析
  • 編譯期驗證協定欄位存取

實務移轉效益

面向 Lua(舊版) Rust(新版)
程式碼量 ~1,500 行 ~500 行核心邏輯
協定解析 手動重寫 官方 crate 重用
欄位定義 100+ 手動定義 巨集自動化
維護 高(需手動同步) 低(自動同步)
型別安全 執行期錯誤可能 編譯期檢查
效能 直譯執行 原生編譯

現代設計的優勢

Rust 重寫顯示良好架構能大幅提升維護效率:

  • 唯一真實來源:協定定義只在 zenoh-protocol
  • 自動一致性:巨集確保 UI 與資料結構完全對齊
  • 未來簡單可擴充:新協定功能只需最少插入
  • 優良開發經驗:編譯期錯誤及早抓到

這種從 Lua 手刻到 Rust 巨集生成,形同從「解析器即協定複製品」轉為「解析器即資料檢視器」,更可持續。

核心設計原則

此實作展現數種關鍵模式:

  • 安全至上:unsafe 隔離加安全包裝 API
  • 高效複用:充分用既有協定實作
  • 巨集魔法:例行介面程式自動化
  • 清晰架構:傳送、協定、UI 層分明

結論

zenoh-dissector 展現了現代 Rust 如何同時滿足系統程式需求與網路協定分析。透過記憶體安全、協定複用與巨集自動化,實作出高維護性與高效能的 Wireshark 插件,堪稱網路分析工具的新典範。

巨集消除重複程式碼,搭配協定 crate 重用,讓 dissector 既健壯又簡潔。這種設計值得 Rust 生態系內其他協定的解析插件採用。


上一篇
Day 21: Zenoh 無所不在:回顧 Zenoh 如何從 Rust 出發,實踐跨語言綁定 (Binding)
下一篇
Day 23: 認識 Zenoh Protocol的架構與實作
系列文
30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言