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
14 changes: 8 additions & 6 deletions docs/source/contributing/conventions/reassembly-integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ the default engine and is disabled at the engine level on every other.
handshake message (:issue:`2496`). IP and TCP have no such method: what parses their
outcome is the general-purpose
:meth:`Protocol.analyze <pcapkit.protocols.protocol.Protocol.analyze>` dispatcher,
reused via :class:`~pcapkit.foundation.reassembly.data.data.Deferred`, not a method
written for reassembly specifically.
not a method written for reassembly specifically.

DTLS differs from the IP/TCP shape where the rule is silent: its reassembled body is
reported eagerly, with no deferred ``packet``, and
:class:`~pcapkit.foundation.reassembly.dtls.DTLS` sets no ``__protocol_type__``
(:issue:`2531`).
**Where the rule is silent, DTLS takes the IP/TCP shape** (:issue:`2531`). IPv4, IPv6,
TCP and DTLS reassembly each set ``__protocol_type__``, and each reports its parsed
outcome as a datagram's ``packet``, deferred to first read by
:class:`~pcapkit.foundation.reassembly.data.data.Deferred`. IP and TCP run
``__protocol_type__``'s ``analyze``. DTLS runs the decoder behind ``dtls_handshake``
instead, where point 3 places it, so its ``packet`` is what ``dtls_handshake`` returns:
:data:`None` for an incomplete message.
9 changes: 6 additions & 3 deletions docs/source/pcapkit/foundation/reassembly/dtls.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ DTLS handshake message split across several records back to origin
.. automethod:: _update_hole_descriptors

.. autoattribute:: __protocol_name__
.. autoattribute:: __protocol_type__

Algorithm
=========
Expand Down Expand Up @@ -63,10 +64,12 @@ Terminology
:attr:`DTLS.datagram <pcapkit.foundation.reassembly.reassembly.Reassembly.datagram>`)
is :class:`~pcapkit.foundation.reassembly.data.dtls.Datagram` --
``completed``, the original packet identifier, the packet numbers
that built it, and ``body``, the reassembled payload (a contiguous
that built it, ``body``, the reassembled payload (a contiguous
:obj:`bytes`, or -- under ``strict=True`` and an incomplete message --
a :obj:`tuple` of the octet runs actually received), which
:func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` decodes.
a :obj:`tuple` of the octet runs actually received), and ``packet``,
what :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` decodes
``body`` to. ``packet`` is decoded on first read, as an IP or TCP
datagram's is, and is :data:`None` for an incomplete message.

reasm.dtls.buffer
Data structure for internal buffering
Expand Down
3 changes: 2 additions & 1 deletion pcapkit/corekit/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ class DeferredPacket:

It is shared by
:class:`pcapkit.foundation.reassembly.data.ip.Datagram`,
:class:`pcapkit.foundation.reassembly.data.tcp.Datagram` and
:class:`pcapkit.foundation.reassembly.data.tcp.Datagram`,
:class:`pcapkit.foundation.reassembly.data.dtls.Datagram` and
:class:`pcapkit.foundation.traceflow.data.tcp.Index`, which differ only in
the placeholder class each names in :attr:`__deferred__`
(:issue:`1516`).
Expand Down
41 changes: 33 additions & 8 deletions pcapkit/foundation/analysis/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
if TYPE_CHECKING:
from typing import Optional

from pcapkit.const.tls.handshake_type import HandshakeType as Enum_HandshakeType
from pcapkit.foundation.reassembly.data.dtls import Datagram as DTLS_Datagram

__all__ = ['dtls_handshake']
Expand All @@ -45,23 +46,47 @@ def dtls_handshake(datagram: 'DTLS_Datagram') -> 'Optional[Data_DTLSHandshake]':
they do not decode as its ``msg_type``. :data:`None` if ``datagram`` is
incomplete, as its missing octets cannot be decoded.

Note:
:attr:`Datagram.packet <pcapkit.foundation.reassembly.data.dtls.Datagram.packet>`
holds the same result, decoded on its first read.

"""
if not datagram.completed:
return None
# NOTE: a complete message is always one contiguous run.
body = cast('bytes', datagram.body)
message = (bytes([int(datagram.msg_type)]) + datagram.length.to_bytes(3, 'big')
+ datagram.message_seq.to_bytes(2, 'big') + bytes(3) # fragment_offset 0
+ datagram.length.to_bytes(3, 'big') + body)
return _read_handshake((datagram.msg_type, datagram.length, datagram.message_seq),
cast('bytes', datagram.body))


def _read_handshake(header: 'tuple[Enum_HandshakeType, int, int]', body: 'bytes') -> 'Data_DTLSHandshake':
"""Decode a complete handshake message from its header and body.

:func:`dtls_handshake` runs this, and so does the
:class:`~pcapkit.foundation.reassembly.data.data.Deferred` that
:class:`~pcapkit.foundation.reassembly.dtls.DTLS` stores as a complete
message's ``packet``.

Args:
header: The message's ``(msg_type, length, message_seq)``.
body: The reassembled body.

Returns:
The message, unfragmented, its body raw if it does not decode.

"""
msg_type, length, message_seq = header
message = (bytes([int(msg_type)]) + length.to_bytes(3, 'big')
+ message_seq.to_bytes(2, 'big') + bytes(3) # fragment_offset 0
+ length.to_bytes(3, 'big') + body)
try:
return DTLS.read_handshake(message)
except ProtocolError:
return Data_DTLSHandshake(
msg_type=datagram.msg_type,
length=datagram.length,
msg_type=msg_type,
length=length,
body=body,
message_seq=datagram.message_seq,
message_seq=message_seq,
fragment_offset=0,
fragment_length=datagram.length,
fragment_length=length,
is_fragmented=False,
)
45 changes: 32 additions & 13 deletions pcapkit/foundation/reassembly/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@

from pcapkit.const.reg.apptype import TCP as AppType_TCP
from pcapkit.const.reg.transtype import TransType
from pcapkit.const.tls.handshake_type import HandshakeType
from pcapkit.foundation.reassembly.data.dtls import Datagram as DTLS_Datagram
from pcapkit.foundation.reassembly.data.ip import Datagram as IP_Datagram
from pcapkit.foundation.reassembly.data.tcp import Datagram as TCP_Datagram
from pcapkit.protocols.data.application.dtls import DTLSHandshake
from pcapkit.protocols.protocol import ProtocolBase

# registry key an analyser dispatches a reassembled payload on; see ``Deferred``
_KT = TypeVar('_KT', 'TransType', 'tuple[AppType_TCP, AppType_TCP]')
_KT = TypeVar('_KT', 'TransType', 'tuple[AppType_TCP, AppType_TCP]', 'tuple[HandshakeType, int, int]')


class Completion(EnumLookup, StrEnum):
Expand Down Expand Up @@ -125,7 +127,7 @@ class Deferred(Generic[_KT]):
the datagram just reassembled. Nothing about postponing it is specific to any
one reassembler, which is why this lives beside
:class:`~pcapkit.foundation.reassembly.data.data.ReassemblyData` rather than in
either protocol's data module.
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
Expand All @@ -139,18 +141,20 @@ class Deferred(Generic[_KT]):
TCP reassembly defers its ``packet`` as well
(:meth:`TCP.submit <pcapkit.foundation.reassembly.tcp.TCP.submit>`), at a far
smaller saving, being FIN/RST-driven rather than per-frame -- 222 submits per
:file:`http.pcap` pass against 1117.
:file:`http.pcap` pass 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 (:issue:`2531`).

Holding the call here defers it to the first read of
:attr:`Datagram.packet <pcapkit.foundation.reassembly.data.ip.Datagram.packet>`,
where :class:`~pcapkit.corekit.packet.DeferredPacket` runs 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 two reassemblers do *not* dispatch on the
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 ``proto`` and the parameter
its ``analyze`` expects are the same type rather than a union either call
its ``analyze`` expects are the same type rather than a union every call
site would have to widen to:

* :meth:`IP.submit <pcapkit.foundation.reassembly.ip.IP.submit>` builds a
Expand All @@ -165,8 +169,15 @@ class Deferred(Generic[_KT]):
<pcapkit.protocols.transport.transport.Transport.analyze>` overrides that
parameter to one: a reassembled transport-layer payload carries no
protocol number, so its application protocol is inferred from the ports.

Those are the two cases the type parameter is constrained to. Both are
* :meth:`DTLS._datagram_from_fragment
<pcapkit.foundation.reassembly.dtls.DTLS._datagram_from_fragment>` builds a
``Deferred[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 decoder
:func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` runs, which reads
the body by its :class:`~pcapkit.const.tls.handshake_type.HandshakeType`.

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
:class:`~pcapkit.const.reg.apptype.tcp.TCP` here -- the per-transport
Expand Down Expand Up @@ -198,32 +209,40 @@ class Deferred(Generic[_KT]):
either way.

Args:
analyze: The analyser to call, i.e.
analyze: The analyser to call:
:meth:`Protocol.analyze <pcapkit.protocols.protocol.Protocol.analyze>`
bound to the reassembly object's protocol.
bound to the reassembly object's protocol for IP and TCP, the
handshake decoder for DTLS.
proto: What the analyser dispatches the payload on -- the payload
protocol type for IP, the port pair for TCP, as above.
protocol type for IP, the port pair for TCP, the handshake header
for DTLS, as above.
payload: Reassembled payload to parse.

.. versionadded:: 1.5.0

.. versionchanged:: 1.6.0

Also constrained to the DTLS handshake header, and the analyser may
return a :class:`~pcapkit.protocols.data.application.dtls.DTLSHandshake`
(:issue:`2531`).

"""

__slots__ = ('analyze', 'proto', 'payload')

def __init__(self, analyze: 'Callable[[_KT, bytes], ProtocolBase]',
def __init__(self, analyze: 'Callable[[_KT, bytes], ProtocolBase | DTLSHandshake]',
proto: '_KT', payload: 'bytes') -> 'None':
# NOTE: the two type-parameterized attributes are annotated on their
# assignment, as ``_omd_bucket`` in :mod:`pcapkit.corekit.multidict`
# does. A slotted class declares no class-level attributes, so mypy
# infers each one from this assignment, and it cannot infer a *class*
# attribute's type from a type variable -- it asks for an annotation
# instead. ``payload`` needs none: :obj:`bytes` is inferred.
self.analyze: 'Callable[[_KT, bytes], ProtocolBase]' = analyze
self.analyze: 'Callable[[_KT, bytes], ProtocolBase | DTLSHandshake]' = analyze
self.proto: '_KT' = proto
self.payload = payload

def __call__(self) -> 'ProtocolBase':
def __call__(self) -> 'ProtocolBase | DTLSHandshake':
"""Run the postponed analysis.

Returns:
Expand Down
31 changes: 19 additions & 12 deletions pcapkit/foundation/reassembly/data/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import TYPE_CHECKING, Generic, TypeVar

from pcapkit.corekit.infoclass import Info, info_final
from pcapkit.corekit.packet import DeferredPacket
from pcapkit.foundation.reassembly.data.data import Deferred
from pcapkit.utilities.compat import Tuple

__all__ = [
Expand All @@ -18,6 +20,7 @@

from pcapkit.const.tls.handshake_type import HandshakeType as Enum_HandshakeType
from pcapkit.foundation.reassembly.data.data import Completion
from pcapkit.protocols.data.application.dtls import DTLSHandshake as Data_DTLSHandshake

_AT = TypeVar('_AT', 'IPv4Address', 'IPv6Address')

Expand Down Expand Up @@ -81,17 +84,16 @@ def __init__(self, src: '_AT', sport: 'int', dst: '_AT', dport: 'int', epoch: 'i


@info_final
class Datagram(Info, Generic[_AT]):
class Datagram(DeferredPacket, Info, Generic[_AT]):
"""Data model for :term:`DTLS <reasm.dtls.datagram>` reassembled
handshake message.
handshake message."""

:attr:`body` is reported eagerly, not through a
:class:`~pcapkit.foundation.reassembly.data.data.Deferred`: a message only
reaches :meth:`DTLS.submit <pcapkit.foundation.reassembly.dtls.DTLS.submit>`
because it was fragmented, so there is no common unfragmented case to
keep cheap.

"""
#: Listing ``packet`` here is what makes :attr:`packet` lazy -- see
#: :class:`~pcapkit.corekit.packet.DeferredPacket`.
__additional__ = ['packet']
#: Placeholder class :attr:`packet` may hold, for
#: :class:`~pcapkit.corekit.packet.DeferredPacket` to resolve.
__deferred__ = Deferred

#: How completely the message was reassembled, and why reassembly
#: stopped; see :class:`~pcapkit.foundation.reassembly.data.data.Completion`.
Expand All @@ -106,12 +108,17 @@ class Datagram(Info, Generic[_AT]):
msg_type: 'Enum_HandshakeType'
#: Total message length, as declared by its fragments.
length: 'int'
#: Reassembled message body, undecoded;
#: :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` decodes it.
#: Reassembled message body, undecoded.
body: 'bytes | tuple[bytes, ...]'
#: Decoded message, as :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake`
#: returns it: decoded on first read, not at construction time, and
#: :data:`None` for an incomplete message. A
#: :class:`~pcapkit.foundation.reassembly.data.data.Deferred` may be passed in
#: its place, and reading this attribute then runs it and keeps the result.
packet: 'Optional[Data_DTLSHandshake]'

if TYPE_CHECKING:
def __init__(self, completed: 'Completion', id: 'DatagramID[_AT]', index: 'tuple[int, ...]', message_seq: 'int', msg_type: 'Enum_HandshakeType', length: 'int', body: 'bytes | tuple[bytes, ...]') -> '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, ...]', message_seq: 'int', msg_type: 'Enum_HandshakeType', length: 'int', body: 'bytes | tuple[bytes, ...]', packet: 'Optional[Data_DTLSHandshake | Deferred[tuple[Enum_HandshakeType, int, int]]]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin


@info_final
Expand Down
33 changes: 23 additions & 10 deletions pcapkit/foundation/reassembly/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
"""
from typing import TYPE_CHECKING, Generic

from pcapkit.foundation.reassembly.data.data import Completion
from pcapkit.foundation.analysis.dtls import _read_handshake
from pcapkit.foundation.reassembly.data.data import Completion, Deferred
from pcapkit.foundation.reassembly.data.dtls import (_AT, Buffer, BufferID, Datagram, DatagramID,
Fragment, HoleDescriptor, Packet)
from pcapkit.foundation.reassembly.reassembly import ReassemblyBase
from pcapkit.protocols.application.dtls import DTLS as DTLS_Protocol

if TYPE_CHECKING:
from typing import Optional
from typing import Optional, Type

from pcapkit.const.tls.handshake_type import HandshakeType as Enum_HandshakeType

__all__ = ['DTLS']

Expand All @@ -47,7 +51,9 @@ class DTLS(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer], Generic
:class:`~pcapkit.foundation.extraction.Extractor` feeds it through
:func:`pcapkit.toolkit.pcap.dtls_reassembly`, which reads the 4-tuple
off the frame, and :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake`
decodes what it reassembles.
decodes what it reassembles; a complete message's
:attr:`~pcapkit.foundation.reassembly.data.dtls.Datagram.packet` holds
that decode, run on first read.

Example:
>>> from pcapkit.foundation.reassembly.dtls import DTLS
Expand All @@ -59,17 +65,17 @@ class DTLS(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer], Generic
>>> result = dtls_reassembly.datagram

"""
if TYPE_CHECKING:
protocol: 'Type[DTLS_Protocol]'

##########################################################################
# Defaults.
##########################################################################

#: Protocol name of current reassembly object. Not set via
#: ``__protocol_type__``, which would import
#: :class:`~pcapkit.protocols.application.dtls.DTLS` at class-definition
#: time; :class:`~pcapkit.foundation.reassembly.reassembly.ReassemblyMeta`
#: looks :attr:`name` up in the protocol registry instead.
#: Protocol name of current reassembly object.
__protocol_name__ = 'DTLS'
#: Protocol of current reassembly object.
__protocol_type__ = DTLS_Protocol

# No ``__timeout__``: RFC 6347 section 4.2.3 and RFC 9147 section 5.5 give
# DTLS handshake reassembly no timeout, so it inherits the base default,
Expand Down Expand Up @@ -236,11 +242,17 @@ def _datagram_from_fragment(self, frag: 'Fragment', bufid: 'BufferID', message_s
completion: how completely it was reassembled

Returns:
The reassembled (or partially reassembled) message.
The reassembled (or partially reassembled) message. Its ``packet``
is a deferred :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake`
decode if the message is complete, and :data:`None` otherwise, as
that function returns.

"""
packet = None # type: Optional[Deferred[tuple[Enum_HandshakeType, int, int]]]
if completion:
payload = bytes(frag.raw) # type: 'bytes | tuple[bytes, ...]'
body = bytes(frag.raw)
payload = body # type: 'bytes | tuple[bytes, ...]'
packet = Deferred(_read_handshake, (frag.msg_type, frag.length, message_seq), body)
elif self._flag_s:
payload = self._received_runs(frag)
else:
Expand All @@ -260,6 +272,7 @@ def _datagram_from_fragment(self, frag: 'Fragment', bufid: 'BufferID', message_s
msg_type=frag.msg_type,
length=frag.length,
body=payload,
packet=packet,
)

@staticmethod
Expand Down
Loading
Loading