Packets Reassembly¶
pcapkit.foundation.reassembly implements datagram reassembly
of IP packets, TCP segments and DTLS handshake messages, based on the
algorithms described in RFC 791, RFC 815 and RFC 6347.
See also
Customisation & Extensions, on customisation and extension.
Every reassembly class subclasses
ReassemblyBase, reassembling
extracted fragments into whole datagrams:
flowchart TD
A{{ReassemblyMeta}} -.->|metaclass| B(ReassemblyBase)
B --> IP & TCP & DTLS
IP --> IPv4 & IPv6
B --> C(Reassembly)
C --> D([user customisation ...])
IPv4 ~~~ C
click A "reassembly.html#pcapkit.foundation.reassembly.reassembly.ReassemblyMeta"
click B "reassembly.html#pcapkit.foundation.reassembly.reassembly.ReassemblyBase"
click C "reassembly.html#pcapkit.foundation.reassembly.reassembly.Reassembly"
click D "../../../ext.html#reassembly-and-flow-tracing"
click IP "ip/ip.html#pcapkit.foundation.reassembly.ip.IP"
click IPv4 "ip/ipv4.html#pcapkit.foundation.reassembly.ipv4.IPv4"
click IPv6 "ip/ipv6.html#pcapkit.foundation.reassembly.ipv6.IPv6"
click TCP "tcp.html#pcapkit.foundation.reassembly.tcp.TCP"
click DTLS "dtls.html#pcapkit.foundation.reassembly.dtls.DTLS"
Auxiliary Data¶
- class pcapkit.foundation.reassembly.ReassemblyManager(ipv4, ipv6, tcp, dtls)[source]¶
Bases:
InfoReassembly Manager.
- class pcapkit.foundation.reassembly.data.data.ReassemblyData(ipv4, ipv6, tcp, dtls)[source]¶
Bases:
InfoData storage for reassembly.
What submits a buffer, and how its holes at that point pick the
Completion it reports:
flowchart TD
subgraph trig ["What submits a buffer"]
direction TB
T1["IP: every octet received<br/>TCP: a FIN and every octet below it"]
T2["a session torn down: a TCP RST,<br/>a TCP SYN on a buffered 4-tuple,<br/>an unfragmented IPv4 datagram reusing the BUFID"]
T3["a TCP segment farther than TCP\_GAP\_LIMIT<br/>from its buffer: the stream splits"]
T4["a DTLS fragment whose length or type disagrees<br/>with the message held under its message\_seq"]
T5["an IPv6 fragment overlapping another,<br/>not an exact duplicate: overlap=True"]
T6["Reassembly.expire, as each packet reaches<br/>IP or TCP reassembly: the timeout passed<br/>since a buffer's first fragment, timeout=True"]
T7[".datagram or fetch() read<br/>while buffers are outstanding"]
T1 ~~~ T2 ~~~ T3 ~~~ T4 ~~~ T5 ~~~ T6 ~~~ T7
end
T1D["DTLS: a message whole"]
SUB["submit(buffer)"]
Q1{"overlap?"}
Q2{"no holes left?"}
Q3{"submitted by expire?"}
OV["OVERLAP"]
CO["COMPLETE"]
TO["TIMEOUT"]
PA["PARTIAL"]
CB["datagrams reported<br/>each \_\_callback\_fn\_\_ called with them"]
trig --> SUB
T1D --> CO
SUB --> Q1
Q1 -->|yes| OV
Q1 -->|no| Q2
Q2 -->|yes| CO
Q2 -->|no| Q3
Q3 -->|yes| TO
Q3 -->|no| PA
OV & CO & TO & PA --> CB
click SUB "reassembly.html#pcapkit.foundation.reassembly.reassembly.Reassembly.submit"
click T6 "reassembly.html#pcapkit.foundation.reassembly.reassembly.Reassembly.expire"
click T7 "reassembly.html#pcapkit.foundation.reassembly.reassembly.Reassembly.fetch"
click OV "index.html#pcapkit.foundation.reassembly.data.data.Completion.OVERLAP"
click CO "index.html#pcapkit.foundation.reassembly.data.data.Completion.COMPLETE"
click TO "index.html#pcapkit.foundation.reassembly.data.data.Completion.TIMEOUT"
click PA "index.html#pcapkit.foundation.reassembly.data.data.Completion.PARTIAL"
- class pcapkit.foundation.reassembly.data.data.Completion(*values)[source]¶
Bases:
EnumLookup,StrEnumHow completely a datagram was reassembled, and why it stopped.
Derives from
EnumLookupper GitHub issue #877’s ruling that every non-registry enumeration shares that lookup contract; the class defines neithergetnor_missing_of its own.This is the value of
Datagram.completed, which widens a plainboolrather than adding a second channel beside it: reassembly has more outcomes to report than aboolcan hold, since a buffer abandoned under the RFC 1122 / RFC 8200 reassembly timeout is a different event from one that simply had not finished when the capture did. An expired datagram says “these fragments are gone”, a partial one says “these fragments had not arrived yet”.Truthiness is that of a
bool:COMPLETEis the only truthy member, soif datagram.completed:reads as it would for a boolean. Equality againstTrueandFalseis not preserved –datagram.completed == TrueisFalseeven for a complete datagram – so a caller comparing against a boolean has to compare against a member instead.It derives from
pcapkit.utilities.compat’sStrEnum, asTypedoes, which buys what a plainenum.Enumdoes not: the value survivesjson.dumps()– a plain enumeration raisesTypeErrorthere, andDatagram.to_dicthands this field straight out – anddatagram.completed == 'timeout'works, so a state can be tested for without importing this class.pcapkit.const.pcapng.tls_key_label.TLSKeyLabelis the same kind of string enumeration, but it is generated like itspcapkit.const.pcapngsiblings (#886), so it derives fromaenum’s ownStrEnum, alongsideEnumRegistry. The properties above hold for it, asaenum.StrEnumis astrsubclass, butisinstance/issubclassagainstpcapkit.utilities.compat.StrEnumdoes not on Python 3.11 and later, where that name is the stdlibenum.StrEnum.Warning
Being a
strwhose members other thanCOMPLETEare falsy makes this a non-empty string that tests false, sobool(x)andbool(str(x))disagree. That is deliberate – the truthiness above is what letsif datagram.completed:read as a boolean test – but code that takes this for an ordinary string and tests it for truth will read it backwards.- COMPLETE = 'complete'¶
Reassembled in whole: every octet of the datagram was received.
- PARTIAL = 'partial'¶
Fragments were still outstanding when the buffer was flushed – at the end of the capture, or when the session was torn down (a TCP RST, a TCP SYN on a 4-tuple already buffered, or an IPv4 datagram whose identifier was reused by an unfragmented packet). The missing octets may simply not have been captured.
- TIMEOUT = 'timeout'¶
Reassembly was abandoned under the reassembly timeout, i.e. the capture clock advanced past the deadline of
Reassembly.timeoutseconds after the first-arriving fragment while the datagram was still incomplete. RFC 8200 Section 4.5 requires the held fragments be discarded, so no further fragment will ever be added to this datagram.
- OVERLAP = 'overlap'¶
Reassembly was abandoned because a fragment overlapped another of the same datagram without being an exact duplicate of it, which RFC 8200 Section 4.5 discards an IPv6 datagram for. The overlapping fragment’s octets are not in the payload;
conflictstill lists where they disagreed with the octets kept.Added in version 1.6.0.
- __bool__()[source]¶
Whether the datagram was reassembled in whole.
Only
COMPLETEis truthy; every other member describes an incomplete datagram.Note
This override is what a
strbase does not give – every non-empty string is otherwise truthy, which would make an incomplete datagram read as a complete one.__str__needs no such override:pcapkit.utilities.compat’sStrEnumalready renders a member as its value.- Return type:
- class pcapkit.foundation.reassembly.data.data.Deferred[_KT: (TransType, tuple[AppType_TCP, AppType_TCP], tuple[HandshakeType, int, int])](analyze, proto, payload)[source]¶
Bases:
ReduceAsProtocol2,Generic[_KT]A postponed analysis of a reassembled payload.
A reassembled datagram’s
packetis a second, full parse of the payload the datagram just reassembled. Nothing about postponing it is specific to any one reassembler, which is why this lives besideReassemblyDatarather than in any one protocol’s data module.IP reassembly is the case that made it necessary. IPv4 reassembly submits a datagram for every frame whose DF flag is clear – not only the fragmented ones, since an unfragmented frame still reaches
IP.reassemblyand is submitted there as a trivially complete datagram – so an eager parse would re-parse captures holding no fragments at all:http.pcaphas 1117 IPv4 frames, none of them fragmented, and the eager parse measured 86% of the cost of IP reassembly over it.TCP reassembly defers its
packetas well (TCP.submit), at a far smaller saving, being FIN/RST-driven rather than per-frame – 222 submits perhttp.pcappass against 1117. DTLS reassembly defers its own for parity with IP and TCP rather than for a saving, since a handshake message reaches it only once fragmented (#2531).Holding the call here defers it to the first read of
Datagram.packet, whereDeferredPacketruns it, so a caller that wants the parsed payload still gets exactly the object an eager call would produce, and one that does not never pays for it.The class is generic because the reassemblers do not dispatch on the same kind of value, and each analyser declares the one it takes. The type parameter is that value’s type, so an instance’s
protoand the parameter itsanalyzeexpects are the same type rather than a union every call site would have to widen to:IP.submitbuilds aDeferred[TransType]. It passes the payload protocol type, aTransTyperead from the reassembled datagram’s Protocol / Next Header field, to the parseip_datagram()runs:Protocol.analyze, which looks the next layer up by protocol registry number.TCP.submitbuilds aDeferred[tuple[AppType_TCP, AppType_TCP]]. It passes a(source, destination)port pair to the parsetcp_datagram()runs, becauseTransport.analyzeoverrides that parameter to one: a reassembled transport-layer payload carries no protocol number, so its application protocol is inferred from the ports.DTLS._datagram_from_fragmentbuilds aDeferred[tuple[HandshakeType, int, int]]for a complete message. It passes the message’s(msg_type, length, message_seq), its handshake header as if unfragmented (RFC 6347 Section 4.2.3), to the decoderdtls_handshake()runs,DTLS.analyze_handshake, which reads the body by itsHandshakeType.
Those are the cases the type parameter is constrained to. IP’s and TCP’s are named registry values rather than one named value and one pair of bare port numbers: a port is a member of
TCPhere – the per-transport registry itself, not the baseAppType, since that is whatTCP._make_portsreally answers with – so what the deferred analysis dispatches on is of a piece with what the eager path dispatches on atTCP.src, whichever reassembler built it. (Of a piece, not identical: for an unassigned port the two paths build equal members by different routes, whichTCP.submit’s own docstring records.)The pairing the regression actually produced – a port pair handed to an analyser declared over
TransType– is rejected, which is the direction a real mismatch takes and the one #2417 was about.Rejecting the reverse pairing is not guaranteed by mypy alone, for a reason outside this class:
aenumships no type stubs, so underignore_missing_importstheaenum.IntEnumbase ofTransTyperesolves toAny. ATransTypeis therefore accepted where atuple[AppType_TCP, AppType_TCP]is expected, and aDeferred[TransType]built over the port-pair analyser type-checks clean. That is a gap in the dependency’s type coverage rather than in the constraint, which would bite both ways wereTransTypefully typed; the values the reassemblers really pass are pinned by the tests either way.- Parameters:
analyze (
Callable[[_KT,bytes],ProtocolBase|DTLSHandshake]) – The analyser to call: the parsepcapkit.foundation.analysisruns for the engine, bound to the reassembly object’s protocol for IP and TCP, whoseanalyzeit calls, and that protocol’sanalyze_handshakefor DTLS.proto (
_KT) – What the analyser dispatches the payload on – the payload protocol type for IP, the port pair for TCP, the handshake header for DTLS, as above.payload (
bytes) – Reassembled payload to parse.
Added in version 1.5.0.
Changed in version 1.6.0: Also constrained to the DTLS handshake header, and the analyser may return a
DTLSHandshake(#2531). IP’s and TCP’s analyser is the parse theirpcapkit.foundation.analysismodule runs, bound to the reassembly’s protocol, rather than that protocol’s boundanalyze(#2581). Protocol 0 and 1 reduce as protocol 2 does, throughReduceAsProtocol2, which carries the slots in Python’s own default state, in place of a reduction and a__getstate__of its own (#2714).- __setstate__(state)[source]¶
Unpickling support.
Accepts the
(dict_or_None, slots)pair Python’s own default state construction builds for a slotted class, which every pickle of this class carries under protocol 2 and above, and whichReduceAsProtocol2carries under protocol 0 and 1 too. It also accepts a baredictof{slot: value}, the state a first, unreleased fix for #2423 pickled (#2425).