Crate wsdf

Source
Expand description

wsdf (Wireshark Dissector Framework) is a proc-macro based framework to generate Wireshark dissectors from your Rust data types. Using wsdf, you can write dissectors in a declarative way, all from within Rust.

Examples can be found in the GitHub repo.

§Getting started

Wireshark dissector plugins are dynamic library files. Thus, wsdf is intended to be used from a Rust library crate and built as a dynamic library.

As a hello world example, the lib.rs file for a UDP dissector looks like this:

// lib.rs
wsdf::version!("0.0.1", 4, 0);

#[derive(wsdf::Protocol)]
#[wsdf(decode_from = [("ip.proto", 17)])]
struct UDP {
    src_port: u16,
    dst_port: u16,
    length: u16,
    checksum: u16,
    #[wsdf(subdissector = ("udp.port", "dst_port", "src_port"))]
    payload: Vec<u8>,
}
  • The wsdf::version! macro specifies the plugin version as 0.0.1, built for Wireshark version 4.0.X. This information is required by Wireshark when loading the plugin.
  • The protocol itself should derive wsdf::Protocol. Since this is UDP, the dissector is registered to the "ip.proto" dissector table, and also sets up the "udp.port" dissector table for subdissectors to use. More details about these annotations can be found in the sections below.

The crate type must be specified in Cargo.toml.

# Cargo.toml
[lib]
crate-type = ["cdylib"]

After running cargo build, the dissector plugin should appear in the target/debug/ folder as a shared library object (.so on Linux, .dylib on macOS, and .dll on Windows). Copying this file to Wireshark’s plugin folder will allow Wireshark or tshark to load it upon startup. On Linux, this is at ~/.local/lib/wireshark/plugins/4.0/epan/.

§Types

§Mapping

wsdf automatically maps some Rust types to Wireshark types.

Rust typeWS typeWS encodingWS display
u8 to u64FT_UINT*ENC_BIG_ENDIANBASE_DEC
i8 to i64FT_INT*ENC_BIG_ENDIANBASE_DEC
Vec<u8> or [u8; _]FT_BYTESENC_NASEP_COLON | BASE_SHOW_ASCII_PRINTABLE

§User-defined types

Each user-defined type must derive ProtocolField.

#[derive(wsdf::Protocol)]
#[wsdf(decode_from = "moldudp64.payload")]
struct MyProtocol {
    header: Header,
}

#[derive(wsdf::ProtocolField)]
struct Header {
    src_port: u16,
    dst_port: u16,
    sequence: SequenceNumber,
}

#[derive(wsdf::ProtocolField)]
struct SequenceNumber(u64);

You may use structs or enums as fields, but their contents must either be named fields or a unit tuple. Something like struct PortPair(u16, u16) cannot derive Protocol or ProtocolField.

The root type which derives Protocol must be a struct.

§Decoding enums

For enum fields, wsdf needs some help to know which variant to continue decoding the packet as. For now, the variant to use must be determined by a prior field, and the enum type must implement a method to determine the variant by returning the “index” of the selected variant. This method must be named dispatch_* by convention, where * is the field’s name.

#[derive(wsdf::ProtocolField)]
struct PacketInfo {
    typ: u8,
    #[wsdf(dispatch_field = "typ")]
    data: Data,
}

#[derive(wsdf::ProtocolField)]
enum Data {
    Foo(u8),
    Bar(u16),
    Baz,
}

impl Data {
    fn dispatch_typ(typ: &u8) -> usize {
        match *typ {
            b'F' => 0, // Foo
            b'B' => 1, // Bar
            _ => 2,    // Baz
        }
    }
}

For large enums, it may be difficult to track the “indices” of each variant. Thus, wsdf provides a Dispatch helper macro.

#[derive(wsdf::ProtocolField, wsdf::Dispatch)]
enum Data {
    Foo(u8),
    Bar(u16),
    Baz,
}

