Skip to content

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

Merged
vxfemboy merged 16 commits into
mainfrom
feat/authenticated-reachability-37-m2
Jul 25, 2026
Merged

Authenticated reachability: endpoint roaming (M2) + signed rendezvous registration (#37)#100
vxfemboy merged 16 commits into
mainfrom
feat/authenticated-reachability-37-m2

Conversation

@vxfemboy

Copy link
Copy Markdown
Member

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 2b: rendezvous server registration is unauthenticated (registration-overwrite DoS) #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 Handshake anti-replay (timestamp) + authenticated endpoint learning #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.

vxfemboy added 15 commits July 23, 2026 15:25
…r on relearn

Relearning peers[idx].endpoint heals ingress demux/deobfuscation, but egress
datagrams are stamped from each epoch's DataPlane::peer_addr, not endpoint. So
a roam that updated endpoint alone left return traffic targeting the peer's
stale (post-rebind, dead) address — bidirectional recovery failed. relearn_endpoint
now also pushes the new source into the live EpochSet (current/next/previous) via
DataPlane::set_peer_addr. New unit test roam_redirects_egress_to_the_new_source.
…path

run-netns-roaming.sh: A (wildcard-bound, obf on) changes its on-wire source
mid-session while its session/keys/conn_tag stay put; B must relearn A's endpoint
from an authenticated inbound packet and redirect egress there. Asserts steady
ping A->B stays <=1% loss across the rebind and the relay was not used. Both
drivers, wired into integration.yml next to the anti-replay.34 money test.
… Important)

EpochSet::set_peer_addr updated current/next/previous but not rekey.target. A
RekeyInFlight captures its target from the pre-roam endpoint, and on completion
rekey_resp_core/rekey_init_core stamp the promoted epoch's peer_addr from that
target — so a roam coinciding with this node's in-flight initiator rekey would be
undone on the next rekey completion (a fresh ~120s black hole), and would not
self-heal (relearn_endpoint only fires when src != the already-updated endpoint).
set_peer_addr now also updates rekey.target. New test
set_peer_addr_redirects_current_and_in_flight_rekey.
Task 1 of the #37 signed-rendezvous-registration milestone: the wire codec
in crates/yip-rendezvous/src/proto.rs adds Message::RegisterSigned
{ record: yip_membership::Record } (Tag = 7) and an optional, backward-
compatible record: Option<Record> field on Message::PeerInfo.

Adding a required field to PeerInfo and a new Message variant breaks every
downstream construction/match site in the workspace (exhaustive matches,
struct literals). Beyond proto.rs + Cargo.toml, this commit also includes
mechanical, non-functional compile shims so `cargo test --workspace` and
`cargo clippy --workspace --all-targets` (the pre-commit gate) stay green:

- crates/yip-rendezvous/src/server.rs: PeerInfo's Lookup reply gets
  `record: None`; a `RegisterSigned { .. } => Vec::new()` no-op arm.
  Task 2 replaces both with real verify+store logic.
- bin/yipd/src/rendezvous.rs, bin/yipd/src/peer_manager.rs: PeerInfo match
  arms widened with `..`; PeerInfo constructors in tests get `record: None`.
  Task 5 threads the real record through for verification.
- bin/yip-rendezvous/tests/smoke.rs: same `..` widening in two match arms.

None of these shims add behavior; they only keep the enum change from
breaking compilation ahead of the tasks that implement the real logic.
… (review Minor)

verify_record proves a record is a valid member record but not that it is FOR the
peer being resolved (PeerInfo.node and record.node_id are independent on the wire).
Without the binding, a server could answer Lookup(Y) with a genuine record
belonging to some other member X and steer a probe for Y at an arbitrary address.
record_ok now also requires record.node_id == node. New test
peer_candidate_with_valid_record_for_wrong_node_is_dropped.
…nal review Minors)

- PeerInfo gains a 1-byte record-presence field, so a rootless Lookup reply is
  +1 byte, not literally byte-identical — corrected the spec to 'wire-compatible'
  (legacy decoders ignore it; obf masks it; no anti-DPI signature).
- deobf_ingress (a') trial is an unauthenticated keystream XOR, not AEAD-gated like
  the plaintext roaming loop it was compared to; corrected the doc comment to say
  the authenticated gate is downstream (inbound_open) and a rare trial false-positive
  is absorbed by FEC/ARQ + self-heals.

Comment/spec only, no logic change.
@vxfemboy
vxfemboy changed the base branch from feat/handshake-anti-replay-34 to main July 25, 2026 00:42
…y for up-to-date

# Conflicts:
#	bin/yipd/src/membership.rs
@vxfemboy
vxfemboy merged commit cf65aaa into main Jul 25, 2026
9 checks passed
@vxfemboy
vxfemboy deleted the feat/authenticated-reachability-37-m2 branch July 25, 2026 01:34
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.

2b: rendezvous server registration is unauthenticated (registration-overwrite DoS)

1 participant