From 94e7777c1bfc3377d1891480289e8a9758b1fea5 Mon Sep 17 00:00:00 2001 From: Ugur Dogru Date: Tue, 7 Jul 2026 11:53:03 +0300 Subject: [PATCH] Add 802.11 Radio Measurement action support AI-Assisted: yes (Codex) --- scapy/layers/dot11.py | 178 ++++++++++++++++++++++++++++++++++++ test/scapy/layers/dot11.uts | 84 +++++++++++++++++ 2 files changed, 262 insertions(+) diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index 0d6d0d69687..c905a7cc27b 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -983,6 +983,8 @@ def network_stats(self): 33: "Power Capability", 36: "Supported Channels", 37: "Channel Switch Announcement", + 38: "Measurement Request", + 39: "Measurement Report", 42: "ERP", 45: "HT Capabilities", 46: "QoS Capability", @@ -2041,6 +2043,179 @@ class Dot11CSA(Packet): ] +# 802.11-2020 9.4.2.20, 9.4.2.21 +_dot11_measurement_type = { + 5: "Beacon", +} + + +# 802.11-2020 9.6.6.1 + +class Dot11RadioMeasurement(Packet): + name = "802.11 Radio Measurement Action" + fields_desc = [ + ByteEnumField("action", 0x00, { + 0x00: "Radio Measurement Request", + 0x01: "Radio Measurement Report", + 0x02: "Link Measurement Request", + 0x03: "Link Measurement Report", + 0x04: "Neighbor Report Request", + 0x05: "Neighbor Report Response", + }) + ] + + +# 802.11-2020 9.4.2.20.7 + +class Dot11RadioMeasurementBeaconRequest(Packet): + name = "802.11 Radio Measurement Beacon Request" + fields_desc = [ + ByteField("operating_class", 0), + ByteField("channel_number", 0), + LEShortField("randomization_interval", 0), + LEShortField("measurement_duration", 0), + ByteField("measurement_mode", 0), + MACField("BSSID", ETHER_ANY), + ] + + def extract_padding(self, s): + return "", s + + +# 802.11-2020 9.6.6.2, 9.4.2.20 + +class Dot11RadioMeasurementRequest(Packet): + name = "802.11 Radio Measurement Request" + fields_desc = [ + ByteField("token", 0), + LEShortField("repetitions", 0), + ByteEnumField("ID", 38, _dot11_id_enum), + ByteField("len", 16), + ByteField("measurement_token", 0), + # Measurement Request Mode + BitField("reserved", 0, 3), + BitField("duration_mandatory", 0, 1), + BitField("report", 0, 1), + BitField("request", 0, 1), + BitField("enable", 0, 1), + BitField("parallel", 0, 1), + ByteEnumField("measurement_request_type", 5, + _dot11_measurement_type), + # Only Beacon requests are specialized for now. Other measurement + # request types stay raw instead of being mis-dissected. + ConditionalField( + PacketField( + "measurement_request", + Dot11RadioMeasurementBeaconRequest(), + Dot11RadioMeasurementBeaconRequest + ), + lambda p: p.measurement_request_type == 5 + ), + ConditionalField( + StrLenField( + "measurement_request_data", + b"", + length_from=lambda p: max(p.len - 3, 0) + ), + lambda p: p.measurement_request_type != 5 + ), + ConditionalField( + PacketListField( + "subelements", + [], + SubelemTLV, + length_from=lambda p: max(p.len - 16, 0) + ), + lambda p: p.measurement_request_type == 5 and p.len > 16 + ) + ] + + +# 802.11-2020 9.4.2.21.7 + +class Dot11RadioMeasurementBeaconReport(Packet): + name = "802.11 Radio Measurement Beacon Report" + fields_desc = [ + ByteField("operating_class", 0), + ByteField("channel_number", 0), + LELongField("measurement_start_time", 0), + LEShortField("measurement_duration", 0), + # Reported Frame Information + BitField("reported_frame_type", 0, 1), + BitField("condensed_phy", 0, 7), + ByteField("RCPI", 0), + ByteField("RSNI", 0), + MACField("BSSID", ETHER_ANY), + ByteField("antenna_id", 0), + LEIntField("parent_tsf", 0), + ] + + def extract_padding(self, s): + return "", s + + +# 802.11-2020 9.6.6.3, 9.4.2.21 + +class Dot11RadioMeasurementReport(Packet): + name = "802.11 Radio Measurement Report" + fields_desc = [ + ByteField("token", 0), + ByteEnumField("ID", 39, _dot11_id_enum), + ByteField("len", 29), + ByteField("measurement_token", 0), + # Measurement Report Mode + BitField("reserved", 0, 5), + BitField("refused", 0, 1), + BitField("incapable", 0, 1), + BitField("late", 0, 1), + ByteEnumField("measurement_report_type", 5, + _dot11_measurement_type), + # Only Beacon reports are specialized for now. Other successful + # measurement report types stay raw. + ConditionalField( + PacketField( + "measurement_report", + Dot11RadioMeasurementBeaconReport(), + Dot11RadioMeasurementBeaconReport + ), + lambda p: ( + p.measurement_report_type == 5 and + p.late == 0 and + p.incapable == 0 and + p.refused == 0 + ) + ), + ConditionalField( + StrLenField( + "measurement_report_data", + b"", + length_from=lambda p: max(p.len - 3, 0) + ), + lambda p: ( + p.measurement_report_type != 5 and + p.late == 0 and + p.incapable == 0 and + p.refused == 0 + ) + ), + ConditionalField( + PacketListField( + "subelements", + [], + SubelemTLV, + length_from=lambda p: max(p.len - 29, 0) + ), + lambda p: ( + p.measurement_report_type == 5 and + p.late == 0 and + p.incapable == 0 and + p.refused == 0 and + p.len > 29 + ) + ) + ] + + class Dot11S1GBeacon(_Dot11EltUtils): name = "802.11 S1G Beacon" fields_desc = [LEIntField("timestamp", 0), @@ -2220,6 +2395,9 @@ class Dot11CCMP(Dot11Encrypted): bind_layers(Dot11Action, Dot11WNM, category=0x0A) bind_layers(Dot11WNM, Dot11BSSTMRequest, action=7) bind_layers(Dot11WNM, Dot11BSSTMResponse, action=8) +bind_layers(Dot11Action, Dot11RadioMeasurement, category=0x05) +bind_layers(Dot11RadioMeasurement, Dot11RadioMeasurementRequest, action=0) +bind_layers(Dot11RadioMeasurement, Dot11RadioMeasurementReport, action=1) conf.l2types.register(DLT_IEEE802_11, Dot11) diff --git a/test/scapy/layers/dot11.uts b/test/scapy/layers/dot11.uts index eaf84b17064..80cdd40b48f 100644 --- a/test/scapy/layers/dot11.uts +++ b/test/scapy/layers/dot11.uts @@ -724,6 +724,90 @@ assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].op_clas assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].channel == 12 assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].phy_type == 0 += Dot11RadioMeasurementRequest - Beacon dissection + +data = b"\x05\x00\x7a\x01\x00\x26\x10\x11\x17\x05\x51\x06\x34\x12\x50\x00\x01\x01\x02\x03\x04\x05\x06" +pkt = Dot11Action(data) +assert Dot11RadioMeasurement in pkt +assert Dot11RadioMeasurementRequest in pkt +assert Dot11RadioMeasurementBeaconRequest in pkt + +req = pkt[Dot11RadioMeasurementRequest] +assert pkt[Dot11Action].category == 5 +assert pkt[Dot11Action][Dot11RadioMeasurement].action == 0 +assert req.token == 0x7a +assert req.repetitions == 1 +assert req.ID == 38 +assert req.len == 16 +assert req.measurement_token == 0x11 +assert req.reserved == 0 +assert req.duration_mandatory == 1 +assert req.report == 0 +assert req.request == 1 +assert req.enable == 1 +assert req.parallel == 1 +assert req.measurement_request_type == 5 + +beacon_req = req[Dot11RadioMeasurementBeaconRequest] +assert beacon_req.operating_class == 81 +assert beacon_req.channel_number == 6 +assert beacon_req.randomization_interval == 0x1234 +assert beacon_req.measurement_duration == 80 +assert beacon_req.measurement_mode == 1 +assert beacon_req.BSSID == "01:02:03:04:05:06" +assert raw(pkt) == data + += Dot11RadioMeasurementReport - Beacon dissection + +data = b"\x05\x01\x7a\x27\x1d\x11\x00\x05\x51\x06\x08\x07\x06\x05\x04\x03\x02\x01\x50\x00\xaa\x37\x16\x01\x02\x03\x04\x05\x06\x03\x44\x33\x22\x11" +pkt = Dot11Action(data) +assert Dot11RadioMeasurement in pkt +assert Dot11RadioMeasurementReport in pkt +assert Dot11RadioMeasurementBeaconReport in pkt + +report = pkt[Dot11RadioMeasurementReport] +assert pkt[Dot11Action].category == 5 +assert pkt[Dot11Action][Dot11RadioMeasurement].action == 1 +assert report.token == 0x7a +assert report.ID == 39 +assert report.len == 29 +assert report.measurement_token == 0x11 +assert report.reserved == 0 +assert report.refused == 0 +assert report.incapable == 0 +assert report.late == 0 +assert report.measurement_report_type == 5 + +beacon_report = report[Dot11RadioMeasurementBeaconReport] +assert beacon_report.operating_class == 81 +assert beacon_report.channel_number == 6 +assert beacon_report.measurement_start_time == 0x0102030405060708 +assert beacon_report.measurement_duration == 80 +assert beacon_report.reported_frame_type == 1 +assert beacon_report.condensed_phy == 42 +assert beacon_report.RCPI == 55 +assert beacon_report.RSNI == 22 +assert beacon_report.BSSID == "01:02:03:04:05:06" +assert beacon_report.antenna_id == 3 +assert beacon_report.parent_tsf == 0x11223344 +assert raw(pkt) == data + += Dot11RadioMeasurementRequest - unsupported measurement type + +data = b"\x05\x00\x01\x00\x00\x26\x05\x02\x00\x07\xaa\xbb" +pkt = Dot11Action(data) +assert Dot11RadioMeasurementRequest in pkt + +req = pkt[Dot11RadioMeasurementRequest] +assert req.token == 1 +assert req.repetitions == 0 +assert req.ID == 38 +assert req.len == 5 +assert req.measurement_token == 2 +assert req.measurement_request_type == 7 +assert req.measurement_request_data == b"\xaa\xbb" +assert raw(pkt) == data + = Dot11Ack pkt = Dot11(bytes(Dot11()/Dot11Ack()))