Skip to content

protocols: complete the Mobility Header registry -- all 24 message types, 70 of 71 options, all 4 CGA extensions - #437

Merged
JarryShaw merged 7 commits into
mainfrom
feat/mh-registry-completion
Sep 18, 2026
Merged

JarryShaw merged 7 commits into
mainfrom
feat/mh-registry-completion

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Closes the Mobility Header item on the Help Wanted page, which docs/source/pep.rst called "the section of this page with the most work left in it".

What now dispatches

Verified against a main baseline by reading the tables directly: MH.__message__ 14 → 24, MH.__option__ 20 → 71, MH.__extension__ 1 → 4.

All 24 registered message data types. New: Home Agent Switch (:rfc:5142 §4), Heartbeat (:rfc:5847 §3.3), Binding Revocation (:rfc:5846 §5.1/§5.2), Localized Routing Initiation and Acknowledgment (:rfc:6705 §10.1/§10.2), Update Notification and Acknowledgement (:rfc:7077 §4.1/§4.2), Flow Binding (:rfc:7109 §6.1.1/§6.1.2), Subscription Query and Response (:rfc:7161 §4.3.1.2/§4.3.2.2).

70 of 71 options — 17, 19, 20, 22–33, 35–70 — across :rfc:5026, :rfc:5094, :rfc:5149, :rfc:5213, :rfc:5555, :rfc:5648, :rfc:5844, :rfc:5845, :rfc:5847, :rfc:5949, :rfc:6058, :rfc:6089, :rfc:6463, :rfc:6602, :rfc:6705, :rfc:6757, :rfc:6909, :rfc:7028, :rfc:7109, :rfc:7148, :rfc:7161, :rfc:7222, :rfc:7389, :rfc:7411, :rfc:8127, :rfc:8278 and :rfc:8885. That includes nested sub-option registries for flow identification (6), access network identifier (6), quality-of-service (11 attributes) and LMA-controlled MAG parameters (2). Every entry cites its RFC and section in the docstring, with the RFC's own diagram.

All 4 CGA extensions; the three :rfc:4581 §3 experimental types share one handler.

Read and construction are symmetric throughout, so each of those is a _read_ and a _make_ handler — the construction half is where this repository's defects cluster, so it was not left for later.

One option deliberately left on the generic handler

CGA Parameters (type 12) cannot be parsed on main either, and this PR does not pretend otherwise: a well-formed 40-octet option raises KeyError: 'length' on both trees. CGAParameter.extensions sizes itself from pkt['length'], but CGAParameter has no length field and SchemaField hands a nested schema a fresh packet dict. Making that lookup optional runs straight into a second fault in how ForwardMatchField counts toward a nested schema's length (FieldValueError) — shared field machinery, so the half-fix was reverted rather than shipped as a change of exception type. Pinned by test_mh_cga_parameters_option_is_unparsable_upstream so the day it starts working is visible.

Registries

No new enum values and no vendor-crawler changes were needed — every sub-registry was already generated. Two spec-only value sets went into mh.py per the house policy, both confirmed absent from IANA: LocalizedRoutingStatus (:rfc:6705 §10.2) and LMAAddressCode (:rfc:5949 §6.2.2).

Defects and specification traps found

Fixed, in the area being completed: _make_ext_multiprefix declared 1 + len(prefixes) * 16 data octets for a 4 + len(prefixes) * 8 payload — 33 declared against 20 emitted for two prefixes — and forwarded the data model's prefixes tuple to a ListField, which raises ProtocolUnbound. So a parsed Multi-Prefix extension could never be re-made.

Reported, not fixed: _make_opt_mn_id measures len(identifier) even for the IPv6_Address subtype, so a string identifier declares 11 octets while the schema emits 16. Pre-existing; passing an IPv6Address is correct.

Two wire-format traps now documented at the handlers: :rfc:7411's multicast options count length in 32-bit words excluding four octets, and :rfc:5213 §8.8's timestamp is not an :rfc:1305 NTP timestamp — 1970 epoch with a 48/16 split — so it gets its own PMIPv6Timestamp.

Two RFCs contradict themselves, noted in docstrings rather than silently resolved: :rfc:6089 §8 prints options 44 and 45 swapped relative to its own figures and to IANA, and :rfc:7563 §3.2 disagrees with itself on the MAG-Group-Identifier width — diagram and Length field say two octets, prose says three.

Verification

Suite 889 passed / 17 skipped / 1064 subtests (711 s), against a 3e696c7d1 baseline of 859 / 35 / 844. The skip delta is fully accounted for: all 18 extras are test_tier_guard.py skipping because the baseline was a git archive with no .git; nothing is newly skipped. +12 collected tests, all in test_mh_unit.py (22 → 34 methods).

Round-trip byte-identical for all 70 options, all 24 message types and all 4 extensions — construct → parse → construct — with no parse falling through to the generic handler.

Captures: 15 files × tree and json with ip=True, tcp=True, reassembly=True, all 30 outputs byte-identical to baseline. make_samples.py regenerates all 19 files byte-identically. mypy exactly unchanged at 128 errors in 41 files with identical per-file counts. pylint errors and fatals exactly unchanged at 78; the score moves 8.76 → 8.55 entirely in convention and warning classes the module already emits by design — long union annotations, **kwargs and TYPE_CHECKING-stub unused arguments, and the type parameter name that is this module's established _make_opt_* convention.

docs/source/pep.rst is rewritten so its counts are accurate rather than stale, and it now spells out the CGA Parameters defect, the opaque MLD/IGMP and :rfc:6088 payloads, and the MN-ID defect. The MH docs page and the class's three dispatch tables are generated from the registries themselves, so they cannot drift.

Comment thread pcapkit/protocols/schema/internet/mh.py
@JarryShaw

Copy link
Copy Markdown
Owner Author

Reviewed at head 076fd24f4 (standing in for Copilot, which is out of tokens). Checked out the PR branch in a worktree, built a real git clone baseline of main (44aa38ae8, full .git history), and ran everything under /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python (3.14) with PYTHONSAFEPATH=1 and PYTHONPATH pointed at whichever tree, confirmed each time by printing pcapkit.__file__.

What I ran

  • Generated fresh sample captures (examples/generators/make_samples.py) and ran the full suite on the PR tree: 889 passed, 17 skipped, 1064 subtests in 782s — matches the PR body's own 889/17/1064 claim exactly.
  • Confirmed dispatch-table sizes at runtime: len(MH.__message__)==24, len(MH.__option__)==71, len(MH.__extension__)==4 — matches the claimed 14→24 / 20→71 / 1→4 growth.
  • git diff 44aa38ae8..076fd24f4 --stat -- pcapkit/const/ pcapkit/vendor/ is empty — confirms no new IANA enum values or crawler changes, as claimed.
  • Independently reproduced (with full tracebacks) both disclosed defects and the CGA test, on both trees.

Wire-format spot checks against the actual RFC text

