2 * Dissector for Lenbrook Service Discovery Protocol
4 * Copyright (c) 2024 by Martin Mayer <martin.mayer@m2-it-solutions.de>
6 * Wireshark - Network traffic analyzer
7 * By Gerald Combs <gerald@wireshark.org>
8 * Copyright 1998 Gerald Combs
10 * SPDX-License-Identifier: GPL-2.0-or-later
14 #include <epan/packet.h>
15 #include <epan/expert.h>
16 #include <wsutil/str_util.h>
18 #define LSDP_UDP_PORT 11430
19 #define LSDP_HEADER_LEN 6
20 #define LSDP_HEADER_VER 1
21 #define LSDP_MAGIC "LSDP"
23 void proto_register_lsdp(void);
24 void proto_reg_handoff_lsdp(void);
26 static dissector_handle_t lsdp_handle
;
28 static int proto_lsdp
;
31 static int hf_lsdp_header_length
;
32 static int hf_lsdp_header_magic_word
;
33 static int hf_lsdp_header_proto_version
;
35 /* Common Message Fields */
36 static int hf_lsdp_msg_length
;
37 static int hf_lsdp_msg_type
;
38 static int hf_lsdp_node_id_length
;
39 static int hf_lsdp_node_id_mac
;
40 static int hf_lsdp_node_id
;
43 static int hf_lsdp_query
;
44 static int hf_lsdp_query_count
;
45 static int hf_lsdp_query_class
;
47 /* Announce Message */
48 static int hf_lsdp_announce
;
49 static int hf_lsdp_announce_addr_length
;
50 static int hf_lsdp_announce_addr_ipv4
;
51 static int hf_lsdp_announce_addr_ipv6
;
52 static int hf_lsdp_announce_count
;
54 /* Announce Message Records */
55 static int hf_lsdp_announce_record
;
56 static int hf_lsdp_announce_record_class
;
57 static int hf_lsdp_announce_record_count
;
59 /* Announce Message Record TXT-Records */
60 static int hf_lsdp_announce_record_txt
;
61 static int hf_lsdp_announce_record_txt_key_length
;
62 static int hf_lsdp_announce_record_txt_key
;
63 static int hf_lsdp_announce_record_txt_value_length
;
64 static int hf_lsdp_announce_record_txt_value
;
67 static int hf_lsdp_delete
;
68 static int hf_lsdp_delete_count
;
69 static int hf_lsdp_delete_class
;
73 static int ett_lsdp_node_id
;
74 static int ett_lsdp_msg
;
75 static int ett_lsdp_msg_rec
;
76 static int ett_lsdp_msg_rec_txt
;
79 static expert_field ei_lsdp_unknown_msg_type
;
80 static expert_field ei_lsdp_invalid_addr_len
;
82 #define CLASS_PLAYER 0x0001
83 #define CLASS_SERVER 0x0002
84 #define CLASS_PLAYER_MZ 0x0003
85 #define CLASS_SOVI_MFG 0x0004
86 #define CLASS_SOVI_KEYPAD 0x0005
87 #define CLASS_PLAYER_SLAVE 0x0006
88 #define CLASS_REMOTE_APP 0x0007
89 #define CLASS_HUB 0x0008
90 #define CLASS_ALL 0xFFFF
92 static const value_string lsdp_class_id_vals
[] = {
93 { CLASS_PLAYER
, "BluOS Player" },
94 { CLASS_SERVER
, "BluOS Server" },
95 { CLASS_PLAYER_MZ
, "BluOS Player (secondary in multi-zone)" },
96 { CLASS_SOVI_MFG
, "sovi-mfg (used for manufacturing testing)" },
97 { CLASS_SOVI_KEYPAD
, "sovi-keypad" },
98 { CLASS_PLAYER_SLAVE
, "BluOS Player (pair slave)" },
99 { CLASS_REMOTE_APP
, "Remote Web App (AVR OSD Web Page)" },
100 { CLASS_HUB
, "BluOS Hub" },
101 { CLASS_ALL
, "All Classes (Query Message)" },
105 static const value_string lsdp_class_id_short_vals
[] = {
106 { CLASS_PLAYER
, "Player" },
107 { CLASS_SERVER
, "Server" },
108 { CLASS_PLAYER_MZ
, "Multizone Player" },
109 { CLASS_SOVI_MFG
, "sovi-mfg" },
110 { CLASS_SOVI_KEYPAD
, "sovi-keypad" },
111 { CLASS_PLAYER_SLAVE
, "Slave Player" },
112 { CLASS_REMOTE_APP
, "Web App" },
113 { CLASS_HUB
, "Hub" },
114 { CLASS_ALL
, "All Classes" },
118 #define MSG_TYPE_ANNOUNCE 0x41 // Chr. 'A'
119 #define MSG_TYPE_DELETE 0x44 // Chr. 'D'
120 #define MSG_TYPE_QUERY_BCR 0x51 // Chr. 'Q'
121 #define MSG_TYPE_QUERY_UCR 0x52 // Chr. 'R'
123 static const value_string lsdp_msg_type_vals
[] = {
124 { MSG_TYPE_ANNOUNCE
, "Announce Message" },
125 { MSG_TYPE_DELETE
, "Delete Message" },
126 { MSG_TYPE_QUERY_BCR
, "Query Message (for broadcast response)" },
127 { MSG_TYPE_QUERY_UCR
, "Query Message (for unicast response)" },
132 dissect_lsdp_node_info(tvbuff_t
*tvb
, proto_tree
*tree
, int offset
)
135 int start_offset
= offset
;
139 node_id_len
= tvb_get_uint8(tvb
, offset
);
140 proto_tree_add_item(tree
, hf_lsdp_node_id_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
144 Normally, Node-ID is the MAC address of an interface, but COULD also be an arbitrary ID.
145 Populate Node-ID field and also MAC if length is 6.
147 pi_id
= proto_tree_add_item(tree
, hf_lsdp_node_id
, tvb
, offset
, node_id_len
, ENC_NA
);
148 if(node_id_len
== 6) {
149 id_tree
= proto_item_add_subtree(pi_id
, ett_lsdp_node_id
);
150 proto_tree_add_item(id_tree
, hf_lsdp_node_id_mac
, tvb
, offset
, node_id_len
, ENC_NA
);
152 offset
+= node_id_len
;
154 return offset
- start_offset
;
158 dissect_lsdp_message(tvbuff_t
*tvb
, packet_info
*pinfo
, proto_tree
*tree
, int offset
)
160 uint8_t msg_type
, msg_len
;
161 uint32_t addr_len
, count
, txt_count
, k_len
, v_len
, class;
162 int offset_s1
, offset_s2
;
163 proto_item
*msg_item
, *rec_item
, *txt_item
;
164 proto_tree
*msg_tree
, *rec_tree
, *txt_tree
;
165 const uint8_t *key
, *val
;
167 msg_len
= tvb_get_uint8(tvb
, offset
);
168 msg_type
= tvb_get_uint8(tvb
, offset
+1);
170 col_append_str(pinfo
->cinfo
, COL_INFO
, val_to_str_const(msg_type
, lsdp_msg_type_vals
, "Unknown Message Type"));
173 case MSG_TYPE_QUERY_BCR
:
174 case MSG_TYPE_QUERY_UCR
:
176 msg_item
= proto_tree_add_item(tree
, hf_lsdp_query
, tvb
, offset
, msg_len
, ENC_NA
);
177 msg_tree
= proto_item_add_subtree(msg_item
, ett_lsdp_msg
);
179 proto_tree_add_item(msg_tree
, hf_lsdp_msg_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
182 proto_tree_add_item(msg_tree
, hf_lsdp_msg_type
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
185 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_query_count
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &count
);
188 for(uint32_t i
=0; i
< count
; i
++) {
189 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_query_class
, tvb
, offset
, 2, ENC_BIG_ENDIAN
, &class);
190 col_append_fstr(pinfo
->cinfo
, COL_INFO
, " [%s]", val_to_str_const(class, lsdp_class_id_short_vals
, "Unknown Class"));
196 case MSG_TYPE_ANNOUNCE
:
198 msg_item
= proto_tree_add_item(tree
, hf_lsdp_announce
, tvb
, offset
, msg_len
, ENC_NA
);
199 msg_tree
= proto_item_add_subtree(msg_item
, ett_lsdp_msg
);
201 proto_tree_add_item(msg_tree
, hf_lsdp_msg_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
204 proto_tree_add_item(msg_tree
, hf_lsdp_msg_type
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
207 offset
+= dissect_lsdp_node_info(tvb
, msg_tree
, offset
);
209 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_announce_addr_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &addr_len
);
213 proto_tree_add_item(msg_tree
, hf_lsdp_announce_addr_ipv4
, tvb
, offset
, addr_len
, ENC_NA
);
214 } else if (addr_len
==16) {
215 proto_tree_add_item(msg_tree
, hf_lsdp_announce_addr_ipv6
, tvb
, offset
, addr_len
, ENC_NA
);
217 expert_add_info(pinfo
, msg_tree
, &ei_lsdp_invalid_addr_len
);
221 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_announce_count
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &count
);
222 proto_item_append_text(msg_item
, " (%d Record%s)", count
, plurality(count
, "", "s"));
225 /* Loop Announce Records */
226 for(uint32_t i
=0; i
< count
; i
++) {
230 rec_item
= proto_tree_add_item(msg_tree
, hf_lsdp_announce_record
, tvb
, offset
, 0, ENC_NA
);
231 rec_tree
= proto_item_add_subtree(rec_item
, ett_lsdp_msg_rec
);
233 proto_tree_add_item_ret_uint(rec_tree
, hf_lsdp_announce_record_class
, tvb
, offset
, 2, ENC_BIG_ENDIAN
, &class);
236 col_append_fstr(pinfo
->cinfo
, COL_INFO
, " [%s]", val_to_str_const(class, lsdp_class_id_short_vals
, "Unknown Class"));
238 proto_tree_add_item_ret_uint(rec_tree
, hf_lsdp_announce_record_count
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &txt_count
);
239 proto_item_append_text(rec_item
, " (%d TXT-Record%s)", txt_count
, plurality(txt_count
, "", "s"));
242 /* Loop TXT records (key-value pairs) */
243 for(uint32_t j
=0; j
< txt_count
; j
++) {
247 txt_item
= proto_tree_add_item(rec_tree
, hf_lsdp_announce_record_txt
, tvb
, offset
, 0, ENC_NA
);
248 txt_tree
= proto_item_add_subtree(txt_item
, ett_lsdp_msg_rec_txt
);
250 proto_tree_add_item_ret_uint(txt_tree
, hf_lsdp_announce_record_txt_key_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &k_len
);
253 proto_tree_add_item_ret_string(txt_tree
, hf_lsdp_announce_record_txt_key
, tvb
, offset
, k_len
, ENC_UTF_8
, pinfo
->pool
, &key
);
256 proto_tree_add_item_ret_uint(txt_tree
, hf_lsdp_announce_record_txt_value_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &v_len
);
259 proto_tree_add_item_ret_string(txt_tree
, hf_lsdp_announce_record_txt_value
, tvb
, offset
, v_len
, ENC_UTF_8
, pinfo
->pool
, &val
);
262 proto_item_append_text(txt_item
, " (%s: %s)", key
, val
);
263 proto_item_set_len(txt_item
, offset
- offset_s2
);
265 /* Keys of interest for info column */
267 strcmp(key
, "name") == 0 ||
268 strcmp(key
, "model") == 0 ||
269 strcmp(key
, "version") == 0
271 col_append_fstr(pinfo
->cinfo
, COL_INFO
, " %s='%s'", key
, val
);
275 proto_item_set_len(rec_item
, offset
- offset_s1
);
280 case MSG_TYPE_DELETE
:
282 msg_item
= proto_tree_add_item(tree
, hf_lsdp_delete
, tvb
, offset
, msg_len
, ENC_NA
);
283 msg_tree
= proto_item_add_subtree(msg_item
, ett_lsdp_msg
);
285 proto_tree_add_item(msg_tree
, hf_lsdp_msg_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
288 proto_tree_add_item(msg_tree
, hf_lsdp_msg_type
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
291 offset
+= dissect_lsdp_node_info(tvb
, msg_tree
, offset
);
293 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_delete_count
, tvb
, offset
, 1, ENC_BIG_ENDIAN
, &count
);
296 for(uint32_t i
=0; i
< count
; i
++) {
297 proto_tree_add_item_ret_uint(msg_tree
, hf_lsdp_delete_class
, tvb
, offset
, 2, ENC_BIG_ENDIAN
, &class);
298 col_append_fstr(pinfo
->cinfo
, COL_INFO
, " [%s]", val_to_str_const(class, lsdp_class_id_short_vals
, "Unknown Class"));
305 expert_add_info(pinfo
, tree
, &ei_lsdp_unknown_msg_type
);
313 dissect_lsdp(tvbuff_t
*tvb
, packet_info
*pinfo
, proto_tree
*tree
, void *data _U_
)
317 proto_tree
*lsdp_tree
;
319 /* Basic tests for LSDP */
321 tvb_reported_length(tvb
) < LSDP_HEADER_LEN
|| // Header must be available
322 tvb_get_uint8(tvb
, 0) != LSDP_HEADER_LEN
|| // Header length must be fixed
323 tvb_memeql(tvb
, 1, LSDP_MAGIC
, strlen(LSDP_MAGIC
)) != 0 || // Magic Word must match
324 tvb_get_uint8(tvb
, 5) != LSDP_HEADER_VER
// We only support version 1
328 col_set_str(pinfo
->cinfo
, COL_PROTOCOL
, "LSDP");
329 col_clear(pinfo
->cinfo
,COL_INFO
);
331 ti
= proto_tree_add_item(tree
, proto_lsdp
, tvb
, 0, 0, ENC_NA
);
332 lsdp_tree
= proto_item_add_subtree(ti
, ett_lsdp
);
336 proto_tree_add_item(lsdp_tree
, hf_lsdp_header_length
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
339 proto_tree_add_item(lsdp_tree
, hf_lsdp_header_magic_word
, tvb
, offset
, 4, ENC_ASCII
);
342 proto_tree_add_item(lsdp_tree
, hf_lsdp_header_proto_version
, tvb
, offset
, 1, ENC_BIG_ENDIAN
);
345 /* One packet can contain multiple messages */
346 while((unsigned)offset
< tvb_reported_length(tvb
)) {
349 Ensure there are enough bytes remaining for another message
350 - at least 2 bytes (length, type) must be available
351 - length must not be zero - ensure offset to advance
354 tvb_reported_length_remaining(tvb
, offset
) < 2 ||
355 tvb_get_uint8(tvb
, offset
) == 0
359 offset
+= dissect_lsdp_message(tvb
, pinfo
, lsdp_tree
, offset
);
363 proto_item_set_len(ti
, offset
);
368 proto_register_lsdp(void)
370 static hf_register_info hf
[] = {
371 { &hf_lsdp_header_length
,
372 { "Header Length", "lsdp.header.length",
377 { &hf_lsdp_header_magic_word
,
378 { "Magic Word", "lsdp.header.magic_word",
379 FT_STRING
, BASE_NONE
,
383 { &hf_lsdp_header_proto_version
,
384 { "Protocol Version", "lsdp.header.proto_version",
389 { &hf_lsdp_msg_length
,
390 { "Message Length", "lsdp.msg.length",
396 { "Message Type", "lsdp.msg.type",
398 VALS(lsdp_msg_type_vals
), 0x0,
401 { &hf_lsdp_node_id_length
,
402 { "Node ID Length", "lsdp.node_id.length",
407 { &hf_lsdp_node_id_mac
,
408 { "Node ID (MAC)", "lsdp.node_id.mac",
414 { "Node ID", "lsdp.node_id",
420 { "Query Message", "lsdp.query",
425 { &hf_lsdp_query_count
,
426 { "Count", "lsdp.query.count",
431 { &hf_lsdp_query_class
,
432 { "Class", "lsdp.query.class",
434 VALS(lsdp_class_id_vals
), 0x0,
438 { "Announce Message", "lsdp.announce",
443 { &hf_lsdp_announce_addr_length
,
444 { "Address Length", "lsdp.announce.addr.length",
449 { &hf_lsdp_announce_addr_ipv4
,
450 { "Address", "lsdp.announce.addr_ipv4",
455 { &hf_lsdp_announce_addr_ipv6
,
456 { "Address", "lsdp.announce.addr_ipv6",
461 { &hf_lsdp_announce_count
,
462 { "Count", "lsdp.announce.count",
467 { &hf_lsdp_announce_record
,
468 { "Announce Record", "lsdp.announce.record",
473 { &hf_lsdp_announce_record_class
,
474 { "Class", "lsdp.announce.record.class",
476 VALS(lsdp_class_id_vals
), 0x0,
479 { &hf_lsdp_announce_record_count
,
480 { "Count", "lsdp.announce.record.count",
485 { &hf_lsdp_announce_record_txt
,
486 { "TXT-Record", "lsdp.announce.record.txt",
491 { &hf_lsdp_announce_record_txt_key_length
,
492 { "Key Length", "lsdp.announce.record.txt.key.length",
497 { &hf_lsdp_announce_record_txt_key
,
498 { "Key", "lsdp.announce.record.txt.key",
499 FT_STRING
, BASE_NONE
,
503 { &hf_lsdp_announce_record_txt_value_length
,
504 { "Value Length", "lsdp.announce.record.txt.val.length",
509 { &hf_lsdp_announce_record_txt_value
,
510 { "Key", "lsdp.announce.record.txt.val",
511 FT_STRING
, BASE_NONE
,
516 { "Delete Message", "lsdp.delete",
521 { &hf_lsdp_delete_count
,
522 { "Count", "lsdp.delete.count",
527 { &hf_lsdp_delete_class
,
528 { "Class", "lsdp.delete.class",
530 VALS(lsdp_class_id_vals
), 0x0,
535 static ei_register_info ei
[] = {
536 { &ei_lsdp_unknown_msg_type
,
537 { "lsdp.unknown_msg_type", PI_MALFORMED
, PI_ERROR
,
538 "Message is of invalid type", EXPFILL
}
540 { &ei_lsdp_invalid_addr_len
,
541 { "lsdp.invalid_addr_len", PI_MALFORMED
, PI_ERROR
,
542 "Address has an invalid length", EXPFILL
}
546 static int *ett
[] = {
551 &ett_lsdp_msg_rec_txt
554 expert_module_t
* expert_lsdp
;
556 proto_lsdp
= proto_register_protocol ("Lenbrook Service Discovery Protocol", "LSDP", "lsdp");
558 proto_register_field_array(proto_lsdp
, hf
, array_length(hf
));
559 proto_register_subtree_array(ett
, array_length(ett
));
561 expert_lsdp
= expert_register_protocol(proto_lsdp
);
562 expert_register_field_array(expert_lsdp
, ei
, array_length(ei
));
564 lsdp_handle
= register_dissector("lsdp", dissect_lsdp
, proto_lsdp
);
568 proto_reg_handoff_lsdp(void)
570 dissector_add_uint("udp.port", LSDP_UDP_PORT
, lsdp_handle
);
574 * Editor modelines - https://www.wireshark.org/tools/modelines.html
579 * indent-tabs-mode: nil
582 * vi: set shiftwidth=4 tabstop=8 expandtab:
583 * :indentSize=4:tabSize=8:noTabs=true: