Skip to content

fix(rekey): complete relay-path session rekey (#91)#92

Open
vxfemboy wants to merge 8 commits into
feat/rekey-9a-session-rotationfrom
feat/rekey-9a-relay-completion
Open

fix(rekey): complete relay-path session rekey (#91)#92
vxfemboy wants to merge 8 commits into
feat/rekey-9a-session-rotationfrom
feat/rekey-9a-relay-completion

Conversation

@vxfemboy

Copy link
Copy Markdown
Member

Closes #91. Stacked on #90 (base = feat/rekey-9a-session-rotation) — review #90 first.

What this does

9a rotates each peer's Noise-IK session ~120s for forward secrecy, but relay-reached peers were gated out of rekey scheduling (drive_rekey_schedule returned early on relay): rekey completion was only wired into the direct handshake handlers, so scheduling a relay rekey would have churned a never-completing ~1 Hz handshake (a DPI fingerprint). #91 wires relay-path completion, removes the gate, and proves it on the wire — so relay-only sessions now get forward-secrecy rotation too.

The security-critical idempotent logic is single-sourced, not duplicated: relay paths reorder more than direct, which makes 9a's ephemeral-keyed convergence fix more load-bearing there, and a copy would be free to drift from it.

Changes

  • Extract the rekey cores (c388cb4, b459dde) — rekey_init_core/rekey_resp_core(.., via_relay: bool) + push_rekey_egress; handle_rekey_init/handle_rekey_resp become thin via_relay=false wrappers. The direct path is byte-identical. push_rekey_egress takes the full EgressDatagram so the prime-emit preserves each packet's FEC fate (review fix — a collapsed fate would defeat GSO coalescing).
  • Wire the relay handlers (e60b7ba) — relayed_handshake_init's Established arm and relayed_handshake_resp route to the cores with via_relay=true. The cold-start cached_resp_init_eph dedup is preserved, so a cold-start Init retransmit still resends the cached Resp instead of being misread as a rekey.
  • Remove the gate (2fbe21e) — relay peers now schedule rekeys, emitting the Init via relay_wrap. A relay_wrap None skips that send only; the round stays in flight and retries.
  • netns relay-forced money test (b163430) — run-netns-rekey-relay.sh: three namespaces with no direct A↔B path, so traffic must traverse the blind relay. rekey_epoch_witness gained an opt-in YIP_WITNESS_UNWRAP_RELAY=1 mode that strips the RelaySend/RelayDeliver envelope before counting distinct on-wire ephemerals (env-gated, so the 9a direct test is unaffected). Wired into CI, both drivers.
  • Path-consistency fix (004c138) — see below.

Defect caught by the final review

Removing 9a's gate silently dropped an invariant it had been providing: relay and direct rekey completion were implicitly mutually exclusive. Without that, a direct peer completing a rekey via a relayed Init/Resp built the new epoch with peer_addr = server_addr() while peers[idx].relay stayed false — and on_tun decides relay-wrapping from peers[idx].relay, not the stamped address. Result: bare datagrams to the relay placeholder, dropped by the server, outbound black-holed for a full rekey interval. Reachable via a source-spoofed server address or a malicious relay, and with no attacker at all under asymmetric reachability (peer relays to us while we reach it directly).

Fixed by gating completion on peers[idx].relay at all four sites (fail-closed DispatchOut::None on mismatch). The relay flag only flips on a Handshaking → Established transition, never mid-session, so this cannot block a legitimate same-path rekey. Regression test direct_peer_ignores_relayed_rekey_resp was confirmed failing before the fix.

Verification

  • 222 unit tests, clippy -D warnings, cargo fmt — clean. Full workspace: 0 failures.
  • netns money test, both drivers (poll + io_uring), run live against the release binary: 0% packet loss (100/100) across ~10 rotations, relay-forwarded = 3147/3151 (the blind relay carried it), COMPLETED_ROUNDS = 10/10 distinct rekey rounds observed on the relayed wire.
  • Regressions green: 9a direct rekey netns (both drivers, 0% loss, 10 rounds); 2b relay cold-start netns (20/20 ping, relay-forwarded=1367).

Known deferred

  • A spoofed/stray relay [HandshakeResp] can abandon an in-flight rekey — rekey-liveness only (current untouched, session survives, rotation delayed). Rides with Handshake anti-replay (timestamp) + authenticated endpoint learning #34 (authenticated endpoint).
  • The relay rekey Init skips the cold-start junk burst; worth filing if an oracle probes mid-session rekey.
  • PQ-hybrid handshake is 9b.

No wire-format change; yip-crypto/yip-wire/handshake.rs untouched apart from reusing the read-only handshake::init_ephemeral helper.

vxfemboy added 8 commits July 19, 2026 02:58
Extract rekey_init_core/rekey_resp_core parameterized by RekeyEgress{Direct,Relay}
(single-source the 9a idempotent-ephemeral Critical fix); wire relayed_handshake_
init/resp to them; remove drive_rekey_schedule relay gate + restore relay-wrap Init
emit. Fail-closed; idempotency covers relay reordering. netns relay-forced money
test. Stacks on 9a (PR #90). Issue #91.
Extract rekey_init_core/rekey_resp_core (via_relay bool) behavior-preserving /
wire relayed_handshake_init/resp to them / remove drive_rekey_schedule gate +
relay-wrap Init emit / netns relay-forced money test. Single-sourced idempotent
convergence; fail-closed relay_wrap-None no-op.
…er the relay

Task 4: prove relay-only sessions rotate loss-free over the rendezvous
relay. run-netns-rekey-relay.sh forks run-netns-relay.sh's RELAY-FORCED
topology (A/B mutually unreachable except via R's blind relay) with
run-netns-rekey.sh's YIP_REKEY_INTERVAL_MS=2000 cadence, asserting (1)
relay-forwarded=<N> N>0, (2) <=1% loss across ~10 rotations, (3)
rekey_epoch_witness reports >=3 distinct completed rekey rounds.

rekey_epoch_witness.rs grows an opt-in YIP_WITNESS_UNWRAP_RELAY=1 mode
that strips the RelaySend (offset 33) / RelayDeliver (offset 17)
rendezvous envelope before applying the existing distinct-ephemeral
logic, so the direct-path 9a run-netns-rekey.sh is byte-for-byte
unaffected (verified by re-running it).

Wired into netns-tunnel-test in .github/workflows/integration.yml,
both drivers, alongside the sibling 9a rekey steps.
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