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
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ flow :class:`~pcapkit.utilities.exceptions.BaseError`'s quiet mode exists for, a
installing a hook from an error the library caught itself is a process-wide side
effect.

A parse whose refusal a fallback absorbs without
:func:`~pcapkit.corekit.decorators.beholder` follows the same rule through an explicit
``CaughtByCaller(ProtocolError)`` around it, as
:meth:`DTLS.analyze_handshake <pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`
keeps a body that does not decode raw -- an application of :issue:`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
:exc:`~pcapkit.utilities.exceptions.ProtocolError` built while that call is on the
stack is quiet, at any depth, unless an inner
Expand Down
52 changes: 39 additions & 13 deletions docs/source/contributing/conventions/reassembly-integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ zero-fills to merge a segment. It bounds the memory a capture can have zero-fill
:data:`~pcapkit.utilities.config.MAX_ZERO_PAD_LENGTH` does for a field, and ``-1``
lifts it to give the uncapped reassembly point 4 asks for. Ruled on :issue:`2546`.

Parse Ownership
~~~~~~~~~~~~~~~

**The protocol class owns the parse of a reassembled outcome**, and
:mod:`pcapkit.foundation.analysis` is a public layer over it. Ruled on :issue:`2622`:

* The parse is a method of the protocol class:
:meth:`Protocol.analyze <pcapkit.protocols.protocol.Protocol.analyze>` for IPv4 and
IPv6, :meth:`Transport.analyze <pcapkit.protocols.transport.transport.Transport.analyze>`
for TCP, and
:meth:`DTLS.analyze_handshake <pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`
for a DTLS handshake message.
* Each :mod:`pcapkit.foundation.analysis` function is a public wrapper that calls that
method and holds no parsing of its own.
* The reassembly :class:`~pcapkit.foundation.reassembly.data.data.Deferred` calls the
same method, on the reassembly's own ``__protocol_type__``.
* Protocol code never imports :mod:`pcapkit.foundation` to do its own work: the
dependency runs from ``foundation`` to the protocols. What the protocol base does
import from :mod:`pcapkit.foundation.registry.protocols` registers a class rather
than parsing. :file:`tests/project/test_protocols_import_boundary_2622_unit.py` fails
on any other import of :mod:`pcapkit.foundation` under :file:`pcapkit/protocols/`
made outside a ``TYPE_CHECKING`` block, the rest of
:mod:`pcapkit.foundation.registry` included.

Compliance, Measured
~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -52,7 +76,8 @@ function on an already-parsed frame, feeding the result to the reassembly instan
:class:`~pcapkit.foundation.extraction.Extractor` holds; for DTLS that function is
:func:`pcapkit.toolkit.pcap.dtls_reassembly` (:issue:`2496`).
:meth:`DTLS.read_handshake <pcapkit.protocols.application.dtls.DTLS.read_handshake>`
is the parser method the rule allows.
and :meth:`DTLS.analyze_handshake <pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`
are the parser methods the rule allows.

**No engine meets point 2.** No single :mod:`pcapkit.toolkit` helper enumerates which
output engines support a given reassembly engine. Support is established ad hoc, per
Expand All @@ -69,21 +94,22 @@ datagram's payload by its payload protocol type, through
datagram's by its ports, through
:meth:`Transport.analyze <pcapkit.protocols.transport.transport.Transport.analyze>`;
and :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` decodes a reassembled
DTLS handshake message (:issue:`2496`). The owner chose on :issue:`2581` to give IP
and TCP modules of their own, each function named for the datagram its engine
reassembles, rather than leave their outcomes to those dispatchers with no analysis
method, which kept the rule true for DTLS alone.
DTLS handshake message (:issue:`2496`), through
:meth:`DTLS.analyze_handshake <pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`
(:issue:`2622`). The owner chose on :issue:`2581` to give IP and TCP modules of their
own, each function named for the datagram its engine reassembles, rather than leave
their outcomes to those dispatchers with no analysis method, which kept the rule true
for DTLS alone.