Verified field order, widths, and what the Length field counts by fetching the RFC text directly and comparing to the code, for:

  • The two disclosed "traps": RFC 7411 §5.3/§5.4 multicast option Length in 32-bit words excluding 4 octets (_read_opt_mcast/_make_opt_mcast and the _ack sibling) — correct; and RFC 5213 §8.8's Timestamp option, 1970-epoch 48/16 fixed-point split vs RFC 5905's 1900-epoch 32/32 NTP split (PMIPv6Timestamp vs NTPTimestamp, _read_opt_timestamp/_make_opt_timestamp) — correct, including a probe that decodes the same bytes correctly as PMIPv6 and as garbage as NTP.
  • The two disclosed self-contradicting RFCs: RFC 6089 §8 vs its own Figures 2/6 vs the live IANA registry on whether type 44 is Flow-Summary or Flow-Identification (code follows the figures/IANA: 44→fs, 45→fid) — correct; and RFC 7563 §3.2's MAG-Group-Identifier width, diagram+Length-field text (2 octets) vs separate prose (3 octets) — code takes the 2-octet reading, which is the only one consistent with ANI Length = 2, and is correct.
  • Message types: Binding Revocation (RFC 5846 §5.1/§5.2, br_type/trigger-or-status sharing one octet, P/V/G/Reserved bit layout), Localized Routing Initiation/Acknowledgment (RFC 6705 §10.1/§10.2, including the "Lifetime is plain seconds here, not RFC 6275's 4-second units" note and the U/Reserved/Status layout) — all correct.
  • Options spanning fixed-length, variable-length, reserved fields, and padding: GRE Key Option (RFC 5845 §6.1, Reserved(16)+optional 32-bit key gated on Length∈{2,6}), Binding Identifier (RFC 5648 §4.3 as updated by RFC 6089 §4.1, BID/Status/H-flag/BID-PRI/CoA gated on Length∈{4,8,20}), Vendor-Specific Mobility Option (RFC 5094 §3, Vendor-ID+Sub-Type+data, min length 5), Pad1/PadN (RFC 6275 base format) — all correct.
  • Nested sub-option registries: Flow Identification (6 sub-options, isinstance-dispatched, all 6 covered both directions), Access Network Identifier (6 sub-options incl. the two's-complement Geo-Location degrees and the RFC 7563-defined Civic-Location/MAG-Group-Identifier/ANI-Update-Timer trio — the 4-second Update-Timer unit checks out against RFC 7563 §3.3), Quality-of-Service (all 11 attribute type codes, correctly grouped across 5 schema classes — verified the plain-bit-rate types 1/2/6/7/8/9 vs the S/E-flagged per-session types 3/4 vs Allocation-Retention-Priority's PL/PC/PV bit-packing against RFC 7222 §4.2), LMA-Controlled MAG Parameters (2 sub-options, RFC 8127 §3.1.1/§3.1.2, incl. the 4-second Re-registration-Start-Time vs plain-second retransmission timers) — all correct, and all round-trip byte-identical.
  • Registry-lookup safety: every read of __message__/__option__/__extension__ in pcapkit/protocols/internet/mh.py goes through _lookup_registry; the only bare [code] subscripts are the legitimate writes inside register_message/register_option/register_extension. The four nested sub-option families don't use dict registries at all (isinstance/if-elif chains instead), so there's no dict-mutate-on-miss risk there either.
  • Enum placement: LocalizedRoutingStatus (RFC 6705 §10.2) and LMAAddressCode (RFC 5949 §6.2.2) are absent from both RFCs' own IANA Considerations sections and from pcapkit/const/mh/ — inline placement in mh.py is correct per house policy.
  • Read/make completeness: every __message__/__option__/__extension__ entry has both a _read_* and _make_* handler with matching names; no new asymmetry.
  • The two claimed defects: _make_ext_multiprefix on main declares length=33 against a real 20-octet payload and raises ProtocolUnbound: unsupported type <class 'tuple'> when re-making a parsed extension (reproduced); on this PR it declares length=20 and round-trips byte-identically (reproduced). _make_opt_mn_id still measures len(identifier) for a string under the IPv6_Address subtype — reproduced with identifier='::a:b:c:d:e' (11 chars, valid IPv6 literal): declares 12 octets, packs 19 — confirmed pre-existing (byte-identical function body against baseline) and accurately disclosed in docs/source/pep.rst, not silently left broken.
  • CGA Parameters (type 12): reproduced the claimed KeyError: 'length' on both trees with the exact 40-octet payload from test_mh_cga_parameters_option_is_unparsable_upstream, traced it to the same pkt['length'] lookup in the CGAParameter.extensions ForwardMatchField lambda on both trees, and confirmed the test asserts the specific exception args ('length'), not a broad catch — it's not vacuous.

Finding posted

One minor, non-blocking finding posted inline on pcapkit/protocols/schema/internet/mh.py: two ANI sub-option classes (ANICivicLocationSuboption, ANIUpdateTimerSuboption) carry no RFC citation, unlike every other handler in this PR — both are actually RFC 7563-defined (confirmed via the live IANA registry) and both are wire-format correct (verified against RFC 7563 §3.1/§3.3 directly), so it's an attribution gap, not a functional defect.

What I did not cover

This is a ~13,000-line diff; I did not check every one of the 70 options and 24 message types individually against their RFCs — I covered a deliberately wide spread (listed above) plus everything the PR body specifically flagged as risky, and had multiple independent passes converge on the same "no wire-format defects" conclusion for the traps/contradictions/nested registries. I did not independently re-verify the mypy (128 errors, 41 files) or pylint (78 errors/fatals, score 8.76→8.55) deltas — out of scope per the review brief, and the numbers are disclosed rather than hidden. I did not check the two large doc-only files (docs/source/pcapkit/protocols/internet/mh.rst, docs/source/pep.rst) beyond the specific claims cited above (the 17-skip accounting, the trap/defect prose) — they're Sphinx autodoc output driven by the same docstrings already checked, and docs/source/pep.rst's defect/trap disclosures were spot-checked and match the code.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Reviewed at head 076fd24f4 (branch feat/mh-registry-completion). No defects found. This comment records what was checked and how, since a bare "looks good" on a 13k-line diff is worthless.

Setup: PR branch checked out in a dedicated worktree; baseline was a real git clone of main (44aa38ae8), not an archive. All runs used /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python (3.14.7) with PYTHONSAFEPATH=1 and PYTHONPATH pointed explicitly at the tree under test; pcapkit.__file__ was printed on every run to confirm which tree actually loaded.

The two wire-format "traps" the body calls out — both verified correct against the RFC text and against direct probes:

  • RFC 7411 §5.3/§5.4: fetched the RFC text directly. "Length: ... in 32-bit words, not including the Type, Length, Option-Code, and Reserved fields" for both the Multicast Mobility option (type 60) and Multicast Acknowledgement option (type 61). _read_opt_mcast/_read_opt_mcast_ack compute length=schema.length * 4 + 4, and _make_opt_mcast/_make_opt_mcast_ack compute length=len(data) // 4 after rejecting non-multiple-of-4 payloads — matches exactly. Confirmed via the existing test_mh_word_counted_option_lengths_are_not_octet_counts (passes) and by reading the schema (BytesField(length=lambda pkt: pkt['length'] * 4)).
  • RFC 5213 §8.8: fetched the RFC text. "the integer number of seconds is contained in the first 48 bits ... the remaining 16 bits indicate the number of 1/65536 fractions of a second," counted from 1970. TimestampOption.timestamp is a BitField(length=8, namespace={'seconds': (0, 48), 'fraction': (48, 16)}), and PMIPv6Timestamp/_make_opt_timestamp/_read_opt_timestamp implement exactly this, distinct from the NTPTimestamp (1900 epoch, 32/32) used elsewhere for the Mobility Message Authentication option's replay timestamp. Confirmed via test_mh_pmipv6_timestamp_is_not_an_ntp_timestamp (passes).

The two RFCs claimed to contradict themselves — both contradictions independently confirmed real, and the PR's resolution checked against IANA:

  • RFC 6089 §8 vs. its own figures: §8's IANA Considerations text reads "44 Flow Identification Mobility Option ... 45 Flow Summary Mobility Option," but Figure 2 (§4.2, "The Flow Identification Mobility Option") itself shows Option Type 45, and Figure 6 (§4.2.2, "The Flow Summary Mobility Option") shows Option Type 44 — backwards from §8's own prose. Checked the live IANA Mobility Options registry: 44 = Flow Summary Mobility Option, 45 = Flow Identification Mobility Option, i.e. matching the figures, not §8. Enum_Option.Flow_Summary_Mobility_Option = 44 / Flow_Identification_Mobility_Option = 45 in pcapkit/const/mh/option.py (pre-existing, IANA-derived) already has this right, and _read_opt_fs/_read_opt_fid implement the layouts that go with each number correctly (repeated-FID list for 44, FID/FID-PRI/Status/sub-options for 45).
  • RFC 7563 §3.2 (MAG-Group-Identifier sub-option): the diagram places group identifier in the last 16 bits of the 4-octet row, and "ANI Length: ... The value is always 2" — both say 2 octets — but the field description text says "a 3-octet unsigned integer value." ANIMAGGroupIdentifierSuboption.group_id is a UInt16Field() (2 octets), which is the choice consistent with the diagram and the explicit Length constraint (a 3-octet field would contradict "ANI Length ... always 2" and misalign every subsequent sub-option). Docstring at pcapkit/protocols/schema/internet/mh.py:1832-1841 states the contradiction accurately.

