Skip to content

Commit 81b3d9f

Browse files
committed
fix(reassembly): pin DTLS handshake buffers at declared message length
- Each message buffer is allocated at its declared length, the unit RFC 6347 section 4.2.3 buffers, bounded at 16 MiB by the 24-bit length field. The base does this; the class docstring documents it. - Overlapping fragments are accepted through the RFC 815 hole list. - Tests: a partial message holds its declared length, and with strict=False a timeout reports that length with the gaps zero-filled. Overlapping fragments reassemble byte-exact in forward, reverse and shuffled order.
1 parent 0fca5b4 commit 81b3d9f

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

‎pcapkit/foundation/reassembly/dtls.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ class DTLS(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer], Generic
5959
uses to pick the next layer -- never the addresses, which are a
6060
Frame-level concept. A caller that wants the automatic, per-record
6161
wiring :meth:`DTLS.read` offers therefore supplies the 5-tuple itself,
62-
via that method's ``flow`` argument; see its docstring.
62+
via that method's ``flow`` argument; see its docstring. Each message's
63+
buffer is allocated at its declared length, the unit
64+
:rfc:`6347#section-4.2.3` buffers, so at most 16 MiB (a 24-bit field);
65+
overlapping fragments are accepted.
6366
6467
Example:
6568
>>> from pcapkit.foundation.reassembly.dtls import DTLS

‎tests/foundation/reassembly/test_dtls_handshake_reassembly_2440_unit.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,64 @@ def test_idle_deadline_extends_on_each_fragment(self) -> None:
387387
self.assertTrue(engine.last.completed)
388388
self.assertEqual(engine.last.body, body)
389389

390+
def test_partial_message_buffer_is_sized_by_declared_length(self) -> None:
391+
"""A partial message buffers its declared length from the first
392+
fragment, not the highest offset received. With ``strict=False``, a
393+
timeout reports it at that length with the gaps zero-filled;
394+
``strict=True`` reports only the received runs."""
395+
from pcapkit.const.tls.handshake_type import HandshakeType
396+
from pcapkit.foundation.reassembly.data.data import Completion
397+
398+
size = 2 ** 24 - 1
399+
bufid = ('103.0.0.1', 1, '103.0.0.2', 5684, 0)
400+
engine = self._engine(strict=False)
401+
engine(self._packet(bufid, 1, HandshakeType.certificate, size, 0, 0, 100,
402+
b'A' * 100, 0.0))
403+
engine(self._packet(bufid, 2, HandshakeType.certificate, size, 0, 1000, 10,
404+
b'B' * 10, 0.0))
405+
frag = engine._buffer[bufid].msg[0] # pylint: disable=protected-access
406+
self.assertEqual(len(frag.raw), size)
407+
self.assertIsNone(engine.last)
408+
409+
(datagram,) = engine.submit(engine._buffer[bufid], # pylint: disable=protected-access
410+
bufid=bufid, timeout=True)
411+
self.assertEqual(datagram.completed, Completion.TIMEOUT)
412+
self.assertEqual(datagram.body,
413+
b'A' * 100 + bytes(900) + b'B' * 10 + bytes(size - 1010))
414+
415+
def test_overlapping_fragments_reassemble_byte_exact_in_any_order(self) -> None:
416+
"""Each fragment overlaps its predecessor by 4 KiB. The buffer is at the
417+
declared length from the first fragment on, and the message reassembles
418+
byte-exact whatever the arrival order."""
419+
from pcapkit.const.tls.handshake_type import HandshakeType
420+
421+
overlap = 4096
422+
for size in (64 * 1024 + 1, 2 ** 24 - 1):
423+
body = (bytes(range(256)) * (size // 256 + 1))[:size]
424+
chunk = size // 4 + 1
425+
frags = []
426+
for start in range(0, size, chunk):
427+
lo = max(0, start - overlap)
428+
hi = min(size, start + chunk)
429+
frags.append((lo, hi - lo))
430+
count = len(frags)
431+
orders = (list(range(count)), list(range(count))[::-1], [2, 0, 3, 1])
432+
for order in orders:
433+
with self.subTest(size=size, order=order):
434+
bufid = ('104.0.0.1', 1, '104.0.0.2', 5684, 0)
435+
engine = self._engine()
436+
for num, idx in enumerate(order, 1):
437+
fo, length = frags[idx]
438+
engine(self._packet(bufid, num, HandshakeType.certificate, size, 0,
439+
fo, length, body[fo:fo + length], 0.0))
440+
if num == 1:
441+
frag = engine._buffer[bufid].msg[0] # pylint: disable=protected-access
442+
self.assertEqual(len(frag.raw), size)
443+
self.assertIsNotNone(engine.last)
444+
self.assertTrue(engine.last.completed)
445+
self.assertEqual(engine.last.length, size)
446+
self.assertEqual(engine.last.body, body)
447+
390448

391449
class TestDTLSReadReassemblyWiring(unittest.TestCase):
392450
""":meth:`DTLS.read`'s ``reassembly``/``flow`` arguments, exercised

0 commit comments

Comments
 (0)