Skip to content

Commit b56cdd2

Browse files
committed
feat(foundation): reassemble fragmented DTLS handshake messages (#2440)
- Add `pcapkit.foundation.reassembly.dtls.DTLS`, a new `Reassembly`-shaped engine keyed on the UDP 5-tuple plus the DTLS epoch -- the record sequence number resets on an epoch change, so a new epoch opens a fresh buffer rather than folding into the previous one's. It ports the RFC 815 hole-descriptor algorithm `TCP._update_hole_descriptors` already implements, since every fragment declares the message's total length up front (RFC 6347 s4.2.3). A disagreeing fragment (different `length`/`msg_type`) under a reused `message_seq` restarts the message, reporting the displaced partial as its own PARTIAL result rather than dropping it. The default timeout (120s) is an idle deadline from each buffer's most recently received fragment, not pinned to its first -- one buffer holds several independent messages, and RFC 6347 s4.2.4.1's 60s is only a floor a retransmission backoff's cap must not go below, not a safe ceiling. - Wire it into `DTLS.read` via new, opt-in `reassembly`/`flow` arguments: a fragment that completes its message is decoded into a `ClientHello`/ `ServerHello` in place, rather than staying raw. `flow` is required unconditionally once `reassembly` is given, not only for a cleartext handshake record; an encrypted (epoch >= 1) `handshake` record is skipped; and a reassembled message that fails to decode, or whose own fragment header runs past the message's declared length, keeps its raw bytes instead of raising or corrupting the round trip. - Fix the construction path so a reassembled message still round-trips byte-exact: the completing record's fragment header keeps describing that record's own byte range, sliced back out of the full decoded body, rather than writing the full body under a mismatched `fragment_length`. - `DTLSHandshake.is_fragmented` keeps reporting the wire fact that a message was split, even once reassembled -- only `body` changes. - Add the `docs/source/pcapkit/foundation/reassembly/dtls.rst` page and its `reasm.dtls.*` glossary terms, list `DTLS_Reassembly` in `pcapkit.foundation.__all__`/`pcapkit.all.__all__`, fix a record's "at most one fragment" claim (s4.2.3 allows several), and fix three stale RFC citations (s4.2.2 -> s4.2.3 twice; an s4.1 citation overstating that fragment offsets, not just the sequence number, reset on an epoch change). Built against `feat/tls-dtls-2437` (#2441), since `DTLS` does not exist on `main` yet. `isort`, `pylint` (10.00/10 on touched files) and `mypy` (zero new errors vs. base) are clean; `tests/project/` (2102 passed) and `tests/foundation/reassembly/`+`tests/protocols/application/` (499 passed, with `examples/generators/make_samples.py` run first) pass in full.
1 parent 27a7872 commit b56cdd2

12 files changed

Lines changed: 1575 additions & 21 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
=================================
2+
DTLS Handshake-Message Reassembly
3+
=================================
4+
5+
.. module:: pcapkit.foundation.reassembly.dtls
6+
7+
:mod:`pcapkit.foundation.reassembly.dtls` contains
8+
:class:`~pcapkit.foundation.reassembly.dtls.DTLS` only, which reconstructs a
9+
DTLS handshake message split across several records back to origin
10+
(:rfc:`6347#section-4.2.3`).
11+
12+
.. autoclass:: pcapkit.foundation.reassembly.dtls.DTLS
13+
:no-members:
14+
:show-inheritance:
15+
16+
.. autoproperty:: last
17+
18+
.. automethod:: reassembly
19+
.. automethod:: submit
20+
.. automethod:: _datagram_from_fragment
21+
.. automethod:: _received_runs
22+
.. automethod:: _update_hole_descriptors
23+
24+
.. autoattribute:: __protocol_name__
25+
.. autoattribute:: __timeout__
26+
27+
Algorithm
28+
=========
29+
30+
.. seealso::
31+
32+
This algorithm is an adaptation of the hole-descriptor algorithm described
33+
in :rfc:`815`, over a DTLS handshake message's own, simpler coordinate
34+
system: a byte offset into the message that neither wraps (unlike a TCP
35+
sequence number) nor needs an open upper bound (unlike IP's or TCP's
36+
not-yet-known total length) -- :rfc:`6347#section-4.2.3`'s fragment header
37+
gives every fragment of a message the same, already-known ``length``.
38+
39+
A DTLS handshake record may carry more than one message fragment --
40+
:rfc:`6347#section-4.2.3` allows several handshake messages (or fragments of
41+
them) to be coalesced into one record, and :meth:`SSL._read_tls_handshake_messages
42+
<pcapkit.protocols.application.ssl.SSL._read_tls_handshake_messages>` already
43+
reads however many fit rather than assuming exactly one. A fragment names the
44+
message it belongs to (``message_seq``), the message's total length
45+
(``length``), and its own byte range within that message (``fragment_offset``,
46+
``fragment_length``). A message is reassembled by punching the arriving
47+
fragment's range out of the message's hole list; it is whole once no hole
48+
remains.
49+
50+
Terminology
51+
===========
52+
53+
.. glossary::
54+
55+
reasm.dtls.packet
56+
Data structure for **DTLS handshake-fragment reassembly**
57+
(:meth:`DTLS.reassembly <pcapkit.foundation.reassembly.dtls.DTLS.reassembly>`)
58+
is :class:`~pcapkit.foundation.reassembly.data.dtls.Packet` -- one
59+
fragment's own ``bufid``, ``message_seq``, ``fragment_offset``,
60+
``fragment_length`` and raw ``payload``, built by
61+
:meth:`DTLS.read <pcapkit.protocols.application.dtls.DTLS.read>` from
62+
a single :class:`~pcapkit.protocols.data.application.dtls.DTLSHandshake`.
63+
64+
reasm.dtls.datagram
65+
Data structure for **reassembled DTLS handshake message** (element of
66+
:attr:`DTLS.datagram <pcapkit.foundation.reassembly.reassembly.Reassembly.datagram>`)
67+
is :class:`~pcapkit.foundation.reassembly.data.dtls.Datagram` --
68+
``completed``, the original packet identifier, the packet numbers
69+
that built it, and ``body``, the reassembled payload (a contiguous
70+
:obj:`bytes`, or -- under ``strict=True`` and an incomplete message --
71+
a :obj:`tuple` of the octet runs actually received).
72+
73+
reasm.dtls.buffer
74+
Data structure for internal buffering
75+
(:attr:`DTLS._buffer <pcapkit.foundation.reassembly.reassembly.Reassembly._buffer>`)
76+
is :class:`~pcapkit.foundation.reassembly.data.dtls.Buffer` -- every
77+
message currently under reconstruction for one
78+
:data:`~pcapkit.foundation.reassembly.data.dtls.BufferID` (one UDP
79+
association at one DTLS epoch), each keyed by its
80+
:attr:`~pcapkit.foundation.reassembly.data.dtls.Packet.message_seq` and
81+
held as a :class:`~pcapkit.foundation.reassembly.data.dtls.Fragment` --
82+
its hole descriptor list
83+
(:class:`~pcapkit.foundation.reassembly.data.dtls.HoleDescriptor`) and
84+
its zero-filled payload buffer.
85+
86+
Data Models
87+
===========
88+
89+
.. module:: pcapkit.foundation.reassembly.data.dtls
90+
.. currentmodule:: pcapkit.foundation.reassembly.dtls
91+
92+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.Packet
93+
:members:
94+
:show-inheritance:
95+
96+
.. autodata:: pcapkit.foundation.reassembly.data.dtls.BufferID
97+
98+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.DatagramID
99+
:members:
100+
:show-inheritance:
101+
102+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.Datagram
103+
:members:
104+
:show-inheritance:
105+
106+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.HoleDescriptor
107+
:members:
108+
:show-inheritance:
109+
110+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.Fragment
111+
:members:
112+
:show-inheritance:
113+
114+
.. autoclass:: pcapkit.foundation.reassembly.data.dtls.Buffer
115+
:members:
116+
:show-inheritance:
117+
118+
Type Variables
119+
==============
120+
121+
.. data:: _AT
122+
:module: pcapkit.foundation.reassembly.data.dtls
123+
:type: ipaddress.IPv4Address | ipaddress.IPv6Address
124+
125+
IP address type.

‎docs/source/pcapkit/foundation/reassembly/index.rst‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and :rfc:`815`.
1919
reassembly
2020
ip/index
2121
tcp
22+
dtls
2223

2324
All reassembly classes are implemented as :class:`~pcapkit.foundation.reassembly.reassembly.ReassemblyBase`
2425
subclasses, which are responsible for processing extracted packets and
@@ -30,7 +31,7 @@ diagram of the class hierarchy of :mod:`pcapkit.foundation.reassembly`:
3031
flowchart LR
3132
A{{ReassemblyMeta}} -.->|metaclass| B(ReassemblyBase)
3233

33-
B --> IP & TCP
34+
B --> IP & TCP & DTLS
3435
IP --> IPv4 & IPv6
3536

3637
B --> C(Reassembly)
@@ -45,6 +46,7 @@ diagram of the class hierarchy of :mod:`pcapkit.foundation.reassembly`:
4546
click IPv4 "ip/ipv4.html#pcapkit.foundation.reassembly.ipv4.IPv4"
4647
click IPv6 "ip/ipv6.html#pcapkit.foundation.reassembly.ipv6.IPv6"
4748
click TCP "tcp.html#pcapkit.foundation.reassembly.tcp.TCP"
49+
click DTLS "dtls.html#pcapkit.foundation.reassembly.dtls.DTLS"
4850

4951
Auxiliary Data
5052
==============

‎pcapkit/all.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
'ReassemblyManager', # Reassembly Manager
8383
'IPv4_Reassembly', 'IPv6_Reassembly', # IP Reassembly
8484
'TCP_Reassembly', # TCP Reassembly
85+
'DTLS_Reassembly', # DTLS Reassembly
8586

8687
# pcapkit.foundation.registry
8788
'register_protocol',

‎pcapkit/foundation/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
__all__ = [
2121
'Extractor',
2222

23-
'IPv4_Reassembly', 'IPv6_Reassembly', 'TCP_Reassembly',
23+
'IPv4_Reassembly', 'IPv6_Reassembly', 'TCP_Reassembly', 'DTLS_Reassembly',
2424
'ReassemblyManager',
2525

2626
'TCP_TraceFlow',

‎pcapkit/foundation/reassembly/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
# Reassembly for TCP
2222
from pcapkit.foundation.reassembly.tcp import TCP as TCP_Reassembly
2323

24+
# Reassembly for DTLS handshake messages
25+
from pcapkit.foundation.reassembly.dtls import DTLS as DTLS_Reassembly
26+
2427
__all__ = [
2528
'IPv4_Reassembly', 'IPv6_Reassembly', # IP Reassembly
2629
'TCP_Reassembly', # TCP Reassembly
30+
'DTLS_Reassembly', # DTLS Reassembly
2731
'ReassemblyManager', # Reassembly Manager, defined below
2832
]
2933

‎pcapkit/foundation/reassembly/data/__init__.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
from pcapkit.foundation.reassembly.data.tcp import HoleDescriptor as TCP_HoleDescriptor
2121
from pcapkit.foundation.reassembly.data.tcp import Packet as TCP_Packet
2222

23+
# DTLS handshake-message reassembly
24+
from pcapkit.foundation.reassembly.data.dtls import Buffer as DTLS_Buffer
25+
from pcapkit.foundation.reassembly.data.dtls import BufferID as DTLS_BufferID
26+
from pcapkit.foundation.reassembly.data.dtls import Datagram as DTLS_Datagram
27+
from pcapkit.foundation.reassembly.data.dtls import DatagramID as DTLS_DatagramID
28+
from pcapkit.foundation.reassembly.data.dtls import Fragment as DTLS_Fragment
29+
from pcapkit.foundation.reassembly.data.dtls import HoleDescriptor as DTLS_HoleDescriptor
30+
from pcapkit.foundation.reassembly.data.dtls import Packet as DTLS_Packet
31+
2332
__all__ = [
2433
'ReassemblyData', 'Completion', 'Deferred',
2534

@@ -28,4 +37,7 @@
2837

2938
'TCP_Packet', 'TCP_DatagramID', 'TCP_Datagram', 'TCP_Buffer',
3039
'TCP_Fragment', 'TCP_HoleDescriptor', 'TCP_BufferID',
40+
41+
'DTLS_Packet', 'DTLS_DatagramID', 'DTLS_Datagram', 'DTLS_Buffer',
42+
'DTLS_Fragment', 'DTLS_HoleDescriptor', 'DTLS_BufferID',
3143
]

0 commit comments

Comments
 (0)