From 08fbf3c1462225052ef90d6e50845c219b73eb36 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 1 Nov 2026 23:41:51 -0500 Subject: [PATCH] feat(link): bind LinkType 105 and 127 to the 802.11 and radiotap dissectors (#2608) - Frame.__proto__, PCAPNG.__proto__: IEEE802_11 (105) -> IEEE802_11 and IEEE802_11_RADIOTAP (127) -> Radiotap, in enum value order; docstring tables to match. - IEEE802_11.__index__ returns LinkType.IEEE802_11 rather than raising. - docs: 802.11 and radiotap pages name the native link types; the FCS note no longer assumes radiotap. - dispatch generator: payload builders and PINNED_TARGETS for both codes in both tables; the registry count pin follows. - tests: pcap and pcapng captures of each parse to 802.11 and Radiotap:802.11 with byte-exact rebuilds; fail without the change. Sample-capture differential matches main; pylint and mypy unchanged. --- .../pcapkit/protocols/link/ieee802_11.rst | 8 +- .../pcapkit/protocols/link/radiotap.rst | 1 + examples/generators/dispatch.py | 20 +++ pcapkit/protocols/link/ieee802_11.py | 37 ++-- pcapkit/protocols/misc/pcap/frame.py | 20 ++- pcapkit/protocols/misc/pcapng.py | 20 ++- .../test_ieee802_11_linktype_2608_unit.py | 165 ++++++++++++++++++ tests/protocols/link/test_ieee802_11_unit.py | 5 +- .../protocols/test_dispatch_registry_unit.py | 10 +- 9 files changed, 246 insertions(+), 40 deletions(-) create mode 100644 tests/protocols/link/test_ieee802_11_linktype_2608_unit.py diff --git a/docs/source/pcapkit/protocols/link/ieee802_11.rst b/docs/source/pcapkit/protocols/link/ieee802_11.rst index 7bdb4ad73c..cc9fb573e0 100644 --- a/docs/source/pcapkit/protocols/link/ieee802_11.rst +++ b/docs/source/pcapkit/protocols/link/ieee802_11.rst @@ -6,8 +6,9 @@ IEEE 802.11 - Wireless LAN MAC Header :mod:`pcapkit.protocols.link.ieee802_11` contains :class:`~pcapkit.protocols.link.ieee802_11.IEEE802_11` only, which implements the extractor for the IEEE 802.11 MAC header [*]_ of a protocol version 0 -frame, as :class:`~pcapkit.protocols.link.radiotap.Radiotap` hands it on. Its -fields, in the order they appear, numeric ones little-endian: +frame, as a ``LINKTYPE_IEEE802_11`` capture carries it or +:class:`~pcapkit.protocols.link.radiotap.Radiotap` hands it on. Its fields, in +the order they appear, numeric ones little-endian: .. table:: @@ -41,7 +42,8 @@ Scope * **The frame body** (§9.2.4.7) is kept as :class:`~pcapkit.protocols.misc.raw.Raw`, protected or not. * **The FCS** (§9.2.4.8), where one was captured, stays at the end of the - body: whether a capture carries it is a radiotap Flags bit, which + body: this class does not decide whether a capture carries one, which + behind radiotap is a Flags bit :class:`~pcapkit.protocols.link.radiotap.Radiotap` does not decode. * **Pad octets** that radiotap's data-pad flag says follow the header stay at the start of the body, for the same reason. diff --git a/docs/source/pcapkit/protocols/link/radiotap.rst b/docs/source/pcapkit/protocols/link/radiotap.rst index 7181e9dca8..347a4552fc 100644 --- a/docs/source/pcapkit/protocols/link/radiotap.rst +++ b/docs/source/pcapkit/protocols/link/radiotap.rst @@ -7,6 +7,7 @@ Radiotap - IEEE 802.11 Radiotap Header :class:`~pcapkit.protocols.link.radiotap.Radiotap` only, which implements the extractor for the radiotap header [*]_ capture tools prepend to an 802.11 frame, as seen on a +``LINKTYPE_IEEE802_11_RADIOTAP`` capture, or on a ``LINKTYPE_LINUX_SLL``/``LINKTYPE_LINUX_SLL2`` capture whose ``arphrd_type`` is ``ARPHRD_IEEE80211_RADIOTAP``. Radiotap is not part of the 802.11 standard itself -- it is a prefix written by the capturing tool, in front of diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 39a271c168..56a50f9da4 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -291,6 +291,18 @@ def _linux_sll2(payload: 'bytes') -> 'bytes': return bytes(LinuxSLL2(payload=payload)) +def _wlan_ack() -> 'bytes': + """An 802.11 Ack frame, which carries no body.""" + from pcapkit.protocols.link.ieee802_11 import IEEE802_11 + return bytes(IEEE802_11(type='Control', subtype='ACK', addr1='02:00:00:00:00:01')) + + +def _radiotap(payload: 'bytes') -> 'bytes': + """A minimal radiotap header carrying ``payload``.""" + from pcapkit.protocols.link.radiotap import Radiotap + return bytes(Radiotap(payload=payload)) + + ############################################################################### # Link -- EtherType. ############################################################################### @@ -580,6 +592,10 @@ def _linktype_payload(code: 'Any') -> 'tuple[bytes, bool]': return _linux_sll(_ip4(6, _tcp(9999))), False if code == LinkType.LINUX_SLL2: return _linux_sll2(_ip4(6, _tcp(9999))), False + if code == LinkType.IEEE802_11: + return _wlan_ack(), False + if code == LinkType.IEEE802_11_RADIOTAP: + return _radiotap(_wlan_ack()), False raise LookupError(f'linktype: no payload builder for {code!r}') # pragma: no cover @@ -811,6 +827,8 @@ def _link_enum() -> 'Any': 'pcap-frame/IPV6': ('pcapkit.protocols.internet', 'IPv6'), 'pcap-frame/LINUX_SLL': ('pcapkit.protocols.link', 'LinuxSLL1'), 'pcap-frame/LINUX_SLL2': ('pcapkit.protocols.link', 'LinuxSLL2'), + 'pcap-frame/IEEE802_11': ('pcapkit.protocols.link', 'IEEE802_11'), + 'pcap-frame/IEEE802_11_RADIOTAP': ('pcapkit.protocols.link', 'Radiotap'), # -- PCAPNG.__proto__ (LinkType) -------------------------------------------- 'pcapng-frame/NULL': ('pcapkit.protocols.link', 'Loopback'), @@ -820,6 +838,8 @@ def _link_enum() -> 'Any': 'pcapng-frame/IPV6': ('pcapkit.protocols.internet', 'IPv6'), 'pcapng-frame/LINUX_SLL': ('pcapkit.protocols.link', 'LinuxSLL1'), 'pcapng-frame/LINUX_SLL2': ('pcapkit.protocols.link', 'LinuxSLL2'), + 'pcapng-frame/IEEE802_11': ('pcapkit.protocols.link', 'IEEE802_11'), + 'pcapng-frame/IEEE802_11_RADIOTAP': ('pcapkit.protocols.link', 'Radiotap'), # -- Loopback.__proto__ (address family) ------------------------------------ 'loopback/2': ('pcapkit.protocols.internet.ipv4', 'IPv4'), diff --git a/pcapkit/protocols/link/ieee802_11.py b/pcapkit/protocols/link/ieee802_11.py index 87a9586959..052d1c89f6 100644 --- a/pcapkit/protocols/link/ieee802_11.py +++ b/pcapkit/protocols/link/ieee802_11.py @@ -7,8 +7,9 @@ :mod:`pcapkit.protocols.link.ieee802_11` contains :class:`~pcapkit.protocols.link.ieee802_11.IEEE802_11` only, which implements the extractor for the IEEE 802.11 MAC header [*]_ of a protocol version 0 -frame, as :class:`~pcapkit.protocols.link.radiotap.Radiotap` hands it on. Its -fields, in the order they appear, numeric ones little-endian: +frame, as a ``LINKTYPE_IEEE802_11`` capture carries it or +:class:`~pcapkit.protocols.link.radiotap.Radiotap` hands it on. Its fields, in +the order they appear, numeric ones little-endian: .. table:: @@ -37,12 +38,12 @@ :class:`~pcapkit.protocols.data.link.ieee802_11.FrameControl`. The frame body (§9.2.4.7) follows the header and is kept as -:class:`~pcapkit.protocols.misc.raw.Raw`, protected or not. So is the FCS -(§9.2.4.8) where one was captured: whether a capture carries it is a -radiotap Flags bit, which :class:`~pcapkit.protocols.link.radiotap.Radiotap` -does not decode, so the FCS stays at the end of the body. For the same reason, -the pad octets that radiotap's data-pad flag says follow the header stay at the -start of the body. +:class:`~pcapkit.protocols.misc.raw.Raw`, protected or not. The FCS +(§9.2.4.8), where one was captured, stays at the end of the body: this class +does not decide whether a capture carries one, which behind radiotap is a +Flags bit :class:`~pcapkit.protocols.link.radiotap.Radiotap` does not decode. +For the same reason, the pad octets that radiotap's data-pad flag says follow +the header stay at the start of the body. A frame of any protocol version but 0 is refused: version 1 is the PV1 frame format of §9.8, whose Frame Control field is laid out differently, and @@ -62,6 +63,7 @@ from pcapkit.const.ieee802_11.extension_subtype import ExtensionSubtype as Enum_ExtensionSubtype from pcapkit.const.ieee802_11.management_subtype import ManagementSubtype as Enum_ManagementSubtype from pcapkit.const.ieee802_11.type import Type as Enum_Type +from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.protocols.data.link.ieee802_11 import IEEE802_11 as Data_IEEE802_11 from pcapkit.protocols.data.link.ieee802_11 import FrameControl as Data_FrameControl from pcapkit.protocols.data.link.ieee802_11 import HTControl as Data_HTControl @@ -71,11 +73,11 @@ from pcapkit.protocols.link.link import Link from pcapkit.protocols.schema.link.ieee802_11 import IEEE802_11 as Schema_IEEE802_11 from pcapkit.protocols.schema.link.ieee802_11 import Layout, layout -from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall +from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: from enum import IntEnum as StdlibEnum - from typing import Any, Mapping, NoReturn, Optional, Type, Union + from typing import Any, Mapping, Optional, Type, Union from aenum import IntEnum as AenumEnum from typing_extensions import Literal @@ -330,16 +332,19 @@ def __length_hint__(self) -> 'Literal[36]': return 36 @classmethod - def __index__(cls) -> 'NoReturn': # pylint: disable=invalid-index-returned + def __index__(cls) -> 'Enum_LinkType': # pylint: disable=invalid-index-returned """Numeral registry index of the protocol. - Raises: - UnsupportedCall: This protocol is reached only through - :class:`~pcapkit.protocols.link.radiotap.Radiotap`'s payload - dispatch, not through a link-layer header type of its own. + Returns: + Numeral registry index of the protocol in `tcpdump`_ link-layer + header types, which + :attr:`Radiotap.__proto__ ` + is keyed by too. + + .. _tcpdump: https://www.tcpdump.org/linktypes.html """ - raise UnsupportedCall(f'{cls.__name__!r} object cannot be interpreted as an integer') + return Enum_LinkType.IEEE802_11 # type: ignore[return-value] ########################################################################## # Utilities. diff --git a/pcapkit/protocols/misc/pcap/frame.py b/pcapkit/protocols/misc/pcap/frame.py index 2bf67003d7..ed253e855b 100644 --- a/pcapkit/protocols/misc/pcap/frame.py +++ b/pcapkit/protocols/misc/pcap/frame.py @@ -83,10 +83,14 @@ class Frame(ProtocolBase[Data_Frame, Schema_Frame], - :class:`pcapkit.protocols.link.loopback.Loopback` * - :attr:`pcapkit.const.reg.linktype.LinkType.ETHERNET` - :class:`pcapkit.protocols.link.ethernet.Ethernet` + * - :attr:`pcapkit.const.reg.linktype.LinkType.IEEE802_11` + - :class:`pcapkit.protocols.link.ieee802_11.IEEE802_11` * - :attr:`pcapkit.const.reg.linktype.LinkType.LOOP` - :class:`pcapkit.protocols.link.loopback.Loopback` * - :attr:`pcapkit.const.reg.linktype.LinkType.LINUX_SLL` - :class:`pcapkit.protocols.link.linux_sll1.LinuxSLL1` + * - :attr:`pcapkit.const.reg.linktype.LinkType.IEEE802_11_RADIOTAP` + - :class:`pcapkit.protocols.link.radiotap.Radiotap` * - :attr:`pcapkit.const.reg.linktype.LinkType.IPV4` - :class:`pcapkit.protocols.internet.ipv4.IPv4` * - :attr:`pcapkit.const.reg.linktype.LinkType.IPV6` @@ -112,13 +116,15 @@ class Frame(ProtocolBase[Data_Frame, Schema_Frame], __proto__ = collections.defaultdict( lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), { - Enum_LinkType.NULL: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), - Enum_LinkType.ETHERNET: ModuleDescriptor('pcapkit.protocols.link', 'Ethernet'), - Enum_LinkType.LOOP: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), - Enum_LinkType.LINUX_SLL: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL1'), - Enum_LinkType.IPV4: ModuleDescriptor('pcapkit.protocols.internet', 'IPv4'), - Enum_LinkType.IPV6: ModuleDescriptor('pcapkit.protocols.internet', 'IPv6'), - Enum_LinkType.LINUX_SLL2: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL2'), + Enum_LinkType.NULL: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), + Enum_LinkType.ETHERNET: ModuleDescriptor('pcapkit.protocols.link', 'Ethernet'), + Enum_LinkType.IEEE802_11: ModuleDescriptor('pcapkit.protocols.link', 'IEEE802_11'), + Enum_LinkType.LOOP: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), + Enum_LinkType.LINUX_SLL: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL1'), + Enum_LinkType.IEEE802_11_RADIOTAP: ModuleDescriptor('pcapkit.protocols.link', 'Radiotap'), + Enum_LinkType.IPV4: ModuleDescriptor('pcapkit.protocols.internet', 'IPv4'), + Enum_LinkType.IPV6: ModuleDescriptor('pcapkit.protocols.internet', 'IPv6'), + Enum_LinkType.LINUX_SLL2: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL2'), }, ) # type: DefaultDict[Enum_LinkType | int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index c94c712442..c3167b2543 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -482,10 +482,14 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], - :class:`pcapkit.protocols.link.loopback.Loopback` * - :attr:`pcapkit.const.reg.linktype.LinkType.ETHERNET` - :class:`pcapkit.protocols.link.ethernet.Ethernet` + * - :attr:`pcapkit.const.reg.linktype.LinkType.IEEE802_11` + - :class:`pcapkit.protocols.link.ieee802_11.IEEE802_11` * - :attr:`pcapkit.const.reg.linktype.LinkType.LOOP` - :class:`pcapkit.protocols.link.loopback.Loopback` * - :attr:`pcapkit.const.reg.linktype.LinkType.LINUX_SLL` - :class:`pcapkit.protocols.link.linux_sll1.LinuxSLL1` + * - :attr:`pcapkit.const.reg.linktype.LinkType.IEEE802_11_RADIOTAP` + - :class:`pcapkit.protocols.link.radiotap.Radiotap` * - :attr:`pcapkit.const.reg.linktype.LinkType.IPV4` - :class:`pcapkit.protocols.internet.ipv4.IPv4` * - :attr:`pcapkit.const.reg.linktype.LinkType.IPV6` @@ -771,13 +775,15 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], __proto__ = collections.defaultdict( lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), { - Enum_LinkType.NULL: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), - Enum_LinkType.ETHERNET: ModuleDescriptor('pcapkit.protocols.link', 'Ethernet'), - Enum_LinkType.LOOP: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), - Enum_LinkType.LINUX_SLL: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL1'), - Enum_LinkType.IPV4: ModuleDescriptor('pcapkit.protocols.internet', 'IPv4'), - Enum_LinkType.IPV6: ModuleDescriptor('pcapkit.protocols.internet', 'IPv6'), - Enum_LinkType.LINUX_SLL2: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL2'), + Enum_LinkType.NULL: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), + Enum_LinkType.ETHERNET: ModuleDescriptor('pcapkit.protocols.link', 'Ethernet'), + Enum_LinkType.IEEE802_11: ModuleDescriptor('pcapkit.protocols.link', 'IEEE802_11'), + Enum_LinkType.LOOP: ModuleDescriptor('pcapkit.protocols.link', 'Loopback'), + Enum_LinkType.LINUX_SLL: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL1'), + Enum_LinkType.IEEE802_11_RADIOTAP: ModuleDescriptor('pcapkit.protocols.link', 'Radiotap'), + Enum_LinkType.IPV4: ModuleDescriptor('pcapkit.protocols.internet', 'IPv4'), + Enum_LinkType.IPV6: ModuleDescriptor('pcapkit.protocols.internet', 'IPv6'), + Enum_LinkType.LINUX_SLL2: ModuleDescriptor('pcapkit.protocols.link', 'LinuxSLL2'), }, ) # type: DefaultDict[Enum_LinkType | int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] diff --git a/tests/protocols/link/test_ieee802_11_linktype_2608_unit.py b/tests/protocols/link/test_ieee802_11_linktype_2608_unit.py new file mode 100644 index 0000000000..56c95c102e --- /dev/null +++ b/tests/protocols/link/test_ieee802_11_linktype_2608_unit.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2608: link types 105 and 127 reach the 802.11 and radiotap dissectors. + +:class:`~pcapkit.protocols.misc.pcap.frame.Frame` and +:class:`~pcapkit.protocols.misc.pcapng.PCAPNG` bind +``LinkType.IEEE802_11`` (105) to +:class:`~pcapkit.protocols.link.ieee802_11.IEEE802_11` and +``LinkType.IEEE802_11_RADIOTAP`` (127) to +:class:`~pcapkit.protocols.link.radiotap.Radiotap`. A pcap and a pcapng capture +of each, built here octet by octet, parse to ``802.11`` and +``Radiotap:802.11``, and every frame and layer rebuilds byte-exactly. + +Each test imports :mod:`pcapkit` itself, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import os +import struct +import tempfile +import unittest +import warnings + +from tests._support import reimport_once_per_class + +#: An 802.11 Ack: Frame Control type Control (1), subtype Ack (13), no flags; +#: Duration/ID 0; Address 1 only (IEEE Std 802.11-2020 §9.3.1.3). +ACK = bytes.fromhex('d400' '0000' '020000000001') + +#: An 802.11 Data frame, no flags: Address 1 to 3, Sequence Control with +#: sequence number 1, then an LLC/SNAP body (§9.3.2.1). +DATA = (bytes.fromhex('0800' '0000' '020000000001' '020000000002' '020000000003' '1000') + + bytes.fromhex('aaaa030000000800')) + +#: A radiotap header whose one present bit, Flags (bit 1), selects a single +#: octet, so ``it_len`` is 9 and the 802.11 frame starts there. +RADIOTAP = bytes.fromhex('00' '00' '0900' '02000000' '00') + + +def _pcap(linktype: int, records: 'tuple[bytes, ...]') -> bytes: + """A little-endian PCAP v2.4 file of ``records`` on ``linktype``.""" + data = struct.pack(' bytes: + """A little-endian Enhanced Packet Block of ``record`` on interface 0.""" + padded = record + bytes(-len(record) % 4) + length = 32 + len(padded) + return (struct.pack(' bytes: + """A little-endian PCAP-NG file: one section, one interface on ``linktype``.""" + shb = struct.pack(' None: + reimport_once_per_class(self) + + def _extract(self, data: bytes, suffix: str) -> 'list': # type: ignore[type-arg] + import pcapkit + + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, f'wlan{suffix}') + with open(path, 'wb') as file: + file.write(data) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + extractor = pcapkit.extract(fin=path, nofile=True, store=True) + return list(extractor.frame) + + def _assert_rebuilds(self, layer, **kwargs) -> None: # type: ignore[no-untyped-def] + """``layer`` rebuilds byte-exactly from its info, and from ``info.to_dict()``.""" + for data in (layer.info, layer.info.to_dict()): + with self.subTest(layer=type(layer).__name__, data=type(data).__name__): + self.assertEqual(type(layer).from_data(data, **kwargs).data.hex(), layer.data.hex()) + + def _assert_frames(self, frames: 'list', records: 'tuple[bytes, ...]', # type: ignore[type-arg] + chains: 'list[str]', pcapng: bool) -> None: + self.assertEqual([frame.protochain.chain for frame in frames], chains) + for frame, record in zip(frames, records): + self.assertEqual(bytes(frame.payload.data), record) + if pcapng: + self._assert_rebuilds(frame, num=frame._fnum, sct=frame._sect, ctx=frame._ctx) + else: + self._assert_rebuilds(frame, num=frame._fnum, header=frame._ghdr) + layer = frame.payload + while type(layer).__name__ not in ('Raw', 'NoPayload'): + self._assert_rebuilds(layer) + layer = layer.payload + + def test_registered_against_frame_and_pcapng(self) -> None: + from pcapkit.const.reg.linktype import LinkType + from pcapkit.corekit.module import ModuleDescriptor + from pcapkit.protocols.link.ieee802_11 import IEEE802_11 + from pcapkit.protocols.link.radiotap import Radiotap + from pcapkit.protocols.misc.pcap.frame import Frame + from pcapkit.protocols.misc.pcapng import PCAPNG + + for cls in (Frame, PCAPNG): + for code, target in ((LinkType.IEEE802_11, IEEE802_11), + (LinkType.IEEE802_11_RADIOTAP, Radiotap)): + with self.subTest(table=cls.__name__, code=code.name): + self.assertEqual(cls.__proto__[code], + ModuleDescriptor('pcapkit.protocols.link', target.__name__)) + self.assertIs(cls._lookup_next_layer(cls.__proto__, code), target) + with self.subTest(table=cls.__name__, index='IEEE802_11'): + self.assertIs(cls._lookup_next_layer(cls.__proto__, IEEE802_11.__index__()), IEEE802_11) + self.assertIs(IEEE802_11.__index__(), LinkType.IEEE802_11) + self.assertEqual(IEEE802_11.__index__(), 105) + self.assertEqual(int(LinkType.IEEE802_11_RADIOTAP), 127) + + def test_native_802_11_capture(self) -> None: + """Link type 105 carries the MAC frame with no header in front of it.""" + from pcapkit.const.ieee802_11.control_subtype import ControlSubtype + from pcapkit.const.ieee802_11.type import Type + from pcapkit.const.reg.linktype import LinkType + + records = (ACK, DATA) + for pcapng, (data, suffix) in enumerate(((_pcap(LinkType.IEEE802_11, records), '.pcap'), + (_pcapng(LinkType.IEEE802_11, records), '.pcapng'))): + with self.subTest(format=suffix): + frames = self._extract(data, suffix) + self._assert_frames(frames, records, ['802.11', '802.11:Raw'], bool(pcapng)) + + ack = frames[0].payload.info + self.assertEqual((ack.frame_control.type, ack.frame_control.subtype), + (Type.Control, ControlSubtype.ACK)) + self.assertEqual(ack.addr1, '02:00:00:00:00:01') + self.assertIsNone(ack.addr2) + body = frames[1].payload + self.assertEqual(body.info.addr3, '02:00:00:00:00:03') + self.assertEqual(body.info.seq_ctrl.sequence_number, 1) + self.assertEqual(bytes(body.payload.data), DATA[24:]) + + def test_radiotap_capture(self) -> None: + """Link type 127 carries a radiotap header, then the MAC frame at ``it_len``.""" + from pcapkit.const.reg.linktype import LinkType + + records = (RADIOTAP + ACK, RADIOTAP + DATA) + for pcapng, (data, suffix) in enumerate(((_pcap(LinkType.IEEE802_11_RADIOTAP, records), '.pcap'), + (_pcapng(LinkType.IEEE802_11_RADIOTAP, records), '.pcapng'))): + with self.subTest(format=suffix): + frames = self._extract(data, suffix) + self._assert_frames(frames, records, ['Radiotap:802.11', 'Radiotap:802.11:Raw'], bool(pcapng)) + + for frame, mac in zip(frames, (ACK, DATA)): + radiotap = frame.payload + self.assertEqual((radiotap.info.it_version, radiotap.info.it_len), (0, 9)) + self.assertEqual(radiotap.length, 9) + self.assertEqual(bytes(radiotap.payload.data), mac) + self.assertEqual(radiotap.payload.info.addr1, '02:00:00:00:00:01') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/link/test_ieee802_11_unit.py b/tests/protocols/link/test_ieee802_11_unit.py index 2d1bba2086..ff4e1b2885 100644 --- a/tests/protocols/link/test_ieee802_11_unit.py +++ b/tests/protocols/link/test_ieee802_11_unit.py @@ -422,12 +422,11 @@ def test_make_rejects_what_does_not_fit(self) -> None: IEEE802_11(**kwargs) def test_id_index_and_names(self) -> None: + from pcapkit.const.reg.linktype import LinkType from pcapkit.protocols.link.ieee802_11 import IEEE802_11 - from pcapkit.utilities.exceptions import UnsupportedCall self.assertEqual(IEEE802_11.id(), ('IEEE802_11',)) - with self.assertRaises(UnsupportedCall): - IEEE802_11.__index__() + self.assertIs(IEEE802_11.__index__(), LinkType.IEEE802_11) # 105 (#2608) made = IEEE802_11() self.assertEqual((made.name, made.alias), ('IEEE 802.11 Wireless LAN', '802.11')) self.assertEqual(made.__length_hint__(), 36) diff --git a/tests/protocols/test_dispatch_registry_unit.py b/tests/protocols/test_dispatch_registry_unit.py index bb9fcee35b..d22580347c 100644 --- a/tests/protocols/test_dispatch_registry_unit.py +++ b/tests/protocols/test_dispatch_registry_unit.py @@ -232,9 +232,11 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: # pcap and pcapng frame tables, #2439's IGMP, #2436's # Internet.__proto__[TransType.ICMP] and [TransType.IPv6_ICMP], #2489's # Internet.__proto__[TransType.DCCP], #2560's two NLPIDs of - # FRAD.__proto__, and #2575's Radiotap.__proto__[LinkType.IEEE802_11]. - self.assertEqual(len(cases), 60, - 'expected exactly 60 entries across the seven __proto__ ' + # FRAD.__proto__, #2575's Radiotap.__proto__[LinkType.IEEE802_11], and + # #2608's LinkType.IEEE802_11 and LinkType.IEEE802_11_RADIOTAP in both + # link-type tables. + self.assertEqual(len(cases), 64, + 'expected exactly 64 entries across the seven __proto__ ' 'tables named in GitHub issue #496, the loopback table ' 'of #1574, the FRAD table of #2560 and the radiotap table ' 'of #2575; a different count means a registry changed ' @@ -245,7 +247,7 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: by_family[case.family] = by_family.get(case.family, 0) + 1 self.assertEqual(by_family, { 'link': 7, 'internet': 21, 'tcp': 5, 'udp': 4, 'sctp': 2, - 'pcap-frame': 7, 'pcapng-frame': 7, 'loopback': 4, 'frad': 2, + 'pcap-frame': 9, 'pcapng-frame': 9, 'loopback': 4, 'frad': 2, 'radiotap': 1, })