IPv4 Datagram Reassembly

pcapkit.foundation.reassembly.ipv4 contains IPv4 only, which reconstructs fragmented IPv4 packets back to origin. Please refer to Base Class for more information.

class pcapkit.foundation.reassembly.ipv4.IPv4(*, strict=True, store=True, timeout=None)[source]

Bases: IP

Reassembly for IPv4 payload.

Parameters:
  • strict (bool) – if True, report a datagram that is not completely reassembled as the tuple of its received runs; otherwise as one contiguous payload, its holes zero-filled – or, while the total length is unknown, only the prefix up to the first hole

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

  • timeout (float | None) – reassembly timeout in seconds, on the capture’s own clock; None selects __timeout__

Example

>>> from pcapkit.foundation.reassembly import IPv4
# Initialise instance:
>>> ipv4_reassembly = IPv4()
# Call reassembly:
>>> ipv4_reassembly(packet_dict)
# Fetch result:
>>> result = ipv4_reassembly.datagram
__protocol_name__: str = 'IPv4'

Protocol name of current reassembly object.

__protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.internet.ipv4.IPv4'>

Protocol of current reassembly object.

__callback_fn__: list[Callable[[list[_DT]], None]]

List of callback functions upon reassembled datagram.

Terminology

reasm.ipv4.packet

Data structure for IPv4 datagram reassembly (IPv4.reassembly) is as following, with ipv4 the protocol instance (frame['IPv4']) and ipv4_info its info – the header fields come off the latter, the raw octets off the former:

packet_dict = dict(
  bufid = (
      ipv4_info.src,              # source IP address
      ipv4_info.dst,              # destination IP address
      ipv4_info.id,               # identification
      ipv4_info.protocol,         # payload protocol type
  ),
  num = frame.info.number,        # original packet range number
  fo = ipv4_info.offset,          # fragment offset, in octets
  ihl = ipv4_info.hdr_len,        # internet header length
  mf = ipv4_info.flags.mf,        # more fragment flag
  tl = ipv4_info.len,             # total length, header includes
  header = ipv4.packet.header,    # raw bytes type header
  payload = bytearray(
      ipv4.packet.payload),       # raw bytearray type payload
  timestamp = float(
      frame.info.time_epoch),     # capture timestamp
)
reasm.ipv4.datagram

Data structure for reassembled IPv4 datagram (element from IPv4.datagram tuple) is as following:

(tuple) datagram
 |--> (Info) data
 |     |--> 'completed' : (Completion) COMPLETE --> reassembled in whole
 |     |--> 'id' : (Info) original packet identifier
 |     |            |--> 'src' --> (IPv4Address) ipv4.src
 |     |            |--> 'dst' --> (IPv4Address) ipv4.dst
 |     |            |--> 'id' --> (int) ipv4.id
 |     |            |--> 'proto' --> (TransType) ipv4.protocol
 |     |--> 'index' : (tuple) packet numbers
 |     |               |--> (int) original packet range number
 |     |--> 'header' : (bytes) IPv4 header
 |     |--> 'payload' : (bytes) reassembled IPv4 payload
 |     |--> 'packet' : (Protocol) parsed reassembled payload
 |     |--> 'conflict' : (tuple) octet ranges on which two fragments disagreed
 |     |                  |--> (tuple) (first, last), absolute and inclusive
 |     |                  |--> ...
 |--> (Info) data
 |     |--> 'completed' : (Completion) PARTIAL or TIMEOUT --> incomplete
 |     |--> 'id' : (Info) original packet identifier
 |     |            |--> 'src' --> (IPv4Address) ipv4.src
 |     |            |--> 'dst' --> (IPv4Address) ipv4.dst
 |     |            |--> 'id' --> (int) ipv4.id
 |     |            |--> 'proto' --> (TransType) ipv4.protocol
 |     |--> 'index' : (tuple) packet numbers
 |     |               |--> (int) original packet range number
 |     |--> 'header' : (bytes) IPv4 header
 |     |--> 'payload' : (tuple) partially reassembled IPv4 payload
 |     |                 |--> (bytes) IPv4 payload fragment
 |     |                 |--> ...
 |     |--> 'packet' : (None)
 |     |--> 'conflict' : (tuple) octet ranges on which two fragments disagreed
 |     |                  |--> (tuple) (first, last), absolute and inclusive
 |     |                  |--> ...
 |--> (Info) data ...

Note

packet is analysed on the first read, not when the datagram is submitted. A datagram is submitted for every frame – an unfragmented one included, since nothing upstream filters it out – and the analysis is a second full parse of the payload, so running it eagerly would charge every caller for a result most never read. Reading the attribute, or any mapping view of it (datagram['packet'], to_dict(), items(), repr()), runs it and keeps the result; see Deferred.

Note

completed and conflict are independent signals: a datagram can be COMPLETE and still carry a non-empty conflict. RFC 791 resolves an overlapping fragment’s disagreement itself – “this procedure will use the more recently arrived copy in the data buffer” – the opposite resolution from TCP’s first-write-wins (RFC 9293 Section 3.10) – so a contested range never leaves a hole on its own, and conflict is what lets a caller tell a clean datagram from a contested one.

reasm.ipv4.buffer

Data structure for internal buffering when performing reassembly algorithms (IPv4._buffer) is as following:

(dict) buffer --> memory buffer for reassembly
 |--> (tuple) BUFID : (dict)
 |     |--> ipv4.src       |
 |     |--> ipv4.dst       |
 |     |--> ipv4.id        |
 |     |--> ipv4.protocol  |
 |                         |--> 'TDL' : (int) total data length
 |                         |--> 'RCVBT' : (bytearray) fragment received bit table
 |                         |               |--> (bytes) b'\\x00' -> not received
 |                         |               |--> (bytes) b'\\x01' -> received
 |                         |               |--> (bytes) ...
 |                         |--> 'index' : (list) list of reassembled packets
 |                         |               |--> (int) packet range number
 |                         |--> 'header' : (bytes) header buffer
 |                         |--> 'datagram' : (bytearray) data buffer, holes set to b'\\x00'
 |                         |--> 'timestamp' : (float) capture timestamp of the
 |                         |                          first-arriving fragment
 |                         |--> 'conflict' : (list) octet ranges on which an arriving
 |                         |                  fragment disagreed with bytes already in
 |                         |                  'datagram'
 |                         |                  |--> (tuple) (first, last), absolute and
 |                         |                               inclusive
 |                         |                  |--> ...
 |--> (tuple) BUFID ...

Note

conflict is only ever appended to, and is checked against datagram and RCVBT before an arriving fragment’s own write and bookkeeping touch them – see IP._detect_conflicts. RCVBT records receipt in 8-octet blocks, which is coarser than the octet a conflict needs; TDL is what recovers the exact extent for the one block marked received that can be partially real – the final fragment’s own tail – so a conflict here is never wider than the octets that genuinely disagreed, even inside that block. Any other partial last block – a non-final, truncated or clipped fragment’s – is left unmarked (#1567), and its octets are not compared: a later fragment that overwrites them records no conflict (#1585).

Note

A buffer is abandoned once the reassembly timeout elapses on the capture’s clock – 60 seconds by default, per RFC 1122 Section 3.3.2 for IPv4 and RFC 8200 Section 4.5 for IPv6, counted from the first-arriving fragment. Its datagram is reported with completed set to Completion.TIMEOUT rather than PARTIAL, which is what tells “these fragments are gone” apart from “these fragments had not arrived yet”. See expire().