Add SparkplugB dissector

Add a dissector for SparkplugB as a heuristic subdissector of MQTT
and which calls protobuf to dissect the messages payload.
This commit is contained in:
Graham Bloice 2021-04-10 19:15:34 +01:00
parent 2c62e2eb3f
commit f6ad4812a2
13 changed files with 414 additions and 3 deletions

View File

@ -2274,6 +2274,23 @@ add_custom_command(
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
file(GLOB _protobuf_src_files RELATIVE "${CMAKE_SOURCE_DIR}" protobuf/*.proto)
set (_protobuf_data_files)
foreach(_data_file ${_protobuf_src_files})
list(APPEND _protobuf_data_files "${DATAFILE_DIR}/${_data_file}")
endforeach()
add_custom_command(
OUTPUT ${_protobuf_data_files}
COMMAND ${CMAKE_COMMAND} -E make_directory "${DATAFILE_DIR}/protobuf"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_protobuf_src_files}"
"${DATAFILE_DIR}/protobuf"
VERBATIM
DEPENDS ${_protobuf_src_files}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
file(GLOB _profiles_src_files RELATIVE "${CMAKE_SOURCE_DIR}" profiles/*/*)
set (_profiles_data_files)
foreach(_data_file ${_profiles_src_files})
@ -2290,6 +2307,7 @@ list(APPEND copy_data_files_depends
${_dtds_data_files}
${_diameter_data_files}
${_radius_data_files}
${_protobuf_data_files}
${_profiles_data_files}
)

4
NEWS
View File

@ -86,8 +86,8 @@ Wireshark 3.5.0 Release Notes
New Protocol Support
Kerberos SPAKE, O-RAN fronthaul UC-plane (O-RAN), PDU Transport
Protocol, R09.x (R09), State Synchronization Protocol (SSyncP), and
UAVCAN\CAN
Protocol, R09.x (R09), SparkplugB, State Synchronization Protocol (SSyncP),
and UAVCAN\CAN.
Updated Protocol Support

View File

@ -88,6 +88,7 @@ They previously shipped with Npcap 1.20.
Kerberos SPAKE
O-RAN fronthaul UC-plane (O-RAN)
PDU Transport Protocol
SparkplugB
State Synchronization Protocol (SSyncP)
UAVCAN\CAN
R09.x (R09)

View File

@ -1773,6 +1773,7 @@ set(DISSECTOR_SRC
${CMAKE_CURRENT_SOURCE_DIR}/packet-someip.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-someip-sd.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-soupbintcp.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-sparkplug.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-spdy.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-spice.c
${CMAKE_CURRENT_SOURCE_DIR}/packet-spp.c

View File

@ -752,7 +752,7 @@ static gboolean mqtt_user_decode_message(proto_tree *tree, proto_tree *mqtt_tree
message_decode_entry->payload_proto_name);
proto_item_set_generated(ti);
call_dissector(message_decode_entry->payload_proto, msg_tvb, pinfo, tree);
call_dissector_with_data(message_decode_entry->payload_proto, msg_tvb, pinfo, tree, (void*)topic_str);
}
}

View File

