Source code for pcapkit.protocols.internet.ipv6_ext.protocol

# -*- coding: utf-8 -*-
"""IPv6-Ext - IPv6 Extension Header
======================================

.. module:: pcapkit.protocols.internet.ipv6_ext.protocol

:mod:`pcapkit.protocols.internet.ipv6_ext.protocol` contains
:class:`~pcapkit.protocols.internet.ipv6_ext.protocol.IPv6_Ext`
only, which is both the shared **base class** of every IPv6 extension header
in this package and a **generic** extractor for a header with no dedicated
parser (:issue:`917`); the class docstring separates the roles.

Length Rules
------------

:rfc:`6564#section-4` is Standards Track and says, with an RFC 2119
**MUST**, that any IPv6 extension header defined from April 2012
onward carries the same first two octets:

======= ========= ===================== =====================================
Octets      Bits        Name                    Description
======= ========= ===================== =====================================
  0           0   ``next``                    Next Header
  1           8   ``len``                     Hdr Ext Len (8-octet units,
                                              excluding the first 8 octets)
  2          16   ``payload``                 Header-specific content
======= ========= ===================== =====================================

so the first two octets of a *conforming* header are parseable without
knowing anything else about it. :rfc:`6564#section-5` is explicit that
this is **not retroactive** -- it applies only to newly defined extension
headers -- which is why the pre-existing headers need the closed exception
table below rather than being assumed to conform. (:rfc:`8200#section-4.8`
restates the layout without a MUST and mislabels the generic length field as
the length of the Destination Options header; cite :rfc:`6564#section-4`
instead.)

The exception table, verified against IANA's ``protocol-numbers-1.csv``
*IPv6 Extension Header* column and each cited RFC:

===================================================================== =================================================
Header(s)                                                             Length rule
===================================================================== =================================================
``HOPOPT``, ``IPv6-Route``, ``IPv6-Opts``, ``MH``, ``HIP``, ``Shim6`` ``(octet[1] + 1) * 8`` -- the :rfc:`6564`
                                                                      generic rule
``IPv6-Frag``                                                         constant ``8``; octet[1] is Reserved, not a
                                                                      length (:rfc:`8200#section-4.5`)
``AH``                                                                ``(octet[1] + 2) * 4`` -- :rfc:`4302#section-2.2`
                                                                      counts in 4-octet units with a bias of 2,
                                                                      not :rfc:`6564`'s 8-octet units and bias of 1
``ESP``                                                               terminal -- has a dedicated, registered parser
                                                                      (:class:`~pcapkit.protocols.internet.esp.protocol.ESP`),
                                                                      but its Next Header byte is inside the
                                                                      encrypted trailer (:rfc:`4303`), so the walk
                                                                      cannot step past it on the wire
``253``, ``254``                                                      terminal -- no dedicated parser exists, so no
                                                                      next header field is ever read at all, either
===================================================================== =================================================

This table classifies by *wire format* alone, over every code
:class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader` enumerates,
which match IANA's *IPv6 Extension Header Types* registry exactly
(:issue:`925`).

``Shim6`` conforms to the generic rule (:rfc:`5533`) but has no dedicated parser class; see
"Entry Paths" below for how it reaches this class by direct dispatch.

:rfc:`8200#section-4.5` sets Encapsulating Security Payload aside: ESP is not
an extension header, and the RFC lists it among upper-layer headers.
:rfc:`4303` puts its Next Header inside the encrypted trailer, with no length
field in the cleartext part. 253 and 254 are reserved for private
experimentation (:rfc:`3692`) with no wire format at all. None of them
is reachable through this class, by construction; see
:meth:`pcapkit.protocols.internet.ipv6.protocol.IPv6._import_next_layer`. Each still
enters :meth:`IPv6._decode_next_layer
<pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer>`'s walk, being a
real :class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader` member, but
they part ways there. ``ESP`` resolves to its own dedicated parser, which
decodes any plaintext it recovers itself and whose info carries a ``next`` of
:data:`None` whenever the payload stays opaque, so the walk ends the ordinary
way with ``ExtensionHeader(None)`` failing at the top of the next iteration. ``253``
and ``254`` have no dedicated parser and resolve to plain
:class:`~pcapkit.protocols.misc.raw.protocol.Raw`, whose info has no ``next``
*attribute*. For these two, and any future IANA code without an
implementation, the walk stops on a *structural* check (does the parsed layer
carry a ``next`` at all?) rather than on a list of codes.

Entry Paths
-----------

This class is reached by direct dispatch only.
:class:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6` is a real
IANA extension header (:rfc:`5533`) with no dedicated parser, so
:meth:`Internet._lookup_next_layer <pcapkit.protocols.protocol.Protocol._lookup_next_layer>`
would default it to plain :class:`~pcapkit.protocols.misc.raw.protocol.Raw`, whose
missing ``next`` field would end the walk in
:meth:`IPv6._decode_next_layer <pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer>`
there (:issue:`891`). This module registers this class for that
code instead (see the bottom of the module), so dispatch reaches a working
generic parser directly. The registration is global, shared by every
:class:`~pcapkit.protocols.internet.internet.Internet` subclass, so
:meth:`__post_init__` gates on ``version == 6`` to keep it from also activating
for an IPv4 payload that carries protocol number 140; see its docstring.

A header with a dedicated class whose parser raises does not reach this class.
:func:`~pcapkit.corekit.decorators.beholder` makes it plain
:class:`~pcapkit.protocols.misc.raw.protocol.Raw`, and the walk stops there, as
:rfc:`8200#section-4` has a node discard such a packet (:issue:`2477`).

The Overrun Guard
-----------------

The house convention for a declared length that does not fit what remains
is warn-and-clip: warn, naming what was declared against what is left, then
read only what is left (see
:func:`pcapkit.protocols.misc.pcapng.schema.bounded_area` and
:meth:`pcapkit.protocols.misc.pcapng.protocol.PCAPNG.read`).

This class keeps the warning -- a
:class:`~pcapkit.utilities.warnings.SchemaWarning`, as
:func:`~pcapkit.protocols.misc.pcapng.schema.bounded_area` emits -- but not the
clip. Here the declared length also decides *where the next header starts*: a
clipped skip distance points at the bytes that end the buffer, which are not a
header, and continuing would record a fabricated layer in
:class:`~pcapkit.corekit.protochain.ProtoChain`. So :meth:`read` warns, then
**stops the walk**: this instance absorbs every remaining octet and reports
:data:`None` for ``next``, which the caller's loop reads as "no more extension
headers".

"""
from typing import TYPE_CHECKING, Generic, cast, overload

