Skip to content

Instantly share code, notes, and snippets.

@JJTech0130
Last active May 9, 2026 21:32
Show Gist options
  • Select an option

  • Save JJTech0130/78527c600f7b4d0a7aeb294eab06d8ba to your computer and use it in GitHub Desktop.

Select an option

Save JJTech0130/78527c600f7b4d0a7aeb294eab06d8ba to your computer and use it in GitHub Desktop.
iAP2 (iPod Accessory Protocol 2) Wireshark Dissector
-- iAP2 (iPod Accessory Protocol 2)
--
-- Layers:
-- 1. iAP2 Link — packet framing, control flags, checksums
-- 2. iAP2 Session — payload interpretation based on session type
-- a. Control Session messages with parameters
-- b. File Transfer Session datagrams
-- c. External Accessory Session datagrams
-- ============================================================
-- Protocol declarations
-- ============================================================
local iap2_proto = Proto("iap2", "iPod Accessory Protocol 2 (iAP2)")
-- ============================================================
-- Link Layer Fields
-- ============================================================
local f_link_sop = ProtoField.uint16("iap2.link.sop", "Start of Packet", base.HEX)
local f_link_length = ProtoField.uint16("iap2.link.length", "Packet Length", base.DEC)
local f_link_ctrl = ProtoField.uint8 ("iap2.link.control", "Control Byte", base.HEX)
local f_link_ctrl_syn = ProtoField.bool ("iap2.link.control.syn","SYN - Link Sync Payload", 8, nil, 0x80)
local f_link_ctrl_ack = ProtoField.bool ("iap2.link.control.ack","ACK - Acknowledgement Valid", 8, nil, 0x40)
local f_link_ctrl_eak = ProtoField.bool ("iap2.link.control.eak","EAK - Extended Ack Payload", 8, nil, 0x20)
local f_link_ctrl_rst = ProtoField.bool ("iap2.link.control.rst","RST - Link Reset", 8, nil, 0x10)
local f_link_ctrl_slp = ProtoField.bool ("iap2.link.control.slp","SLP - Device Sleep", 8, nil, 0x08)
local f_link_seq = ProtoField.uint8 ("iap2.link.seq", "Sequence Number", base.DEC)
local f_link_ack = ProtoField.uint8 ("iap2.link.ack", "Acknowledgement Number", base.DEC)
local f_link_session_id = ProtoField.uint8 ("iap2.link.session_id", "Session Identifier", base.DEC)
local f_link_hdr_cksum = ProtoField.uint8 ("iap2.link.hdr_cksum", "Header Checksum", base.HEX)
local f_link_hdr_cksum_status = ProtoField.string("iap2.link.hdr_cksum_status", "[Header Checksum Status]")
local f_link_pkt_type = ProtoField.string("iap2.link.pkt_type", "[Packet Type]")
-- Link Synchronization Payload
local f_syn_version = ProtoField.uint8 ("iap2.syn.version", "Link Version", base.DEC)
local f_syn_max_out = ProtoField.uint8 ("iap2.syn.max_outstanding", "Max Outstanding Packets", base.DEC)
local f_syn_max_len = ProtoField.uint16("iap2.syn.max_length", "Max Received Packet Length", base.DEC)
local f_syn_retx_to = ProtoField.uint16("iap2.syn.retx_timeout","Retransmission Timeout (ms)", base.DEC)
local f_syn_cumack_to = ProtoField.uint16("iap2.syn.cumack_timeout","Cumulative Ack Timeout (ms)",base.DEC)
local f_syn_max_retx = ProtoField.uint8 ("iap2.syn.max_retx", "Max Retransmissions", base.DEC)
local f_syn_max_cumack = ProtoField.uint8 ("iap2.syn.max_cumack", "Max Cumulative Acks", base.DEC)
local f_syn_link_mode = ProtoField.string("iap2.syn.link_mode", "[Link Mode]")
-- Session entries within SYN
local f_syn_sess_id = ProtoField.uint8 ("iap2.syn.session.id", "Session Identifier", base.DEC)
local f_syn_sess_type = ProtoField.uint8 ("iap2.syn.session.type","Session Type", base.DEC)
local f_syn_sess_ver = ProtoField.uint8 ("iap2.syn.session.ver", "Session Version", base.DEC)
local f_syn_sess_type_str = ProtoField.string("iap2.syn.session.type_str", "[Session Type Name]")
-- Extended Acknowledgement Payload
local f_eak_seq = ProtoField.uint8 ("iap2.eak.seq", "Out-of-Sequence PSN", base.DEC)
-- Payload checksum
local f_payload_cksum = ProtoField.uint8 ("iap2.payload_cksum", "Payload Checksum", base.HEX)
local f_payload_cksum_status = ProtoField.string("iap2.payload_cksum_status", "[Payload Checksum Status]")
-- ============================================================
-- Control Session Message Fields
-- ============================================================
local f_cs_msg_sop = ProtoField.uint16("iap2.cs.sop", "Start of Message", base.HEX)
local f_cs_msg_length = ProtoField.uint16("iap2.cs.length", "Message Length", base.DEC)
local f_cs_msg_id = ProtoField.uint16("iap2.cs.id", "Message ID", base.HEX)
local f_cs_msg_name = ProtoField.string("iap2.cs.name", "[Message Name]")
local f_cs_msg_source = ProtoField.string("iap2.cs.source", "[Source]")
-- Parameter fields
local f_cs_param_length = ProtoField.uint16("iap2.cs.param.length","Parameter Length", base.DEC)
local f_cs_param_id = ProtoField.uint16("iap2.cs.param.id", "Parameter ID", base.HEX)
local f_cs_param_name = ProtoField.string("iap2.cs.param.name", "[Parameter Name]")
local f_cs_param_data = ProtoField.bytes ("iap2.cs.param.data", "Parameter Data")
local f_cs_param_str = ProtoField.string("iap2.cs.param.str", "Value (UTF-8)")
local f_cs_param_uint8 = ProtoField.uint8 ("iap2.cs.param.uint8","Value (uint8)", base.DEC)
local f_cs_param_uint16 = ProtoField.uint16("iap2.cs.param.uint16","Value (uint16)", base.DEC)
local f_cs_param_uint32 = ProtoField.uint32("iap2.cs.param.uint32","Value (uint32)", base.DEC)
local f_cs_param_uint64 = ProtoField.uint64("iap2.cs.param.uint64","Value (uint64)", base.DEC)
local f_cs_param_int8 = ProtoField.int8 ("iap2.cs.param.int8", "Value (int8)", base.DEC)
local f_cs_param_int16 = ProtoField.int16 ("iap2.cs.param.int16","Value (int16)", base.DEC)
local f_cs_param_int32 = ProtoField.int32 ("iap2.cs.param.int32","Value (int32)", base.DEC)
local f_cs_param_bool = ProtoField.uint8 ("iap2.cs.param.bool", "Value (bool)", base.DEC)
-- ============================================================
-- File Transfer Session Fields
-- ============================================================
local f_ft_id = ProtoField.uint8 ("iap2.ft.id", "FileTransfer Identifier", base.DEC)
local f_ft_type = ProtoField.uint8 ("iap2.ft.type", "Datagram Type", base.HEX)
local f_ft_type_name = ProtoField.string("iap2.ft.type_name", "[Datagram Name]")
local f_ft_filesize = ProtoField.uint64("iap2.ft.filesize", "File Size", base.DEC)
local f_ft_filetype = ProtoField.uint16("iap2.ft.filetype", "File Type", base.HEX)
local f_ft_filetype_name= ProtoField.string("iap2.ft.filetype_name","[File Type Name]")
local f_ft_setup_data = ProtoField.bytes ("iap2.ft.setup_data", "File Type Setup Data")
local f_ft_payload = ProtoField.bytes ("iap2.ft.payload", "Transfer Data")
-- ============================================================
-- External Accessory Session Fields
-- ============================================================
local f_ea_sess_id = ProtoField.uint16("iap2.ea.session_id", "EA Session Identifier", base.DEC)
local f_ea_data = ProtoField.bytes ("iap2.ea.data", "EA Session Data")
-- Register all fields
iap2_proto.fields = {
f_link_sop, f_link_length, f_link_ctrl,
f_link_ctrl_syn, f_link_ctrl_ack, f_link_ctrl_eak, f_link_ctrl_rst, f_link_ctrl_slp,
f_link_seq, f_link_ack, f_link_session_id,
f_link_hdr_cksum, f_link_hdr_cksum_status, f_link_pkt_type,
f_syn_version, f_syn_max_out, f_syn_max_len, f_syn_retx_to,
f_syn_cumack_to, f_syn_max_retx, f_syn_max_cumack, f_syn_link_mode,
f_syn_sess_id, f_syn_sess_type, f_syn_sess_ver, f_syn_sess_type_str,
f_eak_seq,
f_payload_cksum, f_payload_cksum_status,
f_cs_msg_sop, f_cs_msg_length, f_cs_msg_id, f_cs_msg_name, f_cs_msg_source,
f_cs_param_length, f_cs_param_id, f_cs_param_name, f_cs_param_data,
f_cs_param_str, f_cs_param_uint8, f_cs_param_uint16, f_cs_param_uint32,
f_cs_param_uint64, f_cs_param_int8, f_cs_param_int16, f_cs_param_int32,
f_cs_param_bool,
f_ft_id, f_ft_type, f_ft_type_name, f_ft_filesize, f_ft_filetype,
f_ft_filetype_name, f_ft_setup_data, f_ft_payload,
f_ea_sess_id, f_ea_data,
}
-- ============================================================
-- Lookup tables
-- ============================================================
-- control byte — human-readable packet type
local function ctrl_to_pkt_type(ctrl)
local syn = bit.band(ctrl, 0x80) ~= 0
local ack = bit.band(ctrl, 0x40) ~= 0
local eak = bit.band(ctrl, 0x20) ~= 0
local rst = bit.band(ctrl, 0x10) ~= 0
local slp = bit.band(ctrl, 0x08) ~= 0
if rst then return "RST" end
if slp then return "SLP" end
if syn and ack then return "SYN+ACK" end
if syn then return "SYN" end
if eak then return "EAK" end
if ack then return "ACK" end
return "Unknown"
end
-- session types
local SESSION_TYPE_NAMES = {
[0] = "Control",
[1] = "File Transfer",
[2] = "External Accessory (EA)",
}
-- Control Session message table: id -> {name, source}
local CONTROL_MESSAGES = {
[0xAA00] = { "RequestAuthenticationCertificate", "Device" },
[0xAA01] = { "AuthenticationCertificate", "Accessory" },
[0xAA02] = { "RequestAuthenticationChallengeResponse", "Device" },
[0xAA03] = { "AuthenticationResponse", "Accessory" },
[0xAA04] = { "AuthenticationFailed", "Device" },
[0xAA05] = { "AuthenticationSucceeded", "Device" },
[0xAA06] = { "AccessoryAuthenticationSerialNumber", "Accessory" },
[0xAA10] = { "RequestDeviceAuthenticationCertificate", "Accessory" },
[0xAA11] = { "DeviceAuthenticationCertificate", "Device" },
[0xAA12] = { "RequestDeviceAuthenticationChallengeResponse","Accessory"},
[0xAA13] = { "DeviceAuthenticationResponse", "Device" },
[0xAA14] = { "DeviceAuthenticationFailed", "Accessory" },
[0xAA15] = { "DeviceAuthenticationSucceeded", "Accessory" },
[0x1D00] = { "StartIdentification", "Device" },
[0x1D01] = { "IdentificationInformation", "Accessory" },
[0x1D02] = { "IdentificationAccepted", "Device" },
[0x1D03] = { "IdentificationRejected", "Device" },
[0x1D05] = { "CancelIdentification", "Accessory" },
[0x1D06] = { "IdentificationInformationUpdate", "Accessory" },
[0xEA00] = { "StartExternalAccessoryProtocolSession", "Device" },
[0xEA01] = { "StopExternalAccessoryProtocolSession", "Device" },
[0xEA02] = { "RequestAppLaunch", "Accessory" },
[0xEA03] = { "StatusExternalAccessoryProtocolSession", "Accessory" },
[0xAD00] = { "StartAppDiscoveryUpdates", "Accessory" },
[0xAD01] = { "AppDiscoveryUpdate", "Device" },
[0xAD02] = { "StopAppDiscoveryUpdates", "Accessory" },
[0x5400] = { "StartAssistiveTouch", "Accessory" },
[0x5401] = { "StopAssistiveTouch", "Accessory" },
[0x5402] = { "StartAssistiveTouchInformation", "Accessory" },
[0x5403] = { "AssistiveTouchInformation", "Device" },
[0x5404] = { "StopAssistiveTouchInformation", "Accessory" },
[0x4E01] = { "BluetoothComponentInformation", "Accessory" },
[0x4E03] = { "StartBluetoothConnectionUpdates", "Accessory" },
[0x4E04] = { "BluetoothConnectionUpdate", "Device" },
[0x4E05] = { "StopBluetoothConnectionUpdates", "Accessory" },
[0x4E09] = { "DeviceInformationUpdate", "Device" },
[0x4E0A] = { "DeviceLanguageUpdate", "Device" },
[0x4E0B] = { "DeviceTimeUpdate", "Device" },
[0x4E0C] = { "DeviceUUIDUpdate", "Device" },
[0x4E0D] = { "WirelessCarPlayUpdate", "Device" },
[0x4E0E] = { "DeviceTransportIdentifierNotification", "Device" },
[0x4154] = { "StartCallStateUpdates", "Accessory" },
[0x4155] = { "CallStateUpdate", "Device" },
[0x4156] = { "StopCallStateUpdates", "Accessory" },
[0x4157] = { "StartCommunicationsUpdates", "Accessory" },
[0x4158] = { "CommunicationsUpdate", "Device" },
[0x4159] = { "StopCommunicationsUpdates", "Accessory" },
[0x415A] = { "InitiateCall", "Accessory" },
[0x415B] = { "AcceptCall", "Accessory" },
[0x415C] = { "EndCall", "Accessory" },
[0x415D] = { "SwapCalls", "Accessory" },
[0x415E] = { "MergeCalls", "Accessory" },
[0x415F] = { "HoldStatusUpdate", "Accessory" },
[0x4160] = { "MuteStatusUpdate", "Accessory" },
[0x4161] = { "SendDTMF", "Accessory" },
[0x4C00] = { "StartMediaLibraryInformation", "Accessory" },
[0x4C01] = { "MediaLibraryInformation", "Device" },
[0x4C02] = { "StopMediaLibraryInformation", "Accessory" },
[0x4C03] = { "StartMediaLibraryUpdates", "Accessory" },
[0x4C04] = { "MediaLibraryUpdate", "Device" },
[0x4C05] = { "StopMediaLibraryUpdates", "Accessory" },
[0x4C06] = { "PlayMediaLibraryCurrentSelection", "Accessory" },
[0x4C07] = { "PlayMediaLibraryItems", "Accessory" },
[0x4C08] = { "PlayMediaLibraryCollection", "Accessory" },
[0x4C09] = { "PlayMediaLibrarySpecial", "Accessory" },
[0x5000] = { "StartNowPlayingUpdates", "Accessory" },
[0x5001] = { "NowPlayingUpdate", "Device" },
[0x5002] = { "StopNowPlayingUpdates", "Accessory" },
[0x5003] = { "SetNowPlayingInformation", "Accessory" },
[0xAE00] = { "StartPowerUpdates", "Accessory" },
[0xAE01] = { "PowerUpdate", "Device" },
[0xAE02] = { "StopPowerUpdates", "Accessory" },
[0xAE03] = { "PowerSourceUpdate", "Accessory" },
[0x6800] = { "StartHID", "Accessory" },
[0x6801] = { "DeviceHIDReport", "Device" },
[0x6802] = { "AccessoryHIDReport", "Accessory" },
[0x6803] = { "StopHID", "Accessory" },
[0x6806] = { "StartNativeHID", "Device" },
[0xFFFA] = { "StartLocationInformation", "Device" },
[0xFFFB] = { "LocationInformation", "Accessory" },
[0xFFFC] = { "StopLocationInformation", "Device" },
[0xDA00] = { "StartUSBDeviceModeAudio", "Accessory" },
[0xDA01] = { "USBDeviceModeAudioInformation", "Device" },
[0xDA02] = { "StopUSBDeviceModeAudio", "Accessory" },
[0x5601] = { "RequestVoiceOverMoveCursor", "Accessory" },
[0x5602] = { "RequestVoiceOverActivateCursor", "Accessory" },
[0x5603] = { "RequestVoiceOverScrollPage", "Accessory" },
[0x5606] = { "RequestVoiceOverSpeakText", "Accessory" },
[0x5608] = { "RequestVoiceOverPauseText", "Accessory" },
[0x5609] = { "RequestVoiceOverResumeText", "Accessory" },
[0x560B] = { "StartVoiceOverUpdates", "Accessory" },
[0x560C] = { "VoiceOverUpdate", "Device" },
[0x560D] = { "StopVoiceOverUpdates", "Accessory" },
[0x560E] = { "RequestVoiceOverConfiguration", "Accessory" },
[0x560F] = { "StartVoiceOverCursorUpdates", "Accessory" },
[0x5610] = { "VoiceOverCursorUpdate", "Device" },
[0x5611] = { "StopVoiceOverCursorUpdates", "Accessory" },
[0x5612] = { "StartVoiceOver", "Accessory" },
[0x5613] = { "StopVoiceOver", "Accessory" },
[0x5700] = { "RequestWiFiInformation", "Accessory" },
[0x5701] = { "WiFiInformation", "Device" },
[0x5702] = { "RequestAccessoryWiFiConfigurationInformation","Device" },
[0x5703] = { "AccessoryWiFiConfigurationInformation", "Accessory" },
[0x0B00] = { "StartBluetoothPairing", "Device" },
[0x0B01] = { "BluetoothPairingAccessoryInformation", "Accessory" },
[0x0B02] = { "BluetoothPairingStatus", "Accessory" },
[0x0B03] = { "StopBluetoothPairing", "Device" },
[0xA100] = { "StartVehicleStatusUpdates", "Device" },
[0xA101] = { "VehicleStatusUpdate", "Accessory" },
[0xA102] = { "StopVehicleStatusUpdates", "Device" },
[0x5200] = { "StartRouteGuidanceUpdates", "Accessory" },
[0x5201] = { "RouteGuidanceUpdate", "Device" },
[0x5202] = { "RouteGuidanceManeuverUpdate", "Device" },
[0x5203] = { "StopRouteGuidanceUpdates", "Accessory" },
}
-- File Transfer datagram types (byte 1)
local FT_DATAGRAM_NAMES = {
[0x00] = "Data",
[0x01] = "Start",
[0x02] = "Cancel",
[0x03] = "Pause",
[0x04] = "Setup",
[0x05] = "Success",
[0x06] = "Failure",
[0x40] = "LastData",
[0x80] = "FirstData",
[0xC0] = "FirstAndOnlyData",
}
-- File types (v2 Setup, bytes 10-11)
local FT_FILE_TYPE_NAMES = {
[0x0000] = "Reserved",
[0x0001] = "CallStateUpdateVCard",
[0x0002] = "NowPlayingArtworkData",
[0x0003] = "NowPlayingPlaybackQueueContents",
[0x0004] = "MediaLibraryUpdatePlaylistContents",
[0x0006] = "MediaItemListNowPlayingPlaybackQueueContents",
[0x0007] = "MediaItemListMediaLibraryUpdatePlaylistContents",
[0x0008] = "AppDiscoveryIconData",
}
-- ============================================================
-- Checksum helper
-- sum all bytes, two's complement mod 256 → result + sum == 0x00
-- ============================================================
local function checksum_ok(tvb, offset, length, stored)
-- verify: sum of all bytes in range + stored checksum == 0 mod 256
local total = 0
for i = offset, offset + length - 1 do
total = total + tvb:range(i, 1):uint()
end
total = total + stored
return bit.band(total, 0xFF) == 0
end
-- ============================================================
-- Control Session dissector
-- Dissects control session payload starting at offset within tvb.
-- ============================================================
local function dissect_control_session(tvb, pinfo, tree, offset, length)
local end_offset = offset + length
while offset < end_offset do
-- Need at least 6 bytes for SOM(2) + MsgLen(2) + MsgID(2)
if offset + 6 > end_offset then break end
local msg_start = offset
local som = tvb:range(offset, 2):uint()
if som ~= 0x4040 then
-- Not a valid control session SOM; show raw remainder
tree:add(iap2_proto, tvb:range(offset, end_offset - offset),
"[Unrecognized control session data]")
break
end
local msg_len = tvb:range(offset + 2, 2):uint()
local msg_id = tvb:range(offset + 4, 2):uint()
-- Guard: msg_len includes its own 6-byte header; minimum is 6
if msg_len < 6 or offset + msg_len > end_offset then break end
local msg_info = CONTROL_MESSAGES[msg_id]
local msg_name = msg_info and msg_info[1] or string.format("Unknown (0x%04X)", msg_id)
local msg_src = msg_info and msg_info[2] or "?"
-- Top-level message subtree
local msg_tree = tree:add(iap2_proto, tvb:range(offset, msg_len),
string.format("Message: %s (0x%04X)", msg_name, msg_id))
msg_tree:add(f_cs_msg_sop, tvb:range(offset, 2))
msg_tree:add(f_cs_msg_length, tvb:range(offset + 2, 2))
msg_tree:add(f_cs_msg_id, tvb:range(offset + 4, 2))
msg_tree:add(f_cs_msg_name, msg_name)
msg_tree:add(f_cs_msg_source, msg_src)
-- Dissect parameters
local param_offset = offset + 6
local param_end = offset + msg_len
while param_offset < param_end do
if param_offset + 4 > param_end then break end
local p_len = tvb:range(param_offset, 2):uint()
local p_id = tvb:range(param_offset + 2, 2):uint()
-- p_len includes its own 4-byte header; minimum for void/none is 4
if p_len < 4 or param_offset + p_len > param_end then break end
local p_data_len = p_len - 4
local param_tree = msg_tree:add(iap2_proto,
tvb:range(param_offset, p_len),
string.format("Parameter 0x%04X (%d bytes)", p_id, p_data_len))
param_tree:add(f_cs_param_length, tvb:range(param_offset, 2))
param_tree:add(f_cs_param_id, tvb:range(param_offset + 2, 2))
-- Attempt best-effort value display based on data size
if p_data_len > 0 then
local data_off = param_offset + 4
-- Show UTF-8 strings and blobs heuristically
if p_data_len == 1 then
param_tree:add(f_cs_param_uint8, tvb:range(data_off, 1))
elseif p_data_len == 2 then
param_tree:add(f_cs_param_uint16, tvb:range(data_off, 2))
elseif p_data_len == 4 then
param_tree:add(f_cs_param_uint32, tvb:range(data_off, 4))
elseif p_data_len == 8 then
param_tree:add(f_cs_param_uint64, tvb:range(data_off, 8))
else
-- Try to detect null-terminated UTF-8 string
local raw = tvb:range(data_off, p_data_len):bytes()
local is_printable = true
for i = 0, p_data_len - 1 do
local b = raw:get_index(i)
if b == 0 then
-- null terminator found; valid string up to here
break
end
if b < 0x20 and b ~= 0x09 and b ~= 0x0A and b ~= 0x0D then
is_printable = false
break
end
end
if is_printable then
param_tree:add(f_cs_param_str, tvb:range(data_off, p_data_len))
else
param_tree:add(f_cs_param_data, tvb:range(data_off, p_data_len))
end
end
end
param_offset = param_offset + p_len
end
offset = offset + msg_len
end
end
-- ============================================================
-- File Transfer Session dissector
-- ============================================================
local function dissect_file_transfer_session(tvb, pinfo, tree, offset, length)
if length < 2 then
tree:add(iap2_proto, tvb:range(offset, length), "[File Transfer: too short]")
return
end
local ft_id = tvb:range(offset, 1):uint()
local ft_type_val = tvb:range(offset + 1, 1):uint()
local type_name = FT_DATAGRAM_NAMES[ft_type_val]
or string.format("Unknown (0x%02X)", ft_type_val)
local ft_tree = tree:add(iap2_proto, tvb:range(offset, length),
string.format("File Transfer: %s (ID %d)", type_name, ft_id))
ft_tree:add(f_ft_id, tvb:range(offset, 1))
ft_tree:add(f_ft_type, tvb:range(offset + 1, 1))
ft_tree:add(f_ft_type_name, type_name)
-- Setup datagram: bytes 2-9 are file size (8 bytes big-endian)
if ft_type_val == 0x04 and length >= 10 then
ft_tree:add(f_ft_filesize, tvb:range(offset + 2, 8))
-- Version 2 Setup: bytes 10-11 are file type
if length >= 12 then
local file_type_val = tvb:range(offset + 10, 2):uint()
local file_type_name = FT_FILE_TYPE_NAMES[file_type_val]
or string.format("Unknown (0x%04X)", file_type_val)
ft_tree:add(f_ft_filetype, tvb:range(offset + 10, 2))
ft_tree:add(f_ft_filetype_name, file_type_name)
if length > 12 then
ft_tree:add(f_ft_setup_data, tvb:range(offset + 12, length - 12))
end
end
-- Data-bearing datagrams: bytes 2+ are payload
elseif (ft_type_val == 0x00 or ft_type_val == 0x40 or
ft_type_val == 0x80 or ft_type_val == 0xC0) and length > 2 then
ft_tree:add(f_ft_payload, tvb:range(offset + 2, length - 2))
end
end
-- ============================================================
-- External Accessory Session dissector
-- ============================================================
local function dissect_ea_session(tvb, pinfo, tree, offset, length)
if length < 2 then
tree:add(iap2_proto, tvb:range(offset, length), "[EA Session: too short]")
return
end
local ea_sess_id = tvb:range(offset, 2):uint()
local ea_tree = tree:add(iap2_proto, tvb:range(offset, length),
string.format("EA Session: ID %d (%d bytes data)", ea_sess_id, length - 2))
ea_tree:add(f_ea_sess_id, tvb:range(offset, 2))
if length > 2 then
ea_tree:add(f_ea_data, tvb:range(offset + 2, length - 2))
end
end
-- ============================================================
-- Link Synchronization Payload dissector
-- ============================================================
local function dissect_syn_payload(tvb, pinfo, tree, offset, length)
local syn_tree = tree:add(iap2_proto, tvb:range(offset, length),
"Link Synchronization Payload")
-- Fixed 10-byte header
if length < 10 then
syn_tree:add(iap2_proto, tvb:range(offset, length), "[SYN payload too short]")
return
end
syn_tree:add(f_syn_version, tvb:range(offset, 1))
syn_tree:add(f_syn_max_out, tvb:range(offset + 1, 1))
syn_tree:add(f_syn_max_len, tvb:range(offset + 2, 2))
syn_tree:add(f_syn_retx_to, tvb:range(offset + 4, 2))
syn_tree:add(f_syn_cumack_to, tvb:range(offset + 6, 2))
syn_tree:add(f_syn_max_retx, tvb:range(offset + 8, 1))
syn_tree:add(f_syn_max_cumack, tvb:range(offset + 9, 1))
-- Derived: ZeroACK mode if all four negotiable timeout/retx fields are 0
local retx_to = tvb:range(offset + 4, 2):uint()
local cumack_to = tvb:range(offset + 6, 2):uint()
local max_retx = tvb:range(offset + 8, 1):uint()
local max_cumack= tvb:range(offset + 9, 1):uint()
if retx_to == 0 and cumack_to == 0 and max_retx == 0 and max_cumack == 0 then
syn_tree:add(f_syn_link_mode, "[ZeroACK / ZeroRetransmit mode]")
else
syn_tree:add(f_syn_link_mode, "[Standard ACK mode]")
end
-- Session entries: 3 bytes each, zero or more
local sess_off = offset + 10
local sess_end = offset + length
local sess_num = 1
while sess_off + 3 <= sess_end do
local s_id = tvb:range(sess_off, 1):uint()
local s_type = tvb:range(sess_off + 1, 1):uint()
local s_ver = tvb:range(sess_off + 2, 1):uint()
local s_type_name = SESSION_TYPE_NAMES[s_type]
or string.format("Unknown (%d)", s_type)
local sess_tree = syn_tree:add(iap2_proto, tvb:range(sess_off, 3),
string.format("Session %d: ID=%d Type=%s Ver=%d",
sess_num, s_id, s_type_name, s_ver))
sess_tree:add(f_syn_sess_id, tvb:range(sess_off, 1))
sess_tree:add(f_syn_sess_type, tvb:range(sess_off + 1, 1))
sess_tree:add(f_syn_sess_type_str, s_type_name)
sess_tree:add(f_syn_sess_ver, tvb:range(sess_off + 2, 1))
sess_off = sess_off + 3
sess_num = sess_num + 1
end
end
-- ============================================================
-- Extended Acknowledgement Payload dissector
-- ============================================================
local function dissect_eak_payload(tvb, pinfo, tree, offset, length)
local eak_tree = tree:add(iap2_proto, tvb:range(offset, length),
"Extended Acknowledgement Payload")
for i = 0, length - 1 do
eak_tree:add(f_eak_seq, tvb:range(offset + i, 1))
end
end
-- ============================================================
-- Session type state — track session ID → type from SYN packets
-- Keyed by conversation; simple global map is fine for most captures.
-- Values shown in [brackets] are derived from earlier SYN frames.
-- ============================================================
local session_type_map = {} -- [conv_id][session_id] = session_type
local function get_session_type(pinfo, session_id)
local conv = tostring(pinfo.net_src) .. "-" .. tostring(pinfo.net_dst)
if session_type_map[conv] then
return session_type_map[conv][session_id]
end
return nil
end
local function store_session_types(pinfo, sessions)
-- sessions: list of {id, type}
local conv = tostring(pinfo.net_src) .. "-" .. tostring(pinfo.net_dst)
if not session_type_map[conv] then
session_type_map[conv] = {}
end
for _, s in ipairs(sessions) do
session_type_map[conv][s.id] = s.sess_type
end
end
-- ============================================================
-- Info-column helpers
-- ============================================================
-- Short session-type prefix for the Info column
local SESSION_TAG = { [0] = "[CS]", [1] = "[FT]", [2] = "[EA]" }
-- Scan a control session payload and return a comma-joined list of message names.
local function summarise_control_messages(tvb, offset, length)
local names = {}
local end_offset = offset + length
while offset + 6 <= end_offset do
if tvb:range(offset, 2):uint() ~= 0x4040 then break end
local msg_len = tvb:range(offset + 2, 2):uint()
local msg_id = tvb:range(offset + 4, 2):uint()
if msg_len < 6 or offset + msg_len > end_offset then break end
local info = CONTROL_MESSAGES[msg_id]
if info then
table.insert(names, info[1])
else
table.insert(names, string.format("Unknown (0x%04X)", msg_id))
end
offset = offset + msg_len
end
return #names > 0 and table.concat(names, ", ") or nil
end
-- Summarise a file-transfer datagram for the Info column.
local function summarise_ft_datagram(tvb, offset, length)
if length < 2 then return nil end
local ft_id = tvb:range(offset, 1):uint()
local ft_type = tvb:range(offset + 1, 1):uint()
local name = FT_DATAGRAM_NAMES[ft_type]
or string.format("Unknown (0x%02X)", ft_type)
return string.format("%s (ID %d)", name, ft_id)
end
-- ============================================================
-- Main dissector
-- ============================================================
-- Tracks the last frame number we wrote to the info column.
-- Used to detect when we are called a second time for the same
-- frame (two TLS records in one TCP packet) and should append
-- rather than overwrite.
iap2_written_frame = -1
iap2_written_frame_info = ""
function iap2_proto.dissector(tvb, pinfo, tree)
local length = tvb:len()
if length < 9 then return 0 end
local offset = 0
local pkt_count = 0
-- Collect info-column fragments across all link packets in this segment
local info_parts = {}
-- A single TCP segment may contain multiple iAP2 link packets
while offset + 9 <= length do
local sop = tvb:range(offset, 2):uint()
if sop ~= 0xFF5A then break end
local pkt_len = tvb:range(offset + 2, 2):uint()
-- Minimum packet: 9-byte header; Maximum: 65535 bytes
if pkt_len < 9 or offset + pkt_len > length then break end
pkt_count = pkt_count + 1
-- ── Layer 1: iAP2 Link ────────────────────────────────────────
local ctrl = tvb:range(offset + 4, 1):uint()
local seq = tvb:range(offset + 5, 1):uint()
local ack_num = tvb:range(offset + 6, 1):uint()
local session_id = tvb:range(offset + 7, 1):uint()
local hdr_cksum = tvb:range(offset + 8, 1):uint()
local pkt_type = ctrl_to_pkt_type(ctrl)
local has_payload = pkt_len > 9
local pkt_label
if has_payload then
pkt_label = string.format("iAP2 Link %s seq=%d ack=%d sid=%d [%d bytes payload]",
pkt_type, seq, ack_num, session_id, pkt_len - 9 - 1)
else
pkt_label = string.format("iAP2 Link %s seq=%d ack=%d", pkt_type, seq, ack_num)
end
local link_tree = tree:add(iap2_proto, tvb:range(offset, pkt_len), pkt_label)
-- Header fields in wire order
link_tree:add(f_link_sop, tvb:range(offset, 2))
link_tree:add(f_link_length, tvb:range(offset + 2, 2))
local ctrl_tree = link_tree:add(f_link_ctrl, tvb:range(offset + 4, 1))
ctrl_tree:add(f_link_ctrl_syn, tvb:range(offset + 4, 1))
ctrl_tree:add(f_link_ctrl_ack, tvb:range(offset + 4, 1))
ctrl_tree:add(f_link_ctrl_eak, tvb:range(offset + 4, 1))
ctrl_tree:add(f_link_ctrl_rst, tvb:range(offset + 4, 1))
ctrl_tree:add(f_link_ctrl_slp, tvb:range(offset + 4, 1))
link_tree:add(f_link_seq, tvb:range(offset + 5, 1))
link_tree:add(f_link_ack, tvb:range(offset + 6, 1))
link_tree:add(f_link_session_id, tvb:range(offset + 7, 1))
link_tree:add(f_link_hdr_cksum, tvb:range(offset + 8, 1))
-- Derived: header checksum status
local hdr_ok = checksum_ok(tvb, offset, 8, hdr_cksum)
link_tree:add(f_link_hdr_cksum_status,
hdr_ok and "[Header Checksum: OK]" or "[Header Checksum: BAD]")
-- Derived: packet type
link_tree:add(f_link_pkt_type, string.format("[Packet Type: %s]", pkt_type))
-- ── Payload ───────────────────────────────────────────────────
if has_payload then
local payload_start = offset + 9
-- Last byte of packet is payload checksum
local payload_len = pkt_len - 9 - 1
local payload_cksum_off = offset + pkt_len - 1
local payload_cksum = tvb:range(payload_cksum_off, 1):uint()
local pay_ok = checksum_ok(tvb, payload_start, payload_len, payload_cksum)
local syn_flag = bit.band(ctrl, 0x80) ~= 0
local eak_flag = bit.band(ctrl, 0x20) ~= 0
local ack_flag = bit.band(ctrl, 0x40) ~= 0
if syn_flag then
-- ── Layer 1 continuation: SYN payload ─────────
local sessions = {}
if payload_len >= 10 then
local sess_off = payload_start + 10
local sess_end = payload_start + payload_len
while sess_off + 3 <= sess_end do
table.insert(sessions, {
id = tvb:range(sess_off, 1):uint(),
sess_type = tvb:range(sess_off + 1, 1):uint(),
})
sess_off = sess_off + 3
end
end
if not pinfo.visited then
store_session_types(pinfo, sessions)
end
dissect_syn_payload(tvb, pinfo, link_tree, payload_start, payload_len)
table.insert(info_parts, pkt_type)
elseif eak_flag then
-- ── Layer 1: EAK payload ──────────────────────
dissect_eak_payload(tvb, pinfo, link_tree, payload_start, payload_len)
table.insert(info_parts, "EAK")
elseif ack_flag and session_id ~= 0 then
-- ── Layer 2: Session payload ──────────────────────────
local sess_type = get_session_type(pinfo, session_id)
local sess_tag = (sess_type ~= nil and SESSION_TAG[sess_type])
or string.format("[S%d]", session_id)
local sess_type_name = sess_type ~= nil
and (SESSION_TYPE_NAMES[sess_type]
or string.format("Unknown Type %d", sess_type))
or "Unknown [no SYN seen]"
local sess_label = string.format(
"[Session %d: %s] (%d bytes)", session_id, sess_type_name, payload_len)
local sess_tree = link_tree:add(iap2_proto,
tvb:range(payload_start, payload_len), sess_label)
-- Build info-column fragment for this session payload
local summary = nil
if sess_type == 0 then
dissect_control_session(tvb, pinfo, sess_tree,
payload_start, payload_len)
summary = summarise_control_messages(tvb, payload_start, payload_len)
elseif sess_type == 1 then
dissect_file_transfer_session(tvb, pinfo, sess_tree,
payload_start, payload_len)
summary = summarise_ft_datagram(tvb, payload_start, payload_len)
elseif sess_type == 2 then
dissect_ea_session(tvb, pinfo, sess_tree,
payload_start, payload_len)
summary = string.format("EA data (%d bytes)", payload_len - 2)
else
sess_tree:add(iap2_proto,
tvb:range(payload_start, payload_len),
"[Unknown session type — raw data]")
end
if summary then
table.insert(info_parts, sess_tag .. " " .. summary)
else
table.insert(info_parts, sess_tag)
end
else
-- ACK-only (no session payload)
link_tree:add(iap2_proto,
tvb:range(payload_start, payload_len),
string.format("[Payload: %d bytes]", payload_len))
table.insert(info_parts, pkt_type)
end
-- Payload checksum always goes last (wire order)
link_tree:add(f_payload_cksum, tvb:range(payload_cksum_off, 1))
link_tree:add(f_payload_cksum_status,
pay_ok and "[Payload Checksum: OK]" or "[Payload Checksum: BAD]")
else
-- Header-only packets: RST, SLP, bare ACK
table.insert(info_parts, pkt_type)
end
offset = offset + pkt_len
end
if pkt_count == 0 then return 0 end
pinfo.cols.protocol:set("iAP2")
-- this is a stupid hack because TLS dissector keeps appending to my info column
local summary = table.concat(info_parts, " | ")
if iap2_written_frame == pinfo.number then
iap2_written_frame_info = iap2_written_frame_info .. ", " .. summary
pinfo.cols.info:set(iap2_written_frame_info)
else
iap2_written_frame = pinfo.number
iap2_written_frame_info = summary
pinfo.cols.info:set(summary)
end
return offset
end
-- ============================================================
-- Heuristic registration: trigger on 0xFF 0x5A magic.
-- Registers on TCP; also works on any DLT if called manually.
-- ============================================================
local function iap2_heuristic(tvb, pinfo, tree)
if tvb:len() < 4 then return false end
local sop = tvb:range(0, 2):uint()
if sop == 0xFF55 then
pinfo.cols.protocol:set("iAP1")
pinfo.cols.info:set("iAP2 Link Initialization")
return true
end
if sop == 0xFF5A then
-- Normal link packet
if tvb:len() < 9 then return false end
local declared_len = tvb:range(2, 2):uint()
if declared_len < 9 or declared_len > tvb:len() then return false end
iap2_proto.dissector(tvb, pinfo, tree)
return true
end
return false
end
iap2_proto:register_heuristic("tls", iap2_heuristic)
# This file was created by Wireshark. Edit with care.
@iAP2 Unknown CS Message@iap2.cs.name contains "Unknown"@[65535,32125,30840][65535,65535,65278]
@iAP2 Control Session@iap2.cs.id@[65535,65535,65535][14906,24929,42148]
@iAP2 Link Control@iap2@[46003,46003,46003][7967,7967,7967]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment