IPv6 Datagram Reassembly¶
pcapkit.foundation.reassembly.ipv6 contains
IPv6 only, which reconstructs
fragmented IPv6 packets back to origin; see IP Reassembly Base Class.
- class pcapkit.foundation.reassembly.ipv6.IPv6[_PT: Info, _DT: Info, _IT: tuple, _BT: Info](*, strict=True, store=True, timeout=None)[source]¶
Bases:
IPReassembly for IPv6 payload.
- Parameters:
strict (
bool) – ifTrue, 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 holestore (
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;Noneselects__timeout__
Example
>>> from pcapkit.foundation.reassembly.ipv6 import IPv6 # Initialise instance: >>> ipv6_reassembly = IPv6() # Call reassembly: >>> ipv6_reassembly(packet_dict) # Fetch result: >>> result = ipv6_reassembly.datagram
Changed in version 1.5.0: The reassembled
headerleaves out the Fragment header and carries its Next Header, per RFC 8200 Section 4.5, whichever engine fed it; 1.4.x kept the Fragment header on thedefaultandpcapngengines (#415). An atomic fragment is reassembled in isolation, without flushing a buffer that shares its BUFID (#2270).Changed in version 1.6.0: A fragment overlapping another of its datagram, other than an exact duplicate, abandons that datagram, which is reported as
Completion.OVERLAP, per RFC 8200 Section 4.5; 1.5.x overwrote the octets it overlapped, as RFC 791 has IPv4 do (#2547).- __protocol_type__: Type[ProtocolBase] = <class 'pcapkit.protocols.internet.ipv6.protocol.IPv6'>¶
Protocol of current reassembly object.
- __callback_fn__: list[Callable[[List[_DT]], None]]¶
List of callback functions upon reassembled datagram.
- __timeout__: float = 60.0¶
Default reassembly timeout, in seconds, on the capture’s clock.
RFC 8200 Section 4.5 is unambiguous and mandatory: “If insufficient fragments are received to complete reassembly of a packet within 60 seconds of the reception of the first-arriving fragment of that packet, reassembly of that packet must be abandoned and all the fragments that have been received for that packet must be discarded.”
Note “first-arriving”, not “first” – the deadline is counted from whichever fragment opened the buffer, not from the one at fragment offset zero, which is why
Buffer.timestampis set once and never revised. The offset-zero fragment matters only to the second half of the rule, the ICMP Time Exceeded that a live stack “should” send and that an offline parser cannot – seeexpire(), which reports the condition in its log record instead.- Type:
- __discard_overlap__: bool = True¶
A fragment that overlaps another of its datagram abandons that datagram: RFC 8200 Section 4.5 says “reassembly of that packet must be abandoned and all the fragments that have been received for that packet must be discarded”. It also lets an implementation “drop exact duplicate fragments while keeping the other fragments belonging to the same packet”, which this one does: a fragment with the same offset, length, M flag and data as one already merged changes nothing but
Datagram.index. A later fragment of an abandoned datagram opens a new buffer, as one does after a datagram completes (#2547).- Type:
Terminology¶
- reasm.ipv6.packet¶
Data structure for IPv6 datagram reassembly (
IPv6.reassembly) is as follows, withipv6_infothe IPv6infoandipv6_frag_infothe 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,headerandtlall 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_lendoes count it – it is a header length, and the Fragment header is one of the extension headers it has walked – so thepcapandpcapngadapters subtract it back off. Thedpktandscapyadapters leave it out of those fields too, but as best effort, not as a contract (#1687).Note
tlis derived from the captured payload, not from the Payload Length field, so a fragment the snapshot length cut does not show it intl. A non-final fragment must carry a multiple of 8 octets (RFC 8200 Section 4.5), so reassembly does not count the partial last block of one that ends mid-block as received: the datagram stays incomplete, rather than completing with zeros where the cut octets were (#1567). A cut final fragment cannot be told from a whole one.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.datagramtuple) is as follows:(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, TIMEOUT or OVERLAP --> 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
headeris 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; uselen(payload)instead.Note
packetis whatip_datagram()parsespayloadto, unless__protocol_type__names another protocol, and it is analysed on the first read, not when the datagram is submitted. 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; seeDeferred.Note
A fragment that overlaps another, other than an exact duplicate, abandons its datagram, as RFC 8200 Section 4.5 has a receiving node do, rather than overwriting the octets it overlaps as IPv4’s RFC 791 procedure does. The datagram is reported as
Completion.OVERLAP, itspayloadthe fragments merged before the overlapping one, andconflictwhere that fragment disagreed with them – empty if it repeated their octets. So aCOMPLETEIPv6 datagram never carries a conflict. An exact duplicate, with the same offset, length, M flag and data, is dropped as the RFC permits, its frame number kept inindex. A later fragment of an abandoned datagram opens a new buffer (#2547).- reasm.ipv6.buffer¶
Data structure for internal buffering when performing reassembly algorithms (
IPv6._buffer) is as follows:(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' | |--> '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 | | |--> ... | |--> 'extent' : (list) merged fragments, sorted and disjoint | | |--> (tuple) (start, stop, mf), stop exclusive | | |--> ... |--> (tuple) BUFID ...
Note
conflictis only ever appended to, and is checked againstdatagramandRCVBTbefore an arriving fragment’s own write and bookkeeping touch them – seeIP._detect_conflicts.RCVBTrecords receipt in 8-octet blocks, which is coarser than the octet a conflict needs;TDLis 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).