from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader
from pcapkit.const.reg.transtype import TransType as Enum_TransType
from pcapkit.protocols.internet.internet import Internet
from pcapkit.protocols.internet.ipv6_ext.data import IPv6_Ext as Data_IPv6_Ext
from pcapkit.protocols.internet.ipv6_ext.schema import IPv6_Ext as Schema_IPv6_Ext
from pcapkit.protocols.misc.no_payload.protocol import NoPayload
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
from pcapkit.protocols.schema import Schema
from pcapkit.utilities.exceptions import ProtocolError, UnsupportedCall, stacklevel
from pcapkit.utilities.warnings import SchemaWarning, warn

if TYPE_CHECKING:
    from typing import IO, Any, NoReturn, Optional, Protocol

    from typing_extensions import Literal

    from pcapkit.corekit.protochain import ProtoChain

    class _NextHeaderData(Protocol):
        """The one field every IPv6 extension header's data model carries.

        :rfc:`8200#section-4` identifies each extension header by the Next
        Header field of the header before it, and every implemented one
        records its own Next Header under this name, ESP from its decrypted
        trailer -- which is what lets :attr:`IPv6_Ext.next` be a *shared* member
        rather than a fallback-role-only one. Narrower than
        :class:`~pcapkit.protocols.internet.ipv6_ext.data.IPv6_Ext` on
        purpose: ``length`` is deliberately absent, because
        :class:`~pcapkit.protocols.internet.ipv6_frag.data.IPv6_Frag` has no
        such field (its length is the constant 8, and its own
        :attr:`~pcapkit.protocols.internet.ipv6_frag.protocol.IPv6_Frag.length`
        property supplies it).

        """

        next: 'Optional[Enum_TransType]'

__all__ = ['IPv6_Ext']


