Skip to content

fix(hip,ipv6-route): stop dropping the ENCRYPTED IV and RPL's pack-time cast (#556) - #561

Merged
JarryShaw merged 1 commit into
mainfrom
fix/556-dropped-keyword-and-wrong-cast
Sep 21, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/556-dropped-keyword-and-wrong-cast

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Closes #556. Two small defects flagged in review earlier this release and never filed.

1. HIP._make_param_encrypted dropped the IV. hip.py:3533 passed cipher= to Schema_EncryptedParameter, a keyword the schema does not declare. Neither spelling lands: __cipher__ is a pre_unpack packet-context key, not a field, so cipher= and __cipher__ both draw UnknownFieldWarning and are dropped. Every AES-cipher ENCRYPTED parameter built through make therefore packed without its IV. Flagged in #430's review and re-flagged in #434 as "carried across unchanged"; it survived because the schema warns rather than raising.

The fix sets the resolved cipher as a plain attribute after construction, so pack()'s packet.update(self.__dict__) carries it into pre_unpack, which now trusts it ahead of its own HIP_CIPHER sibling lookup — a standalone make call has no options list for that lookup to search anyway.

2. RPL.post_process cast a list as bytes. schema/internet/ipv6_route.py:207 cast self.addresses to bytes unconditionally, but it runs on every Schema.pack, not only after a parse, and a schema built via make (IPv6_Route._make_data_type_rpl) still holds addresses as list[bytes]. Slicing and re-joining that list as the concatenated buffer a parse produces raised at pack time. Reproduced during #489's review and left with only a code comment. The fix skips SRH decompression when addresses is not bytes.

Both defects share the shape that made them invisible: the schema layer warns where it could raise. Whether that should be fatal during make is deliberately not decided here — it has an eleven-site precondition in _make_mptcp_* and belongs in its own issue.

