Introduction
Wireshark is an open source, cross platform, network protocol analyzer. It is widely used across many industried for inspecting and debugging network traffic.
Wireshark can be extended to support custom protocols by writing a dissector. A dissector can be added to the base code or created as a plug-in.
This article demonstrates how a built in dissector can be added to Wireshark to help analyze the transmission of NDI media streams.
NDI
Network Device Interface (NDI) is a royalty free software specificaton (delivered as a proprietary SDK) that enables video software to send and receive media streams over gigabit Ethernet networks.
As NDI has evolved it has used various network transports for the media streams. Version 5 introduced reliable UDP transmissions.
As part of some disagreements between NDIs creators (Newtek) and the open source community, there have been some efforts to create open source libraries that can decode NDI natively. e.g. libndi.
This source code details how NDI consists of a 12 byte header, followed by a scrambled information section followed by the compressed media frame. It also provides details of Newtek’s compressed image format, SpeedHQ.
Adding a built-in Wireshark dissector
In order to add a built-in dissector to Wireshark, it must first be built from the source code. The process is detailed online and required a number of third party components be installed on the build machine (including QT for the UI). To successfully build on OSX, the location of the QT libraries needed to be added to an environmental variable using the command export CMAKE_PREFIX_PATH=:/opt/homebrew/Cellar/qt@5/5.15.3/lib/cmake
(assuming homebrew was used to install QT).
The following script might assist in the build process.
mkdir build
cd build
cmake -G Ninja ..
ninja
sudo ../build/run/wireshark
The quickest way to develop a new dissector is to copy an existing example. For example, the folder at /plugins/epan/gryphon/
could be copied to /plugins/epan/ndi/
and packet-gryphon.c
changed to packet-ndi.c
. The CMakeLists.txt
in the root directory of the source should have its PLUGIN_SRC_DIRS
updated to include the newly added directory.
The minimum code required to create a dissector looks like this.
#include <epan/packet.h>
static int proto_ndi = -1;
static int dissect_ndi(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{
unsigned char packet_type = tvb_get_gint8 (tvb, 2);
col_set_str(pinfo->cinfo, COL_PROTOCOL, "NDI");
col_clear(pinfo->cinfo, COL_INFO);
col_add_fstr(pinfo->cinfo, COL_INFO, "Type %d", packet_type);
proto_tree_add_item(tree, proto_ndi, tvb, 0, -1, ENC_NA);
return 12;
}
void proto_register_ndi(void)
{
proto_ndi = proto_register_protocol("NDI Protocol", "NDI", "ndi");
}
void proto_reg_handoff_ndi(void)
{
dissector_handle_t ndi_handle = create_dissector_handle(dissect_ndi, proto_ndi);
dissector_add_for_decode_as_with_preference("tcp.port", ndi_handle);
}
This code performs the necessary registration of the dissect_ndi
function, which is then used to extract the byte at offset 2 in the packet (which happens to correspond to the packet type.) This `packet_type' is then added to the information column for display.
Once this is compiled it can be tested by starting Wireshark when NDI traffic is being sent and received (for example by using the NDI Test Patterns utility and the NDI Video Monitor respectively.) The Wireshark output may look similar to this.
If an NDI packet can be identified, then the context menu can allow the custom dissector to be invoked by choosing Decode As...
and then picking NDI
from the presented list.
Packets from this stream are now identified as NDI and the packet type shown in the information column.
The above screenshot, shows a middle pane that contains a tree control for various layers in the protocol. The NDI Protocol
layer shows an empty tree which could be filled with details of the protocol. libndi
reveals that the first 12 bytes of the NDI header consist of the following data.
unsigned char version;
unsigned char id;
unsigned short packet_type;
unsigned int info_len;
unsigned int data_len;
This can be added to the tree with some small additions to the dissect_ndi
and by registering the tree structures in proto_register_ndi
. Wireshark prefers an approach where the information is added to the tree by reference with the proto_tree_add_item
function. This function informs the framework about the item type, using the preregistered function, and the offset and length of the item in the packet.
static int dissect_ndi(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{
...
proto_tree *ndi_tree = proto_item_add_subtree(ti, ett_ndi);
proto_tree_add_item(ndi_tree, hf_ndi_version, tvb, 0, 1, ENC_NA);
proto_tree_add_item(ndi_tree, hf_ndi_id, tvb, 1, 1, ENC_NA);
proto_tree_add_item(ndi_tree, hf_ndi_packet_type, tvb, 2, 2, ENC_LITTLE_ENDIAN);
proto_tree_add_item(ndi_tree, hf_ndi_info_len, tvb, 4, 4, ENC_LITTLE_ENDIAN);
proto_tree_add_item(ndi_tree, hf_ndi_data_len, tvb, 8, 4, ENC_LITTLE_ENDIAN);
return 12;
}
void proto_register_ndi(void)
{
...
static hf_register_info hf[] =
{
{ &hf_ndi_version, { "Version", "ndi.version", FT_UINT8, BASE_DEC, NULL, 0x0, NULL, HFILL }},
{ &hf_ndi_id, { "ID", "ndi.id", FT_UINT8, BASE_DEC, NULL, 0x0, NULL, HFILL }},
{ &hf_ndi_packet_type, { "Packet Type", "ndi.packet_type", FT_UINT16, BASE_HEX, NULL, 0x0, NULL, HFILL }},
{ &hf_ndi_info_len, { "Info Length", "ndi.info_len", FT_UINT32, BASE_DEC, NULL, 0x0, NULL, HFILL }},
{ &hf_ndi_data_len, { "Data Length", "ndi.data_len", FT_UINT32, BASE_DEC, NULL, 0x0, NULL, HFILL }},
};
static gint *ett[] =
{
&ett_ndi
};
proto_register_field_array(proto_ndi, hf, array_length(hf));
proto_register_subtree_array(ett, array_length(ett));
}
A dissector can also be made a “heuristic dissector”, that is to say a dissector that can be invoked automatically if the packet matches certain criteria. In the NDI case, we know that the initial packet is going to be a metadata packet with an informational length of 8, and this should be enough information to determine the byte signature that can be used. A new function dissect_ndi_heur_tcp
is used when dissecting packets which is setup to return FALSE if this dissector does not understand the packet. In this case we check that the first word is 0x01800200 (a version 1 metadata packet) and the second word is 0x08000000 (a informational length of 8). This function is regostered with the heur_dissector_add
call.
static gboolean dissect_ndi_heur_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
if ((tvb_captured_length(tvb) < 12) || (tvb_get_guint32(tvb, 0, ENC_BIG_ENDIAN) != 0x01800200) || (tvb_get_guint32(tvb, 4, ENC_BIG_ENDIAN) != 0x08000000))
{
return FALSE;
}
conversation_t *conversation = find_or_create_conversation(pinfo);
conversation_set_dissector(conversation, ndi_handle);
dissect_ndi(tvb, pinfo, tree, data);
return TRUE;
}
void proto_reg_handoff_ndi(void)
{
...
heur_dissector_add("tcp", dissect_ndi_heur_tcp, "NDI over TCP", "ndi_tcp", proto_ndi, HEURISTIC_ENABLE);
}
The NDI protocol is now detected automatically but some “malformed” packets are listed in the display.
These malformed packets are a result of the way that TCP combines and splits the data stream into discrete packets. To address this, Wireshark allows the dissector to combine TCP packets together given that the total size of each NDI packet can be calculated (which is possible given the informational and data lengths).
The code in the dissect_ndi
function is moved to dissect_ndi_pdu
and the original function instead calls a Wireshark utility function, tcp_dissect_pdus
which takes care or combining and splitting TCP packets by using the provided get_ndi_pdu_len
to determine the NDI packet lengths.
static guint get_ndi_pdu_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
{
guint32 headerLen = tvb_get_gint32 (tvb, offset + 4, ENC_LITTLE_ENDIAN);
guint32 dataLen = tvb_get_gint32 (tvb, offset + 8, ENC_LITTLE_ENDIAN);
return headerLen + dataLen + NDI_FRAME_HEADER_LEN;
}
static int dissect_ndi_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{
unsigned char packet_type = tvb_get_gint8 (tvb, 2);
col_set_str(pinfo->cinfo, COL_PROTOCOL, "NDI");
col_clear(pinfo->cinfo, COL_INFO);
col_add_fstr(pinfo->cinfo, COL_INFO, "Type %d", packet_type);
proto_item *ti =proto_tree_add_item(tree, proto_ndi, tvb, 0, -1, ENC_NA);
proto_tree *ndi_tree = proto_item_add_subtree(ti, ett_ndi);
proto_tree_add_item(ndi_tree, hf_ndi_version, tvb, 0, 1, ENC_NA);
proto_tree_add_item(ndi_tree, hf_ndi_id, tvb, 1, 1, ENC_NA);
proto_tree_add_item(ndi_tree, hf_ndi_packet_type, tvb, 2, 2, ENC_LITTLE_ENDIAN);
proto_tree_add_item(ndi_tree, hf_ndi_info_len, tvb, 4, 4, ENC_LITTLE_ENDIAN);
proto_tree_add_item(ndi_tree, hf_ndi_data_len, tvb, 8, 4, ENC_LITTLE_ENDIAN);
return NDI_FRAME_HEADER_LEN;
}
static int dissect_ndi(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{
tcp_dissect_pdus(tvb, pinfo, tree, TRUE, NDI_FRAME_HEADER_LEN, get_ndi_pdu_len, dissect_ndi_pdu, data);
return tvb_reported_length(tvb);
}
To complete the NDI dissector, the information packets need to be interpreted. However, these are “scrambled” in the bitstream. Fortunately, libndi provides functions to unscramble this data.
The final code results in the following output in Wireshark.