impl Data {
    fn dispatch_typ(typ: &u8) -> DataDispatch {
        use DataDispatch::*;
        match *typ {
            b'F' => Foo,
            b'B' => Bar,
            _ => Baz,
        }
    }
}

This generates a new enum named DataDispatch which implements Into<usize>, which can be directly returned from the dispatch_typ function.

§Lists

wsdf understands arrays and Vecs. You would use a Vec if the number of elements is unknown at compile time, but provided by another field in the protocol.

#[derive(wsdf::Protocol)]
#[wsdf(decode_from = "udp.port")]
struct MoldUDP64 {
    session: [u8; 10],
    sequence: u64,
    message_count: u16,
    #[wsdf(len_field = "message_count")]
    messages: Vec<MessageBlock>,
}

§Taps and custom displays

wsdf features a tap attribute which allows you to register some function(s) to be called whenever the field is decoded. These functions follow the Axum style magic function parameter passing approach. Each function just needs to declare their parameters based on whatever they are interested in.

The possible parameter types are:

  • Field, the value of the field
  • Fields, a map of the fields encountered so far
  • Offset, the current byte offset into the packet
  • Packet, the raw bytes of the packet
  • PacketNanos, the nanosecond timestamp at which the packet was recorded

Any permutation of the parameters is supported.

use wsdf::tap::{Field, PacketNanos};

#[derive(wsdf::ProtocolField)]
struct IpAddr (
    #[wsdf(tap = ["log_ts", "check_loopback", "slow_down"])]
    [u8; 4],
);

fn log_ts(PacketNanos(ts): PacketNanos) {
    eprintln!("received a packet at {ts} nanoseconds");
}
fn check_loopback(Field(addr): Field<&[u8]>) {
    if addr == &[127, 0, 0, 1] {
        eprintln!("is loopback address");
    }
}
fn slow_down() {
    std::thread::sleep(std::time::Duration::from_millis(100));
}

In this example, wsdf will invoke log_ts, check_loopback, and slow_down, in that order, when it encounters the field. Each function passed to the tap attribute must return ().

§Using Fields

Fields can be marked for saving via the #[wsdf(save)] attribute. You can then access their values through the Fields parameter, which holds a key value store. The key to each field is the Wireshark filter for that field, automatically generated by wsdf. You can double check the filter for each field in Wireshark under View > Internals > Supported Protocols.

use wsdf::tap::Fields;

#[derive(wsdf::Protocol)]
#[wsdf(decode_from = "moldudp64.payload")]
struct MarketByPrice {
    nanos: u64,
    #[wsdf(save)]
    num_updates: u8,
    #[wsdf(len_field = "num_updates")]
    updates: Vec<PriceUpdate>,
}

#[derive(wsdf::ProtocolField)]
struct PriceUpdate {
    side: u8,
    #[wsdf(save)]
    price: i32,
    #[wsdf(save, tap = "peek")]
    quantity: u64,
}

fn peek(Fields(fields): Fields) {
    // `nanos` is an Option<&u64>, but it is not saved, so it should be `None`
    let nanos = fields.get_u64("market_by_price.nanos");
    assert_eq!(nanos, None);

    // `num_updates` is an Option<&u8>, and it is saved, it should be a `Some`
    let num_updates = fields.get_u8("market_by_price.num_updates");
    assert!(matches!(num_updates, Some(_)));

    // `prices` is a `&[i32]`.
    let prices = fields.get_i32_multi("market_by_price.updates.price");
    // Do something with the values...
}

§Custom displays

By default, wsdf does not perform any additional formatting on fields. All formatting and display is handled by Wireshark. However, you may wish to customize the way some fields appear in the UI. wsdf enables this via the decode_with and consume_with attributes, which are similar to taps. Their main differences from taps are

  • You can only have one decode_with or consume_with per field
  • decode_with functions must return something implementing Display
  • consume_with functions must return (usize, T) where T is anything implementing Display

§decode_with

You may use decode_with to customize how a field appears in Wireshark’s UI.

use wsdf::tap::Field;