Spread of RFC-vs-code checks, beyond the two required traps:

  • Home Agent Switch message (RFC 5142 §4): "# of Addresses" (1 octet) → "Reserved" (1 octet) → Home Agent Addresses (16 octets each) → Mobility Options — matches HomeAgentSwitchMessage schema and _make_msg_has (count derived from len(addresses), not carried in the data model) exactly.
  • Binding Identifier option (RFC 5648 §4.3, updated by RFC 6089 §4.1): confirmed the RFC 6089 update actually renames the 7 reserved bits to BID-PRI (fetched RFC 6089 §4.1's own figure and prose), and _read_opt_bid implements the newer BID-PRI reading with the length-implied address family (4/8/20 octets ↔ none/IPv4/IPv6).
  • IPv4 DHCP Support Mode option (RFC 5844 §3.3.4): confirmed the S bit is the last bit after a 15-bit Reserved field (not the usual flag-first layout), matching the code's explicit note about it being "the other way round from every other flag in this module."
  • Quality-of-Service option and its 11 attributes (RFC 7222 §4.1/§4.2.1-§4.2.11, nested sub-option registry): all 11 QoSAttribute codes are covered — 6 share BitRateAttribute (verified against §4.2.1 which specifies Reserved(2)+Rate(4) for the shared shape), 2 share PerSessionBitRateAttribute, and 3 have dedicated classes. Cross-checked the QoS-Traffic-Selector attribute's Reserved-then-Format field order (RFC 7222 §4.2.10) against the opposite order used by RFC 6089 §4.2.1.4's Traffic Selector sub-option (Format-then-Reserved) — the code gets both orders right in their respective classes, and the docstring calls out the inversion explicitly.
  • Access Network Identifier (6 sub-options) and LMA-Controlled MAG Parameters (2 sub-options): exercised via the existing exhaustive round-trip test (all 6 ANI sub-option types, both LCMP sub-options).

Round-trip / registry completeness (script run against MH.__message__/__option__/__extension__): all 24 message types, 71 options, and 4 extensions have a bound _read_* and _make_* method — zero missing pairs. Also ran the module's own test_mh_pmipv6_options_round_trip_byte_for_byte (asserts its cases dict covers every Option except CGA_Parameters, then does construct→parse→reconstruct and checks byte-identity plus that the parse didn't land on UnassignedOption), test_mh_message_types_round_trip_byte_for_byte, test_mh_experimental_cga_extensions_round_trip, and test_mh_every_registered_code_has_both_handlers — all pass (217 subtests across these 7 tests).

