Skip to main content

yip_rendezvous/
server.rs

1//! Pure rendezvous/relay server state machine: soft-state registration with
2//! TTL, per-source rate limiting, and blind relay forwarding. No I/O — the
3//! `bin/yip-rendezvous` loop owns the socket and the clock.
4use std::collections::HashMap;
5use std::net::SocketAddr;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8
9use yip_membership::Record;
10
11use crate::proto::{Message, NodeId};
12
13/// Clock-skew grace applied when verifying a `RegisterSigned` record's
14/// embedded cert/signature window. Mirrors `bin/yipd/src/membership.rs`'s
15/// `CLOCK_SKEW_SECS` production default (300s / `YIP_CERT_SKEW_SECS`
16/// override) — this crate has no access to that binary-local constant, so
17/// the value is duplicated here rather than reused; keep the two in sync by
18/// hand if the #41 default ever changes.
19pub const REGISTRATION_SKEW_SECS: u64 = 300;
20
21/// Registration lifetime; clients refresh well within this.
22pub const REG_TTL_MS: u64 = 60_000;
23/// Hard cap on concurrent registrations (memory bound).
24pub const MAX_REGISTRATIONS: usize = 65_536;
25/// Hard cap on distinct source addresses tracked for rate limiting (memory
26/// bound). Set to 2x `MAX_REGISTRATIONS` as generous headroom for legitimate
27/// distinct sources (registered peers plus in-flight lookups/relays from
28/// addresses that never register) while still bounding memory against a
29/// flood of packets from many distinct (or spoofed) source addresses.
30pub const MAX_RATE_ENTRIES: usize = 131_072;
31/// Rate-limit window and per-source message cap within it.
32pub const RATE_WINDOW_MS: u64 = 1_000;
33pub const MAX_MSGS_PER_WINDOW: usize = 64;
34
35struct Reg {
36    addr: SocketAddr,
37    expiry_ms: u64,
38    last_counter: u64,
39    /// The signed directory record backing this registration, when it
40    /// arrived via `RegisterSigned` and verified. `None` for a legacy
41    /// unsigned `Register` (rootless servers only) — `Lookup` then serves
42    /// `record: None` in `PeerInfo`, same as before this task.
43    record: Option<Record>,
44}
45
46struct Rate {
47    window_start_ms: u64,
48    count: usize,
49}
50
51/// Freshness record for the TLS-front discriminator (`register_if_fresh_tls`),
52/// kept separate from `Reg` — a TLS peer has no meaningful `SocketAddr` (the
53/// caller synthesizes `0.0.0.0:0`) and must never be servable via the
54/// UDP-facing `regs` map.
55struct TlsSeen {
56    last_counter: u64,
57    expiry_ms: u64,
58}
59
60/// Soft-state rendezvous + blind relay. Keyed by `NodeId`.
61pub struct RendezvousServer {
62    regs: HashMap<NodeId, Reg>,
63    rates: HashMap<SocketAddr, Rate>,
64    tls_seen: HashMap<NodeId, TlsSeen>,
65    /// Blind-relay forward count. An `Arc<AtomicU64>` (not a bare `u64`) so a
66    /// front that lives OUTSIDE this mutex-guarded struct — the TLS tunnel in
67    /// `bin/yip-rendezvous/src/conn_tunnel.rs` — can bump the SAME counter via
68    /// a cloned handle ([`forwarded_handle`](Self::forwarded_handle)) without
69    /// acquiring the global `server` mutex per relayed frame (#68).
70    forwarded: Arc<AtomicU64>,
71    /// `Some((ca_pubkeys, network_id))` when this server is running "rooted"
72    /// (mesh mode): `RegisterSigned` records are verified against these
73    /// roots before being trusted, and legacy unsigned `Register` is
74    /// dropped outright (a mesh must not accept an unauthenticated
75    /// registration). `None` (via [`new`](Self::new)) is the rootless/legacy
76    /// mode, byte-identical to pre-#37 behavior.
77    roots_cfg: Option<(Vec<[u8; 32]>, [u8; 16])>,
78}
79
80impl RendezvousServer {
81    pub fn new(_now_ms: u64) -> Self {
82        Self {
83            regs: HashMap::new(),
84            rates: HashMap::new(),
85            tls_seen: HashMap::new(),
86            forwarded: Arc::new(AtomicU64::new(0)),
87            roots_cfg: None,
88        }
89    }
90
91    /// A rooted (mesh-mode) server: `RegisterSigned` records are verified
92    /// against `ca_pubkeys`/`network_id`, and legacy unsigned `Register` is
93    /// dropped (see [`roots_cfg`](Self::roots_cfg)).
94    pub fn new_with_roots(_now_ms: u64, ca_pubkeys: Vec<[u8; 32]>, network_id: [u8; 16]) -> Self {
95        Self {
96            regs: HashMap::new(),
97            rates: HashMap::new(),
98            tls_seen: HashMap::new(),
99            forwarded: Arc::new(AtomicU64::new(0)),
100            roots_cfg: Some((ca_pubkeys, network_id)),
101        }
102    }
103
104    pub fn forwarded_count(&self) -> u64 {
105        self.forwarded.load(Ordering::Relaxed)
106    }
107
108    /// A cheap clone of the shared forward-count handle, so a front that does
109    /// not hold this struct behind the `server` mutex — the TLS tunnel
110    /// (`bin/yip-rendezvous/src/conn_tunnel.rs`) — can bump the SAME counter
111    /// lock-free (#68). The returned handle and this server's `forwarded` are
112    /// one allocation; a `fetch_add` through either is visible via
113    /// [`forwarded_count`](Self::forwarded_count).
114    pub fn forwarded_handle(&self) -> Arc<AtomicU64> {
115        Arc::clone(&self.forwarded)
116    }
117
118    /// Record one relayed hop that happened OUTSIDE `handle`'s own
119    /// `RelaySend` match arm — specifically, the TLS-tunnel relay path
120    /// (`bin/yip-rendezvous/src/conn_tunnel.rs`'s `route`), which forwards
121    /// `RelaySend`->`RelayDeliver` directly over its own per-connection
122    /// `mpsc` channels and never calls `handle` at all. Without this, the
123    /// TLS-relay path would be invisible to `forwarded_count` even while
124    /// genuinely carrying traffic, since `forwarded` would only ever reflect
125    /// the UDP-facing relay half. Same best-effort semantics as the UDP
126    /// path's own increment: counted when a live destination route was
127    /// found and delivery was attempted, not gated on confirmed receipt.
128    pub fn record_relay_forward(&self) {
129        self.forwarded.fetch_add(1, Ordering::Relaxed);
130    }
131
132    /// True iff `node` has a live (unexpired) registration. Used by the TLS
133    /// front to distinguish an upgraded tunnel client from a decoy request.
134    pub fn is_registered(&self, node: &NodeId, now_ms: u64) -> bool {
135        self.regs.get(node).is_some_and(|r| r.expiry_ms > now_ms)
136    }
137
138    /// Returns `Some((ca_pubkeys, network_id))` if this server is in rooted (mesh) mode,
139    /// or `None` if running rootless.
140    pub fn roots_cfg(&self) -> Option<&(Vec<[u8; 32]>, [u8; 16])> {
141        self.roots_cfg.as_ref()
142    }
143
144    /// Verifies a `RegisterSigned` record's cert, signature, network ID, and
145    /// sequence freshness window against this server's `roots_cfg`.
146    pub fn verify_signed_record(&self, record: &Record, now_secs: u64) -> bool {
147        let Some((ca_pubkeys, network_id)) = self.roots_cfg.as_ref() else {
148            return false;
149        };
150        if record
151            .verify(ca_pubkeys, network_id, now_secs, REGISTRATION_SKEW_SECS)
152            .is_err()
153        {
154            return false;
155        }
156        let now_wall_ms = now_secs.saturating_mul(1000);
157        let skew_ms = REGISTRATION_SKEW_SECS.saturating_mul(1000);
158        let min_seq = now_wall_ms.saturating_sub(REG_TTL_MS.saturating_add(skew_ms));
159        let max_seq = now_wall_ms.saturating_add(skew_ms);
160
161        if record.seq < min_seq || record.seq > max_seq {
162            return false;
163        }
164        true
165    }
166
167    /// True iff `src` is within its per-window budget (and records the hit).
168    fn rate_ok(&mut self, src: SocketAddr, now_ms: u64) -> bool {
169        // At capacity, refuse to start tracking a brand-new source rather than
170        // growing the map unbounded (e.g. a flood of packets from many
171        // distinct/spoofed addresses): treat it as over-limit and drop it.
172        // Actively-tracked sources are never evicted mid-window by this
173        // guard, and `sweep` continuously frees entries whose window has
174        // aged out, so capacity is self-healing under normal load.
175        if self.rates.len() >= MAX_RATE_ENTRIES && !self.rates.contains_key(&src) {
176            return false;
177        }
178        let r = self.rates.entry(src).or_insert(Rate {
179            window_start_ms: now_ms,
180            count: 0,
181        });
182        if now_ms.saturating_sub(r.window_start_ms) >= RATE_WINDOW_MS {
183            r.window_start_ms = now_ms;
184            r.count = 0;
185        }
186        if r.count >= MAX_MSGS_PER_WINDOW {
187            return false;
188        }
189        r.count += 1;
190        true
191    }
192
193    /// Evict expired registrations. Call on a timer from the socket loop.
194    pub fn sweep(&mut self, now_ms: u64) {
195        self.regs.retain(|_, reg| reg.expiry_ms > now_ms);
196        // Rate windows are cheap; drop stale ones opportunistically.
197        self.rates
198            .retain(|_, r| now_ms.saturating_sub(r.window_start_ms) < RATE_WINDOW_MS);
199        // Same 60 s horizon as `regs`, so `tls_seen` is equally bounded.
200        self.tls_seen.retain(|_, s| s.expiry_ms > now_ms);
201    }
202
203    /// Register `node` iff the message is fresh: a first-seen/expired node, or a
204    /// counter strictly greater than the last accepted for a currently-live
205    /// registration. Returns true iff THIS call accepted it (fresh insert or a
206    /// counter-advancing refresh); false if rejected as stale/replayed or refused
207    /// at capacity. This is the discriminator's source of truth — no inference
208    /// from expiry timestamps.
209    pub fn register_if_fresh(
210        &mut self,
211        node: NodeId,
212        counter: u64,
213        src: SocketAddr,
214        now_ms: u64,
215    ) -> bool {
216        if let Some(existing) = self.regs.get(&node) {
217            if existing.expiry_ms > now_ms && counter <= existing.last_counter {
218                return false; // stale / replay
219            }
220        }
221        if self.regs.len() >= MAX_REGISTRATIONS && !self.regs.contains_key(&node) {
222            return false; // at capacity; refuse a brand-new id
223        }
224        self.regs.insert(
225            node,
226            Reg {
227                addr: src,
228                expiry_ms: now_ms.saturating_add(REG_TTL_MS),
229                last_counter: counter,
230                // Every insert (fresh or counter-advancing refresh) starts
231                // with no record here; the `RegisterSigned` arm in `handle`
232                // sets it via `get_mut` immediately after this call
233                // succeeds, so the record always tracks the latest
234                // verified registration rather than lingering from a
235                // previous one.
236                record: None,
237            },
238        );
239        true
240    }
241
242    /// Freshness gate for the TLS-front discriminator, kept SEPARATE from the
243    /// UDP-servable `regs` map (a TLS peer is not UDP-reachable and must not
244    /// appear in `Lookup`/`RelaySend` results with a bogus addr). Returns true
245    /// iff `counter` is fresh (first-seen/expired, or strictly greater than the
246    /// last accepted) for this node on the TLS path.
247    pub fn register_if_fresh_tls(&mut self, node: NodeId, counter: u64, now_ms: u64) -> bool {
248        if let Some(seen) = self.tls_seen.get(&node) {
249            if seen.expiry_ms > now_ms && counter <= seen.last_counter {
250                return false;
251            }
252        }
253        self.tls_seen.insert(
254            node,
255            TlsSeen {
256                last_counter: counter,
257                expiry_ms: now_ms.saturating_add(REG_TTL_MS),
258            },
259        );
260        true
261    }
262
263    /// Process one received message; return datagrams to send as `(dst, msg)`.
264    /// `now_ms` is the monotonic clock used for TTL/rate-limit bookkeeping
265    /// (unchanged from before this task); `now_secs` is WALL-CLOCK seconds,
266    /// used to validate a `RegisterSigned` record's embedded cert
267    /// window and sequence freshness window — the two are intentionally distinct
268    /// clocks and must not be conflated (a monotonic clock has no relation to
269    /// wall-clock UNIX timestamps).
270    pub fn handle(
271        &mut self,
272        src: SocketAddr,
273        msg: Message,
274        now_ms: u64,
275        now_secs: u64,
276    ) -> Vec<(SocketAddr, Message)> {
277        if !self.rate_ok(src, now_ms) {
278            return Vec::new();
279        }
280        match msg {
281            Message::Register { node, counter } => {
282                // A rooted (mesh) server requires every registration to be
283                // signed and verified against its roots; an unauthenticated
284                // legacy `Register` is dropped outright rather than trusted.
285                if self.roots_cfg.is_some() {
286                    return Vec::new();
287                }
288                self.register_if_fresh(node, counter, src, now_ms);
289                Vec::new()
290            }
291            Message::RegisterSigned { record } => {
292                if !self.verify_signed_record(&record, now_secs) {
293                    return Vec::new();
294                }
295
296                let node = record.node_id;
297                if self.register_if_fresh(node, record.seq, src, now_ms) {
298                    if let Some(reg) = self.regs.get_mut(&node) {
299                        reg.record = Some(record);
300                    }
301                }
302                Vec::new()
303            }
304            // `RegisterAuth` is TLS-Trojan-front only and verified by `conn.rs` using
305            // `obf_key`. Arriving over UDP, it must be silently dropped.
306            Message::RegisterAuth { .. } => Vec::new(),
307            Message::Lookup { node } => match self.regs.get(&node) {
308                Some(reg) if reg.expiry_ms > now_ms => {
309                    let peer_addr = reg.addr;
310                    let mut out = vec![(
311                        src,
312                        Message::PeerInfo {
313                            node,
314                            reflexive: peer_addr,
315                            record: reg.record.clone(),
316                        },
317                    )];
318                    // Tell the looked-up peer to punch back toward the requester.
319                    out.push((
320                        peer_addr,
321                        Message::PunchHint {
322                            node,
323                            reflexive: src,
324                        },
325                    ));
326                    out
327                }
328                _ => vec![(src, Message::NotFound { node })],
329            },
330            Message::RelaySend {
331                src: sender,
332                dst,
333                payload,
334            } => match self.regs.get(&dst) {
335                Some(reg) if reg.expiry_ms > now_ms => {
336                    self.record_relay_forward();
337                    vec![(
338                        reg.addr,
339                        Message::RelayDeliver {
340                            src: sender,
341                            payload,
342                        },
343                    )]
344                }
345                _ => Vec::new(), // dst unknown: drop
346            },
347            // Server never receives these (they are server->client); ignore.
348            Message::PeerInfo { .. }
349            | Message::NotFound { .. }
350            | Message::PunchHint { .. }
351            | Message::RelayDeliver { .. } => Vec::new(),
352        }
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::proto::{node_id, Message};
360    use ed25519_dalek::{Signer, SigningKey};
361    use getrandom::SysRng;
362    use rand_core::UnwrapErr;
363    use std::net::SocketAddr;
364    use yip_membership::Cert;
365
366    fn addr(s: &str) -> SocketAddr {
367        s.parse().unwrap()
368    }
369
370    /// Wall-clock seconds for `handle`'s `now_secs` param. The pre-#37 tests
371    /// below never touch cert/record validity, so any monotone value works;
372    /// reusing the test's `now_ms` value keeps each call site simple.
373    fn now_secs(t: u64) -> u64 {
374        t
375    }
376
377    /// Synthesize a distinct `SocketAddr` from an index, without relying on
378    /// string formatting (kept fast for large-`i` loops) or `as` casts.
379    fn synth_addr(i: u32) -> SocketAddr {
380        let a = u8::try_from((i >> 24) & 0xff).expect("byte in range");
381        let b = u8::try_from((i >> 16) & 0xff).expect("byte in range");
382        let c = u8::try_from((i >> 8) & 0xff).expect("byte in range");
383        let d = u8::try_from(i & 0xff).expect("byte in range");
384        SocketAddr::from((std::net::Ipv4Addr::new(a, b, c, d), 40_000))
385    }
386
387    #[test]
388    fn register_then_lookup_returns_observed_reflexive() {
389        let mut s = RendezvousServer::new(0);
390        let a = node_id(&[1u8; 32]);
391        let _b = node_id(&[2u8; 32]); // documents which peer looks A up; id itself unused
392                                      // A registers from its observed reflexive addr.
393        let out = s.handle(
394            addr("198.51.100.7:41000"),
395            Message::Register {
396                node: a,
397                counter: 1,
398            },
399            0,
400            now_secs(0),
401        );
402        assert!(out.is_empty(), "register produces no reply");
403        // B looks up A: gets A's reflexive via PeerInfo, and A gets a PunchHint
404        // carrying B's reflexive.
405        let out = s.handle(
406            addr("203.0.113.9:52000"),
407            Message::Lookup { node: a },
408            10,
409            now_secs(10),
410        );
411        // one reply to B (PeerInfo), one to A (PunchHint)
412        assert!(out.iter().any(|(d, m)| *d == addr("203.0.113.9:52000")
413            && matches!(m, Message::PeerInfo { node, reflexive, .. } if *node == a && *reflexive == addr("198.51.100.7:41000"))));
414        assert!(out.iter().any(|(d, m)| *d == addr("198.51.100.7:41000")
415            && matches!(m, Message::PunchHint { reflexive, .. } if *reflexive == addr("203.0.113.9:52000"))));
416    }
417
418    #[test]
419    fn lookup_unregistered_returns_notfound() {
420        let mut s = RendezvousServer::new(0);
421        let a = node_id(&[1u8; 32]);
422        let out = s.handle(
423            addr("203.0.113.9:52000"),
424            Message::Lookup { node: a },
425            0,
426            now_secs(0),
427        );
428        assert_eq!(
429            out,
430            vec![(addr("203.0.113.9:52000"), Message::NotFound { node: a })]
431        );
432    }
433
434    #[test]
435    fn ttl_expiry_evicts_registration() {
436        let mut s = RendezvousServer::new(0);
437        let a = node_id(&[1u8; 32]);
438        s.handle(
439            addr("198.51.100.7:41000"),
440            Message::Register {
441                node: a,
442                counter: 1,
443            },
444            0,
445            now_secs(0),
446        );
447        s.sweep(REG_TTL_MS + 1);
448        let out = s.handle(
449            addr("203.0.113.9:52000"),
450            Message::Lookup { node: a },
451            REG_TTL_MS + 2,
452            now_secs(REG_TTL_MS + 2),
453        );
454        assert!(matches!(out.as_slice(), [(_, Message::NotFound { .. })]));
455    }
456
457    #[test]
458    fn relay_forwards_to_registered_dst_and_counts() {
459        let mut s = RendezvousServer::new(0);
460        let a = node_id(&[1u8; 32]);
461        let b = node_id(&[2u8; 32]);
462        s.handle(
463            addr("198.51.100.7:41000"),
464            Message::Register {
465                node: a,
466                counter: 1,
467            },
468            0,
469            now_secs(0),
470        ); // A registered
471           // B relays a payload to A -> A gets RelayDeliver{src=B, payload}.
472        let out = s.handle(
473            addr("203.0.113.9:52000"),
474            Message::RelaySend {
475                src: b,
476                dst: a,
477                payload: vec![9, 9],
478            },
479            5,
480            now_secs(5),
481        );
482        assert_eq!(
483            out,
484            vec![(
485                addr("198.51.100.7:41000"),
486                Message::RelayDeliver {
487                    src: b,
488                    payload: vec![9, 9]
489                }
490            )]
491        );
492        assert_eq!(s.forwarded_count(), 1);
493    }
494
495    #[test]
496    fn relay_to_unregistered_dst_drops_no_forward() {
497        let mut s = RendezvousServer::new(0);
498        let a = node_id(&[1u8; 32]);
499        let b = node_id(&[2u8; 32]);
500        let out = s.handle(
501            addr("203.0.113.9:52000"),
502            Message::RelaySend {
503                src: b,
504                dst: a,
505                payload: vec![1],
506            },
507            0,
508            now_secs(0),
509        );
510        assert!(out.is_empty());
511        assert_eq!(s.forwarded_count(), 0);
512    }
513
514    #[test]
515    fn rate_limit_caps_messages_per_source_window() {
516        let mut s = RendezvousServer::new(0);
517        let a = node_id(&[1u8; 32]);
518        let src = addr("203.0.113.9:52000");
519        // Exceed the per-window cap; excess Lookups must produce no replies.
520        let mut replies = 0;
521        for _ in 0..(MAX_MSGS_PER_WINDOW + 10) {
522            replies += s
523                .handle(src, Message::Lookup { node: a }, 0, now_secs(0))
524                .len();
525        }
526        // Only up to the cap are serviced (each serviced Lookup -> 1 NotFound).
527        assert!(
528            replies <= MAX_MSGS_PER_WINDOW,
529            "rate limit must drop excess"
530        );
531    }
532
533    #[test]
534    fn rates_map_grows_with_distinct_sources_but_stays_within_cap() {
535        let mut s = RendezvousServer::new(0);
536        let a = node_id(&[1u8; 32]);
537        // Comfortably below MAX_RATE_ENTRIES: exercises normal growth and
538        // confirms the map only ever holds one entry per distinct source.
539        for i in 0..2_000u32 {
540            s.handle(synth_addr(i), Message::Lookup { node: a }, 0, now_secs(0));
541        }
542        assert_eq!(s.rates.len(), 2_000);
543        assert!(s.rates.len() <= MAX_RATE_ENTRIES);
544    }
545
546    #[test]
547    fn register_rejects_stale_or_equal_counter() {
548        let mut s = RendezvousServer::new(0);
549        let n = node_id(&[1u8; 32]);
550        let a = addr("10.0.0.1:41000");
551        // First registration at counter 5 is accepted.
552        s.handle(
553            a,
554            Message::Register {
555                node: n,
556                counter: 5,
557            },
558            0,
559            now_secs(0),
560        );
561        assert!(s.is_registered(&n, 0), "counter 5 accepted");
562        // Replay at counter 5 is rejected: a Lookup still resolves to the
563        // ORIGINAL addr, proving the stale Register did not overwrite it.
564        let a2 = addr("10.0.0.2:41000");
565        s.handle(
566            a2,
567            Message::Register {
568                node: n,
569                counter: 5,
570            },
571            1,
572            now_secs(1),
573        );
574        let out = s.handle(a, Message::Lookup { node: n }, 2, now_secs(2));
575        match &out[0].1 {
576            Message::PeerInfo { reflexive, .. } => assert_eq!(*reflexive, a),
577            other => panic!("expected PeerInfo, got {other:?}"),
578        }
579        // A greater counter is accepted and updates the addr.
580        s.handle(
581            a2,
582            Message::Register {
583                node: n,
584                counter: 6,
585            },
586            3,
587            now_secs(3),
588        );
589        let out = s.handle(a, Message::Lookup { node: n }, 4, now_secs(4));
590        match &out[0].1 {
591            Message::PeerInfo { reflexive, .. } => assert_eq!(*reflexive, a2),
592            other => panic!("expected PeerInfo, got {other:?}"),
593        }
594    }
595
596    #[test]
597    fn register_if_fresh_rejects_stale_and_same_counter() {
598        let mut s = RendezvousServer::new(0);
599        let n = node_id(&[1u8; 32]);
600        let a = addr("10.0.0.1:41000");
601        // First-seen node: always accepted.
602        assert!(s.register_if_fresh(n, 5, a, 0), "first-seen node accepted");
603        // Equal counter: rejected as a replay, even at the SAME now_ms.
604        assert!(
605            !s.register_if_fresh(n, 5, a, 0),
606            "equal counter at same now_ms is a replay"
607        );
608        // Lower counter: rejected as stale.
609        assert!(!s.register_if_fresh(n, 4, a, 1), "lower counter is stale");
610        // Strictly higher counter: accepted as a fresh refresh.
611        assert!(
612            s.register_if_fresh(n, 6, a, 2),
613            "higher counter advances the registration"
614        );
615    }
616
617    #[test]
618    fn register_if_fresh_refuses_new_node_at_capacity() {
619        let mut s = RendezvousServer::new(0);
620        for i in 0..MAX_REGISTRATIONS {
621            let idx = u32::try_from(i).expect("index fits u32");
622            let mut id = [0u8; 32];
623            id[..4].copy_from_slice(&idx.to_be_bytes());
624            let node = node_id(&id);
625            assert!(
626                s.register_if_fresh(node, 1, synth_addr(idx), 0),
627                "filling to capacity must accept each distinct node"
628            );
629        }
630        assert_eq!(s.regs.len(), MAX_REGISTRATIONS);
631
632        // A brand-new node arriving while at capacity must be refused.
633        let new_node = node_id(&[0xffu8; 32]);
634        assert!(
635            !s.register_if_fresh(new_node, 1, addr("198.51.100.50:9000"), 0),
636            "new node over capacity must be refused"
637        );
638        assert_eq!(
639            s.regs.len(),
640            MAX_REGISTRATIONS,
641            "map must not grow past the cap"
642        );
643
644        // An already-registered node's counter-advancing refresh must still
645        // succeed while the table is at capacity.
646        let mut existing_id = [0u8; 32];
647        existing_id[..4].copy_from_slice(&0u32.to_be_bytes());
648        let existing_node = node_id(&existing_id);
649        assert!(
650            s.register_if_fresh(existing_node, 2, synth_addr(0), 0),
651            "existing node's refresh must still succeed at capacity"
652        );
653    }
654
655    /// The TLS-front discriminator (`register_if_fresh_tls`) must never make a
656    /// node visible on the UDP-servable `regs` map — a TLS-connected peer has
657    /// no meaningful UDP reflexive addr, so leaking it in would hand out the
658    /// synthetic `0.0.0.0:0` to a UDP `Lookup` (I4).
659    #[test]
660    fn register_if_fresh_tls_does_not_populate_regs() {
661        let mut s = RendezvousServer::new(0);
662        let node = node_id(&[1u8; 32]);
663
664        assert!(
665            s.register_if_fresh_tls(node, 1, 0),
666            "first-seen node on the TLS path is accepted"
667        );
668
669        // The UDP path must have no idea this node exists.
670        let out = s.handle(
671            addr("203.0.113.9:52000"),
672            Message::Lookup { node },
673            0,
674            now_secs(0),
675        );
676        assert_eq!(
677            out,
678            vec![(addr("203.0.113.9:52000"), Message::NotFound { node })],
679            "a TLS-only registration must not be visible to UDP Lookup"
680        );
681        assert!(
682            !s.is_registered(&node, 0),
683            "TLS registration must not count as a UDP-servable registration"
684        );
685    }
686
687    /// The TLS freshness gate rejects a stale/equal counter and accepts a
688    /// strictly-greater one (mirrors the UDP-path freshness, on `tls_seen`).
689    #[test]
690    fn register_if_fresh_tls_rejects_stale_and_accepts_advance() {
691        let mut s = RendezvousServer::new(0);
692        let n = node_id(&[2u8; 32]);
693        assert!(s.register_if_fresh_tls(n, 5, 0), "first-seen accepted");
694        assert!(
695            !s.register_if_fresh_tls(n, 5, 1),
696            "equal counter rejected (replay)"
697        );
698        assert!(!s.register_if_fresh_tls(n, 3, 1), "lower counter rejected");
699        assert!(
700            s.register_if_fresh_tls(n, 6, 1),
701            "strictly-greater counter accepted"
702        );
703    }
704
705    /// `handle` returns nothing for messages the server only ever *sends*
706    /// (client-bound), never receives.
707    #[test]
708    fn handle_ignores_server_bound_only_messages() {
709        let mut s = RendezvousServer::new(0);
710        let n = node_id(&[3u8; 32]);
711        let a = addr("10.0.0.9:40000");
712        assert!(
713            s.handle(
714                a,
715                Message::RelayDeliver {
716                    src: n,
717                    payload: vec![1, 2, 3]
718                },
719                0,
720                now_secs(0),
721            )
722            .is_empty(),
723            "RelayDeliver is server->client only"
724        );
725        assert!(
726            s.handle(a, Message::NotFound { node: n }, 0, now_secs(0))
727                .is_empty(),
728            "NotFound is server->client only"
729        );
730    }
731
732    /// A source's per-window message budget resets once `RATE_WINDOW_MS` has
733    /// elapsed: a `Lookup` dropped while the window is full flows again after.
734    #[test]
735    fn rate_window_resets_after_interval() {
736        let mut s = RendezvousServer::new(0);
737        let a = addr("10.0.0.5:40000");
738        let n = node_id(&[4u8; 32]);
739        // Exhaust the window at t=0 (Lookups return NotFound but still count).
740        for _ in 0..MAX_MSGS_PER_WINDOW {
741            s.handle(a, Message::Lookup { node: n }, 0, now_secs(0));
742        }
743        assert!(
744            s.handle(a, Message::Lookup { node: n }, 0, now_secs(0))
745                .is_empty(),
746            "over budget within the window ⇒ dropped"
747        );
748        assert!(
749            !s.handle(
750                a,
751                Message::Lookup { node: n },
752                RATE_WINDOW_MS,
753                now_secs(RATE_WINDOW_MS)
754            )
755            .is_empty(),
756            "window reset after RATE_WINDOW_MS ⇒ reply flows again"
757        );
758    }
759
760    #[test]
761    fn rate_capacity_guard_blocks_new_source_but_services_existing() {
762        let mut s = RendezvousServer::new(0);
763        // Pre-fill the rates map to capacity with dummy tracked sources via
764        // direct field access (same module) -- a 131_072-iteration `handle`
765        // loop would be needlessly slow; this exercises the same guard.
766        for i in 0..MAX_RATE_ENTRIES {
767            let idx = u32::try_from(i).expect("index fits u32");
768            s.rates.insert(
769                synth_addr(idx),
770                Rate {
771                    window_start_ms: 0,
772                    count: 0,
773                },
774            );
775        }
776        assert_eq!(s.rates.len(), MAX_RATE_ENTRIES);
777
778        let a = node_id(&[1u8; 32]);
779
780        // A brand-new source arriving while at capacity must be treated as
781        // rate-limited (dropped) rather than growing the map further. Without
782        // the capacity guard this Lookup would be serviced (regs is empty,
783        // so it would return a NotFound reply) and the map would grow past
784        // the cap.
785        let new_src = addr("198.51.100.50:9000");
786        let out = s.handle(new_src, Message::Lookup { node: a }, 0, now_secs(0));
787        assert!(out.is_empty(), "new source over capacity must be dropped");
788        assert_eq!(
789            s.rates.len(),
790            MAX_RATE_ENTRIES,
791            "map must not grow past the cap"
792        );
793
794        // An already-tracked source must still be serviced normally even
795        // while the map is at capacity.
796        let existing_src = synth_addr(0);
797        let out = s.handle(existing_src, Message::Lookup { node: a }, 0, now_secs(0));
798        assert!(
799            !out.is_empty(),
800            "already-tracked source must still be serviced"
801        );
802    }
803
804    // ── #37 task 2: RegisterSigned verification + rooted/rootless mode ──
805
806    /// A fixed, deterministic CA signing key shared by every helper below.
807    /// `valid_registration` and `registration_for_other_member` each need to
808    /// mint an INDEPENDENTLY-signed cert under the SAME trusted CA (so a
809    /// squatting attempt has a genuinely valid cert chain, just a mismatched
810    /// `node_id`), and threading a real private key through the brief's
811    /// `registration_for_other_member(ca_pub, ..)` signature (public key
812    /// only) isn't possible — a deterministic shared key sidesteps that
813    /// without changing the helper signatures.
814    fn ca_signing_key() -> SigningKey {
815        SigningKey::from_bytes(&[0x42u8; 32])
816    }
817
818    /// Mint a CA-signed `Cert` for `member_pubkey`/`member_sign_pub`, wide
819    /// open (`not_before: 0, not_after: u64::MAX`) so it validates under any
820    /// `now_secs` these tests use (mirrors `bin/yipd/src/peer_manager.rs`'s
821    /// `mk_cert` test helper).
822    fn mk_cert(
823        ca: &SigningKey,
824        member_pubkey: [u8; 32],
825        member_sign_pub: [u8; 32],
826        network_id: [u8; 16],
827    ) -> Cert {
828        let mut c = Cert {
829            version: 1,
830            member_pubkey,
831            member_sign_pubkey: member_sign_pub,
832            network_id,
833            not_before: 0,
834            not_after: u64::MAX,
835            tags: vec![],
836            ca_sig: [0u8; 64],
837        };
838        c.ca_sig = ca
839            .sign(&yip_membership::cert::cert_signing_body(&c))
840            .to_bytes();
841        c
842    }
843
844    /// Build and sign a `Record` whose `node_id` correctly derives from
845    /// `cert.member_pubkey` (mirrors `bin/yipd/src/membership.rs`'s
846    /// `build_signed_record` test helper).
847    fn build_signed_record(
848        cert: Cert,
849        endpoints: Vec<SocketAddr>,
850        seq: u64,
851        member_sign_priv: &[u8; 32],
852    ) -> Record {
853        let mut r = Record {
854            node_id: node_id(&cert.member_pubkey),
855            cert,
856            endpoints,
857            seq,
858            sig: [0u8; 64],
859        };
860        let body = yip_membership::record::record_signing_body(&r);
861        r.sig = yip_membership::record::sign(&body, member_sign_priv);
862        r
863    }
864
865    /// Mint a fresh, validly-signed `Record` under a fresh member key,
866    /// plus the CA pubkey/network_id needed to verify it and the reflexive
867    /// `SocketAddr` the registration arrives from.
868    fn valid_registration() -> ([u8; 32], [u8; 16], Record, SocketAddr) {
869        let ca = ca_signing_key();
870        let ca_pub = ca.verifying_key().to_bytes();
871        let network_id = [7u8; 16];
872        let member_pub = [1u8; 32];
873        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
874        let member_sign_pub = member_sign_key.verifying_key().to_bytes();
875        let cert = mk_cert(&ca, member_pub, member_sign_pub, network_id);
876        let member_sign_priv: [u8; 32] = member_sign_key.to_bytes();
877        let rec = build_signed_record(cert, vec![addr("192.0.2.1:9")], 1, &member_sign_priv);
878        (ca_pub, network_id, rec, addr("198.51.100.42:41000"))
879    }
880
881    /// Mint a Record signed by a DIFFERENT (but still validly CA-issued)
882    /// member key that CLAIMS `claim_node_id` — the target's node_id —
883    /// rather than the one that actually derives from its own
884    /// `cert.member_pubkey`. A genuinely valid cert chain and a genuinely
885    /// valid signature, but `record.node_id != node_id(cert.member_pubkey)`:
886    /// exactly the squatting case `Record::verify` closes. `seq` is set
887    /// STRICTLY GREATER than the victim's so `register_if_fresh`'s
888    /// freshness gate alone would have accepted this record — isolating
889    /// that it's the node_id-binding check in `verify`, not staleness, that
890    /// saves the victim.
891    fn registration_for_other_member(
892        ca_pub: [u8; 32],
893        network_id: [u8; 16],
894        claim_node_id: NodeId,
895    ) -> Record {
896        let ca = ca_signing_key();
897        assert_eq!(
898            ca.verifying_key().to_bytes(),
899            ca_pub,
900            "test helper CA must match the CA `valid_registration` used"
901        );
902        let attacker_pub = [2u8; 32]; // distinct from valid_registration's member
903        let attacker_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
904        let attacker_sign_pub = attacker_sign_key.verifying_key().to_bytes();
905        let cert = mk_cert(&ca, attacker_pub, attacker_sign_pub, network_id);
906        let mut r = Record {
907            node_id: claim_node_id, // squatting: claims someone else's node_id
908            cert,
909            endpoints: vec![addr("203.0.113.66:9")],
910            seq: 2, // > victim's seq (1)
911            sig: [0u8; 64],
912        };
913        let body = yip_membership::record::record_signing_body(&r);
914        let attacker_sign_priv: [u8; 32] = attacker_sign_key.to_bytes();
915        r.sig = yip_membership::record::sign(&body, &attacker_sign_priv);
916        r
917    }
918
919    #[test]
920    fn rooted_server_accepts_valid_signed_register_and_serves_it() {
921        let (ca_pub, network_id, rec, member_src) = valid_registration();
922        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
923        let _ = s.handle(
924            member_src,
925            Message::RegisterSigned {
926                record: rec.clone(),
927            },
928            0,
929            now_secs(0),
930        );
931        let out = s.handle(
932            addr("203.0.113.9:5"),
933            Message::Lookup { node: rec.node_id },
934            10,
935            now_secs(10),
936        );
937        assert!(out.iter().any(|(_, m)| matches!(m,
938            Message::PeerInfo { reflexive, record: Some(r), .. }
939                if *reflexive == member_src && r.node_id == rec.node_id)));
940    }
941
942    #[test]
943    fn rooted_server_rejects_forged_signature() {
944        let (ca_pub, network_id, mut rec, member_src) = valid_registration();
945        rec.sig[0] ^= 0xFF; // corrupt the signature
946        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
947        let _ = s.handle(
948            member_src,
949            Message::RegisterSigned {
950                record: rec.clone(),
951            },
952            0,
953            now_secs(0),
954        );
955        // Not stored → lookup yields NotFound.
956        let out = s.handle(
957            addr("203.0.113.9:5"),
958            Message::Lookup { node: rec.node_id },
959            10,
960            now_secs(10),
961        );
962        assert!(out
963            .iter()
964            .all(|(_, m)| !matches!(m, Message::PeerInfo { .. })));
965    }
966
967    #[test]
968    fn rooted_server_rejects_overwrite_by_non_holder() {
969        // Victim registers; attacker sends RegisterSigned for the victim's
970        // node_id signed by a DIFFERENT (valid-CA) member key → node_id !=
971        // node_id(attacker cert) → rejected.
972        let (ca_pub, network_id, victim, victim_src) = valid_registration();
973        let attacker = registration_for_other_member(ca_pub, network_id, victim.node_id);
974        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
975        let _ = s.handle(
976            victim_src,
977            Message::RegisterSigned {
978                record: victim.clone(),
979            },
980            0,
981            now_secs(0),
982        );
983        let _ = s.handle(
984            addr("203.0.113.66:9"),
985            Message::RegisterSigned { record: attacker },
986            1,
987            now_secs(1),
988        );
989        let out = s.handle(
990            addr("203.0.113.9:5"),
991            Message::Lookup {
992                node: victim.node_id,
993            },
994            2,
995            now_secs(2),
996        );
997        assert!(
998            out.iter()
999                .any(|(_, m)| matches!(m, Message::PeerInfo { reflexive, .. } if *reflexive == victim_src)),
1000            "victim's real registration must survive the overwrite attempt"
1001        );
1002    }
1003
1004    #[test]
1005    fn rooted_server_drops_legacy_unsigned_register() {
1006        let (ca_pub, network_id, _rec, _src) = valid_registration();
1007        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
1008        let _ = s.handle(
1009            addr("203.0.113.66:9"),
1010            Message::Register {
1011                node: [1u8; 16],
1012                counter: 9,
1013            },
1014            0,
1015            now_secs(0),
1016        );
1017        let out = s.handle(
1018            addr("203.0.113.9:5"),
1019            Message::Lookup { node: [1u8; 16] },
1020            1,
1021            now_secs(1),
1022        );
1023        assert!(
1024            out.iter()
1025                .all(|(_, m)| !matches!(m, Message::PeerInfo { .. })),
1026            "a rooted (mesh) server must not accept unsigned registrations"
1027        );
1028    }
1029
1030    #[test]
1031    fn rootless_server_keeps_legacy_register() {
1032        let mut s = RendezvousServer::new(0); // no roots
1033        let a = [1u8; 16];
1034        let _ = s.handle(
1035            addr("198.51.100.7:41000"),
1036            Message::Register {
1037                node: a,
1038                counter: 1,
1039            },
1040            0,
1041            now_secs(0),
1042        );
1043        let out = s.handle(
1044            addr("203.0.113.9:5"),
1045            Message::Lookup { node: a },
1046            1,
1047            now_secs(1),
1048        );
1049        assert!(out
1050            .iter()
1051            .any(|(_, m)| matches!(m, Message::PeerInfo { .. })));
1052    }
1053
1054    #[test]
1055    fn register_signed_rejects_historical_stale_record() {
1056        let (ca_pub, network_id, _rec, member_src) = valid_registration();
1057        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
1058        let now_s: u64 = 1_700_000_000;
1059        let now_wall_ms = now_s.saturating_mul(1000);
1060        let stale_seq = now_wall_ms.saturating_sub(600_000);
1061
1062        // Mint a record with stale_seq
1063        let ca = ca_signing_key();
1064        let member_pub = [1u8; 32];
1065        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
1066        let member_sign_pub = member_sign_key.verifying_key().to_bytes();
1067        let cert = mk_cert(&ca, member_pub, member_sign_pub, network_id);
1068        let member_sign_priv: [u8; 32] = member_sign_key.to_bytes();
1069        let stale_rec = build_signed_record(
1070            cert,
1071            vec![addr("192.0.2.1:9")],
1072            stale_seq,
1073            &member_sign_priv,
1074        );
1075
1076        let out = s.handle(
1077            member_src,
1078            Message::RegisterSigned {
1079                record: stale_rec.clone(),
1080            },
1081            1_000,
1082            now_s,
1083        );
1084        assert!(out.is_empty(), "stale register produces no reply");
1085
1086        let lookup = s.handle(
1087            addr("203.0.113.9:5"),
1088            Message::Lookup {
1089                node: stale_rec.node_id,
1090            },
1091            1_010,
1092            now_s,
1093        );
1094        assert_eq!(
1095            lookup,
1096            vec![(
1097                addr("203.0.113.9:5"),
1098                Message::NotFound {
1099                    node: stale_rec.node_id
1100                }
1101            )],
1102            "stale registration must yield NotFound"
1103        );
1104    }
1105
1106    #[test]
1107    fn register_signed_rejects_post_ttl_replay_after_sweep() {
1108        let (ca_pub, network_id, _, _) = valid_registration();
1109        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
1110        let now_s: u64 = 1_700_000_000;
1111        let now_wall_ms = now_s.saturating_mul(1000);
1112
1113        // 1. Initial valid registration at seq = now_wall_ms
1114        let ca = ca_signing_key();
1115        let member_pub = [1u8; 32];
1116        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
1117        let member_sign_pub = member_sign_key.verifying_key().to_bytes();
1118        let cert = mk_cert(&ca, member_pub, member_sign_pub, network_id);
1119        let member_sign_priv: [u8; 32] = member_sign_key.to_bytes();
1120        let rec = build_signed_record(
1121            cert,
1122            vec![addr("192.0.2.1:9")],
1123            now_wall_ms,
1124            &member_sign_priv,
1125        );
1126
1127        let member_src = addr("198.51.100.42:41000");
1128        let _ = s.handle(
1129            member_src,
1130            Message::RegisterSigned {
1131                record: rec.clone(),
1132            },
1133            0,
1134            now_s,
1135        );
1136
1137        let lookup1 = s.handle(
1138            addr("203.0.113.9:5"),
1139            Message::Lookup { node: rec.node_id },
1140            10,
1141            now_s,
1142        );
1143        assert!(lookup1.iter().any(|(_, m)| matches!(m,
1144            Message::PeerInfo { reflexive, record: Some(r), .. }
1145                if *reflexive == member_src && r.node_id == rec.node_id)));
1146
1147        // 2. Advance time by 600s, sweep
1148        s.sweep(600_000);
1149        let lookup_swept = s.handle(
1150            addr("203.0.113.9:5"),
1151            Message::Lookup { node: rec.node_id },
1152            600_001,
1153            now_s + 600,
1154        );
1155        assert_eq!(
1156            lookup_swept,
1157            vec![(
1158                addr("203.0.113.9:5"),
1159                Message::NotFound { node: rec.node_id }
1160            )]
1161        );
1162
1163        // 3. Attacker replays original record from a different address
1164        let attacker_src = addr("203.0.113.66:9");
1165        let out = s.handle(
1166            attacker_src,
1167            Message::RegisterSigned {
1168                record: rec.clone(),
1169            },
1170            600_002,
1171            now_s + 600,
1172        );
1173        assert!(out.is_empty());
1174
1175        let lookup2 = s.handle(
1176            addr("203.0.113.9:5"),
1177            Message::Lookup { node: rec.node_id },
1178            600_003,
1179            now_s + 600,
1180        );
1181        assert_eq!(
1182            lookup2,
1183            vec![(
1184                addr("203.0.113.9:5"),
1185                Message::NotFound { node: rec.node_id }
1186            )],
1187            "replayed registration after sweep must yield NotFound"
1188        );
1189    }
1190
1191    #[test]
1192    fn register_signed_rejects_future_seq_record() {
1193        let (ca_pub, network_id, _rec, member_src) = valid_registration();
1194        let mut s = RendezvousServer::new_with_roots(0, vec![ca_pub], network_id);
1195        let now_s: u64 = 1_700_000_000;
1196        let now_wall_ms = now_s.saturating_mul(1000);
1197        // Mint a record with sequence 10 minutes in the future (beyond 300s skew)
1198        let future_seq = now_wall_ms + 600_000;
1199
1200        let ca = ca_signing_key();
1201        let member_pub = [1u8; 32];
1202        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
1203        let member_sign_pub = member_sign_key.verifying_key().to_bytes();
1204        let cert = mk_cert(&ca, member_pub, member_sign_pub, network_id);
1205        let member_sign_priv: [u8; 32] = member_sign_key.to_bytes();
1206        let future_rec = build_signed_record(
1207            cert,
1208            vec![addr("192.0.2.1:9")],
1209            future_seq,
1210            &member_sign_priv,
1211        );
1212
1213        let out = s.handle(
1214            member_src,
1215            Message::RegisterSigned {
1216                record: future_rec.clone(),
1217            },
1218            1_000,
1219            now_s,
1220        );
1221        assert!(out.is_empty(), "future seq register produces no reply");
1222
1223        let lookup = s.handle(
1224            addr("203.0.113.9:5"),
1225            Message::Lookup {
1226                node: future_rec.node_id,
1227            },
1228            1_010,
1229            now_s,
1230        );
1231        assert_eq!(
1232            lookup,
1233            vec![(
1234                addr("203.0.113.9:5"),
1235                Message::NotFound {
1236                    node: future_rec.node_id
1237                }
1238            )],
1239            "future seq record must not be registered"
1240        );
1241    }
1242
1243    #[test]
1244    fn test_server_handle_register_auth() {
1245        let mut s = RendezvousServer::new(0);
1246        let src = addr("198.51.100.7:41000");
1247        let node = [42u8; 16];
1248        let counter = 1234u64;
1249        let mac = [0x99u8; 16];
1250        let msg = Message::RegisterAuth { node, counter, mac };
1251        let out = s.handle(src, msg, 100, 10);
1252        assert!(
1253            out.is_empty(),
1254            "RegisterAuth over UDP must be silently dropped"
1255        );
1256
1257        // Verify that the node is NOT registered and lookup returns NotFound
1258        assert!(!s.is_registered(&node, 100));
1259        let lookup_src = addr("203.0.113.9:52000");
1260        let lookup_out = s.handle(lookup_src, Message::Lookup { node }, 200, 10);
1261        assert_eq!(lookup_out, vec![(lookup_src, Message::NotFound { node })]);
1262    }
1263
1264    #[test]
1265    fn test_server_forwarded_handle() {
1266        let s = RendezvousServer::new(0);
1267        assert_eq!(s.forwarded_count(), 0);
1268        let handle = s.forwarded_handle();
1269        handle.fetch_add(5, Ordering::Relaxed);
1270        assert_eq!(s.forwarded_count(), 5);
1271        s.record_relay_forward();
1272        assert_eq!(s.forwarded_count(), 6);
1273    }
1274
1275    #[test]
1276    fn test_server_roots_cfg_getter() {
1277        let s_rootless = RendezvousServer::new(0);
1278        assert!(s_rootless.roots_cfg().is_none());
1279
1280        let ca_pubs = vec![[7u8; 32]];
1281        let net_id = [9u8; 16];
1282        let s_rooted = RendezvousServer::new_with_roots(0, ca_pubs.clone(), net_id);
1283        let roots = s_rooted.roots_cfg().expect("roots_cfg should be Some");
1284        assert_eq!(roots.0, ca_pubs);
1285        assert_eq!(roots.1, net_id);
1286    }
1287
1288    #[test]
1289    fn test_server_is_registered_boundary() {
1290        let mut s = RendezvousServer::new(0);
1291        let node = [1u8; 16];
1292        let now = 1_000;
1293        s.register_if_fresh(node, 1, addr("192.0.2.1:1234"), now);
1294        // Expiry is now + 60_000 = 61_000
1295        assert!(s.is_registered(&node, 60_999));
1296        assert!(!s.is_registered(&node, 61_000));
1297        assert!(!s.is_registered(&node, 61_001));
1298    }
1299
1300    #[test]
1301    fn test_server_sweep() {
1302        let mut s = RendezvousServer::new(0);
1303        let node1 = [1u8; 16];
1304        let node2 = [2u8; 16];
1305        let src = addr("192.0.2.1:1234");
1306        s.register_if_fresh(node1, 1, src, 1_000);
1307        s.register_if_fresh_tls(node2, 1, 1_000);
1308        // Populate rates map
1309        s.handle(src, Message::Lookup { node: node1 }, 1_000, 1);
1310        assert_eq!(s.rates.len(), 1);
1311        assert_eq!(s.tls_seen.len(), 1);
1312        assert!(s.is_registered(&node1, 1_000));
1313
1314        // Sweep before rate window expiry (1_000 + 1_000 = 2_000)
1315        s.sweep(1_999);
1316        assert_eq!(s.rates.len(), 1); // 999 < 1000 -> retained
1317        assert_eq!(s.tls_seen.len(), 1);
1318
1319        // Sweep at rate window expiry: 2000 - 1000 = 1000, not < 1000 -> evicted
1320        s.sweep(2_000);
1321        assert_eq!(s.rates.len(), 0);
1322        assert_eq!(s.tls_seen.len(), 1);
1323
1324        // Sweep before tls_seen expiry (61_000)
1325        s.sweep(60_999);
1326        assert_eq!(s.tls_seen.len(), 1);
1327        assert_eq!(s.regs.len(), 1);
1328
1329        // Sweep at exact expiry (61_000): 61000 > 61000 is false -> evicted
1330        s.sweep(61_000);
1331        assert_eq!(s.tls_seen.len(), 0);
1332        assert_eq!(s.regs.len(), 0);
1333        assert!(!s.is_registered(&node1, 1_000));
1334    }
1335
1336    #[test]
1337    fn test_register_if_fresh_expiry_boundaries() {
1338        let mut s = RendezvousServer::new(0);
1339        let node = [1u8; 16];
1340        let src = addr("192.0.2.1:1234");
1341        assert!(s.register_if_fresh(node, 10, src, 1_000));
1342        // Expiry is 61_000.
1343        // If now is 60_999, same counter 10 is rejected as replay:
1344        assert!(!s.register_if_fresh(node, 10, src, 60_999));
1345        // If now is 61_000 (expired), counter 10 is accepted because it has expired:
1346        assert!(s.register_if_fresh(node, 10, src, 61_000));
1347
1348        // Test TLS variant:
1349        let node_tls = [2u8; 16];
1350        assert!(s.register_if_fresh_tls(node_tls, 10, 1_000));
1351        assert!(!s.register_if_fresh_tls(node_tls, 10, 60_999));
1352        assert!(s.register_if_fresh_tls(node_tls, 10, 61_000));
1353    }
1354
1355    #[test]
1356    fn test_verify_signed_record_boundaries() {
1357        let network_id = [9u8; 16];
1358        let ca = ca_signing_key();
1359        let ca_pub = ca.verifying_key().to_bytes();
1360        let ca_pubs = vec![ca_pub];
1361        let s = RendezvousServer::new_with_roots(0, ca_pubs, network_id);
1362
1363        let member_pub = [1u8; 32];
1364        let member_sign_key = SigningKey::generate(&mut UnwrapErr(SysRng));
1365        let member_sign_pub = member_sign_key.verifying_key().to_bytes();
1366        let cert = mk_cert(&ca, member_pub, member_sign_pub, network_id);
1367        let member_sign_priv: [u8; 32] = member_sign_key.to_bytes();
1368
1369        let now_s = 100_000;
1370        let now_ms = now_s * 1000;
1371        let skew_ms = REGISTRATION_SKEW_SECS * 1000;
1372        let min_seq = now_ms - (REG_TTL_MS + skew_ms);
1373        let max_seq = now_ms + skew_ms;
1374
1375        // Exactly at min_seq
1376        let rec_min = build_signed_record(
1377            cert.clone(),
1378            vec![addr("192.0.2.1:9")],
1379            min_seq,
1380            &member_sign_priv,
1381        );
1382        assert!(s.verify_signed_record(&rec_min, now_s));
1383
1384        // Just below min_seq
1385        let rec_below = build_signed_record(
1386            cert.clone(),
1387            vec![addr("192.0.2.1:9")],
1388            min_seq - 1,
1389            &member_sign_priv,
1390        );
1391        assert!(!s.verify_signed_record(&rec_below, now_s));
1392
1393        // Exactly at max_seq
1394        let rec_max = build_signed_record(
1395            cert.clone(),
1396            vec![addr("192.0.2.1:9")],
1397            max_seq,
1398            &member_sign_priv,
1399        );
1400        assert!(s.verify_signed_record(&rec_max, now_s));
1401
1402        // Just above max_seq
1403        let rec_above = build_signed_record(
1404            cert,
1405            vec![addr("192.0.2.1:9")],
1406            max_seq + 1,
1407            &member_sign_priv,
1408        );
1409        assert!(!s.verify_signed_record(&rec_above, now_s));
1410    }
1411
1412    #[test]
1413    fn test_handle_lookup_and_relay_expiry_boundary() {
1414        let mut s = RendezvousServer::new(0);
1415        let node = [1u8; 16];
1416        let src = addr("192.0.2.1:1234");
1417        s.register_if_fresh(node, 1, src, 1_000);
1418        // Expiry is 61_000
1419
1420        // At 60_999 (unexpired):
1421        let lookup_ok = s.handle(addr("192.0.2.2:5000"), Message::Lookup { node }, 60_999, 60);
1422        assert!(matches!(
1423            lookup_ok.first(),
1424            Some((_, Message::PeerInfo { .. }))
1425        ));
1426
1427        let relay_ok = s.handle(
1428            addr("192.0.2.2:5000"),
1429            Message::RelaySend {
1430                src: [2u8; 16],
1431                dst: node,
1432                payload: vec![1, 2],
1433            },
1434            60_999,
1435            60,
1436        );
1437        assert_eq!(relay_ok.len(), 1);
1438
1439        // At 61_000 (expired):
1440        let lookup_exp = s.handle(addr("192.0.2.2:5000"), Message::Lookup { node }, 61_000, 61);
1441        assert_eq!(
1442            lookup_exp,
1443            vec![(addr("192.0.2.2:5000"), Message::NotFound { node })]
1444        );
1445
1446        let relay_exp = s.handle(
1447            addr("192.0.2.2:5000"),
1448            Message::RelaySend {
1449                src: [2u8; 16],
1450                dst: node,
1451                payload: vec![1, 2],
1452            },
1453            61_000,
1454            61,
1455        );
1456        assert!(relay_exp.is_empty());
1457    }
1458}