Protocol Family Grouping

A protocol family is a base class named after the family plus its member classes. What each class returns from id() is what the selectors on Protocol match against. Each ruling below names the thread it was set on.

The id() Criterion

A family base lists its direct concrete subclasses. An abstract intermediate is left out, and so is everything beneath it. Ruled in review of #2441; no issue holds the ruling.

The member list takes either of these forms:

  • Members only. IP.id() is ('IPv4', 'IPv6'); AH and ESP are not in it, since they belong to the separate abstract IPsec, whose id() is ('AH', 'ESP'). HTTP.id() is ('HTTP/1', 'HTTP/2').

  • The base’s own name, then the members. VLAN.id() is ('VLAN', 'C_Tag', 'S_Tag') and L2TP.id() is ('L2TP', 'L2TPv2').

ARP.id() is ('ARP',) alone; see Member Shapes.

Abstract Family Names

An abstract family name composes its members. The ruling on #2462 is that IPv6_Ext lists its direct concrete members in its id(), and that each member narrows its own id() to itself.

Grouping by Name

Members share a base because they share a name, not a wire format. HTTP/1 and HTTP/2 share HTTP, IPv4 and IPv6 share IP, AH and ESP share IPsec, and SSLv2 and SSLv3 share SSL (below). Ruled in review of #2441; no issue holds the ruling.

The SSL Family

SSLv2 and SSLv3 share a base by name, though only SSLv3 shares TLS’s record layer. The owner asked on #2441 for this grouping to be recorded here; no issue holds the ruling. By the id() criterion, SSL.id() is ('SSLv2', 'TLS', 'DTLS'), without SSLv3, which subclasses TLS.

One TLS class reads SSLv3 through TLS 1.3, in the ARP shape (Member Shapes): id() stays ('TLS',) and alias names the version read. Only record version 0x0300 is branded SSLv3, in name as well as alias; every later version is TLS. SSLv3 ('SSLv3',) narrows id() and nothing else, as InARP does under ARP, and nothing dispatches to it: an SSLv3 record on TCP port 443 parses as TLS with alias SSLv3. DTLS ('DTLS',) reads DTLS 1.0, 1.2 and 1.3 the same way.

TCP port 443 binds TLS and UDP port 5684 binds DTLS. SSLv2 ('SSLv2',) is bound to no port: on 443, TLS offers SSLv2 a segment whose first record it rejects, and a record SSLv2 takes stays a TLS layer with alias SSLv2. So 'SSL' in selects both TLS and DTLS layers, while 'SSLv3' in and 'SSLv2' in select no layer port 443 parsed; only frame.protochain matches those two, by alias.

Dispatching Bases

A registry binds a family’s base only where its code cannot tell the members apart, and there the base picks the member. IP protocol number 2 is IGMP’s in every version, and the TCP and UDP ports bound to HTTP are the family’s, so IGMP’s _dispatch and _dispatch_version() read the octets to choose. Every registry that reaches IPv4, IPv6, AH, ESP, TLS or DTLS has a code for that member alone, so no registry binds IP, IPsec or SSL, and none of the three defines read(): none needs a dispatcher there. Ruled for IGMP on #2444 and for SSL on #2441; no issue holds either ruling. The asymmetry is deliberate, and it concerns the registry only; parsing or building through a base directly is the rule below.

A versioned family’s base dispatches on both paths (#2524). Parsing octets through it, constructing it from fields and its from_data give a member, never an instance of the base, and a subclass of the base is constructed as itself.

The member is the one the version keyword names, or else the one the octets or the data model carry, and never one a field picks. version names the member on either path, unchecked against the octets it is given; where their format carries a version, the member’s own parser refuses another’s. Without it, octets give the member the version they carry, where the format has one, and from_data reads the data model’s class or its version, as the table below gives. No field picks a member, not even a field only one member takes, and no member is a default: fields without version raise ProtocolError, and a field the member does not take fails that member’s own keyword check. Ruled on #2574. A class built directly takes version as well, which must then be its own version: a member’s, or, for a subclass outside the members, the one an IGMP class states in its version, which a subclass of the other bases does not have. While parsing, the keyword is handed on unread, since an IP layer passes its own version under that name to the layer it carries.

Base

version

Octets without version

from_data

HTTP

1, 2

_dispatch_version(): the connection preface, a start line or a frame header

the data model; a dict is refused

IGMP

1, 2, 3

_dispatch(): type, max response time and length, placed by _classify()

version, which each IGMP data model carries though no octet does, ruled on #2574

IP

4, 6

the Version nibble

version

ICMP

4, 6

refused, below

the data model; a dict is refused

L2TP

2

the version nibble

version

LinuxSLL

1, 2

refused, below

the data model; a dict is refused

HTTP octets carry their version, but the HTTP data models have no top-level version field, only HTTP/1’s receipt.version string, so HTTP.from_data reads the data model’s class. It refuses a dict even though the receipt and sid keys would tell the members apart, since no key picks a member (#2628).

Octets that carry no version are refused through the base without version (#2574). An ICMP message and a Linux cooked-capture header have no version field: the enclosing layer names it, by IP protocol number 1 or 58 and by link type 113 or 276, and those registries bind the members. So ICMP(...) and LinuxSLL(...) on octets raise ProtocolError rather than guess, as does their from_data on a dict, whose keys pick no member. A version a family has no member for, such as L2TP’s 1 (L2F) and 3, raises the same way.

Octets neither version takes, a frameless HTTP/2 connection preface among them, are Raw with no error on the transport path, and a direct HTTP(...) on them raises ProtocolError (#2524). Octets the dispatcher gives HTTPv2 that its parser then refuses, such as a preface and three octets, are Raw with the parser’s error.

Handled Refusals

A refusal a next-layer fallback handles is quiet, for every protocol and not only a family’s members (#2588). The fallback is beholder(). It decorates Protocol._import_next_layer and the overrides that parse a next layer themselves. Application._import_next_layer and TLS._import_next_layer are not decorated: they delegate to decorated methods, the base’s or TLS’s _import_next_record. The decorated call runs inside a CaughtByCaller block naming ProtocolError. A refused layer, the HTTP/2 case above or a GRE header of a nonzero version, is then recorded on the error of the Raw that replaces it, and nowhere else: nothing reaches the pcapkit logger at any level, and neither sys.excepthook() nor threading.excepthook() is replaced. A refusal the parser handles is the control flow BaseError’s quiet mode exists for, and installing a hook from an error the library caught itself is a process-wide side effect.

A parse whose refusal a fallback absorbs without beholder() follows the same rule through an explicit CaughtByCaller(ProtocolError) around it, as DTLS.analyze_handshake keeps a body that does not decode raw – an application of #2588’s rule rather than a ruling of its own.

The block spans the whole call to the next layer, not the refusal alone. Every ProtocolError built while that call is on the stack is quiet, at any depth, unless an inner CaughtByCaller says otherwise:

  • one a parser below catches itself, such as the supported_versions peek of SSL._tls_negotiates or TLS’s SSLv2 trial, which is then recorded nowhere;

  • one reporting library state rather than the octets, such as a ModuleDescriptor entry whose class its module has since lost – by a later removal, or by a write to __proto__ directly, since register_tcp() refuses such an entry – which ends as the Raw layer’s error;

  • one raised by an inner fallback’s recovery, since the block of the fallback around it is still active.

What stays loud:

  • Building the refusing layer directly. GRE(...) or IPv4(...) on the same octets raises an error that logs at logging.CRITICAL and, outside development mode, installs the hooks.

  • Any other error class. An error the fallback absorbs is as loud as it was constructed, and beholder() logs it at logging.WARNING as well. Inside a payload Protocol._make_payload rebuilds, it is quiet, as the owner ruled on #2711: the layer the payload is rebuilt for parses the same octets again and reports it there, once.

  • A refusal built under an inner CaughtByCaller(). A block naming no class, such as the one register() enters, makes errors loud again inside the fallback’s.

  • The outermost fallback’s recovery. Reading the payload and building the Raw run after the block has ended, so under the outermost beholder() of a parse an error raised there reaches the caller loud. Each block restores the one around it on exit, so none outlives that parse.

Member Shapes

Shape

id()

In the tree

Narrow to self

the member’s own name only

AH ('AH',), ESP ('ESP',); IGMPv2 ('IGMPv2',) and IGMPv3 ('IGMPv3',) – IGMPv1 narrows the same way, but to the RFC canonical name rather than a versioned one (Canonical Names); HTTPv1 ('HTTP/1',) and HTTPv2 ('HTTP/2',); SSLv2 ('SSLv2',)

Family-inclusive

the member’s label plus the family name

C_Tag ('C_Tag', 'VLAN'), S_Tag ('S_Tag', 'VLAN'); L2TPv2 ('L2TP', 'L2TPv2')

One parser, dynamic alias

fixed, whichever protocol was read

ARP ('ARP',) reads the operation code and reports ARP, RARP, DRARP or InARP through alias; TLS ('TLS',) reports SSLv3 or a TLS version, and DTLS ('DTLS',) a DTLS version (The SSL Family)

Nothing dispatches to InARP ('InARP',): under EtherType 0x0806, operation 8 parses as ARP with alias InARP. Only RARP ('RARP', 'DRARP') is bound to EtherType 0x8035 (Reverse_Address_Resolution_Protocol). A parsed RARP takes DRARP as its alias for operation 5, and DRARP ('DRARP',) inherits the __index__ of RARP.

A family-inclusive member is selected by any sibling’s registry key (a key of pcapkit.protocols.__proto__), because each expands to the shared family name (below). A narrow member is selected by its own name and by the key of each family whose id() lists it: HTTPv1 and HTTPv2 by HTTP, and AH and ESP by IPsec and by IPv6_Ext where IPv4 carries them. Under IPv6 they are no layer of their own but entries of IPv6.extension_headers, so on an Ethernet:IPv6:AH frame 'AH' in, 'IPsec' in and 'IPv6_Ext' in are all False; only frame.protochain matches them.

Chained Records

TLS and SSLv2 records sharing a TCP segment, and DTLS records sharing a UDP datagram, are chained layers, not a list on one layer (#2517, #2567, #2535): each record dissected after the first is the previous record’s payload, so a TLS or DTLS record’s dynamic alias names its own version – a ServerHello then a ChangeCipherSpec read TCP:TLS 1.3:TLS 1.2, and a DTLS 1.2 record then a DTLS 1.3 unified-header record read UDP:DTLS 1.2:DTLS 1.3 – and an SSLv2 CLIENT-HELLO then a CLIENT-MASTER-KEY read TCP:SSLv2:SSLv2.

HTTP/2 frames chain the same way (#2536): a SETTINGS frame then a WINDOW_UPDATE read TCP:HTTP/2:HTTP/2. A later whole frame that fails to decode is Raw with the rest of the segment and the parser’s error, a frame the segment ends inside stays Raw with none, and protocol=, layer= and FRAME_LAYER_LIMIT stop the chain, as for TLS.

Canonical Names

A versioned protocol’s display name includes its version. This is the preference stated in review of #2444, not a settled rule; no issue holds it. L2TPv2’s name is Layer 2 Tunnelling Protocol version 2. The HTTP/1.1 and HTTP/2 classes define no name of their own, so they report Hypertext Transfer Protocol, which carries no version.

ICMPv4 is the case where the RFC’s own name carries no version. Ruled in review of #2446 (#2436): ICMPv4 keeps its versioned class and module name, but its id() drops the version for the RFC canonical name (RFC 792 names it plain ICMP), and its name carries no “version 4”. ICMPv6 keeps its own version, since RFC 4443 names it with one.

IGMPv1 follows the same shape, by the same reasoning, ruled in review of #2506; no issue holds the ruling. RFC 1112 names IGMPv1’s protocol plain IGMP, so IGMPv1.id() is ('IGMP',) and its alias is 'IGMP', while IGMPv2.id() and IGMPv3.id() keep their own versions, ('IGMPv2',) and ('IGMPv3',), since RFC 2236 and RFC 3376 name them with one. The family base’s id() follows: IGMP.id() is ('IGMP', 'IGMPv2', 'IGMPv3'), the family-inclusive shape, because IGMPv1’s canonical name and the family’s own name are the same string – the same shape ICMP.id() takes, ('ICMP', 'ICMPv6').

HTTP takes the RFC names as well (#2524). RFC 9113 names its protocol HTTP/2, so HTTPv2.id() is ('HTTP/2',). RFC 9112 names HTTP/1.1, but the HTTP/1 class also reads messages declaring 1.0 or 0.9, so HTTPv1.id() is the major version, ('HTTP/1',), and alias carries the exact one. No member’s name is the family’s, so HTTP.id() lists the members alone, ('HTTP/1', 'HTTP/2'), the shape of IP.id().

Selector Matching

A family name selects every version, and each version name selects its own. Ruled in review of #2443 (#2438). On an IPv4 frame, 'IP' in is True and 'IPv6' in is False. On an IPv6 frame, 'IP' in is True and 'IPv4' in is False.

HTTP follows it too (#2524). On an HTTP/1.1 frame, 'HTTP' in and 'HTTP/1' in are True and 'HTTP/2' in is False, since the layer is HTTPv1; an HTTP/2 frame is the reverse.

What a selector on a frame or a layer reaches is defined by expand_comp(), __contains__(), __getitem__() and Protocol.__eq__. Selectors are case-insensitive: 'ipv4' in an IPv4 frame is True.

  • String == compares against the class and its own id() only; the selector is not expanded. On a parsed IPv4 layer, ipv4 == 'IPv4' is True and ipv4 == 'IP' is False. On a parsed C-Tag layer, == 'VLAN' is True, since VLAN is in its id(), and == 'S_Tag' is False, though 'S_Tag' in the layer is True. On a parsed HTTP/1.1 layer, == 'HTTP/1' is True and == 'HTTP' is False, since no member’s id() carries HTTP, though 'HTTP' in the layer is True.

  • in and [] expand the selector first. A registry key becomes its class plus that class’s id(), as does a class object, and a layer matches when any of those appears among its own class and id(). So 'S_Tag' in a C-Tag frame is True: S_Tag expands to S_Tag and VLAN, and the C-Tag layer’s id() carries VLAN.

  • A string that is not a registry key is matched by name alone against each layer’s class and id(), so an alias such as 'HTTP/1.1' selects no layer: 'HTTP/1.1' in frame is False.

ProtoChain matches more widely: its index(), count() and in also compare each layer’s alias, so 'HTTP/1.1' in frame.protochain is True.

No Invented Suffixes

The family base takes the family’s own name. IP, IPsec, HTTP, VLAN and L2TP are the precedent. No _Family suffix is added, as ruled in review of #2446 (#2436).

A protocol class takes its module’s name, whatever its canonical name and id(). The family base takes the family’s name and a member its module’s versioned name: igmpv1 holds IGMPv1, though its id() is ('IGMP',) (above). A module holds one protocol class. protocol is exempt: its ProtocolBase and Protocol are framework bases rather than dissectors. Both ruled on #2529.

The name match ignores case and underscores: c_tag holds C_Tag and linux_sll1 holds LinuxSLL1. The schema/ and data/ modules follow the same layout: the top-level class takes the module’s name, and sub-structure classes such as Query share its module. Ruled on #2529.