DTLS Handshake-Message Reassembly

pcapkit.foundation.reassembly.dtls contains DTLS only, which reconstructs a DTLS handshake message split across several records back to origin (RFC 6347 Section 4.2.3).

class pcapkit.foundation.reassembly.dtls.DTLS[_PT: Info, _DT: Info, _IT: tuple, _BT: Info](*, strict=True, store=True)[source]

Bases: ReassemblyBase[Packet[_AT], Datagram[_AT], tuple[_AT, int, _AT, int, int], Buffer], Generic[_AT]

Reassembly for DTLS handshake messages.

Parameters:
  • strict (bool) – if True, report a message that is not completely reassembled as the tuple of its received runs; otherwise as one contiguous payload, its holes zero-filled

  • store (bool) – if store reassembled datagram in memory, i.e., self._dtgram (if not, datagram will be discarded after callback)

Note

The buffer ID is the UDP 4-tuple plus the DTLS epoch: the record sequence number resets on each cipher-state change (RFC 6347 Section 4.1), so a new epoch opens a fresh buffer. Overlapping fragments are accepted (RFC 6347 Section 4.2.3, RFC 9147 Section 5.5). The Extractor feeds it through pcapkit.toolkit.pcap.dtls_reassembly(), which reads the 4-tuple off the frame, and dtls_handshake() decodes what it reassembles. A complete message’s packet holds the same decode, run on first read. Both call the protocol’s analyze_handshake(); the packet calls that of __protocol_type__ (#2622).

Example

>>> from pcapkit.foundation.reassembly.dtls import DTLS
# Initialise instance:
>>> dtls_reassembly = DTLS()
# Call reassembly:
>>> dtls_reassembly(packet_dict)
# Fetch result:
>>> result = dtls_reassembly.datagram
property last: Datagram[_AT] | None

The message reassembled by the most recent call to reassembly(), or None if that fragment did not complete one.

This is a synchronous alternative to datagram/fetch() for a caller that wants to know, immediately after feeding one fragment, whether that fragment was the one that completed its message, rather than waiting for the buffer (which may hold several other messages still in flight under the same buffer ID) to be flushed.

reassembly(info)[source]

Reassembly procedure.

Parameters:

info (Packet[_AT]) – info record of packets to be reassembled

submit(buf, *, bufid, checked=False, timeout=False)[source]

Submit reassembled payload.

Parameters:
  • buf (Buffer) – buffer dict of reassembled packets

  • bufid (tuple[_AT, int, _AT, int, int]) – buffer identifier

  • checked (bool) – unused; every message’s completeness is decided from its own hole list, so there is nothing for a caller to assert ahead of time

  • timeout (bool) – whether this buffer is being submitted because expire() abandoned it under the reassembly timeout

Return type:

list[Datagram[_AT]]

Returns:

Every message still held in buf, each reported COMPLETE if its hole list is empty, otherwise TIMEOUT or PARTIAL.

_datagram_from_fragment(frag, bufid, message_seq, completion)[source]

Build a Datagram from a message’s buffered Fragment.

Parameters:
  • frag (Fragment) – message-under-construction to report

  • bufid (tuple[_AT, int, _AT, int, int]) – buffer identifier it was held under

  • message_seq (int) – its message sequence number

  • completion (Completion) – how completely it was reassembled

Return type:

Datagram[_AT]

Returns:

The reassembled (or partially reassembled) message. Its packet is the decode dtls_handshake() runs, by this reassembly’s own protocol, deferred to its first read, if the message is complete, and None otherwise, as that function returns.

static _received_runs(frag)[source]

Extract the octet runs actually received, for strict=True’s report of an incomplete message.

Parameters:

frag (Fragment) – message-under-construction to extract runs from

Return type:

tuple[bytes, ...]

Returns:

Received octet runs, in offset order; empty runs (a hole butting straight up against the start, the end, or another hole) are left out.

static _update_hole_descriptors(hdl, first, last)[source]

Punch a received byte range out of a message’s hole list.

Offsets are message-relative and inclusive. They never wrap and need no open upper bound, since every fragment of a message declares the same total length (RFC 6347 Section 4.2.3).

Parameters:
  • hdl (list[HoleDescriptor]) – the message’s hole descriptor list, mutated in place

  • first (int) – offset of the first octet the arriving fragment actually held

  • last (int) – offset of the last octet the arriving fragment actually held, inclusive

__protocol_name__: str = 'DTLS'

Protocol name of current reassembly object.

__protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.application.dtls.DTLS'>

Protocol of current reassembly object. An extension point – a protocol named here need only define analyze_handshake, with the signature of DTLS.analyze_handshake, and is read for it when a message completes, so one without it raises AttributeError there.

Algorithm

See also

Holes are inclusive byte offsets into the message. They never wrap, and the upper bound is known from the start: every fragment carries the message’s total length (RFC 6347 Section 4.2.3).

A DTLS handshake record may carry more than one message fragment – RFC 6347 Section 4.2.3 allows several handshake messages (or fragments of them) to be coalesced into one record, and SSL._read_tls_handshake_messages reads however many fit rather than assuming exactly one. A fragment names the message it belongs to (message_seq), the message’s total length (length), and its own byte range within that message (fragment_offset, fragment_length). A message is reassembled by punching the arriving fragment’s range out of the message’s hole list; it is whole once no hole remains.

Terminology

reasm.dtls.packet

Data structure for DTLS handshake-fragment reassembly (DTLS.reassembly) is Packet – one fragment’s own bufid, message_seq, fragment_offset, fragment_length and raw payload, built by pcapkit.toolkit.pcap.dtls_reassembly() or pcapkit.toolkit.pcapng.dtls_reassembly() from a single DTLSHandshake.

reasm.dtls.datagram

Data structure for reassembled DTLS handshake message (element of DTLS.datagram) is Datagram – completed, the original packet identifier, the packet numbers that built it, body, the reassembled payload (a contiguous bytes, or – under strict=True and an incomplete message – a tuple of the octet runs actually received), and packet, what dtls_handshake() decodes body to. packet is decoded on first read, as an IP or TCP datagram’s is, and is None for an incomplete message.

reasm.dtls.buffer

Data structure for internal buffering (DTLS._buffer) is Buffer – every message under reconstruction for one BufferID (one UDP association at one DTLS epoch), each keyed by its message_seq and held as a Fragment – its hole descriptor list (HoleDescriptor) and its zero-filled payload buffer.

Data Models

class pcapkit.foundation.reassembly.data.dtls.Packet[_AT: (IPv4Address, IPv6Address)](bufid, num, msg_type, length, message_seq, fragment_offset, fragment_length, payload, timestamp)[source]

Bases: Info, Generic[_AT]

Data model for DTLS handshake-fragment representation.

bufid: tuple[_AT, int, _AT, int, int]

Buffer ID.

num: int

Original packet range number.

msg_type: HandshakeType

Handshake message type.

length: int

Total message length (RFC 6347 Section 4.2.2’s length), fragment headers and bodies excluded – the same on every fragment of one message.

message_seq: int

Message sequence number, the datagram key within bufid.

fragment_offset: int

Fragment offset into the reassembled message.

fragment_length: int

Length of this fragment.

payload: bytes

Raw fragment payload.

timestamp: float

Capture timestamp of the fragment, in seconds since the Unix epoch, i.e. the capture’s clock. Recorded only; it does not expire anything.

pcapkit.foundation.reassembly.data.dtls.BufferID

Buffer ID – the UDP 4-tuple (addresses and ports; the protocol is always UDP, so it is left out), plus the DTLS epoch: the record sequence number resets on every change of cipher state (RFC 6347 Section 4.1), so a fragment from a new epoch belongs to a fresh buffer rather than the one its 4-tuple would otherwise name.

alias of tuple[_AT, int, _AT, int, int]

class pcapkit.foundation.reassembly.data.dtls.DatagramID[_AT: (IPv4Address, IPv6Address)](src, sport, dst, dport, epoch)[source]

Bases: Info, Generic[_AT]

Data model for DTLS original packet identifier.

src: _AT

Source address.

sport: int

Source port.

dst: _AT

Destination address.

dport: int

Destination port.

epoch: int

Epoch the message was reassembled under.

class pcapkit.foundation.reassembly.data.dtls.Datagram[_AT: (IPv4Address, IPv6Address)](completed, id, index, message_seq, msg_type, length, body, packet)[source]

Bases: DeferredPacket, Info, Generic[_AT]

Data model for DTLS reassembled handshake message.

completed: Completion

How completely the message was reassembled, and why reassembly stopped; see Completion.

id: DatagramID[_AT]

Original packet identifier.

index: tuple[int, ...]

Packet numbers of the fragments that built this message.

message_seq: int

Message sequence number.

msg_type: HandshakeType

Handshake message type.

length: int

Total message length, as declared by its fragments.

body: bytes | tuple[bytes, ...]

Reassembled message body, undecoded.

packet: DTLSHandshake | None

Decoded message, as dtls_handshake() 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 None for an incomplete message. A Deferred may be passed in its place, and reading this attribute then runs it and keeps the result.

class pcapkit.foundation.reassembly.data.dtls.HoleDescriptor(first, last)[source]

Bases: Info

Data model for DTLS hole descriptor.

Both bounds are inclusive byte offsets into the reassembled message, so a hole covers last - first + 1 octets. They never wrap and need no open-ended sentinel: every fragment declares the message’s total Fragment.length (RFC 6347 Section 4.2.3), so it is known from the first fragment.

first: int

Offset of the first missing octet.

last: int

Offset of the last missing octet, inclusive.

class pcapkit.foundation.reassembly.data.dtls.Fragment(ind, msg_type, length, raw, hdl)[source]

Bases: Info

Data model for DTLS message-under- construction, one per Packet.message_seq buffered under one BufferID.

ind: list[int]

Numbers of the packets merged into this fragment.

msg_type: HandshakeType

Handshake message type – read off the first-arriving fragment and trusted from there, same as length.

length: int

Total message length, read off the first-arriving fragment.

raw: bytearray

Reassembled payload, holes left zero-filled.

hdl: list[HoleDescriptor]

Hole descriptor list; empty once the message is whole.

class pcapkit.foundation.reassembly.data.dtls.Buffer(msg, timestamp)[source]

Bases: Info

Data model for DTLS buffer entry – every handshake message being reassembled under one BufferID, i.e. one UDP association at one epoch.

msg: dict[int, Fragment]

Messages under reconstruction, by Packet.message_seq. Several may be in flight at once: a Certificate, a ServerKeyExchange and a ServerHelloDone can each fragment independently in the same flight.

timestamp: float

Capture timestamp of the first fragment buffered under this buffer ID, in seconds since the Unix epoch. Recorded only: the buffer has no time-based deadline, so it is never expired by time.

Type Variables

pcapkit.foundation.reassembly.data.dtls._AT: ipaddress.IPv4Address | ipaddress.IPv6Address

IP address type.