Author Benoit Girard
Category Software
Tags tool, reverse-engineering, network protocols, Go, Python, data analysis, Wireshark, 2025
An introduction to Wirego, a tool for Wireshark plugin development
Context
When reversing a protocol, we usually start with a capture of the protocol obtained with tcpdump or Wireshark. If the protocol is a bit complex, we write a parser in order to analyze packets and extract what we've understood so far. Since this parser is simply a tool, we tend to use a language that we consider easy to use and offering many primitives to help us along the process (cryptographic libraries, network stacks, common format parsers etc.). The programming language will differ from person to person but this would frequently be Python, C or Go.
At some point during the reverse engineering process, and if the protocol is quite large, it can become quite hard to have a global overview of what is fully known and what needs further investigation. This is usually where one thinks about writing a dedicated Wireshark plugin. However, the Wireshark plugins API is quite complex, sometimes not properly documented and does not fit for a "quick and dirty" task.
Wirego is a Wireshark plugin that redirects Wireshark API calls through a ZMQ channel. At the other side of this channel, a package/framework/class converts these requests to a simple and user-friendly API in various programming languages.
Wireshark plugins
Wireshark was initially published in 1998 and almost immediately did offer the possibility to add new plugins. The number of network protocols was already massive at the time and offering an open development platform was a strategic move that helped its growth. Today Wireshark is still the go-to tool for visualizing capture files, but when it comes to reverse engineering very few people actually write a dedicated plugin. The main reason is that the plugin API is almost 30 years old and the associated documentation is sometimes quite incomplete. At the end of the day, reverse engineers tend to write a simple parser in Python, using the Scapy library.
Before going any further on how Wirego works, let's take a look into the Wireshark plugin API and its internals.
In order to create a new Wireshark plugin, the first step is to download and build the Wireshark source code. You will need then to create a folder for your plugin in the "plugins/epan" directory where you will place your main source code file, which will usually be "packet-mypluginname.c".
Early registration
When Wireshark starts it will load sequentially all plugins and call for each one of them a function called void proto_register_mypluginname(void) (where "mypluginname" is, as you might have guessed, the name of your plugin).
This function is where you will define all the fields that you may eventually return during packet parsing operations. Wireshark needs to set up its user interface during startup and then create all text fields, boxes, buttons and dialogs. Each registered field specifies among others the field name and the display type (plain text, decimal value, hex value...).
You need then to register your plugin to Wireshark using int proto_register_protocol(const char name, const char short_name, const char *filter_name). The "filter name" will allow you to filter the packets matching with your protocol in Wireshark.
Initialization
Once all plugins have been processed through early registration, a second pass is performed: Wireshark calls a function called void proto_reg_handoff_mypluginname(void).
In this function you will first declare a "dissector" which is a callback that will be used to parse the packets assigned to your plugin. You will then attach filters to this dissector by using a filter name and a filter value. If your protocol works on UDP port 1234, you would typically use the following:
static dissector_handle_t myplugin_handle;
myplugin_handle = create_dissector_handle(dissect_callback_, proto_myplugin);
dissector_add_uint("udp.port"", 1234, myplugin_handle);
Every time a packet matches with UDP port 1234, the dissector callback is called allowing you to analyze this packet and return some of the previously defined fields.
There is also a quite undocumented feature, allowing you to perform some heuristics on the packet and decide if it's yours or not. You need first to declare your "parent protocol" on top of which your heuristic callback will be called.
heur_dissector_add("udp", heuristic_callback, "heuristic101", "myplugin_heuristic101", proto_myplugin, HEURISTIC_ENABLE);
If your heuristic callback returns TRUE on a given payload, the packet is yours and the dissect callback is called.
Now at this point a question arises: how does Wireshark handle priorities? Can I register my plugin on top of TCP port 443 and bypass the TLS plugin?
The simple answer is "no", Wireshark is not a DPI and does not handle priorities. It appears that Wireshark has a quite linear strategy:
- internal plugins are first evaluated
- then external plugins
- then heuristic functions
At the end of the day, if you want to work on HTTP payloads for example, you will have to:
- disable the existing modules/plugins first
- or register on top of the upper layer
Packet dissection
When a pcap is loaded, all packets go through the detection system that may include static filters (such as "udp.port=1234") and heuristic functions. When a detection is performed, the associated plugin dissector function is called. Once the pcap has been loaded, dissectors for the packets currently being displayed are called a second time.
The dissector callback contains a structure holding the packet data, some information about the capture context (packet number, network stack, etc.) and the GUI tree.
You can easily fill the Wireshark "protocol" and "info" columns using the following:
col_set_str(pinfo->cinfo, COL_PROTOCOL, "My protocol");
col_set_str(pinfo->cinfo, COL_INFO, "This packet does something strange");
A plethora of functions can be used to add new items to the current dissection tree. The best way to understand how these works is to dig inside existing modules and plugins. Wireshark doesn't specify who owns the returned data, so you will never know if you are supposed to provide allocated data, data on the stack, internal pointers... Each newly added field points to one of the fields previously declared in the early registration process and simply adds the offset on the dissected packet payload and the field length.
Current plugins limitations in a protocol reverse use-case
As we've seen, the Wireshark API is quite powerful and the previous chapter is just a quick overview. You may also build dedicated settings panels, statistics graphs, work on split packets, compressed/encrypted data payloads and much more. While reversing a protocol it is quite hard to get into this API: the function names are sometimes far from obvious, documentation is quite incomplete (it's usually faster to "grep" the Wireshark code base in order to find some examples), everything is about indexes and pointers to data without a clear view on who "owns" what. First attempts to develop a plugin will probably result in many crashes, broken keyboards and a rollback to this parser written in Python.
In order to make things easier, Wireshark also proposes a LUA interface. This might be a good option if you're familiar with the language but sadly LUA doesn't necessarily provide the primitives that you need (encryption, compression, etc.). Historically Wireshark did also offer a Python API, which is now discontinued.
The Wirego architecture
When it comes to languages, many reverse-engineers tend to use Python, sometimes Go or even Rust. C is not a popular language when it comes to "quickly write code" that will never get into production.
The main principle of Wirego is quite simple:
The Wirego architecture provides a standard Wireshark plugin which converts Wireshark calls to messages sent through a ZMQ (ZeroMQ or 0MQ) channel that are received by a package/Framework/Class (depending on the language) which calls the end user code.
The following diagram shows the architecture for Python and Go:
The Wirego plugin has to be built only once, just like any classical Wireshark plugin. This plugin is written in C and implements what has previously been described. The proto_register_wirego callback attempts to connect to the remote plugin using ZMQ and makes sure everything is running. If the connection attempt fails, Wirego is disabled, otherwise the Wirego plugin retrieves the list of fields that the user plugin may return.
Into the ZMQ tunnel
As stated before, Wireshark calls are retransmitted through a ZMQ channel. ZMQ stands for Zero-MQ (https://zeromq.org), a high performance asynchronous messaging library.
This library is available for most languages (but sadly not WinDev) and proposes many types of communication patterns:
- Request/Reply where a client sends a request to a server and expects a reply
- Pub/Sub where a receiver subscribes to a "topic" and receives "Pub" sent on this "topic"
- Pipeline which acts as a FIFO channel
- Exclusive Pair which acts as a pipeline, with only two connected nodes
ZMQ messages can be sent over TCP, UDP or IPC (basically UNIX local sockets).
In addition to this, ZMQ offers many features such as advanced routing, priorities, timeouts, etc.
ZMQ does not offer any type of data serialization so it's up to the user to decide how and if data needs to be formatted before transmission.
In Wirego, we use the following:
- a Request/Reply pattern where the Wirego plugin sends requests and the Wirego remote (where the end user code resides) replies
- simple data serialization: we use multipart messages ("frames") to send each value associated to a request
- IPC mode by default (in order to avoid capturing our own traffic using Wireshark)
All requests have the following format:
Frame number | Value | Description |
---|---|---|
0 | "xxxx" | The command name as a null-terminated C string |
1 | yyy | The first argument for this command |
... | zzz | The second argument for this command |
Every request receives a reply (even if erroneous), using the following format:
Frame number | Value | Description |
---|---|---|
0 | 0/1 | One byte containing the command status (0 failure/1 success) |
1 | yyy | The first response argument for this command |
... | zzz | The second response argument for this command |
Depending on the command, the arguments for the request and response will change. A complete documentation of the protocol is available on the Wirego's repository.
Most users won't have to deal with the ZMQ layer, since this will be handled by the package/framework/class available for the language.
The languages package/framework/class
The goal of Wirego is to help reversers to implement quick and dirty parsing plugins for Wireshark, so we want to offer something simpler than a protocol over ZMQ. In order to do this, we provide packages receiving the previously described ZMQ requests and convert them back to simple API callbacks.
At the time of writing, two packages are available:
- A Go package
- A Python 3 package
This section will describe how the Python package works, but note that the Go package is extremely similar (same principles, same function names, same data types).
The python remote package is initialized with a ZMQ endpoint, a verbosity flag and a "listener" object. As stated before, the preferred ZMQ endpoint uses the "ipc" mode but the end user is allowed to use TCP or UDP if needed. Using a TCP/UDP endpoint allows the actual plugin to run on another device such as a powerful remote server, eventually with GPUs. The "listener" object is implemented by the end user and needs to inherit from the wirego.WiregoListener class.
Note: In Go, an interface is used.
The Python package declares the WiregoListener as dataclass with abstract methods.
class WiregoListener(ABC):
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_filter(self) -> str:
pass
# [...]
Once the Wirego package is properly initialized, the end used calls the "listen()" method to start listening for ZMQ commands. Every time a ZMQ message is received, the package checks the command name (in the first ZMQ frame) and extracts the associated parameters. The associated abstract method from the listener is called and the command results are sent back to the Wirego plugin using ZMQ.
The Wirego remote package implements a cache which is disabled by default. When a pcap is open, all packets are sent to the plugin for parsing. When the used scrolls on the Wireshark GUI, displayed packets are sent a second time to the plugin. This can be an opportunity to parse the packet again with more context and eventually update the parsing results. If this behavior is not needed, the cache can be enabled and the dissector will be called just once.
Let's take a quick look at the methods to be implemented on the user side when inheriting from wirego.WiregoListener:
- get_name : returns the plugin name to be displayed in Wireshark
- get_filter : returns the plugin filter, used to filter packets in Wireshark search bar to those matching our protocol
- get_fields : returns the fields description that our plugin may eventually return
- get_detection_filters : defines filters used to detect our protocol
- get_detection_heuristics_parents : when using heuristics, define the upper layer
- detection_heuristic : when using heuristic, if a packet matches the upper layer, apply the detection heuristic
- dissect_packet : parses a packet and return fields
Those methods will be explained on the next section with an example.
The end user plugin
The previous sections describe the internals of Wirego which can be useful if you want to go deeper inside Wirego or implement a package for an unsupported language (except WinDev). At the end of the day, the end user only needs to implement the seven callbacks.
Note: This section is based on the "minimal Python example" available on the Wirego's repository. Go developers will find a very similar example on the same repository.
Our Wirego plugin will first import the wirego python package and define a class inheriting the wirego.WiregoListener class:
import wirego
class WiregoMinimal(wirego.WiregoListener):
# This function shall return the plugin name
def get_name(self):
return "Wirego Minimal Example"
# [...]
Let's take a look at the seven methods to be implemented.
get_name
This first method simply returns a string defining the plugin name to be displayed in Wireshark.
# This function shall return the plugin name
def get_name(self):
return "Wirego Minimal Example"
get_filter
In the Wireshark search bar, you can type any protocol name to filter the displayed packets and show only those matching this protocol. The get_filter method defines which keyword will be used for this purpose.
# This function shall return the wireshark filter
def get_filter(self):
return "wgminexample"
get_fields
In order to set up the GUI, Wireshark needs to know every type of field you may possibly return during the dissection. Each field is defined by:
- A unique id, used to refer to this type of field during dissection
- A user readable name
- A filter, that can be used to filter packets with this field set to a given value (think about "tcp.port = 1234")
- The data type
- The display mode
# Define here enum identifiers, used to refer to a specific field
class FieldEnum(IntEnum):
FieldIdCustom1 = 0x01
FieldIdCustom2 = 0x02
FieldIdCustomWithSubFields = 0x03
# GetFields returns the list of fields descriptor that we may eventually return
# when dissecting a packet payload
def get_fields(self):
return [
wirego.WiregoField(FieldEnum.FieldIdCustom1, "Custom1", "wgminexample.custom01", wirego.ValueType.ValueTypeUInt8, wirego.DisplayMode.DisplayModeHexadecimal),
wirego.WiregoField(FieldEnum.FieldIdCustom2, "Custom2", "wgminexample.custom02", wirego.ValueType.ValueTypeUInt16, wirego.DisplayMode.DisplayModeDecimal),
wirego.WiregoField(FieldEnum.FieldIdCustomWithSubFields, "Custom With Subs", "wgminexample.custom_subs", wirego.ValueType.ValueTypeUInt32, wirego.DisplayMode.DisplayModeHexadecimal),
]
get_detection_filters
We now need to tell Wireshark how to detect our protocol. The simpler way is to provide one or more Wireshark filters.
# get_detection_filters returns a wireshark filter that will select which packets
# will be sent to your dissector for parsing.
# Two types of filters can be defined: Integers or Strings
def get_detection_filters(self):
return [
wirego.DetectionFilter(wirego.DetectionFilterType.DetectionFilterTypeInt, "udp.port", 137, ""),
wirego.DetectionFilter(wirego.DetectionFilterType.DetectionFilterTypeStr, "bluetooth.uuid", 0, "1234"),
]
In this example, our protocol can be found on UDP port 137 or on Bluetooth when UUID equals 1234.
get_detection_heuristics_parents
Sometimes, a protocol cannot simply be defined using a Wireshark filter: we need to take a look at the payload in order to decide if it's ours or not. Since Wireshark will not send every packet to our heuristic, we need to prefilter packets by defining our "upper layer".
# get_detection_heuristics_parents returns a list of protocols on top of which detection heuristic
# should be called.
def get_detection_heuristics_parents(self):
return [
"udp"
]
In this example, we define "udp" thus all unknown udp traffic will be sent to our heuristic function.
detection_heuristic
When an unknown packet is found under one of the predefined parents, a call to this method will be made. We will receive
- the packet number on the pcap
- the source address which may be an IP address or an ethernet address
- the destination address
- the stack which is a string containing all the protocols previously found (ex. "frame.eth.ethertype.ip.udp")
- the packet payload
We can then apply our detection function and return a boolean telling Wireshark if this packet is ours or not.
# detection_heuristic applies an heuristic to identify the protocol.
def detection_heuristic(self, packet_number: int, src: str, dst: str, stack: str, packet: bytes) -> bool:
#All packets starting with 0x00 should be passed to our dissector (super advanced heuristic)
if (len(packet) != 0) and (packet[0] == 0x00):
return True
return False
In this example, all packets starting with a null byte will match our protocol.
dissect_packet
This is the actual parsing function, the "dissector". The received arguments are the same defined in the detection_heuristic method.
This function returns a wirego.DissectResult object containing:
- A "Protocol" string to be shown on the Wireshark "protocol" column
- An "Info" string to be shown on the Wireshark "info" column
- A list of fields
Each field on the list is defined by:
- An enum, matching one of the enums previoudly defined in the get_fields method
- An offset (where this field has been found on the payload)
- A size
- An optional list of subfields
Wireshark allows nested fields: one field may contain other fields.
#dissect_packet provides the packet payload to be parsed.
def dissect_packet(self, packet_number: int, src: str, dst: str, stack: str, packet: bytes) -> wirego.DissectResult:
#This string will appear on the packet being parsed
protocol = "Protocol name example"
#This (optional) string will appear in the info section
info = "Info example pkt " + str(packet_number)
fields = []
#Add a few fields and refer to them using our own "internalId"
if len(packet) > 6:
fields.append(wirego.DissectField(FieldEnum.FieldIdCustom1, 0, 2, []))
fields.append(wirego.DissectField(FieldEnum.FieldIdCustom2, 2, 4, []))
#Add a field with two sub field
if len(packet) > 10:
subField1 = wirego.DissectField(FieldEnum.FieldIdCustom1, 6, 2, [])
subField2 = wirego.DissectField(FieldEnum.FieldIdCustom1, 8, 2, [])
field = wirego.DissectField(FieldEnum.FieldIdCustomWithSubFields, 6, 4, [subField1, subField2])
fields.append(field)
return wirego.DissectResult(protocol, info, fields)
In this example we return:
- A FieldIdCustom1 field at offset 0, with a size of 2
- A FieldIdCustom2 field at offset 2, with a size of 4
- A FieldIdCustomWithSubFields field at offset 6, with a size 4 containing two subfields (FieldIdCustom1 and FieldIdCustom1)
Conclusion
Wirego allows the implementation of quick and dirty Wireshark plugins in Python, Go and Rust (but not WinDev). While the complete architecture may look a bit complex, the end user will actually only need to implement 7 simple methods.
Adding a new language (Java, C#, Haskell...) is quite straightforward: reading the ZMQ protocol documentation and mimicking what has already been done for Python, Go or Rust.
Wirego is available here and precompiled Wireshark plugins can be downloaded.