Handshake anti-replay + authenticated endpoint (#34)#99
Merged
Conversation
…d endpoint (#34) TAI64N wall-clock timestamp in the msg1 encrypted payload; a per-peer freshness gate (ts > last_accepted) that replaces the 9a age gate as the single 'build a new session' discriminator (retransmit/cached_resp paths bypass it). Gates endpoint-learning on a fresh Init (closes the replay hijack), enables safe session rebuild on a genuine restart, and RETIRES #36 by inverting it to fresh-Init + safe-rebuild (removes the ephemeral-preservation hack + the cached_resp_init_eph relay-adoption + the accepted downgrade tradeoff). No cleartext fingerprint; yip-crypto/yip-wire unchanged.
T1 handshake.rs TAI64N + payload framing helpers; T2 wire [ts||cert] into begin_handshake+drive_rekey_schedule (build) + responder parse (consume); T3 last_accepted_init_ts + accept_fresh_init + freshness gate replacing the 9a age gate + endpoint-gating + by_tag eviction; T4 #36 retirement (fresh-Init inversion + freshness-gated relay adoption, remove cached_resp_init_eph adoption + the downgrade tradeoff); T5 netns money tests (hijack refused, restart recovery, #36 rebuild) both drivers + CI.
…nt learned only on a fresh Init
…fensive path stale_replayed_init_is_rejected_and_endpoint_unchanged exercises the Established/rekey arm, where endpoint is never touched regardless of the ts check -- a vacuous assertion for the hijack fix. Add a sibling test that drives the actual Idle-arm accept_fresh_init guard: a dropped-to-Idle peer replaying a stale-ts Init from a spoofed source must not move endpoint. Verified this test fails (endpoint hijacked) if the guard is removed.
…ld; downgrade closed
…ebuild - run-netns-replay-hijack.sh (new): A/B establish over a punch topology (T forwards, like run-netns-punch.sh) plus a fourth attacker netns S. After a YIP_REKEY_INTERVAL_MS=2000 burn-in, S replays a captured, now-superseded rekey-round Init at B from its own address. Asserts (1) a steady ping A->B stays <=1% loss (the replay neither wedged nor corrupted B's live session with A), (2) B's stderr shows the freshness-gate marker (the discriminating check that the replay was refused), and (3) killing + restarting A (fresh ephemeral + newer ts) reconverges within a bounded ping window. Both drivers. Since A/B are Established the replay exercises the rekey admission path, which never writes endpoint; the cold-start endpoint-write arm the gate closes is unit-tested (stale_replayed_cold_start_init_does_not_hijack_endpoint). - run-netns-pathswitch-rehandshake.sh: the #36 fix is now inverted (a Punch->Relay retarget draws a FRESH ephemeral instead of preserving it), so the old DISTINCT_INIT_EPHEMERALS==1 assertion is wrong. Replaced with >=2 distinct ephemerals (proving the fresh draw) plus a steady-state >=98% delivery convergence check. - peer_manager.rs: accept_fresh_init now logs a one-line stderr marker on refusal ("stale/replayed Init refused (freshness gate)") so the new netns test can observe the refusal from outside the process; no wire/format change, ts still rides only inside the encrypted payload. - integration.yml: wire the new money test in next to its siblings (both drivers, same SKIP/[FAIL] honesty guards), install tshark (used to extract the captured Init's raw payload for replay).
…(final review M1)
…l-only Two CI flakes surfaced on the #99 run (neither a code regression): - obf_on_data_roundtrips_through_send_and_on_udp asserted a single leading nonce byte != the plaintext PacketType — 1/256-flaky (a random nonce byte can equal it). Replaced with a deterministic two-sample check that obfuscating the same (type, body) twice yields different wire bytes (fresh nonce → no constant signature), which is the actual anti-DPI property. - run-netns-pathswitch-rehandshake under the uring driver is flaky in CI: the punch->relay escalation can miss the fixed capture window (only 1 Init captured), the same discovery-timing sensitivity that already makes discovery/cert-revocation poll-only. The retarget logic is driver-independent (verified both drivers locally), so the test is now poll-only in CI.
…nch protection # Conflicts: # bin/yipd/src/peer_manager.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #34.
Adds a WireGuard-style anti-replay token to the Noise-IK handshake and makes endpoint learning authenticated, closing the on-path Init-replay endpoint-hijack and letting an Established responder safely rebuild on a genuine peer restart. This is the anchor that retires the #36 accepted-downgrade tradeoff.
Design
A 12-byte TAI64N wall-clock timestamp (8-byte BE
2^62+unix_secs‖ 4-byte BE nanos — byte-comparable = chronological) is prefixed to the msg1 cert payload and rides inside the encrypted Noise payload — no new cleartext field, no wire-format change, no anti-DPI fingerprint (yip-crypto / yip-wire untouched).Per-peer
last_accepted_init_ts+accept_fresh_init(ts > last)becomes the single "build a new session" discriminator, replacing the 9aaccept_rekey_initage gate (removed fromepoch.rs). Consequences:DispatchOut::None).peers[idx].endpoint = Some(src)is behind the gate on the cold-start arm; the rekey path never writes endpoint. Off-path replay can't hijack a peer's endpoint.#36 retired / inverted: on path escalation the peer now draws a fresh ephemeral (state→Idle →
begin_handshake) instead of preserving its in-flight one;retarget_handshakeis deleted; relay adoption is re-gated on!relay && last_accepted_init_ts.is_some() && new-ephemeral && accept_fresh_init. A replayed escalation Init can no longer downgrade a direct peer to relay.Tasks
handshake.rs, pure/unit-tested)[ts‖cert]into msg1 build + responder stripby_tageviction on rebuildTesting
yipdunit tests;clippy -D warnings+cargo fmtclean;#![forbid(unsafe_code)].YIP_USE_URING=1):run-netns-replay-hijack.sh(0% loss across the replay, freshness-gate refusal marker observed, restart reconverged) andrun-netns-pathswitch-rehandshake.sh(fresh-ephemeral rebuild, ≥98% convergence, relay-forwarded>0). Both wired into CI.Final whole-branch opus review: READY WITH FOLLOW-UPS, no Critical/Important — the admission tree, endpoint gating, #36 downgrade closure, and #41/#91 interactions all traced clean.
Follow-ups (non-blocking)
peers[idx].endpointis not relearned on the fresh-ts rebuild, so B's own initiator-role rekeys keep targeting the dead address until an A-initiated rekey recovers. Data plane never black-holes (direct egress is stamped fromDataPlane.peer_addr, notendpoint). Pre-existing; relearn endpoint on a fresh-ts rebuild.