Skip to main content

ts_runtime/peer_tracker/
mod.rs

1//! Peer delta update tracking.
2
3use std::{
4    collections::{HashMap, HashSet},
5    net::IpAddr,
6    sync::Arc,
7};
8
9use kameo::{
10    actor::ActorRef,
11    message::{Context, Message},
12    reply::ReplySender,
13};
14use tokio::sync::watch;
15use ts_control::{Node, UserId, UserProfile};
16use ts_keys::{DiscoPublicKey, NodePublicKey};
17use ts_transport::PeerId;
18
19use crate::{Error, dataplane::PeerDiscoKeyAdvertisement, env::Env, status::StatusNode};
20
21mod peer_db;
22
23pub use peer_db::PeerDb;
24
25/// Whether `key` is the all-zero disco key, which Go spells `key.DiscoPublic.IsZero()` and treats
26/// everywhere as "this peer has no disco key" rather than as a usable key.
27fn disco_key_is_zero(key: &DiscoPublicKey) -> bool {
28    key.to_bytes() == [0u8; DiscoPublicKey::KEY_LEN_BYTES]
29}
30
31/// Normalize a disco key as it arrives from control: the all-zero key means "absent", exactly as
32/// Go's `IsZero()` checks in `endpoint.updateDiscoKey` read it.
33fn disco_key_from_control(key: Option<DiscoPublicKey>) -> Option<DiscoPublicKey> {
34    key.filter(|k| !disco_key_is_zero(k))
35}
36
37/// The two disco keys a peer can present, and which of them is currently active — Go
38/// [`magicsock.endpointDisco`] (`wgengine/magicsock/endpoint.go`).
39///
40/// A peer's disco key reaches us from two independent sources: **control**, in a netmap node or a
41/// `PeersChangedPatch`, and the **peer itself**, in a TSMP disco-key advertisement carried inside
42/// the WireGuard tunnel. Go keeps both side by side on the endpoint, and so do we, because control
43/// is the slower of the two: an advertisement exists precisely to cover the window where control has
44/// not caught up with the peer's current key, so collapsing the two into one field would let the
45/// next map poll overwrite a freshly-learned key with control's stale one — losing the feature's own
46/// motivating case.
47///
48/// Only one key is active for sending at a time ([`key`](Self::key)). That active key is what the
49/// peer db carries in [`Node::disco_key`], which is this fork's live lookup for every direct-path
50/// consumer (`direct::DiscoPeerLookup` resolves against it, and `PeerDb`'s disco index is built from
51/// it) — the stand-in for Go's per-endpoint `disco` pointer.
52///
53/// [`magicsock.endpointDisco`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/endpoint.go
54#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
55struct EndpointDisco {
56    /// The key learned from control (Go `endpointDisco.controlKey`).
57    control: Option<DiscoPublicKey>,
58    /// The key learned from a TSMP advertisement (Go `endpointDisco.tsmpKey`).
59    tsmp: Option<DiscoPublicKey>,
60    /// Whether [`tsmp`](Self::tsmp) is the active key (Go `endpointDisco.tsmpActive`).
61    tsmp_active: bool,
62}
63
64impl EndpointDisco {
65    /// The key currently regarded as active — Go `endpointDisco.key()`.
66    fn key(&self) -> Option<DiscoPublicKey> {
67        if self.tsmp_active {
68            self.tsmp
69        } else {
70            self.control
71        }
72    }
73
74    /// The control-learned key, active or not — Go `endpointDisco.keyFromControl()`.
75    fn key_from_control(&self) -> Option<DiscoPublicKey> {
76        self.control
77    }
78
79    /// The TSMP-learned key, active or not — Go `endpointDisco.keyFromTSMP()`.
80    fn key_from_tsmp(&self) -> Option<DiscoPublicKey> {
81        self.tsmp
82    }
83
84    /// Replace the control-learned key, leaving any TSMP-learned key in place — Go
85    /// `endpoint.updateDiscoKey`.
86    ///
87    /// A non-zero control key takes the active slot (control has caught up, so it is authoritative
88    /// again); an absent one hands the slot back to the TSMP key, if there is one.
89    fn update_from_control(&mut self, key: Option<DiscoPublicKey>) {
90        self.control = key;
91        self.tsmp_active = key.is_none();
92    }
93
94    /// Replace the TSMP-learned key, leaving the control-learned key in place — Go
95    /// `endpoint.updateTSMPDiscoKey`.
96    fn update_from_tsmp(&mut self, key: Option<DiscoPublicKey>) {
97        self.tsmp = key;
98        self.tsmp_active = key.is_some();
99    }
100
101    /// No key material from either source — Go nils out the endpoint's `disco` pointer here.
102    fn is_empty(&self) -> bool {
103        self.control.is_none() && self.tsmp.is_none()
104    }
105}
106
107/// Actor that tracks peer delta updates and emits new states.
108pub struct PeerTracker {
109    peer_db: PeerDb,
110    seen_state_update: bool,
111    pending_requests: Vec<Pending>,
112    /// Latest peer snapshot, published on every netmap update so embedders can watch for peer
113    /// changes ([`WatchNetmap`]).
114    peer_watch: watch::Sender<Vec<StatusNode>>,
115    /// Accumulated netmap user profiles (`MapResponse.UserProfiles`), keyed by user id, joined
116    /// against a node's [`Node::user_id`](ts_control::Node::user_id) to resolve the owning user's
117    /// login/display name for a [`WhoIs`](crate::status::WhoIs). Control sends these incrementally
118    /// (only new/changed profiles per response), so this map **accumulates** across updates rather
119    /// than being replaced — a peer upserted in one response may reference a profile delivered in an
120    /// earlier one.
121    user_profiles: HashMap<UserId, UserProfile>,
122    /// Per-peer disco-key provenance ([`EndpointDisco`]), keyed by the peer's node key.
123    ///
124    /// Go keeps this on the magicsock `endpoint`, which the peer map keys by node key; here the peer
125    /// db stores control's [`Node`] verbatim, so the second key (and which of the two is active)
126    /// lives beside it. Keying by node key reproduces Go's lifetime exactly: the state is dropped
127    /// when the peer leaves the netmap, and a peer that ROTATES its node key gets a fresh entry —
128    /// Go builds it a new endpoint, so a key learned over TSMP under the old node key is never
129    /// carried onto the new one. [`prune_endpoint_disco`](PeerTracker::prune_endpoint_disco) does
130    /// the dropping.
131    endpoint_disco: HashMap<NodePublicKey, EndpointDisco>,
132    /// Tailnet-Lock (TKA) authority enforced at the peer-trust chokepoint, matching Go
133    /// `tkaFilterNetmapLocked`. Read on demand from a [`watch`] cell the control runner owns: when it
134    /// holds `Some` (a verified lock has been synced from control), enforcement is **active** — every
135    /// upserted peer must present a `key_signature` this authority authorizes, or it is dropped
136    /// (fail-closed), exactly as Go drops peers with a missing or failing signature. When it holds
137    /// `None` (no lock, or the lock was disabled) enforcement is **inactive** and every peer is
138    /// upserted, identical to pre-TKA behavior and to Go's `b.tka == nil` early return.
139    ///
140    /// A `watch::Receiver` (not the bus) is the transport on purpose: the authority is a single
141    /// security-critical state cell, and `watch` is last-write-wins, never-dropped, and ordered by
142    /// the control runner's own writes — so a disable (`None`) can never be reordered behind or
143    /// silently dropped before a stale `Some` (which a best-effort broadcast bus could do, leaving a
144    /// defunct lock enforcing forever). The control runner is the sole writer; we only ever read.
145    ///
146    /// The authority always passes through `VerifiedAumChain::verify` before the control runner
147    /// publishes it, so enforcement only engages on a chain we have cryptographically verified.
148    /// Connectivity now depends on `ts_tka` verifying genuinely-good signatures correctly (see
149    /// SECURITY.md). Self is structurally never filtered here (the self node never enters `peer_db` —
150    /// it is routed to the control runner's `self_node` cell), so a node cannot lock itself out of
151    /// its own netmap.
152    tka_authority: watch::Receiver<Option<Arc<ts_tka::Authority>>>,
153    env: Env,
154}
155
156impl PeerTracker {
157    fn peer_by_name_opt(&self, name: &str) -> Option<&Node> {
158        // Canonicalization (case + trailing dot) is handled inside the name index lookup.
159        self.peer_db.get(&name).map(|(_id, node)| node)
160    }
161
162    fn peer_by_tailnet_ip_opt(&self, ip: IpAddr) -> Option<&Node> {
163        self.peer_db.get(&ip).map(|(_id, node)| node)
164    }
165
166    /// Build the peer entries for a [`Status`](crate::Status) snapshot from the current peer db.
167    ///
168    /// Connectivity fields (`cur_addr`/`relay`) are left at their `from_node` defaults (`None`) here:
169    /// this is the live-watch/hot path and must stay magicsock-free and synchronous. The explicit
170    /// [`GetStatus`] snapshot enriches them ([`status_peers_with_ids`](Self::status_peers_with_ids)).
171    fn status_peers(&self) -> Vec<StatusNode> {
172        self.peer_db
173            .peers()
174            .values()
175            .map(StatusNode::from_node)
176            .collect()
177    }
178
179    /// Like [`status_peers`](Self::status_peers) but pairs each entry with its [`PeerId`], so the
180    /// caller can join per-peer connectivity (the direct manager's `best_addrs`, keyed by `PeerId`)
181    /// onto the `StatusNode` before returning it. Order is unspecified (a `HashMap` walk).
182    fn status_peers_with_ids(&self) -> Vec<(PeerId, StatusNode)> {
183        self.peer_db
184            .peers()
185            .iter()
186            .map(|(id, node)| (*id, StatusNode::from_node(node)))
187            .collect()
188    }
189
190    fn whois_opt(&self, addr: std::net::SocketAddr) -> Option<crate::status::WhoIs> {
191        let ip = crate::status::whois_addr(addr);
192        let node = self.peer_by_tailnet_ip_opt(ip).cloned()?;
193        // Join the node's owning user id against the accumulated UserProfiles table to resolve a
194        // login/display name. `None` when control sent no profile for that user (e.g. tagged nodes
195        // with no human owner, or a profile not yet delivered).
196        let user = self.resolve_user(node.user_id);
197        Some(crate::status::WhoIs::from_node_with_user(node, user))
198    }
199
200    /// Resolve a user id to its best display label from the accumulated profile table.
201    fn resolve_user(&self, user_id: UserId) -> Option<String> {
202        self.user_profiles
203            .get(&user_id)
204            .and_then(UserProfile::best_label)
205    }
206
207    /// Whether `node` may be admitted to the peer db under Tailnet Lock, matching Go
208    /// `tkaFilterNetmapLocked`'s per-peer verdict (drop unsigned / failed-signature peers).
209    ///
210    /// This consults the live [`tka_authority`](Self::tka_authority) cell on each call (one `borrow`,
211    /// held only for the duration of the verdict). For a `Full` resync — which checks every peer —
212    /// prefer [`tka_authority_snapshot`](Self::tka_authority_snapshot) +
213    /// [`tka_snapshot_admits`](Self::tka_snapshot_admits) to borrow once and verify each peer a single
214    /// time; this method is the convenience wrapper for the single-peer (`Delta`/patch) sites.
215    ///
216    /// Fail-closed and gated:
217    /// - No authority ⇒ no lock synced ⇒ always admit (Go's `b.tka == nil` early return; identical to
218    ///   pre-TKA behavior).
219    /// - **Empty trusted-key state** ⇒ always admit (logged at `error!` — see
220    ///   [`tka_snapshot_admits`](Self::tka_snapshot_admits) for the full rationale).
221    /// - Authority present + peer carries a `key_signature` the authority authorizes for the peer's
222    ///   node key ⇒ admit.
223    /// - Authority present + signature missing or unauthorized/invalid ⇒ **drop** (Go drops peers
224    ///   with a missing signature or failed `NodeKeyAuthorized` under tailnet lock).
225    fn tka_admits(&self, node: &Node) -> bool {
226        // Single-peer sites (`Delta`/patch) only need the admit bool; the rotation details are used
227        // exclusively by the cross-peer `Full` filter (rotation obsolescence is whole-netmap).
228        Self::tka_snapshot_admits(self.tka_authority.borrow().as_deref(), node).admitted
229    }
230
231    /// Borrow the current TKA authority once (cloning the cheap `Arc`) for a batch verdict. Returns
232    /// `None` when no lock is synced (admit-all). Used by the `Full` path so a netmap of N peers
233    /// reads the cell once and runs at most one signature verify per peer (not two).
234    fn tka_authority_snapshot(&self) -> Option<Arc<ts_tka::Authority>> {
235        self.tka_authority.borrow().clone()
236    }
237
238    /// The per-peer Tailnet-Lock verdict against an already-borrowed `authority` snapshot. Factored
239    /// out so both the single-peer [`tka_admits`](Self::tka_admits) and the `Full` batch path share
240    /// one verdict implementation (no divergence) while the batch path verifies each peer exactly
241    /// once.
242    ///
243    /// Returns whether the peer is admitted AND, for an admitted peer signed by a rotation chain, the
244    /// [`RotationDetails`](ts_tka::RotationDetails) of that chain — so the `Full` path can run the
245    /// cross-peer rotation filter (Go's `rotationTracker`) without a second verify per peer. A peer
246    /// that is dropped, unsigned, or signed by a non-rotation chain carries `rotation == None`.
247    ///
248    /// Never logs key/signature bytes — only the `stable_id` and the `TkaError` Display (static
249    /// descriptors). One documented parity gap remains vs Go (under-enforcement, in PARITY_ROADMAP):
250    /// no `UnsignedPeerAPIOnly` exemption (our node model lacks the field).
251    fn tka_snapshot_admits(authority: Option<&ts_tka::Authority>, node: &Node) -> TkaVerdict {
252        let Some(auth) = authority else {
253            return TkaVerdict::admit();
254        };
255
256        // Brick-guard: an authority with no trusted keys would drop every peer. A verified chain is
257        // structurally guaranteed ≥1 key (genesis rejects an empty key set, and the last key cannot
258        // be removed), so reaching here means a `ts_tka` invariant was violated — admit rather than
259        // black-hole the whole netmap, and log at `error!` because it signals a real bug, not an
260        // expected runtime input. This is OUR fail-safe, not a Go behavior. NOTE: it only catches the
261        // empty-keyset shape; a non-empty authority that authorizes none of the offered peers still
262        // (correctly) drops them — that is what a lock that revoked everyone means. The
263        // "authorized-zero-peers" isolation case is surfaced separately by the caller.
264        if auth.state().keys.is_empty() {
265            tracing::error!(
266                "TKA: authority has an empty trusted-key set (verified chains never do — likely a \
267                 ts_tka bug); not enforcing (admitting all) to avoid isolating the node"
268            );
269            return TkaVerdict::admit();
270        }
271
272        if node.key_signature.is_empty() {
273            tracing::warn!(
274                stable_id = ?node.stable_id,
275                "TKA: dropping unsigned peer under tailnet lock"
276            );
277            return TkaVerdict::drop();
278        }
279
280        match auth.node_key_authorized_with_details(&node.node_key.to_bytes(), &node.key_signature)
281        {
282            Ok(rotation) => {
283                tracing::debug!(stable_id = ?node.stable_id, "TKA: peer node-key authorized");
284                TkaVerdict {
285                    admitted: true,
286                    rotation,
287                }
288            }
289            Err(e) => {
290                tracing::warn!(
291                    stable_id = ?node.stable_id,
292                    error = %e,
293                    "TKA: dropping peer with unauthorized node key"
294                );
295                TkaVerdict::drop()
296            }
297        }
298    }
299}
300
301/// The outcome of a per-peer Tailnet-Lock check: whether the peer is admitted, plus (for an admitted
302/// peer signed by a rotation chain) the chain's [`RotationDetails`](ts_tka::RotationDetails) so the
303/// `Full` path can run the cross-peer rotation filter from the SAME verify pass (no second verify).
304struct TkaVerdict {
305    admitted: bool,
306    rotation: Option<ts_tka::RotationDetails>,
307}
308
309impl TkaVerdict {
310    /// Admitted, no rotation details (no lock / brick-guard / non-rotation signature).
311    fn admit() -> Self {
312        Self {
313            admitted: true,
314            rotation: None,
315        }
316    }
317    /// Dropped.
318    fn drop() -> Self {
319        Self {
320            admitted: false,
321            rotation: None,
322        }
323    }
324}
325
326/// Cross-peer rotation-obsolescence tracker, mirroring Go `ipnlocal.rotationTracker`. Fed the
327/// [`RotationDetails`](ts_tka::RotationDetails) of every admitted, rotation-signed peer in a `Full`
328/// netmap; [`obsolete_keys`](Self::obsolete_keys) then returns the node keys to drop on top of the
329/// per-peer verdict. Two rules (Go `tkaFilterNetmapLocked` + `rotationTracker.obsoleteKeys`):
330///
331/// 1. Every prior node key named in any rotation chain is obsolete (a newer chain rotated it away).
332/// 2. Among `Direct`-rooted chains sharing one wrapping pubkey (a clone signal), only the
333///    longest-chain peer survives; if the two longest are tied, ALL in that group are dropped (we
334///    cannot tell which is the latest, so reject for safety). `Credential`-rooted chains are exempt
335///    from rule 2 — several nodes can legitimately join under one reusable auth key (same wrapping
336///    pubkey), so sharing it is not a clone signal there. (Rule 1 still applies to them.)
337///
338/// Node keys are tracked as raw `Vec<u8>` (the verified 32-byte node-public bytes).
339#[derive(Default)]
340struct RotationTracker {
341    obsolete: HashSet<Vec<u8>>,
342    by_wrapping_key: HashMap<Vec<u8>, Vec<SigRotation>>,
343}
344
345/// One admitted peer's rotation entry within a wrapping-key group.
346struct SigRotation {
347    node_key: Vec<u8>,
348    num_prev_keys: usize,
349}
350
351impl RotationTracker {
352    /// Record an admitted peer `node_key` and its rotation `details` (Go `addRotationDetails`).
353    fn add(&mut self, node_key: Vec<u8>, details: &ts_tka::RotationDetails) {
354        // Rule 1: every prior key is obsolete — applied for ALL chains (incl. credential-rooted),
355        // matching Go's ungated `obsolete.AddSlice(d.PrevNodeKeys)`.
356        self.obsolete.extend(details.prev_node_keys.iter().cloned());
357        // Rule 2 (clone-uniqueness) is gated to Direct-rooted chains only.
358        if details.initial_sig_kind != ts_tka::SigKind::Direct {
359            return;
360        }
361        self.by_wrapping_key
362            .entry(details.initial_wrapping_pubkey.clone())
363            .or_default()
364            .push(SigRotation {
365                node_key,
366                num_prev_keys: details.prev_node_keys.len(),
367            });
368    }
369
370    /// Compute the full obsolete node-key set (Go `rotationTracker.obsoleteKeys`). Processes each
371    /// wrapping-key group, mutating the shared `obsolete` set as it goes (so a key obsoleted by one
372    /// group is seen as obsolete by later groups via the `retain` below — Go's
373    /// `slices.DeleteFunc(... Contains)`). Group iteration order (a `HashMap` drain) is
374    /// nondeterministic, but the result is order-INDEPENDENT: this only ever *inserts* into
375    /// `obsolete` (never removes), and rule 1 already obsoleted every prior key before this loop, so
376    /// the final set is a union that does not depend on which group runs first (as in Go).
377    fn obsolete_keys(mut self) -> HashSet<Vec<u8>> {
378        // Drain only the group map so the loop can mutate `self.obsolete` without aliasing it; the
379        // shared `obsolete` set itself is NOT drained, preserving the cross-group visibility above.
380        let groups: Vec<Vec<SigRotation>> = self.by_wrapping_key.drain().map(|(_k, v)| v).collect();
381        for mut group in groups {
382            // Drop entries already obsoleted (rotated away) by another chain.
383            group.retain(|rd| !self.obsolete.contains(&rd.node_key));
384            if group.is_empty() {
385                continue;
386            }
387            // Longest chain (most prior keys) is the newest ⇒ the survivor; sort decreasing.
388            // `sort_by_key` is stable (like Go's `SortStableFunc`); `Reverse` gives descending order.
389            group.sort_by_key(|rd| core::cmp::Reverse(rd.num_prev_keys));
390            if group.len() >= 2 && group[0].num_prev_keys == group[1].num_prev_keys {
391                // Tie for longest ⇒ cannot disambiguate the latest ⇒ drop the WHOLE group.
392                tracing::warn!(
393                    "TKA: multiple peers share a wrapping key with equal rotation depth; dropping all (cannot determine the latest)"
394                );
395                for rd in &group {
396                    self.obsolete.insert(rd.node_key.clone());
397                }
398            } else {
399                // Only the longest-chain peer survives; the rest are obsolete.
400                for rd in &group[1..] {
401                    self.obsolete.insert(rd.node_key.clone());
402                }
403            }
404        }
405        self.obsolete
406    }
407}
408
409impl kameo::Actor for PeerTracker {
410    /// `(env, tka_authority)`: the bus/keys env, plus the read end of the control runner's TKA
411    /// enforcement-authority cell (Go `tkaFilterNetmapLocked`). The control runner is the sole
412    /// writer; it publishes the verified `Authority` after a successful `/machine/tka/sync` and
413    /// `None` when the lock is disabled. A `watch` cell (not a bus message) so the latest value is
414    /// always readable on demand, never dropped, and never reordered (see the control runner's
415    /// `tka_authority` cell).
416    type Args = (Env, watch::Receiver<Option<Arc<ts_tka::Authority>>>);
417    type Error = Error;
418
419    async fn on_start(
420        (env, tka_authority): Self::Args,
421        slf: ActorRef<Self>,
422    ) -> Result<Self, Self::Error> {
423        env.subscribe::<Arc<ts_control::StateUpdate>>(&slf).await?;
424        env.subscribe::<PeerDiscoKeyAdvertisement>(&slf).await?;
425
426        let (peer_watch, _) = watch::channel(Vec::new());
427
428        Ok(Self {
429            peer_db: PeerDb::default(),
430            pending_requests: Default::default(),
431            seen_state_update: false,
432            peer_watch,
433            user_profiles: HashMap::new(),
434            endpoint_disco: HashMap::new(),
435            // The cell starts `None` (no lock synced ⇒ enforcement inactive, admit all, matching
436            // Go's `b.tka == nil`); the control runner flips it to `Some` on the first sync.
437            tka_authority,
438            env,
439        })
440    }
441}
442
443enum Pending {
444    PeerByName(PeerByName, ReplySender<Option<Node>>),
445    AcceptedRoute(PeerByAcceptedRoute, ReplySender<Vec<Node>>),
446    TailnetIp(PeerByTailnetIp, ReplySender<Option<Node>>),
447    Status(ReplySender<Vec<(PeerId, StatusNode)>>),
448    WhoIs(Whois, ReplySender<Option<crate::status::WhoIs>>),
449}
450
451// For messages with arguments, a struct is generated with the args as fields. They aren't
452// documented, and we can't apply attributes directly to the fields. Hence, wrap in a module where
453// docs are turned off everywhere.
454#[allow(missing_docs)]
455mod msg_impl {
456    use std::net::IpAddr;
457
458    use kameo::prelude::DelegatedReply;
459
460    use super::*;
461
462    #[kameo::messages]
463    impl PeerTracker {
464        /// Lookup a peer by name.
465        ///
466        /// Waits until we've received at least one peer update from control.
467        #[message(ctx)]
468        pub async fn peer_by_name(
469            &mut self,
470            ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
471            name: String,
472        ) -> DelegatedReply<Option<Node>> {
473            let (deleg, sender) = ctx.reply_sender();
474            let Some(sender) = sender else { return deleg };
475
476            if !self.seen_state_update {
477                tracing::debug!(query = name, "no peer state seen yet, queueing request");
478
479                self.pending_requests
480                    .push(Pending::PeerByName(PeerByName { name }, sender));
481
482                return deleg;
483            }
484
485            sender.send(self.peer_by_name_opt(&name).cloned());
486
487            deleg
488        }
489
490        /// Lookup all peers that accept packets addressed to the given IP.
491        ///
492        /// This includes the peer's tailnet address and any subnet routes it provides. Only
493        /// the peers with the most specific subnet route match that covers `ip` will be
494        /// returned.
495        ///
496        /// E.g., suppose:
497        ///
498        /// - We're querying for `10.1.2.3`
499        /// - `PeerA` and `PeerB` have accepted routes for `10.1.2.0/24`
500        /// - `PeerC` has an accepted route for `10.1.0.0/16`
501        ///
502        /// Only `PeerA` and `PeerB` will be returned, since they have the most specific
503        /// prefix match.
504        #[message(ctx)]
505        pub fn peer_by_accepted_route(
506            &mut self,
507            ctx: &mut Context<Self, DelegatedReply<Vec<Node>>>,
508            ip: IpAddr,
509        ) -> DelegatedReply<Vec<Node>> {
510            let (deleg, sender) = ctx.reply_sender();
511            let Some(sender) = sender else { return deleg };
512
513            if !self.seen_state_update {
514                tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
515
516                self.pending_requests
517                    .push(Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, sender));
518
519                return deleg;
520            }
521
522            sender.send(
523                self.peer_db
524                    .get_route(ip.into())
525                    .map(|(_id, node)| node.clone())
526                    .collect(),
527            );
528
529            deleg
530        }
531
532        /// Lookup the peer that has the given tailnet IP address.
533        #[message(ctx)]
534        pub fn peer_by_tailnet_ip(
535            &mut self,
536            ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
537            ip: IpAddr,
538        ) -> DelegatedReply<Option<Node>> {
539            let (deleg, sender) = ctx.reply_sender();
540            let Some(sender) = sender else { return deleg };
541
542            if !self.seen_state_update {
543                tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
544
545                self.pending_requests
546                    .push(Pending::TailnetIp(PeerByTailnetIp { ip }, sender));
547
548                return deleg;
549            }
550
551            sender.send(self.peer_by_tailnet_ip_opt(ip).cloned());
552
553            deleg
554        }
555
556        /// Build the peer entries of a [`Status`](crate::Status) snapshot, each paired with its
557        /// [`PeerId`] so [`Runtime::status`](crate::Runtime::status) can join per-peer connectivity
558        /// (`cur_addr`/`relay`) from the direct manager before returning. The self node is *not*
559        /// included here (it lives in the control runner); `Runtime::status` combines both and drops
560        /// the ids.
561        ///
562        /// Waits until we've received at least one peer update from control.
563        #[message(ctx)]
564        pub fn get_status(
565            &mut self,
566            ctx: &mut Context<Self, DelegatedReply<Vec<(PeerId, StatusNode)>>>,
567        ) -> DelegatedReply<Vec<(PeerId, StatusNode)>> {
568            let (deleg, sender) = ctx.reply_sender();
569            let Some(sender) = sender else { return deleg };
570
571            if !self.seen_state_update {
572                tracing::debug!("no peer state seen yet, queueing status request");
573                self.pending_requests.push(Pending::Status(sender));
574                return deleg;
575            }
576
577            sender.send(self.status_peers_with_ids());
578
579            deleg
580        }
581
582        /// Return every known peer's full domain [`Node`] (not the lossy [`StatusNode`]).
583        ///
584        /// Used by [`Runtime::file_targets`](crate::Runtime::file_targets), which needs the full node
585        /// (peerAPI address, owning user id, cap map) to compute Taildrop send targets. The self node
586        /// is not included (it lives in the control runner). Returns empty before the first netmap —
587        /// the natural "not connected yet" analog (an immediate answer, no queueing needed: callers
588        /// that need a populated list await `Running` first).
589        #[message]
590        pub fn all_peers(&self) -> Vec<Node> {
591            self.peer_db.peers().values().cloned().collect()
592        }
593
594        /// Resolve which node owns a tailnet source address.
595        ///
596        /// Maps the source IP of `addr` to the owning node via the tailnet-IP index, returning a
597        /// [`WhoIs`](crate::WhoIs). The port is ignored (a tailnet IP uniquely identifies a node).
598        ///
599        /// The resulting [`WhoIs`](crate::WhoIs) carries no user/login or capability data: this
600        /// fork's domain [`Node`] does not retain those wire fields. See the
601        /// [`status`](crate::status) module docs for the gap.
602        ///
603        /// Waits until we've received at least one peer update from control.
604        #[message(ctx)]
605        pub fn whois(
606            &mut self,
607            ctx: &mut Context<Self, DelegatedReply<Option<crate::status::WhoIs>>>,
608            addr: std::net::SocketAddr,
609        ) -> DelegatedReply<Option<crate::status::WhoIs>> {
610            let (deleg, sender) = ctx.reply_sender();
611            let Some(sender) = sender else { return deleg };
612
613            if !self.seen_state_update {
614                tracing::debug!(query = %addr, "no peer state seen yet, queueing whois request");
615                self.pending_requests
616                    .push(Pending::WhoIs(Whois { addr }, sender));
617                return deleg;
618            }
619
620            sender.send(self.whois_opt(addr));
621
622            deleg
623        }
624
625        /// Subscribe to netmap peer-change events.
626        ///
627        /// Returns a [`watch::Receiver`] whose value is the current set of peer
628        /// [`StatusNode`]s, updated on every netmap state update from control. Embedders can await
629        /// changes via [`watch::Receiver::changed`] to react to peers joining, leaving, or changing.
630        ///
631        /// The receiver's initial value is the peer set at subscription time (empty before the
632        /// first netmap update). This is a peer-only view; combine with the self node from
633        /// [`Runtime::status`](crate::Runtime::status) when a full snapshot is needed.
634        #[message(derive(Clone))]
635        pub fn watch_netmap(&self) -> watch::Receiver<Vec<StatusNode>> {
636            self.peer_watch.subscribe()
637        }
638    }
639}
640
641pub use msg_impl::*;
642
643#[derive(Debug, Clone)]
644pub(crate) struct PeerState {
645    #[allow(unused)]
646    pub deletions: HashSet<PeerId>,
647    #[allow(unused)]
648    pub upserts: HashSet<PeerId>,
649    pub peers: Arc<PeerDb>,
650}
651
652impl Message<Arc<ts_control::StateUpdate>> for PeerTracker {
653    type Reply = ();
654
655    async fn handle(
656        &mut self,
657        msg: Arc<ts_control::StateUpdate>,
658        _ctx: &mut Context<Self, Self::Reply>,
659    ) {
660        // Accumulate user profiles first — control sends them incrementally and a response may
661        // carry profiles with no peer delta (or peers that reference a profile from an earlier
662        // response), so this must happen before the no-peer-update early return below.
663        for profile in &msg.user_profiles {
664            self.user_profiles.insert(profile.id, profile.clone());
665        }
666
667        // Apply the standalone online/last-seen delta maps (channels C/D, `MapResponse.OnlineChange`
668        // / `PeerSeenChange`). These arrive keyed by control node id and may ride a response that
669        // carries NO `peer_update` (a bare online flip is the common case), so they must be applied
670        // *before* the no-peer-update early return — otherwise online status freezes at the last
671        // full-node/patch value. Each entry only ever *sets* a value (never back to unknown).
672        // Wall clock for a `PeerSeenChange: true` (Go uses `clock.Now()`). chrono is built without
673        // its `clock` feature in this workspace, so derive it from `SystemTime` the same way the
674        // control runner / ssh-policy paths do (unix secs → `DateTime::from_timestamp`).
675        let now = std::time::SystemTime::now()
676            .duration_since(std::time::UNIX_EPOCH)
677            .ok()
678            .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
679            .unwrap_or_default();
680        let liveness_changed =
681            self.apply_liveness_changes(&msg.online_change, &msg.peer_seen_change, now);
682
683        if msg.peer_update.is_none() && msg.peer_patches.is_empty() {
684            // No peer set or patch this response. If a liveness delta still mutated the netmap,
685            // publish the refreshed snapshot so watchers (and `GetStatus`) see the new online state.
686            if liveness_changed {
687                self.service_pending_requests();
688                self.peer_watch.send_replace(self.status_peers());
689                if let Err(e) = self
690                    .env
691                    .publish(Arc::new(PeerState {
692                        upserts: HashSet::default(),
693                        deletions: HashSet::default(),
694                        peers: Arc::new(self.peer_db.clone()),
695                    }))
696                    .await
697                {
698                    tracing::error!(error = %e, "publishing liveness-only peer state update");
699                }
700            }
701            return;
702        }
703
704        // Apply the whole-node peer set (if any) FIRST, then the field-level patches on top —
705        // mirroring Go's `controlclient` order (`Peers*` then `PeersChangedPatch`). A response may
706        // carry either, both, or (with a liveness-only delta) neither. Merge the upsert/deletion sets
707        // so the published `PeerState` reflects every node touched by both passes; a node both
708        // upserted by the set and patched stays in `upserts` (the patch removes it from `deletions`).
709        let (mut upserts, mut deletions) = msg
710            .peer_update
711            .as_ref()
712            .map(|u| self.apply_peer_update(u))
713            .unwrap_or_default();
714
715        if !msg.peer_patches.is_empty() {
716            let (patch_upserts, patch_deletions) = self.apply_peer_patches(&msg.peer_patches);
717            // A patch can evict a node the set just upserted (TKA rejection after key rotation), or
718            // re-admit/patch one not in the set — reconcile so each id lands in exactly one set.
719            for id in &patch_upserts {
720                deletions.remove(id);
721            }
722            for id in &patch_deletions {
723                upserts.remove(id);
724            }
725            upserts.extend(patch_upserts);
726            deletions.extend(patch_deletions);
727        }
728
729        tracing::debug!(
730            n_upsert = upserts.len(),
731            n_delete = deletions.len(),
732            peer_count = self.peer_db.peers().len(),
733            "new peer state"
734        );
735
736        self.service_pending_requests();
737
738        // Publish the latest peer snapshot to netmap watchers. `send_replace` keeps the receiver's
739        // value current even when there are no subscribers, so a late subscriber sees fresh state.
740        self.peer_watch.send_replace(self.status_peers());
741
742        if let Err(e) = self
743            .env
744            .publish(Arc::new(PeerState {
745                upserts,
746                deletions,
747                peers: Arc::new(self.peer_db.clone()),
748            }))
749            .await
750        {
751            tracing::error!(error = %e, "publishing peer state update");
752        }
753    }
754}
755
756impl Message<PeerDiscoKeyAdvertisement> for PeerTracker {
757    type Reply = ();
758
759    async fn handle(
760        &mut self,
761        msg: PeerDiscoKeyAdvertisement,
762        _ctx: &mut Context<Self, Self::Reply>,
763    ) {
764        if !self.learn_disco_key(msg.peer, msg.key) {
765            return;
766        }
767
768        // The key changed, so republish: the direct-path machinery resolves a peer's disco key out
769        // of the published `PeerState` snapshot (`direct::DiscoPeerLookup`), which is the whole
770        // point of learning it — it is what lets disco reach this peer without waiting for a
771        // netmap update. Go does the equivalent by writing the key straight into the magicsock
772        // endpoint and re-keying its peer map.
773        self.peer_watch.send_replace(self.status_peers());
774
775        if let Err(e) = self
776            .env
777            .publish(Arc::new(PeerState {
778                upserts: HashSet::from_iter([msg.peer]),
779                deletions: HashSet::default(),
780                peers: Arc::new(self.peer_db.clone()),
781            }))
782            .await
783        {
784            tracing::error!(error = %e, "publishing peer state after a TSMP disco-key advertisement");
785        }
786    }
787}
788
789/// Ask the peer tracker to re-broadcast its current peer snapshot on the bus, without any peer
790/// change. Sent after a runtime preference change so the route updater and source filter (both
791/// `Arc<PeerState>` subscribers) re-resolve against the new value immediately, rather than waiting
792/// for the next netmap update: `Device::set_exit_node` (new exit-node selector) and
793/// `Device::set_accept_routes` (new accept-routes flag) both send it.
794#[derive(Debug, Clone, Copy)]
795pub struct RepublishState;
796
797impl Message<RepublishState> for PeerTracker {
798    type Reply = ();
799
800    async fn handle(&mut self, _msg: RepublishState, _ctx: &mut Context<Self, Self::Reply>) {
801        // An empty upsert/deletion set: this is a re-broadcast of the unchanged peer set, not a
802        // delta. Subscribers recompute their routes/filters against the current peers and the
803        // (just-updated) runtime preferences (exit-node selector, accept-routes flag).
804        if let Err(e) = self
805            .env
806            .publish(Arc::new(PeerState {
807                upserts: HashSet::default(),
808                deletions: HashSet::default(),
809                peers: Arc::new(self.peer_db.clone()),
810            }))
811            .await
812        {
813            tracing::error!(error = %e, "re-publishing peer state after a runtime preference change");
814        }
815    }
816}
817
818impl PeerTracker {
819    /// Learn a peer's disco key from a TSMP disco-key advertisement, returning whether the
820    /// advertisement was applied.
821    ///
822    /// Go [`magicsock.Conn.HandleDiscoKeyAdvertisement`], reduced to the state this fork keeps:
823    /// Go stores the learned key on the magicsock endpoint and re-keys its peer map, whereas here
824    /// the peer db's `disco_key` (and its disco index) *is* the live lookup every direct-path
825    /// consumer reads. The key is recorded in the peer's [`EndpointDisco`] TSMP slot — never on top
826    /// of control's — and the peer db then carries whichever of the two is active, so the next
827    /// netmap cannot silently undo it ([`upsert_from_control`](Self::upsert_from_control)).
828    ///
829    /// The three refusals are Go's, in Go's order:
830    ///
831    /// 1. **A zero key is never learned.** Go checks it twice — `tstun` publishes only
832    ///    `if !Key.IsZero()`, and `HandleDiscoKeyAdvertisement` rejects it again. The dataplane
833    ///    already dropped it here too; this is the second check, kept because the cost of getting
834    ///    it wrong is a peer bound to an unusable key.
835    /// 2. **An unknown peer is ignored** (Go: "endpoint not found for node"). An advertisement
836    ///    never creates a peer — only control does — so one that arrives before or after the
837    ///    peer's netmap entry is a no-op, exactly like a `PeersChangedPatch` for an unknown node.
838    /// 3. **An unchanged key is a no-op**, so a peer re-advertising the key we already hold costs
839    ///    no upsert and no republish (Go counts this as
840    ///    `magicsock_tsmp_disco_key_advertisement_unchanged` and returns). "Unchanged" is measured
841    ///    against the **TSMP-learned** key (Go compares `epDisco.keyFromTSMP()`), NOT against the
842    ///    effective one: an advertisement that merely restates what control already told us is new
843    ///    information — it is the peer itself confirming the key — so it is recorded as the active
844    ///    TSMP key and survives control later dropping or contradicting it.
845    ///
846    /// The tailnet-lock gate is deliberately *not* re-run: unlike a `PeersChangedPatch`, an
847    /// advertisement cannot touch the node key or its TKA signature — only the disco key — so the
848    /// peer-trust decision that admitted this node is unchanged by definition.
849    ///
850    /// [`magicsock.Conn.HandleDiscoKeyAdvertisement`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/magicsock.go
851    fn learn_disco_key(&mut self, peer: PeerId, key: DiscoPublicKey) -> bool {
852        if disco_key_is_zero(&key) {
853            tracing::debug!(?peer, "TSMP-advertised disco key is the zero key; ignoring");
854            return false;
855        }
856
857        let Some((_id, existing)) = self.peer_db.get(&peer) else {
858            tracing::debug!(
859                ?peer,
860                "TSMP disco-key advertisement for unknown peer; ignoring"
861            );
862            return false;
863        };
864
865        let node_key = existing.node_key;
866        if self
867            .endpoint_disco
868            .get(&node_key)
869            .and_then(EndpointDisco::key_from_tsmp)
870            == Some(key)
871        {
872            tracing::trace!(?peer, "TSMP-advertised disco key is unchanged");
873            return false;
874        }
875
876        let mut node = existing.clone();
877        let disco = self.endpoint_disco.entry(node_key).or_default();
878        disco.update_from_tsmp(Some(key));
879        node.disco_key = disco.key();
880        self.peer_db.upsert(&node);
881
882        tracing::info!(
883            ?peer,
884            stable_id = ?node.stable_id,
885            %key,
886            "learned peer disco key from a TSMP advertisement"
887        );
888
889        true
890    }
891
892    /// Upsert a control-sourced [`Node`] into the peer db, resolving its disco key against anything
893    /// this peer has told us over TSMP first.
894    ///
895    /// Every node built from control goes through here — `Full`, `Delta { upsert }`, and a
896    /// `PeersChangedPatch` — so the three cannot diverge on which of the two keys wins. This is the
897    /// disco half of Go [`endpoint.updateFromNode`]: control's key is written through
898    /// [`EndpointDisco::update_from_control`] **only when it differs from what control last said**
899    /// (Go's `if discoKey != n.DiscoKey()` guard, which compares `keyFromControl()`, never the
900    /// effective key). So a netmap that merely restates the key control already sent leaves an
901    /// active TSMP key alone — which is the entire point of the advertisement, whose motivating case
902    /// is a peer whose key control has not caught up with. Control genuinely changing its mind still
903    /// wins, exactly as it does upstream.
904    ///
905    /// The node lands in the db carrying the *effective* key ([`EndpointDisco::key`]), so the disco
906    /// index and every direct-path consumer resolve against the key we would actually send to.
907    ///
908    /// [`endpoint.updateFromNode`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/endpoint.go
909    fn upsert_from_control(&mut self, node: &Node) -> PeerId {
910        let node_key = node.node_key;
911        let from_control = disco_key_from_control(node.disco_key);
912
913        let disco = self.endpoint_disco.entry(node_key).or_default();
914        if disco.key_from_control() != from_control {
915            disco.update_from_control(from_control);
916        }
917        let effective = disco.key();
918
919        // No key material from either source: Go nils the endpoint's `disco` pointer, so a peer
920        // that has never had a disco key costs us no entry either.
921        if disco.is_empty() {
922            self.endpoint_disco.remove(&node_key);
923        }
924
925        if effective == node.disco_key {
926            return self.peer_db.upsert(node);
927        }
928
929        let mut node = node.clone();
930        node.disco_key = effective;
931        self.peer_db.upsert(&node)
932    }
933
934    /// The disco key control last gave us for `node_key` — Go `endpointDisco.keyFromControl()`.
935    fn control_disco_key(&self, node_key: &NodePublicKey) -> Option<DiscoPublicKey> {
936        self.endpoint_disco
937            .get(node_key)
938            .and_then(EndpointDisco::key_from_control)
939    }
940
941    /// Drop [`EndpointDisco`] state for node keys the peer db no longer holds.
942    ///
943    /// Go gets this for free: the two keys live on the magicsock `endpoint`, which the peer map keys
944    /// by node key and deletes when the peer leaves the netmap — and a peer that rotates its node
945    /// key gets a brand-new endpoint, so a TSMP-learned key is not carried across a rotation. Here
946    /// the state is a side table, so every control update prunes it to get the same lifetime.
947    fn prune_endpoint_disco(&mut self) {
948        if self.endpoint_disco.is_empty() {
949            return;
950        }
951
952        let peers = &self.peer_db;
953        self.endpoint_disco
954            .retain(|node_key, _| peers.has(node_key).is_some());
955    }
956
957    /// Apply a single [`PeerUpdate`](ts_control::PeerUpdate) to the peer db, enforcing the
958    /// Tailnet-Lock peer-trust chokepoint ([`tka_admits`](Self::tka_admits)) at every upsert site.
959    ///
960    /// This is the **single source of truth** for the peer-trust enforcement loop: the actor's
961    /// netmap [`handle`](Message::handle) calls it, and so do the TKA enforcement tests, so the two
962    /// real upsert sites (`Full` and `Delta { upsert }`) cannot diverge from what is tested.
963    ///
964    /// Returns `(upserts, deletions)` — the [`PeerId`]s touched — for downstream bookkeeping.
965    fn apply_peer_update(
966        &mut self,
967        peer_update: &ts_control::PeerUpdate,
968    ) -> (HashSet<PeerId>, HashSet<PeerId>) {
969        let mut upserts = HashSet::default();
970        let mut deletions = HashSet::default();
971
972        match peer_update {
973            ts_control::PeerUpdate::Full(new_nodes) => {
974                tracing::trace!("full peer update");
975
976                // Borrow the authority ONCE for the whole batch and verify each peer EXACTLY once
977                // (Go runs `tkaFilterNetmapLocked` once over the assembled netmap; an earlier draft
978                // verified every peer twice — once for `retained_ids`, once in the upsert loop —
979                // doubling the ed25519 cost on the hot resync path). The per-node verdict vector
980                // `admits` is computed once and drives both the `retain` (evict revoked peers, keyed
981                // by stable_id) and the upsert loop (skip rejected peers, by the node's OWN verdict).
982                // Keeping a per-node verdict (not just a stable_id set) means a node whose own
983                // signature fails is never admitted on the strength of a different node that happens
984                // to share its stable_id — matching the old per-node re-verify for that degenerate
985                // (malformed-control) input.
986                //
987                // Revocation evicts: a peer re-included with a now-invalid/missing signature under an
988                // active authority fails its verdict, so it is excluded from `retained_ids` and
989                // `retain` drops the stale (previously-admitted) entry. With no authority the snapshot
990                // is `None`, so every node passes — byte-for-byte the pre-TKA behavior (no regression).
991                let authority = self.tka_authority_snapshot();
992                let verdicts = new_nodes
993                    .iter()
994                    .map(|node| Self::tka_snapshot_admits(authority.as_deref(), node))
995                    .collect::<Vec<_>>();
996
997                // Cross-peer rotation filter (Go `rotationTracker`): from the SAME verify pass above,
998                // feed every admitted, rotation-signed peer's details to the tracker, then drop any
999                // peer presenting a node key a newer rotation has superseded (or a tied clone). This
1000                // is whole-netmap by nature — one peer's chain obsoletes another's key — so it lives
1001                // here, not in the per-peer verdict, matching Go's single pass over `nm.Peers`.
1002                let mut rotation = RotationTracker::default();
1003                for (node, verdict) in new_nodes.iter().zip(&verdicts) {
1004                    if verdict.admitted
1005                        && let Some(details) = &verdict.rotation
1006                    {
1007                        rotation.add(node.node_key.to_bytes().to_vec(), details);
1008                    }
1009                }
1010                let obsolete = rotation.obsolete_keys();
1011
1012                // Final per-node keep verdict: admitted by the per-peer check AND not rotation-obsolete.
1013                // Drives both the `retain` (evict) and the upsert loop, so a node whose own signature
1014                // fails — or whose key was rotated away — is never admitted on the strength of a
1015                // stable_id twin.
1016                let keep = new_nodes
1017                    .iter()
1018                    .zip(&verdicts)
1019                    .map(|(node, v)| {
1020                        // `contains` takes `&[u8]` (HashSet<Vec<u8>> borrows as a slice) — no alloc.
1021                        v.admitted && !obsolete.contains(&node.node_key.to_bytes()[..])
1022                    })
1023                    .collect::<Vec<bool>>();
1024
1025                // `retained_ids` is the set of stable_ids that survive (drives `retain` to evict the
1026                // rest). It must agree with what the upsert loop below will leave in the db. Control
1027                // should never send two distinct nodes with the same `stable_id` in one `Full`, but if
1028                // it does, `peer_db.upsert` is last-writer-wins on `stable_id`, so the db ends holding
1029                // the LAST kept node for that id. Build `retained_ids` from kept nodes only — a
1030                // stable_id is retained iff at least one of its (possibly duplicate) nodes is kept, so
1031                // the upsert loop's last-kept node lands and `retain` never evicts a just-upserted id.
1032                let retained_ids = new_nodes
1033                    .iter()
1034                    .zip(keep.iter().copied())
1035                    .filter(|(_, k)| *k)
1036                    .map(|(node, _)| &node.stable_id)
1037                    .collect::<HashSet<_>>();
1038
1039                // Isolation diagnostic: an ACTIVE lock that authorized none of the offered peers
1040                // leaves this node with no peers — surface it loudly so a self-lockout (vs an attack)
1041                // is diagnosable. `authority.is_some()` means a real keyed lock (the empty-keyset
1042                // brick-guard admits-all, so it never reaches here with zero retained).
1043                if authority.is_some() && !new_nodes.is_empty() && retained_ids.is_empty() {
1044                    tracing::error!(
1045                        offered = new_nodes.len(),
1046                        "TKA: active lock authorized ZERO of the offered peers; node is isolated \
1047                         (verify the lock state, or disable tailnet lock to recover)"
1048                    );
1049                }
1050
1051                self.peer_db.retain(|id, peer| {
1052                    let retain = retained_ids.contains(&peer.stable_id);
1053
1054                    if !retain {
1055                        deletions.insert(id);
1056                    }
1057
1058                    retain
1059                });
1060
1061                for (node, k) in new_nodes.iter().zip(keep.iter().copied()) {
1062                    if !k {
1063                        continue; // fail-CLOSED: rejected by tailnet lock or rotation-obsolete (above)
1064                    }
1065                    let peer_id = self.upsert_from_control(node);
1066                    upserts.insert(peer_id);
1067                }
1068            }
1069
1070            ts_control::PeerUpdate::Delta { remove, upsert } => {
1071                tracing::trace!("delta peer update");
1072
1073                for peer in upsert {
1074                    if !self.tka_admits(peer) {
1075                        // fail-CLOSED: do not upsert a peer rejected by tailnet lock. If the peer is
1076                        // ALREADY in the db (a delta re-upserting an existing peer whose signature is
1077                        // now invalid — e.g. revoked between syncs), evict the stale entry rather than
1078                        // leaving an unverified peer admitted; Go re-filters the whole netmap each map
1079                        // response, so a now-unsigned peer would not survive there either.
1080                        if let Some((id, _)) = self.peer_db.remove(&peer.stable_id) {
1081                            tracing::warn!(
1082                                stable_id = ?peer.stable_id,
1083                                "TKA: delta re-upsert rejected; evicting now-unauthorized peer"
1084                            );
1085                            deletions.insert(id);
1086                        }
1087                        continue;
1088                    }
1089                    let id = self.upsert_from_control(peer);
1090
1091                    upserts.insert(id);
1092                }
1093
1094                for peer in remove {
1095                    let Some((id, _node)) = self.peer_db.remove(peer) else {
1096                        // A benign, expected race: the peer may already be gone (dropped in a prior
1097                        // `Full`, or fail-closed by TKA — whose now-"unknown" ids commonly reappear in
1098                        // a trailing `peers_removed`). Go treats an unknown removal as a no-op; log at
1099                        // debug, not error, to avoid false-alarm noise on a healthy node (matches the
1100                        // unknown-node handling in `apply_peer_patches`).
1101                        tracing::debug!(
1102                            control_node_id = peer,
1103                            "removed peer was unknown; ignoring"
1104                        );
1105                        continue;
1106                    };
1107
1108                    deletions.insert(id);
1109                }
1110            }
1111        }
1112
1113        self.prune_endpoint_disco();
1114
1115        (upserts, deletions)
1116    }
1117
1118    /// Apply field-level peer patches (`MapResponse.PeersChangedPatch`), returning the upserted /
1119    /// deleted [`PeerId`]s.
1120    ///
1121    /// This is a SEPARATE channel from [`apply_peer_update`](Self::apply_peer_update): Go's
1122    /// `controlclient` applies the whole-node `Peers*` set first and then `PeersChangedPatch`, so a
1123    /// response that carries both has the peer set applied first (by the caller) and these patches
1124    /// applied second, on top of the freshly-synced nodes. A patch only mutates a peer already in the
1125    /// netmap; an unknown node id is ignored (the wire contract — a patch never creates a node).
1126    fn apply_peer_patches(
1127        &mut self,
1128        patches: &[ts_control::PeerChange],
1129    ) -> (HashSet<PeerId>, HashSet<PeerId>) {
1130        let mut upserts = HashSet::default();
1131        let mut deletions = HashSet::default();
1132
1133        tracing::trace!(n = patches.len(), "peer patch update");
1134
1135        for patch in patches {
1136            // Clone the current node, apply the present fields, and re-upsert through the same path
1137            // as a delta so indexes/routes stay consistent.
1138            let Some((_id, existing)) = self.peer_db.get(&patch.id) else {
1139                tracing::debug!(
1140                    control_node_id = patch.id,
1141                    "peer patch for unknown node; ignoring"
1142                );
1143                continue;
1144            };
1145
1146            let mut node = existing.clone();
1147            if let Some(endpoints) = &patch.underlay_addresses {
1148                node.underlay_addresses = endpoints.clone();
1149            }
1150            if let Some(derp) = patch.derp_region {
1151                node.derp_region = Some(derp);
1152            }
1153            if let Some(cap) = patch.cap {
1154                node.cap = cap;
1155            }
1156            if let Some(cap_map) = &patch.cap_map {
1157                node.cap_map = cap_map.clone();
1158            }
1159            // The db entry carries the EFFECTIVE disco key, which may have been learned over TSMP,
1160            // so restate what CONTROL last said before folding the patch in. Otherwise a patch that
1161            // says nothing about the disco key would hand a TSMP-learned key back as if control had
1162            // sent it, and `upsert_from_control` would read that as control having caught up —
1163            // deactivating the TSMP key on a patch that never mentioned it.
1164            node.disco_key = self.control_disco_key(&node.node_key);
1165            if let Some(disco_key) = patch.disco_key {
1166                node.disco_key = Some(disco_key);
1167            }
1168            if let Some(expiry) = patch.node_key_expiry {
1169                node.node_key_expiry = Some(expiry);
1170            }
1171            // Online/last-seen liveness deltas (`PeerChange.Online`/`LastSeen`) — the dominant
1172            // channel by which peer online transitions arrive mid-session. A patch only ever *sets*
1173            // a value (never patches back to unknown), so apply when present.
1174            if let Some(online) = patch.online {
1175                node.online = Some(online);
1176            }
1177            if let Some(last_seen) = patch.last_seen {
1178                node.last_seen = Some(last_seen);
1179            }
1180            // Key rotation: a patch may swap the node key (and its TKA signature). Apply both
1181            // together so the trust gate below verifies the new signature against the new key, never
1182            // a mismatched pair.
1183            if let Some(node_key) = patch.node_key {
1184                node.node_key = node_key;
1185            }
1186            if let Some(sig) = &patch.key_signature {
1187                node.key_signature = sig.clone();
1188            }
1189
1190            // Re-run the tailnet-lock gate on the patched node: a patch that rotates the key must
1191            // satisfy the active authority, exactly like a `Delta` upsert, or it would be a
1192            // trust-enforcement bypass. fail-CLOSED — if the patched node is no longer admitted,
1193            // evict it rather than keep the stale (now-unverified) entry.
1194            if !self.tka_admits(&node) {
1195                if let Some((id, _)) = self.peer_db.remove(&patch.id) {
1196                    tracing::warn!(
1197                        control_node_id = patch.id,
1198                        "peer patch rejected by tailnet lock; evicting peer"
1199                    );
1200                    deletions.insert(id);
1201                }
1202                continue;
1203            }
1204
1205            let id = self.upsert_from_control(&node);
1206            upserts.insert(id);
1207        }
1208
1209        self.prune_endpoint_disco();
1210
1211        (upserts, deletions)
1212    }
1213
1214    /// Apply the standalone online/last-seen delta maps (`MapResponse.OnlineChange` /
1215    /// `PeerSeenChange`, channels C/D) onto the retained netmap. Returns `true` if any node was
1216    /// actually mutated (so the caller knows whether to re-publish).
1217    ///
1218    /// Mirrors Go `controlclient/map.go:updatePeersStateFromResponse` (the two channels are
1219    /// semantically DISTINCT and must not be conflated):
1220    /// - `OnlineChange` (channel C) is the sole driver of a peer's `online` flag (`mut.Online = v`).
1221    /// - `PeerSeenChange` (channel D) is the sole driver of `last_seen`: `true ⇒ LastSeen = now`,
1222    ///   `false ⇒ LastSeen = nil` (cleared). It NEVER touches `online` — "not seen recently" is not
1223    ///   the same as "offline", which only `OnlineChange` asserts.
1224    ///
1225    /// Each entry is keyed by control node id and applies to a peer already in the netmap; an unknown
1226    /// node id is ignored (these maps never create a node). `now` is the wall-clock timestamp for a
1227    /// `PeerSeenChange: true` (Go uses `clock.Now()`); the caller passes it so this stays a pure
1228    /// function of its inputs. Returns `true` if any node was actually mutated.
1229    fn apply_liveness_changes(
1230        &mut self,
1231        online_change: &std::collections::BTreeMap<ts_control::NodeId, bool>,
1232        peer_seen_change: &std::collections::BTreeMap<ts_control::NodeId, bool>,
1233        now: chrono::DateTime<chrono::Utc>,
1234    ) -> bool {
1235        let mut changed = false;
1236
1237        // Channel C — direct online flips (the only writer of `online`).
1238        for (&node_id, &online) in online_change {
1239            if let Some((_pid, existing)) = self.peer_db.get(&node_id)
1240                && existing.online != Some(online)
1241            {
1242                let mut node = existing.clone();
1243                node.online = Some(online);
1244                self.peer_db.upsert(&node);
1245                changed = true;
1246            }
1247        }
1248
1249        // Channel D — peer-seen flips (the only writer of `last_seen`; never touches `online`).
1250        // `true` ⇒ last-seen is now; `false` ⇒ last-seen cleared (Go map.go:820-830).
1251        for (&node_id, &seen) in peer_seen_change {
1252            let new_last_seen = if seen { Some(now) } else { None };
1253            if let Some((_pid, existing)) = self.peer_db.get(&node_id)
1254                && existing.last_seen != new_last_seen
1255            {
1256                let mut node = existing.clone();
1257                node.last_seen = new_last_seen;
1258                self.peer_db.upsert(&node);
1259                changed = true;
1260            }
1261        }
1262
1263        changed
1264    }
1265
1266    /// Test-only constructor: build a [`PeerTracker`] with a chosen initial TKA authority without
1267    /// going through the actor `on_start` path. Returns the tracker plus the **`watch::Sender`** for
1268    /// its enforcement-authority cell, so a test can drive the exact enable/disable transitions the
1269    /// control runner drives at runtime (`tx.send_replace(Some(..))` ⇒ enforce, `tx.send_replace(None)`
1270    /// ⇒ clear). The initial `Some` exercises the fail-closed chokepoint
1271    /// ([`tka_admits`](Self::tka_admits)); `None` is the no-lock admit-all path. The returned sender
1272    /// must be kept alive for the tracker to read updated values.
1273    #[cfg(test)]
1274    fn for_test(
1275        env: Env,
1276        tka_authority: Option<ts_tka::Authority>,
1277    ) -> (Self, watch::Sender<Option<Arc<ts_tka::Authority>>>) {
1278        let (peer_watch, _) = watch::channel(Vec::new());
1279        let (tka_tx, tka_rx) = watch::channel(tka_authority.map(Arc::new));
1280        let tracker = Self {
1281            peer_db: PeerDb::default(),
1282            seen_state_update: false,
1283            pending_requests: Vec::new(),
1284            peer_watch,
1285            user_profiles: HashMap::new(),
1286            endpoint_disco: HashMap::new(),
1287            tka_authority: tka_rx,
1288            env,
1289        };
1290        (tracker, tka_tx)
1291    }
1292
1293    fn service_pending_requests(&mut self) {
1294        if self.seen_state_update {
1295            return;
1296        }
1297
1298        self.seen_state_update = true;
1299
1300        if !self.pending_requests.is_empty() {
1301            tracing::debug!(
1302                n_pending = self.pending_requests.len(),
1303                "state update received, servicing pending requests"
1304            );
1305        }
1306
1307        for req in core::mem::take(&mut self.pending_requests) {
1308            match req {
1309                Pending::PeerByName(PeerByName { name }, reply) => {
1310                    reply.send(self.peer_by_name_opt(&name).cloned());
1311                }
1312                Pending::TailnetIp(PeerByTailnetIp { ip }, reply) => {
1313                    reply.send(self.peer_by_tailnet_ip_opt(ip).cloned());
1314                }
1315                Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, reply) => {
1316                    reply.send(
1317                        self.peer_db
1318                            .get_route(ip.into())
1319                            .map(|(_id, node)| node.clone())
1320                            .collect(),
1321                    );
1322                }
1323                Pending::Status(reply) => {
1324                    reply.send(self.status_peers_with_ids());
1325                }
1326                Pending::WhoIs(Whois { addr }, reply) => {
1327                    reply.send(self.whois_opt(addr));
1328                }
1329            }
1330        }
1331    }
1332}
1333
1334#[cfg(test)]
1335mod tka_tests {
1336    //! Tailnet-Lock (TKA) enforcement tests for the peer-trust chokepoint.
1337    //!
1338    //! These exercise [`PeerTracker::tka_admits`] and the `tka_admits ⇒ upsert` loop the netmap
1339    //! handler runs. The test [`ts_tka::Authority`] is built with [`ts_tka::Authority::from_state`]
1340    //! over a known Ed25519 trusted key, and the signed node-key signature CBOR is produced through
1341    //! `ts_tka`'s public `cbor` encoder + `aum_hash` (the exact same canonical bytes `ts_tka`'s own
1342    //! `direct_signature_verifies_end_to_end` test signs, with no new crypto vectors invented and no
1343    //! private `ts_tka` API used).
1344
1345    use ed25519_dalek::{Signer, SigningKey};
1346    use ts_control::{Node, StableNodeId, TailnetAddress};
1347    use ts_tka::{
1348        AumHash, Authority, Key, KeyKind, State,
1349        cbor::{self, Value},
1350    };
1351
1352    use super::*;
1353
1354    /// `SigKind::Direct` wire value (Go `SigKind`; `ts_tka::SigKind::Direct = 1`).
1355    const SIG_KIND_DIRECT: u64 = 1;
1356
1357    /// The 32-byte node key used across the signed-peer fixtures.
1358    const NODE_KEY_BYTES: [u8; 32] = [7u8; 32];
1359
1360    /// Build a real [`Env`] for the tracker. Only the bus/keys/shutdown plumbing matters here; the
1361    /// TKA gate reads neither, so the forwarding preferences are all benign defaults.
1362    pub(super) fn test_env() -> Env {
1363        let (_shutdown_tx, shutdown_rx) = watch::channel(false);
1364        Env::new(
1365            ts_keys::NodeState::generate(),
1366            shutdown_rx,
1367            crate::env::ForwarderConfig {
1368                accept_routes: false,
1369                accept_dns: true,
1370                exit_node: None,
1371                forward_routes: Vec::new(),
1372                forward_tcp_ports: Vec::new(),
1373                forward_udp_ports: Vec::new(),
1374                forward_all_ports: false,
1375                forward_exit_egress: false,
1376                block_incoming: false,
1377                exit_proxy: None,
1378                peerapi_port: None,
1379                taildrop_dir: None,
1380                enable_ipv6: false,
1381                wireguard_listen_port: None,
1382                network_monitor: false,
1383                persistent_keepalive_interval: None,
1384                ingress_active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1385            },
1386        )
1387    }
1388
1389    /// A minimal peer [`Node`] carrying `node_key` and the given `key_signature`.
1390    pub(super) fn peer_node(stable_id: &str, node_key: [u8; 32], key_signature: Vec<u8>) -> Node {
1391        Node {
1392            id: 1,
1393            stable_id: StableNodeId(stable_id.to_string()),
1394            hostname: stable_id.to_string(),
1395            user_id: 0,
1396            tailnet: Some("ts.net".to_string()),
1397            tags: Vec::new(),
1398            addresses: vec![
1399                "100.64.0.1/32".parse().unwrap(),
1400                "fd7a:115c:a1e0::1/128".parse().unwrap(),
1401            ],
1402            tailnet_address: TailnetAddress {
1403                ipv4: "100.64.0.1/32".parse().unwrap(),
1404                ipv6: "fd7a:115c:a1e0::1/128".parse().unwrap(),
1405            },
1406            node_key: node_key.into(),
1407            node_key_expiry: None,
1408            online: None,
1409            last_seen: None,
1410            key_signature,
1411            machine_key: None,
1412            disco_key: None,
1413            accepted_routes: Vec::new(),
1414            underlay_addresses: Vec::new(),
1415            derp_region: None,
1416            cap: Default::default(),
1417            cap_map: Default::default(),
1418            peerapi_port: None,
1419            peerapi_dns_proxy: false,
1420            is_wireguard_only: false,
1421            exit_node_dns_resolvers: Vec::new(),
1422            peer_relay: false,
1423            ssh_host_keys: Vec::new(),
1424            service_vips: Default::default(),
1425        }
1426    }
1427
1428    /// Encode a `Direct` [`ts_tka::NodeKeySignature`] CBOR exactly as `ts_tka`'s private `to_cbor`
1429    /// does (int-map keys: 1=kind, 2=pubkey, 3=key_id, 4=signature; empty byte fields omitted),
1430    /// using only the crate's *public* `cbor` encoder. `signature` of `None` produces the
1431    /// signing-digest preimage (the `SigHash` form).
1432    fn direct_sig_cbor(node_key: &[u8], key_id: &[u8], signature: Option<&[u8]>) -> Vec<u8> {
1433        let mut pairs = alloc_pairs(node_key, key_id);
1434        if let Some(sig) = signature {
1435            pairs.push((4, Some(Value::Bytes(sig.to_vec()))));
1436        }
1437        cbor::int_map(pairs).to_vec()
1438    }
1439
1440    fn alloc_pairs(node_key: &[u8], key_id: &[u8]) -> Vec<(u64, Option<Value>)> {
1441        vec![
1442            (1, Some(Value::Uint(SIG_KIND_DIRECT))),
1443            (2, Some(Value::Bytes(node_key.to_vec()))),
1444            (3, Some(Value::Bytes(key_id.to_vec()))),
1445        ]
1446    }
1447
1448    /// Build a TKA [`Authority`] that trusts `signing.verifying_key()`, plus a valid `Direct`
1449    /// node-key signature CBOR authorizing [`NODE_KEY_BYTES`] under it.
1450    fn authority_and_valid_sig() -> (Authority, Vec<u8>) {
1451        // A fixed, known Ed25519 trusted key (mirrors ts_tka's own end-to-end test seed).
1452        let signing = SigningKey::from_bytes(&[42u8; 32]);
1453        let trusted_pub = signing.verifying_key().to_bytes().to_vec();
1454
1455        let authority = Authority::from_state(
1456            AumHash([0; 32]),
1457            State {
1458                keys: vec![Key {
1459                    kind: KeyKind::Ed25519,
1460                    votes: 1,
1461                    public: trusted_pub.clone(),
1462                }],
1463            },
1464        );
1465
1466        // SigHash preimage = canonical CBOR with the signature field omitted; sign its blake2s hash.
1467        let preimage = direct_sig_cbor(&NODE_KEY_BYTES, &trusted_pub, None);
1468        let sig_hash = ts_tka::aum_hash(&preimage).0;
1469        let signature = signing.sign(&sig_hash).to_bytes().to_vec();
1470
1471        let signed_cbor = direct_sig_cbor(&NODE_KEY_BYTES, &trusted_pub, Some(&signature));
1472        // Sanity: the authority accepts the signature we just built (same path the gate uses).
1473        assert!(
1474            authority
1475                .node_key_authorized(&NODE_KEY_BYTES, &signed_cbor)
1476                .is_ok()
1477        );
1478
1479        (authority, signed_cbor)
1480    }
1481
1482    #[tokio::test]
1483    async fn tka_inactive_upserts_all_peers() {
1484        // No authority ⇒ enforcement inactive ⇒ both a signed and an unsigned peer are admitted.
1485        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
1486
1487        let signed = peer_node("signed", [1u8; 32], vec![0xde, 0xad, 0xbe, 0xef]);
1488        let unsigned = peer_node("unsigned", [2u8; 32], vec![]);
1489
1490        assert!(tracker.tka_admits(&signed));
1491        assert!(tracker.tka_admits(&unsigned));
1492
1493        tracker.peer_db.upsert(&signed);
1494        tracker.peer_db.upsert(&unsigned);
1495        assert_eq!(tracker.peer_db.peers().len(), 2);
1496    }
1497
1498    #[tokio::test]
1499    async fn tka_active_rejects_unsigned_peer() {
1500        // Authority present + peer presents no signature ⇒ rejected (fail-closed), not in peer_db.
1501        let (authority, _sig) = authority_and_valid_sig();
1502        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1503
1504        let unsigned = peer_node("unsigned", NODE_KEY_BYTES, vec![]);
1505        assert!(!tracker.tka_admits(&unsigned));
1506
1507        // Mirror the handler's `if !tka_admits { continue }` loop.
1508        if tracker.tka_admits(&unsigned) {
1509            tracker.peer_db.upsert(&unsigned);
1510        }
1511        assert_eq!(tracker.peer_db.peers().len(), 0);
1512        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1513    }
1514
1515    #[tokio::test]
1516    async fn tka_active_rejects_bad_signature() {
1517        // Authority present + a signature that fails to verify ⇒ rejected, not in peer_db.
1518        let (authority, mut sig) = authority_and_valid_sig();
1519        // Tamper the last byte (the trailing signature byte) so verification fails.
1520        let last = sig.len() - 1;
1521        sig[last] ^= 0xff;
1522
1523        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1524        let bad = peer_node("bad", NODE_KEY_BYTES, sig);
1525        assert!(!tracker.tka_admits(&bad));
1526
1527        if tracker.tka_admits(&bad) {
1528            tracker.peer_db.upsert(&bad);
1529        }
1530        assert_eq!(tracker.peer_db.peers().len(), 0);
1531    }
1532
1533    #[tokio::test]
1534    async fn tka_active_admits_authorized_peer() {
1535        // Authority present + correctly-signed node key ⇒ admitted and upserted.
1536        let (authority, sig) = authority_and_valid_sig();
1537        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1538
1539        let good = peer_node("good", NODE_KEY_BYTES, sig);
1540        assert!(tracker.tka_admits(&good));
1541
1542        if tracker.tka_admits(&good) {
1543            tracker.peer_db.upsert(&good);
1544        }
1545        assert_eq!(tracker.peer_db.peers().len(), 1);
1546        assert!(tracker.peer_db.get(&good.node_key).is_some());
1547    }
1548
1549    // ---------------------------------------------------------------------------------------------
1550    // Tests that drive REAL `PeerUpdate`s through the shared handler body
1551    // ([`PeerTracker::apply_peer_update`], the single source of truth the actor's netmap `handle`
1552    // also calls), so the two real upsert sites (`Full` and `Delta { upsert }`) are exercised via
1553    // the actual enforcement path — not by hand-mirroring `if !tka_admits { continue }`.
1554    // ---------------------------------------------------------------------------------------------
1555
1556    #[tokio::test]
1557    async fn tka_active_delta_upsert_rejects_unauthorized() {
1558        // Drive a real `Delta { upsert }` whose peer carries no signature. The Delta upsert site
1559        // must reject it under an active authority ⇒ not present in peer_db after the handler runs.
1560        let (authority, _sig) = authority_and_valid_sig();
1561        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1562
1563        let unsigned = peer_node("unsigned", NODE_KEY_BYTES, vec![]);
1564        let update = ts_control::PeerUpdate::Delta {
1565            upsert: vec![unsigned.clone()],
1566            remove: Vec::new(),
1567        };
1568
1569        tracker.apply_peer_update(&update);
1570
1571        assert_eq!(tracker.peer_db.peers().len(), 0);
1572        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1573    }
1574
1575    #[tokio::test]
1576    async fn tka_active_delta_upsert_admits_authorized() {
1577        // Drive a real `Delta { upsert }` with a correctly-signed peer ⇒ present in peer_db.
1578        let (authority, sig) = authority_and_valid_sig();
1579        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1580
1581        let good = peer_node("good", NODE_KEY_BYTES, sig);
1582        let update = ts_control::PeerUpdate::Delta {
1583            upsert: vec![good.clone()],
1584            remove: Vec::new(),
1585        };
1586
1587        tracker.apply_peer_update(&update);
1588
1589        assert_eq!(tracker.peer_db.peers().len(), 1);
1590        assert!(tracker.peer_db.get(&good.node_key).is_some());
1591    }
1592
1593    #[tokio::test]
1594    async fn tka_active_full_admits_only_authorized_in_mixed_batch() {
1595        // Drive a real `Full` carrying a MIX of authorized + unauthorized peers. Only the
1596        // correctly-signed peer survives the Full upsert site; the unsigned and bad-sig peers are
1597        // dropped fail-closed.
1598        let (authority, sig) = authority_and_valid_sig();
1599        // A bad-sig variant of the same authorized signature (tamper the trailing byte).
1600        let mut bad_sig = sig.clone();
1601        let last = bad_sig.len() - 1;
1602        bad_sig[last] ^= 0xff;
1603
1604        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1605
1606        // Only the authorized peer carries NODE_KEY_BYTES (the key the authority signed); the
1607        // rejected peers use distinct node keys so the survivor is unambiguous.
1608        let good = peer_node("good", NODE_KEY_BYTES, sig);
1609        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
1610        let bad = peer_node("bad", [9u8; 32], bad_sig);
1611
1612        let update =
1613            ts_control::PeerUpdate::Full(vec![good.clone(), unsigned.clone(), bad.clone()]);
1614
1615        tracker.apply_peer_update(&update);
1616
1617        assert_eq!(tracker.peer_db.peers().len(), 1);
1618        assert!(tracker.peer_db.get(&good.node_key).is_some());
1619        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1620        assert!(tracker.peer_db.get(&bad.node_key).is_none());
1621    }
1622
1623    /// End-to-end through the REAL enforcement-authority transport (the `watch` cell the control
1624    /// runner writes), not a direct field poke: writing `Some(authority)` flips enforcement on so a
1625    /// mixed batch drops the unsigned/bad peers, and a subsequent `None` (lock disabled) clears
1626    /// enforcement so a peer DROPPED while enforced is re-admitted. Exercises the exact `borrow`-based
1627    /// read path `tka_admits` uses — a broken receiver wiring would pass every for_test-field test but
1628    /// fail here.
1629    #[tokio::test]
1630    async fn tka_authority_watch_enables_then_clears_enforcement() {
1631        let (authority, sig) = authority_and_valid_sig();
1632        let mut bad_sig = sig.clone();
1633        let last = bad_sig.len() - 1;
1634        bad_sig[last] ^= 0xff;
1635
1636        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
1637
1638        // 1) No authority yet ⇒ admit-all (Go b.tka == nil).
1639        let good = peer_node("good", NODE_KEY_BYTES, sig.clone());
1640        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
1641        let bad = peer_node("bad", [9u8; 32], bad_sig);
1642        let batch = ts_control::PeerUpdate::Full(vec![good.clone(), unsigned.clone(), bad.clone()]);
1643        tracker.apply_peer_update(&batch);
1644        assert_eq!(tracker.peer_db.peers().len(), 3, "no lock ⇒ admit all");
1645
1646        // 2) Publish the verified authority over the watch cell (exactly what the control runner does
1647        //    on a successful sync) ⇒ enforcement ON. A re-applied Full now drops unsigned + bad.
1648        tka_tx.send_replace(Some(Arc::new(authority)));
1649        tracker.apply_peer_update(&batch);
1650        assert_eq!(
1651            tracker.peer_db.peers().len(),
1652            1,
1653            "lock active ⇒ only the signed peer survives"
1654        );
1655        assert!(tracker.peer_db.get(&good.node_key).is_some());
1656        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1657        assert!(tracker.peer_db.get(&bad.node_key).is_none());
1658
1659        // 3) Lock disabled (None) ⇒ enforcement cleared ⇒ a peer that was DROPPED while enforced is
1660        //    re-admitted by a fresh netmap. Assert the specific previously-dropped key returns (not
1661        //    merely a count), so this proves the drop→clear→re-admit transition, not "admit-all-fresh".
1662        tka_tx.send_replace(None);
1663        tracker.apply_peer_update(&batch);
1664        assert_eq!(
1665            tracker.peer_db.peers().len(),
1666            3,
1667            "lock disabled ⇒ admit all again"
1668        );
1669        assert!(
1670            tracker.peer_db.get(&unsigned.node_key).is_some(),
1671            "the peer dropped under enforcement must come back once the lock is cleared"
1672        );
1673        assert!(tracker.peer_db.get(&bad.node_key).is_some());
1674    }
1675
1676    /// Degenerate input: two DISTINCT nodes sharing one `stable_id` in a single `Full`, one with a
1677    /// valid signature and one unsigned, under an active lock. Each node is judged by its OWN verdict
1678    /// (the per-node `admits` vector), so the unsigned node is never admitted on the strength of its
1679    /// signed twin. The single-verify `Full` refactor keeps this per-node semantics (a stable_id-set
1680    /// alone would have admitted whichever node was upserted last). Malformed control input; asserted
1681    /// only to lock the verdict-per-node behavior against regression.
1682    #[tokio::test]
1683    async fn tka_full_duplicate_stable_id_judges_each_node_on_its_own_signature() {
1684        let (authority, sig) = authority_and_valid_sig();
1685        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1686
1687        // Both carry stable_id "dup"; the signed one authorizes NODE_KEY_BYTES, the other is unsigned
1688        // and uses a different node key. Order them unsigned-last so a last-writer-wins stable_id set
1689        // would (wrongly) leave the unsigned node's key in the db.
1690        let signed = peer_node("dup", NODE_KEY_BYTES, sig);
1691        let unsigned = peer_node("dup", [8u8; 32], vec![]);
1692        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
1693            signed.clone(),
1694            unsigned.clone(),
1695        ]));
1696
1697        // The unsigned node's own verdict failed, so its key must NOT be present, regardless of the
1698        // shared stable_id. (The signed twin retained the stable_id; the db holds the signed key.)
1699        assert!(
1700            tracker.peer_db.get(&unsigned.node_key).is_none(),
1701            "a node whose own signature fails must not be admitted via a stable_id twin"
1702        );
1703        assert!(tracker.peer_db.get(&signed.node_key).is_some());
1704    }
1705
1706    /// Full-path consistency under two KEPT nodes sharing a `stable_id`: `peer_db.upsert` is
1707    /// last-writer-wins on `stable_id`, so the db ends holding exactly one node for that id (the last
1708    /// kept), and `retain` never evicts that just-upserted id (`retained_ids` contains the shared id
1709    /// because at least one of its nodes was kept). No lock here, so both nodes are "kept". This pins
1710    /// the published-state invariant the whole-surface audit flagged: `retain` and the upsert loop
1711    /// agree on the surviving stable_id. Malformed control input; asserted for robustness.
1712    #[tokio::test]
1713    async fn tka_full_duplicate_stable_id_both_kept_is_consistent() {
1714        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
1715        let first = peer_node("dup", [1u8; 32], vec![]);
1716        let last = peer_node("dup", [2u8; 32], vec![]);
1717        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
1718            first.clone(),
1719            last.clone(),
1720        ]));
1721
1722        // Exactly one db entry for the shared stable_id, holding the LAST node (upsert is
1723        // last-writer-wins on stable_id); the first node's key was transparently superseded.
1724        assert_eq!(
1725            tracker.peer_db.peers().len(),
1726            1,
1727            "one entry for the shared stable_id"
1728        );
1729        assert!(
1730            tracker.peer_db.get(&last.node_key).is_some(),
1731            "the db holds the last-upserted node for the shared id"
1732        );
1733        assert!(
1734            tracker.peer_db.get(&first.node_key).is_none(),
1735            "the first node's key was superseded by the last at the shared id"
1736        );
1737    }
1738
1739    /// A peer admitted in one `Full`, then in a later `Full` presenting a key that a co-resident
1740    /// peer's rotation chain has rotated away, is EVICTED — the cross-peer rotation filter applies on
1741    /// every resync, not only at first admission. Exercises the rotation filter through two
1742    /// sequential `Full` updates with real signing.
1743    #[tokio::test]
1744    async fn tka_full_rotation_obsolete_evicts_on_resync() {
1745        use ed25519_dalek::SigningKey;
1746        use ts_tka::NodeKeySignature;
1747
1748        let trusted = SigningKey::from_bytes(&[42u8; 32]);
1749        let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
1750        let authority = Authority::from_state(
1751            AumHash([0; 32]),
1752            State {
1753                keys: vec![Key {
1754                    kind: KeyKind::Ed25519,
1755                    votes: 1,
1756                    public: trusted_pub.clone(),
1757                }],
1758            },
1759        );
1760        let pivot = SigningKey::from_bytes(&[9u8; 32]);
1761        let pivot_pub: [u8; 32] = pivot.verifying_key().to_bytes();
1762
1763        // First Full: the soon-to-be-stale peer presents the pivot key with a valid Direct sig.
1764        let stale_sig = NodeKeySignature::sign_direct(&pivot_pub, &trusted).serialize();
1765        let stale_peer = peer_node("stale", pivot_pub, stale_sig);
1766        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1767        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![stale_peer.clone()]));
1768        assert!(
1769            tracker.peer_db.get(&stale_peer.node_key).is_some(),
1770            "the stale peer is admitted while no rotation has superseded it yet"
1771        );
1772
1773        // Second Full: a freshly-rotated peer (whose chain rotated AWAY the pivot key) joins, and the
1774        // stale peer is re-included. The rotation filter now obsoletes the pivot key ⇒ stale evicted.
1775        let new_key = [4u8; 32];
1776        let new_sig = NodeKeySignature::sign_rotation(&new_key, &trusted, &pivot).serialize();
1777        let new_peer = peer_node("rotated", new_key, new_sig);
1778        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
1779            new_peer.clone(),
1780            stale_peer.clone(),
1781        ]));
1782        assert!(
1783            tracker.peer_db.get(&new_peer.node_key).is_some(),
1784            "the freshly-rotated peer is admitted"
1785        );
1786        assert!(
1787            tracker.peer_db.get(&stale_peer.node_key).is_none(),
1788            "the stale peer is EVICTED on the resync once a rotation supersedes its key"
1789        );
1790    }
1791
1792    /// The empty-trusted-key-state brick-guard: an authority with no keys must NOT drop the whole
1793    /// netmap (a `ts_tka` invariant violation / replayer edge). A verified chain always carries ≥1
1794    /// key, so this never weakens a genuine lock — it only prevents a black-hole. Uses ≥2 peers
1795    /// (one signed, one unsigned) to prove it admits **all**, not accidentally just one.
1796    #[tokio::test]
1797    async fn tka_empty_keyset_authority_admits_all() {
1798        use ts_tka::{AumHash, Authority, State};
1799        let empty_auth = Authority::from_state(AumHash([0u8; 32]), State { keys: Vec::new() });
1800        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(empty_auth));
1801        let signed = peer_node("signed", [7u8; 32], vec![0xde, 0xad]);
1802        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
1803        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
1804            signed.clone(),
1805            unsigned.clone(),
1806        ]));
1807        assert_eq!(
1808            tracker.peer_db.peers().len(),
1809            2,
1810            "an empty-keyset authority must admit ALL peers (brick-guard), not enforce"
1811        );
1812    }
1813
1814    /// Signature-replay / `NodeKeyMismatch`: a structurally-valid signature that authorizes
1815    /// `NODE_KEY_BYTES` must NOT admit a DIFFERENT node key carrying that same signature blob. This is
1816    /// the highest-value bypass — if the sig↔node-key binding in `verify_signature` were dropped, this
1817    /// is the only test that would catch it (the other "bad" peers only flip a byte ⇒ `BadSignature`).
1818    #[tokio::test]
1819    async fn tka_active_rejects_valid_sig_for_wrong_node_key() {
1820        let (authority, sig) = authority_and_valid_sig();
1821        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1822
1823        // The signature authorizes NODE_KEY_BYTES; attach it to an imposter with a different key.
1824        let imposter = peer_node("imposter", [0x55u8; 32], sig);
1825        assert!(
1826            !tracker.tka_admits(&imposter),
1827            "a signature bound to one node key must not authorize a different node key"
1828        );
1829        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![imposter.clone()]));
1830        assert!(tracker.peer_db.get(&imposter.node_key).is_none());
1831    }
1832
1833    /// `UntrustedKey`: a signature produced by a well-formed Ed25519 key that is NOT in the
1834    /// authority's trusted-key state must be rejected — distinct from a tampered-byte `BadSignature`.
1835    #[tokio::test]
1836    async fn tka_active_rejects_sig_from_untrusted_key() {
1837        use ed25519_dalek::{Signer, SigningKey};
1838        let (authority, _sig) = authority_and_valid_sig();
1839        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1840
1841        // Sign a valid CBOR with a DIFFERENT key (not the one the authority trusts). The key_id in
1842        // the signature names this untrusted key, so `get_key` misses ⇒ UntrustedKey.
1843        let rogue = SigningKey::from_bytes(&[99u8; 32]);
1844        let rogue_pub = rogue.verifying_key().to_bytes().to_vec();
1845        let preimage = direct_sig_cbor(&NODE_KEY_BYTES, &rogue_pub, None);
1846        let sig_hash = ts_tka::aum_hash(&preimage).0;
1847        let signature = rogue.sign(&sig_hash).to_bytes().to_vec();
1848        let rogue_cbor = direct_sig_cbor(&NODE_KEY_BYTES, &rogue_pub, Some(&signature));
1849
1850        let peer = peer_node("rogue-signed", NODE_KEY_BYTES, rogue_cbor);
1851        assert!(
1852            !tracker.tka_admits(&peer),
1853            "a signature from a key outside the trusted set must be rejected"
1854        );
1855        // Drive the real upsert path too (match the sibling replay test's depth): an untrusted-key
1856        // signature must keep the peer out of the db, not merely fail the verdict in isolation.
1857        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
1858        assert!(tracker.peer_db.get(&peer.node_key).is_none());
1859    }
1860
1861    /// Bus-enable analogue for `Delta`: enforcement engaged via the watch cell must also gate a
1862    /// `Delta { upsert }` (not only `Full`). Closes the "authority arrived over the transport AND the
1863    /// next update is a Delta" combination.
1864    #[tokio::test]
1865    async fn tka_watch_enable_enforces_delta_upsert() {
1866        let (authority, sig) = authority_and_valid_sig();
1867        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
1868        tka_tx.send_replace(Some(Arc::new(authority)));
1869
1870        let good = peer_node("good", NODE_KEY_BYTES, sig);
1871        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
1872        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
1873            remove: vec![],
1874            upsert: vec![good.clone(), unsigned.clone()],
1875        });
1876        assert!(tracker.peer_db.get(&good.node_key).is_some());
1877        assert!(
1878            tracker.peer_db.get(&unsigned.node_key).is_none(),
1879            "delta upsert under an active lock must drop the unsigned peer"
1880        );
1881    }
1882
1883    /// A `Delta` re-upsert of an ALREADY-ADMITTED peer whose signature is now invalid must EVICT the
1884    /// stale entry (revocation-via-delta), not leave it admitted. Go re-filters the whole netmap each
1885    /// response, so a now-unsigned peer would not survive there either.
1886    #[tokio::test]
1887    async fn tka_delta_reupsert_with_invalid_sig_evicts_existing() {
1888        let (authority, sig) = authority_and_valid_sig();
1889        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1890
1891        // Admit the signed peer.
1892        let good = peer_node("good", NODE_KEY_BYTES, sig.clone());
1893        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
1894        assert!(tracker.peer_db.get(&good.node_key).is_some());
1895
1896        // Re-upsert the SAME stable_id (now with no signature) via a delta ⇒ evicted, not retained.
1897        let revoked = peer_node("good", NODE_KEY_BYTES, vec![]);
1898        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
1899            remove: vec![],
1900            upsert: vec![revoked],
1901        });
1902        assert!(
1903            tracker.peer_db.get(&good.node_key).is_none(),
1904            "a delta re-upsert that fails the lock must evict the previously-admitted peer"
1905        );
1906    }
1907
1908    #[tokio::test]
1909    async fn tka_full_resync_revocation_behavior() {
1910        // Revocation-on-resync: admit a peer, then re-include the SAME stable_id in a `Full` with a
1911        // now-invalid signature. Per the Logic review finding, the pre-fix `retain` kept the stale
1912        // (previously-admitted) entry because membership was decided purely by stable_id.
1913        //
1914        // FIXED (not merely documented): the `Full` `retain` now keys on `tka_admits`-passing
1915        // stable_ids, so a peer whose re-included signature no longer verifies under the active
1916        // authority is EVICTED. This test asserts eviction. The inactive (authority=None) path is
1917        // provably unchanged — `tka_admits` always returns `true` there, so the retained set equals
1918        // the set of re-included stable_ids exactly (see `tka_inactive_full_resync_keeps_*`).
1919        let (authority, sig) = authority_and_valid_sig();
1920        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1921
1922        // 1) Admit the peer with a valid signature via a real `Full`.
1923        let good = peer_node("revoked", NODE_KEY_BYTES, sig.clone());
1924        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
1925        assert_eq!(tracker.peer_db.peers().len(), 1);
1926        assert!(tracker.peer_db.get(&good.node_key).is_some());
1927
1928        // 2) Re-sync the SAME stable_id, but with a now-invalid signature (tamper trailing byte).
1929        let mut bad_sig = sig;
1930        let last = bad_sig.len() - 1;
1931        bad_sig[last] ^= 0xff;
1932        let revoked = peer_node("revoked", NODE_KEY_BYTES, bad_sig);
1933        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![revoked.clone()]));
1934
1935        // Eviction: the stale entry is dropped because its re-included signature fails the gate.
1936        assert_eq!(tracker.peer_db.peers().len(), 0);
1937        assert!(tracker.peer_db.get(&revoked.node_key).is_none());
1938    }
1939
1940    #[tokio::test]
1941    async fn tka_inactive_full_resync_keeps_reincluded_peer() {
1942        // Guard the inactive (authority=None) path against the revocation fix: with no authority,
1943        // a peer re-included in a `Full` survives regardless of its signature bytes — byte-for-byte
1944        // pre-TKA behavior, proving the `Full` `retain` change does not regress the always-taken
1945        // branch this wave.
1946        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
1947
1948        let peer = peer_node("p", NODE_KEY_BYTES, vec![0xde, 0xad]);
1949        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
1950        assert_eq!(tracker.peer_db.peers().len(), 1);
1951
1952        // Re-sync the same stable_id with garbage signature bytes; inactive enforcement keeps it.
1953        let resynced = peer_node("p", NODE_KEY_BYTES, vec![0x00]);
1954        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![resynced.clone()]));
1955        assert_eq!(tracker.peer_db.peers().len(), 1);
1956        assert!(tracker.peer_db.get(&resynced.node_key).is_some());
1957    }
1958
1959    /// A `Patch` for a peer already in the netmap merges only the fields it carries — here new UDP
1960    /// endpoints and a new home DERP — leaving the rest of the node intact. This is the fix for
1961    /// dropped `peers_changed_patch`: without it the netmap keeps stale endpoints and the peer can
1962    /// never re-handshake after it moves.
1963    #[tokio::test]
1964    async fn patch_merges_endpoints_and_derp_into_existing_peer() {
1965        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
1966
1967        // Seed a peer (id == 1, per `peer_node`) with no endpoints / no DERP.
1968        let peer = peer_node("mover", [1u8; 32], vec![]);
1969        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
1970        let (_pid, before) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
1971        assert!(before.underlay_addresses.is_empty());
1972        assert!(before.derp_region.is_none());
1973
1974        // Patch in fresh reachability (the idle-peer-reconnect case).
1975        let new_ep: std::net::SocketAddr = "203.0.113.7:41641".parse().unwrap();
1976        let patch = ts_control::PeerChange {
1977            id: 1,
1978            derp_region: Some(ts_derp::RegionId(core::num::NonZeroU32::new(5).unwrap())),
1979            cap: None,
1980            cap_map: None,
1981            underlay_addresses: Some(vec![new_ep]),
1982            node_key: None,
1983            key_signature: None,
1984            disco_key: None,
1985            node_key_expiry: None,
1986            online: None,
1987            last_seen: None,
1988        };
1989        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
1990
1991        assert_eq!(upserts.len(), 1);
1992        assert_eq!(deletions.len(), 0);
1993        // Same peer, now carrying the patched endpoint + DERP; node key untouched.
1994        assert_eq!(tracker.peer_db.peers().len(), 1);
1995        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
1996        assert_eq!(after.underlay_addresses, vec![new_ep]);
1997        assert_eq!(
1998            after.derp_region,
1999            Some(ts_derp::RegionId(core::num::NonZeroU32::new(5).unwrap()))
2000        );
2001        assert_eq!(after.node_key, peer.node_key);
2002    }
2003
2004    /// Regression for `tsr-5u0`: when a whole-node set (`Delta`/`Full`) and a patch co-occur in one
2005    /// response, the patch is applied *on top of* the node the set just upserted — mirroring the
2006    /// handler's apply-order (peer set first, then `peer_patches`). Before the fix the patch shared
2007    /// the single `peer_update` slot and the co-occurring set silently dropped it, so a peer brought
2008    /// in by the delta kept stale (empty) reachability.
2009    #[tokio::test]
2010    async fn patch_applies_on_top_of_co_occurring_delta() {
2011        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2012
2013        // The whole-node delta upserts a brand-new peer (id == 1) with no reachability.
2014        let peer = peer_node("mover", [1u8; 32], vec![]);
2015        let (set_upserts, _) = tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
2016            upsert: vec![peer.clone()],
2017            remove: vec![],
2018        });
2019        assert_eq!(set_upserts.len(), 1, "delta upserts the new peer");
2020
2021        // The patch from the SAME response then sets that peer's endpoints + DERP. This is exactly
2022        // the consumer order the handler runs (apply_peer_update then apply_peer_patches).
2023        let new_ep: std::net::SocketAddr = "203.0.113.7:41641".parse().unwrap();
2024        let patch = ts_control::PeerChange {
2025            id: 1,
2026            derp_region: Some(ts_derp::RegionId(core::num::NonZeroU32::new(7).unwrap())),
2027            cap: None,
2028            cap_map: None,
2029            underlay_addresses: Some(vec![new_ep]),
2030            node_key: None,
2031            key_signature: None,
2032            disco_key: None,
2033            node_key_expiry: None,
2034            online: None,
2035            last_seen: None,
2036        };
2037        let (patch_upserts, patch_deletions) =
2038            tracker.apply_peer_patches(std::slice::from_ref(&patch));
2039
2040        assert_eq!(
2041            patch_upserts.len(),
2042            1,
2043            "patch re-upserts the just-added peer"
2044        );
2045        assert_eq!(patch_deletions.len(), 0);
2046        // The peer added by the delta now carries the patched reachability — the patch was NOT lost.
2047        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2048        assert_eq!(after.underlay_addresses, vec![new_ep]);
2049        assert_eq!(
2050            after.derp_region,
2051            Some(ts_derp::RegionId(core::num::NonZeroU32::new(7).unwrap()))
2052        );
2053    }
2054
2055    /// A `Patch` whose node id is not in the current netmap is ignored (the wire contract: a patch
2056    /// never creates a node). No upsert, no deletion, peer set unchanged.
2057    #[tokio::test]
2058    async fn patch_for_unknown_node_is_ignored() {
2059        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2060        let known = peer_node("known", [1u8; 32], vec![]); // id == 1
2061        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![known]));
2062
2063        let patch = ts_control::PeerChange {
2064            id: 999, // not in the netmap
2065            derp_region: None,
2066            cap: None,
2067            cap_map: None,
2068            underlay_addresses: Some(vec!["198.51.100.9:1".parse().unwrap()]),
2069            node_key: None,
2070            key_signature: None,
2071            disco_key: None,
2072            node_key_expiry: None,
2073            online: None,
2074            last_seen: None,
2075        };
2076        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
2077
2078        assert_eq!(upserts.len(), 0);
2079        assert_eq!(deletions.len(), 0);
2080        assert_eq!(tracker.peer_db.peers().len(), 1);
2081        assert!(tracker.peer_db.get(&(999 as ts_control::NodeId)).is_none());
2082    }
2083
2084    /// An expiry-only `Patch` updates `node_key_expiry` on the matching peer (Go
2085    /// `PeerChange.KeyExpiry`), rather than being silently dropped until the next full resync.
2086    #[tokio::test]
2087    async fn patch_updates_node_key_expiry() {
2088        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2089        let peer = peer_node("expiring", [1u8; 32], vec![]); // id == 1, node_key_expiry: None
2090        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2091
2092        let expiry = "2027-01-01T00:00:00Z"
2093            .parse::<chrono::DateTime<chrono::Utc>>()
2094            .unwrap();
2095        let patch = ts_control::PeerChange {
2096            id: 1,
2097            derp_region: None,
2098            cap: None,
2099            cap_map: None,
2100            underlay_addresses: None,
2101            node_key: None,
2102            key_signature: None,
2103            disco_key: None,
2104            node_key_expiry: Some(expiry),
2105            online: None,
2106            last_seen: None,
2107        };
2108        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2109
2110        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2111        assert_eq!(after.node_key_expiry, Some(expiry));
2112    }
2113
2114    /// Channel B: a `PeerChange.online` patch flips a peer's online state without a full node.
2115    #[tokio::test]
2116    async fn patch_updates_online() {
2117        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2118        let peer = peer_node("p", [1u8; 32], vec![]); // id == 1, online: None
2119        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2120        assert_eq!(
2121            tracker
2122                .peer_db
2123                .get(&(1 as ts_control::NodeId))
2124                .unwrap()
2125                .1
2126                .online,
2127            None
2128        );
2129
2130        let mut patch = ts_control::PeerChange {
2131            id: 1,
2132            derp_region: None,
2133            cap: None,
2134            cap_map: None,
2135            underlay_addresses: None,
2136            node_key: None,
2137            key_signature: None,
2138            disco_key: None,
2139            node_key_expiry: None,
2140            online: Some(true),
2141            last_seen: None,
2142        };
2143        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2144        assert_eq!(
2145            tracker
2146                .peer_db
2147                .get(&(1 as ts_control::NodeId))
2148                .unwrap()
2149                .1
2150                .online,
2151            Some(true),
2152            "PeerChange.online=Some(true) marks the peer online"
2153        );
2154
2155        // A subsequent patch flips it offline.
2156        patch.online = Some(false);
2157        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2158        assert_eq!(
2159            tracker
2160                .peer_db
2161                .get(&(1 as ts_control::NodeId))
2162                .unwrap()
2163                .1
2164                .online,
2165            Some(false)
2166        );
2167    }
2168
2169    /// Channel C/D (Go `map.go:updatePeersStateFromResponse`): `online_change` is the sole driver of
2170    /// `online`; `peer_seen_change` is the sole driver of `last_seen` (true ⇒ now, false ⇒ cleared)
2171    /// and must NEVER touch `online`. Both apply to a peer already in the netmap and ignore unknown
2172    /// ids. This pins the fix for the prior bug where channel D wrote `online=false` (conflating
2173    /// "not seen recently" with "offline" — distinct signals in Go).
2174    #[tokio::test]
2175    async fn liveness_change_maps_apply_online() {
2176        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2177        let peer = peer_node("p", [1u8; 32], vec![]); // id == 1
2178        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2179        // A fixed timestamp (chrono is built without its `clock` feature, so no `Utc::now()`).
2180        let now = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
2181
2182        // Channel C: online_change sets online=true.
2183        let mut online_change = std::collections::BTreeMap::new();
2184        online_change.insert(1 as ts_control::NodeId, true);
2185        online_change.insert(999 as ts_control::NodeId, true); // unknown id — ignored
2186        let changed = tracker.apply_liveness_changes(&online_change, &Default::default(), now);
2187        assert!(changed);
2188        assert_eq!(
2189            tracker
2190                .peer_db
2191                .get(&(1 as ts_control::NodeId))
2192                .unwrap()
2193                .1
2194                .online,
2195            Some(true)
2196        );
2197
2198        // Channel D: peer_seen_change=true sets last_seen=now and leaves online UNTOUCHED.
2199        let mut seen_true = std::collections::BTreeMap::new();
2200        seen_true.insert(1 as ts_control::NodeId, true);
2201        let changed = tracker.apply_liveness_changes(&Default::default(), &seen_true, now);
2202        assert!(changed);
2203        {
2204            let (_id, node) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2205            assert_eq!(
2206                node.last_seen,
2207                Some(now),
2208                "peer_seen_change=true sets last_seen=now"
2209            );
2210            assert_eq!(
2211                node.online,
2212                Some(true),
2213                "channel D must NOT touch online (still true from channel C)"
2214            );
2215        }
2216
2217        // Channel D: peer_seen_change=false clears last_seen, still leaving online untouched.
2218        let mut seen_false = std::collections::BTreeMap::new();
2219        seen_false.insert(1 as ts_control::NodeId, false);
2220        let changed = tracker.apply_liveness_changes(&Default::default(), &seen_false, now);
2221        assert!(changed);
2222        {
2223            let (_id, node) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2224            assert_eq!(
2225                node.last_seen, None,
2226                "peer_seen_change=false clears last_seen"
2227            );
2228            assert_eq!(node.online, Some(true), "channel D must NOT mark offline");
2229        }
2230        assert_eq!(
2231            tracker.peer_db.peers().len(),
2232            1,
2233            "the node is retained, not removed"
2234        );
2235
2236        // No-op when nothing matches / changes.
2237        assert!(!tracker.apply_liveness_changes(&Default::default(), &Default::default(), now));
2238    }
2239
2240    /// Security: a `Patch` that rotates the node key must re-satisfy the tailnet-lock authority,
2241    /// exactly like a `Delta` upsert. A key-rotation patch whose new signature does NOT verify
2242    /// evicts the peer (fail-closed) rather than leaving a now-unverified entry — closing what would
2243    /// otherwise be a trust-enforcement bypass via the patch path.
2244    #[tokio::test]
2245    async fn patch_key_rotation_failing_tka_evicts_peer() {
2246        let (authority, sig) = authority_and_valid_sig();
2247        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2248
2249        // Admit a correctly-signed peer (id == 1).
2250        let good = peer_node("rotator", NODE_KEY_BYTES, sig.clone());
2251        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
2252        assert_eq!(tracker.peer_db.peers().len(), 1);
2253
2254        // Patch a new node key whose signature is garbage under the active authority.
2255        let patch = ts_control::PeerChange {
2256            id: 1,
2257            derp_region: None,
2258            cap: None,
2259            cap_map: None,
2260            underlay_addresses: None,
2261            node_key: Some([0x33u8; 32].into()),
2262            key_signature: Some(vec![0x00, 0x01, 0x02]),
2263            disco_key: None,
2264            node_key_expiry: None,
2265            online: None,
2266            last_seen: None,
2267        };
2268        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
2269
2270        assert_eq!(upserts.len(), 0);
2271        assert_eq!(deletions.len(), 1);
2272        assert_eq!(tracker.peer_db.peers().len(), 0);
2273    }
2274
2275    /// A node's `user_id` joins against the accumulated UserProfiles table to resolve the owning
2276    /// user's login name in `WhoIs.user`. With no matching profile, `user` is `None` (the
2277    /// pre-existing behavior); once a profile arrives, the same node resolves to its login. This
2278    /// proves the accumulate-then-join path the netmap handler builds.
2279    fn profile(id: ts_control::UserId, login: &str) -> ts_control::UserProfile {
2280        ts_control::UserProfile {
2281            id,
2282            login_name: login.to_string(),
2283            display_name: None,
2284        }
2285    }
2286
2287    #[tokio::test]
2288    async fn whois_resolves_user_from_accumulated_profiles() {
2289        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2290
2291        // A peer owned by user id 42 at 100.64.0.1 (the peer_node fixture's address).
2292        let mut peer = peer_node("p", NODE_KEY_BYTES, Vec::new());
2293        peer.user_id = 42;
2294        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2295        let addr = "100.64.0.1:0".parse().unwrap();
2296
2297        // No profile yet: the node resolves but its owner is unknown.
2298        let who = tracker.whois_opt(addr).expect("peer is known");
2299        assert_eq!(who.user, None);
2300
2301        // Profile for a DIFFERENT user must not match.
2302        tracker
2303            .user_profiles
2304            .insert(7, profile(7, "someone-else@example.com"));
2305        assert_eq!(tracker.whois_opt(addr).unwrap().user, None);
2306
2307        // The owning user's profile arrives (as the netmap handler would accumulate it): now the
2308        // login resolves.
2309        tracker
2310            .user_profiles
2311            .insert(42, profile(42, "alice@example.com"));
2312        assert_eq!(
2313            tracker.whois_opt(addr).unwrap().user,
2314            Some("alice@example.com".to_string())
2315        );
2316    }
2317
2318    /// `UserProfile::best_label` prefers the login name, falling back to display name, else `None`.
2319    #[test]
2320    fn user_profile_best_label_prefers_login() {
2321        assert_eq!(
2322            profile(1, "alice@example.com").best_label(),
2323            Some("alice@example.com".to_string())
2324        );
2325        let display_only = ts_control::UserProfile {
2326            id: 2,
2327            login_name: String::new(),
2328            display_name: Some("Bob".to_string()),
2329        };
2330        assert_eq!(display_only.best_label(), Some("Bob".to_string()));
2331        let empty = ts_control::UserProfile {
2332            id: 3,
2333            login_name: String::new(),
2334            display_name: None,
2335        };
2336        assert_eq!(empty.best_label(), None);
2337    }
2338
2339    // ----- tsr-jo1: RotationTracker (Go ipnlocal.rotationTracker.obsoleteKeys) -----
2340
2341    /// A `RotationDetails` for a `Direct`-rooted chain with the given prior keys + wrapping key.
2342    fn rot_details(
2343        prev: &[&[u8]],
2344        wrapping: &[u8],
2345        kind: ts_tka::SigKind,
2346    ) -> ts_tka::RotationDetails {
2347        ts_tka::RotationDetails {
2348            prev_node_keys: prev.iter().map(|p| p.to_vec()).collect(),
2349            initial_sig_kind: kind,
2350            initial_wrapping_pubkey: wrapping.to_vec(),
2351        }
2352    }
2353
2354    /// Rule 1: every prior node key named by any rotation chain is obsolete, regardless of the
2355    /// chain's root kind (Go's ungated `obsolete.AddSlice(d.PrevNodeKeys)`).
2356    #[test]
2357    fn rotation_tracker_prev_keys_always_obsolete() {
2358        let mut t = RotationTracker::default();
2359        // A Direct-rooted chain that rotated away OLD1, and a Credential-rooted one that rotated OLD2.
2360        t.add(
2361            b"newA".to_vec(),
2362            &rot_details(&[b"OLD1"], b"wrapA", ts_tka::SigKind::Direct),
2363        );
2364        t.add(
2365            b"newB".to_vec(),
2366            &rot_details(&[b"OLD2"], b"wrapB", ts_tka::SigKind::Credential),
2367        );
2368        let obsolete = t.obsolete_keys();
2369        assert!(
2370            obsolete.contains(b"OLD1".as_slice()),
2371            "Direct chain's prior key obsolete"
2372        );
2373        assert!(
2374            obsolete.contains(b"OLD2".as_slice()),
2375            "Credential chain's prior key obsolete too (rule 1 is ungated)"
2376        );
2377        // The current keys themselves are not obsolete (only one peer per wrapping key here).
2378        assert!(!obsolete.contains(b"newA".as_slice()));
2379        assert!(!obsolete.contains(b"newB".as_slice()));
2380    }
2381
2382    /// Rule 2: among `Direct`-rooted chains sharing a wrapping key, only the longest survives; the
2383    /// shorter (older) clone's key is obsolete.
2384    #[test]
2385    fn rotation_tracker_unequal_chain_keeps_longest() {
2386        let mut t = RotationTracker::default();
2387        // Same wrapping key; "long" has 2 prior keys, "short" has 1 ⇒ "short" is the older clone.
2388        t.add(
2389            b"long".to_vec(),
2390            &rot_details(&[b"p1", b"p2"], b"wrap", ts_tka::SigKind::Direct),
2391        );
2392        t.add(
2393            b"short".to_vec(),
2394            &rot_details(&[b"q1"], b"wrap", ts_tka::SigKind::Direct),
2395        );
2396        let obsolete = t.obsolete_keys();
2397        assert!(
2398            obsolete.contains(b"short".as_slice()),
2399            "the shorter-chain clone is obsolete"
2400        );
2401        assert!(
2402            !obsolete.contains(b"long".as_slice()),
2403            "the longest-chain peer survives"
2404        );
2405    }
2406
2407    /// Rule 2 tie: two `Direct`-rooted chains sharing a wrapping key with EQUAL chain length cannot
2408    /// be disambiguated ⇒ BOTH are dropped (Go's safety branch).
2409    #[test]
2410    fn rotation_tracker_equal_chain_drops_both() {
2411        let mut t = RotationTracker::default();
2412        t.add(
2413            b"cloneA".to_vec(),
2414            &rot_details(&[b"p1"], b"wrap", ts_tka::SigKind::Direct),
2415        );
2416        t.add(
2417            b"cloneB".to_vec(),
2418            &rot_details(&[b"p2"], b"wrap", ts_tka::SigKind::Direct),
2419        );
2420        let obsolete = t.obsolete_keys();
2421        assert!(
2422            obsolete.contains(b"cloneA".as_slice()),
2423            "tied clone A dropped"
2424        );
2425        assert!(
2426            obsolete.contains(b"cloneB".as_slice()),
2427            "tied clone B dropped"
2428        );
2429    }
2430
2431    /// `Credential`-rooted chains sharing a wrapping key are EXEMPT from rule 2 (reusable-authkey
2432    /// carve-out): both are kept even with equal chain length.
2433    #[test]
2434    fn rotation_tracker_credential_root_clones_both_kept() {
2435        let mut t = RotationTracker::default();
2436        t.add(
2437            b"credA".to_vec(),
2438            &rot_details(&[b"p1"], b"wrap", ts_tka::SigKind::Credential),
2439        );
2440        t.add(
2441            b"credB".to_vec(),
2442            &rot_details(&[b"p2"], b"wrap", ts_tka::SigKind::Credential),
2443        );
2444        let obsolete = t.obsolete_keys();
2445        assert!(
2446            !obsolete.contains(b"credA".as_slice()),
2447            "credential-rooted clone A kept"
2448        );
2449        assert!(
2450            !obsolete.contains(b"credB".as_slice()),
2451            "credential-rooted clone B kept"
2452        );
2453    }
2454
2455    /// A peer that another chain already rotated away does not also act as a surviving clone: it is
2456    /// removed from its wrapping-key group before the longest-survivor pick (Go's `DeleteFunc`).
2457    #[test]
2458    fn rotation_tracker_already_obsolete_peer_not_a_survivor() {
2459        let mut t = RotationTracker::default();
2460        // "victim" is rotated away by "rotator" (different wrapping key), AND shares wrapping key
2461        // "w" with "other". Because "victim" is already obsolete, only "other" is in play for "w" and
2462        // survives (no spurious tie-drop of "other").
2463        t.add(
2464            b"rotator".to_vec(),
2465            &rot_details(&[b"victim"], b"wRot", ts_tka::SigKind::Direct),
2466        );
2467        t.add(
2468            b"victim".to_vec(),
2469            &rot_details(&[b"x"], b"w", ts_tka::SigKind::Direct),
2470        );
2471        t.add(
2472            b"other".to_vec(),
2473            &rot_details(&[b"y"], b"w", ts_tka::SigKind::Direct),
2474        );
2475        let obsolete = t.obsolete_keys();
2476        assert!(
2477            obsolete.contains(b"victim".as_slice()),
2478            "victim rotated away by rotator"
2479        );
2480        assert!(
2481            !obsolete.contains(b"other".as_slice()),
2482            "other survives — victim was removed from the group before the tie check"
2483        );
2484    }
2485
2486    /// Empty tracker (no rotation-signed peers) ⇒ no obsolete keys (the non-rotation netmap path).
2487    #[test]
2488    fn rotation_tracker_empty_is_noop() {
2489        let t = RotationTracker::default();
2490        assert!(t.obsolete_keys().is_empty());
2491    }
2492
2493    /// End-to-end through the real `Full` path: a peer presenting a freshly-rotated key (a Rotation
2494    /// chain) is admitted, while a second peer still presenting the rotated-AWAY pivot key — even with
2495    /// that key's own still-valid Direct signature — is DROPPED by the cross-peer rotation filter.
2496    /// This is the gap closed here: Go `tkaFilterNetmapLocked` drops the stale clone; we used to admit
2497    /// it. Uses real `ts_tka` signing (`sign_direct` + `sign_rotation`) so the whole
2498    /// verify → details → filter pipeline runs.
2499    ///
2500    /// Construction: the trusted key signs an inner `Direct` over the PIVOT keypair's public key; the
2501    /// pivot key then signs an outer `Rotation` authorizing `new_key`. That chain's `prev_node_keys`
2502    /// names the pivot pubkey — so a peer presenting the pivot pubkey as its node key is the
2503    /// rotated-away key the filter must drop.
2504    #[tokio::test]
2505    async fn tka_full_drops_rotated_away_key_e2e() {
2506        use ed25519_dalek::SigningKey;
2507        use ts_tka::NodeKeySignature;
2508
2509        let trusted = SigningKey::from_bytes(&[42u8; 32]);
2510        let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
2511        let authority = Authority::from_state(
2512            AumHash([0; 32]),
2513            State {
2514                keys: vec![Key {
2515                    kind: KeyKind::Ed25519,
2516                    votes: 1,
2517                    public: trusted_pub.clone(),
2518                }],
2519            },
2520        );
2521
2522        // The rotation pivot: a keypair whose public key the inner Direct authorizes and whose
2523        // private key signs the outer rotation wrap. This pivot pubkey IS the key being rotated away.
2524        let pivot = SigningKey::from_bytes(&[9u8; 32]);
2525        let pivot_pub: [u8; 32] = pivot.verifying_key().to_bytes();
2526
2527        let new_key = [4u8; 32]; // the freshly-rotated node key
2528
2529        // Fresh peer: a Rotation chain authorizing `new_key`, inner Direct over the pivot signed by
2530        // trusted, outer wrap signed by the pivot. Its prev_node_keys names `pivot_pub`.
2531        let new_sig = NodeKeySignature::sign_rotation(&new_key, &trusted, &pivot).serialize();
2532        let new_peer = peer_node("rotated", new_key, new_sig);
2533
2534        // Stale peer: still presents the pivot pubkey (the rotated-away key) with its own valid
2535        // Direct signature — valid in isolation, but obsoleted by the fresh peer's rotation chain.
2536        let stale_sig = NodeKeySignature::sign_direct(&pivot_pub, &trusted).serialize();
2537        let stale_peer = peer_node("stale", pivot_pub, stale_sig);
2538
2539        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2540        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2541            new_peer.clone(),
2542            stale_peer.clone(),
2543        ]));
2544
2545        assert!(
2546            tracker.peer_db.get(&new_peer.node_key).is_some(),
2547            "the freshly-rotated peer is admitted"
2548        );
2549        assert!(
2550            tracker.peer_db.get(&stale_peer.node_key).is_none(),
2551            "the peer presenting the rotated-away key is dropped (Go tkaFilterNetmapLocked)"
2552        );
2553    }
2554}
2555
2556#[cfg(test)]
2557mod tsmp_disco_key_tests {
2558    //! Receive side of the TSMP disco-key advertisement, at the point the key is *learned*.
2559    //!
2560    //! These exercise [`PeerTracker::learn_disco_key`] — the fork's stand-in for Go
2561    //! `magicsock.Conn.HandleDiscoKeyAdvertisement` — which is the single place an advertisement
2562    //! reaches peer state. The wire decode and the "consumed, not delivered" drop are covered in
2563    //! `ts_packet::tsmp` and `ts_dataplane` respectively.
2564
2565    use ts_keys::DiscoPublicKey;
2566
2567    use super::{
2568        tka_tests::{peer_node, test_env},
2569        *,
2570    };
2571
2572    /// The key a peer advertises, and a second one for the re-advertise case.
2573    const ADVERTISED: [u8; 32] = [0xa5u8; 32];
2574    const READVERTISED: [u8; 32] = [0x5au8; 32];
2575    /// The (staler) key control has for that same peer, and the one control eventually catches up
2576    /// to.
2577    const FROM_CONTROL: [u8; 32] = [0xc0u8; 32];
2578    const CONTROL_CAUGHT_UP: [u8; 32] = [0x0cu8; 32];
2579
2580    /// The node key of the single peer these tests use.
2581    const PEER_NODE_KEY: [u8; 32] = [1u8; 32];
2582
2583    /// A tracker holding one peer with no disco key yet, plus that peer's [`PeerId`].
2584    fn tracker_with_peer() -> (PeerTracker, PeerId) {
2585        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2586        let node = peer_node("peer", PEER_NODE_KEY, Vec::new());
2587        let id = tracker.peer_db.upsert(&node);
2588        (tracker, id)
2589    }
2590
2591    /// The peer as CONTROL describes it: the same node, carrying whatever disco key the netmap says
2592    /// it has (`None` for a peer control has no disco key for at all).
2593    fn node_from_control(disco_key: Option<[u8; 32]>) -> Node {
2594        let mut node = peer_node("peer", PEER_NODE_KEY, Vec::new());
2595        node.disco_key = disco_key.map(DiscoPublicKey::from);
2596        node
2597    }
2598
2599    /// A netmap `Full` carrying just this peer, as control currently describes it.
2600    fn control_full(disco_key: Option<[u8; 32]>) -> ts_control::PeerUpdate {
2601        ts_control::PeerUpdate::Full(vec![node_from_control(disco_key)])
2602    }
2603
2604    /// A tracker whose single peer arrived through the netmap carrying `disco_key`, exactly as the
2605    /// actor's handler applies it. Returns the peer's [`PeerId`] too.
2606    fn tracker_with_control_peer(disco_key: Option<[u8; 32]>) -> (PeerTracker, PeerId) {
2607        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2608        let node = node_from_control(disco_key);
2609        tracker.apply_peer_update(&control_full(disco_key));
2610        let id = tracker
2611            .peer_db
2612            .has(&node.node_key)
2613            .expect("control delivered it");
2614        (tracker, id)
2615    }
2616
2617    /// The disco key the peer db currently holds for `peer` — the effective key every direct-path
2618    /// consumer resolves against.
2619    fn effective_key(tracker: &PeerTracker, peer: PeerId) -> Option<DiscoPublicKey> {
2620        tracker
2621            .peer_db
2622            .get(&peer)
2623            .expect("peer still present")
2624            .1
2625            .disco_key
2626    }
2627
2628    /// The happy path: an advertised key is applied to the peer AND lands in the disco index, which
2629    /// is what the direct-path machinery (`direct::DiscoPeerLookup`) reads. Re-advertising the same
2630    /// key is a no-op; advertising a different one replaces it, retracting the old index entry.
2631    #[tokio::test]
2632    async fn advertisement_learns_the_peers_disco_key() {
2633        let (mut tracker, peer) = tracker_with_peer();
2634        let key = DiscoPublicKey::from(ADVERTISED);
2635
2636        assert!(
2637            tracker.learn_disco_key(peer, key),
2638            "a first advertisement changes the peer db"
2639        );
2640        assert_eq!(
2641            tracker
2642                .peer_db
2643                .get(&peer)
2644                .expect("peer still present")
2645                .1
2646                .disco_key,
2647            Some(key),
2648            "the advertised disco key is learned"
2649        );
2650        assert_eq!(
2651            tracker.peer_db.has(&key),
2652            Some(peer),
2653            "and is reachable through the disco index the direct path resolves against"
2654        );
2655
2656        assert!(
2657            !tracker.learn_disco_key(peer, key),
2658            "re-advertising the same key is a no-op (Go counts it 'unchanged' and returns)"
2659        );
2660
2661        let rotated = DiscoPublicKey::from(READVERTISED);
2662        assert!(tracker.learn_disco_key(peer, rotated));
2663        assert_eq!(
2664            tracker
2665                .peer_db
2666                .get(&peer)
2667                .expect("peer still present")
2668                .1
2669                .disco_key,
2670            Some(rotated),
2671            "a later advertisement replaces the key without a netmap update"
2672        );
2673        assert_eq!(tracker.peer_db.has(&rotated), Some(peer));
2674        assert_eq!(
2675            tracker.peer_db.has(&key),
2676            None,
2677            "the superseded key no longer resolves to the peer"
2678        );
2679    }
2680
2681    /// The refusals, each of which must leave the peer db untouched: the zero key is never learned,
2682    /// and an advertisement never creates a peer.
2683    #[tokio::test]
2684    async fn refused_advertisements_change_nothing() {
2685        let (mut tracker, peer) = tracker_with_peer();
2686
2687        assert!(
2688            !tracker.learn_disco_key(peer, DiscoPublicKey::from([0u8; 32])),
2689            "the zero key is never learned"
2690        );
2691        assert_eq!(
2692            tracker
2693                .peer_db
2694                .get(&peer)
2695                .expect("peer still present")
2696                .1
2697                .disco_key,
2698            None,
2699            "a zero-key advertisement must not bind the peer to an unusable key"
2700        );
2701
2702        // An advertisement for a peer control has never told us about. Go logs "endpoint not found
2703        // for node" and returns; it must not conjure a peer into existence.
2704        let unknown = PeerId(4242);
2705        assert_eq!(tracker.peer_db.get(&unknown), None, "precondition");
2706        assert!(
2707            !tracker.learn_disco_key(unknown, DiscoPublicKey::from(ADVERTISED)),
2708            "an advertisement for an unknown peer is ignored"
2709        );
2710        assert_eq!(
2711            tracker.peer_db.peers().len(),
2712            1,
2713            "an advertisement never creates a peer — only control does"
2714        );
2715        assert_eq!(
2716            tracker.peer_db.has(&DiscoPublicKey::from(ADVERTISED)),
2717            None,
2718            "and never indexes a key against a peer that does not exist"
2719        );
2720    }
2721
2722    /// The feature's motivating case, end to end: the peer told us a key control has not caught up
2723    /// with, and then control polls again with the SAME stale key it had before. The advertisement
2724    /// must survive.
2725    ///
2726    /// Go keeps the two keys apart on the endpoint (`endpointDisco.controlKey` /
2727    /// `tsmpKey`), and `updateFromNode` only rewrites the control side when control's key actually
2728    /// changed — so a netmap restating the old key never touches the active TSMP key. With a single
2729    /// field the next map poll silently reverted the peer to control's stale key, which is precisely
2730    /// the state the advertisement exists to escape.
2731    #[tokio::test]
2732    async fn netmap_restating_controls_stale_key_keeps_the_tsmp_key() {
2733        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
2734        let advertised = DiscoPublicKey::from(ADVERTISED);
2735        assert_eq!(
2736            effective_key(&tracker, peer),
2737            Some(DiscoPublicKey::from(FROM_CONTROL)),
2738            "precondition: the peer starts on the key control gave us"
2739        );
2740
2741        assert!(tracker.learn_disco_key(peer, advertised));
2742        assert_eq!(effective_key(&tracker, peer), Some(advertised));
2743
2744        // Control polls again, still behind: a `Full` resync, then a `Delta` re-upsert, both
2745        // carrying the key control already sent.
2746        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
2747        assert_eq!(
2748            effective_key(&tracker, peer),
2749            Some(advertised),
2750            "a Full restating control's stale key must not undo the TSMP-learned key"
2751        );
2752        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
2753            upsert: vec![node_from_control(Some(FROM_CONTROL))],
2754            remove: vec![],
2755        });
2756        assert_eq!(
2757            effective_key(&tracker, peer),
2758            Some(advertised),
2759            "and neither must a Delta re-upsert of the same node"
2760        );
2761        assert_eq!(
2762            tracker.peer_db.has(&advertised),
2763            Some(peer),
2764            "the direct path still resolves the peer by the key it advertised"
2765        );
2766        assert_eq!(
2767            tracker.peer_db.has(&DiscoPublicKey::from(FROM_CONTROL)),
2768            None,
2769            "and control's superseded key does not resolve to it"
2770        );
2771
2772        // Control finally changes its mind. A genuinely NEW control key wins, exactly as it does
2773        // upstream (`updateDiscoKey` clears `tsmpActive` for a non-zero control key).
2774        tracker.apply_peer_update(&control_full(Some(CONTROL_CAUGHT_UP)));
2775        assert_eq!(
2776            effective_key(&tracker, peer),
2777            Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP)),
2778            "control changing the key is authoritative again"
2779        );
2780    }
2781
2782    /// An advertisement that merely restates the key control already gave us is still *new*
2783    /// information — it is the peer itself confirming the key — so Go records it as the TSMP key and
2784    /// makes it active. Its "unchanged" early return compares `epDisco.keyFromTSMP()`, the
2785    /// TSMP-learned key specifically, never the effective one.
2786    ///
2787    /// The observable consequence, asserted here: once the peer has confirmed the key, control
2788    /// dropping it (a netmap node with no disco key) leaves the confirmed key in place instead of
2789    /// blinding the direct path.
2790    #[tokio::test]
2791    async fn advertisement_restating_controls_key_is_recorded_as_the_tsmp_key() {
2792        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
2793        let key = DiscoPublicKey::from(FROM_CONTROL);
2794
2795        assert!(
2796            tracker.learn_disco_key(peer, key),
2797            "an advertisement of the key control already sent is recorded, not dropped"
2798        );
2799        assert_eq!(
2800            tracker
2801                .endpoint_disco
2802                .get(&PEER_NODE_KEY.into())
2803                .and_then(EndpointDisco::key_from_tsmp),
2804            Some(key),
2805            "it lands in the TSMP slot (Go epDisco.tsmpKey), not only in control's"
2806        );
2807        assert!(
2808            !tracker.learn_disco_key(peer, key),
2809            "re-advertising it now IS unchanged, and is refused"
2810        );
2811
2812        // Control drops the peer's disco key. The key the peer itself confirmed stays active.
2813        tracker.apply_peer_update(&control_full(None));
2814        assert_eq!(
2815            effective_key(&tracker, peer),
2816            Some(key),
2817            "a control key going away hands the active slot to the TSMP-learned key"
2818        );
2819        assert_eq!(tracker.peer_db.has(&key), Some(peer));
2820    }
2821
2822    /// A `PeersChangedPatch` is a control write like any other: one that says nothing about the
2823    /// disco key must leave an active TSMP key alone, and one that carries a new key is control
2824    /// catching up, so it wins.
2825    ///
2826    /// The patch path is the subtle one — it starts from the db node, which carries the *effective*
2827    /// key, so without re-deriving what control last said it would hand the TSMP key back as if
2828    /// control had sent it.
2829    #[tokio::test]
2830    async fn patch_without_a_disco_key_leaves_the_tsmp_key_active() {
2831        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
2832        let advertised = DiscoPublicKey::from(ADVERTISED);
2833        assert!(tracker.learn_disco_key(peer, advertised));
2834
2835        // A reachability-only patch (the idle-peer-reconnect case) for the same node.
2836        let endpoint: std::net::SocketAddr = "203.0.113.9:41641".parse().unwrap();
2837        let mut patch = ts_control::PeerChange {
2838            id: 1,
2839            derp_region: None,
2840            cap: None,
2841            cap_map: None,
2842            underlay_addresses: Some(vec![endpoint]),
2843            node_key: None,
2844            key_signature: None,
2845            disco_key: None,
2846            node_key_expiry: None,
2847            online: None,
2848            last_seen: None,
2849        };
2850        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2851        assert_eq!(
2852            effective_key(&tracker, peer),
2853            Some(advertised),
2854            "a patch that never mentions the disco key must not revert it to control's"
2855        );
2856        assert_eq!(
2857            tracker
2858                .peer_db
2859                .get(&peer)
2860                .expect("peer still present")
2861                .1
2862                .underlay_addresses,
2863            vec![endpoint],
2864            "and the patch it DID carry still applied"
2865        );
2866
2867        // Now control catches up through the patch channel.
2868        patch.disco_key = Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP));
2869        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2870        assert_eq!(
2871            effective_key(&tracker, peer),
2872            Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP)),
2873            "a patch that does carry a disco key is control catching up, and wins"
2874        );
2875    }
2876
2877    /// The TSMP-learned key lives exactly as long as Go's endpoint does: it is dropped when the peer
2878    /// leaves the netmap, and it is not carried across a node-key rotation (Go builds the rotated
2879    /// peer a brand-new endpoint, with a brand-new `endpointDisco`).
2880    #[tokio::test]
2881    async fn tsmp_key_does_not_outlive_the_peer_or_its_node_key() {
2882        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
2883        assert!(tracker.learn_disco_key(peer, DiscoPublicKey::from(ADVERTISED)));
2884
2885        // The peer leaves the netmap, then comes back on control's key.
2886        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![]));
2887        assert!(tracker.peer_db.peers().is_empty());
2888        assert!(
2889            tracker.endpoint_disco.is_empty(),
2890            "the departed peer's disco state goes with it"
2891        );
2892        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
2893        let readded = node_from_control(Some(FROM_CONTROL));
2894        let peer = tracker.peer_db.has(&readded.node_key).expect("re-added");
2895        assert_eq!(
2896            effective_key(&tracker, peer),
2897            Some(DiscoPublicKey::from(FROM_CONTROL)),
2898            "a peer that left and rejoined starts from control's key again"
2899        );
2900
2901        // Learn a key again, then rotate the node key underneath it.
2902        assert!(tracker.learn_disco_key(peer, DiscoPublicKey::from(READVERTISED)));
2903        let mut rotated = node_from_control(Some(FROM_CONTROL));
2904        rotated.node_key = [2u8; 32].into();
2905        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![rotated.clone()]));
2906        let peer = tracker
2907            .peer_db
2908            .has(&rotated.node_key)
2909            .expect("rotated peer");
2910        assert_eq!(
2911            effective_key(&tracker, peer),
2912            Some(DiscoPublicKey::from(FROM_CONTROL)),
2913            "a key learned under the old node key is not carried onto the new one"
2914        );
2915        assert_eq!(
2916            tracker.endpoint_disco.len(),
2917            1,
2918            "and the old node key's state is pruned"
2919        );
2920    }
2921}