TCP Datagram Reassembly¶
pcapkit.foundation.reassembly.tcp contains
Reassembly only,
which reconstructs fragmented TCP packets back to origin.
- class pcapkit.foundation.reassembly.tcp.TCP(*, strict=True, store=True, timeout=None)[source]¶
Bases:
ReassemblyBase[Packet,Datagram,tuple[_AT,int,_AT,int],Buffer]Reassembly for TCP payload.
- Parameters:
strict (
bool) – if return all datagrams (including those not implemented) when submitstore (
bool) – if store reassembled datagram in memory, i.e.,self._dtgram(if not, datagram will be discarded after callback)timeout (
float|None) – reassembly timeout in seconds, on the capture’s own clock;Noneselects__timeout__, which for TCP disables expiry
Example
>>> from pcapkit.foundation.reassembly import TCP # Initialise instance: >>> tcp_reassembly = TCP() # Call reassembly: >>> tcp_reassembly(packet_dict) # Fetch result: >>> result = tcp_reassembly.datagram
Note
There are two coordinate systems in play here, and keeping them apart matters. The hole descriptor list of RFC 815 is kept in absolute TCP sequence numbers, inclusive of both bounds, because a hole belongs to the connection’s sequence space for that direction and not to any one payload buffer: the list is held once per buffer ID, while each acknowledgement number gets a payload buffer of its own with an initial sequence number of its own, and that initial sequence number is revised whenever a segment turns up below the data already buffered. A payload buffer, on the other hand, is indexed from zero, such that
buffer.raw[n]holds the octet with sequence numberbuffer.isn+ n.submit()is therefore the one place that converts between the two, subtracting that buffer’s initial sequence number from each hole bound.- submit(buf, *, bufid, timeout=False)[source]¶
Submit reassembled payload.
- Parameters:
- Return type:
- Returns:
Reassembled packets.
- __protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.transport.tcp.TCP'>¶
Protocol of current reassembly object.
Algorithm¶
See also
This algorithm is an adaptation of the algorithm described in RFC 815.
Attribute |
Definition |
|---|---|
|
Data Sequence Number |
|
TCP Acknowledgement |
|
TCP Synchronisation Flag |
|
TCP Finish Flag |
|
TCP Reset Connection Flag |
|
Buffer Identifier |
|
Hole Descriptor List |
|
Initial Sequence Number |
|
Payload Sequence Number |
|
source IP |
|
destination IP |
|
source TCP port |
|
destination TCP port |
DO {
BUFID <- src|srcport|dst|dstport;
/* a SYN occupies a sequence number of its own, so payload sent by
or after it starts one octet later than the segment's DSN */
PSN <- DSN + 1 IF (SYN is true) ELSE DSN;
IF (SYN is true) {
IF (buffer with BUFID is allocated) {
flush all reassembly for this BUFID;
submit datagram to next step;
}
}
IF (no buffer with BUFID is allocated) {
allocate reassembly resources with BUFID;
ISN <- PSN;
put data from fragment into data buffer with BUFID
[from octet fragment.first to octet fragment.last];
HDL <- [one hole from PSN + fragment.len to infinity];
} ELSE {
put data from fragment into data buffer with BUFID
[from octet fragment.first to octet fragment.last];
/* a segment with no payload fills no hole, and its "last" lies
one below its "first", so it is not run through the algorithm */
IF (fragment.len > 0) {
update HDL;
}
}
IF (FIN is true or RST is true) {
submit datagram to next step;
free all reassembly resources for this BUFID;
BREAK.
}
} give up until (next fragment);
update HDL: {
DO {
select the next hole descriptor from HDL;
IF (fragment.first > hole.last) CONTINUE.
IF (fragment.last < hole.first) CONTINUE.
delete the current entry from HDL;
IF (fragment.first > hole.first) {
create new entry "new_hole" in HDL;
new_hole.first <- hole.first;
new_hole.last <- fragment.first - 1;
}
IF (fragment.last < hole.last AND FIN is false AND RST is false) {
create new entry "new_hole" in HDL;
new_hole.first <- fragment.last + 1;
new_hole.last <- hole.last;
}
BREAK.
} give up until (no entry from HDL)
}
The following algorithm implement is based on the IP Datagram
Reassembly Algorithm introduced in RFC 815, which presents hole
descriptors as an alternative to the RCVBT (fragment received bit
table) approach used by the reassembly procedure in RFC 791. And
here is the process:
Select the next hole descriptor from the hole descriptor list. If there are no more entries, go to step eight.
If
fragment.firstis greater thanhole.last, go to step one.If
fragment.lastis less thanhole.first, go to step one.Delete the current entry from the hole descriptor list.
If
fragment.firstis greater thanhole.first, then create a new hole descriptornew_holewithnew_hole.firstequal tohole.first, andnew_hole.lastequal tofragment.firstminus one (-1).If
fragment.lastis less thanhole.lastand neitherFINnorRSTis set – TCP has no more fragments flag, so the termination flags take its place – then create a new hole descriptornew_hole, withnew_hole.firstequal tofragment.lastplus one (+1) andnew_hole.lastequal tohole.last.Go to step one.
If the hole descriptor list is now empty, the datagram is now complete. Pass it on to the higher level protocol processor for further handling. Otherwise, return.
Terminology¶
- reasm.tcp.packet¶
Data structure for TCP datagram reassembly (
TCP.reassembly) is as following:packet_dict = Info( bufid = tuple( ip.src, # source IP address tcp.srcport, # source port ip.dst, # destination IP address tcp.dstport, # destination port ), dsn = tcp.seq, # data sequence number ack = tcp.ack, # acknowledgement number num = frame.number, # original packet range number syn = tcp.flags.syn, # synchronise flag fin = tcp.flags.fin, # finish flag rst = tcp.flags.rst, # reset connection flag len = tcp.raw_len, # payload length, header excludes first = tcp.seq, # first sequence number of payload last = tcp.seq + tcp.raw_len - 1, # last sequence number of payload header = tcp.packet.header, # raw bytes type header payload = tcp.raw, # raw bytearray type payload timestamp = float( frame.time_epoch), # capture timestamp )
Both
firstandlastare absolute TCP sequence numbers and both are inclusive, so a segment carrying no payload at all haslastone belowfirst.- reasm.tcp.datagram¶
Data structure for reassembled TCP datagram (element from
TCP.datagramtuple) is as following:(tuple) datagram |--> (Info) data | |--> 'completed' : (Completion) COMPLETE --> reassembled in whole | |--> 'id' : (Info) original packet identifier | | |--> 'src' --> (tuple) | | | |--> (IPv4Address) ip.src | | | |--> (int) tcp.srcport | | |--> 'dst' --> (tuple) | | | |--> (IPv4Address) ip.dst | | | |--> (int) tcp.dstport | | |--> 'ack' --> (int) original packet ACK number | |--> 'index' : (tuple) packet numbers | | |--> (int) original packet range number | | |--> ... | |--> 'header' : (bytes) initial TCP header | |--> 'payload' : (bytes) reassembled payload | |--> 'packet' : (Protocol) parsed reassembled payload | |--> 'conflict' : (tuple) sequence ranges on which two segments disagreed | | |--> (tuple) (first, last), absolute and inclusive | | |--> ... |--> (Info) data | |--> 'completed' : (Completion) PARTIAL or TIMEOUT --> incomplete | |--> 'id' : (Info) original packet identifier | | |--> 'src' --> (tuple) | | | |--> (IPv4Address) ip.src | | | |--> (int) tcp.srcport | | |--> 'dst' --> (tuple) | | | |--> (IPv4Address) ip.dst | | | |--> (int) tcp.dstport | | |--> 'ack' --> (int) original packet ACK number | |--> 'index' : (tuple) packet numbers | | |--> (int) original packet range number | | |--> ... | |--> 'header' : (bytes) initial TCP header | |--> 'payload' : (tuple) partially reassembled payload | | |--> (bytes) payload fragment | | |--> ... | |--> 'packet' : (None) not implemented | |--> 'conflict' : (tuple) sequence ranges on which two segments disagreed | | |--> (tuple) (first, last), absolute and inclusive | | |--> ... |--> (Info) data ...
completedandconflictare independent signals: a datagram can beCOMPLETEand still carry a non-emptyconflict– the resolution of a conflicting overlap is first-write-wins (RFC 9293 Section 3.10), so it never leaves a hole, and a contested range that was later filled in around does not stop the datagram from completing.conflictis what lets a caller tell a clean stream from a contested one, now thatcompletedalone no longer can.- reasm.tcp.buffer¶
Data structure for internal buffering when performing reassembly algorithms (
TCP._buffer) is as following:(dict) buffer --> memory buffer for reassembly |--> (tuple) BUFID : (dict) | |--> ip.src | | |--> tcp.srcport | | |--> ip.dst | | |--> tcp.dstport | | |--> 'hdl' : (list) hole descriptor list | | |--> (Info) hole --> hole descriptor | | |--> "first" --> (int) sequence number of the | | | first missing octet | | |--> "last" --> (int) sequence number of the | | last missing octet, inclusive | |--> 'hdr' : (bytes) initial TCP header | |--> 'ack' : (dict) ACK list | |--> (int) ACK : (dict) | | |--> 'ind' : (list) list of reassembled packets | | | |--> (int) packet range number | | |--> 'isn' : (int) sequence number of the octet | | | held in raw[0] | | |--> 'len' : (int) length of payload buffer | | |--> 'raw' : (bytearray) reassembled payload, | | holes set to b'\x00' | | |--> 'gap' : (list) sequence ranges still | | | zero-fill placeholder in 'raw' | | | |--> (tuple) (first, last), | | | absolute and | | | inclusive | | | |--> ... | | |--> 'conflict' : (list) sequence ranges on which | | | an arriving segment disagreed | | | with bytes already in 'raw' | | | |--> (tuple) (first, last), | | | absolute and | | | inclusive | | | |--> ... | |--> (int) ACK ... | |--> ... | |--> 'timestamp' : (float) capture timestamp of the | first segment buffered |--> (tuple) BUFID ...
gapis deliberately not derived fromhdlabove.hdlis shared by every ACK in this dict, while each ACK’s ownrawis private to it, so a different ACK’s segment closing a hole inhdlsays nothing about whether this ACK has received anything at the same sequence numbers – consultinghdlfor that question previously discarded a fragment’s own real bytes whenever a different ACK bucket under the same buffer ID happened to cover the same range first.gapis kept in the same absolute, inclusive sequence number convention asconflictabove (and ashdl’s own hole descriptors), rather than as a per-octet marker aligned withraw. That is what lets it surviveisnbeing revised downwards by a reach-back segment: a per-octet marker aligned withrawhas to be re-prefixed in lockstep with every such revision, while an absolute interval needs no shifting at all. It also means a fragment with no holes carries an empty list instead of araw-sized marker.Note
TCP reassembly has no timeout by default: no specification gives stream reassembly a deadline the way RFC 1122 Section 3.3.2 and RFC 8200 Section 4.5 give IP fragmentation one, and an idle connection is ordinary rather than pathological.
timestampis recorded regardless, so passingtimeouttoTCPenables the same eviction the IP reassemblers use – seeTCP.__timeout__.The hole descriptor list is kept in absolute TCP sequence numbers, once per
BUFID, whereas each ACK’s payload buffer is indexed from its ownisn–raw[n]holds the octet with sequence numberisn + n, andisnis revised downwards whenever a segment turns up below the data already buffered, so it is not necessarily the connection’s own initial sequence number.TCP.submitis the one place that converts between the two.
Data Models¶
- class pcapkit.foundation.reassembly.data.tcp.Packet(*args: VT, **kwargs: VT)[source]¶
Bases:
InfoData model for TCP packet representation.
- first: int¶
Sequence number of the first octet of
payload, i.e. the segment’s own sequence number. Absolute, not an offset into any payload buffer.
- class pcapkit.foundation.reassembly.data.tcp.DatagramID(*args: VT, **kwargs: VT)[source]¶
-
Data model for TCP original packet identifier.
- class pcapkit.foundation.reassembly.data.tcp.Datagram(*args: VT, **kwargs: VT)[source]¶
Bases:
DeferredPacket,Info,Generic[_AT]Data model for TCP.
- completed: Completion¶
How completely the datagram was reassembled, and why reassembly stopped; see
Completion. OnlyCompletion.COMPLETEis truthy, soif datagram.completed:reads as it did while this was abool.
- id: DatagramID[_AT]¶
Original packet identifier.
- packet: ProtocolBase | None¶
Parsed TCP payload. Analysed on first read rather than at construction; a
Deferredmay be passed in its place, and reading this attribute then runs it and keeps the result.
- conflict: tuple[tuple[int, int], ...]¶
Sequence ranges on which two segments disagreed, i.e. where an arriving segment overlapped bytes already buffered but did not repeat them. Each entry is
(first, last), absolute TCP sequence numbers and both inclusive – the same convention asPacket.firstandPacket.last. Empty when the stream never saw a contested byte.Resolution keeps the already-buffered bytes and discards the conflicting portion of whichever segment arrived later, per RFC 9293 Section 3.10 (“we reconstruct the segment to contain just the new data”); this field is what lets a caller tell a clean stream from a contested one now that
completedno longer does, since a contested range does not, on its own, leave a hole.
- class pcapkit.foundation.reassembly.data.tcp.HoleDescriptor(*args: VT, **kwargs: VT)[source]¶
Bases:
InfoData model for TCP hole descriptor.
Both bounds are absolute TCP sequence numbers and both are inclusive, so a hole covers
last - first + 1octets. They are not offsets intoFragment.raw: the descriptor list is kept once per buffer ID, whereas each acknowledgement number’s payload buffer carries an initial sequence number of its own, so onlyTCP.submit– which knows which buffer it is looking at – can convert one to the other.
- class pcapkit.foundation.reassembly.data.tcp.Fragment(*args: VT, **kwargs: VT)[source]¶
Bases:
InfoData model for TCP ACK list fragment item.
- isn: int¶
Sequence number of the octet held in
raw[0], i.e. the origin this buffer is indexed from:raw[n]holds the octet whose sequence number isisn + n. Revised downwards whenever a segment turns up below the data already buffered, so it is not necessarily the connection’s own initial sequence number.
- gap: list[tuple[int, int]]¶
Sequence ranges, absolute and inclusive, still zero-fill placeholder in
rawrather than an actually-received byte of this fragment. Only the two gap-creating sites inTCP.reassembly– the forward append and the reach-back prepend, the only places that ever splice abytearray(GAP)filler intoraw– add an entry; an overlap merge only ever shrinks or removes one, filling it from the arriving segment.This is deliberately not derived from
Buffer.hdl.hdlis one list shared by every acknowledgement number under the same buffer ID, so a segment landing in a different fragment can close a hole inhdlthat this fragment’s ownrawnever filled – and consultinghdlto decide whether an overlapping position here was “already received” then answers a question about the wrong fragment. Tracking gaps on the fragment itself is what keeps the merge inTCP.reassemblyfrom discarding this fragment’s own real bytes because some other fragment happened to have received something at the same absolute sequence numbers.Absolute sequence numbers rather than offsets into
raw–conflictbelow uses the same convention – for two reasons: it is whatTCP.reassemblyalready computes (GAP = PSN - (ISN + LEN)and its mirror), so this reuses an existing concept rather than adding a second one; and it means a gap entry never needs shifting whenisnis revised downward by the reach-back path, unlike an offset-based or a per-octet representation. A typical fragment carries zero or a handful of entries, against a per-octet marker the length of the whole payload – the difference that matters on a clean stream, where the per-octet form pays a buffer-sized cost to record that nothing is missing at all.
- conflict: list[tuple[int, int]]¶
Sequence ranges, absolute and inclusive, on which an arriving segment disagreed with bytes already held in
raw. Accumulated across every merge into this fragment, in the order the conflicts were found; carried ontoDatagram.conflictverbatim when the buffer is submitted.
Type Variables¶
- pcapkit.foundation.reassembly.data.tcp._AT: ipaddress.IPv4Address | ipaddress.IPv6Address¶