A Wireshark Lua plugin that decodes USP — User Services Platform messages carried inside MQTT publish payloads, so packet captures show the full protocol structure instead of opaque protobuf bytes.
USP (specified by the Broadband Forum in TR-369) is the successor to TR-069 / CWMP for managing CPE devices. It serializes its messages as Protocol Buffers and rides over MQTT, WebSocket, STOMP, or UDS. Wireshark's stock MQTT dissector stops at the publish payload — this plugin teaches it to keep going.
Without the plugin every USP packet looks like an MQTT publish with an opaque blob:
With it loaded, packets are reclassified as USP and you get full structural decoding — for example, a controller's GET request:
…and notifications, including the inner OperationComplete / Event / ValueChange / OnBoardRequest shapes:
Indicators are also given in the packet list:
- Decodes the full USP Record wrapper (TR-369 §5.4):
NoSessionContextRecord,SessionContextRecord,MQTTConnectRecord,WebSocketConnectRecord,STOMPConnectRecord,UDSConnectRecord,DisconnectRecord
- Decodes the inner USP Msg (all 22 message types — GET / SET / ADD /
DELETE / OPERATE / NOTIFY / GET_SUPPORTED_DM / GET_INSTANCES /
GET_SUPPORTED_PROTO / REGISTER / DEREGISTER, each with its
_RESPcounterpart, plusERROR). - Decodes request/response body specifics: parameter paths and values, resolved paths, unique keys, error codes, search-expression filters, notification subtypes (Event, ValueChange, ObjectCreation/Deletion, OperationComplete, OnBoardRequest), event params, and operation arguments / output args.
- Works on plain MQTT and on MQTT-over-TLS (with a Wireshark TLS key log).
| OS | Plugin directory |
|---|---|
| Linux | ~/.local/lib/wireshark/plugins/ |
| macOS | ~/.config/wireshark/plugins/ (or ~/.local/lib/wireshark/plugins/ on Homebrew builds) |
| Windows | %APPDATA%\Wireshark\plugins\ |
Copy usp_dissector.lua into the directory, then in Wireshark do
Analyze → Reload Lua Plugins (Ctrl+Shift+L). No restart required.
To verify it loaded: Help → About Wireshark → Plugins should list
usp_dissector.lua.
Open any capture containing MQTT traffic that carries USP records and
look at the Protocol column. USP-bearing packets are now classified
as USP and the detail pane shows a USP Record (TR-369) subtree
beneath the MQTT layer.
Useful display filters:
| Filter | What it shows |
|---|---|
usp_record |
Every USP-bearing packet |
usp_msg.msg_type == 1 |
Only USP GET requests |
usp_msg.msg_type == 2 |
Only USP GET_RESP |
usp_msg.param_path contains "Device.WiFi." |
GETs that ask for WiFi paths |
usp_record.to_id == "proto::agent-1" |
Traffic addressed to a specific endpoint |
usp_msg.body_type == "Error" |
USP Error messages |
For MQTT-over-TLS captures, configure Wireshark to use a (Pre)-Master-Secret
log file (Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret
log filename), point the broker / agent at the same file via
SSLKEYLOGFILE, and TLS will be decrypted live — the dissector picks up
the decrypted MQTT payload automatically.
- MQTT transport only. WebSocket / STOMP / UDS-carried USP would each need a separate post-dissector hooked into their respective payload fields. The bytes are identical — only the carriage is different.
- No SessionContext SAR reassembly. Each
SessionContextRecordis dissected independently; the dissector does not stitch segmented payloads across multiple records. - JSON exports lose data. When exporting dissected packets to JSON
via
tshark -T json, the nestedusp_msgsubtree's repeated sub-elements share key names and collide when consumed by standard JSON parsers. For reliable downstream processing, re-parse the rawmqtt.msghex bytes against the official.protofiles. - Heuristic, not deterministic. Other binary MQTT payloads whose first byte happens to look like a protobuf tag in fields 1–16 / wire type 0 or 2 will be attempted; they fail safely (no tree output) but briefly show up as USP in the protocol column. Combining with a Content-Type filter narrows scope when needed.
- 64-bit varint precision. The pure-Lua parser uses double-precision arithmetic for varints, exact up to 2^53. Beyond that (e.g. very large session IDs) the last few bits may round. Not seen in practice.
- Built against TR-369 1.0 / 1.3 / 1.4 captures — version is reported as a string in the Record envelope and shown verbatim by the dissector.
- The protobuf parser is deliberately minimal (≈80 lines of Lua) and
message-specific decoders are explicit per message type. No code-gen,
no
.protoruntime; you can read the entire mapping inline. - Field-number / wire-type assertions are loose: unknown or out-of-schema fields are simply skipped rather than fatal-erroring, which keeps the dissector robust against minor TR-369 revisions and vendor extensions.
- TR-369 — USP (Broadband Forum)
- BroadbandForum/usp — official protobuf schemas (
proto-files/) - Wireshark Lua API reference
- Protocol Buffers wire format
- IANA registration of
application/vnd.bbf.usp.msg: media type registry
Benedikt Strobl (NSIDE ATTACK LOGIC GmbH) — developed with AI assistance
MIT — feel free to use, modify, and redistribute. No warranty.



