Conversation
… unit (#672) (#679) * `R1CounterParameter.counter` was a `UInt32Field` where :rfc:`7401#section-5.2.3` states eight octets twice -- "R1 generation counter, 8 bytes" in the diagram, "a 64-bit unsigned integer in network byte order" in the prose. The parameter declared the correct `len=12` and packed 12 octets where the RFC total is 16, four short at `4 (mod 8)`. Now `UInt64Field`, so both codes that reach the class are fixed: `R1_Counter` (128) and `R1_COUNTER` (129). RFC 5201 section 5.2.3 gives the same layout, so no version excused it (#672). * `_make_param_locator_set` wrote `len` as `sum(Locator.len)`, in the 4-octet units RFC 8046 section 4 gives `Locator Length`, where RFC 7401 section 5.2.1's `Length` counts bytes -- `4n` declared for `24n` octets of contents. Now `sum(8 + Locator.len * 4)`, the octets each locator record occupies (#679). * `LocatorSetParameter.padding` read the nested `Locator.len` rather than the parameter's, because `ListField` packs each nested schema into the enclosing packet context and `Schema.pack` opens with `packet.update(self.__dict__)`. The shadowed value is 4 for any IPv6 locator, so the old expression appended four octets at every locator count. `locator_set_len_callback` now snapshots the parameter's `Length` under `LOCATOR_SET_LEN` before the list packs, and `locator_set_padding_len` reads that, deferring to `parameter_padding_len` for the arithmetic (#679). * `_read_param_locator_set` reports `parameter_total_len(schema.len)`, the last of the 49 record lengths still on the pre-#651 expression (#679). * `_hip_overrides()` gives both R1_COUNTER codes a non-zero `counter`. A zero-valued field cannot discriminate a width defect from a correct one, and that is why the RFC-only fixture walk reported nothing on this parameter (#672). The two had to land together: the padding correction and the `Length` unit cancelled at homogeneous plain-IPv6 sets, so #651 and #664 excluded this one site deliberately. Measured across shapes on `f0999858e` and after -- empty 4 -> 8, plain n=1/2/5 32/56/128 unchanged, SPI n=1 35 -> 32, SPI n=2 63 -> 64, mixed 59/60 -> 56/56 -- every shape is now `11 + Length - (Length + 3) % 8`. The reader was starved by the same quantity: a one-copy `LOCATOR_SET` of n plain locators parsed one truncated locator and had its unconsumed 20n octets read as a second, fabricated parameter; all eight shapes now parse back exactly and repack identically. An RFC-only walk over `options-internet.pcap` goes from 2 violations over 91 records to 0 over 92, with a non-zero counter present so the 0 cannot be concealing a mis-stride. `EXPECTED_FAILURES` is byte-identical at 44 entries: `hip-parameter/R1_Counter` stays, its cause being the schema registry key (#690), not the width. Coverage stays 100% on all three HIP modules, 586 -> 623 subtests in the same scope. pylint, mypy and isort are at exact parity with `f0999858e`. Fixes #672 Fixes #679
GOOD TO GOCross-review verdict, posted by the authoring agent. Reviewing model: Sonnet (the author ran on Confirmed independently, not by agreement with the PR body
What it added that the review was not asked for
What it disputedOne finding, and it was correct. The PR body's opening verification bullet said "9 new test It also hit the documented pytest-9 trap while establishing that the tests fail on Stated as unverified, rather than folded away
Nothing found overturns the change. This PR remains unmerged and awaits the maintainer — no CI |
Fixes #672. Fixes #679.
Two wire-format defects in the HIP schema and protocol modules, fixed together. Both change the
octets pcapkit emits, so both are
breakingas well asfix.Why one PR and not two
They are independent defects and I considered splitting them. Three things decided it:
pcapkit/protocols/schema/internet/hip.py,pcapkit/protocols/internet/hip.pyandexamples/generators/options.py. Sequencing two PRs over the same hunks costs a rebase and invitesexactly the clobber that collapsing Re-serialising a parsed HIP PUZZLE or SOLUTION loses leading zero octets: a len=20 parameter rebuilds as len=6 #653/SOLUTION's Reserved octet is written as a PUZZLE Lifetime, and lifetime=0 escapes a bare ValueError from math.log2 #654/Neither the HIP PUZZLE nor SOLUTION builder consults version when sizing, so HIPv1 rejects what it builds #655 into one PR avoided.
options-internet.pcapfinds twoviolations, one per defect — the
R1_COUNTERphantom record and the emptyLOCATOR_SET. Fixingeither alone leaves the walk reporting a non-zero count, so neither PR could have stated a clean
result on its own.
HIP_COPIES' note is only truthful once both have landed. Its one-copy tally goes 44 → 46, andit takes both fixes to get there:
R1_COUNTERmovesCONSTRUCT→OKandLOCATOR_SETmovesMISMATCH→OK. Written twice, in two PRs, one of them would have been wrong when merged.#672 —
R1_COUNTERpacked 12 octets where the RFC total is 16R1CounterParameter.counterwas aUInt32Field. RFC 7401 §5.2.3 states the width twice — "R1generation counter, 8 bytes" in the diagram, and "contains a 64-bit unsigned integer in network byte
order" in the prose below it — and RFC 5201 §5.2.3 gives the same
Reserved, 4 bytes+ 8-octet layout,so no version excused it. The parameter declared the correct
len=12and packed eight of thosetwelve, for a record of 12 octets against the 16 that
Total Length = 11 + Length - (Length + 3) % 8requires. Both codes that reach the class are affected.
Measured at
counter=1, before and after:R1_Counterv100 80 00 0c 00000000 00000001— 12 octets,4 (mod 8)00 80 00 0c 00000000 0000000000000001— 16, alignedR1_COUNTERv200 81 00 0c 00000000 00000001— 12 octets,4 (mod 8)00 81 00 0c 00000000 0000000000000001— 16, alignedNo padding either way:
Length = 12plus the four header octets is already a multiple of eight, so thefour missing octets were the counter's, not the padding's. A lone
R1_COUNTERin a packet went fromProtocolError: HIPv2: invalid formatto parsing back with the counter intact — the two-copy pairing inexamples/generators/options.pyis what had concealed that, two four-octet shortfalls summing to eight.A counter above
2**32now survives the round trip at all, which is the assertion no amount of paddingcan satisfy.
_hip_overrides()now gives both codes a non-zerocounter, which is the other half of #672 and themore useful half. See "the fixture walk" below.
#679 —
LOCATOR_SETwas conformant only at one shape, by two wrongsTwo defects, neither fixable alone, which is why #651 and #664 excluded this one site deliberately
rather than accidentally:
paddingread the nestedLocator.len.ListFieldpacks each nested schema into the enclosingpacket context and
Schema.packopens withpacket.update(self.__dict__), so by the timepaddingwas evaluated — after the list, fields being packed in declaration order —
pkt['len']was the lastlocator's. That is 4 for any IPv6 locator whatever the count, so the old expression appended exactly
four octets every time.
lenwassum(Locator.len). RFC 8046 §4 givesLocator Length"in 4-octet units" and it coversonly the
Locatorfield; RFC 7401 §5.2.1'sLengthis "length of the Contents, in bytes". So4nwas declared for
24noctets of contents.4 + 24n + 4 = 24n + 8, and since24nis a multiple of eight the RFC total for a byte-countLengthof
24nis the same24n + 8. Hence conformance at n = 1, 2, 5 — and nowhere else.Shapes, not counts
Sampling counts within the homogeneous plain-IPv6 shape is how the cancellation stayed hidden: n = 1, 2
and 5 pass on
mainand would pass a half-fix too. Asserted against11 + Length - (Length + 3) % 8,written out locally in the test rather than imported from
parameter_total_len:lenbeforelenafterFour of the eight were not multiples of eight before. The three plain shapes are byte-for-byte unchanged
in length, which is the baseline a correct fix must not move — only the two
Lengthoctets differ,0004→0018at n = 1.This also independently reproduces the SPI and mixed figures (35 / 63 / 59 / 60) that #679's owner
recorded as #664's worker's measurement rather than their own, having hit
FieldValueError: invalid locator type or length. The reason is a rough edge left unfixed here:_make_param_locator_setsetslength = 5for anspibut leavestypeat 0, andlocator_value_selectoraccepts onlytype == 0, len == 4ortype == 1, len == 5— sospi=needs an explicittype=1. Noted at thetest's
SPIconstant.The reader consumed the same quantity — this is why #679 was a different class of change
locatorsisListField(length=lambda pkt: pkt['len']), andSchema.unpackreads exactlyfield.lengthoctets off the stream and hands only those to the list. WithLengthat4n, a set of nplain locators was offered
4noctets of a24n-octet contents. The expression was right and thequantity was wrong, so fixing the unit repairs parsing rather than breaking it:
SchemaWarning: packet length < 0, repack differslengthThe silent case is the sharpest: a one-copy plain
LOCATOR_SETpacket succeeded onmainwhileparsing as two parameters, the second fabricated out of the 20n octets never consumed, with a reported
lengthof 12 for a 32-octet record._read_param_locator_setnow reportsparameter_total_len(schema.len)— the last of the 49 record lengths still on the pre-#651 expression.How the shadowing is fixed
locator_set_len_callback, installed as thelocatorsListField'scallback, snapshots theparameter's
LengthunderLOCATOR_SET_LENwhen the field is resolved — before any nestedLocatorhas packed into the context.
locator_set_padding_lenreads that snapshot and defers toparameter_padding_lenfor the arithmetic, so the formula still lives in one place. The callback fireson both the pack and the unpack path, which is what makes one expression correct on both rather than
accidentally correct on the one where nothing shadows anything (
Schema.unpackhands each fieldpacket.copy(), so the shadowing is pack-only).The fixture walk is trustworthy again — and it did not say what #672 claimed
#672 cites an RFC-only walk over
options-internet.pcapreporting 0 violations over 92 records,becoming 1 when a counter is patched to
aabbccdd. A walker written from scratch for this PR — purestruct, no pcapkit import, stride taken solely from11 + Length - (Length + 3) % 8— does notreproduce those numbers, and the disagreement is worth more than the agreement:
f0999858e,counter = 0(as generated)frame 102: type 193 padding not zeroed: 00c10000f0999858e,counter = 0xaabbccddframe 101: type 0 padding not zeroed: aabbccddcounter = 0xaabbccddSo the "0 violations" baseline was never right: the empty
LOCATOR_SETat frame 102 —00 c1 00 00 00 c1 00 00, two four-octet records where11 + 0 - (0 + 3) % 8 = 8is required — isindependently detectable by a literal RFC walk and was there all along. Patching the counter takes it
from 1 to 2, not from 0 to 1. The record count is 91 rather than 92 because the
LOCATOR_SETframe'stwo copies collapse into one mis-parsed record, offsetting the phantom that fills a slot in the
R1_COUNTERframe; after the fix both frames yield two real records each and the count is 92.Cross-checked two independent ways that agree exactly — patching the generated bytes, and regenerating
from a tree whose
_hip_overrides()supplies the non-zero counter.The 0 is now a real 0, and specifically one that cannot be concealing a mis-stride, because
R1_COUNTERcarriesaabbccddrather than zeros. That is what the override in_hip_overrides()isfor, and why it is worth keeping now that the width is right: a zero-valued field cannot discriminate a
width defect from a correct one, which is the same trap that let the
SOLUTIONwidth (#608) survive aslong as it did.
EXPECTED_FAILURESdid not moveVerified by importing the table rather than grepping it, on both trees: 44 entries each,
byte-identical, three of them HIP.
hip-parameter/R1_Counterstays, and deliberately so — its causeis the schema registry key, not the width. At one copy on
mainthe width defect fires first and hidesit; at two copies the pair cancels the width and this is what fails, which is the setting the table runs
at and why its recorded gap reads
PARSE/"no attribute 'counter'". After this PR the registrydefect is what remains at both settings. Filed as #690, with the measurement, and not fixed here.
Follow-ups filed, not folded in
HIP_COPIEScan drop to 1. Measured 44 → 46 OK of 49 at one copy, and one copy and twocopies now give the identical set:
HOST_ID,HIP_TRANSFORMandR1_Counter, none of which a secondcopy helps. Left at two here because changing it halves every
hip-parameterframe in the fixture thewalk above reads, which would make these before-and-after figures incomparable.
R1CounterParameterregisterscode=129only, so HIPv1 code 128 parses as anUnassignedParameter.Lifetimeread is lossy below0x0c, since2^(t−32)s falls undertimedelta'smicrosecond resolution and the octet is unrecoverable. Unfiled, noted in fix(hip): pad HIP parameters to the record length RFC 7401 gives, not the contents (#651) #664, and untouched here —
nothing in this PR goes near it.
Verification
.venvCPython 3.14.7. Every measurement tookpcapkit.__file__from the tree under test and assertedit, having stripped the editable finder with
getattr(f, '__module__', '')— filtering ontype(f).__name__ortype(f).__module__strips nothing, indices 1-4 ofsys.meta_pathbeing classes.Measured
/local/.../worktrees/agent-a65b30a424080d398/pcapkit/__init__.pyfor "after" and agit archive f0999858eextraction for "before".test_hip_r1_counter_width_unit.py, 6 intest_hip_locator_set_length_unit.py-- fail on a pristinef0999858etree, plus the updatedtest_fields_ipaddress.pypin, for 11 distinct failing methods.None of them passes on
main. Exit code read from a file rather than a pipeline:rc=1, 41 failed /29 passed / 58 subtests passed. With the fix:
rc=0, 10 passed, 37 subtests passed.rc=0read from a file: the two new modules;test_hip_unit.py+test_fields_ipaddress.py+test_fields_collections.py(65 passed, 226 subtests);test_option_roundtrip_unit.py(6 passed, 360 subtests);tests/project/(119 passed, 471 subtests).protocols/internet/hip.py925 statements / 314branches,
schema/internet/hip.py384 → 389 statements / 28 branches,data/internet/hip.py118 — 0missed and 0 partial before and after. The changed lines in
protocols/internet/hip.pyalreadyexecuted, so the gain is in assertions rather than reach: 586 → 623 subtests over the same scope,
+37 from the two new modules.
coverage run -m pytest, notpytest-cov.exact parity with
f0999858eover the changed library files: the pylint message multiset isidentical after adding the
unused-argumentdisable the module's other callbacks carry, mypy reports"Success: no issues found in 2 source files" on both, and isort's two pre-existing
options.pyhunksare identical before and after (neither is in this diff —
_mh_option_overrides'import ipaddressand
scapy.all's import order).tests/protocols/internet/test_ip_runtime.pyandtest_ipv6_extension_runtime.pyfail 5 tests —identically on pristine
main, being runtime-tier cases that needmake samples, which was notrun in either tree.
The #664 exclusion guard is updated rather than deleted. It still passed unchanged, because
locator_set_padding_lenis notparameter_padding_len— but its prose and failure messages had becomefalse. It now classifies three buckets, requires the bespoke one to be empty, and pins both that
LOCATOR_SETis the single parameter on the snapshot callback and that the callback is installed on thelocatorsfield, so both ways of regressing the shadowing fix are caught.No changelog file on this branch. Bullet text is handed to the maintainer for #657.
I have not claimed anything about CI.