#[derive(wsdf::ProtocolField)]
struct Order {
    #[wsdf(decode_with = "decode_side")]
    side: [u8; 1],
    price: i32,
    quantity: u64,
}

fn decode_side(Field(side): Field<&[u8]>) -> &'static str {
    match side[0] {
        b'B' => "Bid",
        b'A' => "Ask",
        _ => "Unknown",
    }
}

By default, the side field will appear as an ascii byte string in the UI (B, A). The decode_side function takes the value of side and returns a more user friendly display.

In this example, our decode_side function returned a &'static str. But it can be anything which implements Display, so String, Box<dyn Display>, etc. are all okay.

§consume_with

The consume_with attribute is intended for bytes in the network where the size is not known beforehand. This may appear for fields which use TLV style encoding. You can see an example of this in the sample DNS dissector. The function must return the number of bytes consumed for the field, as well as how to display it in Wireshark.

use wsdf::tap::{Offset, Packet};

#[derive(wsdf::ProtocolField)]
struct MyProto {
    #[wsdf(consume_with = "consume_bytes")]
    xs: Vec<u8>,
}

fn consume_bytes(Offset(offset): Offset, Packet(pkt): Packet) -> (usize, String) {
    // Use the combination of the current offset and raw bytes from
    // `Packet` to manually parse these bytes.
    unimplemented!()
}

§Calling subdissectors

For lower level protocols, you would want to hand the packet’s payload to a subdissector. There are two ways to achieve this - using a “Decode As” subdissector or using a regular dissector table.

The first variant can be seen in the MoldUDP64 dissector.

#[derive(wsdf::ProtocolField)]
struct MessageBlock {
    message_length: u16,
    #[wsdf(len_field = "message_length", subdissector = "moldudp64.payload")]
    message_data: Vec<u8>,
}

Whichever dissector has been registered to the pattern "moldudp64.payload" will be invoked with the payload bytes.

The second variant can be seen in the UDP dissector.

#[derive(wsdf::Protocol)]
#[wsdf(decode_from = [("ip.proto", 17)])]
struct UDP {
    src_port: u16,
    dst_port: u16,
    length: u16,
    checksum: u16,
    #[wsdf(subdissector = ("udp.port", "src_port", "dst_port"))]
    payload: Vec<u8>,
}

Here, the "udp.port" dissector table is set up. To decode the payload, wsdf will first try to find a subdissector registered to the "udp.port" table interested in the value of the source port. If no subdissector is found, wsdf tries again with the destination port. And if that fails, Wireshark’s default data dissector is invoked.

§Attributes

Attributes are used to customize fields or provide additional information. They can appear on the protocol root, user-defined types, enum variants, and individual fields.

§Protocol attributes

  • #[wsdf(decode_from = ["foo.payload", ("foo.port", 30000, ...)])]

Specifies the dissector table(s) to register the dissector with. Each value is one of

A single string, e.g. "moldudp64.payload" uses the “Decode As” table.

A tuple like ("udp.port", 30000, 30001) registers the dissector to be used for UDP port values 30000 and 30001.

  • #[wsdf(proto_desc = "...")]

Full protocol description. This is used in the packet list pane.

  • #[wsdf(proto_name = "...")]

Short protocol name. This is used in the packet details pane.

  • #[wsdf(proto_filter = "...")]

Protocol name used in the display filter.

§Type-level attributes

These attributes can appear on any type which derives Protocol or ProtocolField.

  • #[wsdf(pre_dissect = "...")]
  • #[wsdf(pre_dissect = ["...", ...])]

Provide path(s) to function(s) to call before the first field of the type is dissected. The functions’ parameters follow the same rules as taps.

  • #[wsdf(post_dissect = "...")]
  • #[wsdf(post_dissect = ["...", ...])]

Provide path(s) to function(s) to call after the last field of the type is dissected. The functions’ parameters follow the same rules as taps.

§Variant attributes

  • #[wsdf(rename = "...")]