@ -0,0 +1,169 @@
/* packet-sparkplug.c
* Routines for Sparkplug dissection
* Copyright 2021 Graham Bloice <graham.bloice<at>trihedral.com>
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "config.h"
#include <epan/packet.h>
#include <epan/expert.h>
/*
* See
*
* https://cirrus-link.com/mqtt-sparkplug-tahu/
*
* and the specification is at
*
* https://www.eclipse.org/tahu/spec/Sparkplug%20Topic%20Namespace%20and%20State%20ManagementV2.2-with%20appendix%20B%20format%20-%20Eclipse.pdf
*
*/
/* Prototypes */
/* (Required to prevent [-Wmissing-prototypes] warnings */
void proto_reg_handoff_sparkplug(void);
/* Initialize the protocol field */
static int proto_sparkplugb = -1;
/* Initialize the subtree pointers */
static gint ett_sparkplugb = -1;
static gint ett_sparkplugb_namespace = -1;
/* The handle to the protobuf dissector */
dissector_handle_t protobuf_handle = NULL;
/* The hf items */
static int hf_sparkplugb_namespace = -1;
static int hf_sparkplugb_groupid = -1;
static int hf_sparkplugb_messagetype = -1;
static int hf_sparkplugb_edgenodeid = -1;
static int hf_sparkplugb_deviceid = -1;
/* The expert info items */
static expert_field ei_sparkplugb_missing_groupid = EI_INIT;
static expert_field ei_sparkplugb_missing_messagetype = EI_INIT;
static expert_field ei_sparkplugb_missing_edgenodeid = EI_INIT;
static gboolean
dissect_sparkplugb(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
proto_item *ti;
proto_tree *sparkplugb_tree, *namespace_tree;
gchar **topic_elements, **current_element;
char *topic = (char *)data;
/* Parse the topic into the elements */
topic_elements = g_strsplit(topic, "/", 5);
/* Heuristic check that the first element of the topic is the SparkplugB namespace */
if (!topic_elements || (strcmp("spBv1.0", topic_elements[0]) != 0)) {
return FALSE;
}
/* Make entries in Protocol column */
col_set_str(pinfo->cinfo, COL_PROTOCOL, "SparkplugB");
/* Adjust the info column */
col_clear(pinfo->cinfo, COL_INFO);
col_set_str(pinfo->cinfo, COL_INFO, "SparkplugB");
/* create display subtree for the protocol */
ti = proto_tree_add_item(tree, proto_sparkplugb, tvb, 0, -1, ENC_NA);
sparkplugb_tree = proto_item_add_subtree(ti, ett_sparkplugb);
/* Add the elements parsed out from the topic string */
namespace_tree = proto_tree_add_subtree(sparkplugb_tree, tvb, 0, 0, ett_sparkplugb_namespace, &ti, "Topic Namespace");
proto_item_set_generated(ti);
current_element = topic_elements;
ti = proto_tree_add_string(namespace_tree, hf_sparkplugb_namespace, tvb, 0, 0, current_element[0]);
proto_item_set_generated(ti);
current_element += 1;
ti = proto_tree_add_string(namespace_tree, hf_sparkplugb_groupid, tvb, 0, 0, current_element[0]);
proto_item_set_generated(ti);
if (!current_element[0]) {
expert_add_info(pinfo, ti, &ei_sparkplugb_missing_groupid);
}
/* Adjust the info colum text with the message type */
current_element += 1;
if (current_element[0]) {
col_append_sep_str(pinfo->cinfo, COL_INFO, NULL, current_element[0]);
col_set_fence(pinfo->cinfo, COL_INFO);
}
ti = proto_tree_add_string(namespace_tree, hf_sparkplugb_messagetype, tvb, 0, 0, current_element[0]);
proto_item_set_generated(ti);
if (!current_element[0]) {
expert_add_info(pinfo, ti, &ei_sparkplugb_missing_messagetype);
}
current_element += 1;
ti = proto_tree_add_string(namespace_tree, hf_sparkplugb_edgenodeid, tvb, 0, 0, current_element[0]);
proto_item_set_generated(ti);
if (!current_element[0]) {
expert_add_info(pinfo, ti, &ei_sparkplugb_missing_edgenodeid);
}
/* Device ID is optional */
current_element += 1;
ti = proto_tree_add_string(namespace_tree, hf_sparkplugb_deviceid, tvb, 0, 0, current_element[0]);
proto_item_set_generated(ti);
g_strfreev(topic_elements);
/* Now handoff the Payload message to the protobuf dissector */
call_dissector_with_data(protobuf_handle, tvb, pinfo, sparkplugb_tree, "message,com.cirruslink.sparkplug.protobuf.Payload");
return TRUE;
}
void proto_register_sparkplug(void)
{
expert_module_t* expert_sparkplugb;
static gint *ett[] = {
&ett_sparkplugb,
&ett_sparkplugb_namespace
};
static hf_register_info hf[] = {
{&hf_sparkplugb_namespace, {"Namespace", "sparkplugb.namespace", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL}},
{&hf_sparkplugb_groupid, {"Group ID", "sparkplugb.groupid", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL}},
{&hf_sparkplugb_messagetype, {"Message Type", "sparkplugb.messagetype", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL}},
{&hf_sparkplugb_edgenodeid, {"Edge Node ID", "sparkplugb.edgenodeid", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL}},
{&hf_sparkplugb_deviceid, {"Device ID", "sparkplugb.deviceid", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL}},
};
static ei_register_info ei[] = {
{ &ei_sparkplugb_missing_groupid, { "sparkplugb.missing_groupid", PI_MALFORMED, PI_ERROR, "Missing Group ID", EXPFILL }},
{ &ei_sparkplugb_missing_messagetype, { "sparkplugb.missing_messagetype", PI_MALFORMED, PI_ERROR, "Missing Message Type", EXPFILL }},
{ &ei_sparkplugb_missing_edgenodeid, { "sparkplugb.missing_edgenodeid", PI_MALFORMED, PI_ERROR, "Missing Edge Node ID", EXPFILL }},
};
/* Register the protocol name and description, fields and trees */
proto_sparkplugb = proto_register_protocol("SparkplugB", "SparkplugB", "sparkplugb");
register_dissector("sparkplugb", dissect_sparkplugb, proto_sparkplugb);
proto_register_field_array(proto_sparkplugb, hf, array_length(hf));
proto_register_subtree_array(ett, array_length(ett));
expert_sparkplugb = expert_register_protocol(proto_sparkplugb);
expert_register_field_array(expert_sparkplugb, ei, array_length(ei));
}
void proto_reg_handoff_sparkplug(void)
{
protobuf_handle = find_dissector_add_dependency("protobuf", proto_sparkplugb);
/* register as heuristic dissector with MQTT */
heur_dissector_add("mqtt.topic", dissect_sparkplugb, "SparkplugB over MQTT",
"sparkplugb_mqtt", proto_sparkplugb, HEURISTIC_ENABLE);
}

