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 type | WS type | WS encoding | WS display |
---|---|---|---|
u8 to u64 | FT_UINT* | ENC_BIG_ENDIAN | BASE_DEC |
i8 to i64 | FT_INT* | ENC_BIG_ENDIAN | BASE_DEC |
Vec<u8> or [u8; _] | FT_BYTES | ENC_NA | SEP_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 Vec
s. 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 fieldFields
, a map of the fields encountered so farOffset
, the current byte offset into the packetPacket
, the raw bytes of the packetPacketNanos
, 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
orconsume_with
per field decode_with
functions must return something implementingDisplay
consume_with
functions must return(usize, T)
whereT
is anything implementingDisplay
§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§
- Fields
Store - 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.
- Protocol
Field - A data type whose fields can be registered in Wireshark and dissected. Not intended for public use.
Type Aliases§
Derive Macros§
- Dispatch
- A helper macro to generate an “index” for an enum.
- Protocol
- Marks a struct as the protocol root.
- Protocol
Field - Registers a type to be used as a field within the main
#[derive(Protocol)]
type.