Fix four IPv6 defects (#352, #353, #360, #369) - #389
Merged
Merged
Conversation
…the flow label Four defects, two of which silently corrupted data while reporting success. The fragment offset was passed to the reassembler as raw wire units where it wants octets: foundation/reassembly/ip.py indexes the datagram buffer with it directly but uses FO // 8 for the received-bit table, so fragments landed at an eighth of their offsets and the completion check still passed. On ipv6.pcap the reassembled datagram was 977 octets instead of 4778, completed=True either way. _make_data now emits wire units again so Data -> make round-trips, deliberately not replicating the asymmetry ipv4.py has. scapy's toolkit needed the same scaling at its own site. Reassembly keyed on the flow label instead of the fragment identification, so id.id surfaced as 0 where the fixture carries 110308, and two concurrent fragmented datagrams sharing a label were merged into one. Fixed in the pcap, pcapng and scapy toolkits. The flow label itself was parsed from bits 8-27 rather than 12-31, so it included the last version/traffic-class nibble and dropped the label's low four bits. Worse than the issue reported: BitField uses the same namespace for packing, so pcapkit was *writing* malformed IPv6 headers - class 0x2a with label 0x12345 serialised as 62123450 instead of 62a12345. One line fixes both directions. The IPv6-Opts option registry lacked the __enum__ declaration hopopt.py has, so QuickStartOption's subclasses clobbered two of the parent registry's keys, not one as filed: Pad1 at key 0 and _SMFDPDOption at key 8. #353 and #360 interacted - a truncated label widened the collision surface of a label-keyed buffer - and keying on the identification dissolves that entirely. Four existing expectations changed because each encoded a bug, with the wire value now pinned alongside the octet value so the two cannot drift again. The toolkit fixtures also gained a fragment id distinct from their flow label, since a fixture where the two are equal proves nothing. Audited the whole schema tree for the same shapes: IPv6.hextet was the only overlapping BitField namespace and QuickStartOption the only genuine EnumSchema collision, so neither is part of a pattern. Suite 508 -> 517 passed, 315 -> 338 subtests; pylint delta exactly zero. Closes #352, closes #353, closes #360, closes #369.
There was a problem hiding this comment.
🟢 Approval recommended
The fixes are consistent across toolkits/schema/protocol layers and are backed by focused unit + runtime regression tests covering the previously silent corruption cases.
Pull request overview
This pull request fixes four IPv6 correctness defects spanning fragment reassembly (offset units + buffer key), IPv6 flow-label bit layout (read/write symmetry), and IPv6-Opts Quick Start option registry scoping, and adds targeted regression tests to prevent silent data corruption from recurring.
Changes:
- Correct IPv6 Fragment offset handling (wire 8-octet units ↔ internal octets) and ensure Data→make round-trips.
- Fix IPv6 reassembly buffer keying to use Fragment Identification (not flow label) across PCAP/PCAPNG/Scapy toolkits.
- Fix IPv6 flow-label bitfield layout and prevent registry clobbering in IPv6-Opts QuickStartOption via a scoped enum registry; add new unit/runtime tests for all four defects.
File summaries
| File | Description |
|---|---|
| pcapkit/protocols/internet/ipv6_frag.py | Scale fragment offset to octets on read; scale back to wire units in _make_data; clarify offset units in docs. |
| pcapkit/toolkit/pcap.py | Use IPv6 Fragment Identification (not flow label) for IPv6 reassembly buffer IDs. |
| pcapkit/toolkit/pcapng.py | Align PCAPNG IPv6 reassembly buffer IDs with Fragment Identification keying. |
| pcapkit/toolkit/scapy.py | Key on fragment identification and scale Scapy fragment offsets from units to octets for reassembly. |
| pcapkit/protocols/schema/internet/ipv6.py | Fix flow-label bit range to start at bit 12; add rationale comment tied to BitField semantics + RFC 8200. |
| pcapkit/protocols/schema/internet/ipv6_opts.py | Add QuickStartOption.__enum__ to prevent option-registry clobbering (Pad1/SMF_DPD). |
| tests/protocols/internet/test_ipv6_extension_unit.py | Update IPv6-Frag unit expectations (offset scaling + round-trip) and add registry collision guards. |
| tests/protocols/internet/test_ipv6_extension_runtime.py | Update runtime expectation for IPv6 fragment offset to octets. |
| tests/protocols/internet/test_ipv6_unit.py | Add flow-label bit-tiling + wire-round-trip tests to guard both parse and build paths. |
| tests/protocols/internet/test_ipv6_reassembly_runtime.py | Add end-to-end synthetic PCAP reassembly regression tests for offset scaling + identification keying. |
| tests/toolkit/test_pcap_unit.py | Update toolkit fixtures/assertions for identification keying and octet offsets. |
| tests/toolkit/test_scapy_unit.py | Update Scapy fixture/assertions for fragment identification and octet-offset scaling. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four filed IPv6 defects. Two of them silently corrupted data while reporting success, and one turned out to affect the write path as well as the read path.
#352 — fragment offset used as octets, not 8-octet units
read()passed the raw 13-bit wire field straight through, butfoundation/reassembly/ip.pyindexes the datagram buffer with it directly (datagram[FO:TL-IHL+FO]) while usingFO // 8for the received-bit table. So fragments landed at an eighth of their offsets and the completion check still passed.On
examples/captures/ipv6.pcap: 977 octets before, 4778 after —completed=Trueeither way, which is what made it dangerous. Verified by payload bytes, not just length: sha256 identical to an independent offset×8 reassembly._make_datanow emits wire units again soData → makeround-trips exactly; this deliberately does not replicate the round-trip asymmetryipv4.py:504has.toolkit/scapy.pyneeded the same scaling at its own site.#353 — reassembly keyed on the flow label, not the fragment identification
bufid[2]flows intoDatagramID.id, so the label both surfaced as the wrongid.id(0 where the fixture carries 110308) and merged unrelated datagrams. On a synthetic two-datagram capture sharing a flow label with ids 1001/2002 interleaved: before, one mergedcompleted=Truedatagram withindex=(1,2,3)mixing both bodies plus an orphan; after, two complete datagrams with payloads exactly equal to the originals. Fixed in thepcap,pcapngandscapytoolkits.#360 — flow label parsed from bits 8-27 instead of 12-31
Verified against RFC 8200 §3 (Version 4 / Traffic Class 8 / Flow Label 20 ⇒ the label starts at bit 12).
This is worse than the issue reports:
BitFielduses the same namespace forpre_process, so the overlap corrupted the build path too. At HEAD,class=0x2a, label=0x12345serialised to62123450instead of62a12345— traffic class emitted as0x21, label as0x23450. pcapkit was writing malformed IPv6 headers, not merely misreading them. One line fixes both directions; I confirmed62a12345independently on this branch.#369 — the IPv6-Opts registry clobbers Pad1
ipv6_opts.pylacked the__enum__declarationhopopt.py:489has, soQuickStartOption's subclasses overwrote keys in the parent registry. Two keys, not one as filed:Quick_Start_Request(0) clobberedPadOption, andReport_of_Approved_Rate(8) clobbered_SMFDPDOption. Both now match hopopt.How #353 and #360 interact
Only through #353's key, and fixing #353 dissolves it. #360 discarded the label's low four bits, so datagrams differing only there collided in the label-keyed buffer — #360 widened #353's collision surface. Once the buffer keys on the identification, the flow label no longer participates in reassembly at all. Independent defects, one amplifying the other.
Existing expectations changed
Four, each of which encoded a bug. In every case the wire value is now pinned alongside the octet value, so the two cannot drift apart again:
test_ipv6_extension_runtime.py:119frag.info.offsettest_ipv6_extension_unit.py:220data.offsettest_ipv6_extension_unit.py:114_make_data()['offset']test_pcap_unit.py:151,test_scapy_unit.py:152bufid[2]Both toolkit fixtures gained a fragment
id(4321) distinct from the flow label (7), plus anassertNotEqual(bufid[2], 7)— a fixture where the two are equal proves nothing.Verification
New
tests/protocols/internet/test_ipv6_reassembly_runtime.py(4 tests, synthetic captures, no fixture dependency): byte-exact 1536-octet payload assertion; the interleaved two-datagram merge with disjoint even/odd alphabets so a merge fails both a byte and a census check; the mirror case (same identification, different labels); and a three-fragment out-of-order datagram, since two fragments can pass by accident and three cannot. Plus 3 unit tests for the label including an exact bit-tiling assertion and a wire round trip, and 2 registry-scoping tests comparing hopopt's and ipv6_opts' registries key for key.Each fix was reverted individually to confirm the tests catch it. Suite 508 → 517 passed, 315 → 338 subtests; isort diffs byte-identical to HEAD; pylint delta exactly zero (the test was rewritten to snapshot registries with
dict(...), which also avoids mutating adefaultdicton a missing-key read).Audited the whole schema tree for both shapes:
IPv6.hextetwas the only overlappingBitFieldnamespace andQuickStartOptionthe only genuineEnumSchemacollision — so neither defect is part of a pattern.Reported, not fixed
toolkit/dpkt.py:195readsipv6_frag.nxt— the next-header field — as the fragment offset. Not merely unscaled; the wrong field entirely. dpkt exposes the offset asfrag_off. More severe than IPv6 fragment offset used as octets, not 8-octet units #352 and in no issue. That file was owned by another stream (Fix five dpkt toolkit defects (#351, #370) #385) at the time.PadOption.padreads aConditionalField-absentlenthat holdsNoValue, givingstruct.error: bad char in struct format. Left alone deliberately — it lives inhopopt.pytoo, and fixing only my copy would recreate exactly the one-module-diverges asymmetry that caused IPv6-Opts QuickStartOption registry clobbers Pad1 (option type 0) #369. Same family as MH: a Pad1 mobility option crashes parsing with struct.error #355; wants an issue covering both.ipv6.py:268_read_ip_hextet()has a third flow-label bug (version nibble included, 24-bit label). Dead code — its only caller is a test pinning the wrong values.toolkit/scapy.py:143andtoolkit/dpkt.py:147have the same unit defect as IPv6 fragment offset used as octets, not 8-octet units #352 on the IPv4 paths.tests/integration/test_reassembly_end_to_end.py:285has a@unittest.skipfor IPv6 fragment offset used as octets, not 8-octet units #352 that these fixes make obsolete. It is in another stream's directory, so untouched — but I verified its assertions pass now ([1448, 1448, 1448, 434], 4778 octets, payload equal to the joined fragments). It can be lifted in a follow-up.Closes #352, closes #353, closes #360, closes #369.