Registry-lookup safety: grepped for bare __message__[, __option__[, __extension__[ subscripting in mh.py. The only three hits are the assignments inside register_message/register_option/register_extension; every read-side dispatch (8 call sites, covering read+make for all three tables) goes through self._lookup_registry(...). No leak of the kind this repo has had before.

Enum placement: independently fetched RFC 6705 §14 (IANA Considerations) — it registers only the two message-type codes (17, 18) and the MAG IPv6 Address option (51), not the LRA Status values from §10.2. Fetched RFC 5949 §8 — it registers three option types and four flag/status-code tables (marked with asterisks as new IANA tables), but not the LMAA option's Option-Code values from §6.2.2. Both LocalizedRoutingStatus and LMAAddressCode are correctly kept out of pcapkit/const/mh/ + a vendor crawler.

The two disclosed behavior items, both verified directly rather than taken on faith:

  • _make_ext_multiprefix fix: diffed against main's version of the same function (length=1 + len(prefixes) * 16 and a no-op cast() on a tuple option.prefixes handed to a ListField). Fetched RFC 5535 §5: "Ext Len ... Length of the Extension in octets, not including the first 4 octets," with a 4-octet P-flag/Reserved word plus 64-bit (8-octet) prefixes — the PR's length=4 + len(prefixes) * 8 and list(option.prefixes) match. test_mh_multiprefix_extension_length_matches_its_payload passes (4 subtests, counts 0/1/2/5).
  • _make_opt_mn_id known-not-fixed bug: reproduced directly — _make_opt_mn_id(Option.MN_ID_OPTION_TYPE, subtype=MNIDSubtype.IPv6_Address, identifier='2001:db8::1') produces schema.length == 12 (1 + len('2001:db8::1') = 1+11) but packs to 19 bytes total (17 octets of option data after Type/Length), because the identifier field is a fixed-width IPv6AddressField() that always emits 16 octets regardless of what the else: id_len = len(identifier) branch declared. Confirmed this is pre-existing/unfixed, not newly introduced.
  • CGA Parameters (type 12): confirmed test_mh_cga_parameters_option_is_unparsable_upstream's 40-octet fixture decodes to a genuinely well-formed MH packet (Header Len=4→40 octets total, MH Type 0 = Binding Refresh Request, a 32-octet CGA Parameters option with valid 16-octet modifier / 8-octet subnet prefix / collision count 0 / 5-octet public key, zero extensions) — not a truncated/malformed strawman. Ran the exact same bytes through both the PR tree and the main baseline clone directly (MH(io.BytesIO(raw), len(raw), extension=True)): both raise KeyError: 'length', confirming the defect is real, in shared field machinery, and not new.

Suite and metric claims: ran the full suite on the PR tree (pytest tests/ -q) — 889 passed, 17 skipped, 1064 subtests passed in 782.57s, matching the claimed 889/17/1064 exactly. Test-method count in test_mh_unit.py: 22 on main vs. 34 on the PR branch (+12), matching. Ran mypy pcapkit on the PR tree directly: Found 128 errors in 41 files (checked 493 source files), matching the claimed count exactly. Did not reproduce the pylint score delta (a subset-of-files pylint run is not comparable to a full-package run and wasn't worth the wall-clock time; pylint/mypy findings are out of scope for this review regardless per the review brief).

Not independently re-verified: the 15-capture tree/json byte-identity claim against examples/captures/ (none of the stock captures contain Mobility Header traffic, so this is a non-regression check on unrelated protocols rather than something that exercises this PR's new code — captures were regenerated via make_samples.py but not diffed against a baseline run given the time cost relative to expected signal), and the exact pylint score movement (8.76 → 8.55).

Handlers/sections checked directly against RFC text: RFC 5142 §4, RFC 5213 §8.8, RFC 5535 §5, RFC 5648 §4.3, RFC 5844 §3.3.4, RFC 5949 §6.2.2 + §8, RFC 6089 §4.1, §4.2, §4.2.1.4, §4.2.2, §8, RFC 6705 §10.2 + §14, RFC 7222 §4.1, §4.2.1, §4.2.10, RFC 7411 §5.3-§5.5, RFC 7563 §3.2. (RFC 6275 §6.2.2/§6.2.3 pad-option logic was also read for the padding-rules category, but it is pre-existing code unchanged by this diff, so it is out of scope here.)

Comment thread pcapkit/protocols/internet/mh.py
@JarryShaw

Copy link
Copy Markdown
Owner Author

Addendum at 076fd24f4 — closing the one gap I listed as unchecked above: the 17-skip accounting.

Re-ran the full suite with -rs on the PR tree (889 passed, 17 skipped, 1064 subtests, 723s). Every one of the 17 skips is environmental or a pre-existing upstream block, and none is in the mobility header:

  • 11 × optional third-party engine dependencies absent: pypcap (4), pypcapfile (6, in tests/foundation/engines/test_new_engine_parity_runtime.py), pcap-ct (1)
  • 4 × pypcapfile not installed or not importable in tests/toolkit/test_pypcapfile_unit.py
  • 2 × blocked on upstream dictdumper defects, each with a specific cited cause — dictdumper/plist.py:278 rejecting fractional-second <date> values, and dictdumper/json.py:224 interpolating mapping keys unescaped

Nothing from tests/protocols/internet/test_mh_unit.py is skipped, so no part of the new MH work is silently not running. Notably, no test_tier_guard.py skips appear at all — which independently confirms the PR body's explanation of the skip delta against its 859 / 35 / 844 baseline: those 18 extra baseline skips were purely an artifact of that baseline being a git archive with no .git directory, not newly-skipped tests. Running against a real git clone of main (44aa38ae8, full history), the tier-guard tests all execute. The claim "nothing is newly skipped" holds.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Reviewed at 076fd24f4 (branch feat/mh-registry-completion vs main). Worked from a git clone checkout (not archive), interpreter /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python (3.14), PYTHONSAFEPATH=1 with PYTHONPATH pointed at the checkout and pcapkit.__file__ printed on every probe to confirm the tree under test. One finding posted as an inline comment (silent payload loss in _make_qos_attribute/_make_fid_suboption/_make_ani_suboption/_make_lcmp_suboption when a caller passes data= as a raw-bytes kwarg, since it collides with the "data model" positional parameter of the same name). Everything below is what I checked and found to match the PR's own claims.

Wire-format traps (the two the body says it handled specially), verified against the RFC text itself, not just the docstrings:

  • RFC 7411 §5.3/§5.4 (Multicast Mobility / Multicast Acknowledgement options): fetched the RFC directly — Length is "in 32-bit words, not including the Type, Length, Option-Code, and Reserved/Status fields." _make_opt_mcast/_read_opt_mcast (and the _ack twin) implement exactly length = len(data)//4 on construct and length*4+4 on read. Probed: an 8-octet payload packs as 3c 02 02 00 <8 bytes> — Type=60, Length=2 words, matching the RFC exactly.
  • RFC 5213 §8.8 (Timestamp option / PMIPv6Timestamp): fetched the RFC — "the integer number of seconds is contained in the first 48 bits ... the remaining 16 bits indicate the number of 1/65536 fractions of a second," counted from 1970. PMIPv6Timestamp and _read_opt_timestamp/_make_opt_timestamp implement exactly that 48/16, 1970-epoch split, distinct from the pre-existing NTPTimestamp (1900 epoch, 32/32) used by _read/_make_opt_mesg_id. Type=27, Length fixed at 8, matching the RFC.

The two "RFC contradicts itself" claims, verified against the RFC text and against pcapkit/const/mh/option.py (IANA-crawler-generated):

  • RFC 6089 §8 vs its own body figures for Flow Summary/Flow Identification options: the RFC's own Figure 6 (§4.2.2) says Flow Summary = type 44, Figure 2 (§4.2) says Flow Identification = type 45; §8 (IANA Considerations) swaps them. pcapkit/const/mh/option.py (crawler output from the real IANA registry) has Flow_Summary_Mobility_Option = 44, Flow_Identification_Mobility_Option = 45 — siding with the body figures, which is what the code and its docstring at _read_opt_fs implement and state.
  • RFC 7563 §3.2 MAG-Group-Identifier width: fetched the RFC — the diagram places group identifier in the last 16 bits of the word, and "ANI Length: ... The value is always 2," but the field description separately calls it "a 3-octet unsigned integer." The code (ANIMAGGroupIdentifierSuboption.group_id: UInt16Field()) picks 2 octets, which is what both the diagram and the Length-field prose agree on; the docstring states this reasoning correctly.

Spread of handlers checked against their RFCs directly (fixed-length, variable-length, nested registry, reserved field, padding):

  • Binding Identifier option (RFC 5648 §4.3, updated by RFC 6089 §4.1 renaming Reserved to BID-PRI): probed with an IPv6 CoA — wire bytes 23 14 00 2a 00 85 <16-byte addr> decode to Type=35, Length=20, BID=42, Status=0, H=1, BID-PRI=5, matching the RFC's figure field-for-field; the length∈{4,8,20} branching matches the RFC's MUST-set rule.
  • Vendor-Specific Mobility Option (RFC 5094 §3, variable length): probed with a 7-octet vendor payload — wire bytes 13 0c 00000009 c8 deadbeef010203, matching Vendor ID(4)+Sub-Type(1)+Data, Length=12 excluding Type/Length, per the RFC.
  • Access Network Identifier (RFC 6757, 6 nested sub-options: Network-Identifier, Geo-Location, Operator-Identifier, Civic-Location, MAG-Group-Identifier, ANI-Update-Timer): constructed all 6 in one option and round-tripped construct→parse→reconstruct byte-identical; outer ANI Length (63) equals the exact sum of all 6 sub-options' own lengths (18+8+14+15+4+4), consistent with RFC 6757's "no reserved octet before the first sub-option" note already in the docstring.
  • Home Network Prefix option (RFC 5213 §8.3, reserved field): RFC says the Reserved octet "MUST be ignored by the receiver." Hand-built an option with reserved=0xFF and confirmed parsing does not raise and the value isn't surfaced.
  • Padding (RFC 6275 §6.2.5, Pad1/PadN): read _make_pad_options/_pad_mh_message and confirmed via the round-trip probes above (e.g. a 22-octet option padded with a 4-octet PadN to reach 8-octet alignment) that the Pad1(×1/×2)-vs-PadN(Length=N-2) choice matches the RFC.

Other nested sub-option registries (flow identification, RFC 6089 §4.2.1, 6 sub-options incl. Pad/PadN/BID-Reference/Traffic-Selector/Flow-Binding-Action/Target-CoA; quality-of-service, RFC 7222, 11 attributes; LMA-controlled MAG parameters, RFC 8127 §3.1, 2 sub-options) — constructed every sub-option/attribute type in each registry and confirmed byte-identical round-trips, except the QoS Vendor-Specific Attribute payload issue posted inline.

Completeness and registry policy:

  • Every one of the 24 __message__, 71 __option__ (including CGA Parameters, see below) and 4 __extension__ entries has both a _read_* and _make_* handler (checked programmatically against the method tables).
  • Every read of MH.__message__/__option__/__extension__ goes through _lookup_registry (8 call sites); the only bare registry[...] indexing in the three touched files is the pre-existing, unrelated EnumSchema.registry mechanism at schema/internet/mh.py:337, byte-identical to main and untouched by this PR.
  • LocalizedRoutingStatus (RFC 6705 §10.2) and LMAAddressCode (RFC 5949 §6.2.2) are absent from pcapkit/const/mh/ and from the pcapkit/vendor/mh/ crawler file list (1:1 with const/mh/), matching the "IANA keeps no registry" claim; RFC 6705 §10.2's own status values (0/128/129) match the enum exactly. The placement follows the pre-existing precedent of FastBindingAcknowledgmentStatus/IPv6AddressPrefixCode, including near-identical docstring wording.
  • CGA Parameters (option 12): confirmed the dispatch entry and both _read_opt_cga_param/_make_opt_cga_param pre-date this PR (byte-identical to main), and the CGAParameter/CGAParametersOption schema classes are also byte-identical to main — this PR did not touch the broken code path, only documented and pinned it. Ran test_mh_cga_parameters_option_is_unparsable_upstream: it constructs a genuinely well-formed 40-octet CGA Parameters option (verified the hex by hand against the CGAParameter field layout) and the test passes, asserting KeyError with args[0] == 'length', matching the CGAParameter.extensions field's pkt['length'] lookup exactly.

The two disclosed defects:

  • Fixed: _make_ext_multiprefix now computes length = 4 + len(prefixes)*8 (was 1 + len(prefixes)*16) and converts option.prefixes to a list before handing it to the ListField (was a no-op cast). Reproduced the old bug on a fresh main clone (ProtocolUnbound: unsupported type <class 'tuple'> when re-making a parsed extension) and confirmed the PR's code no longer raises and emits exactly 24 octets (4-octet header + 20-octet Length-field payload) for two prefixes.
  • Reported, not fixed: _make_opt_mn_id measures len(identifier) for a string identifier even under the IPv6_Address subtype. Probed with identifier='2001:db8::1' (11 characters): declared Length field comes out to 12 (1 + 11), while the schema's IPv6AddressField always emits a fixed 16-octet address regardless — an IPv6Address object (the correct usage) is unaffected.

Verification run: pytest tests/ -q on this branch: 889 passed, 17 skipped, 1064 subtests, 0 failures — matches the PR body exactly. Regenerated examples/captures/ via make_samples.py on both this branch and a fresh main clone and diffed the output directories: byte-identical.

Not independently re-verified (disclosed by the PR body, and outside what this review prioritized): the mypy (128 errors/41 files) and pylint (78 errors/fatals, score 8.76→8.55) counts, and the full 15-file × tree/json (30-output) capture-extraction byte-identity claim beyond the sample-generation check above. Also not exhaustively checked: the remaining ~55 options and ~20 message types beyond the ones named above (Home Agent Switch, Heartbeat, Binding Revocation, Localized Routing Init/Ack, Update Notification/Ack, Flow Binding, Subscription Query/Response, and most of the PMIPv6/NEMO/DMM option blocks) — spot-checking a representative spread rather than all ~94 handlers was the deliberate scope given the size of this diff.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Correction: the review comments on this PR reported a clean run without checking CI

Three review passes ran on 076fd24f4 and reported the local suite as 889 passed / 17 skipped / 1064 subtests, 0 failures. That number is real, but it was measured on Python 3.14 only, and this PR is red on Python 3.10Python 3.10 and Integration Python 3.10 were already failing on this exact sha while those comments were being written. Two of the three made that omission; one caught it. So treat the "0 failures" line in them as scoped to 3.14, not as a statement about CI.

This PR is not ready to merge. Six failures, all in the new nested sub-option tests:

FAILED    test_mh_geo_location_degrees_are_signed
          FieldValueError: Field suboptions has invalid value
SUBFAILED Flow_Identification_Mobility_Option    (byte mismatch)
SUBFAILED Access_Network_Identifier              AttributeError: 'ANIGeoLocationSuboption' has no attribute 'flags'
SUBFAILED IPv4_Traffic_Offload_Selector          (byte mismatch)
SUBFAILED Quality_of_Service                     AttributeError: 'BitRateAttribute' has no attribute 'flags'
SUBFAILED LMA_Controlled_MAG_Parameters          AttributeError: 'HeartbeatControlSuboption' has no attribute 'start_time'

Probable root cause, and it is worth recording

On Python 3.10, isinstance() returns True between two sibling schema classes that have no subclass relationship. Measured side by side on the same tree:

check 3.10 3.14
isinstance(heartbeat_obj, BindingReregistrationControlSuboption) True parses correctly
type(obj) is HeartbeatControlSuboption True True
issubclass either direction False False
MROs identical identical

type() identity and issubclass() are both correct; only isinstance() is wrong. That points at the abc.ABCMeta instance-check path rather than MRO resolution — SchemaMeta derives from abc.ABCMeta (pcapkit/protocols/schema/schema.py:136) and Schema subclasses Mapping, so every schema class carries a virtual-subclass registry and an _abc_impl cache behind __instancecheck__.

It hits these four registries specifically because _read_lcmp_suboptions, _read_ani_suboptions, _read_fid_suboptions and _read_qos_attributes dispatch through a chain of isinstance(schema, Schema_X) tests instead of on the enum code or type(). The first matching branch wins, so a HeartbeatControlSuboption is handled as a BindingReregistrationControlSuboption and dies on .start_time.

Ruled out with evidence: a double import (distinct id()s, and type(obj) is the right class); MRO or class-construction order (identical MROs); the EnumSchema code→class registry (it maps both codes correctly — the parse builds the right object and only the isinstance chain mis-sorts it); ForwardMatchField, BitField, annotation evaluation and PEP 563 (the wrong branch is taken on an already-correct object, before any field re-read); and enum/aenum.

Not established: the precise trigger. A.__eq__ is B.__eq__ is True and CPython reworked the _abc cache across the 3.11/3.12 line, which is a lead, not a finding — nobody has read the 3.10 versus 3.14 _abc source, and the fix should not rest on it.

What is being done

Routed to the agent that owns this module, together with the two open findings on this PR, for a single push: the root cause fixed and confirmed on a real 3.10 interpreter, the data keyword rename across the four makers with a construction-path test, and the two missing RFC 7563 citations. The steer given was to stop dispatching on isinstance in those four readers and dispatch on the enum code or type() as the rest of the module does — which removes the dependency on interpreter-specific ABC behaviour rather than working around it.

Everything the reviews verified against RFC text stands, and is worth keeping: the RFC 7411 word-length and RFC 5213 §8.8 timestamp traps, the RFC 6089 §8 and RFC 7563 §3.2 self-contradictions, the four nested registries round-tripping byte-identically on 3.14, and registry-read safety. This is a dispatch-mechanism failure that 3.14 happens to mask, not a wire-format error.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Reviewed at head 14a588fa4 (standing in for Copilot, out of tokens), focused on the two fix commits (c1c84087c and the origin/main merge 14a588fa4) per the request, rather than re-reviewing the whole registry from scratch.

CI status (checked first, per the standing note that earlier passes on this PR missed this)

gh pr view 437 --json statusCheckRollup: every check is COMPLETED/SUCCESS (or the expected SKIPPED for the docs gate and the Gate (full suite, Python 3.14) job), including Python 3.10 and Integration Python 3.10, which were red on the previous head. Three Integration checks (3.13/3.14/3.15) were still IN_PROGRESS when I first looked; re-checked a few minutes later and all had landed SUCCESS. No red anywhere on 14a588fa4.

The isinstance→code-based dispatch fix (c1c84087c)

Confirmed all eight nested helpers (_read_fid_suboptions, _read_ani_suboptions, _read_qos_attributes, _read_lmacontrolledmag's suboption reader, and their four _make_* counterparts) now dispatch on schema.type/code rather than isinstance against a sibling schema class, with the else/fallback branch still landing on the matching Unassigned* schema in every case.

One correction to the review brief: grep -n "isinstance(.*Schema" pcapkit/protocols/internet/mh.py at 14a588fa4 is not empty — it still matches 8 lines (1424, 7377, 7802, 8908, 9259, 9588, 9873, 10274; a 9th match at 4586 is inside the new docstring's own explanation, not code). I traced all 8: they're the codebase-wide "is this list item already bytes / an already-built Schema instance / a (code, kwargs) tuple" idiom (isinstance(item, (bytes, Schema)) and isinstance(schema, Schema)), checked only against the generic base Schema class or one specific top-level class (Schema_Packet, Schema_CGAParameter) — never against a sibling MH sub-option/option/message subclass the way the fixed dispatch code used to. git diff 076fd24f4 c1c84087c touches none of these 8 lines, and the identical lines (at their pre-fix line numbers) are already present unchanged in pcapkit/protocols/internet/mh.py at the pre-PR commit 44aa38ae8, and in 8 other protocol files (hip.py, sctp.py, tcp.py, ipv4.py, hopopt.py, ipv6_opts.py, pcapng.py x2, protocol.py). So this is the pre-existing, filed-as-#439 defect surface, not something this PR introduced or missed — but the grep genuinely isn't empty, and per the docstring this PR added (the ListField.pack note), that generic isinstance(x, Schema) idiom is exactly the kind of check #439 says can still be poisoned by unrelated code elsewhere in the process on 3.10. Nothing here demonstrates that happening — the full suite is green on 3.10.20 (below) — but it's worth knowing the grep doesn't actually come back clean before treating #439 as fully contained to code this PR touched.

The dataoption kwarg fix (c1c84087c)

Confirmed the rename is complete: _make_qos_attribute, _make_fid_suboption, _make_ani_suboption, _make_lcmp_suboption all take the model parameter as option, matching every _make_opt_*/_make_msg_*/_make_ext_* in the module (checked all of them — none use a bare data parameter). Every call site (entries.append(self._make_fid_suboption(code, **args)) and the four [self._make_X(code, option) for code, option in ...] list comprehensions) matches the new signature; no call site still passes data=. The new test test_mh_nested_suboptions_build_from_raw_kwargs covers the fixed vendor-specific QoS attribute plus the data-field fallback on all four families plus the selector-named sibling, and passes on both interpreters.

RFC 7563 citations

Fetched the RFC text directly. §3.1 is "Civic-Location Sub-Option", §3.2 is "MAG-Group-Identifier Sub-Option" (already cited before this PR), §3.3 is "ANI Update-Timer Sub-Option" — the two new citations are on the right sections. The retained RFC 4776 §3.1 encoding-format cross-reference and the "one time unit is 4 seconds" note also match the RFC text verbatim.

What I ran (tree: this worktree checked out to 14a588fa4; confirmed via pcapkit.__file__ on every run)

  • PYTHONSAFEPATH=1, examples/generators/make_samples.py regenerated first.
  • 3.14.7 (/local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python), full suite: 890 passed, 17 skipped, 1068 subtests, 0 failed in 698s — matches the claimed numbers exactly.
  • 3.10.20 (/tmp/mh310-a3a0ca27/venv, PYTHONPATH forced to this tree and pcapkit.__file__ checked to rule out the tree it has installed against main), full suite: 819 passed, 88 skipped, 970 subtests, 0 failed in 429s — also matches exactly.
  • test_mh_pmipv6_options_round_trip_byte_for_byte, test_mh_message_types_round_trip_byte_for_byte, test_mh_experimental_cga_extensions_round_trip individually on both interpreters: all pass, 103 subtests each run.
  • test_mh_unit.py alone on 3.10: 35 passed, 266 subtests (the commit message says 34/262; the file's test count is 35, so that number in the commit message is slightly stale — not a functional issue, the suite itself is green).

One more correction, minor: not all 88 skips on 3.10 are missing-optional-dependency. Two are the same code-reason skips already on main and already called out for the 3.14/17-skip accounting in an earlier review pass on this PR — tests/integration/test_output_formats.py:121 and tests/integration/test_pcapng_end_to_end.py:160, both "blocked on" upstream dictdumper bugs, unrelated to and untouched by this PR (git diff 44aa38ae8 14a588fa4 touches neither file).

Merge commit (14a588fa4)

docs/source/pep.rst is the only file the origin/main merge touched. Diffed 44aa38ae8:docs/source/pep.rst against 14a588fa4:docs/source/pep.rst: the only delta is this PR's own "Mobility Header" section rewrite (unchanged from 076fd24f4); origin/main's "More Protocols, More!!!" section changes are present verbatim. Zero conflict markers. Both sides preserved correctly.

Coverage

Covered: both fix commits in full (pcapkit/protocols/internet/mh.py, pcapkit/protocols/schema/internet/mh.py, tests/protocols/internet/test_mh_unit.py), the merge commit's only file, and CI. Did not re-review the original 076fd24f4 registry-completion diff beyond what the fix commits touch (message/extension dispatch tables, the CGA Parameters known-gap, RFC wire-format traps) — that was covered by the earlier review passes on this PR and was explicitly out of scope here.

Verdict

No defects found in the fix commits. Good to merge at 14a588fa4.

… 51 options, 3 CGA extensions

`MH` implemented the RFC 5568 fast-handover messages and the options they need;
everything else in the registry fell through to a generic handler and parsed as
opaque. This fills it in, in both directions.

* **All 24 registered message data types** are now dispatched. New: Home Agent
  Switch [RFC 5142], Heartbeat [RFC 5847], Binding Revocation [RFC 5846],
  Localized Routing Initiation and Acknowledgment [RFC 6705], Update
  Notification and its Acknowledgement [RFC 7077], Flow Binding [RFC 7109],
  Subscription Query and Response [RFC 7161].
* **70 of the 71 registered options** are now dispatched, spanning the PMIPv6,
  NEMO, flow-binding, QoS, multicast and DMM blocks, with nested sub-option
  registries for the flow identification, access network identifier,
  quality-of-service and LMA-controlled MAG parameter families.
* **All 4 CGA extensions**: the three RFC 4581 experimental types join
  Multi-Prefix. RFC 4581 gives their extension data no structure, so it is
  opaque by specification rather than undecoded.
* Two spec-only value sets go in `mh.py` rather than `pcapkit.const.mh`, since
  IANA registers neither: `LocalizedRoutingStatus` [RFC 6705] and
  `LMAAddressCode` [RFC 5949]. No new IANA enumeration or vendor crawler was
  needed -- every sub-registry was already generated.

Fixes two Multi-Prefix CGA extension construction defects found on the way:
`_make_ext_multiprefix` declared `1 + len(prefixes) * 16` data octets for a
payload of `4 + len(prefixes) * 8`, so two prefixes claimed 33 octets where 20
were emitted and a re-parse overran the extension; and it forwarded the data
model's `prefixes` tuple to a `ListField`, which rejects tuples, so a parsed
extension could not be re-made at all.

Every new message type, option and extension round-trips byte-for-byte, and the
14 sample captures produce identical tree and json output. The CGA Parameters
option (type 12) is left on the generic handler and documented: it cannot be
parsed on `main` either, and both halves of the fault are in shared field
machinery rather than in the mobility header.

Suite: 889 passed, 17 skipped, 1064 subtests (was 859/17 with 844 subtests).
mypy unchanged at 128 errors in 41 files; pylint errors and fatals unchanged
at 78.
Fixes six Python 3.10 failures in the new nested sub-option handlers. One root
cause, not six.

Every schema class descends from `collections.abc.Mapping`, and on Python 3.10
and older they do **not** each get their own `_abc_impl` -- they share
`Schema`'s. A single `abc.ABCMeta` cache therefore serves the whole family, and
it is keyed only on the class being *tested*, not on the class it is tested
*against*, so one answer poisons every later question about that class:

    >>> issubclass(ANIGeoLocationSuboption, Schema)                 # True, cached
    True
    >>> issubclass(ANIGeoLocationSuboption, ANINetworkIdentifierSuboption)
    True   # wrong -- the cached True for Schema is handed back

That produced both shapes of failure. A false positive sent a sub-option down a
sibling's branch (`AttributeError: ... has no attribute 'flags'`), and the
mirror case -- a correct `False` against a sibling, cached and then returned for
`Schema` -- made `ListField.pack`'s `isinstance(item, Schema)` false, so packing
a valid option raised `FieldValueError` and reconstruction emitted zeros where
payload belonged. Python 3.11+ gives each class its own cache, which is why this
was invisible on 3.14.

The eight nested helpers now dispatch on the sub-option type code, which is on
the wire, is what the registry keys on, is what every other dispatch in this
module already uses, and cannot be poisoned. The shared `_abc_impl` is
pre-existing and untouched here; no MH code depended on those checks before.

Also fixes a version-independent defect the round-trip tests could not see: the
four `_make_*_suboption` helpers took the data model as a parameter named `data`,
which shadowed the field of that name on the vendor-specific quality-of-service
attribute [RFC 7222 section 4.2.11]. A caller's `data=` bound to the parameter
instead of reaching `**kwargs`, so the payload was dropped with no exception and
the length written as though empty, while `vendor` and `subtype` arrived intact.
The parameter is now `option`, matching `_make_opt_*`. Covered by a new test on
the raw-kwargs construction path, which the round-trip tests cannot reach
because rebuilding from a parsed model takes the other branch.

And two missing citations: the Civic-Location and ANI Update-Timer sub-options
are RFC 7563 sections 3.1 and 3.3, as their MAG-Group-Identifier sibling already
recorded for section 3.2.

Suite on 3.10: 34 passed, 262 subtests in test_mh_unit (was 6 failed).
All 70 options, 24 message types and 4 extensions still round-trip
byte-identically on both 3.10 and 3.14.
… the wish list

The Help Wanted page tracks feature requests. Three of the things this PR added
to it are defects in code that already exists, which belong in the issue tracker
where they can be triaged, not in prose on a wish list where they cannot.

- The two faults blocking the CGA Parameters option are now #445 (a nested
  schema cannot reach the enclosing packet's fields by name) and #446 (a
  ForwardMatchField's non-consuming bytes count towards the schema's length).
  Both are shared field machinery, and both must be fixed for the option to
  parse -- which is why the half-fix was reverted. The pinning test stays.
- The MN-ID constructor mis-sizing is now #448. Measuring it turned out worse
  than reported: it sizes from the Python type of `identifier` rather than from
  `subtype`, so the method's own default arguments emit a 19-octet option
  declaring length 3.
- `http.HTTP`'s explicit `version=` path is now #447. That paragraph is edited
  to match what has landed on `main`, so the two do not conflict on merge; the
  UDP/TCP port asymmetry above it stays, being a decision rather than a bug.

The opaque-payload item stays, because it is a genuine request, and now records
the approach rather than only the situation: carry those payloads as `Raw`
rather than bare `bytes`, dispatched through a per-payload registry in the style
of `MH.__option__`, keyed on the field that already names the format. `Raw` is
what an unregistered dispatch falls back to everywhere else, so registering a
real dissector later needs no change at the option site. It deliberately does
not use `_decode_next_layer`: that is only called at a layer boundary and
appends to the frame's protocol chain, and an MLD record inside a mobility
option did not follow MH on the wire, so it would make `layer=` and `protocol=`
limits behave wrongly.

Docs only -- no code, no tests, no behaviour change.
@JarryShaw
JarryShaw force-pushed the feat/mh-registry-completion branch from 7a4a066 to d306dca Compare September 17, 2026 23:28
…d the CGA extension carrier

PR #440's round-trip harness enumerates the registries, so completing
`MH.__option__` and `MH.__extension__` added 64 cases to it -- and seven of them
were red. Two different reasons, and only one of them is a defect.

Four are the harness constructing an option with no arguments at all, which for
these four is not a well-formed instance of the option:

* Service Selection: `Length` of 0 "is not allowed" and the identifier is 1-255
  octets [RFC 5149 section 3].
* Redirect: "Both the 'K' and 'N' flags cannot be set or unset simultaneously"
  [RFC 6463 section 4.2], so with neither address given the option's own length
  is undetermined.
* Access Network Identifier: "MUST contain at least one ANI sub-option"
  [RFC 6757 section 3]; it is a pure container.
* LMA-Controlled MAG Parameters: likewise at least one sub-option
  [RFC 8127 section 3].

The constructors refuse all four correctly, so the validations stay and
`_mh_option_overrides` gains the arguments that make the codes reachable --
which is what that table is already for, alongside the eleven MH options whose
no-argument default is likewise invalid. This raises coverage rather than
avoiding anything: all four now round-trip carrying real content, e.g. the ANI
option emits `340f 010d 0004 77696669 06 001122334455`.

The other three are `mh-extension/Exp_FFF*`, and they are #445 rather than mine.
Measured: all four extension codes -- including `Multi_Prefix`, which the table
already records -- fail identically with `PARSE / KeyError: 'length'`, raised
from `SchemaField.unpack` at `corekit/fields/misc.py:619` by way of
`CGAParameter.extensions`, before any extension schema is unpacked at all. A CGA
extension has no carrier but the CGA Parameters option, so the whole registry is
unreachable until #445 and #446 land. Three `EXPECTED_FAILURES` entries record
that, grouped with the existing one and naming the same site; that entry's
`file:line` is also refreshed, since this branch moved the lambda from :516 to
:873.

`pep.rst` claimed all four CGA extensions round-trip byte-for-byte. Their
handlers do, when driven directly, but nothing can reach them through the public
API, so the claim is narrowed to what is actually true and points at the
recorded gap instead.

Suite on 3.14: 973 passed, 17 skipped, 1544 subtests. On 3.10: 899 passed, 91
skipped, 1446 subtests. Baseline e2d8ed6 on 3.14: 942 passed, 1256 subtests.
The option harness is 7 passed / 363 subtests on both interpreters, and
`make_samples.py` regenerates all 24 captures byte-identically.
@JarryShaw

Copy link
Copy Markdown
Owner Author

Pushed 8a4929ccf. The seven failures were two different things, and only three of them were a defect.

Four were the harness building an option with no arguments

Completing MH.__option__ and MH.__extension__ added 64 cases to #440's harness, and four of them constructed with no arguments at all — which for these four is not a well-formed instance of the option:

case why the no-argument default is invalid
Service_Selection_Mobility_Option Length of 0 "is not allowed"; the identifier is 1–255 octets — RFC 5149 §3
Redirect_Mobility_Option "Both the 'K' and 'N' flags cannot be set or unset simultaneously" — RFC 6463 §4.2, so with neither address the option's own length is undetermined
Access_Network_Identifier "MUST contain at least one ANI sub-option" — RFC 6757 §3; it is a pure container
LMA_Controlled_MAG_Parameters likewise at least one LCMP sub-option — RFC 8127 §3

So the four validations are correct and stay, and _mh_option_overrides gains the arguments that make those codes reachable — which is what that table already does for eleven other MH options whose no-argument default is likewise invalid ("the authenticator has to be a multiple of 8 octets", and so on). This raises coverage rather than avoiding anything: all four now round-trip carrying real content, e.g. the ANI option emits 340f 010d 0004 77696669 06 001122334455 — sub-option 1, net_name=b'wifi', a six-octet AP name — and reads back with those values intact.

Three were #445, not this PR

mh-extension/Exp_FFFD, Exp_FFFE, Exp_FFFF. Measured rather than inferred: all four extension codes — including Multi_Prefix, which EXPECTED_FAILURES already records — fail identically with PARSE / KeyError: 'length', raised from SchemaField.unpack (pcapkit/corekit/fields/misc.py:619, the {'__packet__': packet} call) by way of CGAParameter.extensions. That happens before any extension schema is unpacked, so it cannot be anything local to ExperimentalExtension; the three new codes simply were not yet in the table.

A CGA extension has no carrier but the CGA Parameters option, so the whole MH.__extension__ registry is unreachable until #445 and #446 land. Three entries added, grouped under the existing comment and naming the same site. I also refreshed that entry's file:line — this branch moved the lambda from :516 to :873, so the recorded citation had gone stale.

One doc correction

pep.rst claimed all four CGA extensions "round-trip byte-for-byte". Their handlers do when driven directly, but nothing can reach them through the public API, so that claim was an overstatement. Narrowed to what is true, pointing at the recorded gap.

Verification

Every run with PYTHONSAFEPATH=1 and pcapkit.__file__ printed to prove the tree.

passed skipped subtests
branch, 3.14.7 973 17 1544
branch, 3.10.20 899 91 1446
baseline e2d8ed6d1, 3.14.7 942 35 1256

Zero failures on either interpreter. The option harness is 7 passed / 363 subtests on both — identical counts, so nothing here is interpreter-conditional. make_samples.py regenerates all 24 captures byte-identically, and test_option_coverage_runtime.py passes with the regenerated options-internet.pcap.

Two notes on the baseline numbers, since they should not be taken at face value: the 3.14 baseline's extra 18 skips are test_tier_guard.py, which needs a real .git and my baseline is a git archive export — adjusted, 942 + 18 = 960, and the branch's 973 is exactly +13, the thirteen test methods this PR adds to test_mh_unit.py. The 3.10 skip count is high because that venv carries only the mandatory runtime deps, not the optional engines (dpkt, scapy, pyshark, pypcapfile, pypcap, pycrate, cryptography); all 91 are dependency skips, none for a code reason.

origin/main was still e2d8ed6d1 at push time and is fully merged in. Fast-forward, no force-push, no rebase.

Comment thread examples/generators/options.py
Comment thread tests/protocols/test_option_roundtrip_unit.py
Comment thread docs/source/pep.rst
@JarryShaw

Copy link
Copy Markdown
Owner Author

Standing in for Copilot (out of tokens). Reviewed 8a4929ccf89314f2ae0cc012bb66a2c8fe409afc, branch feat/mh-registry-completion. The GOOD TO MERGE verdict posted earlier at 14a588fa4 is superseded and void -- that sha no longer exists on this branch after the rebase, and this review covers the commit that landed after it.

Scope confirmation

  • git fetch origin refs/pull/437/head -> 8a4929ccf89314f2ae0cc012bb66a2c8fe409afc, exactly the sha above.
  • git rev-list --count refs/pull/437/head..origin/main -> 0. This branch is not behind main (e2d8ed6d1).
  • git show 8a4929ccf --stat: 3 files changed, 62 insertions, 5 deletions -- docs/source/pep.rst, examples/generators/options.py, tests/protocols/test_option_roundtrip_unit.py. No protocol code (pcapkit/**/*.py) touched, confirmed.
  • All code read via git show <ref>:<path>, never the ambient worktree.

CI

gh pr view 437 --json statusCheckRollup now shows 20 SUCCESS, 2 SKIPPED (Docs test gate, Gate (full suite, Python 3.14)), 0 pending, 0 failures. (There's also one empty StatusContext entry with null name/status/conclusion in the raw JSON -- not a real check, carries no data, ignored.) This is fully settled and green; the up-to-twelve-red-legs runs the task brief mentions are from older shas and are not current evidence.

The four validation/override cases -- verified against RFC text, not just read

Fetched the actual RFC text for all four citations:

  • RFC 5149 §3: "A value of zero (0) is not allowed" (Length), "The identifier string length is between 1 and 255 octets" -- matches.
  • RFC 6463 §4.2: "Both the 'K' and 'N' flags cannot be set or unset simultaneously" -- verbatim match.
  • RFC 6757 §3: "The Access Network Identifier mobility option MUST contain one or more Access Network Identifier sub-options" -- matches, and the option really is Type+Length+sub-options with nothing else.
  • RFC 8127 §3: "This option MUST contain one or more LCMP sub-options" -- matches, same pure-container shape.

Checked the actual validating code on this head (pcapkit/protocols/internet/mh.py): _make_opt_service (~8115-8118), _make_opt_redirect (~9095-9098), _make_opt_ani (~9386-9388), _make_opt_lcmp (~9977-9979) all raise exactly the quoted messages, and the logic in each (e.g. _make_opt_redirect's if (ipv6 is None) == (ipv4 is None): raise ...) correctly implements "exactly one required" / "at least one required." All four validations are RFC-correct, not over-strict -- keeping them and supplying real content via _mh_option_overrides is the right fix, and relaxing them would have been wrong. Detail and the ANI-hex reproduction are in the inline comment on examples/generators/options.py.

Confirmed the override mechanism is pre-existing (not invented here): _mh_option_overrides at afcedfaea (tip of the already-reviewed registry-completion work) already had entries. One correction: it's ten pre-existing entries, not the "eleven" the commit message and the "Pushed 8a4929c" comment claim -- minor, non-blocking, detailed inline.

The three EXPECTED_FAILURES additions -- reproduced, not trusted

Built the module's own pinned 40-octet, extension-free CGA Parameters payload and fed it to MH(..., extension=True) directly. Got KeyError: 'length' with a full traceback landing at pcapkit/protocols/schema/internet/mh.py:873 (CGAParameter.extensions's length lambda), reached via pcapkit/corekit/fields/misc.py:619 (SchemaField.unpack) -- before any extension type is even considered, since the test payload has zero extensions. This is airtight confirmation that the fault is shared CGAParameter/SchemaField machinery (issue #445, whose body matches this mechanism exactly), not anything local to Multi_Prefix or the three new Exp_FFFx codes. Grouping all four under one cause is the right call.

Also confirmed: the :516 -> :873 file:line refresh is the same lambda (checked both trees by line), and read all four other currently-open PRs against this repo (#449, #450, #451, #453) -- none fix issue #445's __packet__-nesting contract; #453 touches the adjacent mh.py:535 SchemaField declaration for an unrelated, explicitly-disclaimed-as-inert-here defect. No live conflict today. Full detail inline on tests/protocols/test_option_roundtrip_unit.py.

Docs

pep.rst's narrowed claim ("round-trip when driven directly, but nothing can reach them through the public API") checked against test_mh_experimental_cga_extensions_round_trip, which does drive the extension handlers directly and passes (9 subtests) -- accurate, and correctly narrower than the old blanket claim rather than now understating anything.

Measurements -- verified independently, not accepted on the PR's say-so

Environment: /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python 3.14.7 and /tmp/mh310-a3a0ca27/venv/bin/python 3.10.20, both with PYTHONSAFEPATH=1, PYTHONPATH pointed at the tree under test, pcapkit.__file__ printed and confirmed before every run. Fresh worktree, so examples/generators/make_samples.py was run first each time.

  • Option round-trip harness: tests/protocols/test_option_roundtrip_unit.py on this head gives exactly 7 passed, 363 subtests passed on both 3.14.7 and 3.10.20 -- matches the claim exactly, confirmed independently on both interpreters.
  • Full suite, branch, 3.14.7: 973 passed, 17 skipped, 1544 subtests passed in 794.92s -- exact match to the claimed 973/17/1544.
  • Full suite, baseline, 3.14.7: built a real git clone of e2d8ed6d1 (not an archive, per the task's own caution) at /tmp/pypcapkit-baseline-e2d8ed6, confirmed pcapkit.__file__ pointed there, and got 960 passed, 17 skipped, 1264 subtests passed in 595.70s. The PR's own archive-based baseline was 942/35/1256 with a claimed adjustment of 942+18=960 passed and 35-18=17 skipped for the 18 test_tier_guard.py skips that need a real .git. My real-clone baseline lands on exactly 960 passed and 17 skipped -- independent confirmation that the archive-vs-clone theory is correct, not just plausible. Branch-minus-baseline is 973-960=13, matching "the thirteen test methods this PR adds." One thing that does not fully reconcile: my clone-based baseline shows 1264 subtests, not 1256 -- an 8-subtest difference I can't explain from the archive/skip mechanism alone (skipped tests contribute 0 subtests either way), so I'm reporting it rather than smoothing it over.
  • 3.10.20 branch: did not run the full suite (see below) but confirmed pcapkit.__file__ resolves correctly under that venv (inserted the tree at sys.path[0]) and that only the 4 mandatory RUNTIME_DEPS (tbtrim, aenum, chardet, dictdumper) are importable there -- dpkt, scapy, pyshark, pypcapfile, pcap, cryptography are all absent -- consistent with "all 91 skips are dependency skips, none for a code reason."
  • make_samples.py: regenerated the whole capture set three times and sha256sum-diffed the results -- byte-identical every time, confirming that property. But the specific count is off: my run reports wrote 18 capture(s) (arp.pcap, ipv4.pcap, ipv6.pcap, tcp.pcap, stream.pcap, http.pcap, dhcp_big_endian.pcapng, dhcp_little_endian.pcapng, many_interfaces.pcapng, test.pcapng, profile.pcapng, test.pcap, http6.cap, options-internet.pcap, options-ipv4.pcap, options-ipv6.pcap, options-tcp.pcap, options-transport.pcap), not 24 -- and for what it's worth, this number has drifted across the PR's own history (14 -> 15 -> 19 -> 24 in successive comments), none of which match what I measured. Not a functional defect -- the byte-identical property is real -- just a number that's never been right and should probably stop being asserted precisely.
  • Skip-count discrepancy (17 vs 20, per the task brief): my environment gives 17 on both the branch and my real-clone baseline. I'm not offering a theory for why other environments would see 20, per the brief's own instruction not to invent one.

Verdict

GOOD TO MERGE at 8a4929ccf89314f2ae0cc012bb66a2c8fe409afc. No defects found in this commit. The two judgement calls it makes (keep-and-override the four validations; classify the three new extension failures as issue #445 rather than local) are both correct, and I reproduced the load-bearing evidence for each independently rather than trusting the PR's own account. The only issues found are cosmetic and non-blocking: the "eleven" pre-existing overrides should read "ten," and the "24 captures" / "byte-identical" claim should drop the specific count or recompute it -- neither affects correctness. CI is fully green with no red legs at this sha.

@JarryShaw
JarryShaw merged commit 489eef6 into main Sep 18, 2026
23 checks passed
@JarryShaw
JarryShaw deleted the feat/mh-registry-completion branch September 18, 2026 02:28
JarryShaw added a commit that referenced this pull request Sep 18, 2026
- main merged #437 (MH registry completion, including the four
  mh-extension codes and the _make_ext_multiprefix arithmetic fix) and
  #456/#446 (the ForwardMatchField double-count in Schema.__len__)
  since this branch's last merge. Combined with this PR's own fix,
  all four mh-extension/{Multi_Prefix,Exp_FFFD,Exp_FFFE,Exp_FFFF} cases
  now round-trip cleanly -- verified directly against the round-trip
  harness (all four return 'OK'), not assumed from the PR descriptions.
  Deleted their EXPECTED_FAILURES entries; a stale PARSE/KeyError
  expectation would otherwise have failed this module outright, per its
  own two-way assertion.
- The issue's own 40-octet CGA Parameters reproduction now parses
  completely end to end (confirmed directly: MH(raw, len(raw),
  extension=True) returns a populated CGAParametersOption, no
  exception). Rewrote test_cga_parameters_option_reaches_the_446_boundary
  _not_a_keyerror, which asserted the (now stale) FieldValueError
  boundary, as test_cga_parameters_option_now_parses_end_to_end,
  asserting the parsed fields directly.
- #437 had pinned the pre-fix KeyError as
  test_mh_cga_parameters_option_is_unparsable_upstream, explicitly so
  that "whoever fixes it finds out here" -- and it did: this run turned
  that test red once the merge above landed. Replaced it with
  test_mh_cga_parameters_option_now_parses, asserting the option parses
  and its fields are what the wire says, and fixed the now-stale
  cross-reference and claim in
  test_mh_pmipv6_options_round_trip_byte_for_byte's docstring (CGA_Parameters
  is still excluded from that test's cases, but no longer because it
  cannot be parsed -- that is now a separate, deliberate scope decision
  for whoever adds its full round-trip identity).
- Merged origin/main (0283a6d) with one conflict, in this exact
  region of tests/protocols/test_option_roundtrip_unit.py, resolved by
  re-deriving the correct entries from the actual post-merge behaviour
  rather than picking either side.

Verified: mypy pcapkit -> 123 errors/40 files (a fresh main, 0283a6d,
is 124 -- unchanged from before this merge). Round-trip harness: 7
passed, 363 subtests passed, 0 failed (up from 299 subtests before
#437 grew the mh-extension family to four codes). tests/protocols/
internet/test_mh_unit.py: 35 passed, 266 subtests passed, 0 failed.
Full local suite result to follow in the PR description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant