IPv6-Ext - IPv6 Extension Header

pcapkit.protocols.internet.ipv6_ext contains IPv6_Ext only, which plays a double role. It is the shared base class of every IPv6 extension header this package implements, supplying the extension-mode contract: the guards that make payload, protocol and protochain unavailable on a header parsed as part of an IPv6 chain. It also implements a generic extractor for IPv6 extension headers [*], standing in whenever a header’s own dedicated parser is unavailable or has failed. RFC 6564 Section 4 requires, with an RFC 2119 MUST, that any IPv6 extension header defined since April 2012 carry 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 those two octets are parseable without knowing anything else about the header. The sections below the class give the headers that depart from that rule, the ways this class is dispatched to, and why an overrun stops the walk instead of clipping it.

class pcapkit.protocols.internet.ipv6_ext.IPv6_Ext[_PT: Data, _ST: Schema](file=None, length=None, **kwargs)[source]

Bases: Internet[_PT, _ST], Generic[_PT, _ST]

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 length rules, the ways this class is dispatched to, and why an overrun stops the walk rather than clipping.

Base and Fallback Roles

One class plays both, on the owner’s ruling for GitHub issue #917:

  1. The concrete fallback parser for an RFC 6564-conforming header with no dedicated class, or whose dedicated class raised. read(), make(), name, alias and the schema=/data= class keywords implement it.

  2. The base class of the implemented extension headers (HOPOPT, IPv6_Route, IPv6_Frag, IPv6_Opts, HIP, MH, AH and ESP), which is what the _extf guards on payload, protocol and protochain are for.

It is generic in its data and schema types, like IPsec, another base in this package, so that a subclass keeps its own _PT/_ST instead of inheriting this class’s. AH and ESP therefore double-inherit two identically-parameterised generic bases, IPsec[…] and IPv6_Ext[…].

Warning

A subclass must define name, alias, protocol, length and __index__() 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, and __index__() would raise rather than return its IANA number. The language cannot enforce the override, so tests/protocols/internet/test_ipv6_ext_unit.py does, over every subclass discovered at runtime.

Added in version 1.5.0.

property name: 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.

property alias: str

Acronym of corresponding protocol.

Annotated str rather than Literal, for the reason given on name.

Hyphenated, like IPv6_Frag.alias and IPv6_Opts.alias, for the same reason: IPv6._decode_next_layer builds the packet-dict key with self.alias.lstrip('IPv6-').lower(), and 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.

property length: 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 IPv6_Frag), must override it. Every implemented header does, and tests/protocols/internet/test_ipv6_ext_unit.py holds them to it.

property protocol: ExtensionHeader | None | 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 Protocol.protocol meaning (“name of next layer protocol”). On the owner’s ruling for GitHub issue #891 it is deliberately repointed at this instance’s own identity, i.e. which header’s format it parsed, such as HOPOPT or Shim6. The identity is resolved per instance from the numeric code handed to the constructor (alias), which pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer() already holds before it dispatches. See __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 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 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 __data__ rather than an isinstance() test on self._info, because a make-only instance has no _info at all. Reading it would turn ProtocolBase.protocol, which only needs self._protos, into an AttributeError; tests/protocols/internet/test_ipv6_extension_unit.py constructs exactly that instance.

property next: TransType | None

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.

None when the declared length would have overrun what remained of the chain and the walk stopped instead of trusting it; see The Overrun Guard.

property payload: ProtocolBase | NoReturn

Payload of current instance.

Raises:

UnsupportedCall – if the protocol is used as an IPv6 extension header

property protochain: ProtoChain | NoReturn

Protocol chain of current instance.

Raises:

UnsupportedCall – if the protocol is used as an IPv6 extension header

read(length=None, *, extension=False, **kwargs)[source]

Read a generically-parsed IPv6 extension header.

Parameters:
  • length (int | None) – Length of packet data.

  • extension (bool) – If the protocol is used as an IPv6 extension header.

  • **kwargs (Any) –

    Arbitrary keyword arguments, including these, which are this class’s own and are supplied by IPv6._import_next_layer rather than typed by a caller:

    • alias – the numeric extension header code this instance stands in for, e.g. 0 for HOPOPT or 140 for Shim6.

    • error – the parsing error, when this instance was reached as a beholder() fallback rather than by direct dispatch.

Return type:

_PT

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 __post_init__(), which still takes it explicitly.

make(*, next=TransType.UDP, len=0, data=b'', payload=b'', **kwargs)[source]

Make (construct) packet data.

Parameters:
  • next (TransType | int) – Next header type.

  • len (int) – 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 Length Rules) it must satisfy.

  • data (bytes) – Header-specific content after the two fixed octets, emitted verbatim ahead of payload.

  • payload (bytes | ProtocolBase | Any) – Payload of current instance.

  • **kwargs (Any) – Arbitrary keyword arguments.

Return type:

_ST

Returns:

Constructed packet data.

Note

Declared, and keyword-only, each for its own reason.

