Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/source/pcapkit/foundation/reassembly/tcp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ Terminology
| |--> '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
Expand All @@ -225,8 +228,20 @@ Terminology
| | |--> (bytes) payload fragment
| | |--> ...
| |--> 'packet' : (None) not implemented
| |--> 'conflict' : (tuple) sequence ranges on which two segments disagreed
| | |--> (tuple) (first, last), absolute and inclusive
| | |--> ...
|--> (Info) data ...

``completed`` and ``conflict`` are independent signals: a datagram can
be :attr:`~pcapkit.foundation.reassembly.data.data.Completion.COMPLETE`
and still carry a non-empty ``conflict`` -- 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. ``conflict`` is
what lets a caller tell a clean stream from a contested one, now that
``completed`` alone no longer can.

reasm.tcp.buffer
Data structure for internal buffering when performing reassembly algorithms
(:attr:`TCP._buffer <pcapkit.foundation.reassembly.reassembly.Reassembly._buffer>`)
Expand Down Expand Up @@ -256,12 +271,43 @@ Terminology
| | |--> '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 ...

``gap`` is deliberately **not** derived from ``hdl`` above. ``hdl`` is
shared by every ACK in this dict, while each ACK's own ``raw`` is
private to it, so a different ACK's segment closing a hole in ``hdl``
says nothing about whether *this* ACK has received anything at the
same sequence numbers -- consulting ``hdl`` for 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.

``gap`` is kept in the same **absolute, inclusive sequence number**
convention as ``conflict`` above (and as ``hdl``'s own hole
descriptors), rather than as a per-octet marker aligned with ``raw``.
That is what lets it survive ``isn`` being revised downwards by a
reach-back segment: a per-octet marker aligned with ``raw`` has 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 a ``raw``-sized marker.

.. note::

TCP reassembly has **no** timeout by default: no specification gives
Expand Down
59 changes: 57 additions & 2 deletions pcapkit/foundation/reassembly/data/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,27 @@ class Datagram(DeferredPacket, Info, Generic[_AT]):
#: Parsed TCP payload. Analysed on first read rather than at construction;
#: a :class:`Deferred` may be passed in its place.
packet: 'Optional[Protocol]'
#: 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 as :attr:`Packet.first` and
#: :attr:`Packet.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 :attr:`completed` no longer does, since a
#: contested range does not, on its own, leave a hole.
conflict: 'tuple[tuple[int, int], ...]'

if TYPE_CHECKING:
# NOTE: one signature rather than a pair of ``@overload``\\ s keyed on
# ``completed`` -- for the reason given on
# :class:`~pcapkit.foundation.reassembly.data.ip.Datagram`, which applies
# here identically: ``strict=False`` reports an incomplete payload buffer as
# one contiguous ``bytes`` and analyses it.
def __init__(self, completed: 'Completion', id: 'DatagramID[_AT]', index: 'tuple[int, ...]', header: 'bytes', payload: 'bytes | tuple[bytes, ...]', packet: 'Optional[Protocol | Deferred]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
def __init__(self, completed: 'Completion', id: 'DatagramID[_AT]', index: 'tuple[int, ...]', header: 'bytes', payload: 'bytes | tuple[bytes, ...]', packet: 'Optional[Protocol | Deferred]', conflict: 'tuple[tuple[int, int], ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin


@info_final
Expand Down Expand Up @@ -156,9 +169,51 @@ class Fragment(Info):
len: 'int'
#: Reassembled payload holes set to b'\x00'.
raw: 'bytearray'
#: Sequence ranges, absolute and inclusive, still zero-fill placeholder in
#: :attr:`raw` rather than an actually-received byte *of this fragment*.
#: Only the two gap-creating sites in :meth:`TCP.reassembly
#: <pcapkit.foundation.reassembly.tcp.TCP.reassembly>` -- the forward
#: append and the reach-back prepend, the only places that ever splice a
#: ``bytearray(GAP)`` filler into :attr:`raw` -- add an entry; an overlap
#: merge only ever shrinks or removes one, filling it from the arriving
#: segment.
#:
#: This is deliberately **not** derived from
#: :attr:`Buffer.hdl <pcapkit.foundation.reassembly.data.tcp.Buffer.hdl>`.
#: ``hdl`` is one list shared by every acknowledgement number under the
#: same buffer ID, so a segment landing in a *different* fragment can
#: close a hole in ``hdl`` that this fragment's own :attr:`raw` never
#: filled -- and consulting ``hdl`` to 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 in :meth:`TCP.reassembly
#: <pcapkit.foundation.reassembly.tcp.TCP.reassembly>` from 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 :attr:`raw` --
#: :attr:`conflict` below uses the same convention -- for two reasons:
#: it is what :meth:`TCP.reassembly
#: <pcapkit.foundation.reassembly.tcp.TCP.reassembly>` already 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 when :attr:`isn` is 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.
gap: 'list[tuple[int, int]]'
#: Sequence ranges, absolute and inclusive, on which an arriving segment
#: disagreed with bytes already held in :attr:`raw`. Accumulated across
#: every merge into this fragment, in the order the conflicts were found;
#: carried onto :attr:`Datagram.conflict
#: <pcapkit.foundation.reassembly.data.tcp.Datagram.conflict>` verbatim
#: when the buffer is submitted.
conflict: 'list[tuple[int, int]]'

if TYPE_CHECKING:
def __init__(self, ind: 'list[int]', isn: 'int', len: 'int', raw: 'bytearray') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin
def __init__(self, ind: 'list[int]', isn: 'int', len: 'int', raw: 'bytearray', gap: 'list[tuple[int, int]]', conflict: 'list[tuple[int, int]]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin


@info_final
Expand Down
Loading
Loading