Wireshark dissector for Pro DJ Link protocol
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

add device name to status packet dissection

Stella 4d881085 61a29281

+234 -94
+234 -94
pro_dj_link.lua
··· 11 11 local f_device_name = ProtoField.string("pdjl.device_name", "Device Name") 12 12 local f_dev_num = ProtoField.uint8("pdjl.device_number", "Device Number", base.DEC) 13 13 14 - -- ============================================================================ 15 - -- PORT 50000 (Management) FIELDS 16 - -- ============================================================================ 14 + -- Port 50000 (Management) 17 15 local f_mac = ProtoField.ether("pdjl.mac", "MAC Address") 18 16 local f_ip = ProtoField.ipv4("pdjl.ip", "IP Address") 19 17 local f_sequence = ProtoField.uint8("pdjl.sequence", "Sequence", base.DEC) 20 18 21 - -- ============================================================================ 22 - -- PORT 50001 (Beat Sync) FIELDS 23 - -- ============================================================================ 19 + -- Port 50001 (Beat Sync) 24 20 local f_bpm_raw = ProtoField.uint16("pdjl.bpm_raw", "BPM (Raw x100)", base.DEC) 25 21 local f_pitch = ProtoField.uint32("pdjl.pitch", "Pitch (Raw)", base.HEX) 26 22 local f_beat_in_bar = ProtoField.uint8("pdjl.beat_in_bar", "Beat in Bar", base.DEC) ··· 29 25 local f_playhead = ProtoField.uint32("pdjl.playhead", "Playhead (ms)", base.DEC) 30 26 local f_sync_cmd = ProtoField.uint8("pdjl.sync_cmd", "Sync Command", base.HEX) 31 27 32 - -- ============================================================================ 33 - -- PORT 50002 (Status) FIELDS 34 - -- ============================================================================ 28 + -- Port 50002 (Status) 35 29 local f_rb_id = ProtoField.uint32("pdjl.rekordbox_id", "Rekordbox ID", base.DEC) 36 30 local f_track_num = ProtoField.uint16("pdjl.track_number", "Track Number", base.DEC) 37 31 local f_track_dev = ProtoField.uint8("pdjl.track_device", "Track Host Device", base.DEC) ··· 40 34 local f_firmware = ProtoField.string("pdjl.firmware", "Firmware") 41 35 local f_beat_count = ProtoField.uint32("pdjl.beat_count", "Beat Count", base.DEC) 42 36 43 - -- Bitfields for Status Flags (offset 0x89) 44 37 local f_flags = ProtoField.uint8("pdjl.flags", "Status Flags", base.HEX) 45 38 local f_flag_playing= ProtoField.bool("pdjl.flags.playing", "Is Playing", 8, nil, 0x40) 46 39 local f_flag_master = ProtoField.bool("pdjl.flags.master", "Is Master", 8, nil, 0x20) 47 40 local f_flag_sync = ProtoField.bool("pdjl.flags.sync", "Is Syncing", 8, nil, 0x10) 48 41 local f_flag_onair = ProtoField.bool("pdjl.flags.onair", "Is On Air", 8, nil, 0x08) 49 42 50 - -- ============================================================================ 51 - -- TCP DB SERVER FIELDS 52 - -- ============================================================================ 43 + -- TCP DB Server 53 44 local f_db_magic = ProtoField.uint32("pdjl.db.magic", "DB Magic", base.HEX) 54 45 local f_db_tx_id = ProtoField.uint32("pdjl.db.tx_id", "Transaction ID", base.HEX) 55 46 local f_db_msg_type = ProtoField.uint16("pdjl.db.msg_type", "DB Message Type", base.HEX) ··· 62 53 local f_db_field_str= ProtoField.string("pdjl.db.field.str", "Field (String)") 63 54 local f_db_field_bin= ProtoField.bytes("pdjl.db.field.bin", "Field (Blob)") 64 55 65 - -- Port discovery response 56 + -- DMST decoding 57 + local f_dmst = ProtoField.uint32("pdjl.dmst", "DMST Parameter", base.HEX) 58 + local f_dmst_dev = ProtoField.uint8("pdjl.dmst.device", "Device", base.DEC) 59 + local f_dmst_menu = ProtoField.uint8("pdjl.dmst.menu", "Menu", base.HEX) 60 + local f_dmst_slot = ProtoField.uint8("pdjl.dmst.slot", "Slot", base.HEX) 61 + local f_dmst_type = ProtoField.uint8("pdjl.dmst.type", "Type", base.HEX) 62 + 66 63 local f_db_port = ProtoField.uint16("pdjl.db_port", "DB Server Port", base.DEC) 64 + local f_raw_payload = ProtoField.bytes("pdjl.raw_payload", "Raw Payload") 67 65 68 66 p_djl.fields = { 69 67 f_header, f_type, f_device_name, f_dev_num, ··· 73 71 f_flags, f_flag_playing, f_flag_master, f_flag_sync, f_flag_onair, 74 72 f_db_magic, f_db_tx_id, f_db_msg_type, f_db_arg_count, f_db_tag_blob, 75 73 f_db_field_u8, f_db_field_u16, f_db_field_u32, f_db_field_str, f_db_field_bin, 76 - f_db_port 74 + f_dmst, f_dmst_dev, f_dmst_menu, f_dmst_slot, f_dmst_type, 75 + f_db_port, f_raw_payload 77 76 } 78 77 79 78 -- ============================================================================ 80 - -- ENUMS 79 + -- HELPERS 81 80 -- ============================================================================ 82 81 local PACKET_TYPES = { 83 82 [0x00] = "Channel Claim 1", ··· 92 91 } 93 92 94 93 local DB_MSG_TYPES = { 95 - [0x0000] = "QueryContextSetup", 96 - [0x4000] = "QueryContextSuccess", 94 + [0x0000] = "GetQueryContext", 95 + [0x4000] = "Success/Ack", 97 96 [0x1001] = "Disconnect", 98 - [0x2002] = "MetadataReq", 99 - [0x3000] = "RenderReq", 97 + [0x2002] = "GetMetadata", 98 + [0x3000] = "RenderMenu", 100 99 [0x4001] = "MenuHeader", 101 100 [0x4101] = "MenuItem", 102 101 [0x4201] = "MenuFooter", 103 - [0x2003] = "ArtworkReq", 102 + [0x2003] = "GetArtwork", 104 103 [0x4002] = "ArtworkResp", 105 - [0x2204] = "BeatGridReq", 104 + [0x2204] = "GetBeatGrid", 106 105 [0x4602] = "BeatGridResp", 107 - [0x2104] = "CuePointsReq", 106 + [0x2104] = "GetCuePoints", 108 107 [0x4702] = "CuePointsResp", 109 - [0x2b04] = "ExtCuePointsReq", 108 + [0x2b04] = "GetExtCuePoints", 110 109 [0x4e02] = "ExtCuePointsResp", 111 - [0x2004] = "WaveformPreviewReq", 112 - [0x4402] = "WaveformPreviewResp" 110 + [0x2004] = "GetWaveformPreview", 111 + [0x4402] = "WaveformPreviewResp", 112 + [0x2904] = "GetWaveformDetail", 113 + [0x4a02] = "WaveformDetailResp", 114 + [0x2c04] = "GetAnalysisTag", 115 + [0x4f02] = "AnalysisTagResp", 116 + [0x1105] = "GetTrackCount", 117 + [0x5105] = "TrackCountResp", 118 + [0x1103] = "GetTrackList", 119 + [0x5103] = "TrackListResp", 120 + } 121 + 122 + local MENU_ITEM_TYPES = { 123 + [0x0002] = "Album", 124 + [0x0004] = "TrackTitle", 125 + [0x0006] = "Genre", 126 + [0x0007] = "Artist", 127 + [0x000a] = "Rating", 128 + [0x000b] = "Duration", 129 + [0x000d] = "Tempo", 130 + [0x000f] = "Key", 131 + [0x0023] = "Comment", 132 + [0x002e] = "DateAdded", 113 133 } 114 134 135 + -- Simple UTF-16BE to UTF-8 conversion (ASCII only) 136 + function utf16_to_utf8(buf) 137 + local out = "" 138 + for i = 0, buf:len()-1, 2 do 139 + local c = buf(i+1, 1):uint() 140 + if c == 0 then break end 141 + if c < 128 then 142 + out = out .. string.char(c) 143 + else 144 + out = out .. "?" 145 + end 146 + end 147 + return out 148 + end 149 + 150 + -- Label for specific arguments based on message type 151 + function get_arg_label(msg_type, arg_idx, val) 152 + if arg_idx == 1 and (msg_type == 0x2002 or msg_type == 0x3000 or msg_type == 0x1105 or msg_type == 0x1103) then 153 + return "DMST" 154 + end 155 + 156 + if msg_type == 0x4101 then -- MenuItem 157 + local labels = { 158 + [1] = "Parent ID", 159 + [2] = "Main ID", 160 + [3] = "Label 1 Len", 161 + [4] = "Label 1", 162 + [5] = "Label 2 Len", 163 + [6] = "Label 2", 164 + [7] = "Item Type", 165 + [8] = "Flags", 166 + [9] = "Artwork ID", 167 + [10] = "Position" 168 + } 169 + return labels[arg_idx] 170 + elseif msg_type == 0x4201 then -- MenuFooter 171 + return arg_idx == 1 and "Status" or (arg_idx == 2 and "Total Items" or nil) 172 + elseif msg_type == 0x4001 then -- MenuHeader 173 + return arg_idx == 1 and "Item Count" or nil 174 + elseif msg_type == 0x4000 then -- Success/Ack 175 + return arg_idx == 1 and "Request Type Echo" or (arg_idx == 2 and "Items Found" or nil) 176 + elseif msg_type == 0x3000 then -- RenderMenu 177 + local labels = { 178 + [1] = "DMST", 179 + [2] = "Offset", 180 + [3] = "Limit", 181 + [4] = "Unknown", 182 + [5] = "Total Items", 183 + [6] = "Unknown" 184 + } 185 + return labels[arg_idx] 186 + elseif msg_type == 0x0000 then -- Setup 187 + return arg_idx == 1 and "Our Device Number" or nil 188 + end 189 + return nil 190 + end 191 + 115 192 -- ============================================================================ 116 - -- DB TAGGED FIELD DISSECTOR (Recursive) 193 + -- DB TAGGED FIELD DISSECTOR 117 194 -- ============================================================================ 118 - function dissect_db_field(buf, offset, tree) 195 + function dissect_db_field(buf, offset, tree, msg_type, arg_idx) 119 196 local len = buf:len() 120 197 if len - offset < 1 then return offset end 121 198 local tag = buf(offset, 1):uint() 122 199 local bytes_consumed = 1 200 + 201 + local label = get_arg_label(msg_type, arg_idx) 202 + local prefix = arg_idx and string.format("Arg %d", arg_idx) or "Field" 203 + if label then prefix = prefix .. " (" .. label .. ")" end 123 204 124 205 if tag == 0x0f then -- U8 125 206 if len - offset >= 2 then 126 - tree:add(f_db_field_u8, buf(offset+1, 1)) 207 + tree:add(f_db_field_u8, buf(offset+1, 1)):set_text(prefix .. ": " .. buf(offset+1, 1):uint()) 127 208 bytes_consumed = 2 128 209 end 129 210 elseif tag == 0x10 then -- U16 130 211 if len - offset >= 3 then 131 - tree:add(f_db_field_u16, buf(offset+1, 2)) 212 + tree:add(f_db_field_u16, buf(offset+1, 2)):set_text(prefix .. ": " .. buf(offset+1, 2):uint()) 132 213 bytes_consumed = 3 133 214 end 134 215 elseif tag == 0x11 then -- U32 135 216 if len - offset >= 5 then 136 - tree:add(f_db_field_u32, buf(offset+1, 4)) 217 + local val = buf(offset+1, 4):uint() 218 + local item = tree:add(f_db_field_u32, buf(offset+1, 4)) 219 + 220 + -- Special handling for DMST 221 + if label == "DMST" then 222 + item:set_text(prefix .. ": " .. string.format("0x%08x", val)) 223 + local dmst_tree = item:add(f_dmst, buf(offset+1, 4)) 224 + dmst_tree:add(f_dmst_dev, buf(offset+1, 1)) 225 + dmst_tree:add(f_dmst_menu, buf(offset+2, 1)) 226 + dmst_tree:add(f_dmst_slot, buf(offset+3, 1)) 227 + dmst_tree:add(f_dmst_type, buf(offset+4, 1)) 228 + elseif label == "Item Type" and MENU_ITEM_TYPES[val] then 229 + item:set_text(prefix .. ": " .. MENU_ITEM_TYPES[val] .. " (0x" .. string.format("%04x", val) .. ")") 230 + else 231 + item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%08x", val) .. ")") 232 + end 137 233 bytes_consumed = 5 138 234 end 139 235 elseif tag == 0x14 then -- Blob 140 236 if len - offset >= 5 then 141 237 local blob_len = buf(offset+1, 4):uint() 142 238 if len - offset >= 5 + blob_len then 143 - tree:add(f_db_field_bin, buf(offset+5, blob_len)) 239 + tree:add(f_db_field_bin, buf(offset+5, blob_len)):set_text(prefix .. ": [Blob " .. blob_len .. " bytes]") 144 240 bytes_consumed = 5 + blob_len 145 241 else 146 242 bytes_consumed = len - offset ··· 151 247 local chars = buf(offset+1, 4):uint() 152 248 local byte_len = chars * 2 153 249 if len - offset >= 5 + byte_len then 154 - tree:add(f_db_field_str, buf(offset+5, byte_len)) 250 + local str = utf16_to_utf8(buf(offset+5, byte_len)) 251 + tree:add(f_db_field_str, buf(offset+5, byte_len)):set_text(prefix .. ": \"" .. str .. "\"") 155 252 bytes_consumed = 5 + byte_len 156 253 else 157 254 bytes_consumed = len - offset ··· 168 265 function p_djl.dissector(buf, pkt, root) 169 266 local length = buf:len() 170 267 171 - -- Handle TCP DB Port Discovery Response (Special Case) 268 + -- TCP DB Port Discovery Response 172 269 if (pkt.src_port == 12523 or pkt.dst_port == 12523) and length == 2 then 270 + 173 271 local tree = root:add(p_djl, buf(0, 2)) 174 272 pkt.cols.protocol = "PDJL (DB Port)" 175 273 tree:add(f_db_port, buf(0, 2)) 176 274 return 177 275 end 178 276 179 - if length < 10 then return end 180 277 181 - -- Check for "Qspt1WmJOL" header 182 - local header_buf = buf(0, 10) 278 + if length < 5 then return end 279 + 280 + -- Check for "Qspt1WmJOL" header (UDP) 281 + local header_buf = buf(0, length >= 10 and 10 or length) 183 282 local header_str = header_buf:string() 184 283 185 - -- If it's not a UDP header, check if it's a DB Server packet (Magic 0x11 0x872349ae) 186 284 if header_str ~= "Qspt1WmJOL" then 187 - if length >= 5 and buf(0, 1):uint() == 0x11 and buf(1, 4):uint() == 0x872349ae then 188 - pkt.cols.protocol = "PDJL (DB)" 189 - local tree = root:add(p_djl, buf(0, length)) 190 - tree:add(f_db_magic, buf(1, 4)) 191 - 192 - local off = 5 193 - if length - off >= 5 then 194 - local tx_off = off 195 - off = dissect_db_field(buf, off, tree) -- Transaction ID 196 - end 197 - if length - off >= 3 then 198 - local type_off = off 199 - local m_type = buf(type_off+1, 2):uint() 200 - local m_type_str = DB_MSG_TYPES[m_type] or string.format("Unknown(0x%04x)", m_type) 285 + local off = 0 286 + local found_db = false 287 + local db_types = {} 288 + 289 + while off + 5 <= length do 290 + if buf(off, 1):uint() == 0x11 and buf(off+1, 4):uint() == 0x872349ae then 291 + if not found_db then 292 + pkt.cols.protocol = "PDJL (DB)" 293 + found_db = true 294 + end 201 295 202 - off = dissect_db_field(buf, off, tree) -- Message Type 296 + local msg_tree = root:add(p_djl, buf(off, length - off)) 297 + msg_tree:add(f_db_magic, buf(off+1, 4)) 203 298 204 - -- Set Info column and fence it 205 - pkt.cols.info:set("DB: " .. m_type_str) 206 - pkt.cols.info:fence() 207 - end 208 - if length - off >= 2 then 209 - off = dissect_db_field(buf, off, tree) -- Arg Count 210 - end 211 - 212 - -- Arg Tag Blob 213 - if off < length and buf(off, 1):uint() == 0x14 then 214 - if length - off >= 5 then 215 - local tag_len = buf(off+1, 4):uint() 216 - if length - off >= 5 + tag_len then 217 - tree:add(f_db_tag_blob, buf(off+5, tag_len)) 218 - off = off + 5 + tag_len 219 - else 220 - off = length -- premature end 299 + local current = off + 5 300 + 301 + -- Transaction ID 302 + if length - current >= 5 then 303 + current = dissect_db_field(buf, current, msg_tree) 304 + end 305 + 306 + -- Message Type 307 + local m_type = 0 308 + if length - current >= 3 then 309 + local type_off = current 310 + m_type = buf(type_off+1, 2):uint() 311 + local m_type_str = DB_MSG_TYPES[m_type] or string.format("Unknown(0x%04x)", m_type) 312 + table.insert(db_types, m_type_str) 313 + 314 + current = dissect_db_field(buf, current, msg_tree) 315 + msg_tree:append_text(": " .. m_type_str) 316 + end 317 + 318 + -- Arg Count 319 + local arg_count = 0 320 + if length - current >= 2 then 321 + if buf(current, 1):uint() == 0x0f then 322 + arg_count = buf(current + 1, 1):uint() 221 323 end 324 + current = dissect_db_field(buf, current, msg_tree) 222 325 end 223 - end 326 + 327 + -- Arg Tag Blob 328 + if current < length and buf(current, 1):uint() == 0x14 then 329 + if length - current >= 5 then 330 + local tag_len = buf(current+1, 4):uint() 331 + if length - current >= 5 + tag_len then 332 + msg_tree:add(f_db_tag_blob, buf(current+5, tag_len)) 333 + current = current + 5 + tag_len 334 + else 335 + current = length 336 + end 337 + end 338 + end 224 339 225 - -- Arguments 226 - while off < length do 227 - local prev_off = off 228 - off = dissect_db_field(buf, off, tree) 229 - if off <= prev_off then break end -- prevent infinite loop if dissector stuck 340 + -- Arguments 341 + for i = 1, arg_count do 342 + if current >= length then break end 343 + local prev_current = current 344 + current = dissect_db_field(buf, current, msg_tree, m_type, i) 345 + if current <= prev_current then break end 346 + end 347 + 348 + msg_tree:set_len(current - off) 349 + off = current 350 + else 351 + if off == 0 and length == 5 and buf(0, 5):uint() == 0x1100000001 then 352 + pkt.cols.protocol = "PDJL (DB)" 353 + pkt.cols.info:set("DB: Greeting") 354 + root:add(p_djl, buf(0, 5)):append_text(": Greeting (0x00000001)") 355 + return 356 + end 357 + break 230 358 end 359 + end 360 + 361 + if found_db then 362 + pkt.cols.info:set("DB: " .. table.concat(db_types, ", ")) 363 + pkt.cols.info:fence() 231 364 return 232 365 end 233 366 return 234 367 end 235 368 236 - -- Standard UDP Header Handling 369 + -- UDP Logic 237 370 pkt.cols.protocol = "PDJL" 238 371 local tree = root:add(p_djl, buf(0, length)) 239 372 tree:add(f_header, header_buf) 240 373 241 374 local p_type = buf(10, 1):uint() 242 - local p_type_str = PACKET_TYPES[p_type] or string.format("Unknown(0x%02x)", p_type) 243 - tree:add(f_type, buf(10, 1)):append_text(" (" .. p_type_str .. ")") 375 + local p_type_str = PACKET_TYPES[p_type] 376 + 377 + if p_type_str then 378 + tree:add(f_type, buf(10, 1)):append_text(" (" .. p_type_str .. ")") 379 + pkt.cols.info:set(p_type_str) 380 + else 381 + local unknown_label = string.format("Unknown(0x%02x)", p_type) 382 + tree:add(f_type, buf(10, 1)):append_text(" (" .. unknown_label .. ")") 383 + 384 + if length > 11 then 385 + local raw_hex = buf(11):bytes():tohex() 386 + tree:add(f_raw_payload, buf(11)) 387 + pkt.cols.info:set(unknown_label .. ": " .. raw_hex) 388 + else 389 + pkt.cols.info:set(unknown_label) 390 + end 391 + end 244 392 245 - pkt.cols.info:set(p_type_str) 246 393 pkt.cols.info:fence() 247 394 248 395 local src_port = pkt.src_port 249 396 local dst_port = pkt.dst_port 250 397 251 - -- Port 50000: Discovery & Claims 252 398 if dst_port == 50000 or src_port == 50000 then 253 399 tree:add(f_device_name, buf(0x0C, 20)) 254 - if p_type == 0x06 then -- Keep-Alive 400 + if p_type == 0x06 then 255 401 tree:add(f_dev_num, buf(0x24, 1)) 256 402 tree:add(f_mac, buf(0x26, 6)) 257 403 tree:add(f_ip, buf(0x2C, 4)) 258 - elseif p_type == 0x00 or p_type == 0x02 or p_type == 0x04 then -- Claims 404 + elseif p_type == 0x00 or p_type == 0x02 or p_type == 0x04 then 259 405 tree:add(f_sequence, buf(length-1, 1)) 260 406 if p_type == 0x02 or p_type == 0x04 then 261 - -- Device number is near end 262 407 tree:add(f_dev_num, buf(length-2, 1)) 263 408 end 264 409 end 265 - 266 - -- Port 50001: Beat & Position 267 410 elseif dst_port == 50001 or src_port == 50001 then 268 411 if p_type == 0x28 then -- Beat 269 412 tree:add(f_device_name, buf(0x0B, 20)) ··· 281 424 tree:add(f_dev_num, buf(0x21, 1)) 282 425 tree:add(f_sync_cmd, buf(0x2b, 1)) 283 426 end 284 - 285 - -- Port 50002: Detailed Status 286 427 elseif dst_port == 50002 or src_port == 50002 then 287 428 if p_type == 0x0a then -- CDJ Status 429 + tree:add(f_device_name, buf(0x0B, 20)) 288 430 tree:add(f_dev_num, buf(0x21, 1)) 431 + 289 432 tree:add(f_activity, buf(0x27, 1)) 290 433 tree:add(f_track_dev, buf(0x28, 1)) 291 434 tree:add(f_slot, buf(0x29, 1)) ··· 294 437 tree:add(f_bpm_raw, buf(0x92, 2)) 295 438 tree:add(f_beat_count, buf(0xa0, 4)) 296 439 tree:add(f_firmware, buf(0x7c, 4)) 297 - 298 - -- Expanded Flags 299 440 local flag_tree = tree:add(f_flags, buf(0x89, 1)) 300 441 flag_tree:add(f_flag_playing, buf(0x89, 1)) 301 442 flag_tree:add(f_flag_master, buf(0x89, 1)) ··· 305 446 end 306 447 end 307 448 308 - -- Register to standard ports 309 449 local udp_table = DissectorTable.get("udp.port") 310 450 udp_table:add(50000, p_djl) 311 451 udp_table:add(50001, p_djl) 312 452 udp_table:add(50002, p_djl) 313 453 314 454 local tcp_table = DissectorTable.get("tcp.port") 315 - tcp_table:add(12523, p_djl) -- Port discovery 316 - tcp_table:add(1051, p_djl) -- Default CDJ DB Server port 317 - tcp_table:add(1052, p_djl) -- XDJ-1000/3000 often uses secondary 455 + tcp_table:add(12523, p_djl) 456 + tcp_table:add(1051, p_djl) 457 + tcp_table:add(1052, p_djl)