[docs] class IPv6_Ext(Internet[_PT, _ST], Generic[_PT, _ST], schema=Schema_IPv6_Ext, data=Data_IPv6_Ext): """This class implements a generic IPv6 extension header parser, and is the shared base of every IPv6 extension header in this package. The module documentation has the RFC citations behind the :ref:`length rules <ipv6-ext-length-rules>`, the :ref:`ways this class is dispatched to <ipv6-ext-entry-paths>`, and :ref:`why an overrun stops the walk <ipv6-ext-overrun-guard>` rather than clipping. .. rubric:: Base and Fallback Roles One class plays both, as ruled on :issue:`917`: 1. **The concrete fallback parser** for an :rfc:`6564`-conforming header with no dedicated class, which is Shim6. :meth:`read`, :meth:`make`, :attr:`name`, :attr:`alias` and the ``schema=``/``data=`` class keywords implement it. 2. **The base class** of the implemented extension headers (:class:`~pcapkit.protocols.internet.hopopt.protocol.HOPOPT`, :class:`~pcapkit.protocols.internet.ipv6_route.protocol.IPv6_Route`, :class:`~pcapkit.protocols.internet.ipv6_frag.protocol.IPv6_Frag`, :class:`~pcapkit.protocols.internet.ipv6_opts.protocol.IPv6_Opts`, :class:`~pcapkit.protocols.internet.hip.protocol.HIP`, :class:`~pcapkit.protocols.internet.mh.protocol.MH`, :class:`~pcapkit.protocols.internet.ah.protocol.AH` and :class:`~pcapkit.protocols.internet.esp.protocol.ESP`), which is what the ``_extf`` guards on :attr:`payload` and :attr:`protochain`, and each subclass's own on ``protocol``, are for. It is generic in its data and schema types, like :class:`~pcapkit.protocols.internet.ipsec.protocol.IPsec`, another base in this package, so that a subclass keeps its *own* ``_PT``/``_ST`` instead of inheriting this class's. :class:`~pcapkit.protocols.internet.ah.protocol.AH` and :class:`~pcapkit.protocols.internet.esp.protocol.ESP` therefore double-inherit two identically-parameterised generic bases, ``IPsec[…]`` and ``IPv6_Ext[…]``. Warning: A subclass **must** define :attr:`name`, :attr:`alias`, :attr:`protocol`, :attr:`length`, :meth:`__index__` and :meth:`id` itself. Each carries this class's *fallback-role* answer, which is wrong for a header with an identity of its own: it would report itself as ``IPv6 Extension Header`` / ``IPv6-Ext``, read its length off a data model that is not its own, :meth:`__index__` would raise rather than return its IANA number, and :meth:`id` would match every member's name rather than narrow to its own class name. The language cannot enforce the override, so ``tests/protocols/internet/test_ipv6_ext_unit.py`` does, over every subclass discovered at runtime. .. versionadded:: 1.5.0 .. versionchanged:: 1.6.0 * The library does not pass ``error`` to :meth:`read`, so :attr:`Data_IPv6_Ext.error <pcapkit.protocols.internet.ipv6_ext.data.IPv6_Ext.error>` is :data:`None` unless a caller passes one. A header whose dedicated parser raises is :class:`~pcapkit.protocols.misc.raw.protocol.Raw`, whose ``info.error`` holds the error (:issue:`2477`). * ``next`` of :meth:`make` takes a member or an :class:`int` code, resolved with the enumeration's ``get``; a name, a :obj:`bool`, or a code that does not fit in one octet raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than being handed to the schema unchecked (:issue:`2594`). * :meth:`from_data` keeps :attr:`protocol`, handing :meth:`read` the ``alias`` its parse took, rather than rebuilding it as :data:`None` (:issue:`2768`). """ #: Every subclass overrides :meth:`make` with its own fields and forwards #: nothing to this one, so this class's ``len`` and ``data`` are accepted #: only by this class itself. Without the marker, a subclass whose ``make`` #: does not declare them would accept and then drop them (:issue:`2286`). __own_make_keywords__ = True #: :meth:`read` takes ``alias`` out of ``**kwargs`` by name, and #: :meth:`_make_data` hands it back, so a rebuild keeps the :attr:`protocol` #: its parse resolved (:issue:`2768`). Every subclass accepts it as well, #: and drops it, as it does while parsing. __keywords__ = frozenset({'alias'}) ########################################################################## # Properties. ########################################################################## @property def name(self) -> 'str': """Name of current protocol. Annotated ``str`` rather than the ``Literal`` every *leaf* protocol in this package uses, because this one is also a base: a ``Literal`` here makes each subclass's own ``Literal`` an incompatible override (mypy ``[override]``). The value is still the single fallback-role constant. """ return 'IPv6 Extension Header' @property def alias(self) -> 'str': """Acronym of corresponding protocol. Annotated ``str`` rather than ``Literal``, for the reason given on :attr:`name`. Hyphenated, like :attr:`IPv6_Frag.alias <pcapkit.protocols.internet.ipv6_frag.protocol.IPv6_Frag.alias>` and :attr:`IPv6_Opts.alias <pcapkit.protocols.internet.ipv6_opts.protocol.IPv6_Opts.alias>`, for the same reason: :meth:`IPv6._decode_next_layer <pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer>` keys the packet dict through ``IPv6._exthdr_key``, which is ``alias.lstrip('IPv6-').lower()``, and :meth:`str.lstrip` strips a character *set*, not a prefix. The default (class-name) alias ``'IPv6_Ext'`` would strip to ``'_Ext'`` and key the dict as ``_ext``; the hyphen makes every leading character (``I``, ``P``, ``v``, ``6``, ``-``) part of the stripped set, giving ``ext``. """ return 'IPv6-Ext' @property def length(self) -> 'int': """Header length of current protocol. Fallback-role member: it reads this module's *own* data model, so a subclass whose data model records its length elsewhere, or not at all (as :class:`~pcapkit.protocols.internet.ipv6_frag.data.IPv6_Frag`), must override it. Every implemented header does, and ``tests/protocols/internet/test_ipv6_ext_unit.py`` holds them to it. """ return cast('Data_IPv6_Ext', self._info).length @property def protocol(self) -> 'Optional[Enum_ExtensionHeader] | Optional[str]': """The extension header this instance stands in for. In the *fallback* role (this instance parsed this module's own schema) this is **not** the base :attr:`Protocol.protocol <pcapkit.protocols.protocol.Protocol.protocol>` meaning ("name of next layer protocol"). As ruled on :issue:`891`, it is repointed at this instance's *own* identity, i.e. which header's format it parsed, which for this class is only :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6`. The identity is resolved per instance from the numeric code handed to the constructor (``alias``), which :meth:`pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer` already holds before it dispatches. See :meth:`__index__` for why the *class-level* identity cannot work the same way. It is deliberately *not* ``_extf``-guarded in this role: that same call passes ``extension=True`` for every extension header in a chain, so guarding it would make the identity unreadable in exactly the case it exists for. In the *base* role it falls through to ``super()``, restoring the ordinary :class:`~pcapkit.protocols.protocol.ProtocolBase` meaning. That is load-bearing: every subclass implements its own ``_extf``-guarded ``protocol`` as ``return super().protocol``, and this class sits between them and :class:`~pcapkit.protocols.protocol.ProtocolBase` in the MRO, so without the discriminator below each would read ``self._info.protocol`` off a data model with no such field (``AttributeError``). The discriminator is the *class-level* :attr:`~pcapkit.protocols.protocol.Protocol.__data__` rather than an :func:`isinstance` test on ``self._info``, because a ``make``-only instance has no ``_info`` at all. Reading it would turn :attr:`Protocol.protocol <pcapkit.protocols.protocol.Protocol.protocol>`, which only needs ``self._protos``, into an ``AttributeError``; ``tests/protocols/internet/test_ipv6_extension_unit.py`` constructs exactly that instance. """ if self.__data__ is Data_IPv6_Ext: return cast('Data_IPv6_Ext', self._info).protocol return super().protocol @property def next(self) -> 'Optional[Enum_TransType]': """Next header, as parsed off the wire. Shared by every subclass rather than fallback-role-only, since each of their data models records the Next Header value under this name. :data:`None` when the declared length would have overrun what remained of the chain and the walk stopped instead of trusting it; see :ref:`ipv6-ext-overrun-guard`. """ return cast('_NextHeaderData', self._info).next @property def payload(self) -> 'ProtocolBase | NoReturn': """Payload of current instance. Raises: UnsupportedCall: if the protocol is used as an IPv6 extension header """ if self._extf: raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute 'payload'") return super().payload @property def protochain(self) -> 'ProtoChain | NoReturn': """Protocol chain of current instance. Raises: UnsupportedCall: if the protocol is used as an IPv6 extension header """ if self._extf: raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute 'protochain'") return super().protochain ########################################################################## # Methods. ##########################################################################
[docs] @classmethod def id(cls) -> 'tuple[Literal["HOPOPT"], Literal["IPv6_Route"], Literal["IPv6_Frag"], Literal["ESP"], Literal["AH"], Literal["IPv6_Opts"], Literal["MH"], Literal["HIP"]]': # pylint: disable=line-too-long """Index ID of the protocol. Returns: Index ID of every direct concrete subclass, each of which narrows its own :meth:`id` to itself. A Shim6 header, which this class parses, therefore matches every one of these names. Note: ``== 'IPv6_Ext'`` is :data:`False`, as ``== 'IP'`` is on :class:`~pcapkit.protocols.internet.ipv4.protocol.IPv4`; ``== IPv6_Ext`` is :data:`True`. """ return ('HOPOPT', 'IPv6_Route', 'IPv6_Frag', 'ESP', 'AH', 'IPv6_Opts', 'MH', 'HIP')
[docs] def read(self, length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ extension: 'bool' = False, **kwargs: 'Any') -> '_PT': """Read a generically-parsed IPv6 extension header. Args: length: Length of packet data. extension: If the protocol is used as an IPv6 extension header. **kwargs: Arbitrary keyword arguments, including these, which are this class's own: * ``alias`` -- the numeric code of the extension header this instance parses, ``140`` for ``Shim6`` (the only code registered to this class), supplied by :meth:`IPv6._import_next_layer <pcapkit.protocols.internet.ipv6.protocol.IPv6._import_next_layer>`. * ``error`` -- a parsing error to record in the result, if any, which the library does not pass. Returns: Parsed packet data. Note: These are read out of ``**kwargs`` rather than declared, and ``version`` is not declared either, because this class is *also* the base of subclasses whose own ``read`` accepts none of them. Declaring them would assert of the whole family an interface only the fallback has, which mypy (``Signature of "read" incompatible with supertype``, ``[override]``) and pylint (``arguments-differ``) both reject. This method never reads ``version``; the version gate lives in :meth:`__post_init__`, which still takes it explicitly. """ alias = kwargs.get('alias') # type: Optional[int] error = kwargs.get('error') # type: Optional[Exception] if length is None: length = len(self) schema = cast('Schema_IPv6_Ext', self.__header__) ext_code = None # type: Optional[Enum_ExtensionHeader] if alias is not None: try: ext_code = Enum_ExtensionHeader(alias) except ValueError: ext_code = None if ext_code == Enum_ExtensionHeader.IPv6_Frag: # RFC 8200 §4.5: octet 1 is Reserved, not a length; the fragment # header is always exactly 8 octets, and RFC 6564 §5 says it predates # and does not follow the generic format. nominal = 8 elif ext_code == Enum_ExtensionHeader.AH: # RFC 4302 §2.2: Payload Len is in 4-octet units, excluding the # first 8 octets: a different unit and bias from RFC 6564's # Hdr Ext Len. nominal = (schema.len + 2) * 4 else: # RFC 6564 §4 (MUST): Hdr Ext Len is in 8-octet units, excluding # the first 8 octets. Also the best generic guess when ``alias`` # named no known extension header. nominal = (schema.len + 1) * 8 if nominal > length: # Warns like the house convention but stops rather than clips; see # the module docstring's "The Overrun Guard" section. warn(f'IPv6: extension header declares a length of {nominal} octet(s) with ' f'{length} octet(s) left in the chain; stopping the walk instead of ' f'skipping past it', SchemaWarning, stacklevel=stacklevel()) ext_len = length next_header = None # type: Optional[Enum_TransType] else: ext_len = nominal next_header = schema.next # NOTE: The schema's payload holds every octet after the two fixed # ones; the first ``ext_len - 2`` of them are this header's own body. rest = schema.get_payload() generic_ext = Data_IPv6_Ext( protocol=ext_code, next=next_header, declared_next=schema.next, len=schema.len, length=ext_len, data=rest[:max(ext_len - 2, 0)], error=error, ) if extension: # NOTE: As an IPv6 extension header the upper layer belongs to the enclosing # IPv6 header, so hold NoPayload here so that membership does not raise (#2487). self._next = NoPayload() # pylint: disable=attribute-defined-outside-init return cast('_PT', generic_ext) return self._decode_next_layer(cast('_PT', generic_ext), next_header, length - ext_len, payload=rest[max(ext_len - 2, 0):])
[docs] def make(self, *, next: 'Enum_TransType | int' = Enum_TransType.UDP, # pylint: disable=redefined-builtin len: 'int' = 0, # pylint: disable=redefined-builtin data: 'bytes' = b'', payload: 'bytes | ProtocolBase | Any' = b'', **kwargs: 'Any') -> '_ST': """Make (construct) packet data. Args: next: Next header type. len: Raw ``Hdr Ext Len`` octet to emit. The caller must size it, since this class cannot know at construction time which per-protocol rule (see :ref:`ipv6-ext-length-rules`) it must satisfy. data: Header-specific content after the two fixed octets, emitted verbatim ahead of ``payload``. payload: Payload of current instance. **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data. Raises: ProtocolError: If ``next`` is a name, a :obj:`bool`, or a member or code that does not fit in one octet. Note: **Declared, and keyword-only**, each for its own reason. These keywords stay *declared*, unlike :meth:`read`'s own, because ``_check_construction_keywords()`` builds its allowlist from the :func:`inspect.signature` of ``make`` and the other construction methods of every class in the MRO, plus any ``__keywords__``. Hiding them in ``**kwargs`` **breaks construction**: a plain ``IPv6_Ext(next=..., len=..., data=..., payload=...)`` raises ``UnsupportedCall: IPv6_Ext: unexpected keyword(s)``. The ``__keywords__`` escape hatch would cover that too, but a set there is unioned down the MRO, so every subclass would accept these names and drop them. A ``make`` signature can be kept from subclasses instead: this class sets :attr:`Protocol.__own_make_keywords__ <pcapkit.protocols.protocol.Protocol.__own_make_keywords__>`, so a subclass that overrides ``make`` accepts ``len`` or ``data`` only where its own ``make`` declares it (:issue:`2286`). They are keyword-*only* because this class is also a base and each subclass's ``make`` puts different names in the same positions. As positional parameters they draw pylint ``arguments-renamed`` warnings (:meth:`ESP.make <pcapkit.protocols.internet.esp.protocol.ESP.make>` has ``spi``, ``seq`` and ``next`` where these have ``next``, ``len`` and ``data``) and mypy ``[override]`` errors. With no positional parameters there is no position to disagree about, and the allowlist is unaffected because :func:`~pcapkit.protocols.protocol._declared_keywords` collects ``KEYWORD_ONLY`` parameters too. Nothing calls a protocol's ``make`` positionally; ``__init__`` spreads ``**kwargs`` into it. ``read`` needs none of this for ``error``: it arrives on the *parse* path alone, which ``__init__`` exempts from the construction check. ``alias`` arrives on a rebuild too, from :meth:`_make_data`, so this class lists it in :attr:`~pcapkit.protocols.protocol.Protocol.__keywords__` (:issue:`2768`). """ # NOTE: The schema keeps everything after the two fixed octets as one # opaque payload, so the body is packed in front of the upper layer. if data: if isinstance(payload, ProtocolBase): payload = bytes(payload) elif isinstance(payload, Schema): payload = payload.pack() payload = data + payload return cast('_ST', Schema_IPv6_Ext( next=self._make_index(next, Enum_TransType, bits=8, name='next header'), len=len, payload=payload, ))
########################################################################## # Data models. ########################################################################## @overload def __post_init__(self, file: 'IO[bytes] | bytes', length: 'Optional[int]' = ..., *, # pylint: disable=arguments-differ version: 'Literal[4, 6]' = ..., extension: 'bool' = ..., **kwargs: 'Any') -> 'None': ... @overload def __post_init__(self, **kwargs: 'Any') -> 'None': ... # pylint: disable=arguments-differ
[docs] def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ version: 'Literal[4, 6]' = 6, extension: 'bool' = False, **kwargs: 'Any') -> 'None': """Post initialisation hook. Args: file: Source packet stream. length: Length of packet data. version: IP protocol version. extension: If the protocol is used as an IPv6 extension header. **kwargs: Arbitrary keyword arguments. Raises: ProtocolError: If this is the *fallback* parser (see the class docstring's "Base and Fallback Roles") and ``version`` is not ``6``. See Also: For construction arguments, please refer to :meth:`make`. Note: The version gate is scoped to the fallback role by the same ``__data__`` discriminator :attr:`protocol` uses. Applying it to subclasses would break those that are *not* IPv6-only: :class:`~pcapkit.protocols.internet.ah.protocol.AH` and :class:`~pcapkit.protocols.internet.esp.protocol.ESP` both default to ``version=4`` and are valid under IPv4, so an unconditional gate would reject them (``ProtocolError: ESP: invalid version: must be 6, not 4``). The gate exists because this class is registered into :attr:`Internet.__proto__ <pcapkit.protocols.internet.internet.Internet.__proto__>`, which *every* :class:`~pcapkit.protocols.internet.internet.Internet` subclass shares, :class:`~pcapkit.protocols.internet.ipv4.protocol.IPv4` included, so that :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6` resolves to a working parser instead of :class:`~pcapkit.protocols.misc.raw.protocol.Raw`. Without it, an IPv4 packet whose protocol byte is 140 would reach this class and walk an IPv6-style extension-header chain out of an IPv4 payload, reading ``IPv4:IPv6-Ext:...`` instead of the ``IPv4:Shim6`` that a plain, non-continuing :class:`~pcapkit.protocols.misc.raw.protocol.Raw` gives. Rejecting here sends construction back through :func:`~pcapkit.corekit.decorators.beholder` at the *caller's* layer, which substitutes that same :class:`~pcapkit.protocols.misc.raw.protocol.Raw`: the version-agnostic behaviour, with no IPv4-specific code. """ if self.__data__ is Data_IPv6_Ext and version != 6: raise ProtocolError( f'{self.__class__.__name__}: invalid version: must be 6, not {version!r}') #: bool: If the protocol is used as an IPv6 extension header. self._extf = extension # call super __post_init__ super().__post_init__(file, length, version=version, extension=extension, **kwargs)
def __length_hint__(self) -> 'int': """Return an estimated length for the object. Eight octets, the shortest header :rfc:`6564#section-4` allows: Hdr Ext Len counts 8-octet units "not including the first 8 octets", and :rfc:`8200#section-4` makes every extension header a multiple of 8 octets. Annotated ``int`` rather than ``Literal[8]``, for the reason given on :attr:`name`: every subclass has its own fixed header and its own ``Literal``. """ return 8
[docs] @classmethod def __index__(cls) -> 'NoReturn': """Numeral registry index of the protocol. Raises: UnsupportedCall: This protocol has no *class-level* registry entry. Unlike :meth:`Raw.__index__ <pcapkit.protocols.misc.raw.protocol.Raw.__index__>`, which raises because :class:`~pcapkit.protocols.misc.raw.protocol.Raw` has no identity to report at all, this class *does* have one -- see :attr:`protocol` -- but it is resolved per instance, not per class: one :class:`IPv6_Ext` parses :attr:`~pcapkit.const.ipv6.extension_header.ExtensionHeader.Shim6` (and any other RFC 6564-format code registered to it), while :meth:`__index__` is a ``@classmethod`` with nowhere to put a value that differs per instance. """ raise UnsupportedCall(f'{cls.__name__!r} object cannot be interpreted as an integer')
########################################################################## # Utilities. ##########################################################################
[docs] @classmethod def _make_data(cls, data: 'Data_IPv6_Ext') -> 'dict[str, Any]': # type: ignore[override] """Create key-value pairs from ``data`` for protocol construction. Emits the ``Hdr Ext Len`` octet and the body as they were read, so the rebuild is byte-exact whichever per-protocol length rule :meth:`read` applied, and also for an overrun, where ``data.next`` is :data:`None` and ``data.declared_next`` keeps the octet. Hands :meth:`read` the ``protocol`` it resolved as ``alias``, which picks that rule and the identity again (:issue:`2768`). Args: data: protocol data Returns: Key-value pairs for protocol construction. """ return { 'next': data.declared_next, 'len': data.len, 'data': data.data, 'payload': cls._make_payload(data), 'alias': data.protocol, }
# NOTE: Assigned directly rather than via ``code=``, whose registry import at class definition # closes an import cycle through the half-built ``pcapkit.protocols.internet``. Internet.__proto__[Enum_TransType.Shim6] = IPv6_Ext