Skip to content

Handshake anti-replay + authenticated endpoint (#34)#99

Merged
vxfemboy merged 12 commits into
mainfrom
feat/handshake-anti-replay-34
Jul 25, 2026
Merged

Handshake anti-replay + authenticated endpoint (#34)#99
vxfemboy merged 12 commits into
mainfrom
feat/handshake-anti-replay-34

Conversation

@vxfemboy

Copy link
Copy Markdown
Member

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 9a accept_rekey_init age gate (removed from epoch.rs). Consequences:

  • Replay refused — a captured Init with a stale ts is silently dropped (DispatchOut::None).
  • Retransmit / cached-response paths bypass the gate — steady-state retransmits and the 9a/9a follow-up: relay-path session rekey (relay-only sessions get no forward-secrecy rotation) #91 idempotent rekey are unaffected (they run before the gate on a seen ephemeral).
  • Endpoint learned only on a fresh acceptpeers[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.
  • Safe restart — a genuine restart (fresh ephemeral + newer ts) rebuilds; a replayed old Init cannot.

#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_handshake is 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

  1. TAI64N + msg1 payload framing helpers (handshake.rs, pure/unit-tested)
  2. Frame [ts‖cert] into msg1 build + responder strip
  3. Freshness gate replaces the age gate; endpoint-learning gated; by_tag eviction on rebuild
  4. 2b: path-switch re-initiation can half-open a session (needs rekey/anti-replay) #36 retirement — fresh-Init + freshness-gated rebuild; downgrade closed
  5. netns money tests (replay refused + no session disruption + restart recovery; 2b: path-switch re-initiation can half-open a session (needs rekey/anti-replay) #36 path-switch converges via rebuild) + CI, both drivers

Testing

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)

vxfemboy added 10 commits July 23, 2026 04:25
…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.
…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.
…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).
vxfemboy added 2 commits July 24, 2026 20:39
…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
@vxfemboy
vxfemboy merged commit 277a575 into main Jul 25, 2026
9 checks passed
@vxfemboy
vxfemboy deleted the feat/handshake-anti-replay-34 branch July 25, 2026 01:05
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.

Handshake anti-replay (timestamp) + authenticated endpoint learning

1 participant