-- Pro DJ Link Full Wireshark Dissector (Lua) -- Supports UDP Ports 50000, 50001, 50002 and TCP DB Server traffic local p_djl = Proto("pdjl", "Pro DJ Link") local TRACK_SORT_MODES = { [0x00] = "Default (By Title)", [0x01] = "By Title With Artist", [0x02] = "Artist", [0x03] = "Album", [0x04] = "Genre", [0x05] = "BPM", [0x06] = "Key", [0x07] = "Rating", [0x08] = "Duration", [0x09] = "Color", [0x0a] = "Date Added", [0x0b] = "Label", [0x0c] = "Bit Rate", [0x0d] = "Year", [0x0e] = "Original Artist", [0x0f] = "Remixer", [0x10] = "Comment", [0x11] = "DJ Play Count", [0x12] = "History", [0x13] = "Matching", } local PACKET_TYPES = { [0x00] = "Channel Claim 1", [0x02] = "Channel Claim 2", [0x04] = "Channel Claim Final", [0x06] = "Keep-Alive", [0x0a] = "CDJ Status / Announcement", [0x0b] = "Absolute Position", [0x28] = "Beat", [0x2a] = "Sync Control", [0x29] = "Mixer Status" } local DB_MSG_TYPES = { [0x0000] = "GetQueryContext", [0x0001] = "Termination / End Transaction", [0x0100] = "Disconnect", [0x1000] = "Root Menu", [0x4000] = "Success/Ack", [0x1001] = "Disconnect Acknowledgment", [0x1002] = "Artist Menu", [0x1003] = "Album Menu", [0x1004] = "Track Menu", [0x1006] = "BPM Menu", [0x1007] = "Rating Menu", [0x1008] = "Year Menu", [0x100a] = "Label Menu", [0x100d] = "Color Menu", [0x1010] = "Time Menu", [0x1011] = "Bitrate Menu", [0x1012] = "History Menu", [0x1013] = "Filename Menu", [0x1014] = "Key Menu", [0x2002] = "GetMetadata", [0x3000] = "RenderMenu", [0x4001] = "MenuHeader", [0x4101] = "MenuItem", [0x4201] = "MenuFooter", [0x2003] = "GetArtwork", [0x4002] = "ArtworkResponse", [0x2204] = "GetBeatGrid", [0x4602] = "BeatGridResponse", [0x2104] = "GetCuePoints", [0x4702] = "CuePointsResponse", [0x2b04] = "GetExtCuePoints", [0x4e02] = "ExtCuePointsResponse", [0x2004] = "GetWaveformPreview", [0x4402] = "WaveformPreviewResponse", [0x2102] = "GetTrackStreamingReq", [0x2904] = "GetWaveformDetail", [0x4a02] = "WaveformDetailResponse", [0x2c04] = "GetAnalysisTag", [0x4f02] = "AnalysisTagResponse", [0x3100] = "TrackLoadNotification", [0x3e03] = "GetCapabilities", [0x4b02] = "CapabilitiesResponse", [0x3007] = "SetupBrowseContext", [0x1101] = "ArtistsForGenre", [0x1102] = "AlbumsForArtist", [0x1103] = "GetTrackList", [0x1114] = "DistancesForKey", [0x1214] = "TracksNearKey", [0x1300] = "Search", [0x1301] = "TracksForGenreArtistAlbum", [0x2006] = "Folder", [0x1105] = "GetTrackCount", [0x1106] = "BPM Distances", [0x1206] = "TracksForBpmRange", [0x5105] = "TrackCountResponse", [0x5103] = "TrackListResponse", } local PLAY_MODES = { [0x00] = "No Track", [0x02] = "Loading", [0x03] = "Playing", [0x04] = "Looping", [0x05] = "Paused", [0x06] = "Paused at Cue", [0x07] = "Cue Play", [0x08] = "Cue Scratch", [0x09] = "Searching", [0x0e] = "CD Spun Down", [0x11] = "Ended", } local TRACK_TYPES = { [0x00] = "None", [0x01] = "Rekordbox", [0x02] = "Unanalyzed", [0x05] = "Audio CD", } local SYNC_COMMANDS = { [0x10] = "Turn On Sync", [0x20] = "Turn Off Sync", [0x01] = "Become Master", } local DEV_TYPES = { [0x01] = "Player (CDJ)", [0x02] = "Player (XDJ)", [0x03] = "Mixer", [0x04] = "PC/Rekordbox", } local INTERFACE_COLORS = { [0] = "Default", [1] = "Pink", [2] = "Red", [3] = "Orange", [4] = "Yellow", [5] = "Green", [6] = "Aqua", [7] = "Blue", [8] = "Purple", } local DMST_MENUS = { [0x01] = "Left/Main", [0x02] = "Right/Split", [0x03] = "Metadata Preview", [0x08] = "Waveform/Art/Grid", } local DMST_SLOTS = { [0x00] = "No Track", [0x01] = "CD", [0x02] = "SD", [0x03] = "USB", [0x04] = "Rekordbox", [0x06] = "Streaming (Direct)", [0x07] = "USB 2", [0x09] = "Beatport LINK", } local DMST_TYPES = { [0x01] = "Rekordbox", [0x02] = "Unanalyzed", [0x05] = "Audio CD", [0x06] = "Streaming", } local ANALYSIS_TAGS = { [0x49414d50] = "PMAI (Header)", [0x5a545150] = "PQTZ (Beat Grid)", [0x424f4350] = "PCOB (Cue List)", [0x54504350] = "PCPT (Cue List Entry)", [0x324f4350] = "PCO2 (Extended Cue List)", [0x32504350] = "PCP2 (Extended Cue List Entry)", [0x48545050] = "PPTH (Path)", [0x52425650] = "PVBR (VBR Index)", [0x56415750] = "PWAV (Waveform Preview)", [0x32565750] = "PWV2 (Tiny Waveform Preview)", [0x33565750] = "PWV3 (Waveform Detail)", [0x34565750] = "PWV4 (Color Waveform Preview)", [0x35565750] = "PWV5 (Color Waveform Detail)", [0x36565750] = "PWV6 (3-Band Waveform Preview)", [0x37565750] = "PWV7 (3-Band Waveform Detail)", [0x49535350] = "PSSI (Song Structure)", } local ARG_TAGS = { [0x00] = "none", [0x02] = "str", [0x03] = "blob", [0x06] = "u32", } -- Table to track TCP streams that have been identified as PDJL local pdjl_streams = {} -- Table to track Analysis Tag requests by Transaction ID for showing in responses local analysis_requests = {} -- key: stream_id .. ":" .. tx_id, value: tag_name -- Helper to get a unique key for a bidirectional TCP stream local function get_stream_key(pkt) local ip1 = tostring(pkt.net_src) local ip2 = tostring(pkt.net_dst) local p1 = pkt.src_port local p2 = pkt.dst_port if ip1 < ip2 or (ip1 == ip2 and p1 < p2) then return ip1 .. ":" .. p1 .. "-" .. ip2 .. ":" .. p2 else return ip2 .. ":" .. p2 .. "-" .. ip1 .. ":" .. p1 end end local MENU_ITEM_TYPES = { [0x0001] = "Folder", [0x0002] = "Album Title", [0x0003] = "Disc", [0x0004] = "Track Title", [0x0006] = "Genre", [0x0007] = "Artist", [0x0008] = "Playlist", [0x000a] = "Rating", [0x000b] = "Duration", [0x000d] = "Tempo", [0x000e] = "Label", [0x000f] = "Key", [0x0010] = "Bit Rate", [0x0011] = "Year", [0x0013] = "Color None", [0x0014] = "Color Pink", [0x0015] = "Color Red", [0x0016] = "Color Orange", [0x0017] = "Color Yellow", [0x0018] = "Color Green", [0x0019] = "Color Aqua", [0x001a] = "Color Blue", [0x001b] = "Color Purple", [0x0023] = "Comment", [0x0024] = "History Playlist", [0x0028] = "Original Artist", [0x0029] = "Remixer", [0x002e] = "Date Added", [0x0080] = "Genre menu", [0x0081] = "Artist menu", [0x0082] = "Album menu", [0x0083] = "Track menu", [0x0084] = "Playlist menu", [0x0085] = "Bpm menu", [0x0086] = "Rating menu", [0x0087] = "Year menu", [0x0088] = "Remixer menu", [0x0089] = "Label menu", [0x008a] = "Original Artist menu", [0x008b] = "Key menu", [0x008c] = "Date Added menu", [0x008e] = "Color menu", [0x0090] = "Folder menu", [0x0091] = "Search \"menu\"", [0x0092] = "Time menu", [0x0093] = "Bit Rate menu", [0x0094] = "Filename menu", [0x0095] = "History menu", [0x0098] = "Hot cue bank menu", [0x00a0] = "All", [0x00aa] = "Matching", [0x0204] = "Track Title and Album", [0x0604] = "Track Title and Genre", [0x0704] = "Track Title and Artist", [0x0a04] = "Track Title and Rating", [0x0b04] = "Track Title and Time", [0x0d04] = "Track Title and BPM", [0x0e04] = "Track Title and Label", [0x0f04] = "Track Title and Key", [0x1004] = "Track Title and Bit Rate", [0x1a04] = "Track Title and Color", [0x2304] = "Track Title and Comment", [0x2804] = "Track Title and Original Artist", [0x2904] = "Track Title and Remixer", [0x2a04] = "Track Title and DJ Play Count", [0x2e04] = "Track Title and Date Added", } -- ============================================================================ -- COMMON FIELDS -- ============================================================================ local f_header = ProtoField.string("pdjl.header", "Header") local f_type = ProtoField.uint8("pdjl.type", "Packet Type", base.HEX) local f_device_name = ProtoField.string("pdjl.device_name", "Device Name") local f_dev_num = ProtoField.uint8("pdjl.device_number", "Device Number", base.DEC) -- Port 50000 (Management) local f_mac = ProtoField.ether("pdjl.mac", "MAC Address") local f_ip = ProtoField.ipv4("pdjl.ip", "IP Address") local f_sequence = ProtoField.uint8("pdjl.sequence", "Sequence", base.DEC) local f_dev_type = ProtoField.uint8("pdjl.device_type", "Device Type", base.HEX) local f_peer_count = ProtoField.uint8("pdjl.peer_count", "Peer Count", base.DEC) -- Port 50001 (Beat Sync) local f_bpm_raw = ProtoField.uint16("pdjl.bpm_raw", "BPM", base.DEC) local f_pitch = ProtoField.uint32("pdjl.pitch", "Pitch", base.DEC) local f_beat_in_bar = ProtoField.uint8("pdjl.beat_in_bar", "Beat in Bar", base.DEC) local f_next_beat = ProtoField.uint32("pdjl.next_beat", "Next Beat (ms)", base.DEC) local f_second_beat = ProtoField.uint32("pdjl.second_beat", "Second Beat (ms)", base.DEC) local f_next_bar = ProtoField.uint32("pdjl.next_bar", "Next Bar (ms)", base.DEC) local f_fourth_beat = ProtoField.uint32("pdjl.fourth_beat", "Fourth Beat (ms)", base.DEC) local f_second_bar = ProtoField.uint32("pdjl.second_bar", "Second Bar (ms)", base.DEC) local f_eighth_beat = ProtoField.uint32("pdjl.eighth_beat", "Eighth Beat (ms)", base.DEC) local f_track_len = ProtoField.uint32("pdjl.track_length", "Track Length (s)", base.DEC) local f_playhead = ProtoField.uint32("pdjl.playhead", "Playhead (ms)", base.DEC) local f_pitch_abs = ProtoField.int32("pdjl.pitch_abs", "Position Pitch", base.DEC) local f_bpm_abs = ProtoField.uint32("pdjl.bpm_abs", "Position BPM", base.DEC) local f_sync_cmd = ProtoField.uint8("pdjl.sync_cmd", "Sync Command", base.HEX) -- Port 50002 (Status) local f_rb_id = ProtoField.uint32("pdjl.rekordbox_id", "Rekordbox ID", base.DEC) local f_track_num = ProtoField.uint16("pdjl.track_number", "Track Number", base.DEC) local f_track_sort = ProtoField.uint8("pdjl.track_sort", "Track Sort State", base.HEX, TRACK_SORT_MODES) local f_track_dev = ProtoField.uint8("pdjl.track_device", "Track Host Device", base.DEC) local f_slot = ProtoField.uint8("pdjl.slot", "Slot", base.HEX) local f_track_type = ProtoField.uint8("pdjl.track_type", "Track Type", base.HEX) local f_activity = ProtoField.uint8("pdjl.activity", "Activity", base.HEX) local f_firmware = ProtoField.string("pdjl.firmware", "Firmware") local f_beat_count = ProtoField.uint32("pdjl.beat_count", "Beat Count", base.DEC) local f_play_mode_1 = ProtoField.uint8("pdjl.play_mode_1", "Play Mode 1", base.HEX) local f_play_mode_2 = ProtoField.uint8("pdjl.play_mode_2", "Play Mode 2", base.HEX) local f_pitch_1 = ProtoField.uint32("pdjl.pitch_1", "Effective Pitch", base.DEC) local f_pitch_2 = ProtoField.uint32("pdjl.pitch_2", "Local Pitch", base.DEC) local f_master_info = ProtoField.uint8("pdjl.master_info", "Master Info", base.HEX) local f_handoff = ProtoField.uint8("pdjl.handoff", "Master Handoff", base.DEC) local f_countdown = ProtoField.uint16("pdjl.cue_countdown", "Cue Countdown", base.DEC) local f_pitch_3 = ProtoField.uint32("pdjl.pitch_3", "Pitch 3", base.DEC) local f_querying_ip = ProtoField.ipv4("pdjl.queried_ip", "Querying IP") local f_queried_dev = ProtoField.uint8("pdjl.queried_device", "Target Device", base.DEC) local f_queried_slot = ProtoField.uint8("pdjl.queried_slot", "Target Slot", base.HEX) local f_slot_owner = ProtoField.uint8("pdjl.slot_owner", "Slot Owner Device", base.DEC) local f_playlist_count = ProtoField.uint16("pdjl.playlist_count", "Playlist Count", base.DEC) local f_total_media_space = ProtoField.uint64("pdjl.total_media_space", "Total Space", base.DEC) local f_avail_media_space = ProtoField.uint64("pdjl.avail_media_space", "Available Space", base.DEC) local f_interface_color = ProtoField.uint8("pdjl.interface_color", "Interface Color", base.DEC, INTERFACE_COLORS) local f_settings_present = ProtoField.bool("pdjl.settings_present", "MYSETTINGS.DAT present", 1, nil, 0x01) local f_media_type = ProtoField.uint8("pdjl.media_type", "Media Type", base.HEX) local f_media_status = ProtoField.uint8("pdjl.media_status", "Media Status", base.HEX) local f_media_tracks = ProtoField.uint16("pdjl.media_tracks", "Media Tracks", base.DEC) local f_media_name = ProtoField.string("pdjl.media_name", "Media Name") local f_media_db_date= ProtoField.string("pdjl.media_db_date", "DB Creation Date") local f_flags = ProtoField.uint8("pdjl.flags", "Status Flags", base.HEX) local f_flag_playing= ProtoField.bool("pdjl.flags.playing", "Is Playing", 8, nil, 0x40) local f_flag_master = ProtoField.bool("pdjl.flags.master", "Is Master", 8, nil, 0x20) local f_flag_sync = ProtoField.bool("pdjl.flags.sync", "Is Syncing", 8, nil, 0x10) local f_flag_onair = ProtoField.bool("pdjl.flags.onair", "Is On Air", 8, nil, 0x08) local f_flag_bpm = ProtoField.bool("pdjl.flags.bpm", "BPM Sync Active", 8, nil, 0x02) -- TCP DB Server local f_db_magic = ProtoField.uint32("pdjl.db.magic", "DB Magic", base.HEX) local f_db_tx_id = ProtoField.uint32("pdjl.db.tx_id", "Transaction ID", base.HEX) local f_db_msg_type = ProtoField.uint16("pdjl.db.msg_type", "DB Message Type", base.HEX) local f_db_arg_count= ProtoField.uint8("pdjl.db.arg_count", "Argument Count", base.DEC) local f_db_tag_blob = ProtoField.bytes("pdjl.db.tags", "Argument Tags") local f_db_field_u8 = ProtoField.uint8("pdjl.db.field.u8", "Field (U8)", base.DEC) local f_db_field_u16= ProtoField.uint16("pdjl.db.field.u16", "Field (U16)", base.DEC) local f_db_field_u32= ProtoField.uint32("pdjl.db.field.u32", "Field (U32)", base.HEX) local f_db_field_u64= ProtoField.uint64("pdjl.db.field.u64", "Field (U64)", base.DEC) local f_db_field_str= ProtoField.string("pdjl.db.field.str", "Field (String)") local f_db_field_bin= ProtoField.bytes("pdjl.db.field.bin", "Field (Blob)") -- DMST decoding local f_dmst = ProtoField.uint32("pdjl.dmst", "DMST Parameter", base.HEX) local f_dmst_dev = ProtoField.uint8("pdjl.dmst.device", "Device", base.DEC) local f_dmst_menu = ProtoField.uint8("pdjl.dmst.menu", "Menu", base.HEX, DMST_MENUS) local f_dmst_slot = ProtoField.uint8("pdjl.dmst.slot", "Slot", base.HEX, DMST_SLOTS) local f_dmst_type = ProtoField.uint8("pdjl.dmst.type", "Type", base.HEX, DMST_TYPES) local f_db_port = ProtoField.uint16("pdjl.db_port", "DB Server Port", base.DEC) local f_raw_payload = ProtoField.bytes("pdjl.raw_payload", "Raw Payload") p_djl.fields = { f_header, f_type, f_device_name, f_dev_num, f_mac, f_ip, f_sequence, f_dev_type, f_peer_count, f_bpm_raw, f_pitch, f_beat_in_bar, f_next_beat, f_second_beat, f_next_bar, f_fourth_beat, f_second_bar, f_eighth_beat, f_track_len, f_playhead, f_pitch_abs, f_bpm_abs, f_sync_cmd, f_rb_id, f_track_num, f_track_sort, f_track_dev, f_slot, f_track_type, f_activity, f_firmware, f_beat_count, f_play_mode_1, f_play_mode_2, f_pitch_1, f_pitch_2, f_master_info, f_handoff, f_countdown, f_pitch_3, f_querying_ip, f_queried_dev, f_queried_slot, f_slot_owner, f_playlist_count, f_total_media_space, f_avail_media_space, f_interface_color, f_settings_present, f_media_type, f_media_status, f_media_tracks, f_media_name, f_media_db_date, f_flags, f_flag_playing, f_flag_master, f_flag_sync, f_flag_onair, f_flag_bpm, f_db_magic, f_db_tx_id, f_db_msg_type, f_db_arg_count, f_db_tag_blob, f_db_field_u8, f_db_field_u16, f_db_field_u32, f_db_field_u64, f_db_field_str, f_db_field_bin, f_dmst, f_dmst_dev, f_dmst_menu, f_dmst_slot, f_dmst_type, f_db_port, f_raw_payload } -- ============================================================================ -- HELPERS -- ============================================================================ -- Decodes a 4-byte buffer as a reverse-ASCII string local function get_reverse_ascii(buf) local b = buf:bytes() local s = "" for j = 3, 0, -1 do local c = b:get_index(j) if c ~= 0 then s = s .. string.char(c) end end return s end -- Label for specific arguments based on message type function get_arg_label(msg_type, arg_idx, val) -- Many request types have DMST as the first argument if arg_idx == 1 and ( msg_type == 0x1000 or -- Root Menu msg_type == 0x1001 or -- Genre Menu msg_type == 0x1002 or -- Artist Menu msg_type == 0x1003 or -- Album Menu msg_type == 0x1004 or -- Track Menu msg_type == 0x1006 or -- BPM Menu msg_type == 0x1007 or -- Rating Menu msg_type == 0x1008 or -- Year Menu msg_type == 0x100a or -- Label Menu msg_type == 0x100d or -- Color Menu msg_type == 0x1010 or -- Time Menu msg_type == 0x1011 or -- Bitrate Menu msg_type == 0x1012 or -- History Menu msg_type == 0x1013 or -- Filename Menu msg_type == 0x1014 or -- Key Menu msg_type == 0x1101 or -- ArtistsForGenre msg_type == 0x1102 or -- AlbumsForArtist msg_type == 0x1103 or -- GetTrackList msg_type == 0x1105 or -- GetTrackCount msg_type == 0x1106 or -- BPM Distances msg_type == 0x1114 or -- DistancesForKey msg_type == 0x1206 or -- TracksForBpmRange msg_type == 0x1214 or -- TracksNearKey msg_type == 0x1300 or -- Search msg_type == 0x1301 or -- TracksForGenreArtistAlbum msg_type == 0x2002 or -- GetMetadata msg_type == 0x2003 or -- GetArtwork msg_type == 0x2004 or -- GetWaveformPreview msg_type == 0x2102 or -- GetTrackStreamingReq msg_type == 0x2104 or -- GetCuePoints msg_type == 0x2204 or -- GetBeatGrid msg_type == 0x2904 or -- GetWaveformDetail msg_type == 0x2b04 or -- GetExtCuePoints msg_type == 0x2c04 or -- GetAnalysisTag msg_type == 0x3000 or -- RenderMenu msg_type == 0x3007 or -- SetupBrowseContext msg_type == 0x3100 or -- TrackLoadNotification msg_type == 0x3e03 -- GetCapabilities ) then return "DMST" end if msg_type == 0x1000 then -- Root Menu local labels = { [1] = "DMST", [2] = "Sort", [3] = "Unknown (0x00ffffff)" } return labels[arg_idx] elseif msg_type == 0x3100 then -- TrackLoadNotification local labels = { [1] = "DMST", [2] = "Rekordbox ID", [3] = "Unknown 1", [4] = "Unknown 2" } return labels[arg_idx] elseif msg_type == 0x2c04 then -- GetAnalysisTag local labels = { [1] = "DMST", [2] = "Rekordbox ID", [3] = "Tag", [4] = "Extension" } return labels[arg_idx] elseif msg_type == 0x2102 then -- GetTrackStreamingReq local labels = { [1] = "DMST", [2] = "Rekordbox ID" } return labels[arg_idx] elseif msg_type == 0x4101 then -- MenuItem local labels = { [1] = "Parent ID", [2] = "Main ID", [3] = "Label 1 Len", [4] = "Label 1", [5] = "Label 2 Len", [6] = "Label 2", [7] = "Item Type", [8] = "Flags", [9] = "Artwork ID", [10] = "Position", [11] = "Unknown", [12] = "Unknown" } return labels[arg_idx] elseif msg_type == 0x4201 then -- MenuFooter return arg_idx == 1 and "Status" or (arg_idx == 2 and "Total Items" or nil) elseif msg_type == 0x4001 then -- MenuHeader return arg_idx == 1 and "Item Count" or nil elseif msg_type == 0x4000 then -- Success/Ack return arg_idx == 1 and "Request Type Echo" or (arg_idx == 2 and "Items Found" or nil) elseif msg_type == 0x4602 or msg_type == 0x4702 or msg_type == 0x4e02 or msg_type == 0x4a02 or msg_type == 0x4402 or msg_type == 0x4002 or msg_type == 0x4f02 then -- Blob Responses if arg_idx == 1 then return "Request Type Echo" end if arg_idx == 2 then return "Status/Echo" end if arg_idx == 3 then return "Length" end if arg_idx == 4 then local labels = { [0x4002] = "Blob (image)", [0x4602] = "Beat Grid Data", [0x4702] = "Cue Points Data", [0x4e02] = "Extended Cue Points Data", [0x4402] = "Waveform Preview Data", [0x4a02] = "Waveform Detail Data", [0x4f02] = "Analysis Tag Data", } return labels[msg_type] or "Blob Data" end return nil elseif msg_type == 0x3000 then -- RenderMenu local labels = { [1] = "DMST", [2] = "Offset", [3] = "Limit", [4] = "Unknown", [5] = "Total Items", [6] = "Unknown" } return labels[arg_idx] elseif msg_type == 0x0000 then -- Setup return arg_idx == 1 and "Our Device Number" or nil elseif msg_type == 0x4b02 then -- CapabilitiesResp local labels = { [1] = "Request Type Echo", [2] = "Reserved", [3] = "Capability Level", [4] = "Placeholder (Empty String)" } return labels[arg_idx] end return nil end -- ============================================================================ -- DB TAGGED FIELD DISSECTOR -- ============================================================================ function dissect_db_field(buf, offset, tree, msg_type, arg_idx) local len = buf:len() if len - offset < 1 then return offset, nil, nil end local tag = buf(offset, 1):uint() local bytes_consumed = 1 local val = nil local item = nil local prefix local label if type(arg_idx) == "string" then prefix = arg_idx else label = get_arg_label(msg_type, arg_idx) prefix = arg_idx and string.format("Arg %d", arg_idx) or "Field" if label then prefix = prefix .. " (" .. label .. ")" end end if tag == 0x0f then -- U8 if len - offset >= 2 then val = buf(offset+1, 1):uint() item = tree:add(f_db_field_u8, buf(offset+1, 1)) item:set_text(prefix .. ": " .. val) bytes_consumed = 2 end elseif tag == 0x10 then -- U16 if len - offset >= 3 then val = buf(offset+1, 2):uint() item = tree:add(f_db_field_u16, buf(offset+1, 2)) if prefix == "Message Type" then local m_type_str = DB_MSG_TYPES[val] or string.format("Unknown(0x%04x)", val) item:set_text(prefix .. ": " .. m_type_str .. " (0x" .. string.format("%04x", val) .. ")") else item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%04x", val) .. ")") end bytes_consumed = 3 end elseif tag == 0x11 then -- U32 if len - offset >= 5 then val = buf(offset+1, 4):uint() -- Special handling for DMST to keep it as one expandable layer if label == "DMST" then local dmst_tree = tree:add(f_dmst, buf(offset+1, 4)) dmst_tree:set_text(prefix .. ": " .. string.format("0x%08x", val)) dmst_tree:add(f_dmst_dev, buf(offset+1, 1)) dmst_tree:add(f_dmst_menu, buf(offset+2, 1)) dmst_tree:add(f_dmst_slot, buf(offset+3, 1)) dmst_tree:add(f_dmst_type, buf(offset+4, 1)) item = dmst_tree else item = tree:add(f_db_field_u32, buf(offset+1, 4)) if label == "Transaction ID" and val == 0xfffffffe then item:set_text(prefix .. ": Setup/Teardown (0xfffffffe)") elseif label == "Item Type" and MENU_ITEM_TYPES[val] then item:set_text(prefix .. ": " .. MENU_ITEM_TYPES[val] .. " (0x" .. string.format("%04x", val) .. ")") elseif label == "Tag" then local tag_str = ANALYSIS_TAGS[val] or get_reverse_ascii(buf(offset+1, 4)) item:set_text(prefix .. ": " .. tag_str .. " (0x" .. string.format("%08x", val) .. ")") elseif label == "Extension" then item:set_text(prefix .. ": \"" .. get_reverse_ascii(buf(offset+1, 4)) .. "\" (0x" .. string.format("%08x", val) .. ")") else item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%08x", val) .. ")") end end bytes_consumed = 5 end elseif tag == 0x14 then -- Blob if len - offset >= 5 then local blob_len = buf(offset+1, 4):uint() local available = len - offset - 5 local to_read = math.min(blob_len, available) item = tree:add(f_db_field_bin, buf(offset+5, to_read)) if to_read < blob_len then item:set_text(prefix .. ": [Blob Truncated " .. to_read .. "/" .. blob_len .. " bytes]") else item:set_text(prefix .. ": [Blob " .. blob_len .. " bytes]") end bytes_consumed = 5 + to_read val = buf(offset+5, to_read):bytes() else bytes_consumed = len - offset end elseif tag == 0x26 then -- String (UTF-16BE) if len - offset >= 5 then local chars = buf(offset+1, 4):uint() local byte_len = chars * 2 if len - offset >= 5 + byte_len then -- Use Wireshark's native UTF-16BE decoding local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0) local str = buf(offset+5, byte_len):string(enc) -- Strip trailing nulls to clean up UI display str = str:gsub("%z", "") -- Use f_db_field_bin for the highlight range to avoid "trailing stray chars" warning -- caused by Wireshark's internal string dissector seeing high-byte nulls. item = tree:add(f_db_field_bin, buf(offset+5, byte_len)) item:set_text(prefix .. ": \"" .. str .. "\"") bytes_consumed = 5 + byte_len val = str else bytes_consumed = len - offset end end end return offset + bytes_consumed, val, item end -- ============================================================================ -- DB SERVER DISSECTION LOGIC -- ============================================================================ -- Helper to calculate the full byte length of a DB message starting at `offset`. -- Returns: -- > 0: The total byte length of the complete message. -- < 0: The number of missing bytes needed to complete the message (returns -missing). -- 0: Not a valid DB message or parse error. local function get_db_msg_len(buf, offset) local len = buf:len() local available = len - offset if available < 5 then return -(5 - available) end local tag = buf(offset, 1):uint() local magic = buf(offset+1, 4):uint() if tag == 0x11 and magic == 0x00000001 then return 5 end if tag ~= 0x11 or magic ~= 0x872349ae then return 0 end local cur = offset + 5 local function read_field() local a = len - cur if a < 1 then return false, (1 - a) end local ftag = buf(cur, 1):uint() if ftag == 0x0f then if a < 2 then return false, (2 - a) end local val = buf(cur+1, 1):uint() cur = cur + 2 return true, val elseif ftag == 0x10 then if a < 3 then return false, (3 - a) end local val = buf(cur+1, 2):uint() cur = cur + 3 return true, val elseif ftag == 0x11 then if a < 5 then return false, (5 - a) end local val = buf(cur+1, 4):uint() cur = cur + 5 return true, val elseif ftag == 0x14 then if a < 5 then return false, (5 - a) end local blen = buf(cur+1, 4):uint() if a < 5 + blen then return false, (5 + blen - a) end cur = cur + 5 + blen return true, nil elseif ftag == 0x26 then if a < 5 then return false, (5 - a) end local chars = buf(cur+1, 4):uint() local blen = chars * 2 if a < 5 + blen then return false, (5 + blen - a) end cur = cur + 5 + blen return true, nil else return false, nil end end local ok, val = read_field() -- Tx ID if not ok then return val and -val or 0 end ok, val = read_field() -- Msg Type if not ok then return val and -val or 0 end ok, val = read_field() -- Arg Count if not ok then return val and -val or 0 end local arg_count = val local a = len - cur if a < 1 then return -(1 - a) end if buf(cur, 1):uint() == 0x14 then ok, val = read_field() if not ok then return val and -val or 0 end end for i = 1, arg_count do ok, val = read_field() if not ok then return val and -val or 0 end end return cur - offset end local function dissect_db_tcp(buf, pkt, root, length) local off = 0 local found_db = false local db_types = {} local tx_ids = {} local src_port = pkt.src_port local dst_port = pkt.dst_port local stream_id = get_stream_key(pkt) while off < length do local available = length - off if available < 5 then pkt.desegment_offset = off pkt.desegment_len = 5 - available return true end local tag = buf(off, 1):uint() local magic_or_val = buf(off+1, 4):uint() if tag == 0x11 and (magic_or_val == 0x872349ae or magic_or_val == 0x00000001) then pdjl_streams[stream_id] = true local msg_len = get_db_msg_len(buf, off) if msg_len < 0 then pkt.desegment_offset = off pkt.desegment_len = -msg_len return true elseif msg_len == 0 then break -- parse error, stop processing end if magic_or_val == 0x872349ae then if not found_db then pkt.cols.protocol = "Pro DJ Link (DB)" found_db = true end local msg_tree = root:add(p_djl, buf(off, msg_len)) msg_tree:add(f_db_magic, buf(off+1, 4)) local current = off + 5 -- Transaction ID local tx_id = 0 if msg_len - (current - off) >= 5 then local bytes_read, val = dissect_db_field(buf, current, msg_tree, nil, "Transaction ID") tx_id = val or 0 local tx_str = (tx_id == 0xfffffffe) and "Setup/Teardown" or string.format("0x%08x", tx_id) local already_added = false for _, v in ipairs(tx_ids) do if v == tx_str then already_added = true break end end if not already_added then table.insert(tx_ids, tx_str) end current = bytes_read end -- Message Type local m_type = 0 if msg_len - (current - off) >= 3 then local bytes_read, val, m_type_item = dissect_db_field(buf, current, msg_tree, nil, "Message Type") m_type = val or 0 local m_type_str = DB_MSG_TYPES[m_type] or string.format("Unknown(0x%04x)", m_type) if m_type == 0x0100 and tx_id == 0xfffffffe then m_type_str = "Device Disconnect" if m_type_item then m_type_item:set_text("Message Type: " .. m_type_str .. " (0x" .. string.format("%04x", m_type) .. ")") end end table.insert(db_types, m_type_str) current = bytes_read msg_tree:append_text(": " .. m_type_str) end -- Arg Count local arg_count = 0 if msg_len - (current - off) >= 2 then local bytes_read, val = dissect_db_field(buf, current, msg_tree, nil, "Arg Count") arg_count = val or 0 current = bytes_read end -- Arg Tag Blob (usually 12 bytes) if msg_len - (current - off) >= 5 and buf(current, 1):uint() == 0x14 then local tag_len = buf(current+1, 4):uint() if msg_len - (current - off) >= 5 + tag_len then local tag_item = msg_tree:add(f_db_tag_blob, buf(current+5, tag_len)) local arg_types = {} for i = 0, tag_len - 1 do local t = buf(current+5+i, 1):uint() if t ~= 0 then table.insert(arg_types, ARG_TAGS[t] or string.format("0x%02x", t)) end end if #arg_types > 0 then tag_item:set_text("Arg Types: " .. table.concat(arg_types, ", ")) end current = current + 5 + tag_len end end -- Arguments for i = 1, arg_count do if current >= off + msg_len then break end local prev_current = current local bytes_read, val = dissect_db_field(buf, current, msg_tree, m_type, i) -- If it's a Success/Ack, show what it's acking in the summary if m_type == 0x4000 then if i == 1 and val then local ack_type_str = DB_MSG_TYPES[val] or string.format("0x%04x", val) msg_tree:append_text(" - " .. ack_type_str) db_types[#db_types] = "Ack " .. ack_type_str elseif i == 2 and val then msg_tree:append_text(": " .. val .. " Items") db_types[#db_types] = db_types[#db_types] .. ": " .. val .. " Items" end end -- If it's a GetAnalysisTag request, record the tag for the response if m_type == 0x2c04 and i == 3 and val then local tag_name = ANALYSIS_TAGS[val] or get_reverse_ascii(buf(prev_current+1, 4)) analysis_requests[stream_id .. ":" .. tx_id] = tag_name msg_tree:append_text(" - " .. tag_name) -- Update the last entry in db_types to include the tag db_types[#db_types] = db_types[#db_types] .. " - " .. tag_name end -- If it's a TrackLoadNotification, show the Rekordbox ID in the summary if m_type == 0x3100 and i == 2 and val then msg_tree:append_text(" - RB ID: " .. val) db_types[#db_types] = db_types[#db_types] .. " - RB ID: " .. val end -- If it's an AnalysisTagResponse, try to find the tag from the request if m_type == 0x4f02 and i == 1 then local tag_name = analysis_requests[stream_id .. ":" .. tx_id] if tag_name then msg_tree:append_text(" - " .. tag_name) db_types[#db_types] = db_types[#db_types] .. " - " .. tag_name end end current = bytes_read if current <= prev_current then break end end -- Special handling for trailing binary data in blob responses if (m_type == 0x4002 or m_type == 0x4602 or m_type == 0x4702 or m_type == 0x4e02 or m_type == 0x4a02 or m_type == 0x4402 or m_type == 0x4f02) and current < off + msg_len then local label = get_arg_label(m_type, 4) or "Blob Data" local remaining = (off + msg_len) - current msg_tree:add(f_db_field_bin, buf(current, remaining)):set_text(label .. ": [" .. remaining .. " bytes]") current = current + remaining end msg_tree:set_len(msg_len) off = off + msg_len elseif magic_or_val == 0x00000001 then if not found_db then pkt.cols.protocol = "Pro DJ Link (DB)" found_db = true end table.insert(db_types, "Greeting") root:add(p_djl, buf(off, 5)):set_text("Greeting (0x00000001)") off = off + 5 end else break end end if found_db then -- Group consecutive message types for a concise summary (e.g., "MenuItem x 10") local summarized_types = {} local current_type = nil local current_count = 0 for _, t in ipairs(db_types) do if t == current_type then current_count = current_count + 1 else if current_type then local entry = (current_count > 1) and (current_type .. " x " .. current_count) or current_type table.insert(summarized_types, entry) end current_type = t current_count = 1 end end -- Final entry if current_type then local entry = (current_count > 1) and (current_type .. " x " .. current_count) or current_type table.insert(summarized_types, entry) end local info_str = table.concat(summarized_types, ", ") -- Only append the first transaction ID found in the packet if #tx_ids > 0 then info_str = info_str .. " [tx: " .. tx_ids[1] .. "]" end pkt.cols.info:set(info_str) pkt.cols.info:fence() return true end return false end -- ============================================================================ -- HELPERS - PORT 50000/50001/50002 -- ============================================================================ local function dissect_management_port(buf, pkt, tree, p_type) tree:add(f_device_name, buf(0x0C, 20)) if p_type == 0x06 then -- Keep-Alive tree:add(f_dev_num, buf(0x24, 1)) local dtype = buf(0x25, 1):uint() tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")") tree:add(f_mac, buf(0x26, 6)) tree:add(f_ip, buf(0x2C, 4)) tree:add(f_peer_count, buf(0x30, 1)) elseif p_type == 0x0a then -- Announcement local dtype = buf(0x24, 1):uint() tree:add(f_dev_type, buf(0x24, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")") elseif p_type == 0x00 then -- Claim Stage 1 tree:add(f_sequence, buf(0x24, 1)) local dtype = buf(0x25, 1):uint() tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")") tree:add(f_mac, buf(0x26, 6)) elseif p_type == 0x02 then -- Claim Stage 2 tree:add(f_ip, buf(0x24, 4)) tree:add(f_mac, buf(0x28, 6)) tree:add(f_dev_num, buf(0x2E, 1)) tree:add(f_sequence, buf(0x2F, 1)) elseif p_type == 0x04 then -- Claim Final tree:add(f_dev_num, buf(0x24, 1)) tree:add(f_sequence, buf(0x25, 1)) end end local function dissect_beat_sync_port(buf, pkt, tree, p_type) if p_type == 0x28 then -- Beat tree:add(f_device_name, buf(0x0B, 20)) tree:add(f_dev_num, buf(0x21, 1)) tree:add(f_next_beat, buf(0x24, 4)) tree:add(f_second_beat, buf(0x28, 4)) tree:add(f_next_bar, buf(0x2C, 4)) tree:add(f_fourth_beat, buf(0x30, 4)) tree:add(f_second_bar, buf(0x34, 4)) tree:add(f_eighth_beat, buf(0x38, 4)) local raw_pitch = buf(0x54, 4):uint() local pitch_pct = (raw_pitch / 0x100000 - 1.0) * 100 tree:add(f_pitch, buf(0x54, 4)):set_text(string.format("Pitch: %+.2f%%", pitch_pct)) local bpm_val = buf(0x5a, 2):uint() tree:add(f_bpm_raw, buf(0x5a, 2)):set_text(string.format("BPM: %.2f", bpm_val / 100)) tree:add(f_beat_in_bar, buf(0x5c, 1)) elseif p_type == 0x0b then -- Absolute Position tree:add(f_dev_num, buf(0x21, 1)) tree:add(f_track_len, buf(0x24, 4)) tree:add(f_playhead, buf(0x28, 4)) tree:add(f_pitch_abs, buf(0x2C, 4)) tree:add(f_bpm_abs, buf(0x38, 4)) elseif p_type == 0x2a then -- Sync Control tree:add(f_dev_num, buf(0x21, 1)) local scmd = buf(0x2b, 1):uint() tree:add(f_sync_cmd, buf(0x2b, 1)):append_text(" (" .. (SYNC_COMMANDS[scmd] or "Unknown") .. ")") end end local function dissect_status_media_port(buf, pkt, tree, p_type, length) if p_type == 0x05 then -- Media Query tree:add(f_device_name, buf(0x0B, 20)) tree:add(f_dev_num, buf(0x21, 1)) if length >= 0x30 then tree:add(f_querying_ip, buf(0x24, 4)) tree:add(f_queried_dev, buf(0x2B, 1)) -- Target Device: 1 byte at 0x2B tree:add(f_queried_slot, buf(0x2F, 1)) -- Target Slot: 1 byte at 0x2F end elseif p_type == 0x06 then -- Media Response tree:add(f_device_name, buf(0x0B, 20)) tree:add(f_dev_num, buf(0x21, 1)) -- The real Pioneer response is 192 bytes (0xC0): 36 bytes header + 156 bytes payload if length >= 0xC0 then local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0) tree:add(f_slot_owner, buf(0x27, 1)) -- Target Device ($D_r$) tree:add(f_queried_slot, buf(0x2B, 1)) -- Target Slot ($S_r$): 1 byte at 0x2B -- Media Name starts at 0x2C (UTF-16, 64 bytes) local name_str = buf(0x2C, 64):string(enc):gsub("%z", "") tree:add(f_media_name, buf(0x2C, 64), name_str) -- DB Creation Date starts at 0x6C (UTF-16, 40 bytes) local date_str = buf(0x6C, 40):string(enc):gsub("%z", "") tree:add(f_media_db_date, buf(0x6C, 40), date_str) -- Unknown Text at 0x94–0xA5 (18 bytes) local unknown_str = buf(0x94, 18):string(enc):gsub("%z", "") tree:add(f_raw_payload, buf(0x94, 18)):set_text("Unknown Text: \"" .. unknown_str .. "\"") tree:add(f_media_tracks, buf(0xa6, 2)) local c_val = buf(0xa8, 1):uint() local c_str = INTERFACE_COLORS[c_val] or "Unknown" tree:add(f_interface_color, buf(0xa8, 1)):set_text("Interface Color: " .. c_str .. " (" .. c_val .. ")") tree:add(f_media_type, buf(0xaa, 1)) -- Media Type $T_r$ at correct offset tree:add(f_settings_present, buf(0xab, 1)) -- Settings presence bit tree:add(f_playlist_count, buf(0xae, 2)) tree:add(f_total_media_space, buf(0xb0, 8)) tree:add(f_avail_media_space, buf(0xb8, 8)) elseif length >= 0x2C + 32 then -- Fallback: 80-byte synthetic format from our own NFS server (djlink/src/server.rs). -- This does NOT match real Pioneer hardware; offsets differ from the spec above. local enc = (ENC_ASCII or 0) local name_str = buf(0x2C, 32):string(enc):gsub("%z", "") tree:add(f_media_type, buf(0x26, 1)) tree:add(f_media_status, buf(0x27, 1)) tree:add(f_media_tracks, buf(0x28, 4)) tree:add(f_media_name, buf(0x2C, 32), name_str) end elseif p_type == 0x0a then -- CDJ Status tree:add(f_device_name, buf(0x0B, 20)) tree:add(f_dev_num, buf(0x21, 1)) tree:add(f_activity, buf(0x27, 1)) tree:add(f_track_dev, buf(0x28, 1)) tree:add(f_slot, buf(0x29, 1)) local ttype = buf(0x2a, 1):uint() tree:add(f_track_type, buf(0x2a, 1)):append_text(" (" .. (TRACK_TYPES[ttype] or "Unknown") .. ")") tree:add(f_rb_id, buf(0x2c, 4)) tree:add(f_track_num, buf(0x32, 2)) local tsort = buf(0x35, 1):uint() tree:add(f_track_sort, buf(0x35, 1)) local pmode1 = buf(0x7b, 1):uint() tree:add(f_play_mode_1, buf(0x7b, 1)):append_text(" (" .. (PLAY_MODES[pmode1] or "Unknown") .. ")") tree:add(f_firmware, buf(0x7c, 4)) local flag_tree = tree:add(f_flags, buf(0x89, 1)) flag_tree:add(f_flag_playing, buf(0x89, 1)) flag_tree:add(f_flag_master, buf(0x89, 1)) flag_tree:add(f_flag_sync, buf(0x89, 1)) flag_tree:add(f_flag_onair, buf(0x89, 1)) flag_tree:add(f_flag_bpm, buf(0x89, 1)) local pmode2 = buf(0x8b, 1):uint() tree:add(f_play_mode_2, buf(0x8b, 1)):append_text(" (" .. (PLAY_MODES[pmode2] or "Unknown") .. ")") local p_1 = buf(0x8c, 4):uint() local p_1_pct = (p_1 / 0x100000 - 1.0) * 100 tree:add(f_pitch_1, buf(0x8c, 4)):set_text(string.format("Effective Pitch: %+.2f%%", p_1_pct)) local bpm_val = buf(0x92, 2):uint() if bpm_val == 0xffff then tree:add(f_bpm_raw, buf(0x92, 2)):set_text("BPM: None") else tree:add(f_bpm_raw, buf(0x92, 2)):set_text(string.format("BPM: %.2f", bpm_val / 100)) end local p_2 = buf(0x98, 4):uint() local p_2_pct = (p_2 / 0x100000 - 1.0) * 100 tree:add(f_pitch_2, buf(0x98, 4)):set_text(string.format("Local Pitch: %+.2f%%", p_2_pct)) tree:add(f_master_info, buf(0x9e, 1)) tree:add(f_handoff, buf(0x9f, 1)) local beat_count = buf(0xa0, 4):uint() if beat_count == 0xffffffff then tree:add(f_beat_count, buf(0xa0, 4)):set_text("Beat Count: None") else tree:add(f_beat_count, buf(0xa0, 4)):set_text("Beat Count: " .. beat_count) end tree:add(f_countdown, buf(0xa4, 2)) tree:add(f_beat_in_bar, buf(0xa6, 1)) local p_3 = buf(0xc0, 4):uint() local p_3_pct = (p_3 / 0x100000 - 1.0) * 100 tree:add(f_pitch_3, buf(0xc0, 4)):set_text(string.format("Pitch 3: %+.2f%%", p_3_pct)) end end -- ============================================================================ -- MAIN DISSECTOR ENTRY -- ============================================================================ local function dissect_pdjl_main(buf, pkt, root, length) local src_port = pkt.src_port local dst_port = pkt.dst_port -- TCP DB Port Discovery if (src_port == 12523 or dst_port == 12523) then if length == 2 then local tree = root:add(p_djl, buf(0, 2)) pkt.cols.protocol = "Pro DJ Link (DB Port)" pkt.cols.info:set("Port Discovery Response") tree:add(f_db_port, buf(0, 2)) return true elseif (length == 15 and buf(0, 1):uint() == 0x52) or (length == 19 and buf(0, 4):uint() == 0x0000000f) then local tree = root:add(p_djl, buf(0, length)) pkt.cols.protocol = "Pro DJ Link (DB Port)" pkt.cols.info:set("Port Discovery Request") tree:add(f_raw_payload, buf(0, length)):set_text("Discovery Request: RemoteDBServer") return true end end if length < 5 then return false end -- Check for "Qspt1WmJOL" header (UDP) if length >= 10 and buf(0, 10):string() == "Qspt1WmJOL" then -- UDP Logic pkt.cols.protocol = "Pro DJ Link" local tree = root:add(p_djl, buf(0, length)) tree:add(f_header, buf(0, 10)) local p_type = buf(10, 1):uint() local p_type_str = PACKET_TYPES[p_type] if dst_port == 50002 or src_port == 50002 then if p_type == 0x05 then p_type_str = "Media Query" end if p_type == 0x06 then p_type_str = "Media Response" end end if p_type_str then tree:add(f_type, buf(10, 1)):append_text(" (" .. p_type_str .. ")") pkt.cols.info:set(p_type_str) else local unknown_label = string.format("Unknown(0x%02x)", p_type) tree:add(f_type, buf(10, 1)):append_text(" (" .. unknown_label .. ")") if length > 11 then local raw_hex = buf(11):bytes():tohex() tree:add(f_raw_payload, buf(11)) pkt.cols.info:set(unknown_label .. ": " .. raw_hex) else pkt.cols.info:set(unknown_label) end end pkt.cols.info:fence() if dst_port == 50000 or src_port == 50000 then dissect_management_port(buf, pkt, tree, p_type) elseif dst_port == 50001 or src_port == 50001 then dissect_beat_sync_port(buf, pkt, tree, p_type) elseif dst_port == 50002 or src_port == 50002 then dissect_status_media_port(buf, pkt, tree, p_type, length) end return true else -- Fallback to DB TCP parser return dissect_db_tcp(buf, pkt, root, length) end end function p_djl.dissector(buf, pkt, root) dissect_pdjl_main(buf, pkt, root, buf:len()) end -- Heuristic TCP dissector to pick up DB traffic on dynamic ports p_djl:register_heuristic("tcp", function(buf, pkt, root) local stream_id = get_stream_key(pkt) if buf:len() < 5 then -- If we already know this stream, claim even small packets if pdjl_streams[stream_id] then return dissect_pdjl_main(buf, pkt, root, buf:len()) end return false end local tag = buf(0, 1):uint() local val = buf(1, 4):uint() -- Match DB Magic, DB Greeting, or a previously identified PDJL stream if (tag == 0x11 and (val == 0x872349ae or val == 0x00000001)) or pdjl_streams[stream_id] then return dissect_pdjl_main(buf, pkt, root, buf:len()) end return false end) local udp_table = DissectorTable.get("udp.port") udp_table:add(50000, p_djl) udp_table:add(50001, p_djl) udp_table:add(50002, p_djl) local tcp_table = DissectorTable.get("tcp.port") tcp_table:add(12523, p_djl) tcp_table:add(1051, p_djl) tcp_table:add(1052, p_djl)