Skip to content

Commit 69fb728

Browse files
middleendianUgur Dogru
andauthored
Add 802.11 Radio Measurement action support (#5039)
* Add 802.11 Radio Measurement action support AI-Assisted: yes (Codex) * dot11: address Radio Measurement review feedback Return bytes from Beacon request/report extract_padding(). Keep non-decoded report bodies in measurement_report_data, including refused Beacon reports. Use field names consistent with existing dot11 layers and add regression tests for unsupported/refused report fallback. AI-Assisted: yes (Codex) --------- Co-authored-by: Ugur Dogru <ugur.dogru@axon-networks.com>
1 parent f02ca8d commit 69fb728

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

scapy/layers/dot11.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,8 @@ def network_stats(self):
983983
33: "Power Capability",
984984
36: "Supported Channels",
985985
37: "Channel Switch Announcement",
986+
38: "Measurement Request",
987+
39: "Measurement Report",
986988
42: "ERP",
987989
45: "HT Capabilities",
988990
46: "QoS Capability",
@@ -2041,6 +2043,181 @@ class Dot11CSA(Packet):
20412043
]
20422044

20432045

2046+
# 802.11-2020 9.4.2.20, 9.4.2.21
2047+
_dot11_measurement_type = {
2048+
5: "Beacon",
2049+
}
2050+
2051+
2052+
# 802.11-2020 9.6.6.1
2053+
2054+
class Dot11RadioMeasurement(Packet):
2055+
name = "802.11 Radio Measurement Action"
2056+
fields_desc = [
2057+
ByteEnumField("action", 0x00, {
2058+
0x00: "Radio Measurement Request",
2059+
0x01: "Radio Measurement Report",
2060+
0x02: "Link Measurement Request",
2061+
0x03: "Link Measurement Report",
2062+
0x04: "Neighbor Report Request",
2063+
0x05: "Neighbor Report Response",
2064+
})
2065+
]
2066+
2067+
2068+
# 802.11-2020 9.4.2.20.7
2069+
2070+
class Dot11RadioMeasurementBeaconRequest(Packet):
2071+
name = "802.11 Radio Measurement Beacon Request"
2072+
fields_desc = [
2073+
ByteField("op_class", 0),
2074+
ByteField("channel", 0),
2075+
LEShortField("randomization_interval", 0),
2076+
LEShortField("measurement_duration", 0),
2077+
ByteField("measurement_mode", 0),
2078+
MACField("BSSID", ETHER_ANY),
2079+
]
2080+
2081+
def extract_padding(self, s):
2082+
return b"", s
2083+
2084+
2085+
# 802.11-2020 9.6.6.2, 9.4.2.20
2086+
2087+
class Dot11RadioMeasurementRequest(Packet):
2088+
name = "802.11 Radio Measurement Request"
2089+
fields_desc = [
2090+
ByteField("token", 0),
2091+
LEShortField("repetitions", 0),
2092+
ByteEnumField("ID", 38, _dot11_id_enum),
2093+
ByteField("len", 16),
2094+
ByteField("measurement_token", 0),
2095+
# Measurement Request Mode
2096+
BitField("reserved", 0, 3),
2097+
BitField("duration_mandatory", 0, 1),
2098+
BitField("report", 0, 1),
2099+
BitField("request", 0, 1),
2100+
BitField("enable", 0, 1),
2101+
BitField("parallel", 0, 1),
2102+
ByteEnumField("measurement_request_type", 5,
2103+
_dot11_measurement_type),
2104+
# Only Beacon requests are specialized for now. Other measurement
2105+
# request types stay raw instead of being mis-dissected.
2106+
ConditionalField(
2107+
PacketField(
2108+
"measurement_request",
2109+
Dot11RadioMeasurementBeaconRequest(),
2110+
Dot11RadioMeasurementBeaconRequest
2111+
),
2112+
lambda p: p.measurement_request_type == 5
2113+
),
2114+
ConditionalField(
2115+
StrLenField(
2116+
"measurement_request_data",
2117+
b"",
2118+
length_from=lambda p: max(p.len - 3, 0)
2119+
),
2120+
lambda p: p.measurement_request_type != 5
2121+
),
2122+
ConditionalField(
2123+
PacketListField(
2124+
"subelems",
2125+
[],
2126+
SubelemTLV,
2127+
length_from=lambda p: max(p.len - 16, 0)
2128+
),
2129+
lambda p: p.measurement_request_type == 5 and p.len > 16
2130+
)
2131+
]
2132+
2133+
2134+
# 802.11-2020 9.4.2.21.7
2135+
2136+
class Dot11RadioMeasurementBeaconReport(Packet):
2137+
name = "802.11 Radio Measurement Beacon Report"
2138+
fields_desc = [
2139+
ByteField("op_class", 0),
2140+
ByteField("channel", 0),
2141+
LELongField("measurement_start_time", 0),
2142+
LEShortField("measurement_duration", 0),
2143+
# Reported Frame Information
2144+
BitField("reported_frame_type", 0, 1),
2145+
BitField("condensed_phy", 0, 7),
2146+
ByteField("RCPI", 0),
2147+
ByteField("RSNI", 0),
2148+
MACField("BSSID", ETHER_ANY),
2149+
ByteField("antenna_id", 0),
2150+
LEIntField("parent_tsf", 0),
2151+
]
2152+
2153+
def extract_padding(self, s):
2154+
return b"", s
2155+
2156+
2157+
# 802.11-2020 9.6.6.3, 9.4.2.21
2158+
2159+
class Dot11RadioMeasurementReport(Packet):
2160+
name = "802.11 Radio Measurement Report"
2161+
fields_desc = [
2162+
ByteField("token", 0),
2163+
ByteEnumField("ID", 39, _dot11_id_enum),
2164+
ByteField("len", 29),
2165+
ByteField("measurement_token", 0),
2166+
# Measurement Report Mode
2167+
BitField("reserved", 0, 5),
2168+
BitField("refused", 0, 1),
2169+
BitField("incapable", 0, 1),
2170+
BitField("late", 0, 1),
2171+
ByteEnumField("measurement_report_type", 5,
2172+
_dot11_measurement_type),
2173+
# Only successful Beacon reports are specialized for now. Other or
2174+
# unsuccessful measurement report bodies stay raw.
2175+
ConditionalField(
2176+
PacketField(
2177+
"measurement_report",
2178+
Dot11RadioMeasurementBeaconReport(),
2179+
Dot11RadioMeasurementBeaconReport
2180+
),
2181+
lambda p: (
2182+
p.measurement_report_type == 5 and
2183+
p.late == 0 and
2184+
p.incapable == 0 and
2185+
p.refused == 0
2186+
)
2187+
),
2188+
ConditionalField(
2189+
StrLenField(
2190+
"measurement_report_data",
2191+
b"",
2192+
length_from=lambda p: max(p.len - 3, 0)
2193+
),
2194+
lambda p: (
2195+
not (
2196+
p.measurement_report_type == 5 and
2197+
p.late == 0 and
2198+
p.incapable == 0 and
2199+
p.refused == 0
2200+
)
2201+
)
2202+
),
2203+
ConditionalField(
2204+
PacketListField(
2205+
"subelems",
2206+
[],
2207+
SubelemTLV,
2208+
length_from=lambda p: max(p.len - 29, 0)
2209+
),
2210+
lambda p: (
2211+
p.measurement_report_type == 5 and
2212+
p.late == 0 and
2213+
p.incapable == 0 and
2214+
p.refused == 0 and
2215+
p.len > 29
2216+
)
2217+
)
2218+
]
2219+
2220+
20442221
class Dot11S1GBeacon(_Dot11EltUtils):
20452222
name = "802.11 S1G Beacon"
20462223
fields_desc = [LEIntField("timestamp", 0),
@@ -2220,6 +2397,9 @@ class Dot11CCMP(Dot11Encrypted):
22202397
bind_layers(Dot11Action, Dot11WNM, category=0x0A)
22212398
bind_layers(Dot11WNM, Dot11BSSTMRequest, action=7)
22222399
bind_layers(Dot11WNM, Dot11BSSTMResponse, action=8)
2400+
bind_layers(Dot11Action, Dot11RadioMeasurement, category=0x05)
2401+
bind_layers(Dot11RadioMeasurement, Dot11RadioMeasurementRequest, action=0)
2402+
bind_layers(Dot11RadioMeasurement, Dot11RadioMeasurementReport, action=1)
22232403

22242404

22252405
conf.l2types.register(DLT_IEEE802_11, Dot11)

test/scapy/layers/dot11.uts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,121 @@ assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].op_clas
724724
assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].channel == 12
725725
assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].phy_type == 0
726726