View File

@ -215,6 +215,7 @@ Delete "$INSTDIR\snmp\*.*"
Delete "$INSTDIR\snmp\mibs\*.*"
Delete "$INSTDIR\styles\translations\*.*"
Delete "$INSTDIR\styles\*.*"
Delete "$INSTDIR\protobuf\*.*"
Delete "$INSTDIR\tpncp\*.*"
Delete "$INSTDIR\translations\*.*"
Delete "$INSTDIR\ui\*.*"
@ -295,6 +296,7 @@ RMDir "$INSTDIR\snmp\mibs"
RMDir "$INSTDIR\snmp"
RMDir "$INSTDIR\radius"
RMDir "$INSTDIR\dtds"
RMDir "$INSTDIR\protobuf"
RMDir "$INSTDIR\tpncp"
RMDir "$INSTDIR\translations"
RMDir "$INSTDIR\ui"

View File

@ -857,6 +857,12 @@ SetOutPath $INSTDIR
; Create the extcap directory
CreateDirectory $INSTDIR\extcap
;
; install the protobuf .proto definitions in the protobuf subdirectory
;
SetOutPath $INSTDIR\protobuf
File "${STAGING_DIR}\protobuf\sparkplug_b.proto"
; Install the TPNCP DAT file in the "tpncp" subdirectory
; of the installation directory.
SetOutPath $INSTDIR\tpncp

View File

@ -221,6 +221,20 @@
</ComponentGroup>
</Fragment>
<!-- protobuf TLV definitions -->
<Fragment>
<DirectoryRef Id="dirProtobuf">
<Component Id="cmpProtobuf_sparkplug_b_proto" Guid="*">
<File Id="filProtobuf_sparkplug_b_proto" KeyPath="yes" Source="$(var.Protobuf.Dir)\sparkplug_b.proto" />
</Component>
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="CG.Protobuf">
<ComponentRef Id="cmpProtobuf_sparkplug_b_proto" />
</ComponentGroup>
</Fragment>
<!-- TShark -->
<Fragment>
<DirectoryRef Id="INSTALLFOLDER">

View File

@ -30,6 +30,7 @@
<Directory Id="dirTpncp" Name="tpncp"/>
<Directory Id="dirTranslations" Name="translations"/>
<Directory Id="dirWimaxasncp" Name="wimaxasncp"/>
<Directory Id="dirProtobuf" Name="protobuf"/>
</Directory>
</Directory>

View File

@ -20,6 +20,7 @@
<ComponentGroupRef Id="CG.Dtds" />
<ComponentGroupRef Id="CG.Tpncp" />
<ComponentGroupRef Id="CG.Wimaxasncp" />
<ComponentGroupRef Id="CG.Protobuf" />
<ComponentGroupRef Id="CG.RequiredDependencies" />
</Feature>

View File

@ -15,6 +15,7 @@
<?define Dtds.Dir ="$(var.Staging.Dir)\dtds" ?>
<?define Tpncp.Dir ="$(var.Staging.Dir)\tpncp" ?>
<?define Wimaxasncp.Dir ="$(var.Staging.Dir)\wimaxasncp" ?>
<?define Protobuf.Dir ="$(var.Staging.Dir)\protobuf" ?>
<?define Help.Dir ="$(var.Staging.Dir)\help" ?>
<?define Epan.Lua.Dir ="..\..\epan\wslua" ?>

