IPv6 Datagram Reassembly

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

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

Bases: IP

Reassembly for IPv6 payload.

Parameters:
  • strict (bool) – if return all datagrams (including those not implemented) when submit

  • 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 IPv6
# Initialise instance:
>>> ipv6_reassembly = IPv6()
# Call reassembly:
>>> ipv6_reassembly(packet_dict)
# Fetch result:
>>> result = ipv6_reassembly.datagram
__protocol_name__: str = 'IPv6'

Protocol name of current reassembly object.

__protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.internet.ipv6.IPv6'>

Protocol of current reassembly object.

Terminology

reasm.ipv6.packet

Data structure for IPv6 datagram reassembly (IPv6.reassembly) is as following, with ipv6_info the IPv6 info and ipv6_frag_info the Fragment header’s:

hdr_len = ipv6_info.hdr_len - ipv6_frag.length
payload = bytearray(ipv6_info.fragment.payload)

packet_dict = dict(
  bufid = (
      ipv6_info.src,              # source IP address
      ipv6_info.dst,              # destination IP address
      ipv6_frag_info.id,          # identification
      ipv6_frag_info.next,        # next header field in IPv6 Fragment Header
  ),
  num = frame.info.number,        # original packet range number
  fo = ipv6_frag_info.offset,     # fragment offset, in octets
  ihl = hdr_len,                  # header length, only headers before IPv6-Frag
  mf = ipv6_frag_info.mf,         # more fragment flag
  tl = hdr_len + len(payload),    # total length, header includes
  header = ipv6_info.fragment
           .header[:hdr_len],     # raw bytes type header before IPv6-Frag
  payload = payload,              # raw bytearray type payload after IPv6-Frag
  timestamp = float(
      frame.info.time_epoch),     # capture timestamp
)

Note

ihl, header and tl all stop short of the 8-octet Fragment header, because RFC 8200 Section 4.5 says it is not present in the reassembled packet. IPv6.hdr_len does count it – it is a header length, and the Fragment header is one of the extension headers it has walked – so the adapters subtract it back off. All four adapters (pcap, pcapng, dpkt and scapy) agree on the three fields; they used to report three different values for tl alone, which is what #415 was about.

Note

The reassembly key is the Fragment header’s Identification (RFC 8200 Section 4.5), not the IPv6 header’s Flow Label. The label is optional and routinely zero, so keying on it collapses every datagram between one address pair into a single buffer and interleaves their fragments.

reasm.ipv6.datagram

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

(tuple) datagram
 |--> (Info) data
 |     |--> 'completed' : (Completion) COMPLETE --> reassembled in whole
 |     |--> 'id' : (Info) original packet identifier
 |     |            |--> 'src' --> (IPv6Address) ipv6.src
 |     |            |--> 'dst' --> (IPv6Address) ipv6.dst
 |     |            |--> 'id' --> (int) ipv6_frag.id
 |     |            |--> 'proto' --> (TransType) ipv6_frag.next
 |     |--> 'index' : (tuple) packet numbers
 |     |               |--> (int) original packet range number
 |     |--> 'header' : (bytes) header before IPv6-Frag
 |     |--> 'payload' : (bytes) reassembled IPv6 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' --> (IPv6Address) ipv6.src
 |     |            |--> 'dst' --> (IPv6Address) ipv6.dst
 |     |            |--> 'id' --> (int) ipv6_frag.id
 |     |            |--> 'proto' --> (TransType) ipv6_frag.next
 |     |--> 'index' : (tuple) packet numbers
 |     |               |--> (int) original packet range number
 |     |--> 'header' : (bytes) header before IPv6-Frag
 |     |--> 'payload' : (tuple) partially reassembled IPv6 payload
 |     |                 |--> (bytes) IPv6 payload fragment
 |     |                 |--> ...
 |     |--> 'packet' : (None)
 |     |--> 'conflict' : (tuple) octet ranges on which two fragments disagreed
 |     |                  |--> (tuple) (first, last), absolute and inclusive
 |     |                  |--> ...
 |--> (Info) data ...

Note

header is the fragment’s unfragmentable part with one field rewritten: the Next Header field of its last header carries the Fragment header’s Next Header value, as RFC 8200 Section 4.5 requires of a reassembled packet. Without that rewrite the datagram would still advertise a Fragment header (44) on a datagram that is no longer a fragment. The Payload Length field is not adjusted, so it still describes the first fragment rather than the reassembled datagram; use len(payload) instead.

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 charged 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, fixed for TCP by #443) – 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. See #477.

reasm.ipv6.buffer

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

(dict) buffer --> memory buffer for reassembly
 |--> (tuple) BUFID : (dict)
 |     |--> ipv6.src       |
 |     |--> ipv6.dst       |
 |     |--> ipv6_frag.id   |
 |     |--> ipv6_frag.next |
 |                         |--> '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'
 |                         |--> '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 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.