Milestone 9a (#9, PR #90) rotates each peer's Noise-IK session ~every 120s, but relay-reached peers are deliberately gated out of rekey scheduling (drive_rekey_schedule returns early on relay).
Why they're gated out
Task 4 wired rekey completion only into the direct handshake handlers (handle_handshake_resp/handle_handshake_init), not the relay ones (relayed_handshake_init/relayed_handshake_resp). If relay peers were scheduled for rekey, the handshake would never complete → abandon after HANDSHAKE_TOTAL_MS → immediately restart (epoch still past the age threshold) → a continuous ~1 Hz [HandshakeInit] stream forever = wasted relay bandwidth + a constant-cadence DPI fingerprint. Gating relay peers out is the correct minimal fix; it leaves relay-only sessions without forward-secrecy rotation.
The work
Wire relay-path rekey completion so relay-reached peers rotate too:
relayed_handshake_init (Established arm) → accept_rekey_init gate + EpochSet::install_next (mirroring the direct handle_rekey_init, incl. the idempotent ephemeral-keyed cached-resp resend added in the 9a Critical fix — relay reordering makes idempotency more important here).
relayed_handshake_resp (currently drops anything against a non-Handshaking peer) → EpochSet::promote_from_rekey for an Established peer with a rekey in flight.
- Remove the relay gate in
drive_rekey_schedule once completion is wired.
- Mind the relay
dst-placeholder (server_addr()) + reordering semantics that differ from the direct path.
- netns money test: a relay-forced topology (block direct/punch) that rotates loss-free over the relay.
Deferred from 9a to keep that milestone focused on the direct/punched path (the common case). See PR #90.
Milestone 9a (#9, PR #90) rotates each peer's Noise-IK session ~every 120s, but relay-reached peers are deliberately gated out of rekey scheduling (
drive_rekey_schedulereturns early onrelay).Why they're gated out
Task 4 wired rekey completion only into the direct handshake handlers (
handle_handshake_resp/handle_handshake_init), not the relay ones (relayed_handshake_init/relayed_handshake_resp). If relay peers were scheduled for rekey, the handshake would never complete → abandon afterHANDSHAKE_TOTAL_MS→ immediately restart (epoch still past the age threshold) → a continuous ~1 Hz[HandshakeInit]stream forever = wasted relay bandwidth + a constant-cadence DPI fingerprint. Gating relay peers out is the correct minimal fix; it leaves relay-only sessions without forward-secrecy rotation.The work
Wire relay-path rekey completion so relay-reached peers rotate too:
relayed_handshake_init(Established arm) →accept_rekey_initgate +EpochSet::install_next(mirroring the directhandle_rekey_init, incl. the idempotent ephemeral-keyed cached-resp resend added in the 9a Critical fix — relay reordering makes idempotency more important here).relayed_handshake_resp(currently drops anything against a non-Handshakingpeer) →EpochSet::promote_from_rekeyfor an Established peer with a rekey in flight.drive_rekey_scheduleonce completion is wired.dst-placeholder (server_addr()) + reordering semantics that differ from the direct path.Deferred from 9a to keep that milestone focused on the direct/punched path (the common case). See PR #90.