197
protobuf/sparkplug_b.proto Normal file
View File

@ -0,0 +1,197 @@
syntax = "proto2";
//
// To compile:
// cd client_libraries/java
// protoc --proto_path=../../ --java_out=src/main/java ../../sparkplug_b.proto
//
package com.cirruslink.sparkplug.protobuf;
option java_package = "com.cirruslink.sparkplug.protobuf";
option java_outer_classname = "SparkplugBProto";
message Payload {
/*
// Indexes of Data Types
// Unknown placeholder for future expansion.
Unknown = 0;
// Basic Types
Int8 = 1;
Int16 = 2;
Int32 = 3;
Int64 = 4;
UInt8 = 5;
UInt16 = 6;
UInt32 = 7;
UInt64 = 8;
Float = 9;
Double = 10;
Boolean = 11;
String = 12;
DateTime = 13;
Text = 14;
// Additional Metric Types
UUID = 15;
DataSet = 16;
Bytes = 17;
File = 18;
Template = 19;
// Additional PropertyValue Types
PropertySet = 20;
PropertySetList = 21;
*/
message Template {
message Parameter {
optional string name = 1;
optional uint32 type = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
ParameterValueExtension extension_value = 9;
}
message ParameterValueExtension {
extensions 1 to max;
}
}
optional string version = 1; // The version of the Template to prevent mismatches
repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value
repeated Parameter parameters = 3;
optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance
optional bool is_definition = 5;
extensions 6 to max;
}
message DataSet {
message DataSetValue {
oneof value {
uint32 int_value = 1;
uint64 long_value = 2;
float float_value = 3;
double double_value = 4;
bool boolean_value = 5;
string string_value = 6;
DataSetValueExtension extension_value = 7;
}
message DataSetValueExtension {
extensions 1 to max;
}
}
message Row {
repeated DataSetValue elements = 1;
extensions 2 to max; // For third party extensions
}
optional uint64 num_of_columns = 1;
repeated string columns = 2;
repeated uint32 types = 3;
repeated Row rows = 4;
extensions 5 to max; // For third party extensions
}
message PropertyValue {
optional uint32 type = 1;
optional bool is_null = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
PropertySet propertyset_value = 9;
PropertySetList propertysets_value = 10; // List of Property Values
PropertyValueExtension extension_value = 11;
}
message PropertyValueExtension {
extensions 1 to max;
}
}
message PropertySet {
repeated string keys = 1; // Names of the properties
repeated PropertyValue values = 2;
extensions 3 to max;
}
message PropertySetList {
repeated PropertySet propertyset = 1;
extensions 2 to max;
}
message MetaData {
// Bytes specific metadata
optional bool is_multi_part = 1;
// General metadata
optional string content_type = 2; // Content/Media type
optional uint64 size = 3; // File size, String size, Multi-part size, etc
optional uint64 seq = 4; // Sequence number for multi-part messages
// File metadata
optional string file_name = 5; // File name
optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc)
optional string md5 = 7; // md5 of data
// Catchalls and future expansion
optional string description = 8; // Could be anything such as json or xml of custom properties
extensions 9 to max;
}
message Metric {
optional string name = 1; // Metric name - should only be included on birth
optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages
optional uint64 timestamp = 3; // Timestamp associated with data acquisition time
optional uint32 datatype = 4; // DataType of the metric/tag value
optional bool is_historical = 5; // If this is historical data and should not update real time tag
optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag
optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes.
optional MetaData metadata = 8; // Metadata for the payload
optional PropertySet properties = 9;
oneof value {
uint32 int_value = 10;
uint64 long_value = 11;
float float_value = 12;
double double_value = 13;
bool boolean_value = 14;
string string_value = 15;
bytes bytes_value = 16; // Bytes, File
DataSet dataset_value = 17;
Template template_value = 18;
MetricValueExtension extension_value = 19;
}
message MetricValueExtension {
extensions 1 to max;
}
}
optional uint64 timestamp = 1; // Timestamp at message sending time
repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs
optional uint64 seq = 3; // Sequence number
optional string uuid = 4; // UUID to track message type in terms of schema definitions
optional bytes body = 5; // To optionally bypass the whole definition above
extensions 6 to max; // For third party extensions
}