From 4f92d5de1e0916b377a37d141910589b9284c70c Mon Sep 17 00:00:00 2001 From: glitch Date: Thu, 23 Jul 2026 14:46:53 -0500 Subject: [PATCH] Read Mesh Control field in 802.11 QoS frames Data frames on an 802.11s mesh contain a variable length mesh control field which must be consumed by scapy to get to the rest of the packet. AI-Assisted: no --- scapy/layers/dot11.py | 97 ++++++++++++++++++++++++++++++++++++- test/scapy/layers/dot11.uts | 31 ++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index bef8452355d..4f986bc8ee4 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -671,6 +671,30 @@ def post_build(self, p, pay): ], ] +# 802.11-2024 9.3.5: Table 9-77 +# invalid combinations may throw ValueError +_dot11_mesh_addr_meaning = [ + [ # no extension + [ # toDs = 0 + ["DA", "TA", "SA=Mesh SA", None], # fromDs 0 + ["DA", "TA", "SA=Mesh SA", None] # fromDs 1 + ], + { 1: ["RA", "TA", "DA=Mesh DA", "SA=Mesh SA"]} + ], + [ # addr4 + [["RA", "TA", "DA=Mesh DA", None], + ["DA", "TA", "Mesh SA", "SA"]] + ], + # addr5&6 + { 1: { 1: ["RA", "TA", "Mesh DA", "Mesh SA"] } } # to_ds 1 from_ds 1 +] + +def _get_dot11_mesh_addr_meaning(to_ds, from_ds, payload, idx): + try: + extension_mode = payload.Mesh_Control_Field.Address_Extension_Mode if payload.Mesh_Control_Present else 0 + return _dot11_mesh_addr_meaning[extension_mode][to_ds][from_ds][idx] + except ValueError: + return None class _Dot11MacField(MACField): """ @@ -797,6 +821,8 @@ def address_meaning(self, index): return ["RA", "NAV-SA", "NAV-DA"][index] return _dot11_addr_meaning[1][index] elif self.type == 2: # Data + if isinstance(self.payload, Dot11MeshQoS): + return _get_dot11_mesh_addr_meaning(self.FCfield.to_DS, self.FCfield.from_DS, self.payload, index) meaning = _dot11_addr_meaning[2][index][ self.FCfield.to_DS ][self.FCfield.from_DS] @@ -843,18 +869,88 @@ def post_build(self, p, pay): class Dot11QoS(Packet): name = "802.11 QoS" + # see 802.11-2024 9.2.4.5.1 and Table 9-10 fields_desc = [BitField("A_MSDU_Present", 0, 1), BitField("Ack_Policy", 0, 2), BitField("EOSP", 0, 1), BitField("TID", 0, 4), ByteField("TXOP", 0)] + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _dot11qos_is_mesh(_pkt): + return Dot11MeshQoS + return Dot11QoS + def guess_payload_class(self, payload): if isinstance(self.underlayer, Dot11): if self.underlayer.FCfield.protected: return Dot11Encrypted return Packet.guess_payload_class(self, payload) +def _dot11qos_is_mesh(payload): + # 2 byte normal QoS header + 6 byte (minimum) Mesh Control field + if len(payload) < 8: + return False + last_octet = orb(payload[1]) + # Mesh Control Flag 1, reserved bits of both last octet and first octet of mesh control field 0 + return last_octet & 1 and last_octet & 0xF8 == 0 and orb(payload[2]) & 0xFC == 0 + +# 802.11-2024 9.2.4.8.3 +class Dot11MeshControl(Packet): + name = "802.11 Mesh Control field" + fields_desc = [BitField("reserved",0,6), + BitEnumField("Address_Extension_Mode", 0,2, {0:"none", 1:"address4", 2:"address5_6", 3:"reserved"}), + ByteField("Mesh_TTL", 0), + LEIntField("Mesh_Sequence_Number", 0), + # 802.11-2024 Table 9-35 + ConditionalField( + _Dot11MacField("addr4", ETHER_ANY, 4), + lambda pkt: pkt.Address_Extension_Mode == 1 + ), + ConditionalField( + _Dot11MacField("addr5", ETHER_ANY, 5), + lambda pkt: pkt.Address_Extension_Mode == 2 + ), + ConditionalField( + _Dot11MacField("addr6", ETHER_ANY, 6), + lambda pkt: pkt.Address_Extension_Mode == 2 + ) + ] + def address_meaning(self, index): + if index not in [4, 5, 6]: + raise ValueError("Wrong index: should be [4, 5, 6]") + # see 802.11-2024 9.3.5 and Table 9-77 + if index == 4: + return "SA" + # TODO: currently the Dot11 address_meaning will incorrectly label Address 4 as BSSID instead of Mesh SA + elif index == 5: + return "DA" + elif index == 6: + return "SA" + return None + + def guess_payload_class(self, payload): + return conf.padding_layer + + +class Dot11MeshQoS(Dot11QoS): + name = "802.11 Mesh QoS" + match_subclass = True + fields_desc = Dot11QoS.fields_desc[:-1] + [ + BitField("reserved", 0, 5), + BitField("RSPI", 0, 1), + BitField("Mesh_Power_Save_Level", 0, 1), + BitField("Mesh_Control_Present", 0, 1), + ConditionalField( + PacketField("Mesh_Control_Field", Dot11MeshControl(), Dot11MeshControl), + lambda pkt: ( + # 802.11-2024 9.2.4.8.2 -- Mesh Control field is encrypted + False if isinstance(pkt.underlayer, Dot11) and pkt.underlayer.FCfield.protected + else pkt.Mesh_Control_Present + ) + ) + ] capability_list = ["res8", "res9", "short-slot", "res11", "res12", "DSSS-OFDM", "res14", "res15", @@ -2360,7 +2456,6 @@ class Dot11CCMP(Dot11Encrypted): bind_top_down(RadioTap, Dot11FCS, present=2, Flags=16) bind_top_down(Dot11, Dot11QoS, type=2, subtype=0xc) - bind_layers(PrismHeader, Dot11,) bind_layers(Dot11, LLC, type=2) bind_layers(Dot11QoS, LLC,) diff --git a/test/scapy/layers/dot11.uts b/test/scapy/layers/dot11.uts index ac0573c3063..78cfc5fd588 100644 --- a/test/scapy/layers/dot11.uts +++ b/test/scapy/layers/dot11.uts @@ -61,6 +61,37 @@ assert "DA" in p.address_meaning(1) assert "SA" in p.address_meaning(2) assert "BSSID" in p.address_meaning(3) += Dot11MeshQoS - build +assert raw(Dot11MeshQoS(Mesh_Control_Present=1)) == b'\x00\x01\x00\x00\x00\x00\x00\x00' +s = raw(Dot11MeshQoS(Mesh_Control_Present=1, Mesh_Control_Field=Dot11MeshControl(Address_Extension_Mode=1))) +assert s == b'\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +s = raw(Dot11()/Dot11MeshQoS()) +assert s == 'b'\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + += Dot11MeshQoS - dissection +p = Dot11(s) +assert Dot11MeshQoS in p + +p = Dot11MeshQoS(b"\x00\x01\x02\x1e\xc6\x41\x00\x00\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb") +assert p.RSPI == 0 +assert p.Mesh_Power_Save_level == 0 +assert p.Mesh_Control_Present == 1 +p = p.Mesh_Control_Field +assert p.reserved == 0 +assert p.Address_Extension_Mode == 2 # address5&6 +assert p.Mesh_TTL == 30 +assert p.Mesh_Sequence_Number == 16838 +assert p.addr4 == None +assert p.addr5 == '00:11:22:33:44:55' +assert p.addr6 == '66:77:88:99:aa:bb' + +p = Dot11() / p +assert "RA" in p.address_meaning(1) +assert "TA" in p.address_meaning(2) +assert "Mesh DA" in p.address_meaning(3) +assert "Mesh SA" in p.address_meaning(4) +assert "DA" in p.address_meaning(5) +assert "SA" in p.address_meaning(6) = Dot11 - answers query = Dot11(type=0, subtype=0) Dot11(type=0, subtype=1).answers(query) == True