IPv6-Ext - IPv6 Extension Header

pcapkit.protocols.internet.ipv6_ext.protocol contains IPv6_Ext only, in a double role:

  • the base class of every IPv6 extension header this package implements, supplying the extension-mode guards that make payload and protochain unavailable on a header parsed as part of an IPv6 chain. Each subclass guards its own protocol the same way; this class’s protocol deliberately does not.

  • a generic extractor [*] for a header with no dedicated parser. 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, so those are parseable without knowing anything else about the header:

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

class pcapkit.protocols.internet.ipv6_ext.protocol.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, as ruled on #917:

  1. The concrete fallback parser for an RFC 6564-conforming header with no dedicated class, which is Shim6. 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 and protochain, and each subclass’s own on protocol, 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, __index__() and 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, __index__() would raise rather than return its IANA number, and 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.

Added in version 1.5.0.

Changed in version 1.6.0:

  • The library does not pass error to read(), so Data_IPv6_Ext.error is None unless a caller passes one. A header whose dedicated parser raises is Raw, whose info.error holds the error (#2477).

  • next of make() takes a member or an int code, resolved with the enumeration’s get; a name, a bool, or a code that does not fit in one octet raises ProtocolError, rather than being handed to the schema unchecked (#2594).

  • from_data() keeps protocol, handing read() the alias its parse took, rather than rebuilding it as None (#2768).

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 keys the packet dict through IPv6._exthdr_key, which is 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”). As ruled on #891, it is repointed at this instance’s own identity, i.e. which header’s format it parsed, which for this class is only Shim6. The identity is resolved per instance from the numeric code handed to the constructor (alias), which pcapkit.protocols.internet.ipv6.protocol.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 Protocol.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

classmethod id()[source]

Index ID of the protocol.

Return type:

tuple[Literal['HOPOPT'], Literal['IPv6_Route'], Literal['IPv6_Frag'], Literal['ESP'], Literal['AH'], Literal['IPv6_Opts'], Literal['MH'], Literal['HIP']]

Returns:

Index ID of every direct concrete subclass, each of which narrows its own id() to itself. A Shim6 header, which this class parses, therefore matches every one of these names.

Note

== 'IPv6_Ext' is False, as == 'IP' is on IPv4; == IPv6_Ext is True.

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:

    • alias – the numeric code of the extension header this instance parses, 140 for Shim6 (the only code registered to this class), supplied by IPv6._import_next_layer.

    • error – a parsing error to record in the result, if any, which the library does not pass.

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.

Raises:

ProtocolError – If next is a name, a 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 read()’s own, because _check_construction_keywords() builds its allowlist from the 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 Protocol.__own_make_keywords__, so a subclass that overrides make accepts len or data only where its own make declares it (#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 (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 for error: it arrives on the parse path alone, which __init__ exempts from the construction check. alias arrives on a rebuild too, from _make_data(), so this class lists it in __keywords__ (#2768).

__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: invalid version: must be 6, not 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 parses Shim6 (and any other RFC 6564-format code registered to it), while __index__() is a @classmethod with nowhere to put a value that differs per instance.

Return type:

NoReturn

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. Hands read() the protocol it resolved as alias, which picks that rule and the identity again (#2768).

Parameters:

data (IPv6_Ext) – protocol data

Return type:

dict[str, Any]

Returns:

Key-value pairs for protocol construction.

Length Rules

The generic layout is not retroactive (RFC 6564 Section 5), so the headers that predate it get the closed exception table below rather than being assumed to conform. RFC 8200 Section 4.8 restates the layout without a MUST and mislabels its length field as the Destination Options header’s; 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 but has no dedicated parser; it reaches this class as Entry Paths describes.

RFC 8200 Section 4.5 counts ESP among upper-layer headers, not extension headers, and 253 and 254 are reserved for experimentation (RFC 3692), with no wire format at all. None of them reaches this class, by construction (IPv6._import_next_layer). Each is still an ExtensionHeader member, so it enters the walk below, where they part ways:

  • ESP resolves to its own parser, which decodes any plaintext it recovers itself and reports a next of None while the payload stays opaque, so ExtensionHeader(None) fails at the next membership check and the walk ends.

  • 253 and 254 resolve to plain Raw, whose info has no next attribute. For these, and any future IANA code without an implementation, the walk stops on that structural check rather than on a list of codes.

The Extension Header Walk

The walk IPv6._decode_next_layer runs over the header chain, and where it stops:

        flowchart TD
    S["proto = the IPv6 Next Header"] --> Q1
    Q1{"proto an ExtensionHeader member?"}
    Q1 -->|yes| Q2{"EXTENSION\_HEADER\_LIMIT headers walked?"}
    Q2 -->|no| Q3{"the frame at FRAME\_LAYER\_LIMIT layers?"}
    Q3 -->|no| IMP["\_import\_next\_layer(proto, extension=True)<br/>its dedicated class; Raw for 253, 254,<br/>or a parser that raises"]
    IMP --> Q4{"its info has a next attribute?"}
    Q4 -->|no| STOP["walk ends<br/>a plain Raw is reused as the upper layer"]
    Q4 -->|yes| REC["recorded among the extension headers<br/>payload moves past it; proto = its next"]
    REC --> Q5{"an IPv6-Frag header?"}
    Q5 -->|no| Q1
    Q5 -->|yes| J{"the first Fragment header,<br/>in a jumbogram?"}
    J -->|yes| ERR["ProtocolError"]
    J -->|no| Q6{"Fragment Offset zero?"}
    Q6 -->|yes| Q1
    Q6 -->|no| OPQ["walk ends opaque<br/>the upper layer gets no code, so Raw"]
    Q1 -->|no| EXIT
    Q2 -->|"yes: the rest kept Raw, one warning"| EXIT
    Q3 -->|"yes: the rest kept Raw, one warning"| EXIT
    STOP --> EXIT
    OPQ --> EXIT
    EXIT["Internet.\_decode\_next\_layer<br/>the upper layer, with the extension header chain"]

    click S "ipv6.html#pcapkit.protocols.internet.ipv6.protocol.IPv6._decode_next_layer"
    click IMP "ipv6.html#pcapkit.protocols.internet.ipv6.protocol.IPv6._import_next_layer"
    click EXIT "../protocol.html#protocol-next-layer-dispatch"
    

Entry Paths

This class is reached by direct dispatch only. Shim6 (RFC 5533) has no dedicated parser, so Internet._lookup_next_layer would default it to plain Raw, whose missing next would end the walk (#891); this module registers this class for that code instead. The registration is global, shared by every Internet subclass, so __post_init__() gates on version == 6 to keep an IPv4 payload of protocol number 140 out.

A header with a dedicated class whose parser raises does not reach this class. beholder() makes it plain Raw, and the walk stops there, as RFC 8200 Section 4 has a node discard such a packet (#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 – as pcapkit.protocols.misc.pcapng.schema.bounded_area() and pcapkit.protocols.misc.pcapng.protocol.PCAPNG.read() do.

This class keeps the warning – a SchemaWarning, in bounded_area()’s form – and deliberately diverges on the action. Clipping is right where the declared length governs only how much of the current object to read. Here it also decides where the next header starts, and a clipped skip points at whatever bytes end the buffer, which are not a header: continuing would fabricate a layer and record a false ProtoChain entry, which reads as a parsed fact. So read() warns, then stops the walk: this instance absorbs every remaining octet and reports a next of None, which the walk already reads as no more extension headers.

Header Schemas

Removed in version 1.6.0: pcapkit.protocols.schema.internet.ipv6_ext and pcapkit.protocols.schema.internet.ipv6_option, with no alias: the modules merged here (#2534).

class pcapkit.protocols.internet.ipv6_ext.schema.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.protocol.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

Removed in version 1.6.0: pcapkit.protocols.data.internet.ipv6_ext, with no alias: the module moved here (#2534).

class pcapkit.protocols.internet.ipv6_ext.data.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.protocol.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

Parsing error a caller passed to read, if any.

Footnotes