Skip to content

Preserve IPv6 scope zone in pairing addresses and peer URLs - #82

Open
mkalkere wants to merge 2 commits into
NVIDIA:mainfrom
mkalkere:fix/pairing-ipv6-link-local
Open

mkalkere wants to merge 2 commits into
NVIDIA:mainfrom
mkalkere:fix/pairing-ipv6-link-local

Conversation

@mkalkere

@mkalkere mkalkere commented Sep 14, 2026

Copy link
Copy Markdown

Description

Fixes #69: pairing failed after a correct PIN when the inviter connected over IPv6 link-local.

Root cause is not TLS, as first suspected -- the pairing client pins the server key (DER compare, no hostname verification), so the handshake was fine. The failure is visible in the reporter's log: outboundIP() rendered the local address with ip.String(), dropping the IPv6 scope zone. The inviter advertised [fe80::...]:14321 with no zone, so after the PIN was accepted the joiner's Completion Exchange POST died with no route to host -- the address was unroutable without %en0.

The fix, in services/nvpair-cluster-manager: (1) outboundIP now preserves the zone via a new scopedHost helper; (2) a new peerURL helper renders peer addresses into URLs through net/url, percent-encoding the zone per RFC 6874. The second part is required, not cosmetic: Go's url.Parse rejects a raw %en0 as an invalid URL escape, so preserving the zone alone would have traded a dial error for a parse error. Applied at all three raw string-concat URL sites: pairing POST (invite.go), removal notify (mtls.go), roster reconcile (roster_http.go).

Scope

Included: the three call sites above, pairing_linklocal_test.go, version bump, docs/pairing-ipv6-link-local.mdx. Excluded: the same raw-concat pattern in nvpair-workload-manager/broadcast.go:127 and nvpair-errors/peersync.go:280 -- same latent defect, different components, left for a focused follow-up.

Validation

  • go test -race ./... in services/nvpair-cluster-manager: pass, except TestReachableEndpointFirstLeadsWithTheAddressThatAnswers, which fails identically on pristine upstream (sandbox networking, unrelated to this change).
  • New tests (5): zone preserved through outboundIP, RFC 6874 rendering round-trips, all three call sites produce parseable URLs for zone'd addresses.
  • Toolchain: Go 1.26.8 (repo requires Go 1.25+), Linux sandbox. go vet clean, gofmt clean, node scripts/spdx-headers.mjs reports 0 missing headers on every branch.

Risk

  • URL rendering now percent-encodes the zone (%25en0); required for url.Parse to accept it. No behavior change for IPv4 or loopback addresses.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • New or existing tests cover the change.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded the validation commands and results above.
  • I bumped any affected component in services/versions.json, and described user-visible changes above so they reach the release notes.

When the inviter was reached over IPv6 link-local, outboundIP dropped the scope zone when advertising its address, so the joiner's Completion Exchange POST had no route and pairing failed after the PIN step. Keep the zone, and render peer addresses into URLs via net/url so the zone is percent-encoded per RFC 6874 (a raw % is rejected as an invalid URL escape).

Signed-off-by: mkalkere <14184493+mkalkere@users.noreply.github.com>
Signed-off-by: mkalkere <14184493+mkalkere@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant