Skip to content

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

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

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

Conversation

@mkalkere

@mkalkere mkalkere commented Sep 14, 2026 •

Copy link
Copy Markdown

Changelog title

Pairing over IPv6 link-local keeps the scope zone (fixes #69)

Changelog body

  • The address advertised during pairing now preserves the IPv6 scope zone, and peer URLs render it percent-encoded per RFC 6874, so pairing completes when the inviter is reached over link-local IPv6.

Bumps

  • services: patch
  • nvpair-cluster-manager: patch
  • nvpair-engine-manager: none
  • nvpair-errors: none
  • nvpair-job-scheduler: none
  • nvpair-manual-nodes: none
  • nvpair-node-info: none
  • nvpair-node-scanner: none
  • nvpair-node-settings: none
  • nvpair-proxy: none
  • nvpair-tui: none
  • nvpair-ui-broker: none
  • nvpair-workload-manager: none

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, release-intent bump declaration, 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. Follow-up analysis (2026-09-26): traced both data flows and a zoned hostport cannot actually reach either site -- every address there funnels through netpick.Candidates, which drops zoned addresses (net.ParseIP("fe80::1%eth0") is nil), and the fallbacks are mDNS hostnames, never IPs. The only zone-carrying path is the pairing path fixed here. No follow-up needed.

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 declared the component bump in the pair-release-intent:v1 block above (services/versions.json is automation-managed; CI rejects hand edits), and described user-visible changes so they reach the release notes.

@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia changed the base branch from main to develop September 21, 2026 21:56
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: Mallikh Kaula <mallikh@users.noreply.github.com>
Signed-off-by: Mallikh Kaula <mallikh@users.noreply.github.com>
@mkalkere
mkalkere force-pushed the fix/pairing-ipv6-link-local branch from 87427c1 to 912f8af Compare September 26, 2026 19:33
@mkalkere

mkalkere commented Sep 26, 2026 •

Copy link
Copy Markdown
Author

Rebased onto current develop like the others; versions.json bump moved to the release-intent block. No code changes needed for the rebase itself. One note: TestReachableEndpointFirstLeadsWithTheAddressThatAnswers fails in my sandbox, but it fails identically on pristine develop — I dug in and it's the sandbox network, not the code. This environment's egress proxy accepts TCP dials to 192.0.2.1:14321 (the TEST-NET-1 address the test assumes is unroutable), so reach.First "confirms" the unreachable candidate and the ordering never changes. Unrelated to this change. The five new link-local tests pass with -race.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants