From 90c91199bf8d3bf469bee5157e24d2de0f003551 Mon Sep 17 00:00:00 2001 From: Xeno Kovah Date: Tue, 28 Apr 2026 17:01:36 -0400 Subject: [PATCH] bluetooth: add missing HCI events, EIR elements, and SMP packet types New packet definitions covering frequently-seen Bluetooth Core 5.4 events and Generic Access Profile data types that scapy did not yet decode: HCI events - HCI_Event_Connection_Request (code=0x04) - HCI_Event_Remote_Host_Supported_Features_Notification (code=0x3d) - HCI_Event_Vendor (vendor-specific debug) (code=0xff) - HCI_LE_Meta_LE_Read_Remote_Features_Complete (LE Meta event=0x04) EIR/AD elements - EIR_RandomTargetAddress (type=0x18) - EIR_LERole (type=0x1c) - EIR_BroadcastName (type=0x30) - EIR_3DInformation (type=0x3d) SMP - SM_Keypress_Notification (sm_command=0x0e) Each packet has an entry in test/scapy/layers/bluetooth.uts with a sample drawn from a real HCI snoop log (BR/EDR Connection Request, Remote Host Supported Features Notification, LE Read Remote Features Complete, vendor-specific debug event, EIR 3D Information, EIR Broadcast Name) and synthetic round-trip build/parse tests for the remainder. --- scapy/layers/bluetooth.py | 106 ++++++++++++++++++++++++++ test/scapy/layers/bluetooth.uts | 130 ++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 2784a515e9f..567f83339b9 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -959,6 +959,17 @@ class SM_DHKey_Check(Packet): fields_desc = [StrFixedLenField("dhkey_check", b'\x00' * 16, 16), ] +class SM_Keypress_Notification(Packet): + name = "Keypress Notification" + fields_desc = [ByteEnumField("notification_type", 0, { + 0: "Passkey entry started", + 1: "Passkey digit entered", + 2: "Passkey digit erased", + 3: "Passkey cleared", + 4: "Passkey entry completed", + })] + + class EIR_Hdr(Packet): name = "EIR Header" fields_desc = [ @@ -1008,6 +1019,8 @@ class EIR_Hdr(Packet): 0x2a: "mesh_message", 0x2b: "mesh_beacon", + 0x30: "broadcast_name", + 0x3d: "3d_information", 0xff: "mfg_specific_data", @@ -1297,6 +1310,13 @@ class EIR_PublicTargetAddress(EIR_Element): ] +class EIR_RandomTargetAddress(EIR_Element): + name = "Random Target Address" + fields_desc = [ + LEMACField('bd_addr', None) + ] + + class EIR_AdvertisingInterval(EIR_Element): name = "Advertising Interval" fields_desc = [ @@ -1328,6 +1348,40 @@ class EIR_LEBluetoothDeviceAddress(EIR_Element): ] +class EIR_LERole(EIR_Element): + name = "LE Role" + fields_desc = [ + ByteEnumField("role", 0, { + 0: "Only Peripheral Role supported", + 1: "Only Central Role supported", + 2: "Peripheral and Central Role supported, " + "Peripheral Role preferred for connection establishment", + 3: "Peripheral and Central Role supported, " + "Central Role preferred for connection establishment", + }), + ] + + +class EIR_BroadcastName(EIR_Element): + name = "Broadcast Name" + fields_desc = [ + StrLenField("broadcast_name", "", + length_from=EIR_Element.length_from) + ] + + +class EIR_3DInformation(EIR_Element): + name = "3D Information" + fields_desc = [ + BitField("factory_test_mode", 0, 1, tot_size=-1), + BitField("reserved", 0, 4), + BitField("send_battery_level_on_startup", 0, 1), + BitField("battery_level_reporting", 0, 1), + BitField("association_notification", 0, 1, end_tot_size=-1), + ByteField("path_loss_threshold", 0), + ] + + class EIR_Appearance(EIR_Element): name = "EIR_Appearance" fields_desc = [ @@ -2459,6 +2513,18 @@ class HCI_Event_Connection_Complete(Packet): 1: "link level encryption enabled", }), ] +class HCI_Event_Connection_Request(Packet): + """ + 7.7.4 Connection Request event + """ + name = "HCI_Connection_Request" + fields_desc = [LEMACField("bd_addr", None), + XLE3BytesField("device_class", 0), + ByteEnumField("link_type", 0, {0: "SCO connection", + 1: "ACL connection", + 2: "eSCO connection", }), ] + + class HCI_Event_Disconnection_Complete(Packet): """ 7.7.5 Disconnection Complete event @@ -2501,6 +2567,17 @@ class HCI_Event_Read_Remote_Supported_Features_Complete(Packet): ] +class HCI_Event_Remote_Host_Supported_Features_Notification(Packet): + """ + 7.7.50 Remote Host Supported Features Notification event + """ + name = "HCI_Remote_Host_Supported_Features_Notification" + fields_desc = [ + LEMACField('bd_addr', None), + FlagsField('host_supported_features', 0, -64, _bluetooth_features) + ] + + class HCI_Event_Read_Remote_Version_Information_Complete(Packet): """ 7.7.12 Read Remote Version Information Complete event @@ -2637,6 +2714,19 @@ class HCI_Event_IO_Capability_Response(Packet): ] +class HCI_Event_Vendor(Packet): + """ + Vendor-Specific Debug event (event code 0xFF). + + Bluetooth Core 5.4, Vol 4, Part E, section 5.4.4 reserves 0xFF for + vendor-specific debugging events; the format of the parameters is + vendor-defined, so the data is exposed as a raw byte string. + """ + name = "HCI_Vendor_Specific" + fields_desc = [StrLenField("data", b"", + length_from=lambda pkt: pkt.underlayer.len)] + + class HCI_Event_LE_Meta(Packet): """ 7.7.65 LE Meta event @@ -2781,6 +2871,13 @@ class HCI_LE_Meta_Connection_Update_Complete(Packet): LEShortField("timeout", 42), ] +class HCI_LE_Meta_LE_Read_Remote_Features_Complete(Packet): + name = "LE Read Remote Features Complete" + fields_desc = [ByteEnumField("status", 0, _bluetooth_error_codes), + LEShortField("handle", 0), + XLELongField("le_features", 0)] + + class HCI_LE_Meta_Advertising_Report(Packet): name = "Advertising Report" fields_desc = [ByteEnumField("type", 0, {0: "conn_und", 4: "scan_rsp"}), @@ -2993,6 +3090,7 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Complete, code=0x01) bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Result, code=0x02) bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Complete, code=0x03) +bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Request, code=0x04) bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x08) @@ -3006,7 +3104,9 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_Hdr, HCI_Event_Read_Remote_Extended_Features_Complete, code=0x23) bind_layers(HCI_Event_Hdr, HCI_Event_Extended_Inquiry_Result, code=0x2f) bind_layers(HCI_Event_Hdr, HCI_Event_IO_Capability_Response, code=0x32) +bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Host_Supported_Features_Notification, code=0x3d) # noqa: E501 bind_layers(HCI_Event_Hdr, HCI_Event_LE_Meta, code=0x3e) +bind_layers(HCI_Event_Hdr, HCI_Event_Vendor, code=0xff) bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Name, opcode=0x0c14) # noqa: E501 bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Version_Information, opcode=0x1001) # noqa: E501 @@ -3018,6 +3118,7 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Enhanced_Connection_Complete, event=0x0a) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Advertising_Reports, event=0x02) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Update_Complete, event=0x03) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_LE_Read_Remote_Features_Complete, event=0x04) # noqa: E501 bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Long_Term_Key_Request, event=0x05) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Extended_Advertising_Reports, event=0x0d) @@ -3041,12 +3142,16 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(EIR_Hdr, EIR_ServiceSolicitation128BitUUID, type=0x15) bind_layers(EIR_Hdr, EIR_ServiceData16BitUUID, type=0x16) bind_layers(EIR_Hdr, EIR_PublicTargetAddress, type=0x17) +bind_layers(EIR_Hdr, EIR_RandomTargetAddress, type=0x18) bind_layers(EIR_Hdr, EIR_Appearance, type=0x19) bind_layers(EIR_Hdr, EIR_AdvertisingInterval, type=0x1a) bind_layers(EIR_Hdr, EIR_LEBluetoothDeviceAddress, type=0x1b) +bind_layers(EIR_Hdr, EIR_LERole, type=0x1c) bind_layers(EIR_Hdr, EIR_ServiceData32BitUUID, type=0x20) bind_layers(EIR_Hdr, EIR_ServiceData128BitUUID, type=0x21) bind_layers(EIR_Hdr, EIR_URI, type=0x24) +bind_layers(EIR_Hdr, EIR_BroadcastName, type=0x30) +bind_layers(EIR_Hdr, EIR_3DInformation, type=0x3d) bind_layers(EIR_Hdr, EIR_Manufacturer_Specific_Data, type=0xff) bind_layers(EIR_Hdr, EIR_Raw) @@ -3121,6 +3226,7 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(SM_Hdr, SM_Security_Request, sm_command=0x0b) bind_layers(SM_Hdr, SM_Public_Key, sm_command=0x0c) bind_layers(SM_Hdr, SM_DHKey_Check, sm_command=0x0d) +bind_layers(SM_Hdr, SM_Keypress_Notification, sm_command=0x0e) ############### diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 124cc974b51..0aafc3fcb0a 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -375,6 +375,29 @@ assert evt_pkt[HCI_Event_Connection_Complete].bd_addr == "54:b7:e5:91:34:09" assert evt_pkt[HCI_Event_Connection_Complete].link_type == 1 assert evt_pkt[HCI_Event_Connection_Complete].encryption_enabled == 0 += Connection Request +# Sample from a real BR/EDR controller HCI snoop log. Event 0x04 is sent by +# the controller when a remote BR/EDR/SCO device tries to connect. +# Layout: HCI_Hdr type=0x04, event code 0x04, len 0x0a, BD_ADDR (6 LE bytes), +# device_class (3 LE bytes), link_type (1 byte). +evt_raw_data = hex_bytes("04" "04" "0a" "8c5b6fd28330" "0c025a" "01") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Connection_Request in evt_pkt +assert evt_pkt[HCI_Event_Connection_Request].bd_addr == "30:83:d2:6f:5b:8c" +assert evt_pkt[HCI_Event_Connection_Request].device_class == 0x5a020c +assert evt_pkt[HCI_Event_Connection_Request].link_type == 1 # ACL connection +assert raw(evt_pkt) == evt_raw_data + +# Roundtrip build +built = HCI_Hdr() / HCI_Event_Hdr() / HCI_Event_Connection_Request( + bd_addr="aa:bb:cc:dd:ee:ff", device_class=0x240404, link_type=2) +parsed = HCI_Hdr(raw(built)) +assert parsed[HCI_Event_Hdr].code == 0x04 +assert parsed[HCI_Event_Hdr].len == 10 +assert parsed[HCI_Event_Connection_Request].bd_addr == "aa:bb:cc:dd:ee:ff" +assert parsed[HCI_Event_Connection_Request].device_class == 0x240404 +assert parsed[HCI_Event_Connection_Request].link_type == 2 + = Disconnection Complete evt_raw_data = hex_bytes("04050400400016") evt_pkt = HCI_Hdr(evt_raw_data) @@ -407,6 +430,18 @@ assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].status == 0 assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].handle == 0x000b assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].lmp_features == 0x875bffdbfe8ffeff += Remote Host Supported Features Notification +# Sample from a real BR/EDR controller HCI snoop log. Event code 0x3d is +# emitted after the controller learns the remote host's LMP features page. +evt_raw_data = hex_bytes("04" "3d" "0e" "f472de5c0530" "0f00000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Remote_Host_Supported_Features_Notification in evt_pkt +notif = evt_pkt[HCI_Event_Remote_Host_Supported_Features_Notification] +assert notif.bd_addr == "30:05:5c:de:72:f4" +assert notif.host_supported_features == 0x000000000000000f +# 0x0f -> first four LMP features set +assert raw(evt_pkt) == evt_raw_data + = Read Remote Version Information Complete evt_raw_data = hex_bytes("040c080002000bb0022c04") evt_pkt = HCI_Hdr(evt_raw_data) @@ -558,6 +593,35 @@ assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].interval == 20 assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].latency == 1 assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].timeout == 60 += LE Meta LE Read Remote Features Complete +# Sample from a real BLE HCI snoop log: subevent 0x04 (LE Read Remote +# Features [Page 0] Complete) returns the remote peer's LE feature mask. +evt_raw_data = hex_bytes("04" "3e" "0c" "04" "00" "4000" "1f00000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_LE_Meta_LE_Read_Remote_Features_Complete in evt_pkt +evt = evt_pkt[HCI_LE_Meta_LE_Read_Remote_Features_Complete] +assert evt.status == 0 +assert evt.handle == 0x0040 +assert evt.le_features == 0x000000000000001f +assert raw(evt_pkt) == evt_raw_data + += HCI Event Vendor (vendor-specific debug event) +# Sample from a real HCI snoop log. Event code 0xff is reserved by the +# Bluetooth Core spec for vendor-specific debugging events; the body is an +# opaque, vendor-defined byte string. +evt_raw_data = hex_bytes("04" "ff" "04" "26000101") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Vendor in evt_pkt +assert evt_pkt[HCI_Event_Vendor].data == b"\x26\x00\x01\x01" +assert raw(evt_pkt) == evt_raw_data + +# Roundtrip with arbitrary payload +built = HCI_Hdr() / HCI_Event_Hdr() / HCI_Event_Vendor(data=b"\xde\xad\xbe\xef") +parsed = HCI_Hdr(raw(built)) +assert parsed[HCI_Event_Hdr].code == 0xff +assert parsed[HCI_Event_Hdr].len == 4 +assert parsed[HCI_Event_Vendor].data == b"\xde\xad\xbe\xef" + + Bluetooth LE Advertising / Scan Response Data Parsing = Parse EIR_IncompleteList32BitServiceUUIDs @@ -600,6 +664,56 @@ assert EIR_LEBluetoothDeviceAddress in p assert p[EIR_LEBluetoothDeviceAddress].addr_type == 0x0 assert p[EIR_LEBluetoothDeviceAddress].bd_addr == '4c:ba:d7:19:35:d9' += Parse EIR_RandomTargetAddress +# Type 0x18 (Random Target Address) — six little-endian BD_ADDR bytes +p = EIR_Hdr(hex_bytes("0718aabbccddeeff")) +assert EIR_RandomTargetAddress in p +assert p[EIR_RandomTargetAddress].bd_addr == "ff:ee:dd:cc:bb:aa" +assert raw(p) == hex_bytes("0718aabbccddeeff") +# Roundtrip build +built = EIR_Hdr() / EIR_RandomTargetAddress(bd_addr="11:22:33:44:55:66") +assert raw(built) == hex_bytes("0718665544332211") + += Parse EIR_LERole +# Type 0x1c (LE Role) — single byte selecting one of four GAP roles. +for role in (0, 1, 2, 3): + p = EIR_Hdr(hex_bytes("021c%02x" % role)) + assert EIR_LERole in p + assert p[EIR_LERole].role == role + assert raw(p) == hex_bytes("021c%02x" % role) + += Parse EIR_BroadcastName +# Type 0x30 (Broadcast Name) — sample observed in a real scan response from +# a Sony BRAVIA TV advertising for LE Audio broadcast assistant clients. +raw_data = hex_bytes("0a30" "416e746520526f6f6d") # "Ante Room" +p = EIR_Hdr(raw_data) +assert EIR_BroadcastName in p +assert p[EIR_BroadcastName].broadcast_name == b"Ante Room" +assert raw(p) == raw_data +# Roundtrip build +built = EIR_Hdr() / EIR_BroadcastName(broadcast_name=b"Speaker 1") +assert raw(built) == hex_bytes("0a30") + b"Speaker 1" + += Parse EIR_3DInformation +# Type 0x3d (3D Information) — sample from a real BR/EDR Extended Inquiry +# Response (Sony XBR-65X850F display, observed in an HCI snoop log). +raw_data = hex_bytes("033d0764") +p = EIR_Hdr(raw_data) +assert EIR_3DInformation in p +info = p[EIR_3DInformation] +assert info.factory_test_mode == 0 +assert info.send_battery_level_on_startup == 1 +assert info.battery_level_reporting == 1 +assert info.association_notification == 1 +assert info.path_loss_threshold == 100 +assert raw(p) == raw_data +# Roundtrip build with factory_test_mode bit set +built = EIR_Hdr() / EIR_3DInformation(factory_test_mode=1, path_loss_threshold=0x80) +assert raw(built) == hex_bytes("033d8080") +parsed = EIR_Hdr(raw(built)) +assert parsed[EIR_3DInformation].factory_test_mode == 1 +assert parsed[EIR_3DInformation].path_loss_threshold == 0x80 + = Parse EIR_Appearance p = BTLE(hex_bytes("d6be898e201660d4d3cebffb0201050319420c0303e7fe040948393850c27c")) assert EIR_Appearance in p @@ -924,6 +1038,22 @@ assert r == b'\rscapy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' p = SM_Hdr(r) assert SM_DHKey_Check in p and p.dhkey_check[:5] == b"scapy" += SM_Keypress_Notification() tests +# SMP opcode 0x0e (Keypress Notification) is used during passkey entry to +# echo each keypress event from one peer to the other. The body is a single +# byte selecting which keypress phase the notification represents. +for nt in range(5): + r = raw(SM_Hdr() / SM_Keypress_Notification(notification_type=nt)) + assert r == bytes([0x0e, nt]) + p = SM_Hdr(r) + assert SM_Keypress_Notification in p + assert p[SM_Keypress_Notification].notification_type == nt + +# Wrapping in L2CAP on the SMP fixed channel (cid=0x0006) parses too. +pkt = HCI_Hdr(hex_bytes('0200260600020006000e02')) +assert SM_Keypress_Notification in pkt +assert pkt[SM_Keypress_Notification].notification_type == 0x02 + + HCIMon tests