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