Custom name for the variant when displayed in Wireshark. See the sample DNS dissector for examples.

§Field attributes

  • #[wsdf(rename = "...")]

Custom name for the field when displayed in Wireshark.

  • #[wsdf(hide)]

Hide the field, so it is not displayed in Wireshark.

  • #[wsdf(save)]

Mark a field to be saved, such that it becomes accessible from the Fields parameter. See the section on Using Fields for more information.

  • #[wsdf(len_field = "...")]

Intended for fields of type Vec<_>. Must point to a prior integer field which specifies the number of elements for the field.

  • #[wsdf(typ = "...")]

Specifies a Wireshark type to map the field to. Sensible mappings are chosen for most types, e.g. FT_UINT8 for u8, FT_BYTES for [u8; _]. However, a specific type can be selected this way. The full list of field types can be found in Wireshark’s README.dissector file.

  • #[wsdf(enc = "...")]

Specifies an encoding for the field, e.g. ENC_LITTLE_ENDIAN. By default, all integer fields are encoded as big endian. The full list of encodings and where they are applicable can be found in Wireshark’s README.dissector file.

  • #[wsdf(display = "...")]
  • #[wsdf(display = "..." | "...")]

Specifies a Wireshark display hint, e.g. BASE_HEX. The full list of possible values can be found in Wireshark’s README.dissector file.

Note that this attribute permits a “bitwise-or” syntax to emulate the C API, e.g. you may use #[wsdf(display = "SEP_COLON" | "BASE_SHOW_ASCII_PRINTABLE")] to mean “try to decode the bytes as ascii characters, failing which, show them as regular octets separated by a colon”.

  • #[wsdf(dispatch_field = "...")]

For enum fields, specifies a previous field which is used to determine the variant. The enum type must implement a corresponding method to receive this field and return an integer representing the variant (the first is 0, the next is 1, etc.). See the section on Decoding enums for more information.

  • #[wsdf(tap = "...")]
  • #[wsdf(tap = ["...", ...])]

Specifies the path to function(s) to inspect the packet. See the section on Taps for more information.

  • #[wsdf(decode_with = "...")]

Specifies the path to a function which takes the field’s value as an argument and returns how to display that field in Wireshark. Used to customize how fields are shown in the UI. See the section on Custom displays for details.

  • #[wsdf(consume_with = "...")]

Specifies the path to a function which takes a slice of the entire packet and an offset, and returns the number of bytes consumed and how to display the field. This is used for fields with funky encoding schemes where you are unable to know its size beforehand.

An example of this can be seen in the sample DNS dissector. See the section on Custom displays for details.

  • #[wsdf(subdissector = "foo.payload")]
  • #[wsdf(subdissector = ("foo.port", "dst_port", "src_port", ...))]

Specify subdissectors to try for payloads, i.e. it is meant for fields of type Vec<u8> only. You can use two variants.

The first variant, with a single string, tries a “Decode As” subdissector. You must configure this via the “Decode As” menu or the decode_as_entries configuration file. This is used in the MoldUDP64 example.

The second variant sets up a regular dissector table named by the first value ("foo.port" in the example above). Each field listed afterwards is used to try and find a subdissector registered to the table and field’s value, one by one, until the first success. This is used in the UDP example.

Re-exports§

pub use epan_sys;

Modules§

tap
Helper types to work with taps, inspired by Axum’s magic functions.

Macros§

version
Declares the plugin version and supported Wireshark version.

Structs§

FieldsStore
A key-value store of fields saved. Each type is kept in its own multimap.

Traits§

Protocol
A data type which represents the root of the protocol. Not intended for public use.
ProtocolField
A data type whose fields can be registered in Wireshark and dissected. Not intended for public use.

Type Aliases§

FieldsMap

Derive Macros§

Dispatch
A helper macro to generate an “index” for an enum.
Protocol
Marks a struct as the protocol root.
ProtocolField
Registers a type to be used as a field within the main #[derive(Protocol)] type.