From aac4e2714f52fef81985bd31e85f1fc36803c76b Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sat, 31 Oct 2026 18:09:14 -0400 Subject: [PATCH] feat(link): add GRE dissector for IPGRE/IP6GRE on LINUX_SLL/SLL2 Part of #2460. - Add pcapkit.protocols.link.gre.GRE, dissecting the RFC 2784 base header (C/Reserved0/Ver/Protocol Type, plus optional Checksum/Reserved1) and the RFC 2890 Key/Sequence Number extensions, gated by the C/K/S flag bits. - Wire LinuxSLL/LinuxSLL1/LinuxSLL2 to dispatch both ARPHRD_IPGRE and ARPHRD_IP6GRE to this one class -- same GRE framing either way, the "IP"/"IP6" distinction being only which outer IP version the tunnel carries. - Protocol Type dispatches onward as an EtherType through the shared Link.__proto__ registry (as VLAN/Ethernet do), so GRE keeps no registry of its own, unlike FRAD's NLPID-keyed one. - Register in all 8 places; add docs page and changelog entry. Build and tests: pylint 10.00/10, mypy clean on touched files; new tests/protocols/link/test_gre_unit.py (17 passed/42 subtests); tests/protocols/link/ (170 passed/922 subtests); tests/project/ full run (2132 passed, 80 skipped, 10619 subtests, 0 failed). --- CHANGELOG.md | 2 + docs/source/changelog/1.6.0.rst | 17 + docs/source/pcapkit/protocols/link/gre.rst | 90 +++++ docs/source/pcapkit/protocols/link/index.rst | 1 + pcapkit/__init__.py | 2 +- pcapkit/all.py | 2 +- pcapkit/protocols/__init__.py | 2 +- pcapkit/protocols/data/__init__.py | 3 + pcapkit/protocols/data/link/__init__.py | 6 + pcapkit/protocols/data/link/gre.py | 57 +++ pcapkit/protocols/link/__init__.py | 3 +- pcapkit/protocols/link/gre.py | 354 ++++++++++++++++++ pcapkit/protocols/link/linux_sll.py | 22 +- pcapkit/protocols/schema/__init__.py | 1 + pcapkit/protocols/schema/link/__init__.py | 2 + pcapkit/protocols/schema/link/gre.py | 91 +++++ .../test_rfc_constants_one_home_1739_unit.py | 13 + tests/protocols/link/test_gre_unit.py | 278 ++++++++++++++ tests/protocols/link/test_linux_sll1_unit.py | 33 +- tests/protocols/link/test_linux_sll2_unit.py | 28 +- .../test_short_read_roundtrip_unit.py | 3 + 21 files changed, 993 insertions(+), 17 deletions(-) create mode 100644 docs/source/pcapkit/protocols/link/gre.rst create mode 100644 pcapkit/protocols/data/link/gre.py create mode 100644 pcapkit/protocols/link/gre.py create mode 100644 pcapkit/protocols/schema/link/gre.py create mode 100644 tests/protocols/link/test_gre_unit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a616c60e..87e6c3f8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Wave 2 of the roadmap, per the contributing docs' enhancement-proposal page: the protocol pairs that wanted a shared abstract base, and the two that simply had no dissector at all. +- **Added** -- `GRE`, a dissector for the Generic Routing Encapsulation base header ([RFC 2784](https://datatracker.ietf.org/doc/html/rfc2784)) plus the optional Key and Sequence Number extensions ([RFC 2890](https://datatracker.ietf.org/doc/html/rfc2890)). `LinuxSLL`/ `LinuxSLL1`/ `LinuxSLL2` dispatch to it for both the `ARPHRD_IPGRE` and `ARPHRD_IP6GRE` cases -- the same GRE framing either way, the "IP"/"IP6" distinction being only which outer IP version the tunnel carries -- instead of falling back to `Raw`. The Protocol Type field dispatches onward as an EtherType, through the same family registry (`Link.__proto__`) Ethernet, VLAN and LinuxSLL already share. One of [#2460](https://github.com/JarryShaw/PyPCAPKit/issues/2460)'s five `arphrd_type` follow-ups to get a dissector (`NETLINK` and `FRAD` are tracked separately on [#2507](https://github.com/JarryShaw/PyPCAPKit/pull/2507) and [#2499](https://github.com/JarryShaw/PyPCAPKit/pull/2499)); `IEEE80211_RADIOTAP` remains `Raw`. + --- Full changelog: diff --git a/docs/source/changelog/1.6.0.rst b/docs/source/changelog/1.6.0.rst index 4800343b1f..7f0e90d720 100644 --- a/docs/source/changelog/1.6.0.rst +++ b/docs/source/changelog/1.6.0.rst @@ -4,3 +4,20 @@ Wave 2 of the roadmap, per the contributing docs' enhancement-proposal page: the protocol pairs that wanted a shared abstract base, and the two that simply had no dissector at all. + +* **Added** -- :class:`~pcapkit.protocols.link.gre.GRE`, a dissector for the + Generic Routing Encapsulation base header (:rfc:`2784`) plus the optional Key + and Sequence Number extensions (:rfc:`2890`). + :class:`~pcapkit.protocols.link.linux_sll.LinuxSLL`/ + :class:`~pcapkit.protocols.link.linux_sll1.LinuxSLL1`/ + :class:`~pcapkit.protocols.link.linux_sll2.LinuxSLL2` dispatch to it for both + the ``ARPHRD_IPGRE`` and ``ARPHRD_IP6GRE`` cases -- the same GRE framing either + way, the "IP"/"IP6" distinction being only which outer IP version the tunnel + carries -- instead of falling back to + :class:`~pcapkit.protocols.misc.raw.Raw`. The Protocol Type field dispatches + onward as an EtherType, through the same family registry + (:attr:`Link.__proto__ `) + Ethernet, VLAN and LinuxSLL already share. One of :issue:`2460`'s five + ``arphrd_type`` follow-ups to get a dissector (``NETLINK`` and ``FRAD`` are + tracked separately on :pr:`2507` and :pr:`2499`); ``IEEE80211_RADIOTAP`` + remains :class:`~pcapkit.protocols.misc.raw.Raw`. diff --git a/docs/source/pcapkit/protocols/link/gre.rst b/docs/source/pcapkit/protocols/link/gre.rst new file mode 100644 index 0000000000..625e9a87da --- /dev/null +++ b/docs/source/pcapkit/protocols/link/gre.rst @@ -0,0 +1,90 @@ +GRE - Generic Routing Encapsulation +==================================== + +.. module:: pcapkit.protocols.link.gre + +:mod:`pcapkit.protocols.link.gre` contains +:class:`~pcapkit.protocols.link.gre.GRE` +only, which implements the extractor for the Generic Routing Encapsulation +base header [*]_ plus the optional Key and Sequence Number extensions [*]_, as +seen on a ``LINKTYPE_LINUX_SLL``/``LINKTYPE_LINUX_SLL2`` capture whose +``arphrd_type`` is ``ARPHRD_IPGRE`` or ``ARPHRD_IP6GRE`` -- both land on this +one class, the "IP"/"IP6" distinction being only which outer IP version the +tunnel carries. Its structure, with every optional field present, is +described as below: + +.. table:: + + ====== ==== ========================= ==================================== + Octets Bits Name Description + ====== ==== ========================= ==================================== + 0 0 ``gre.checksum_present`` Checksum Present (``C``) + ------ ---- ------------------------- ------------------------------------ + 0 2 ``gre.key_present`` Key Present (``K``), :rfc:`2890` + ------ ---- ------------------------- ------------------------------------ + 0 3 ``gre.sequence_present`` Sequence Number Present (``S``), :rfc:`2890` + ------ ---- ------------------------- ------------------------------------ + 0 13 ``gre.version`` Version Number (``0``) + ------ ---- ------------------------- ------------------------------------ + 2 16 ``gre.protocol_type`` Protocol Type (EtherType) + ------ ---- ------------------------- ------------------------------------ + 4 32 ``gre.checksum`` Checksum, present only if ``C`` + ------ ---- ------------------------- ------------------------------------ + 6 48 ``gre.reserved1`` Reserved1, present only if ``C`` + ------ ---- ------------------------- ------------------------------------ + 8/4 64 ``gre.key`` Key, present only if ``K``, :rfc:`2890` + ------ ---- ------------------------- ------------------------------------ + 12/8/4 96 ``gre.sequence_number`` Sequence Number, present only if ``S``, :rfc:`2890` + ====== ==== ========================= ==================================== + +``C``, ``K`` and ``S`` each gate their own field independently, so the header +is 4, 8, 12 or 16 octets depending on which are set. The Protocol Type field +is an EtherType, dispatched onward through +:attr:`Link.__proto__ `, the same +family registry Ethernet, VLAN and LinuxSLL already share -- unlike +:class:`~pcapkit.protocols.link.frad.FRAD`, this class keeps no registry of +its own. A nonzero Version Number (RFC 2637's "Enhanced GRE"/PPTP, version 1) +is a different, incompatible protocol and is refused rather than misread; see +the module docstring's *Scope* section. + +.. autoclass:: pcapkit.protocols.link.gre.GRE + :no-members: + :show-inheritance: + + .. autoproperty:: name + .. autoproperty:: alias + .. autoproperty:: length + .. autoproperty:: protocol + + .. automethod:: id + + .. automethod:: read + .. automethod:: make + + .. automethod:: __length_hint__ + .. automethod:: __index__ + + .. automethod:: _make_data + +Header Schemas +-------------- + +.. module:: pcapkit.protocols.schema.link.gre + +.. autoclass:: pcapkit.protocols.schema.link.gre.GRE + :members: + :show-inheritance: + +Data Models +----------- + +.. module:: pcapkit.protocols.data.link.gre + +.. autoclass:: pcapkit.protocols.data.link.gre.GRE + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] :rfc:`2784` +.. [*] :rfc:`2890` diff --git a/docs/source/pcapkit/protocols/link/index.rst b/docs/source/pcapkit/protocols/link/index.rst index f0b47982e2..ec598c0523 100644 --- a/docs/source/pcapkit/protocols/link/index.rst +++ b/docs/source/pcapkit/protocols/link/index.rst @@ -24,6 +24,7 @@ link layer, with detailed implementation and methods. link ethernet arp + gre l2tp l2tpv2 linux_sll diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index 19841fab0c..f5f397f7f5 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -111,7 +111,7 @@ 'NoPayload', # No Payload 'Raw', # Raw Packet - 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', # Link Layer + 'ARP', 'C_Tag', 'Ethernet', 'GRE', 'InARP', 'L2TP', # Link Layer 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', # Internet Layer diff --git a/pcapkit/all.py b/pcapkit/all.py index 20b9563aa7..3e69a42ab8 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -120,7 +120,7 @@ 'PCAPNG', # PCAPNG Format 'NoPayload', # No Payload 'Raw', # Raw Packet - 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', + 'ARP', 'C_Tag', 'Ethernet', 'GRE', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', # Link Layer 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', # Internet Layer 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', diff --git a/pcapkit/protocols/__init__.py b/pcapkit/protocols/__init__.py index 5aab10622d..b9e41abe59 100644 --- a/pcapkit/protocols/__init__.py +++ b/pcapkit/protocols/__init__.py @@ -50,7 +50,7 @@ 'Raw', # Link Layer - 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', + 'ARP', 'C_Tag', 'Ethernet', 'GRE', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', # Internet Layer diff --git a/pcapkit/protocols/data/__init__.py b/pcapkit/protocols/data/__init__.py index 4fee25a195..c5979c90b0 100644 --- a/pcapkit/protocols/data/__init__.py +++ b/pcapkit/protocols/data/__init__.py @@ -62,6 +62,9 @@ # Ethernet Protocol 'Ethernet', + # Generic Routing Encapsulation + 'GRE', + # Layer Two Tunnelling Protocol 'L2TP', 'L2TP_Flags', diff --git a/pcapkit/protocols/data/link/__init__.py b/pcapkit/protocols/data/link/__init__.py index cae293e6cc..2610d8f9d8 100644 --- a/pcapkit/protocols/data/link/__init__.py +++ b/pcapkit/protocols/data/link/__init__.py @@ -9,6 +9,9 @@ # Ethernet Protocol from pcapkit.protocols.data.link.ethernet import Ethernet +# Generic Routing Encapsulation +from pcapkit.protocols.data.link.gre import GRE + # Layer Two Tunnelling Protocol from pcapkit.protocols.data.link.l2tp import L2TP from pcapkit.protocols.data.link.l2tp import Flags as L2TP_Flags @@ -31,6 +34,9 @@ # Ethernet Protocol 'Ethernet', + # Generic Routing Encapsulation + 'GRE', + # Layer Two Tunnelling Protocol 'L2TP', 'L2TP_Flags', diff --git a/pcapkit/protocols/data/link/gre.py b/pcapkit/protocols/data/link/gre.py new file mode 100644 index 0000000000..d0c8271f84 --- /dev/null +++ b/pcapkit/protocols/data/link/gre.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""data models for Generic Routing Encapsulation (GRE) frames""" + +from typing import TYPE_CHECKING + +from pcapkit.corekit.infoclass import info_final +from pcapkit.protocols.data.protocol import Protocol + +if TYPE_CHECKING: + from typing import Optional + + from pcapkit.const.reg.ethertype import EtherType + +__all__ = ['GRE'] + + +@info_final +class GRE(Protocol): + """Data model for a Generic Routing Encapsulation (GRE) frame.""" + + #: Checksum Present bit (``C``), :rfc:`2784`. + checksum_present: 'bool' + #: Key Present bit (``K``), :rfc:`2890`. + key_present: 'bool' + #: Sequence Number Present bit (``S``), :rfc:`2890`. + sequence_present: 'bool' + #: Reserved bits of the flags word outside ``C``/``K``/``S``/``Ver``, in + #: their on-wire positions (i.e. masked by ``0x4FF8``). :rfc:`2784` calls + #: the whole 12-bit span (before :rfc:`2890` carved ``K``/``S`` out of it) + #: ``Reserved0`` and requires it sent as zero; carried verbatim so a + #: rebuild reproduces the word as it arrived. + reserved: 'int' + #: Version Number; :rfc:`2784` requires this to be ``0``. + version: 'int' + #: Protocol Type: the EtherType of the encapsulated payload. + protocol_type: 'EtherType' + #: Checksum, present only when :attr:`checksum_present` is set. + checksum: 'Optional[int]' + #: Reserved1, present only when :attr:`checksum_present` is set; + #: :rfc:`2784` requires it sent as zero, carried verbatim. + reserved1: 'Optional[int]' + #: Key, present only when :attr:`key_present` is set, :rfc:`2890`. + key: 'Optional[int]' + #: Sequence Number, present only when :attr:`sequence_present` is set, + #: :rfc:`2890`. + sequence_number: 'Optional[int]' + #: Header length, in octets -- ``4``, plus ``4`` more for each of + #: :attr:`checksum_present`, :attr:`key_present` and + #: :attr:`sequence_present` that is set. + len: 'int' + + if TYPE_CHECKING: + def __init__(self, checksum_present: 'bool', key_present: 'bool', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + sequence_present: 'bool', reserved: 'int', version: 'int', # pylint: disable=unused-argument + protocol_type: 'EtherType', checksum: 'Optional[int]', # pylint: disable=unused-argument + reserved1: 'Optional[int]', key: 'Optional[int]', # pylint: disable=unused-argument + sequence_number: 'Optional[int]', len: 'int') -> 'None': ... # pylint: disable=unused-argument,redefined-builtin diff --git a/pcapkit/protocols/link/__init__.py b/pcapkit/protocols/link/__init__.py index 0a35e6a5db..9a4daa5196 100644 --- a/pcapkit/protocols/link/__init__.py +++ b/pcapkit/protocols/link/__init__.py @@ -17,6 +17,7 @@ # Link Layer Protocols from pcapkit.protocols.link.arp import ARP, InARP from pcapkit.protocols.link.ethernet import Ethernet +from pcapkit.protocols.link.gre import GRE from pcapkit.protocols.link.loopback import Loopback # Linux "Cooked" Capture Encapsulation Family @@ -41,6 +42,6 @@ 'LINKTYPE', # Link Layer Protocols - 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', + 'ARP', 'C_Tag', 'Ethernet', 'GRE', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', ] diff --git a/pcapkit/protocols/link/gre.py b/pcapkit/protocols/link/gre.py new file mode 100644 index 0000000000..b54b559ee0 --- /dev/null +++ b/pcapkit/protocols/link/gre.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +"""GRE - Generic Routing Encapsulation +========================================= + +.. module:: pcapkit.protocols.link.gre + +:mod:`pcapkit.protocols.link.gre` contains +:class:`~pcapkit.protocols.link.gre.GRE` only, which implements the extractor +for the Generic Routing Encapsulation base header [:rfc:`2784`] plus the +optional Key and Sequence Number extensions [:rfc:`2890`], as seen on a +``LINKTYPE_LINUX_SLL``/``LINKTYPE_LINUX_SLL2`` capture whose ``arphrd_type`` +is ``ARPHRD_IPGRE`` or ``ARPHRD_IP6GRE`` -- both land on this one class: +:class:`~pcapkit.protocols.link.linux_sll.LinuxSLL` dispatches here instead +of falling back to :class:`~pcapkit.protocols.misc.raw.Raw` for either case; +see :data:`~pcapkit.protocols.link.linux_sll._ARPHRD_NOT_ETHERTYPE`. The +"IP"/"IP6" distinction in the two ``arphrd_type`` names is about which IP +version the tunnel carries on its *outer* layer -- i.e. which ``arphrd_type`` +fired -- not a different GRE header shape; the GRE framing itself is +identical either way. + +Structure, the base header plus every optional field (:rfc:`2784` section +2.1, :rfc:`2890` section 2): + +.. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |C| |K|S| Reserved0 | Ver | Protocol Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Checksum (optional) | Reserved1 (optional) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Key (optional) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number (optional) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +``C`` gates the Checksum/Reserved1 pair, ``K`` gates the Key field and ``S`` +gates the Sequence Number field, each independently -- so the header is +4, 8, 12 or 16 octets depending on which of the three are set. ``Ver`` is +:rfc:`2784`'s 3-bit Version Number, which MUST be ``0``; a nonzero version is +a different protocol entirely (RFC 2637's "Enhanced GRE", the PPTP variant, +is version ``1``) and is not implemented here. The Protocol Type field is an +EtherType selecting the encapsulated payload -- resolved through +:attr:`Link.__proto__ `, the same +family registry Ethernet, VLAN and LinuxSLL already share, exactly as +:class:`~pcapkit.protocols.link.vlan.VLAN` resolves its own ``type`` field. +This class therefore keeps no registry of its own. + +Scope +----- + +Only the framing above is dissected. The Key and Sequence Number fields are +recorded but not interpreted further -- their meaning (which traffic flow, and +where in its sequence) is a matter for the tunnel endpoints, not something a +passive dissector can resolve from the header alone. RFC 2637's Enhanced GRE +(version 1, used by PPTP) has a different, incompatible layout from this point +on -- a Key field split into Payload Length and Call ID, and an optional +Acknowledgment Number -- and is out of scope; :meth:`read` refuses it rather +than misreading it as RFC 2784/2890 GRE. + +.. versionadded:: 1.6.0 + +""" +from typing import TYPE_CHECKING + +from pcapkit.const.reg.ethertype import EtherType as Enum_EtherType +from pcapkit.protocols.data.link.gre import GRE as Data_GRE +from pcapkit.protocols.link.link import Link +from pcapkit.protocols.schema.link.gre import GRE as Schema_GRE +from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall + +if TYPE_CHECKING: + from enum import IntEnum as StdlibEnum + from typing import Any, NoReturn, Optional, Type + + from aenum import IntEnum as AenumEnum + from typing_extensions import Literal + + from pcapkit.protocols.protocol import ProtocolBase + from pcapkit.protocols.schema.schema import Schema + +__all__ = ['GRE'] + + +class GRE(Link[Data_GRE, Schema_GRE], + schema=Schema_GRE, data=Data_GRE): + """This class implements Generic Routing Encapsulation (GRE). + + The Protocol Type field is dispatched as an EtherType through + :attr:`Link.__proto__ `, + exactly as :class:`~pcapkit.protocols.link.vlan.VLAN` resolves its own + ``type`` field -- so, unlike + :class:`~pcapkit.protocols.link.frad.FRAD`, this class declares no + registry of its own. + + """ + + ########################################################################## + # Properties. + ########################################################################## + + @property + def name(self) -> 'Literal["Generic Routing Encapsulation"]': + """Name of current protocol.""" + return 'Generic Routing Encapsulation' + + @property + def alias(self) -> 'Literal["GRE"]': + """Acronym of corresponding protocol.""" + return 'GRE' + + @property + def length(self) -> 'int': + """Header length of current protocol.""" + return self._info.len + + @property + def protocol(self) -> 'Enum_EtherType': + """Name of next layer protocol.""" + return self._info.protocol_type + + ########################################################################## + # Methods. + ########################################################################## + + @classmethod + def id(cls) -> 'tuple[Literal["GRE"]]': + """Index ID of the protocol.""" + return ('GRE',) + + def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_GRE': # pylint: disable=unused-argument + """Read Generic Routing Encapsulation (GRE) frame. + + Structure of a GRE frame, with every optional field present; see the + module docstring for the full diagram: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |C| |K|S| Reserved0 | Ver | Protocol Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + length: Length of packet data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Parsed packet data. + + Raises: + ProtocolError: If the Version Number is not ``0`` -- see the + module docstring's *Scope* section. + + """ + if length is None: + length = len(self) + schema = self.__header__ + + flags = schema.flags + if flags['version']: + raise ProtocolError(f'{self.alias}: invalid version: {flags["version"]}') + + _hdr_len = 4 + _checksum = None # type: Optional[int] + _reserved1 = None # type: Optional[int] + if flags['checksum_present']: + _checksum = schema.checksum + _reserved1 = schema.reserved1 + _hdr_len += 4 + + _key = None # type: Optional[int] + if flags['key_present']: + _key = schema.key + _hdr_len += 4 + + _sequence_number = None # type: Optional[int] + if flags['sequence_present']: + _sequence_number = schema.sequence_number + _hdr_len += 4 + + gre = Data_GRE( + checksum_present=bool(flags['checksum_present']), + key_present=bool(flags['key_present']), + sequence_present=bool(flags['sequence_present']), + reserved=(flags['reserved_1'] << 14) | (flags['reserved_2'] << 3), + version=flags['version'], + protocol_type=schema.protocol_type, + checksum=_checksum, + reserved1=_reserved1, + key=_key, + sequence_number=_sequence_number, + len=_hdr_len, + ) + return self._decode_next_layer(gre, schema.protocol_type, length - _hdr_len) + + def make(self, + checksum: 'Optional[int]' = None, + reserved1: 'Optional[int]' = None, + key: 'Optional[int]' = None, + sequence_number: 'Optional[int]' = None, + reserved: 'int' = 0, + version: 'Literal[0]' = 0, + protocol_type: 'Enum_EtherType | StdlibEnum | AenumEnum | str | int' = Enum_EtherType.Internet_Protocol_version_4, # pylint: disable=line-too-long + protocol_type_default: 'Optional[int]' = None, + protocol_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + protocol_type_reversed: 'bool' = False, + payload: 'bytes | ProtocolBase | Schema' = b'', + **kwargs: 'Any') -> 'Schema_GRE': + """Make (construct) packet data. + + The presence of each optional field is inferred from whether its + value is given, as :meth:`L2TPv2.make + ` infers ``S`` from + whether ``ns``/``nr`` are given: passing ``checksum`` sets ``C``, + ``key`` sets ``K``, and ``sequence_number`` sets ``S``. + + Args: + checksum: Checksum; setting this also sets ``C``. + reserved1: Reserved1; only meaningful, and only accepted, when + ``checksum`` is also given. Defaults to ``0`` when + ``checksum`` is given and this is not. + key: Key, :rfc:`2890`; setting this also sets ``K``. + sequence_number: Sequence Number, :rfc:`2890`; setting this also + sets ``S``. + reserved: Reserved bits of the flags word, in their on-wire + positions (bit 1 and bits 4-12, i.e. within ``0x4FF8``). + version: Version Number; :rfc:`2784` fixes this at ``0``. + protocol_type: Protocol Type: the EtherType of the encapsulated + payload. + protocol_type_default: Default value of ``protocol_type``. + protocol_type_namespace: Namespace of ``protocol_type``. + protocol_type_reversed: Reversed namespace of ``protocol_type``. + payload: Payload data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed packet data. + + Raises: + ProtocolError: If ``reserved1`` is given without ``checksum``; if + ``reserved`` sets a bit outside ``0x4FF8``; if ``version`` + does not fit in 3 bits; or if ``checksum``, ``key`` or + ``sequence_number`` is given but does not fit the width of + its field (16 bits for ``checksum``, 32 bits for ``key`` and + ``sequence_number``). + + """ + protocol_type_ = self._make_index(protocol_type, protocol_type_default, + namespace=protocol_type_namespace, + reversed=protocol_type_reversed, pack=False) + + checksum_present = checksum is not None + if reserved1 is not None and not checksum_present: + raise ProtocolError(f'{self.alias}: reserved1 given without checksum') + if checksum_present: + if (not isinstance(checksum, int) or isinstance(checksum, bool) + or not 0 <= checksum <= 0xFFFF): + raise ProtocolError(f'{self.alias}: invalid checksum: {checksum!r}') + if reserved1 is None: + reserved1 = 0 + elif (not isinstance(reserved1, int) or isinstance(reserved1, bool) + or not 0 <= reserved1 <= 0xFFFF): + raise ProtocolError(f'{self.alias}: invalid reserved1: {reserved1!r}') + + key_present = key is not None + if key_present and (not isinstance(key, int) or isinstance(key, bool) + or not 0 <= key <= 0xFFFF_FFFF): + raise ProtocolError(f'{self.alias}: invalid key: {key!r}') + + sequence_present = sequence_number is not None + if sequence_present and (not isinstance(sequence_number, int) + or isinstance(sequence_number, bool) + or not 0 <= sequence_number <= 0xFFFF_FFFF): + raise ProtocolError(f'{self.alias}: invalid sequence_number: {sequence_number!r}') + + if reserved & ~0x4FF8: + raise ProtocolError(f'{self.alias}: reserved bits outside 0x4FF8: {reserved:#06x}') + if not isinstance(version, int) or isinstance(version, bool) or not 0 <= version <= 7: + raise ProtocolError(f'{self.alias}: invalid version: {version!r}') + + return Schema_GRE( + flags={ + 'checksum_present': checksum_present, + 'reserved_1': (reserved >> 14) & 0b1, + 'key_present': key_present, + 'sequence_present': sequence_present, + 'reserved_2': (reserved >> 3) & 0x1FF, + 'version': version, + }, + protocol_type=protocol_type_, # type: ignore[arg-type] + checksum=checksum, + reserved1=reserved1, + key=key, + sequence_number=sequence_number, + payload=payload, + ) + + ########################################################################## + # Data models. + ########################################################################## + + def __length_hint__(self) -> 'Literal[16]': + """Return an estimated length for the object. + + The longest form -- every optional field present, 16 octets -- per + the reason :meth:`L2TPv2.__length_hint__ + ` gives for a + variable-length header: the shortest form would reject a fully + captured longer header as too short. + + """ + return 16 + + @classmethod + def __index__(cls) -> 'NoReturn': # pylint: disable=invalid-index-returned + """Numeral registry index of the protocol. + + Raises: + UnsupportedCall: This protocol is reached only through + :class:`~pcapkit.protocols.link.linux_sll.LinuxSLL`'s + ``arphrd_type`` dispatch, not through a link-layer header + type of its own. + + """ + raise UnsupportedCall(f'{cls.__name__!r} object cannot be interpreted as an integer') + + ########################################################################## + # Utilities. + ########################################################################## + + @classmethod + def _make_data(cls, data: 'Data_GRE') -> 'dict[str, Any]': # type: ignore[override] + """Create key-value pairs from ``data`` for protocol construction. + + Args: + data: protocol data + + Returns: + Key-value pairs for protocol construction. + + """ + return { + 'checksum': data.checksum, + 'reserved1': data.reserved1, + 'key': data.key, + 'sequence_number': data.sequence_number, + 'reserved': data.reserved, + 'version': data.version, + 'protocol_type': data.protocol_type, + 'payload': cls._make_payload(data), + } diff --git a/pcapkit/protocols/link/linux_sll.py b/pcapkit/protocols/link/linux_sll.py index bc5209d8fc..b58dc6f699 100644 --- a/pcapkit/protocols/link/linux_sll.py +++ b/pcapkit/protocols/link/linux_sll.py @@ -150,16 +150,30 @@ def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # `) Returns: - Instance of next layer: :class:`~pcapkit.protocols.misc.raw.Raw`, - recording ``proto``, when ``arphrd_type`` is in - :data:`_ARPHRD_NOT_ETHERTYPE`. + Instance of next layer: :class:`~pcapkit.protocols.link.gre.GRE` + when ``arphrd_type`` is :attr:`ARPHRD.IPGRE + ` or :attr:`ARPHRD.IP6GRE + ` -- both land on the + same GRE framing, see :mod:`pcapkit.protocols.link.gre`; otherwise + :class:`~pcapkit.protocols.misc.raw.Raw`, recording ``proto``, for + every other ``arphrd_type`` in :data:`_ARPHRD_NOT_ETHERTYPE`. """ file_ = self._get_payload() if length is None: length = len(file_) + + if not min(length, len(file_)): + return super()._import_next_layer(proto, length, packet=packet) # type: ignore[misc,call-arg] + arphrd_type = int(self.__header__.arphrd_type) # type: ignore[attr-defined] - if not min(length, len(file_)) or arphrd_type not in _ARPHRD_NOT_ETHERTYPE: + if arphrd_type in (ARPHRD.IPGRE, ARPHRD.IP6GRE): + from pcapkit.protocols.link.gre import GRE # pylint: disable=import-outside-toplevel + return self._parse_next_layer(GRE, file_, length, alias=proto, packet=packet, + layer=self._exlayer, protocol=self._exproto, + __context__=self._exctx) + + if arphrd_type not in _ARPHRD_NOT_ETHERTYPE: return super()._import_next_layer(proto, length, packet=packet) # type: ignore[misc,call-arg] from pcapkit.protocols.misc.raw import Raw # pylint: disable=import-outside-toplevel # isort: skip diff --git a/pcapkit/protocols/schema/__init__.py b/pcapkit/protocols/schema/__init__.py index cf7cca6790..e6a73674cd 100644 --- a/pcapkit/protocols/schema/__init__.py +++ b/pcapkit/protocols/schema/__init__.py @@ -27,6 +27,7 @@ # Link Layer Protocols 'ARP', 'Ethernet', + 'GRE', 'L2TP', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', diff --git a/pcapkit/protocols/schema/link/__init__.py b/pcapkit/protocols/schema/link/__init__.py index b9eae9ffab..678d77a7fd 100644 --- a/pcapkit/protocols/schema/link/__init__.py +++ b/pcapkit/protocols/schema/link/__init__.py @@ -3,6 +3,7 @@ from pcapkit.protocols.schema.link.arp import ARP from pcapkit.protocols.schema.link.ethernet import Ethernet +from pcapkit.protocols.schema.link.gre import GRE from pcapkit.protocols.schema.link.l2tp import L2TP from pcapkit.protocols.schema.link.linux_sll1 import LinuxSLL1 from pcapkit.protocols.schema.link.linux_sll2 import LinuxSLL2 @@ -13,6 +14,7 @@ __all__ = [ 'ARP', 'Ethernet', + 'GRE', 'L2TP', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', diff --git a/pcapkit/protocols/schema/link/gre.py b/pcapkit/protocols/schema/link/gre.py new file mode 100644 index 0000000000..305a7e7579 --- /dev/null +++ b/pcapkit/protocols/schema/link/gre.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=assignment +"""header schema for Generic Routing Encapsulation (GRE) frames""" + +from typing import TYPE_CHECKING + +from pcapkit.const.reg.ethertype import EtherType as Enum_EtherType +from pcapkit.corekit.fields.misc import ConditionalField, PayloadField +from pcapkit.corekit.fields.numbers import EnumField, UInt16Field, UInt32Field +from pcapkit.corekit.fields.strings import BitField +from pcapkit.protocols.schema.schema import Schema, schema_final +from pcapkit.utilities.logging import SPHINX_TYPE_CHECKING + +__all__ = ['GRE'] + +if TYPE_CHECKING: + from typing import Any, Optional + + from pcapkit.protocols.protocol import ProtocolBase + +if SPHINX_TYPE_CHECKING: # pragma: no cover + from typing_extensions import TypedDict + + class FlagsType(TypedDict): + """Type of the flags-and-version word of a GRE header.""" + + #: Checksum Present (``C``). + checksum_present: int + #: Reserved bit 1 of the flags word. + reserved_1: int + #: Key Present (``K``), :rfc:`2890`. + key_present: int + #: Sequence Number Present (``S``), :rfc:`2890`. + sequence_present: int + #: Reserved bits 4-12 of the flags word. + reserved_2: int + #: Version Number; :rfc:`2784` requires this to be ``0``. + version: int + + +@schema_final +class GRE(Schema): + """Header schema for a Generic Routing Encapsulation (GRE) frame. + + The base header [:rfc:`2784`] plus the optional Key and Sequence Number + extensions [:rfc:`2890`], as presented to a ``LINKTYPE_LINUX_SLL``/ + ``LINKTYPE_LINUX_SLL2`` capture whose ``arphrd_type`` is ``ARPHRD_IPGRE`` + or ``ARPHRD_IP6GRE``. + + """ + + #: Flags and version. + flags: 'FlagsType' = BitField(length=2, namespace={ + 'checksum_present': (0, 1), + 'reserved_1': (1, 1), + 'key_present': (2, 1), + 'sequence_present': (3, 1), + 'reserved_2': (4, 9), + 'version': (13, 3), + }) + #: Protocol Type: the EtherType of the encapsulated payload. + protocol_type: 'Enum_EtherType' = EnumField(length=2, namespace=Enum_EtherType) + #: Checksum, present only when ``flags.checksum_present`` is set. + checksum: 'Optional[int]' = ConditionalField( + UInt16Field(), + lambda packet: packet['flags']['checksum_present'], + ) + #: Reserved1, present only when ``flags.checksum_present`` is set. + reserved1: 'Optional[int]' = ConditionalField( + UInt16Field(), + lambda packet: packet['flags']['checksum_present'], + ) + #: Key, present only when ``flags.key_present`` is set, :rfc:`2890`. + key: 'Optional[int]' = ConditionalField( + UInt32Field(), + lambda packet: packet['flags']['key_present'], + ) + #: Sequence Number, present only when ``flags.sequence_present`` is set, + #: :rfc:`2890`. + sequence_number: 'Optional[int]' = ConditionalField( + UInt32Field(), + lambda packet: packet['flags']['sequence_present'], + ) + #: Payload. + payload: 'bytes' = PayloadField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, flags: 'FlagsType', protocol_type: 'Enum_EtherType', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + checksum: 'Optional[int]', reserved1: 'Optional[int]', # pylint: disable=unused-argument + key: 'Optional[int]', sequence_number: 'Optional[int]', # pylint: disable=unused-argument + payload: 'bytes | ProtocolBase | Schema') -> 'None': ... # pylint: disable=unused-argument diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index 73c50ff8da..93f11096bc 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -354,6 +354,19 @@ 'the last code point of the Basic Multilingual Plane, not an IP length', ('pcapkit/dumpkit/common.py', 'codepoint > 65535'): 'above the Basic Multilingual Plane, needs a UTF-16 surrogate pair, not an IP length', + ('pcapkit/protocols/link/gre.py', 'return 16'): + "GRE's own longest possible header -- the 4-octet base plus all three optional " + 'extensions at 4 octets each (:rfc:`2784`/:rfc:`2890`) -- not a PCAP record header', + ('pcapkit/protocols/link/gre.py', '0 <= checksum <= 65535'): + "the max value of GRE's own 2-octet Checksum field (:rfc:`2784#section-2.5`), " + 'not an IP total length', + ('pcapkit/protocols/link/gre.py', '0 <= reserved1 <= 65535'): + "the max value of GRE's own 2-octet Reserved1 field (:rfc:`2784#section-2.6`), " + 'not an IP total length', + ('pcapkit/protocols/schema/link/gre.py', "{'reserved_2': (4, 9)}"): + "the 9-bit span bits 4-12 leave of RFC 2784's 12-bit Reserved0 once RFC 2890 " + 'carves ``K``/``S`` out of it (:rfc:`2890#section-2`), not a PCAP-NG resolution-places ' + 'table', } #: Sweep hits of a homed value in a module whose own protocol owns no constant diff --git a/tests/protocols/link/test_gre_unit.py b/tests/protocols/link/test_gre_unit.py new file mode 100644 index 0000000000..6d64365974 --- /dev/null +++ b/tests/protocols/link/test_gre_unit.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2501: Generic Routing Encapsulation (GRE) frames. + +:class:`~pcapkit.protocols.link.gre.GRE` dissects the ``arphrd_type`` +``ARPHRD_IPGRE``/``ARPHRD_IP6GRE`` payload that +:class:`~pcapkit.protocols.link.linux_sll.LinuxSLL` previously kept whole as +:class:`~pcapkit.protocols.misc.raw.Raw` (see +:data:`~pcapkit.protocols.link.linux_sll._ARPHRD_NOT_ETHERTYPE`). Covers the +base header [:rfc:`2784`], the optional Key and Sequence Number extensions +[:rfc:`2890`], the EtherType-keyed dispatch through +:attr:`Link.__proto__ `, and +byte-exact round trips. The GRE-dispatch side of +``LinuxSLL1``/``LinuxSLL2`` is covered in their own test modules. + +Each test imports :mod:`pcapkit` itself, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import struct +import unittest + +from tests._support import reimport_once_per_class + +#: An IPv4 header (DF clear, protocol UDP) and an 8-octet UDP header. +IPV4 = (bytes.fromhex('4500001c00010000401100000a0000010a000002') + + bytes.fromhex('3039003500080000')) +#: An IPv6 header (next header UDP) and an 8-octet UDP header. +IPV6 = (bytes.fromhex('6000000000081140') + bytes(15) + b'\x01' + bytes(15) + b'\x02' + + bytes.fromhex('3039003500080000')) + +#: Bit positions (MSB-first, per the module docstring's diagram) of the ``C``, +#: ``K`` and ``S`` flags within the 16-bit flags-and-version word. +_C, _K, _S = 1 << 15, 1 << 13, 1 << 12 + + +def _gre(protocol_type: int, payload: bytes, *, checksum: 'int | None' = None, + reserved1: int = 0, key: 'int | None' = None, sequence_number: 'int | None' = None, + reserved: int = 0, version: int = 0) -> bytes: + """Pack a bare GRE header plus ``payload``; see the module docstring for the layout.""" + flags = version | reserved + data = b'' + if checksum is not None: + flags |= _C + data += struct.pack('>HH', checksum, reserved1) + if key is not None: + flags |= _K + data += struct.pack('>I', key) + if sequence_number is not None: + flags |= _S + data += struct.pack('>I', sequence_number) + return struct.pack('>HH', flags, protocol_type) + data + payload + + +class TestGRE(unittest.TestCase): + """Pin the GRE layer, alone.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + 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 test_base_header_dispatches_ipv4_and_ipv6(self) -> None: + """:rfc:`2784`'s base header, no optional fields -- the common case.""" + from pcapkit.protocols.link.gre import GRE + + for protocol_type, payload, name in ((0x0800, IPV4, 'IPv4'), (0x86DD, IPV6, 'IPv6')): + with self.subTest(protocol_type=hex(protocol_type)): + raw = _gre(protocol_type, payload) + parsed = GRE(raw, len(raw)) + self.assertEqual((parsed.info.checksum_present, parsed.info.key_present, + parsed.info.sequence_present), (False, False, False)) + self.assertEqual(parsed.info.version, 0) + self.assertIsNone(parsed.info.checksum) + self.assertIsNone(parsed.info.reserved1) + self.assertIsNone(parsed.info.key) + self.assertIsNone(parsed.info.sequence_number) + self.assertEqual(parsed.protocol, protocol_type) + self.assertEqual((parsed.name, parsed.alias, parsed.length), + ('Generic Routing Encapsulation', 'GRE', 4)) + self.assertEqual(type(parsed.payload).__name__, name) + self.assertEqual(parsed.protochain.chain, f'GRE:{name}:UDP') + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_checksum_present_adds_checksum_and_reserved1(self) -> None: + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, checksum=0x1234, reserved1=0) + parsed = GRE(raw, len(raw)) + self.assertTrue(parsed.info.checksum_present) + self.assertFalse(parsed.info.key_present) + self.assertFalse(parsed.info.sequence_present) + self.assertEqual(parsed.info.checksum, 0x1234) + self.assertEqual(parsed.info.reserved1, 0) + self.assertEqual(parsed.length, 8) + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_key_present_adds_key_rfc_2890(self) -> None: + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, key=0xDEADBEEF) + parsed = GRE(raw, len(raw)) + self.assertTrue(parsed.info.key_present) + self.assertFalse(parsed.info.checksum_present) + self.assertFalse(parsed.info.sequence_present) + self.assertEqual(parsed.info.key, 0xDEADBEEF) + self.assertEqual(parsed.length, 8) + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_sequence_present_adds_sequence_number_rfc_2890(self) -> None: + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, sequence_number=12345) + parsed = GRE(raw, len(raw)) + self.assertTrue(parsed.info.sequence_present) + self.assertFalse(parsed.info.checksum_present) + self.assertFalse(parsed.info.key_present) + self.assertEqual(parsed.info.sequence_number, 12345) + self.assertEqual(parsed.length, 8) + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_every_optional_field_together_is_the_longest_form(self) -> None: + """``C``, ``K`` and ``S`` all set: the 16-octet longest form.""" + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, checksum=0xBEEF, reserved1=0, key=0x11223344, sequence_number=99) + parsed = GRE(raw, len(raw)) + self.assertEqual((parsed.info.checksum_present, parsed.info.key_present, + parsed.info.sequence_present), (True, True, True)) + self.assertEqual((parsed.info.checksum, parsed.info.key, parsed.info.sequence_number), + (0xBEEF, 0x11223344, 99)) + self.assertEqual(parsed.length, 16) + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_reserved_bits_round_trip_even_when_non_zero(self) -> None: + """Bit 1 and bits 4-12 of the flags word -- :rfc:`2784`'s ``Reserved0`` + less the ``K``/``S`` bits :rfc:`2890` carves out of it -- are carried + verbatim rather than silently zeroed. + """ + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, reserved=0x4FF8) + parsed = GRE(raw, len(raw)) + self.assertEqual(parsed.info.reserved, 0x4FF8) + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_nonzero_version_is_refused(self) -> None: + """:rfc:`2784` requires ``Ver`` to be ``0``; a nonzero version is a + different protocol (RFC 2637's Enhanced GRE/PPTP, version 1), not + implemented here -- refused rather than misread. + """ + from pcapkit.protocols.link.gre import GRE + from pcapkit.utilities.exceptions import ProtocolError + + raw = _gre(0x0800, IPV4, version=1) + with self.assertRaises(ProtocolError): + GRE(raw, len(raw)) + + def test_make_then_parse_base_header(self) -> None: + from pcapkit.protocols.link.gre import GRE + + made = GRE(payload=IPV4).data + self.assertEqual(made, _gre(0x0800, IPV4)) + + parsed = GRE(made, len(made)) + self.assertEqual(type(parsed.payload).__name__, 'IPv4') + self._assert_rebuilds(parsed) + + def test_make_then_parse_every_optional_field(self) -> None: + from pcapkit.protocols.link.gre import GRE + + made = GRE(checksum=0xBEEF, key=0x11223344, sequence_number=99, payload=IPV4).data + self.assertEqual(made, _gre(0x0800, IPV4, checksum=0xBEEF, key=0x11223344, + sequence_number=99)) + + parsed = GRE(made, len(made)) + self.assertEqual((parsed.info.checksum, parsed.info.key, parsed.info.sequence_number), + (0xBEEF, 0x11223344, 99)) + self._assert_rebuilds(parsed) + + def test_make_defaults(self) -> None: + from pcapkit.protocols.link.gre import GRE + + self.assertEqual(GRE().data, _gre(0x0800, b'')) + + def test_make_infers_presence_from_the_value_given(self) -> None: + """As :meth:`L2TPv2.make ` + infers ``S`` from whether ``ns``/``nr`` are given, ``C``/``K``/``S`` + here are inferred from whether ``checksum``/``key``/``sequence_number`` + are given, not from a separate flag argument. + """ + from pcapkit.protocols.link.gre import GRE + + self.assertFalse(GRE().info.checksum_present) + self.assertTrue(GRE(checksum=0).info.checksum_present) + self.assertTrue(GRE(key=0).info.key_present) + self.assertTrue(GRE(sequence_number=0).info.sequence_present) + + def test_make_rejects_reserved1_without_checksum(self) -> None: + from pcapkit.protocols.link.gre import GRE + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError): + GRE(reserved1=5) + + def test_make_rejects_reserved_bits_outside_the_mask(self) -> None: + from pcapkit.protocols.link.gre import GRE + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError): + GRE(reserved=0x8000) + + def test_make_rejects_what_does_not_fit(self) -> None: + from pcapkit.protocols.link.gre import GRE + from pcapkit.utilities.exceptions import ProtocolError + + for kwargs in ( + {'checksum': -1}, {'checksum': 0x1_0000}, {'checksum': True}, + {'reserved1': -1}, {'reserved1': 0x1_0000, 'checksum': 0}, + {'key': -1}, {'key': 0x1_0000_0000}, {'key': True}, + {'sequence_number': -1}, {'sequence_number': 0x1_0000_0000}, + {'version': -1}, {'version': 8}, {'version': True}, + ): + with self.subTest(**kwargs), self.assertRaises(ProtocolError): + GRE(**kwargs) + + def test_id_and_index(self) -> None: + from pcapkit.protocols.link.gre import GRE + from pcapkit.utilities.exceptions import UnsupportedCall + + self.assertEqual(GRE.id(), ('GRE',)) + with self.assertRaises(UnsupportedCall): + GRE.__index__() + + def test_unregistered_protocol_type_is_kept_raw(self) -> None: + from pcapkit.protocols.link.gre import GRE + from pcapkit.protocols.misc.raw import Raw + + raw = _gre(0x1234, IPV4) + parsed = GRE(raw, len(raw)) + self.assertIsInstance(parsed.payload, Raw) + self.assertEqual(parsed.protochain.chain, 'GRE:') + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + + def test_truncated_header_rebuilds_as_captured(self) -> None: + """A capture ending inside the header must not blow up the parse, and + rebuilds to exactly the octets captured. See + ``tests/protocols/test_short_read_roundtrip_unit.py`` for the sweep + that cuts this (and every other) family at every offset. + """ + import warnings + + from pcapkit.protocols.link.gre import GRE + + raw = _gre(0x0800, IPV4, checksum=0xBEEF, key=0x11223344, sequence_number=99) + for keep in (1, 2, 3, 4, 8, 12, 15): + with self.subTest(keep=keep): + cut = raw[:keep] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = GRE(cut, len(cut)) + self.assertEqual(parsed.data, cut) + self.assertEqual(GRE.from_data(parsed.info).data, cut) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/link/test_linux_sll1_unit.py b/tests/protocols/link/test_linux_sll1_unit.py index 54813ffd0e..cd2146a9a5 100644 --- a/tests/protocols/link/test_linux_sll1_unit.py +++ b/tests/protocols/link/test_linux_sll1_unit.py @@ -73,16 +73,16 @@ def test_ethernet_arphrd_dispatches_by_ethertype(self) -> None: self._assert_rebuilds(parsed) def test_the_non_ethertype_arphrds_are_kept_raw(self) -> None: - """``NETLINK``, ``IPGRE``, ``IP6GRE``, ``IEEE80211_RADIOTAP`` and - ``FRAD``, per tcpdump.org's LINUX_SLL page, never have ``protocol_type`` looked up as an - EtherType, even though 0x0800 is IPv4's. + """``NETLINK``, ``IEEE80211_RADIOTAP`` and ``FRAD``, per tcpdump.org's + LINUX_SLL page, never have ``protocol_type`` looked up as an + EtherType, even though 0x0800 is IPv4's. ``IPGRE``/``IP6GRE`` are the + exception: see :meth:`test_ipgre_and_ip6gre_arphrd_dispatch_to_gre`. """ from pcapkit.const.linux.arphrd import ARPHRD from pcapkit.protocols.link.linux_sll1 import LinuxSLL1 from pcapkit.protocols.misc.raw import Raw - for arphrd in (ARPHRD.NETLINK, ARPHRD.IPGRE, ARPHRD.IP6GRE, - ARPHRD.IEEE80211_RADIOTAP, ARPHRD.FRAD): + for arphrd in (ARPHRD.NETLINK, ARPHRD.IEEE80211_RADIOTAP, ARPHRD.FRAD): with self.subTest(arphrd=arphrd): # protocol_type 0x0800 is IPv4's EtherType, but these # hardware types never carry an EtherType in protocol_type. @@ -97,6 +97,29 @@ def test_the_non_ethertype_arphrds_are_kept_raw(self) -> None: self.assertEqual(parsed.data, raw) self._assert_rebuilds(parsed) + def test_ipgre_and_ip6gre_arphrd_dispatch_to_gre(self) -> None: + """GitHub issue #2501: ``ARPHRD_IPGRE`` and ``ARPHRD_IP6GRE`` both + dispatch to :class:`~pcapkit.protocols.link.gre.GRE` rather than + falling back to :class:`~pcapkit.protocols.misc.raw.Raw`; + ``protocol_type`` is ignored (GRE's own Protocol Type field, inside + the GRE header, is what actually selects the payload), per + tcpdump.org's LINUX_SLL page. + """ + from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.protocols.link.linux_sll1 import LinuxSLL1 + + for arphrd in (ARPHRD.IPGRE, ARPHRD.IP6GRE): + with self.subTest(arphrd=arphrd): + gre = bytes.fromhex('00000800') + IPV4 + raw = _header(2, arphrd, b'', 0, 0x0800) + gre + parsed = LinuxSLL1(raw, len(raw)) + self.assertEqual(parsed.info.arphrd_type, arphrd) + self.assertEqual(type(parsed.payload).__name__, 'GRE') + self.assertEqual(parsed.payload.info.protocol_type, 0x0800) + self.assertEqual(parsed.protochain.chain, 'LINUX_SLL1:GRE:IPv4:UDP') + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + def test_non_ethernet_non_excluded_arphrds_still_dispatch_by_ethertype(self) -> None: """Loopback, PPP and SLIP/CSLIP are not :attr:`~pcapkit.const.arp.hardware.Hardware.Ethernet`, and have no diff --git a/tests/protocols/link/test_linux_sll2_unit.py b/tests/protocols/link/test_linux_sll2_unit.py index 619dd93277..af3d9c304b 100644 --- a/tests/protocols/link/test_linux_sll2_unit.py +++ b/tests/protocols/link/test_linux_sll2_unit.py @@ -75,15 +75,15 @@ def test_ethernet_arphrd_dispatches_by_ethertype(self) -> None: self._assert_rebuilds(parsed) def test_the_non_ethertype_arphrds_are_kept_raw(self) -> None: - """``NETLINK``, ``IPGRE``, ``IP6GRE``, ``IEEE80211_RADIOTAP`` and - ``FRAD``, per tcpdump.org's LINUX_SLL2 page, are kept raw. + """``NETLINK``, ``IEEE80211_RADIOTAP`` and ``FRAD``, per tcpdump.org's + LINUX_SLL2 page, are kept raw. ``IPGRE``/``IP6GRE`` are the + exception: see :meth:`test_ipgre_and_ip6gre_arphrd_dispatch_to_gre`. """ from pcapkit.const.linux.arphrd import ARPHRD from pcapkit.protocols.link.linux_sll2 import LinuxSLL2 from pcapkit.protocols.misc.raw import Raw - for arphrd in (ARPHRD.NETLINK, ARPHRD.IPGRE, ARPHRD.IP6GRE, - ARPHRD.IEEE80211_RADIOTAP, ARPHRD.FRAD): + for arphrd in (ARPHRD.NETLINK, ARPHRD.IEEE80211_RADIOTAP, ARPHRD.FRAD): with self.subTest(arphrd=arphrd): raw = _header(0x0800, 0, 0, arphrd, 2, b'', 0) + IPV4 parsed = LinuxSLL2(raw, len(raw)) @@ -96,6 +96,26 @@ def test_the_non_ethertype_arphrds_are_kept_raw(self) -> None: self.assertEqual(parsed.data, raw) self._assert_rebuilds(parsed) + def test_ipgre_and_ip6gre_arphrd_dispatch_to_gre(self) -> None: + """GitHub issue #2501: ``ARPHRD_IPGRE`` and ``ARPHRD_IP6GRE`` both + dispatch to :class:`~pcapkit.protocols.link.gre.GRE` rather than + falling back to :class:`~pcapkit.protocols.misc.raw.Raw`. + """ + from pcapkit.const.linux.arphrd import ARPHRD + from pcapkit.protocols.link.linux_sll2 import LinuxSLL2 + + for arphrd in (ARPHRD.IPGRE, ARPHRD.IP6GRE): + with self.subTest(arphrd=arphrd): + gre = bytes.fromhex('00000800') + IPV4 + raw = _header(0x0800, 0, 0, arphrd, 2, b'', 0) + gre + parsed = LinuxSLL2(raw, len(raw)) + self.assertEqual(parsed.info.arphrd_type, arphrd) + self.assertEqual(type(parsed.payload).__name__, 'GRE') + self.assertEqual(parsed.payload.info.protocol_type, 0x0800) + self.assertEqual(parsed.protochain.chain, 'LINUX_SLL2:GRE:IPv4:UDP') + self.assertEqual(parsed.data, raw) + self._assert_rebuilds(parsed) + def test_non_ethernet_non_excluded_arphrds_still_dispatch_by_ethertype(self) -> None: """Loopback, PPP and SLIP/CSLIP are not among the exceptions, so ``protocol_type`` is still an EtherType for them. diff --git a/tests/protocols/test_short_read_roundtrip_unit.py b/tests/protocols/test_short_read_roundtrip_unit.py index f3c81b28c0..8366a6d8f5 100644 --- a/tests/protocols/test_short_read_roundtrip_unit.py +++ b/tests/protocols/test_short_read_roundtrip_unit.py @@ -50,6 +50,9 @@ def _ipv6(code: int, body: bytes) -> bytes: ('arp', 'link.arp:ARP', {}, bytes.fromhex('000186dd06100001') + bytes(range(44))), ('rarp', 'application.rarp:RARP', {}, bytes.fromhex('0001080006040003') + bytes(range(20))), ('l2tpv2', 'link.l2tpv2:L2TPv2', {}, bytes.fromhex('fff2000e00000000000000000000')), + ('gre', 'link.gre:GRE', {}, struct.pack('>HH', 0xB000, 0x0800) + struct.pack('>HH', 0, 0) + + struct.pack('>I', 0x11223344) + struct.pack('>I', 99) + + bytes.fromhex('450000141234000040fd7b83c0000201c6336401')), ('loopback-null', 'link.loopback:Loopback', {}, bytes.fromhex('020000004500')), ('loopback-loop', 'link.loopback:Loopback', {'alias': 108}, bytes.fromhex('000000024500')), ('ospf', 'application.ospf:OSPF', {}, bytes.fromhex('020100180102030400000000abcd0000') + bytes(8)),