**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`. Each ``Deferred`` runs the
parse behind its engine's analysis function, so a ``packet`` is what that function
returns. IP and TCP bind that parse to ``__protocol_type__`` and call its ``analyze``,
so a subclass naming another protocol has its ``packet`` parsed by that one
(:issue:`2581`); DTLS decodes with
:meth:`DTLS.read_handshake <pcapkit.protocols.application.dtls.DTLS.read_handshake>`
whatever ``__protocol_type__`` names. The engines differ in what an incomplete outcome
holds. IP and TCP parse a contiguous payload whether or not it is complete, as
``strict=False`` reports one, and hold :data:`None` only for the received runs
``strict=True`` reports; DTLS holds :data:`None` for any incomplete message.
returns. Each binds that parse to ``__protocol_type__``, calling its ``analyze`` for
IP and TCP (:issue:`2581`) and its ``analyze_handshake`` for DTLS (:issue:`2622`), so a
subclass naming another protocol has its ``packet`` parsed by that one. The engines
differ in what an incomplete outcome holds. IP and TCP parse a contiguous payload
whether or not it is complete, as ``strict=False`` reports one, and hold :data:`None`
only for the received runs ``strict=True`` reports; DTLS holds :data:`None` for any
incomplete message.
4 changes: 3 additions & 1 deletion docs/source/pcapkit/foundation/analysis/dtls.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ DTLS Handshake-Message Analysis
:mod:`pcapkit.foundation.analysis.dtls` contains
:func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` only, which decodes a
DTLS handshake message that :class:`~pcapkit.foundation.reassembly.dtls.DTLS`
reassembled (:rfc:`6347#section-4.2.3`).
reassembled (:rfc:`6347#section-4.2.3`). The decode itself is
:meth:`DTLS.analyze_handshake
<pcapkit.protocols.application.dtls.DTLS.analyze_handshake>` (:issue:`2622`).

.. autofunction:: pcapkit.foundation.analysis.dtls.dtls_handshake
3 changes: 2 additions & 1 deletion docs/source/pcapkit/foundation/analysis/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Reassembled Payload Analysis
:mod:`pcapkit.foundation.analysis` parses what a
:mod:`pcapkit.foundation.reassembly` engine reassembled into a usable result,
so that a caller does not interpret the raw reassembled octets itself
(:ref:`reassembly-integration`).
(:ref:`reassembly-integration`). Each function is a public wrapper: the parse
itself is a method of the protocol class, which the function calls.

.. versionadded:: 1.6.0

Expand Down
4 changes: 3 additions & 1 deletion docs/source/pcapkit/protocols/application/dtls.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ Known Limitations
and its :attr:`~pcapkit.protocols.data.application.dtls.DTLSHandshake.body`
kept as raw octets. ``Extractor(reassembly=True, dtls=True)`` reassembles the
message, and :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` decodes it
with :meth:`~pcapkit.protocols.application.dtls.DTLS.read_handshake`.
with :meth:`~pcapkit.protocols.application.dtls.DTLS.analyze_handshake`, which
reads it with :meth:`~pcapkit.protocols.application.dtls.DTLS.read_handshake`.

**A unified-header record with a connection ID is not decoded**
(:rfc:`9147#section-4.1`, :issue:`2467`). Its length is negotiated, not carried
Expand Down Expand Up @@ -161,6 +162,7 @@ ones on the wire.
.. automethod:: make
.. automethod:: unpack
.. automethod:: read_handshake
.. automethod:: analyze_handshake

.. automethod:: __length_hint__

Expand Down
58 changes: 13 additions & 45 deletions pcapkit/foundation/analysis/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@
:mod:`pcapkit.foundation.analysis.dtls` contains
:func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` only, which decodes a
DTLS handshake message that :class:`~pcapkit.foundation.reassembly.dtls.DTLS`
reassembled (:rfc:`6347#section-4.2.3`).
reassembled (:rfc:`6347#section-4.2.3`). The decode itself is
:meth:`DTLS.analyze_handshake
<pcapkit.protocols.application.dtls.DTLS.analyze_handshake>` (:issue:`2622`).

.. versionadded:: 1.6.0

"""
from typing import TYPE_CHECKING, cast

from pcapkit.protocols.application.dtls import DTLS
from pcapkit.protocols.data.application.dtls import DTLSHandshake as Data_DTLSHandshake
from pcapkit.utilities.exceptions import ProtocolError

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
from pcapkit.protocols.data.application.dtls import DTLSHandshake as Data_DTLSHandshake

__all__ = ['dtls_handshake']

Expand All @@ -31,10 +31,10 @@ def dtls_handshake(datagram: 'DTLS_Datagram') -> 'Optional[Data_DTLSHandshake]':
"""Parse a DTLS handshake message reassembled by
:class:`~pcapkit.foundation.reassembly.dtls.DTLS`.