Fixing defect 2 unmasks two pre-existing defects rather than introducing one. With RPL.post_process no longer raising, the RPL round-trip case now reaches IPv6_Route._read_data_type_rpl's own length guard, pcapkit/protocols/internet/ipv6_route.py:612 (if header.length % 16 != 0:), and fails there instead: ProtocolError: IPv6-Route: [TypeNo 3] invalid format. That guard treats header.length (Hdr Ext Len) as octets when it is actually in 8-octet units — the same unit confusion #487 fixed for Source Route and Type 2. But the header the guard is judging is not well-formed either way: RPL's fixed area (cmpr_i + cmpr_e + pad) packs to 5 octets against the 4 RFC 6554 specifies (echoed in the method's own docstring diagram) — measured, it constructs to 41 octets against the 48 its own Hdr Ext Len of 5 declares. Filed as #564. Neither is fixed here: RPL addresses are also variable-length under cmpr_i/cmpr_e, so no fixed bound is obviously right even once the units and the field widths are both correct, and nothing has been checked against a real RPL capture. tests/protocols/test_option_roundtrip_unit.py's EXPECTED_FAILURES entry for ipv6-route-type/RPL_Source_Route_Header is re-pointed at these, not deleted, since the case still does not construct — only the reasons changed, and the entry now names both.

9 files, +190/-18, one commit, on top of 8cfd6ab01.

Provenance, verified rather than asserted. Earlier drafts of this PR body said the test evidence was missing because the authoring session was killed by a host OOM (from this branch's own coverage run -m pytest tests, which reached 36.9 GB RSS). That is no longer true. I reproduced both halves of the "unmasked, not introduced" claim directly, using plain pytest (never coverage run -m pytest tests) and reading exit codes from files rather than summary lines. A cross-review on a separate model reviewed this PR independently and reports having reproduced the same unmasking and the #564 field-width measurement on its own, with exit codes in its own comment on this PR:

  • On origin/main at 8cfd6ab01 (before this PR's fix), pytest tests/protocols/test_option_roundtrip_unit.py -k round_trip_is_identity exits 0 — the RPL case fails there exactly as the old EXPECTED_FAILURES entry recorded (does not appear to be an IPv4 or IPv6 address).
  • With this PR's fix applied but the old, unedited EXPECTED_FAILURES entry still in place, the same run exits 1 with exactly one SUBFAILED (ipv6-route-type/RPL_Source_Route_Header, 321 of 322 subtests passing), reproducing the CI failure verbatim: AssertionError: 'does not appear to be an IPv4 or IPv6 address' not found in 'ProtocolError: IPv6-Route: [TypeNo 3] invalid format'.
  • With the EXPECTED_FAILURES entry re-pointed as described above, the same run exits 0 again, with all 322 subtests passing.
  • The full CI "unit tier" command (pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py') exits 0: 1121 passed, 5 skipped, 256 warnings, 2704 subtests passed.
  • Building the RPL schema directly via IPv6_Route.make() and packing it (bypassing the read-back guard) confirms the RPL schema's fixed area is 5 octets where RFC 6554 gives 4, so a built header is wider than its own Hdr Ext Len #564 numbers above: Hdr Ext Len field = 5, actual packed total = 41 octets, (Hdr Ext Len + 1) * 8 = 48 octets.

EXPECTED_FAILURES had 54 entries before this PR and has 54 after — this re-points one entry, it does not add or delete any.

@JarryShaw
JarryShaw force-pushed the fix/556-dropped-keyword-and-wrong-cast branch from c0705bc to 9025729 Compare September 21, 2026 03:42
@JarryShaw

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES

Cross-model review (Opus 5; the PR was authored on Sonnet), head 9025729f2.

Both code fixes are correct, and I independently supplied the fails-without-the-fix proof the description says is missing. The EXPECTED_FAILURES re-point is right to be a re-point rather than a deletion — I proved the ProtocolError is unmasked, not introduced — and its status, fragment and cited line are all correct.

Two changes needed, one substantive:

  1. The entry's defect prose asserts something measurably false. It reads "the reader rejects a well-formed header". The header this case actually builds is not well-formed: it packs to 41 octets while its own Hdr Ext Len of 5 declares 48, because the RPL schema's fixed area is 5 octets (cmpr_i 1 + cmpr_e 1 + pad 3) where RFC 6554 — per the ASCII diagram in _read_data_type_rpl's own docstring — specifies 4 (CmprI 4b | CmprE 4b | Pad 4b | Reserved 20b = 32 bits). So there is a third pre-existing defect sitting behind the % 16 guard, and as written this entry tells the next person the header is fine and only the guard is wrong. Someone who fixes % 16 on the strength of it will find the case still red and no note explaining why. Please reword, and file the octet-width defect as its own issue so the entry can point at it.

  2. The description's stated shape is stale. It says "8 files, +166/-11, one commit, on top of 4c6b12189". Actual: 9 files, +180/-18, one commit, on top of 8cfd6ab01. The commit shape itself is now correct — the earlier merge commit is gone and this is properly rebased to one commit.

Also worth relaxing: the provenance caveat says "the fails-without proof this repository normally requires is therefore missing". It is no longer missing — I obtained it for both defects and both exit codes are in the detailed comment below.

Nothing else I checked disputes the change. Detail follows.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Detailed cross-review — #561 @ 9025729f2

Reviewer: Opus 5, per the standing rule that an agent-raised PR is cross-reviewed on a different model than the one that wrote it (this PR: Sonnet). Every measurement below was run on Python 3.14.7 with PYTHONSAFEPATH=1 and PYTHONPATH pinned to my worktree, asserting pcapkit.__file__ resolves inside it before importing anything else. Exit codes are read out of files, never off a summary line — pytest 9.1.1 prints a failing subtest's parent as PASSED, so a run with subtest failures can show zero FAILED lines.

The force-push from c0705bc84 to 9025729f2 changed only tests/protocols/test_option_roundtrip_unit.py, so the source-level measurements I took against the earlier head carry over verbatim; I re-ran both probes against 9025729f2 to confirm.


1. Defect 1 was fixed on the correct side — and the alternative would have moved the bug, not fixed it

This was the claim I most wanted to falsify, because picking the wrong side here relocates the defect. I was briefed that the schema field is __cipher__ and that the maker's keyword was simply misspelled. That framing is wrong, and the PR's is right.

EncryptedParameter's declared fields are exactly:

['type', 'len', 'reserved', 'iv', 'data', 'padding']

Neither cipher nor __cipher__ is among them. __cipher__ is only ever read, by the iv field's ConditionalField predicate. Constructing with each spelling in turn:

constructor keyword warning drawn IV in packed bytes
cipher=Cipher.AES_128_CBC UnknownFieldWarning: 'cipher' is not a valid field name absent
__cipher__=Cipher.AES_128_CBC UnknownFieldWarning: '__cipher__' is not a valid field name absent

Identical. Re-spelling the maker's keyword to __cipher__ would have changed nothing, and declaring it as a field would put a cipher ID on the wire that ENCRYPTED does not carry — the ID lives in the preceding HIP_CIPHER parameter. Setting it as a plain attribute so pack()'s packet.update(self.__dict__) carries it into pre_unpack is the only one of the three that works. It is also symmetric with the parse path, where post_process already does self.cipher = packet['__cipher__'].

The defect was worse than the description states. The dropped keyword did not merely omit the IV; it produced an internally inconsistent parameter:

02 81 00 18 | 00 00 00 00 | 'DATA' | 00 00 00 00
^^^^^ ^^^^^
type  len = 0x18 = 24

len declares 24 octets of value while only 8 are present. That is not a parameter missing a field, it is an unparseable one.

One thing I checked and found correct rather than suspicious: pre_unpack's new early return does packet.pop('cipher'), and Schema.pack calls it on a dict the caller may own. The pop is load-bearing, not sloppy — with two ENCRYPTED parameters in one packet where only the first carries a resolved cipher, leaving cipher in the shared context would silently hijack the second one's sibling lookup. Popping it is what keeps them independent.

2. Defect 2's test does exercise pack()

I was asked to confirm this specifically, since constructing an RPL schema does not reproduce the fault. It does: the test calls bytes(rpl_schema) and bytes(header), and Schema.__bytes__ calls self.pack() when __updated__ is set, which __update__ sets at construction. Both the direct schema level and the public make entry point are covered.

3. Fails-without-the-fix proof — supplied, since the description says it is missing

Reverted each fix in isolation, with the PR's tests in place. Restored from byte-identical copies afterwards and confirmed git status clean and md5sums matching in both cases.

tree test exit code (from file) failure
PR head, both fixes both affected test files 0 — 75 passed, 146 subtests —
HIP fix reverted to main -k encrypted_preserves_iv 1 UnknownFieldWarning("'cipher' is not a valid field name") + ProtocolWarning('HIP: [ParamNo 641] missing HIP_CIPHER parameter')
RPL fix reverted to main -k rpl_packs_a_multi_address 1 ValueError: [b' \x01\r\xb8…\x01', b' \x01\r\xb8…\x02'] does not appear to be an IPv4 or IPv6 address

Both defects are real and both tests genuinely pin them.

Minor: the HIP test's first assertion is assertEqual(caught, []), so in the reverted state it fails on the warning and never reaches assertIn(iv, packed) — the assertion that names the actual harm. Asserting the IV first would make the failure message say "the IV is gone" rather than "a warning fired". Not blocking; I confirmed the IV loss independently above.

4. The ProtocolError was unmasked, not introduced — proven

This is the part of the PR I was most sceptical of, and it holds up.

tree exit code (from file) result
main @ 8cfd6ab01 0 322 subtests passed
main + only the RPL schema fix (HIP untouched) 1 exactly one SUBFAILED(case='ipv6-route-type/RPL_Source_Route_Header'), detail ProtocolError: IPv6-Route: [TypeNo 3] invalid format
PR head 9025729f2 (full, with the re-point) 0 6 passed, 358 subtests

The middle row is the decisive one: 18 lines in one file, no HIP involvement, and the new failure appears. And the guard itself is untouched by this PR — if header.length % 16 != 0: is at line 612 and its raise at 613 on main and on the PR head alike, same line numbers. So the entry must be re-pointed rather than deleted, exactly as claimed.

The guard is genuinely broken, independently confirmed. Instrumenting it for the recorded case gives header.length (Hdr Ext Len) = 5, so 5 % 16 = 5 and it raises. Two lines below the guard, ipv6_route_header_length(header.length) converts from 8-octet units — which settles the unit question from the code itself. A % 16 != 0 test on a value in 8-octet units accepts only Hdr Ext Len ∈ {0, 16, 32, …}, i.e. it rejects very nearly every real RPL header. Same class of confusion as #487.

5. The re-pointed entry is correct in every field I could check

  • status='CONSTRUCT' — correct, and not obviously so. The raise comes from a _read_* method, but STATUSES' own docstring records that ProtocolBase.__post_init__ packs and then unpacks, "so a _read_* fault on a perfectly good pack also lands here". The harness did report CONSTRUCT.
  • fragment='IPv6-Route: [TypeNo 3] invalid format' — correct and specific, satisfying Gap.fragment's requirement of the alias plus the bracketed code, and not the bare 'invalid format' form the docstring warns occurs 205 times. I verified it pins one site: Routing.RPL_Source_Route_Header == 3, and the three sites in this file that print [TypeNo …] invalid format sit in _read_data_type_src (511), _read_data_type_2 (559) and _read_data_type_rpl (612/613) — each reachable only for its own routing type, so [TypeNo 3] can only come from 613's site.
  • defect line :612 — correct. 612 is the condition (the cause), 613 the raise, matching the convention in neighbouring entries, which name the cause (e.g. the TCP entry cites tcp.py:2506 -- _make_mode_timeout sets length=3).
  • It also fixes a pre-existing error. The old entry cited schema/internet/ipv6_route.py:156, which is SourceRoute.ip — a different class in a different file. The real defective cast was at line 208 (post_process begins at 198). That entry had been wrong by ~52 lines and the wrong file; the re-point corrects both.

6. The one substantive problem: "well-formed" is false

The new defect string reads "the reader rejects a well-formed header". Measured, on the exact case the entry records — the harness builds it from {'ip': ['2001:db8::1', '2001:db8::2']} with next=TransType.UDP, which is precisely what I reproduced (the leading 11 octet matches):

packed:  11 05 03 00 | 00 00 | 00 00 00 | <32 octets of addresses>
         next=UDP       cmpr_i cmpr_e  pad(3)
         Hdr Ext Len = 5
         actual packed octets = 41
         octets declared by Hdr Ext Len = 8 + 8*5 = 48   -> inconsistent by 7

Per-field widths from the schema's own buffer: cmpr_i 1, cmpr_e 1, pad 3, addresses 32, padding 0. The fixed (non-address) area is therefore 5 octets. RFC 6554 — as drawn in _read_data_type_rpl's own docstring — lays that area out as a single 32-bit row:

| CmprI | CmprE |  Pad  |               Reserved                |

CmprI and CmprE are 4 bits each and share one octet; the area is 4 octets, not 5. A spec-correct two-address RPL header is 4 + 4 + 32 = 40 octets, i.e. Hdr Ext Len = (40-8)/8 = 4. The schema emits one octet too many, which is also why the declared length cannot be made consistent.

So there is a third pre-existing defect behind the % 16 guard, and the header being rejected is not well-formed. This matters because the defect field is, in the table's own words, "the field that makes the entry worth keeping rather than just silencing": as written it directs the next person to fix % 16 and implies that will close the cycle. It will not. Please reword to say what is actually true — the guard is wrong and the header it is handed is malformed independently — and file the octet-width defect so the entry can cite it.

I am flagging this as a prose/accuracy problem in the entry, not as a demand that this PR fix the RPL round trip. Leaving that unfixed here is the right call and the entry says why convincingly.

7. Other checks

  • Changelog is generated, not hand-written. python util/changelog_md.py --check → exit 0, "CHANGELOG.md is in step with docs/source/changelog/1.5.0.rst". This is the check CI runs as Changelog drift.
  • Commit shape — one commit, 9025729f2, on top of current main 8cfd6ab01, no merge commit. (The earlier head c0705bc84 was a merge commit despite the description claiming one rebased commit; the force-push fixed that.)
  • Scope — pcapkit/protocols/internet/ipv6_route.py is in the diff as a comment rewrite only; it does not touch the guard. Consistent with the description.

Could not verify

  • CI. Reached deliberately on local evidence only, per my brief; I did not wait on GitHub Actions and make no claim about its tally.
  • Behaviour on any interpreter other than CPython 3.14.7. Everything above is single-version.
  • Real-capture RPL behaviour. Like the PR, I have no real RPL capture. My octet-width finding is derived from the module's own RFC 6554 diagram and the schema's packed field widths, not from wire data.
  • Whether the HIP fix is correct for a multi-ENCRYPTED-parameter packet parsed from a real HIP capture. I reasoned about the pop making siblings independent and believe it is right, but I exercised it only through make, not against a capture containing two ENCRYPTED parameters with different ciphers.

…me cast (#556)

Two small defects flagged in review this release and never filed:

- HIP._make_param_encrypted passed cipher= to Schema_EncryptedParameter,
  a keyword the schema does not declare. __cipher__ is a pre_unpack
  packet-context key, not a field either spelling reaches -- both draw
  UnknownFieldWarning and are dropped -- so every AES-cipher ENCRYPTED
  parameter built through make packed without its IV. Fix: set the
  resolved cipher as a plain attribute after construction; pack()'s
  packet.update(self.__dict__) carries it into pre_unpack, which now
  trusts it ahead of its own HIP_CIPHER sibling lookup (a standalone
  make call has no options list for that lookup to search anyway).

- RPL.post_process cast self.addresses to bytes unconditionally, but it
  runs on every Schema.pack, not only after a parse, and a schema built
  via make (IPv6_Route._make_data_type_rpl) still holds addresses as
  list[bytes]. Slicing and re-joining that list as the concatenated
  buffer a parse produces raised at pack time. Fix: skip the SRH
  decompression when addresses is not bytes -- nothing reads .ip on the
  make path.

Fixing the second defect exposes two pre-existing ones immediately
behind it, neither fixed here: IPv6_Route._read_data_type_rpl's own
`header.length % 16 != 0` guard (ipv6_route.py:612) treats Hdr Ext Len
as octets rather than 8-octet units, the same confusion #487 fixed for
Source Route and Type 2; and the header it is judging is not
well-formed either way, since RPL's fixed area (cmpr_i + cmpr_e + pad)
packs to 5 octets against the 4 RFC 6554 specifies, filed as #564. RPL
addresses are also variable-length under cmpr_i/cmpr_e, so no fixed
bound is obviously right even once both are fixed, and nothing has
been checked against a real RPL capture. The RPL_Source_Route_Header
entry in tests/protocols/test_option_roundtrip_unit.py's
EXPECTED_FAILURES is re-pointed at these, not deleted, since the case
still does not construct -- just for different, now-documented reasons.

New regression tests for both original defects, each shown to fail
without its fix. Build and full test suite pass.
@JarryShaw
JarryShaw force-pushed the fix/556-dropped-keyword-and-wrong-cast branch from 9025729 to d429404 Compare September 21, 2026 04:01
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE

Re-review after the force-push, head d42940414 (previously reviewed at 9025729f2). Cross-model: Opus 5; the PR was authored on Sonnet.

Both items from my earlier ❌ NEEDS CHANGES are resolved, and resolved properly rather than papered over:

  1. The false "well-formed" claim is gone, and the entry now records all three defects in the order they sit behind each other, with the measured numbers (5-octet fixed area against RFC 6554's 4; 41 octets packed against the 48 its own Hdr Ext Len declares). Better than what I asked for — I suggested rewording and filing the octet-width defect; you did both and filed it as RPL schema's fixed area is 5 octets where RFC 6554 gives 4, so a built header is wider than its own Hdr Ext Len #564 with the diagram citation, so the entry now points somewhere. That is the thing that makes the entry useful to whoever fixes % 16: they will now know the case still will not close afterwards, and why.
  2. The description's stated shape is now exact. I measured 9 files changed, 190 insertions(+), 18 deletions(-), one commit, on top of 8cfd6ab01 — matching the body verbatim. The provenance paragraph is also corrected rather than left overstating a gap that no longer exists.

Re-verified on this head:

check exit code (from file) result
tests/protocols/test_option_roundtrip_unit.py 0 6 passed, 358 subtests
test_hip_unit.py + test_ipv6_extension_unit.py 0 75 passed, 146 subtests
util/changelog_md.py --check 0 in step with 1.5.0.rst

And importantly: git diff c0705bc84 d42940414 -- pcapkit/ is empty, so the two force-pushes changed only the test table. Every source-level measurement in my earlier detailed comment — the __cipher__-is-not-a-field probe, the fails-without-the-fix exit codes for both defects, and the main vs main+RPL-fix-only unmasking proof — applies to this head unchanged. I did not have to take any of it on trust a second time, and I did not.

The one remaining nit from my earlier comment is unchanged and still not blocking: the HIP test asserts assertEqual(caught, []) before assertIn(iv, packed), so in a reverted tree it reports "a warning fired" rather than "the IV is gone". Worth swapping the order the next time that file is touched; not worth a revision now.

Nothing else to raise. Good work on #564 — that was the right call over folding a speculative RPL fix into this PR.

JarryShaw added a commit that referenced this pull request Sep 21, 2026
…tes (#540)

Follow-up to #508/#539/#552: bool is an int subclass, so a bare
ipaddress.IPv4Address/IPv6Address/ip_address call in a _make_* helper
silently laundered True/False into 0.0.0.1/::1 instead of raising. Four
sites were deliberately left out of #539 because their files were owned
by other work at the time; all four are now routed through the existing
parse_ip_address helper, the same pattern #539 and #552 used.

- ARP._make_proto_resolve (pcapkit/protocols/link/arp.py): addr=True
  packed as 00000001 (IPv4) or ::1 (IPv6) with no exception.
- IPv6_Route._make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py):
  worse than a packed-address defect, since cmpr_i/cmpr_e are derived from
  the laundered value -- ip=[True] packed with cmpr_e=0 and an address of
  00000001 instead of raising.
- IPv6_Route.make's dst parameter: the most reachable of the four, on the
  public make() entry point; dst=True converted to ::1 silently.
- OSPF._make_id_numbers: latent, no production caller today, fixed anyway
  so it does not resurface the defect the moment one is added.

ARP and OSPF use self.__class__.__name__ rather than self.alias in the
FieldValueError message, because their alias properties read state
(_acnm, _version) that read() only assigns -- unavailable to a
construction-only instance that never went through read().

Beyond bool rejection, pinning version=6 on the two IPv6_Route sites is a
second, smaller behaviour change: both previously converted a plain
integer through the bare, family-inferring ipaddress.ip_address, so
ip=[258] packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only
header; it now packs the 16-octet IPv6 form (::102) instead, which is
what an IPv6-only header should hold regardless of what an int happens
to fit as an IPv4 address.

Overlaps PR #561 (open, unmerged) in ipv6_route.py, which touches
_read_data_type_rpl; this change stays inside make()/_make_data_type_rpl
so the conflict on merge should be trivial.

Four new tests, each shown to fail without its fix (FieldValueError not
raised). Full unit tier green: 1120 passed, 8 skipped, 2673 subtests
passed. CHANGELOG.md regenerated via util/changelog_md.py from
docs/source/changelog/1.5.0.rst.
@JarryShaw
JarryShaw merged commit 1e652a9 into main Sep 21, 2026
25 checks passed
@JarryShaw
JarryShaw deleted the fix/556-dropped-keyword-and-wrong-cast branch September 21, 2026 15:18
JarryShaw added a commit that referenced this pull request Sep 21, 2026
…tes (#540) (#568)

Follow-up to #508/#539/#552: bool is an int subclass, so a bare
ipaddress.IPv4Address/IPv6Address/ip_address call in a _make_* helper
silently laundered True/False into 0.0.0.1/::1 instead of raising. Four
sites were deliberately left out of #539 because their files were owned
by other work at the time; all four are now routed through the existing
parse_ip_address helper, the same pattern #539 and #552 used.

- ARP._make_proto_resolve (pcapkit/protocols/link/arp.py): addr=True
  packed as 00000001 (IPv4) or ::1 (IPv6) with no exception.
- IPv6_Route._make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py):
  worse than a packed-address defect, since cmpr_i/cmpr_e are derived from
  the laundered value -- ip=[True] packed with cmpr_e=0 and an address of
  00000001 instead of raising.
- IPv6_Route.make's dst parameter: the most reachable of the four, on the
  public make() entry point; dst=True converted to ::1 silently.
- OSPF._make_id_numbers: latent, no production caller today, fixed anyway
  so it does not resurface the defect the moment one is added.

ARP and OSPF use self.__class__.__name__ rather than self.alias in the
FieldValueError message, because their alias properties read state
(_acnm, _version) that read() only assigns -- unavailable to a
construction-only instance that never went through read().

Beyond bool rejection, pinning version=6 on the two IPv6_Route sites is a
second, smaller behaviour change: both previously converted a plain
integer through the bare, family-inferring ipaddress.ip_address, so
ip=[258] packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only
header; it now packs the 16-octet IPv6 form (::102) instead, which is
what an IPv6-only header should hold regardless of what an int happens
to fit as an IPv4 address.

Overlaps PR #561 (open, unmerged) in ipv6_route.py, which touches
_read_data_type_rpl; this change stays inside make()/_make_data_type_rpl
so the conflict on merge should be trivial.

Four new tests, each shown to fail without its fix (FieldValueError not
raised). Full unit tier green: 1120 passed, 8 skipped, 2673 subtests
passed. CHANGELOG.md regenerated via util/changelog_md.py from
docs/source/changelog/1.5.0.rst.
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Two dropped-keyword/wrong-cast defects flagged in review and never filed (hip.py:3533, ipv6_route.py:207)

1 participant