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');AHandESPare not in it, since they belong to the separate abstractIPsec, whoseid()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')andL2TP.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 |
|
Octets without |
|
|---|---|---|---|
|
|
the data model; a |
|
|
|
|
|
|
the Version nibble |
|
|
|
refused, below |
the data model; a |
|
|
the version nibble |
|
|
|
refused, below |
the data model; a |
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_versionspeek ofSSL._tls_negotiatesor TLS’s SSLv2 trial, which is then recorded nowhere;one reporting library state rather than the octets, such as a
ModuleDescriptorentry whose class its module has since lost – by a later removal, or by a write to__proto__directly, sinceregister_tcp()refuses such an entry – which ends as theRawlayer’serror;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(...)orIPv4(...)on the same octets raises an error that logs atlogging.CRITICALand, 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 atlogging.WARNINGas well. Inside a payloadProtocol._make_payloadrebuilds, 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 oneregister()enters, makes errors loud again inside the fallback’s.The outermost fallback’s recovery. Reading the payload and building the
Rawrun after the block has ended, so under the outermostbeholder()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 |
|
In the tree |
|---|---|---|
Narrow to self |
the member’s own name only |
|
Family-inclusive |
the member’s label plus the family name |
|
One parser, dynamic alias |
fixed, whichever protocol was read |
|
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 ownid()only; the selector is not expanded. On a parsed IPv4 layer,ipv4 == 'IPv4'isTrueandipv4 == 'IP'isFalse. On a parsed C-Tag layer,== 'VLAN'isTrue, sinceVLANis in itsid(), and== 'S_Tag'isFalse, though'S_Tag' inthe layer isTrue. On a parsed HTTP/1.1 layer,== 'HTTP/1'isTrueand== 'HTTP'isFalse, since no member’sid()carriesHTTP, though'HTTP' inthe layer isTrue.inand[]expand the selector first. A registry key becomes its class plus that class’sid(), as does a class object, and a layer matches when any of those appears among its own class andid(). So'S_Tag' ina C-Tag frame isTrue:S_Tagexpands toS_TagandVLAN, and the C-Tag layer’sid()carriesVLAN.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 frameisFalse.
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.