The message is read as if it had arrived unfragmented, ``fragment_offset``
0 and ``fragment_length`` its ``length`` (:rfc:`6347#section-4.2.3`), by
:meth:`DTLS.read_handshake
<pcapkit.protocols.application.dtls.DTLS.read_handshake>`. So a
The message is decoded by :meth:`DTLS.analyze_handshake
<pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`, which reads
it as if it had arrived unfragmented, ``fragment_offset`` 0 and
``fragment_length`` its ``length`` (:rfc:`6347#section-4.2.3`). So a
``ClientHello``, ``ServerHello`` or ``EncryptedExtensions`` body decodes
as an unfragmented one does, whatever its length, and any other body stays
raw octets.
Expand All @@ -49,45 +49,13 @@ def dtls_handshake(datagram: 'DTLS_Datagram') -> 'Optional[Data_DTLSHandshake]':

Note:
:attr:`Datagram.packet <pcapkit.foundation.reassembly.data.dtls.Datagram.packet>`
holds the same result, decoded on its first read.
holds the same result, decoded on its first read, unless the
reassembly's ``__protocol_type__`` names another protocol, whose
``analyze_handshake`` then decodes it instead.

"""
if not datagram.completed:
return None
# NOTE: a complete message is always one contiguous run.
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=msg_type,
length=length,
body=body,
message_seq=message_seq,
fragment_offset=0,
fragment_length=length,
is_fragmented=False,
)
return DTLS.analyze_handshake((datagram.msg_type, datagram.length, datagram.message_seq),
cast('bytes', datagram.body))
6 changes: 4 additions & 2 deletions pcapkit/foundation/reassembly/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ class Deferred(Generic[_KT]):
``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
:func:`~pcapkit.foundation.analysis.dtls.dtls_handshake` runs,
:meth:`DTLS.analyze_handshake
<pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`, 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
Expand Down Expand Up @@ -215,7 +217,7 @@ class Deferred(Generic[_KT]):
analyze: The analyser to call: the parse
:mod:`pcapkit.foundation.analysis` runs for the engine, bound to
the reassembly object's protocol for IP and TCP, whose ``analyze``
it calls, and the handshake decoder for DTLS.
it calls, and that protocol's ``analyze_handshake`` for DTLS.
proto: 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.
Expand Down
10 changes: 6 additions & 4 deletions pcapkit/foundation/reassembly/data/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,12 @@ class Datagram(DeferredPacket, Info, Generic[_AT]):
#: 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.
#: returns it unless the reassembly's ``__protocol_type__`` names another
#: protocol, whose ``analyze_handshake`` then decodes 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:
Expand Down
25 changes: 17 additions & 8 deletions pcapkit/foundation/reassembly/dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"""
from typing import TYPE_CHECKING, Generic

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)
Expand Down Expand Up @@ -51,9 +50,11 @@ 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; a complete message's
decodes what it reassembles. A complete message's
:attr:`~pcapkit.foundation.reassembly.data.dtls.Datagram.packet` holds
that decode, run on first read.
the same decode, run on first read. Both call the protocol's
:meth:`~pcapkit.protocols.application.dtls.DTLS.analyze_handshake`; the
``packet`` calls that of :attr:`__protocol_type__` (:issue:`2622`).

Example:
>>> from pcapkit.foundation.reassembly.dtls import DTLS
Expand All @@ -74,7 +75,12 @@ class DTLS(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer], Generic

#: Protocol name of current reassembly object.
__protocol_name__ = 'DTLS'
#: Protocol of current reassembly object.
#: Protocol of current reassembly object. An extension point: a protocol
#: named here need only define ``analyze_handshake``, with the signature of
#: :meth:`DTLS.analyze_handshake
#: <pcapkit.protocols.application.dtls.DTLS.analyze_handshake>`, and is read
#: for it when a message completes, so one without it raises
#: :exc:`AttributeError` there.
__protocol_type__ = DTLS_Protocol

# No ``__timeout__``: RFC 6347 section 4.2.3 and RFC 9147 section 5.5 give
Expand Down Expand Up @@ -243,16 +249,19 @@ def _datagram_from_fragment(self, frag: 'Fragment', bufid: 'BufferID', message_s

Returns:
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.
is the decode :func:`~pcapkit.foundation.analysis.dtls.dtls_handshake`
runs, by this reassembly's own
:attr:`protocol <pcapkit.foundation.reassembly.reassembly.Reassembly.protocol>`,
deferred to its first read, 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:
body = bytes(frag.raw)
payload = body # type: 'bytes | tuple[bytes, ...]'
packet = Deferred(_read_handshake, (frag.msg_type, frag.length, message_seq), body)
packet = Deferred(self.protocol.analyze_handshake,
(frag.msg_type, frag.length, message_seq), body)
elif self._flag_s:
payload = self._received_runs(frag)
else:
Expand Down
Loading
Loading