727+
= Dot11RadioMeasurementRequest - Beacon dissection
728+
729+
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"
730+
pkt = Dot11Action(data)
731+
assert Dot11RadioMeasurement in pkt
732+
assert Dot11RadioMeasurementRequest in pkt
733+
assert Dot11RadioMeasurementBeaconRequest in pkt
734+
735+
req = pkt[Dot11RadioMeasurementRequest]
736+
assert pkt[Dot11Action].category == 5
737+
assert pkt[Dot11Action][Dot11RadioMeasurement].action == 0
738+
assert req.token == 0x7a
739+
assert req.repetitions == 1
740+
assert req.ID == 38
741+
assert req.len == 16
742+
assert req.measurement_token == 0x11
743+
assert req.reserved == 0
744+
assert req.duration_mandatory == 1
745+
assert req.report == 0
746+
assert req.request == 1
747+
assert req.enable == 1
748+
assert req.parallel == 1
749+
assert req.measurement_request_type == 5
750+
751+
beacon_req = req[Dot11RadioMeasurementBeaconRequest]
752+
assert beacon_req.op_class == 81
753+
assert beacon_req.channel == 6
754+
assert beacon_req.randomization_interval == 0x1234
755+
assert beacon_req.measurement_duration == 80
756+
assert beacon_req.measurement_mode == 1
757+
assert beacon_req.BSSID == "01:02:03:04:05:06"
758+
assert raw(pkt) == data
759+
760+
= Dot11RadioMeasurementReport - Beacon dissection
761+
762+
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"
763+
pkt = Dot11Action(data)
764+
assert Dot11RadioMeasurement in pkt
765+
assert Dot11RadioMeasurementReport in pkt
766+
assert Dot11RadioMeasurementBeaconReport in pkt
767+
768+
report = pkt[Dot11RadioMeasurementReport]
769+
assert pkt[Dot11Action].category == 5
770+
assert pkt[Dot11Action][Dot11RadioMeasurement].action == 1
771+
assert report.token == 0x7a
772+
assert report.ID == 39
773+
assert report.len == 29
774+
assert report.measurement_token == 0x11
775+
assert report.reserved == 0
776+
assert report.refused == 0
777+
assert report.incapable == 0
778+
assert report.late == 0
779+
assert report.measurement_report_type == 5
780+
781+
beacon_report = report[Dot11RadioMeasurementBeaconReport]
782+
assert beacon_report.op_class == 81
783+
assert beacon_report.channel == 6
784+
assert beacon_report.measurement_start_time == 0x0102030405060708
785+
assert beacon_report.measurement_duration == 80
786+
assert beacon_report.reported_frame_type == 1
787+
assert beacon_report.condensed_phy == 42
788+
assert beacon_report.RCPI == 55
789+
assert beacon_report.RSNI == 22
790+
assert beacon_report.BSSID == "01:02:03:04:05:06"
791+
assert beacon_report.antenna_id == 3
792+
assert beacon_report.parent_tsf == 0x11223344
793+
assert raw(pkt) == data
794+
795+
= Dot11RadioMeasurementRequest - unsupported measurement type
796+
797+
data = b"\x05\x00\x01\x00\x00\x26\x05\x02\x00\x07\xaa\xbb"
798+
pkt = Dot11Action(data)
799+
assert Dot11RadioMeasurementRequest in pkt
800+
801+
req = pkt[Dot11RadioMeasurementRequest]
802+
assert req.token == 1
803+
assert req.repetitions == 0
804+
assert req.ID == 38
805+
assert req.len == 5
806+
assert req.measurement_token == 2
807+
assert req.measurement_request_type == 7
808+
assert req.measurement_request_data == b"\xaa\xbb"
809+
assert raw(pkt) == data
810+
811+
= Dot11RadioMeasurementReport - unsupported measurement type
812+
813+
data = b"\x05\x01\x01\x27\x05\x02\x00\x07\xaa\xbb"
814+
pkt = Dot11Action(data)
815+
assert Dot11RadioMeasurementReport in pkt
816+
817+
report = pkt[Dot11RadioMeasurementReport]
818+
assert "measurement_report" not in report.fields
819+
assert report.token == 1
820+
assert report.ID == 39
821+
assert report.len == 5
822+
assert report.measurement_token == 2
823+
assert report.measurement_report_type == 7
824+
assert report.measurement_report_data == b"\xaa\xbb"
825+
assert raw(pkt) == data
826+
827+
= Dot11RadioMeasurementReport - refused Beacon report
828+
829+
data = b"\x05\x01\x01\x27\x05\x02\x04\x05\xaa\xbb"
830+
pkt = Dot11Action(data)
831+
assert Dot11RadioMeasurementReport in pkt
832+
833+
report = pkt[Dot11RadioMeasurementReport]
834+
assert "measurement_report" not in report.fields
835+
assert report.refused == 1
836+
assert report.incapable == 0
837+
assert report.late == 0
838+
assert report.measurement_report_type == 5
839+
assert report.measurement_report_data == b"\xaa\xbb"
840+
assert raw(pkt) == data
841+
727842
= Dot11Ack
728843

729844
pkt = Dot11(bytes(Dot11()/Dot11Ack()))

0 commit comments

Comments
 (0)