Authenticated reachability: endpoint roaming (M2) + signed rendezvous registration (#37) #100

Merged
vxfemboy merged 16 commits from feat/authenticated-reachability-37-m2 into main 2026-07-25 01:33:57 +00:00
vxfemboy commented 2026-07-25 00:15:25 +00:00 (Migrated from github.com)

Closes #37. Resolves the M2 follow-up filed in #34's review.

One milestone, two subsystems, stacked on #34 (PR #99). Both are "don't trust unauthenticated source/registration information."

M2 — authenticated endpoint roaming (data plane)

A peer's endpoint now follows a legitimate address change (NAT rebind / mobility), gated on cryptographic authentication (WireGuard's model): endpoint moves only on a datagram that both decrypts and passes the AEAD replay window (a non-None EpochSet::inbound_open), and only for !relay peers. A replayed/spoofed packet fails and cannot move it. #34's Init-path anti-hijack is untouched (the Init path never calls inbound_open).

  • The relearn also redirects egress via EpochSet::set_peer_addr (current/next/previous DataPlanes and any in-flight rekey.target) — egress is stamped from DataPlane::peer_addr, not endpoint, so updating endpoint alone left return traffic targeting the dead address.
  • deobf_ingress gained a trial-key fallback so a roamed peer's obfuscated data is found when the endpoint==src fast-path misses.

Fixes the ~120s ingress black-hole a mid-session NAT rebind otherwise caused under the obfuscation path.

#37 — signed rendezvous registration (control plane)

The registration is a membership::Record (reuses Record::verify — cert-vs-roots + signature + node_id-binds-cert, so squatting is closed; no new crypto). A new RegisterSigned message carries it; PeerInfo gained an optional record.

  • Rooted server (--roots/--network-id) verifies before storing and drops legacy unsigned Register in mesh mode → the victim's real registration survives a forged-overwrite attempt. Rootless is unchanged.
  • Client sends RegisterSigned when membership is configured, and verifies a returned PeerInfo.record and binds record.node_id == node before probing (Option-3 defense-in-depth). Address integrity is still backstopped by the 2b handshake-commit invariant.

Testing

  • 259 yipd unit + 32 yip-rendezvous lib/bin tests; clippy -D warnings + fmt clean; #![forbid(unsafe_code)].
  • Two netns money tests, both drivers, discriminating assertions verified from ground truth:
    • run-netns-roaming.sh — a mid-session NAT rebind (obf on) recovers at 0% loss; a raw before/after check proves the egress redirect.
    • run-netns-registration-hijack.sh — a rooted server refuses a forged registration; a raw Lookup(A) probe returns A's real address before and after the attack (would flip to the attacker's address if the overwrite were accepted), and B↔A stays reachable.
  • Final whole-branch opus review: READY WITH FOLLOW-UPS, no Critical/Important; the authenticated-only roaming invariant (verified by enumeration of every endpoint/peer_addr writer) and the #37 verify-before-mutation invariant both hold.

Follow-ups (non-blocking, from the final review)

  • reg_seq restart residual: a client's registration counter resets to 0 on restart, so up to ~REG_TTL (~60s) of stale-rejected re-registrations until the old entry expires; a replay-capable eavesdropper can extend that within a capture window. Fix: wall-clock/TAI64N-seed reg_seq (mirrors #34's timestamp).
  • Mesh + record-less PeerInfo: a mesh client currently probes a candidate that carries no record without verification (a malicious server could omit it to skip defense-in-depth). Backstopped by server-side verify + handshake-commit; consider requiring a record in mesh mode.
  • TLS-front registration stays unsigned (separate tls_seen map, never the servable directory — not the overwrite-DoS surface). Consider signing it for completeness.

Stacked on #34 (#99) — merge after it. This milestone's design lives in docs/superpowers/specs/2026-07-23-authenticated-reachability-design.md.

Closes #37. Resolves the M2 follow-up filed in #34's review. One milestone, two subsystems, stacked on #34 (PR #99). Both are "don't trust unauthenticated source/registration information." ## M2 — authenticated endpoint roaming (data plane) A peer's `endpoint` now follows a legitimate address change (NAT rebind / mobility), gated on cryptographic authentication (WireGuard's model): endpoint moves only on a datagram that both decrypts and passes the AEAD replay window (a non-`None` `EpochSet::inbound_open`), and only for `!relay` peers. A replayed/spoofed packet fails and cannot move it. #34's Init-path anti-hijack is untouched (the Init path never calls `inbound_open`). - The relearn also **redirects egress** via `EpochSet::set_peer_addr` (current/next/previous DataPlanes **and** any in-flight `rekey.target`) — egress is stamped from `DataPlane::peer_addr`, not `endpoint`, so updating `endpoint` alone left return traffic targeting the dead address. - `deobf_ingress` gained a trial-key fallback so a roamed peer's obfuscated data is found when the `endpoint==src` fast-path misses. Fixes the ~120s ingress black-hole a mid-session NAT rebind otherwise caused under the obfuscation path. ## #37 — signed rendezvous registration (control plane) The registration **is** a `membership::Record` (reuses `Record::verify` — cert-vs-roots + signature + `node_id`-binds-cert, so squatting is closed; no new crypto). A new `RegisterSigned` message carries it; `PeerInfo` gained an optional `record`. - **Rooted server** (`--roots`/`--network-id`) verifies before storing and drops legacy unsigned `Register` in mesh mode → the victim's real registration survives a forged-overwrite attempt. Rootless is unchanged. - **Client** sends `RegisterSigned` when membership is configured, and verifies a returned `PeerInfo.record` **and** binds `record.node_id == node` before probing (Option-3 defense-in-depth). Address integrity is still backstopped by the 2b handshake-commit invariant. ## Testing - 259 `yipd` unit + 32 `yip-rendezvous` lib/bin tests; `clippy -D warnings` + `fmt` clean; `#![forbid(unsafe_code)]`. - Two netns money tests, both drivers, discriminating assertions verified from ground truth: - `run-netns-roaming.sh` — a mid-session NAT rebind (obf on) recovers at 0% loss; a raw before/after check proves the egress redirect. - `run-netns-registration-hijack.sh` — a rooted server refuses a forged registration; a raw `Lookup(A)` probe returns A's real address **before and after** the attack (would flip to the attacker's address if the overwrite were accepted), and B↔A stays reachable. - Final whole-branch opus review: **READY WITH FOLLOW-UPS**, no Critical/Important; the authenticated-only roaming invariant (verified by enumeration of every `endpoint`/`peer_addr` writer) and the #37 verify-before-mutation invariant both hold. ## Follow-ups (non-blocking, from the final review) - **`reg_seq` restart residual:** a client's registration counter resets to 0 on restart, so up to ~`REG_TTL` (~60s) of stale-rejected re-registrations until the old entry expires; a replay-capable eavesdropper can extend that within a capture window. Fix: wall-clock/TAI64N-seed `reg_seq` (mirrors #34's timestamp). - **Mesh + record-less `PeerInfo`:** a mesh client currently probes a candidate that carries *no* record without verification (a malicious server could omit it to skip defense-in-depth). Backstopped by server-side verify + handshake-commit; consider requiring a record in mesh mode. - **TLS-front registration stays unsigned** (separate `tls_seen` map, never the servable directory — not the overwrite-DoS surface). Consider signing it for completeness. Stacked on #34 (#99) — merge after it. This milestone's design lives in `docs/superpowers/specs/2026-07-23-authenticated-reachability-design.md`.
Sign in to join this conversation.
No description provided.