These keywords stay declared, unlike read()’s own, because _check_construction_keywords() builds its allowlist from inspect.signature() of make. 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, but it is unioned down the MRO, so it would widen the allowlist, and weaken that misspelling check, for every subclass too.

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 (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 _declared_keywords() collects KEYWORD_ONLY parameters too. Nothing calls a protocol’s make positionally; __init__ spreads **kwargs into it.

read needs none of this: its alias/error only arrive on the parse path, which __init__ exempts from the construction check.

classmethod _make_data(data)[source]

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 read() applied, and also for an overrun, where data.next is None and data.declared_next keeps the octet.

Parameters:

data (IPv6_Ext) – protocol data

Return type:

dict[str, Any]

Returns:

Key-value pairs for protocol construction.

__post_init__(file=None, length=None, *, version=6, extension=False, **kwargs)[source]

Post initialisation hook.

Overloads:
Parameters:
  • file (IO[bytes] | bytes | None) – Source packet stream.

  • length (int | None) – Length of packet data.

  • version (Literal[4, 6]) – IP protocol version.

  • extension (bool) – If the protocol is used as an IPv6 extension header.

  • **kwargs (Any) – 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 make().

Note

The version gate is scoped to the fallback role by the same __data__ discriminator protocol uses. Applying it to subclasses would break those that are not IPv6-only: AH and ESP both default to version=4 and are valid under IPv4, so an unconditional gate would reject them (ProtocolError: ESP: only valid for IPv6, got version=4).

The gate exists because this class is registered into Internet.__proto__, which every Internet subclass shares, IPv4 included, so that Shim6 resolves to a working parser instead of 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 Raw gives. Rejecting here sends construction back through beholder() at the caller’s layer, which substitutes that same Raw: the version-agnostic behaviour, with no IPv4-specific code.

classmethod __index__()[source]

Numeral registry index of the protocol.

Raises:

UnsupportedCall – This protocol has no class-level registry entry. Unlike Raw.__index__, which raises because Raw has no identity to report at all, this class does have one – see protocol – but it is resolved per instance, not per class: one IPv6_Ext stands in for HOPOPT, Shim6 and any other RFC 6564-conforming code alike, while __index__() is a @classmethod with nowhere to put a value that differs per instance.

Return type:

NoReturn

Length Rules

RFC 6564 Section 5 is explicit that the generic layout 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 (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 ExtensionHeader enumerates, which match IANA’s IPv6 Extension Header Types registry exactly (#925).

Shim6 conforms to the generic rule (RFC 5533) but has no dedicated parser class; see Entry Paths 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 IPv6._import_next_layer. Each still enters IPv6._decode_next_layer’s walk, being a real 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 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 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 in these ways:

  1. An unrecognised protocol number. Shim6 is a real IANA extension header (RFC 5533) with no dedicated parser, so Internet._lookup_next_layer would default it to plain Raw. That has no next field, so the walk in IPv6._decode_next_layer would crash on it (#891). This module registers this class for that code instead, at the bottom of the module, so dispatch reaches a working generic parser directly. The registration is global, shared by every Internet subclass, so __post_init__() gates on version == 6 to keep it from also activating for an IPv4 payload that carries protocol number 140; see its docstring.

  2. A recognised header whose own parser raises. HOPOPT, IPv6-Route, IPv6-Opts, MH, HIP, IPv6-Frag and AH all have dedicated classes. When one of those raises, beholder() (Protocol._import_next_layer) would substitute plain Raw, which loses next and crashes the walk as in path 1. IPv6._import_next_layer catches the failure itself, one layer in from beholder, and substitutes this class: the bad header costs only itself, not the rest of the chain.

The Overrun Guard

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

This class follows that convention’s warning (SchemaWarning) and deliberately diverges on the action. Clipping is right when the declared length only governs how much of the current object to read, since there is always a well-defined “what is left” to fall back to. Here the declared length also decides where the next header starts; a clipped skip distance points at whatever bytes end the buffer, which are not a header. Continuing from there would fabricate a layer and record a false entry in ProtoChain, which reads as a parsed fact rather than a guess. So read() warns in the same wording as the sites above, then stops the walk: this instance absorbs every remaining octet and reports None for next, which the caller’s loop already reads as “no more extension headers”. The chain ends honestly at the bad header instead of inventing what follows it.

Header Schemas

class pcapkit.protocols.schema.internet.ipv6_ext.IPv6_Ext(next=NO_VALUE, len=NO_VALUE, payload=NO_VALUE, *, __packet__=None, **kwargs)[source]

Bases: Schema

Header schema for a generically-parsed IPv6 extension header.

Only the two octets RFC 6564 Section 4 guarantees are parsed at this layer – next and the raw Hdr Ext Len octet. Combining them into an actual skip distance is per protocol (a constant for IPv6-Frag, 4-octet units for AH, 8-octet units for the rest), so that part is done in pcapkit.protocols.internet.ipv6_ext.IPv6_Ext.read(), which knows which protocol this instance stands in for; this schema does not.

next: TransType = <EnumField next>

Next header.

len: int = <UInt8Field len>

Raw Hdr Ext Len octet – see the class docstring for why its interpretation is not fixed here.

payload: bytes = <PayloadField payload>

Everything after the two fixed octets; opaque at this layer.

Data Models

class pcapkit.protocols.data.internet.ipv6_ext.IPv6_Ext(protocol, next, declared_next, len, length, data, error)[source]

Bases: Protocol

Data model for a generically-parsed IPv6 extension header.

See pcapkit.protocols.internet.ipv6_ext.IPv6_Ext for how each field below is derived.

protocol: ExtensionHeader | None

The extension header this instance stands in for – the numeric code the caller dispatched on, resolved to its ExtensionHeader member. None when alias was not given or named no such member (read sets it so in both cases, and the class property at protocol is typed to match).

next: TransType | None

Next header, parsed off the wire. None when the declared length would have overrun what remained and the walk stopped instead of trusting it.

declared_next: TransType

Next header as declared on the wire. Equal to next except on an overrun, where next is None but this keeps the octet so that a rebuild reproduces it.

len: int

Raw Hdr Ext Len octet, as on the wire.

length: int

Length of this extension header, in octets, actually consumed.

data: bytes

Header-specific content after the two fixed octets, as consumed.

error: Exception | None

Original parsing error, if this instance was reached as a beholder() fallback rather than by direct dispatch.

Footnotes