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::{
20    Error, dataplane::PeerDiscoKeyAdvertisement, direct::DiscoKeyObserved, env::Env,
21    status::StatusNode,
22};
23
24mod peer_db;
25
26pub use peer_db::{DiscoKeyMatch, PeerDb};
27
28/// Whether `key` is the all-zero disco key, which Go spells `key.DiscoPublic.IsZero()` and treats
29/// everywhere as "this peer has no disco key" rather than as a usable key.
30fn disco_key_is_zero(key: &DiscoPublicKey) -> bool {
31    key.to_bytes() == [0u8; DiscoPublicKey::KEY_LEN_BYTES]
32}
33
34/// Normalize a disco key as it arrives from control: the all-zero key means "absent", exactly as
35/// Go's `IsZero()` checks in `endpoint.updateDiscoKey` read it.
36fn disco_key_from_control(key: Option<DiscoPublicKey>) -> Option<DiscoPublicKey> {
37    key.filter(|k| !disco_key_is_zero(k))
38}
39
40/// The two disco keys a peer can present, and which of them is currently active — Go
41/// [`magicsock.endpointDisco`] (`wgengine/magicsock/endpoint.go`).
42///
43/// A peer's disco key reaches us from two independent sources: **control**, in a netmap node or a
44/// `PeersChangedPatch`, and the **peer itself**, in a TSMP disco-key advertisement carried inside
45/// the WireGuard tunnel. Go keeps both side by side on the endpoint, and so do we, because control
46/// is the slower of the two: an advertisement exists precisely to cover the window where control has
47/// not caught up with the peer's current key, so collapsing the two into one field would let the
48/// next map poll overwrite a freshly-learned key with control's stale one — losing the feature's own
49/// motivating case.
50///
51/// Only one key is active for sending at a time ([`key`](Self::key)). That active key is what the
52/// peer db carries in [`Node::disco_key`], which is this fork's live lookup for every direct-path
53/// consumer (`direct::DiscoPeerLookup` resolves against it, and `PeerDb`'s disco index is built from
54/// it) — the stand-in for Go's per-endpoint `disco` pointer.
55///
56/// [`magicsock.endpointDisco`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/endpoint.go
57#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
58struct EndpointDisco {
59    /// The key learned from control (Go `endpointDisco.controlKey`).
60    control: Option<DiscoPublicKey>,
61    /// The key learned from a TSMP advertisement (Go `endpointDisco.tsmpKey`).
62    tsmp: Option<DiscoPublicKey>,
63    /// Whether [`tsmp`](Self::tsmp) is the active key (Go `endpointDisco.tsmpActive`).
64    tsmp_active: bool,
65}
66
67impl EndpointDisco {
68    /// The key currently regarded as active — Go `endpointDisco.key()`.
69    fn key(&self) -> Option<DiscoPublicKey> {
70        if self.tsmp_active {
71            self.tsmp
72        } else {
73            self.control
74        }
75    }
76
77    /// The control-learned key, active or not — Go `endpointDisco.keyFromControl()`.
78    fn key_from_control(&self) -> Option<DiscoPublicKey> {
79        self.control
80    }
81
82    /// The TSMP-learned key, active or not — Go `endpointDisco.keyFromTSMP()`.
83    fn key_from_tsmp(&self) -> Option<DiscoPublicKey> {
84        self.tsmp
85    }
86
87    /// Replace the control-learned key, leaving any TSMP-learned key in place — Go
88    /// [`endpoint.updateDiscoKey`].
89    ///
90    /// Control's key is always recorded in control's own slot, but it takes the *active* slot only
91    /// if no TSMP-learned key already holds it: Go `epDisco.tsmpActive = old.tsmpActive ||
92    /// key.IsZero()`. A key the peer told us itself is better evidence than a control server that
93    /// is, by construction, the slower of the two sources — so control changing its mind no longer
94    /// preempts an active TSMP key. Upstream returns to control's key when disco is actually
95    /// *received* under it (`endpoint.checkAndUpdateDiscoKey`), not when control asserts it.
96    ///
97    /// An absent (Go: zero) control key still hands the slot to the TSMP key, if there is one. When
98    /// there is neither key, the caller drops the whole entry ([`is_empty`](Self::is_empty)) — which
99    /// is what stops an active TSMP slot with no TSMP key in it outliving this call, exactly as Go
100    /// nils the endpoint's `disco` pointer in the same case.
101    ///
102    /// [`endpoint.updateDiscoKey`]: https://github.com/tailscale/tailscale/blob/9ea7cba44591e0cd840c6c94d23274dd222059bf/wgengine/magicsock/endpoint.go
103    fn update_from_control(&mut self, key: Option<DiscoPublicKey>) {
104        self.control = key;
105        self.tsmp_active = self.tsmp_active || key.is_none();
106    }
107
108    /// Replace the TSMP-learned key, leaving the control-learned key in place — Go
109    /// `endpoint.updateTSMPDiscoKey`.
110    fn update_from_tsmp(&mut self, key: Option<DiscoPublicKey>) {
111        self.tsmp = key;
112        self.tsmp_active = key.is_some();
113    }
114
115    /// The peer's other known key: the slot that is not active, when it holds a key that differs
116    /// from the active one.
117    ///
118    /// This is what makes ingress under the peer's *other* key resolvable
119    /// ([`PeerDb::set_inactive_disco_key`]). `None` when the inactive slot is empty or holds the
120    /// same key as the active one — there is no second key to accept in either case.
121    fn inactive_key(&self) -> Option<DiscoPublicKey> {
122        let inactive = if self.tsmp_active {
123            self.control
124        } else {
125            self.tsmp
126        };
127
128        inactive.filter(|k| Some(*k) != self.key())
129    }
130
131    /// Accept `key` as this peer's, switching the active slot to it when it is the currently
132    /// *inactive* one — Go [`endpoint.checkAndUpdateDiscoKey`].
133    ///
134    /// Called with the sender key of a disco frame we have opened, which proves the sender holds
135    /// that key's private half. Receiving under a key is therefore demonstrative: it is what the
136    /// peer is actually using, so upstream makes it the key we send to as well.
137    ///
138    /// Returns `None` when `key` belongs to **neither** slot — the refusal that is the whole
139    /// security value of the check, and the reason this is not simply "trust whatever key opened".
140    /// Otherwise `Some(changed)`, where `changed` reports whether the active key moved (and so
141    /// whether the direct path built under the old one has to be invalidated).
142    ///
143    /// [`endpoint.checkAndUpdateDiscoKey`]: https://github.com/tailscale/tailscale/blob/9ea7cba44591e0cd840c6c94d23274dd222059bf/wgengine/magicsock/endpoint.go
144    fn check_and_update(&mut self, key: DiscoPublicKey) -> Option<bool> {
145        if self.key() == Some(key) {
146            return Some(false);
147        }
148
149        // Not the active key. Go's compare-and-swap on `tsmpActive`: whichever slot holds it
150        // becomes the active one. Control's slot is tried first only for determinism — the two
151        // holding the same key is already handled by the equality check above.
152        if self.control == Some(key) {
153            self.tsmp_active = false;
154            return Some(true);
155        }
156        if self.tsmp == Some(key) {
157            self.tsmp_active = true;
158            return Some(true);
159        }
160
161        None
162    }
163
164    /// No key material from either source — Go nils out the endpoint's `disco` pointer here.
165    fn is_empty(&self) -> bool {
166        self.control.is_none() && self.tsmp.is_none()
167    }
168}
169
170/// Actor that tracks peer delta updates and emits new states.
171pub struct PeerTracker {
172    peer_db: PeerDb,
173    seen_state_update: bool,
174    pending_requests: Vec<Pending>,
175    /// Latest peer snapshot, published on every netmap update so embedders can watch for peer
176    /// changes ([`WatchNetmap`]).
177    peer_watch: watch::Sender<Vec<StatusNode>>,
178    /// Accumulated netmap user profiles (`MapResponse.UserProfiles`), keyed by user id, joined
179    /// against a node's [`Node::user_id`](ts_control::Node::user_id) to resolve the owning user's
180    /// login/display name for a [`WhoIs`](crate::status::WhoIs). Control sends these incrementally
181    /// (only new/changed profiles per response), so this map **accumulates** across updates rather
182    /// than being replaced — a peer upserted in one response may reference a profile delivered in an
183    /// earlier one.
184    user_profiles: HashMap<UserId, UserProfile>,
185    /// Per-peer disco-key provenance ([`EndpointDisco`]), keyed by the peer's node key.
186    ///
187    /// Go keeps this on the magicsock `endpoint`, which the peer map keys by node key; here the peer
188    /// db stores control's [`Node`] verbatim, so the second key (and which of the two is active)
189    /// lives beside it. Keying by node key reproduces Go's lifetime exactly: the state is dropped
190    /// when the peer leaves the netmap, and a peer that ROTATES its node key gets a fresh entry —
191    /// Go builds it a new endpoint, so a key learned over TSMP under the old node key is never
192    /// carried onto the new one. [`prune_endpoint_disco`](PeerTracker::prune_endpoint_disco) does
193    /// the dropping.
194    endpoint_disco: HashMap<NodePublicKey, EndpointDisco>,
195    /// Tailnet-Lock (TKA) authority enforced at the peer-trust chokepoint, matching Go
196    /// `tkaFilterNetmapLocked`. Read on demand from a [`watch`] cell the control runner owns: when it
197    /// holds `Some` (a verified lock has been synced from control), enforcement is **active** — every
198    /// upserted peer must present a `key_signature` this authority authorizes, or it is dropped
199    /// (fail-closed), exactly as Go drops peers with a missing or failing signature. When it holds
200    /// `None` (no lock, or the lock was disabled) enforcement is **inactive** and every peer is
201    /// upserted, identical to pre-TKA behavior and to Go's `b.tka == nil` early return.
202    ///
203    /// A `watch::Receiver` (not the bus) is the transport on purpose: the authority is a single
204    /// security-critical state cell, and `watch` is last-write-wins, never-dropped, and ordered by
205    /// the control runner's own writes — so a disable (`None`) can never be reordered behind or
206    /// silently dropped before a stale `Some` (which a best-effort broadcast bus could do, leaving a
207    /// defunct lock enforcing forever). The control runner is the sole writer; we only ever read.
208    ///
209    /// The authority always passes through `VerifiedAumChain::verify` before the control runner
210    /// publishes it, so enforcement only engages on a chain we have cryptographically verified.
211    /// Connectivity now depends on `ts_tka` verifying genuinely-good signatures correctly (see
212    /// SECURITY.md). Self is structurally never filtered here (the self node never enters `peer_db` —
213    /// it is routed to the control runner's `self_node` cell), so a node cannot lock itself out of
214    /// its own netmap.
215    tka_authority: watch::Receiver<Option<Arc<ts_tka::Authority>>>,
216    env: Env,
217}
218
219impl PeerTracker {
220    fn peer_by_name_opt(&self, name: &str) -> Option<&Node> {
221        // Canonicalization (case + trailing dot) is handled inside the name index lookup.
222        self.peer_db.get(&name).map(|(_id, node)| node)
223    }
224
225    fn peer_by_tailnet_ip_opt(&self, ip: IpAddr) -> Option<&Node> {
226        self.peer_db.get(&ip).map(|(_id, node)| node)
227    }
228
229    /// Build the peer entries for a [`Status`](crate::Status) snapshot from the current peer db.
230    ///
231    /// Connectivity fields (`cur_addr`/`relay`) are left at their `from_node` defaults (`None`) here:
232    /// this is the live-watch/hot path and must stay magicsock-free and synchronous. The explicit
233    /// [`GetStatus`] snapshot enriches them ([`status_peers_with_ids`](Self::status_peers_with_ids)).
234    fn status_peers(&self) -> Vec<StatusNode> {
235        self.peer_db
236            .peers()
237            .values()
238            .map(StatusNode::from_node)
239            .collect()
240    }
241
242    /// Like [`status_peers`](Self::status_peers) but pairs each entry with its [`PeerId`], so the
243    /// caller can join per-peer connectivity (the direct manager's `best_addrs`, keyed by `PeerId`)
244    /// onto the `StatusNode` before returning it. Order is unspecified (a `HashMap` walk).
245    fn status_peers_with_ids(&self) -> Vec<(PeerId, StatusNode)> {
246        self.peer_db
247            .peers()
248            .iter()
249            .map(|(id, node)| (*id, StatusNode::from_node(node)))
250            .collect()
251    }
252
253    fn whois_opt(&self, addr: std::net::SocketAddr) -> Option<crate::status::WhoIs> {
254        let ip = crate::status::whois_addr(addr);
255        let node = self.peer_by_tailnet_ip_opt(ip).cloned()?;
256        // Join the node's owning user id against the accumulated UserProfiles table. `None` when
257        // control sent no profile for that user (e.g. tagged nodes with no human owner, or a
258        // profile not yet delivered). The whole profile is handed over, not a flattened label:
259        // `WhoIs` is what an embedder authorises on, and `UserProfile::groups` is the only owner
260        // attribute it cannot re-derive from the netmap itself.
261        let user_profile = self.resolve_user_profile(node.user_id);
262        Some(crate::status::WhoIs::from_node_with_profile(
263            node,
264            user_profile,
265        ))
266    }
267
268    /// Merge a response's `MapResponse.UserProfiles` into the accumulated table, keyed by user id.
269    ///
270    /// Control sends profiles incrementally — only new or changed ones per response — so this
271    /// **accumulates**: a profile already held for a user id that this response does not restate
272    /// stays, and one it does restate is replaced wholesale (control's newer copy wins, including
273    /// a group list that shrank).
274    fn accumulate_user_profiles(&mut self, profiles: &[UserProfile]) {
275        for profile in profiles {
276            self.user_profiles.insert(profile.id, profile.clone());
277        }
278    }
279
280    /// Resolve a user id to its profile from the accumulated profile table.
281    fn resolve_user_profile(&self, user_id: UserId) -> Option<UserProfile> {
282        self.user_profiles.get(&user_id).cloned()
283    }
284
285    /// Whether `node` may be admitted to the peer db under Tailnet Lock, matching Go
286    /// `tkaFilterNetmapLocked`'s per-peer verdict (drop unsigned / failed-signature peers).
287    ///
288    /// This consults the live [`tka_authority`](Self::tka_authority) cell on each call (one `borrow`,
289    /// held only for the duration of the verdict). For a `Full` resync — which checks every peer —
290    /// prefer [`tka_authority_snapshot`](Self::tka_authority_snapshot) +
291    /// [`tka_snapshot_admits`](Self::tka_snapshot_admits) to borrow once and verify each peer a single
292    /// time; this method is the convenience wrapper for the single-peer (`Delta`/patch) sites.
293    ///
294    /// Fail-closed and gated:
295    /// - No authority ⇒ no lock synced ⇒ always admit (Go's `b.tka == nil` early return; identical to
296    ///   pre-TKA behavior).
297    /// - **Empty trusted-key state** ⇒ always admit (logged at `error!` — see
298    ///   [`tka_snapshot_admits`](Self::tka_snapshot_admits) for the full rationale).
299    /// - Authority present + peer carries a `key_signature` the authority authorizes for the peer's
300    ///   node key ⇒ admit.
301    /// - Authority present + signature missing or unauthorized/invalid ⇒ **drop** (Go drops peers
302    ///   with a missing signature or failed `NodeKeyAuthorized` under tailnet lock).
303    fn tka_admits(&self, node: &Node) -> bool {
304        // Single-peer sites (`Delta`/patch) only need the admit bool; the rotation details are used
305        // exclusively by the cross-peer `Full` filter (rotation obsolescence is whole-netmap).
306        Self::tka_snapshot_admits(self.tka_authority.borrow().as_deref(), node).admitted
307    }
308
309    /// Borrow the current TKA authority once (cloning the cheap `Arc`) for a batch verdict. Returns
310    /// `None` when no lock is synced (admit-all). Used by the `Full` path so a netmap of N peers
311    /// reads the cell once and runs at most one signature verify per peer (not two).
312    fn tka_authority_snapshot(&self) -> Option<Arc<ts_tka::Authority>> {
313        self.tka_authority.borrow().clone()
314    }
315
316    /// The per-peer Tailnet-Lock verdict against an already-borrowed `authority` snapshot. Factored
317    /// out so both the single-peer [`tka_admits`](Self::tka_admits) and the `Full` batch path share
318    /// one verdict implementation (no divergence) while the batch path verifies each peer exactly
319    /// once.
320    ///
321    /// Returns whether the peer is admitted AND, for an admitted peer signed by a rotation chain, the
322    /// [`RotationDetails`](ts_tka::RotationDetails) of that chain — so the `Full` path can run the
323    /// cross-peer rotation filter (Go's `rotationTracker`) without a second verify per peer. A peer
324    /// that is dropped, unsigned, or signed by a non-rotation chain carries `rotation == None`.
325    ///
326    /// Never logs key/signature bytes — only the `stable_id` and the `TkaError` Display (static
327    /// descriptors). One documented parity gap remains vs Go (in PARITY_ROADMAP): no
328    /// `UnsignedPeerAPIOnly` *admission* exemption — Go admits such a peer unsigned under an active
329    /// lock, we drop it (stricter, the safe direction). [`Node::unsigned_peer_api_only`] is now
330    /// carried, and the routes half of upstream's treatment is enforced at decode
331    /// (`ts_control::Node`'s `From` impl clamps such a peer's accepted routes to its own addresses,
332    /// unconditionally, whether or not a lock is active); only the admission carve-out is deferred.
333    fn tka_snapshot_admits(authority: Option<&ts_tka::Authority>, node: &Node) -> TkaVerdict {
334        let Some(auth) = authority else {
335            return TkaVerdict::admit();
336        };
337
338        // Brick-guard: an authority with no trusted keys would drop every peer. A verified chain is
339        // structurally guaranteed ≥1 key (genesis rejects an empty key set, and the last key cannot
340        // be removed), so reaching here means a `ts_tka` invariant was violated — admit rather than
341        // black-hole the whole netmap, and log at `error!` because it signals a real bug, not an
342        // expected runtime input. This is OUR fail-safe, not a Go behavior. NOTE: it only catches the
343        // empty-keyset shape; a non-empty authority that authorizes none of the offered peers still
344        // (correctly) drops them — that is what a lock that revoked everyone means. The
345        // "authorized-zero-peers" isolation case is surfaced separately by the caller.
346        if auth.state().keys.is_empty() {
347            tracing::error!(
348                "TKA: authority has an empty trusted-key set (verified chains never do — likely a \
349                 ts_tka bug); not enforcing (admitting all) to avoid isolating the node"
350            );
351            return TkaVerdict::admit();
352        }
353
354        if node.key_signature.is_empty() {
355            tracing::warn!(
356                stable_id = ?node.stable_id,
357                "TKA: dropping unsigned peer under tailnet lock"
358            );
359            return TkaVerdict::drop();
360        }
361
362        match auth.node_key_authorized_with_details(&node.node_key.to_bytes(), &node.key_signature)
363        {
364            Ok(rotation) => {
365                tracing::debug!(stable_id = ?node.stable_id, "TKA: peer node-key authorized");
366                TkaVerdict {
367                    admitted: true,
368                    rotation,
369                }
370            }
371            Err(e) => {
372                tracing::warn!(
373                    stable_id = ?node.stable_id,
374                    error = %e,
375                    "TKA: dropping peer with unauthorized node key"
376                );
377                TkaVerdict::drop()
378            }
379        }
380    }
381
382    /// The **keep** verdict for a whole batch of peers under `authority` — one complete Go
383    /// `tkaFilterNetmapLocked` pass (`ipn/ipnlocal/tailnet-lock.go`, v1.100.0), in Go's order:
384    ///
385    /// 1. the per-peer signature verdict ([`tka_snapshot_admits`](Self::tka_snapshot_admits)), then
386    /// 2. the cross-peer rotation filter (Go `rotationTracker`): a peer presenting a node key that a
387    ///    newer rotation has superseded — or a tied clone of one — is dropped even though its own
388    ///    signature verifies. That is whole-batch by nature (one peer's chain obsoletes another's
389    ///    key), which is why it lives here and not in the per-peer verdict.
390    ///
391    /// Factored out because two call sites must agree exactly on what "admitted" means: the `Full`
392    /// netmap upsert in [`apply_peer_update`](Self::apply_peer_update), and
393    /// [`tka_reevaluate_peer_db`](Self::tka_reevaluate_peer_db), which re-runs the same pass over the
394    /// peers already in the db when a freshly-synced authority is installed. A divergence between
395    /// them would be a peer admitted by one path and dropped by the other.
396    ///
397    /// `authority` is borrowed once and each peer verified exactly once (the ed25519 verify is the
398    /// expensive part). Returns one `bool` per input node, in input order; `None` authority ⇒ all
399    /// `true` (no lock synced ⇒ admit all, Go's `b.tka == nil` early return).
400    ///
401    /// `pub(crate)` for a third caller with the same requirement: the cold-start replay of a cached
402    /// netmap ([`control_runner::load_cached_netmap`](crate::control_runner::load_cached_netmap)),
403    /// which must apply the same pass to the cached peers that the netmap they were cached from
404    /// already went through — Go replays its cached map through `setNetMapLocked`, so it runs this
405    /// very filter.
406    pub(crate) fn tka_keep_verdicts(
407        authority: Option<&ts_tka::Authority>,
408        nodes: &[&Node],
409    ) -> Vec<bool> {
410        let verdicts = nodes
411            .iter()
412            .map(|node| Self::tka_snapshot_admits(authority, node))
413            .collect::<Vec<_>>();
414
415        let mut rotation = RotationTracker::default();
416        for (node, verdict) in nodes.iter().zip(&verdicts) {
417            if verdict.admitted
418                && let Some(details) = &verdict.rotation
419            {
420                rotation.add(node.node_key.to_bytes().to_vec(), details);
421            }
422        }
423        let obsolete = rotation.obsolete_keys();
424
425        nodes
426            .iter()
427            .zip(&verdicts)
428            .map(|(node, v)| {
429                // `contains` takes `&[u8]` (HashSet<Vec<u8>> borrows as a slice) — no alloc.
430                v.admitted && !obsolete.contains(&node.node_key.to_bytes()[..])
431            })
432            .collect()
433    }
434}
435
436/// The outcome of a per-peer Tailnet-Lock check: whether the peer is admitted, plus (for an admitted
437/// peer signed by a rotation chain) the chain's [`RotationDetails`](ts_tka::RotationDetails) so the
438/// `Full` path can run the cross-peer rotation filter from the SAME verify pass (no second verify).
439struct TkaVerdict {
440    admitted: bool,
441    rotation: Option<ts_tka::RotationDetails>,
442}
443
444impl TkaVerdict {
445    /// Admitted, no rotation details (no lock / brick-guard / non-rotation signature).
446    fn admit() -> Self {
447        Self {
448            admitted: true,
449            rotation: None,
450        }
451    }
452    /// Dropped.
453    fn drop() -> Self {
454        Self {
455            admitted: false,
456            rotation: None,
457        }
458    }
459}
460
461/// Cross-peer rotation-obsolescence tracker, mirroring Go `ipnlocal.rotationTracker`. Fed the
462/// [`RotationDetails`](ts_tka::RotationDetails) of every admitted, rotation-signed peer in a `Full`
463/// netmap; [`obsolete_keys`](Self::obsolete_keys) then returns the node keys to drop on top of the
464/// per-peer verdict. Two rules (Go `tkaFilterNetmapLocked` + `rotationTracker.obsoleteKeys`):
465///
466/// 1. Every prior node key named in any rotation chain is obsolete (a newer chain rotated it away).
467/// 2. Among `Direct`-rooted chains sharing one wrapping pubkey (a clone signal), only the
468///    longest-chain peer survives; if the two longest are tied, ALL in that group are dropped (we
469///    cannot tell which is the latest, so reject for safety). `Credential`-rooted chains are exempt
470///    from rule 2 — several nodes can legitimately join under one reusable auth key (same wrapping
471///    pubkey), so sharing it is not a clone signal there. (Rule 1 still applies to them.)
472///
473/// Node keys are tracked as raw `Vec<u8>` (the verified 32-byte node-public bytes).
474#[derive(Default)]
475struct RotationTracker {
476    obsolete: HashSet<Vec<u8>>,
477    by_wrapping_key: HashMap<Vec<u8>, Vec<SigRotation>>,
478}
479
480/// One admitted peer's rotation entry within a wrapping-key group.
481struct SigRotation {
482    node_key: Vec<u8>,
483    num_prev_keys: usize,
484}
485
486impl RotationTracker {
487    /// Record an admitted peer `node_key` and its rotation `details` (Go `addRotationDetails`).
488    fn add(&mut self, node_key: Vec<u8>, details: &ts_tka::RotationDetails) {
489        // Rule 1: every prior key is obsolete — applied for ALL chains (incl. credential-rooted),
490        // matching Go's ungated `obsolete.AddSlice(d.PrevNodeKeys)`.
491        self.obsolete.extend(details.prev_node_keys.iter().cloned());
492        // Rule 2 (clone-uniqueness) is gated to Direct-rooted chains only.
493        if details.initial_sig_kind != ts_tka::SigKind::Direct {
494            return;
495        }
496        self.by_wrapping_key
497            .entry(details.initial_wrapping_pubkey.clone())
498            .or_default()
499            .push(SigRotation {
500                node_key,
501                num_prev_keys: details.prev_node_keys.len(),
502            });
503    }
504
505    /// Compute the full obsolete node-key set (Go `rotationTracker.obsoleteKeys`). Processes each
506    /// wrapping-key group, mutating the shared `obsolete` set as it goes (so a key obsoleted by one
507    /// group is seen as obsolete by later groups via the `retain` below — Go's
508    /// `slices.DeleteFunc(... Contains)`). Group iteration order (a `HashMap` drain) is
509    /// nondeterministic, but the result is order-INDEPENDENT: this only ever *inserts* into
510    /// `obsolete` (never removes), and rule 1 already obsoleted every prior key before this loop, so
511    /// the final set is a union that does not depend on which group runs first (as in Go).
512    fn obsolete_keys(mut self) -> HashSet<Vec<u8>> {
513        // Drain only the group map so the loop can mutate `self.obsolete` without aliasing it; the
514        // shared `obsolete` set itself is NOT drained, preserving the cross-group visibility above.
515        let groups: Vec<Vec<SigRotation>> = self.by_wrapping_key.drain().map(|(_k, v)| v).collect();
516        for mut group in groups {
517            // Drop entries already obsoleted (rotated away) by another chain.
518            group.retain(|rd| !self.obsolete.contains(&rd.node_key));
519            if group.is_empty() {
520                continue;
521            }
522            // Longest chain (most prior keys) is the newest ⇒ the survivor; sort decreasing.
523            // `sort_by_key` is stable (like Go's `SortStableFunc`); `Reverse` gives descending order.
524            group.sort_by_key(|rd| core::cmp::Reverse(rd.num_prev_keys));
525            if group.len() >= 2 && group[0].num_prev_keys == group[1].num_prev_keys {
526                // Tie for longest ⇒ cannot disambiguate the latest ⇒ drop the WHOLE group.
527                tracing::warn!(
528                    "TKA: multiple peers share a wrapping key with equal rotation depth; dropping all (cannot determine the latest)"
529                );
530                for rd in &group {
531                    self.obsolete.insert(rd.node_key.clone());
532                }
533            } else {
534                // Only the longest-chain peer survives; the rest are obsolete.
535                for rd in &group[1..] {
536                    self.obsolete.insert(rd.node_key.clone());
537                }
538            }
539        }
540        self.obsolete
541    }
542}
543
544impl kameo::Actor for PeerTracker {
545    /// `(env, tka_authority)`: the bus/keys env, plus the read end of the control runner's TKA
546    /// enforcement-authority cell (Go `tkaFilterNetmapLocked`). The control runner is the sole
547    /// writer; it publishes the verified `Authority` after a successful `/machine/tka/sync` and
548    /// `None` when the lock is disabled. A `watch` cell (not a bus message) so the latest value is
549    /// always readable on demand, never dropped, and never reordered (see the control runner's
550    /// `tka_authority` cell).
551    type Args = (Env, watch::Receiver<Option<Arc<ts_tka::Authority>>>);
552    type Error = Error;
553
554    async fn on_start(
555        (env, tka_authority): Self::Args,
556        slf: ActorRef<Self>,
557    ) -> Result<Self, Self::Error> {
558        env.subscribe::<Arc<ts_control::StateUpdate>>(&slf).await?;
559        env.subscribe::<PeerDiscoKeyAdvertisement>(&slf).await?;
560        env.subscribe::<DiscoKeyObserved>(&slf).await?;
561
562        // Re-filter the peer db whenever the enforcement authority changes. Go gets this for free:
563        // `SetControlClientStatus` runs `tkaSyncIfNeeded` and `tkaFilterNetmapLocked` back to back
564        // over one netmap. Here the sync is asynchronous, so the peers admitted before the authority
565        // arrived need a second pass — see `tka_reevaluate_peer_db`. `changed()` resolves on every
566        // write to the cell (enable, re-sync, disable); the task ends when the control runner drops
567        // the sender (shutdown) or the tracker itself is gone.
568        //
569        // A **weak** ref on purpose: the runtime holds only a `WeakActorRef` to the peer tracker, so
570        // a strong one parked in this task would keep the actor's mailbox alive past shutdown.
571        let mut authority_changes = tka_authority.clone();
572        let notify = slf.downgrade();
573        tokio::spawn(async move {
574            while authority_changes.changed().await.is_ok() {
575                let Some(tracker) = notify.upgrade() else {
576                    break; // the peer tracker is gone; nothing left to re-filter
577                };
578                if tracker.tell(TkaAuthorityChanged).await.is_err() {
579                    break; // the peer tracker stopped
580                }
581            }
582        });
583
584        let (peer_watch, _) = watch::channel(Vec::new());
585
586        Ok(Self {
587            peer_db: PeerDb::default(),
588            pending_requests: Default::default(),
589            seen_state_update: false,
590            peer_watch,
591            user_profiles: HashMap::new(),
592            endpoint_disco: HashMap::new(),
593            // The cell starts `None` (no lock synced ⇒ enforcement inactive, admit all, matching
594            // Go's `b.tka == nil`); the control runner flips it to `Some` on the first sync.
595            tka_authority,
596            env,
597        })
598    }
599}
600
601enum Pending {
602    PeerByName(PeerByName, ReplySender<Option<Node>>),
603    AcceptedRoute(PeerByAcceptedRoute, ReplySender<Vec<Node>>),
604    TailnetIp(PeerByTailnetIp, ReplySender<Option<Node>>),
605    Status(ReplySender<Vec<(PeerId, StatusNode)>>),
606    WhoIs(Whois, ReplySender<Option<crate::status::WhoIs>>),
607}
608
609// For messages with arguments, a struct is generated with the args as fields. They aren't
610// documented, and we can't apply attributes directly to the fields. Hence, wrap in a module where
611// docs are turned off everywhere.
612#[allow(missing_docs)]
613mod msg_impl {
614    use std::net::IpAddr;
615
616    use kameo::prelude::DelegatedReply;
617
618    use super::*;
619
620    #[kameo::messages]
621    impl PeerTracker {
622        /// Lookup a peer by name.
623        ///
624        /// Waits until we've received at least one peer update from control.
625        #[message(ctx)]
626        pub async fn peer_by_name(
627            &mut self,
628            ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
629            name: String,
630        ) -> DelegatedReply<Option<Node>> {
631            let (deleg, sender) = ctx.reply_sender();
632            let Some(sender) = sender else { return deleg };
633
634            if !self.seen_state_update {
635                tracing::debug!(query = name, "no peer state seen yet, queueing request");
636
637                self.pending_requests
638                    .push(Pending::PeerByName(PeerByName { name }, sender));
639
640                return deleg;
641            }
642
643            sender.send(self.peer_by_name_opt(&name).cloned());
644
645            deleg
646        }
647
648        /// Lookup all peers that accept packets addressed to the given IP.
649        ///
650        /// This includes the peer's tailnet address and any subnet routes it provides. Only
651        /// the peers with the most specific subnet route match that covers `ip` will be
652        /// returned.
653        ///
654        /// E.g., suppose:
655        ///
656        /// - We're querying for `10.1.2.3`
657        /// - `PeerA` and `PeerB` have accepted routes for `10.1.2.0/24`
658        /// - `PeerC` has an accepted route for `10.1.0.0/16`
659        ///
660        /// Only `PeerA` and `PeerB` will be returned, since they have the most specific
661        /// prefix match.
662        #[message(ctx)]
663        pub fn peer_by_accepted_route(
664            &mut self,
665            ctx: &mut Context<Self, DelegatedReply<Vec<Node>>>,
666            ip: IpAddr,
667        ) -> DelegatedReply<Vec<Node>> {
668            let (deleg, sender) = ctx.reply_sender();
669            let Some(sender) = sender else { return deleg };
670
671            if !self.seen_state_update {
672                tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
673
674                self.pending_requests
675                    .push(Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, sender));
676
677                return deleg;
678            }
679
680            sender.send(
681                self.peer_db
682                    .get_route(ip.into())
683                    .map(|(_id, node)| node.clone())
684                    .collect(),
685            );
686
687            deleg
688        }
689
690        /// Lookup the peer that has the given tailnet IP address.
691        #[message(ctx)]
692        pub fn peer_by_tailnet_ip(
693            &mut self,
694            ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
695            ip: IpAddr,
696        ) -> DelegatedReply<Option<Node>> {
697            let (deleg, sender) = ctx.reply_sender();
698            let Some(sender) = sender else { return deleg };
699
700            if !self.seen_state_update {
701                tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
702
703                self.pending_requests
704                    .push(Pending::TailnetIp(PeerByTailnetIp { ip }, sender));
705
706                return deleg;
707            }
708
709            sender.send(self.peer_by_tailnet_ip_opt(ip).cloned());
710
711            deleg
712        }
713
714        /// Build the peer entries of a [`Status`](crate::Status) snapshot, each paired with its
715        /// [`PeerId`] so [`Runtime::status`](crate::Runtime::status) can join per-peer connectivity
716        /// (`cur_addr`/`relay`) from the direct manager before returning. The self node is *not*
717        /// included here (it lives in the control runner); `Runtime::status` combines both and drops
718        /// the ids.
719        ///
720        /// Waits until we've received at least one peer update from control.
721        #[message(ctx)]
722        pub fn get_status(
723            &mut self,
724            ctx: &mut Context<Self, DelegatedReply<Vec<(PeerId, StatusNode)>>>,
725        ) -> DelegatedReply<Vec<(PeerId, StatusNode)>> {
726            let (deleg, sender) = ctx.reply_sender();
727            let Some(sender) = sender else { return deleg };
728
729            if !self.seen_state_update {
730                tracing::debug!("no peer state seen yet, queueing status request");
731                self.pending_requests.push(Pending::Status(sender));
732                return deleg;
733            }
734
735            sender.send(self.status_peers_with_ids());
736
737            deleg
738        }
739
740        /// Return every known peer's full domain [`Node`] (not the lossy [`StatusNode`]).
741        ///
742        /// Used by [`Runtime::file_targets`](crate::Runtime::file_targets), which needs the full node
743        /// (peerAPI address, owning user id, cap map) to compute Taildrop send targets. The self node
744        /// is not included (it lives in the control runner). Returns empty before the first netmap —
745        /// the natural "not connected yet" analog (an immediate answer, no queueing needed: callers
746        /// that need a populated list await `Running` first).
747        #[message]
748        pub fn all_peers(&self) -> Vec<Node> {
749            self.peer_db.peers().values().cloned().collect()
750        }
751
752        /// Resolve which node owns a tailnet source address.
753        ///
754        /// Maps the source IP of `addr` to the owning node via the tailnet-IP index, returning a
755        /// [`WhoIs`](crate::WhoIs). The port is ignored (a tailnet IP uniquely identifies a node).
756        ///
757        /// The resulting [`WhoIs`](crate::WhoIs) carries no user/login or capability data: this
758        /// fork's domain [`Node`] does not retain those wire fields. See the
759        /// [`status`](crate::status) module docs for the gap.
760        ///
761        /// Waits until we've received at least one peer update from control.
762        #[message(ctx)]
763        pub fn whois(
764            &mut self,
765            ctx: &mut Context<Self, DelegatedReply<Option<crate::status::WhoIs>>>,
766            addr: std::net::SocketAddr,
767        ) -> DelegatedReply<Option<crate::status::WhoIs>> {
768            let (deleg, sender) = ctx.reply_sender();
769            let Some(sender) = sender else { return deleg };
770
771            if !self.seen_state_update {
772                tracing::debug!(query = %addr, "no peer state seen yet, queueing whois request");
773                self.pending_requests
774                    .push(Pending::WhoIs(Whois { addr }, sender));
775                return deleg;
776            }
777
778            sender.send(self.whois_opt(addr));
779
780            deleg
781        }
782
783        /// Subscribe to netmap peer-change events.
784        ///
785        /// Returns a [`watch::Receiver`] whose value is the current set of peer
786        /// [`StatusNode`]s, updated on every netmap state update from control. Embedders can await
787        /// changes via [`watch::Receiver::changed`] to react to peers joining, leaving, or changing.
788        ///
789        /// The receiver's initial value is the peer set at subscription time (empty before the
790        /// first netmap update). This is a peer-only view; combine with the self node from
791        /// [`Runtime::status`](crate::Runtime::status) when a full snapshot is needed.
792        #[message(derive(Clone))]
793        pub fn watch_netmap(&self) -> watch::Receiver<Vec<StatusNode>> {
794            self.peer_watch.subscribe()
795        }
796    }
797}
798
799pub use msg_impl::*;
800
801#[derive(Debug, Clone)]
802pub(crate) struct PeerState {
803    #[allow(unused)]
804    pub deletions: HashSet<PeerId>,
805    #[allow(unused)]
806    pub upserts: HashSet<PeerId>,
807    pub peers: Arc<PeerDb>,
808}
809
810impl Message<Arc<ts_control::StateUpdate>> for PeerTracker {
811    type Reply = ();
812
813    async fn handle(
814        &mut self,
815        msg: Arc<ts_control::StateUpdate>,
816        _ctx: &mut Context<Self, Self::Reply>,
817    ) {
818        // Accumulate user profiles first — control sends them incrementally and a response may
819        // carry profiles with no peer delta (or peers that reference a profile from an earlier
820        // response), so this must happen before the no-peer-update early return below.
821        self.accumulate_user_profiles(&msg.user_profiles);
822
823        // Apply the standalone online/last-seen delta maps (channels C/D, `MapResponse.OnlineChange`
824        // / `PeerSeenChange`). These arrive keyed by control node id and may ride a response that
825        // carries NO `peer_update` (a bare online flip is the common case), so they must be applied
826        // *before* the no-peer-update early return — otherwise online status freezes at the last
827        // full-node/patch value. Each entry only ever *sets* a value (never back to unknown).
828        // Wall clock for a `PeerSeenChange: true` (Go uses `clock.Now()`). chrono is built without
829        // its `clock` feature in this workspace, so derive it from `SystemTime` the same way the
830        // control runner / ssh-policy paths do (unix secs → `DateTime::from_timestamp`).
831        let now = std::time::SystemTime::now()
832            .duration_since(std::time::UNIX_EPOCH)
833            .ok()
834            .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
835            .unwrap_or_default();
836        let liveness_changed =
837            self.apply_liveness_changes(&msg.online_change, &msg.peer_seen_change, now);
838
839        if msg.peer_update.is_none() && msg.peer_patches.is_empty() {
840            // No peer set or patch this response. If a liveness delta still mutated the netmap,
841            // publish the refreshed snapshot so watchers (and `GetStatus`) see the new online state.
842            if liveness_changed {
843                self.service_pending_requests();
844                self.peer_watch.send_replace(self.status_peers());
845                if let Err(e) = self
846                    .env
847                    .publish(Arc::new(PeerState {
848                        upserts: HashSet::default(),
849                        deletions: HashSet::default(),
850                        peers: Arc::new(self.peer_db.clone()),
851                    }))
852                    .await
853                {
854                    tracing::error!(error = %e, "publishing liveness-only peer state update");
855                }
856            }
857            return;
858        }
859
860        // Apply the whole-node peer set (if any) FIRST, then the field-level patches on top —
861        // mirroring Go's `controlclient` order (`Peers*` then `PeersChangedPatch`). A response may
862        // carry either, both, or (with a liveness-only delta) neither. Merge the upsert/deletion sets
863        // so the published `PeerState` reflects every node touched by both passes; a node both
864        // upserted by the set and patched stays in `upserts` (the patch removes it from `deletions`).
865        let (mut upserts, mut deletions) = msg
866            .peer_update
867            .as_ref()
868            .map(|u| self.apply_peer_update(u))
869            .unwrap_or_default();
870
871        if !msg.peer_patches.is_empty() {
872            let (patch_upserts, patch_deletions) = self.apply_peer_patches(&msg.peer_patches);
873            // A patch can evict a node the set just upserted (TKA rejection after key rotation), or
874            // re-admit/patch one not in the set — reconcile so each id lands in exactly one set.
875            for id in &patch_upserts {
876                deletions.remove(id);
877            }
878            for id in &patch_deletions {
879                upserts.remove(id);
880            }
881            upserts.extend(patch_upserts);
882            deletions.extend(patch_deletions);
883        }
884
885        tracing::debug!(
886            n_upsert = upserts.len(),
887            n_delete = deletions.len(),
888            peer_count = self.peer_db.peers().len(),
889            "new peer state"
890        );
891
892        self.service_pending_requests();
893
894        // Publish the latest peer snapshot to netmap watchers. `send_replace` keeps the receiver's
895        // value current even when there are no subscribers, so a late subscriber sees fresh state.
896        self.peer_watch.send_replace(self.status_peers());
897
898        if let Err(e) = self
899            .env
900            .publish(Arc::new(PeerState {
901                upserts,
902                deletions,
903                peers: Arc::new(self.peer_db.clone()),
904            }))
905            .await
906        {
907            tracing::error!(error = %e, "publishing peer state update");
908        }
909    }
910}
911
912impl Message<PeerDiscoKeyAdvertisement> for PeerTracker {
913    type Reply = ();
914
915    async fn handle(
916        &mut self,
917        msg: PeerDiscoKeyAdvertisement,
918        _ctx: &mut Context<Self, Self::Reply>,
919    ) {
920        if !self.learn_disco_key(msg.peer, msg.key) {
921            return;
922        }
923
924        // The key changed, so republish: the direct-path machinery resolves a peer's disco key out
925        // of the published `PeerState` snapshot (`direct::DiscoPeerLookup`), which is the whole
926        // point of learning it — it is what lets disco reach this peer without waiting for a
927        // netmap update. Go does the equivalent by writing the key straight into the magicsock
928        // endpoint and re-keying its peer map.
929        self.peer_watch.send_replace(self.status_peers());
930
931        if let Err(e) = self
932            .env
933            .publish(Arc::new(PeerState {
934                upserts: HashSet::from_iter([msg.peer]),
935                deletions: HashSet::default(),
936                peers: Arc::new(self.peer_db.clone()),
937            }))
938            .await
939        {
940            tracing::error!(error = %e, "publishing peer state after a TSMP disco-key advertisement");
941        }
942    }
943}
944
945impl Message<DiscoKeyObserved> for PeerTracker {
946    type Reply = ();
947
948    async fn handle(&mut self, msg: DiscoKeyObserved, _ctx: &mut Context<Self, Self::Reply>) {
949        if !self.observe_disco_key(msg.peer, msg.key) {
950            return;
951        }
952
953        // The active key moved, so republish. This is the *same* channel a TSMP advertisement and a
954        // netmap disco-key change use, and it is what makes the direct manager invalidate the
955        // trusted path built under the old key: it diffs consecutive snapshots
956        // (`direct::disco_key_rotations`) and calls `MagicSock::changed_active_disco` — this fork's
957        // `endpoint.changedActiveDiscoLocked`, which Go likewise reaches from
958        // `checkAndUpdateDiscoKey`. Keeping the switch and the invalidation on one path is why the
959        // switch is done here rather than on the packet path that spotted it.
960        self.peer_watch.send_replace(self.status_peers());
961
962        if let Err(e) = self
963            .env
964            .publish(Arc::new(PeerState {
965                upserts: HashSet::from_iter([msg.peer]),
966                deletions: HashSet::default(),
967                peers: Arc::new(self.peer_db.clone()),
968            }))
969            .await
970        {
971            tracing::error!(error = %e, "publishing peer state after a disco active-key switch");
972        }
973    }
974}
975
976/// Internal self-message: the Tailnet-Lock enforcement-authority cell changed — the control runner
977/// installed a freshly-synced [`Authority`](ts_tka::Authority) after a `/machine/tka/sync`, or
978/// cleared it because the lock was disabled. Sent by the watch task
979/// [`on_start`](kameo::Actor::on_start) spawns, so the peer db is re-filtered the moment enforcement
980/// changes instead of at whatever later `Full` netmap happens to arrive.
981#[derive(Debug, Clone, Copy)]
982pub(crate) struct TkaAuthorityChanged;
983
984impl Message<TkaAuthorityChanged> for PeerTracker {
985    type Reply = ();
986
987    async fn handle(&mut self, _msg: TkaAuthorityChanged, _ctx: &mut Context<Self, Self::Reply>) {
988        let deletions = self.tka_reevaluate_peer_db();
989        if deletions.is_empty() {
990            // The common case: enforcement is inactive, or every admitted peer still verifies.
991            return;
992        }
993
994        // An evicted peer must lose its data path, not just its db row, so republish the snapshot
995        // the `Arc<PeerState>` subscribers (route updater, source filter, dataplane) resolve
996        // against — the same publish the netmap handler does after a peer set changes.
997        self.peer_watch.send_replace(self.status_peers());
998
999        if let Err(e) = self
1000            .env
1001            .publish(Arc::new(PeerState {
1002                upserts: HashSet::default(),
1003                deletions,
1004                peers: Arc::new(self.peer_db.clone()),
1005            }))
1006            .await
1007        {
1008            tracing::error!(error = %e, "publishing peer state after a TKA authority change");
1009        }
1010    }
1011}
1012
1013/// Ask the peer tracker to re-broadcast its current peer snapshot on the bus, without any peer
1014/// change. Sent after a runtime preference change so the route updater and source filter (both
1015/// `Arc<PeerState>` subscribers) re-resolve against the new value immediately, rather than waiting
1016/// for the next netmap update: `Device::set_exit_node` (new exit-node selector) and
1017/// `Device::set_accept_routes` (new accept-routes flag) both send it.
1018#[derive(Debug, Clone, Copy)]
1019pub struct RepublishState;
1020
1021impl Message<RepublishState> for PeerTracker {
1022    type Reply = ();
1023
1024    async fn handle(&mut self, _msg: RepublishState, _ctx: &mut Context<Self, Self::Reply>) {
1025        // An empty upsert/deletion set: this is a re-broadcast of the unchanged peer set, not a
1026        // delta. Subscribers recompute their routes/filters against the current peers and the
1027        // (just-updated) runtime preferences (exit-node selector, accept-routes flag).
1028        if let Err(e) = self
1029            .env
1030            .publish(Arc::new(PeerState {
1031                upserts: HashSet::default(),
1032                deletions: HashSet::default(),
1033                peers: Arc::new(self.peer_db.clone()),
1034            }))
1035            .await
1036        {
1037            tracing::error!(error = %e, "re-publishing peer state after a runtime preference change");
1038        }
1039    }
1040}
1041
1042impl PeerTracker {
1043    /// Learn a peer's disco key from a TSMP disco-key advertisement, returning whether the
1044    /// advertisement was applied.
1045    ///
1046    /// Go [`magicsock.Conn.HandleDiscoKeyAdvertisement`], reduced to the state this fork keeps:
1047    /// Go stores the learned key on the magicsock endpoint and re-keys its peer map, whereas here
1048    /// the peer db's `disco_key` (and its disco index) *is* the live lookup every direct-path
1049    /// consumer reads. The key is recorded in the peer's [`EndpointDisco`] TSMP slot — never on top
1050    /// of control's — and the peer db then carries whichever of the two is active, so the next
1051    /// netmap cannot silently undo it ([`upsert_from_control`](Self::upsert_from_control)).
1052    ///
1053    /// The three refusals are Go's, in Go's order:
1054    ///
1055    /// 1. **A zero key is never learned.** Go checks it twice — `tstun` publishes only
1056    ///    `if !Key.IsZero()`, and `HandleDiscoKeyAdvertisement` rejects it again. The dataplane
1057    ///    already dropped it here too; this is the second check, kept because the cost of getting
1058    ///    it wrong is a peer bound to an unusable key.
1059    /// 2. **An unknown peer is ignored** (Go: "endpoint not found for node"). An advertisement
1060    ///    never creates a peer — only control does — so one that arrives before or after the
1061    ///    peer's netmap entry is a no-op, exactly like a `PeersChangedPatch` for an unknown node.
1062    /// 3. **An unchanged key is a no-op**, so a peer re-advertising the key we already hold costs
1063    ///    no upsert and no republish (Go counts this as
1064    ///    `magicsock_tsmp_disco_key_advertisement_unchanged` and returns). "Unchanged" is measured
1065    ///    against the **TSMP-learned** key (Go compares `epDisco.keyFromTSMP()`), NOT against the
1066    ///    effective one: an advertisement that merely restates what control already told us is new
1067    ///    information — it is the peer itself confirming the key — so it is recorded as the active
1068    ///    TSMP key and survives control later dropping or contradicting it.
1069    ///
1070    /// The tailnet-lock gate is deliberately *not* re-run: unlike a `PeersChangedPatch`, an
1071    /// advertisement cannot touch the node key or its TKA signature — only the disco key — so the
1072    /// peer-trust decision that admitted this node is unchanged by definition.
1073    ///
1074    /// [`magicsock.Conn.HandleDiscoKeyAdvertisement`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/magicsock.go
1075    fn learn_disco_key(&mut self, peer: PeerId, key: DiscoPublicKey) -> bool {
1076        if disco_key_is_zero(&key) {
1077            tracing::debug!(?peer, "TSMP-advertised disco key is the zero key; ignoring");
1078            return false;
1079        }
1080
1081        let Some((_id, existing)) = self.peer_db.get(&peer) else {
1082            tracing::debug!(
1083                ?peer,
1084                "TSMP disco-key advertisement for unknown peer; ignoring"
1085            );
1086            return false;
1087        };
1088
1089        let node_key = existing.node_key;
1090        if self
1091            .endpoint_disco
1092            .get(&node_key)
1093            .and_then(EndpointDisco::key_from_tsmp)
1094            == Some(key)
1095        {
1096            tracing::trace!(?peer, "TSMP-advertised disco key is unchanged");
1097            return false;
1098        }
1099
1100        let node = existing.clone();
1101        let disco = self.endpoint_disco.entry(node_key).or_default();
1102        disco.update_from_tsmp(Some(key));
1103        let disco = *disco;
1104        self.store_disco(&node, disco);
1105
1106        tracing::info!(
1107            ?peer,
1108            stable_id = ?node.stable_id,
1109            %key,
1110            "learned peer disco key from a TSMP advertisement"
1111        );
1112
1113        true
1114    }
1115
1116    /// Write a peer's resolved disco state onto the peer db.
1117    ///
1118    /// The node lands carrying the **effective** key ([`EndpointDisco::key`]), which is what the
1119    /// disco index — and so every *send* path — resolves against, and the peer's other known key
1120    /// (if any) is registered as its inactive ingress key so a frame arriving under it still
1121    /// attributes to this peer ([`PeerDb::peer_by_known_disco_key`]).
1122    ///
1123    /// Every disco-key writer goes through here — control, a TSMP advertisement, and an
1124    /// active-slot switch on receive — so the two cannot drift apart on which key is which.
1125    fn store_disco(&mut self, node: &Node, disco: EndpointDisco) -> PeerId {
1126        let effective = disco.key();
1127
1128        let id = if effective == node.disco_key {
1129            self.peer_db.upsert(node)
1130        } else {
1131            let mut node = node.clone();
1132            node.disco_key = effective;
1133            self.peer_db.upsert(&node)
1134        };
1135
1136        self.peer_db
1137            .set_inactive_disco_key(id, disco.inactive_key());
1138
1139        id
1140    }
1141
1142    /// Apply the sender key of an inbound disco frame to this peer's two-slot disco state — the
1143    /// `ts_runtime` half of Go [`endpoint.checkAndUpdateDiscoKey`].
1144    ///
1145    /// A peer mid-rotation keeps sending disco under the key it has not yet switched away from.
1146    /// Upstream accepts either of the two keys it knows for the peer and, when the one received is
1147    /// the currently-inactive one, makes it active: receiving under a key is proof of what the peer
1148    /// is using, and is stronger evidence than what control last said. Without this a rotation
1149    /// costs the peer its direct path until control catches up or the peer re-advertises.
1150    ///
1151    /// Returns whether the active key changed, so the caller can republish — which is how the
1152    /// direct manager learns to invalidate the trusted path built under the old key (Go's
1153    /// `changedActiveDiscoLocked`, reached here through the same snapshot diff every other
1154    /// disco-key transition uses).
1155    ///
1156    /// The refusals, all of which leave the peer db untouched:
1157    ///
1158    /// 1. **An unknown peer**, exactly as for a TSMP advertisement.
1159    /// 2. **A peer with no disco key material at all** (Go: `epDisco == nil` ⇒ `false`).
1160    /// 3. **A key belonging to neither slot.** This is the one that carries the security value:
1161    ///    a peer must not be able to move itself onto a key nobody told us about, so a third key
1162    ///    is refused even though the frame that carried it opened correctly.
1163    ///
1164    /// [`endpoint.checkAndUpdateDiscoKey`]: https://github.com/tailscale/tailscale/blob/9ea7cba44591e0cd840c6c94d23274dd222059bf/wgengine/magicsock/endpoint.go
1165    fn observe_disco_key(&mut self, peer: PeerId, key: DiscoPublicKey) -> bool {
1166        let Some((_id, existing)) = self.peer_db.get(&peer) else {
1167            tracing::debug!(?peer, "disco received for an unknown peer; ignoring");
1168            return false;
1169        };
1170
1171        let node = existing.clone();
1172        let Some(disco) = self.endpoint_disco.get_mut(&node.node_key) else {
1173            // Go's `epDisco == nil`: the peer has no key from either source, so there is nothing
1174            // this key could match and nothing to switch to.
1175            tracing::debug!(
1176                ?peer,
1177                "disco received for a peer with no known disco key; ignoring"
1178            );
1179            return false;
1180        };
1181
1182        let Some(changed) = disco.check_and_update(key) else {
1183            tracing::debug!(
1184                ?peer,
1185                %key,
1186                "refusing disco under a key that is neither of the peer's known disco keys"
1187            );
1188            return false;
1189        };
1190
1191        if !changed {
1192            return false;
1193        }
1194
1195        let disco = *disco;
1196        self.store_disco(&node, disco);
1197
1198        tracing::info!(
1199            ?peer,
1200            stable_id = ?node.stable_id,
1201            %key,
1202            "peer is sending disco under its other known key; making that key active"
1203        );
1204
1205        true
1206    }
1207
1208    /// Upsert a control-sourced [`Node`] into the peer db, resolving its disco key against anything
1209    /// this peer has told us over TSMP first.
1210    ///
1211    /// Every node built from control goes through here — `Full`, `Delta { upsert }`, and a
1212    /// `PeersChangedPatch` — so the three cannot diverge on which of the two keys wins. This is the
1213    /// disco half of Go [`endpoint.updateFromNode`]: control's key is written through
1214    /// [`EndpointDisco::update_from_control`] **only when it differs from what control last said**
1215    /// (Go's `if discoKey != n.DiscoKey()` guard, which compares `keyFromControl()`, never the
1216    /// effective key). So a netmap that merely restates the key control already sent leaves an
1217    /// active TSMP key alone — which is the entire point of the advertisement, whose motivating case
1218    /// is a peer whose key control has not caught up with. Control genuinely changing its mind is
1219    /// *recorded* in control's slot, but it does not take the active slot back from a TSMP-learned
1220    /// key: upstream switches back only when disco is received under control's key
1221    /// (`endpoint.checkAndUpdateDiscoKey`). See [`EndpointDisco::update_from_control`].
1222    ///
1223    /// The node lands in the db carrying the *effective* key ([`EndpointDisco::key`]), so the disco
1224    /// index and every send path resolve against the key we would actually send to; the other known
1225    /// key is registered for ingress attribution ([`store_disco`](Self::store_disco)).
1226    ///
1227    /// [`endpoint.updateFromNode`]: https://github.com/tailscale/tailscale/blob/49e148c4a30b4f8098f69468fd27a7021d85ea02/wgengine/magicsock/endpoint.go
1228    fn upsert_from_control(&mut self, node: &Node) -> PeerId {
1229        let node_key = node.node_key;
1230        let from_control = disco_key_from_control(node.disco_key);
1231
1232        let disco = self.endpoint_disco.entry(node_key).or_default();
1233        if disco.key_from_control() != from_control {
1234            disco.update_from_control(from_control);
1235        }
1236        let disco = *disco;
1237
1238        // No key material from either source: Go nils the endpoint's `disco` pointer, so a peer
1239        // that has never had a disco key costs us no entry either.
1240        if disco.is_empty() {
1241            self.endpoint_disco.remove(&node_key);
1242        }
1243
1244        self.store_disco(node, disco)
1245    }
1246
1247    /// The disco key control last gave us for `node_key` — Go `endpointDisco.keyFromControl()`.
1248    fn control_disco_key(&self, node_key: &NodePublicKey) -> Option<DiscoPublicKey> {
1249        self.endpoint_disco
1250            .get(node_key)
1251            .and_then(EndpointDisco::key_from_control)
1252    }
1253
1254    /// Drop [`EndpointDisco`] state for node keys the peer db no longer holds.
1255    ///
1256    /// Go gets this for free: the two keys live on the magicsock `endpoint`, which the peer map keys
1257    /// by node key and deletes when the peer leaves the netmap — and a peer that rotates its node
1258    /// key gets a brand-new endpoint, so a TSMP-learned key is not carried across a rotation. Here
1259    /// the state is a side table, so every control update prunes it to get the same lifetime.
1260    fn prune_endpoint_disco(&mut self) {
1261        if self.endpoint_disco.is_empty() {
1262            return;
1263        }
1264
1265        let peers = &self.peer_db;
1266        self.endpoint_disco
1267            .retain(|node_key, _| peers.has(node_key).is_some());
1268    }
1269
1270    /// Apply a single [`PeerUpdate`](ts_control::PeerUpdate) to the peer db, enforcing the
1271    /// Tailnet-Lock peer-trust chokepoint ([`tka_admits`](Self::tka_admits)) at every upsert site.
1272    ///
1273    /// This is the **single source of truth** for the peer-trust enforcement loop: the actor's
1274    /// netmap [`handle`](Message::handle) calls it, and so do the TKA enforcement tests, so the two
1275    /// real upsert sites (`Full` and `Delta { upsert }`) cannot diverge from what is tested.
1276    ///
1277    /// Returns `(upserts, deletions)` — the [`PeerId`]s touched — for downstream bookkeeping.
1278    fn apply_peer_update(
1279        &mut self,
1280        peer_update: &ts_control::PeerUpdate,
1281    ) -> (HashSet<PeerId>, HashSet<PeerId>) {
1282        let mut upserts = HashSet::default();
1283        let mut deletions = HashSet::default();
1284
1285        match peer_update {
1286            ts_control::PeerUpdate::Full(new_nodes) => {
1287                tracing::trace!("full peer update");
1288
1289                // Borrow the authority ONCE for the whole batch and verify each peer EXACTLY once
1290                // (Go runs `tkaFilterNetmapLocked` once over the assembled netmap; an earlier draft
1291                // verified every peer twice — once for `retained_ids`, once in the upsert loop —
1292                // doubling the ed25519 cost on the hot resync path). `tka_keep_verdicts` is that one
1293                // pass — per-peer signature verdict AND the cross-peer rotation filter — and is
1294                // shared verbatim with `tka_reevaluate_peer_db`, so the netmap path and the
1295                // authority-install path cannot drift apart on what "admitted" means.
1296                //
1297                // The result is a per-NODE keep vector (not a stable_id set), which drives both the
1298                // `retain` (evict revoked peers, keyed by stable_id) and the upsert loop. Judging
1299                // each node by its own verdict means a node whose signature fails is never admitted
1300                // on the strength of a different node that happens to share its stable_id.
1301                //
1302                // Revocation evicts: a peer re-included with a now-invalid/missing signature under an
1303                // active authority fails its verdict, so it is excluded from `retained_ids` and
1304                // `retain` drops the stale (previously-admitted) entry. With no authority the snapshot
1305                // is `None`, so every node passes — byte-for-byte the pre-TKA behavior (no regression).
1306                let authority = self.tka_authority_snapshot();
1307                let node_refs = new_nodes.iter().collect::<Vec<&Node>>();
1308                let keep = Self::tka_keep_verdicts(authority.as_deref(), &node_refs);
1309
1310                // `retained_ids` is the set of stable_ids that survive (drives `retain` to evict the
1311                // rest). It must agree with what the upsert loop below will leave in the db. Control
1312                // should never send two distinct nodes with the same `stable_id` in one `Full`, but if
1313                // it does, `peer_db.upsert` is last-writer-wins on `stable_id`, so the db ends holding
1314                // the LAST kept node for that id. Build `retained_ids` from kept nodes only — a
1315                // stable_id is retained iff at least one of its (possibly duplicate) nodes is kept, so
1316                // the upsert loop's last-kept node lands and `retain` never evicts a just-upserted id.
1317                let retained_ids = new_nodes
1318                    .iter()
1319                    .zip(keep.iter().copied())
1320                    .filter(|(_, k)| *k)
1321                    .map(|(node, _)| &node.stable_id)
1322                    .collect::<HashSet<_>>();
1323
1324                // Isolation diagnostic: an ACTIVE lock that authorized none of the offered peers
1325                // leaves this node with no peers — surface it loudly so a self-lockout (vs an attack)
1326                // is diagnosable. `authority.is_some()` means a real keyed lock (the empty-keyset
1327                // brick-guard admits-all, so it never reaches here with zero retained).
1328                if authority.is_some() && !new_nodes.is_empty() && retained_ids.is_empty() {
1329                    tracing::error!(
1330                        offered = new_nodes.len(),
1331                        "TKA: active lock authorized ZERO of the offered peers; node is isolated \
1332                         (verify the lock state, or disable tailnet lock to recover)"
1333                    );
1334                }
1335
1336                self.peer_db.retain(|id, peer| {
1337                    let retain = retained_ids.contains(&peer.stable_id);
1338
1339                    if !retain {
1340                        deletions.insert(id);
1341                    }
1342
1343                    retain
1344                });
1345
1346                for (node, k) in new_nodes.iter().zip(keep.iter().copied()) {
1347                    if !k {
1348                        continue; // fail-CLOSED: rejected by tailnet lock or rotation-obsolete (above)
1349                    }
1350                    let peer_id = self.upsert_from_control(node);
1351                    upserts.insert(peer_id);
1352                }
1353            }
1354
1355            ts_control::PeerUpdate::Delta { remove, upsert } => {
1356                tracing::trace!("delta peer update");
1357
1358                for peer in upsert {
1359                    if !self.tka_admits(peer) {
1360                        // fail-CLOSED: do not upsert a peer rejected by tailnet lock. If the peer is
1361                        // ALREADY in the db (a delta re-upserting an existing peer whose signature is
1362                        // now invalid — e.g. revoked between syncs), evict the stale entry rather than
1363                        // leaving an unverified peer admitted; Go re-filters the whole netmap each map
1364                        // response, so a now-unsigned peer would not survive there either.
1365                        if let Some((id, _)) = self.peer_db.remove(&peer.stable_id) {
1366                            tracing::warn!(
1367                                stable_id = ?peer.stable_id,
1368                                "TKA: delta re-upsert rejected; evicting now-unauthorized peer"
1369                            );
1370                            deletions.insert(id);
1371                        }
1372                        continue;
1373                    }
1374                    let id = self.upsert_from_control(peer);
1375
1376                    upserts.insert(id);
1377                }
1378
1379                for peer in remove {
1380                    let Some((id, _node)) = self.peer_db.remove(peer) else {
1381                        // A benign, expected race: the peer may already be gone (dropped in a prior
1382                        // `Full`, or fail-closed by TKA — whose now-"unknown" ids commonly reappear in
1383                        // a trailing `peers_removed`). Go treats an unknown removal as a no-op; log at
1384                        // debug, not error, to avoid false-alarm noise on a healthy node (matches the
1385                        // unknown-node handling in `apply_peer_patches`).
1386                        tracing::debug!(
1387                            control_node_id = peer,
1388                            "removed peer was unknown; ignoring"
1389                        );
1390                        continue;
1391                    };
1392
1393                    deletions.insert(id);
1394                }
1395            }
1396        }
1397
1398        self.prune_endpoint_disco();
1399
1400        (upserts, deletions)
1401    }
1402
1403    /// Re-run the Tailnet-Lock filter over the peers **already in the peer db**, evicting the ones
1404    /// the current authority does not admit. Returns the evicted [`PeerId`]s (empty when nothing
1405    /// changed, which is the overwhelmingly common case).
1406    ///
1407    /// # Why this exists (a Go-ordering gap, not an extra feature)
1408    /// Go filters the very netmap that announced the lock: `SetControlClientStatus`
1409    /// (`ipn/ipnlocal/local.go`, v1.100.0) calls `tkaSyncIfNeeded` and then, a few lines later,
1410    /// `tkaFilterNetmapLocked(st.NetMap)` — synchronously, on the same `st.NetMap`, in one pass. So
1411    /// the peers announced alongside `TKAEnabled` are checked by the authority that sync just built.
1412    ///
1413    /// Here the sync is a spawned task (`control_runner`'s `maybe_sync_tka`), so the ordering is
1414    /// inverted: the netmap that carried the `TkaStatus` reaches the peer db *before* the authority
1415    /// exists, and is admitted with enforcement inactive. Without this pass those peers stay
1416    /// admitted — unauthorized ones included — until control happens to send another `Full`, which on
1417    /// a steady map poll may be never. That is the whole initial peer set escaping a lock the node
1418    /// really did sync, so this runs the moment the authority is installed ([`TkaAuthorityChanged`])
1419    /// and brings the db back in line.
1420    ///
1421    /// No authority (nothing synced yet, or the lock was disabled) ⇒ no eviction: enforcement is
1422    /// inactive and every peer is admitted, exactly Go's `b.tka == nil` early return. A peer dropped
1423    /// while the lock was active is **not** resurrected by a later disable — the db no longer holds
1424    /// it and this fork keeps no shadow copy of filtered nodes (Go's `b.tka.filtered`); it returns on
1425    /// the next netmap that re-includes it. That is the safe direction: more restrictive, and
1426    /// connectivity-only.
1427    fn tka_reevaluate_peer_db(&mut self) -> HashSet<PeerId> {
1428        let Some(authority) = self.tka_authority_snapshot() else {
1429            return HashSet::default();
1430        };
1431
1432        // Verdicts first, under an immutable borrow of the db; the eviction below needs `&mut`.
1433        let evicted: HashSet<PeerId> = {
1434            let entries = self
1435                .peer_db
1436                .peers()
1437                .iter()
1438                .map(|(id, node)| (*id, node))
1439                .collect::<Vec<(PeerId, &Node)>>();
1440            let nodes = entries
1441                .iter()
1442                .map(|(_, node)| *node)
1443                .collect::<Vec<&Node>>();
1444            let keep = Self::tka_keep_verdicts(Some(&authority), &nodes);
1445            entries
1446                .iter()
1447                .zip(keep)
1448                .filter_map(|((id, _), keep)| (!keep).then_some(*id))
1449                .collect()
1450        };
1451
1452        if evicted.is_empty() {
1453            return evicted;
1454        }
1455
1456        tracing::warn!(
1457            n_evicted = evicted.len(),
1458            peer_count = self.peer_db.peers().len(),
1459            "TKA: re-filtered the peer db against the newly installed lock authority; evicted \
1460             already-admitted peers"
1461        );
1462        self.peer_db.retain(|id, _| !evicted.contains(&id));
1463        self.prune_endpoint_disco();
1464        evicted
1465    }
1466
1467    /// Apply field-level peer patches (`MapResponse.PeersChangedPatch`), returning the upserted /
1468    /// deleted [`PeerId`]s.
1469    ///
1470    /// This is a SEPARATE channel from [`apply_peer_update`](Self::apply_peer_update): Go's
1471    /// `controlclient` applies the whole-node `Peers*` set first and then `PeersChangedPatch`, so a
1472    /// response that carries both has the peer set applied first (by the caller) and these patches
1473    /// applied second, on top of the freshly-synced nodes. A patch only mutates a peer already in the
1474    /// netmap; an unknown node id is ignored (the wire contract — a patch never creates a node).
1475    fn apply_peer_patches(
1476        &mut self,
1477        patches: &[ts_control::PeerChange],
1478    ) -> (HashSet<PeerId>, HashSet<PeerId>) {
1479        let mut upserts = HashSet::default();
1480        let mut deletions = HashSet::default();
1481
1482        tracing::trace!(n = patches.len(), "peer patch update");
1483
1484        for patch in patches {
1485            // Clone the current node, apply the present fields, and re-upsert through the same path
1486            // as a delta so indexes/routes stay consistent.
1487            let Some((_id, existing)) = self.peer_db.get(&patch.id) else {
1488                tracing::debug!(
1489                    control_node_id = patch.id,
1490                    "peer patch for unknown node; ignoring"
1491                );
1492                continue;
1493            };
1494
1495            let mut node = existing.clone();
1496            if let Some(endpoints) = &patch.underlay_addresses {
1497                node.underlay_addresses = endpoints.clone();
1498            }
1499            if let Some(derp) = patch.derp_region {
1500                node.derp_region = Some(derp);
1501            }
1502            if let Some(cap) = patch.cap {
1503                node.cap = cap;
1504            }
1505            if let Some(cap_map) = &patch.cap_map {
1506                node.cap_map = cap_map.clone();
1507            }
1508            // The db entry carries the EFFECTIVE disco key, which may have been learned over TSMP,
1509            // so restate what CONTROL last said before folding the patch in. Otherwise a patch that
1510            // says nothing about the disco key would hand a TSMP-learned key back as if control had
1511            // sent it, and `upsert_from_control` would write it into control's slot — losing the key
1512            // control actually gave us, on a patch that never mentioned the disco key at all.
1513            node.disco_key = self.control_disco_key(&node.node_key);
1514            if let Some(disco_key) = patch.disco_key {
1515                node.disco_key = Some(disco_key);
1516            }
1517            if let Some(expiry) = patch.node_key_expiry {
1518                node.node_key_expiry = Some(expiry);
1519            }
1520            // Online/last-seen liveness deltas (`PeerChange.Online`/`LastSeen`) — the dominant
1521            // channel by which peer online transitions arrive mid-session. A patch only ever *sets*
1522            // a value (never patches back to unknown), so apply when present.
1523            if let Some(online) = patch.online {
1524                node.online = Some(online);
1525            }
1526            if let Some(last_seen) = patch.last_seen {
1527                node.last_seen = Some(last_seen);
1528            }
1529            // Key rotation: a patch may swap the node key (and its TKA signature). Apply both
1530            // together so the trust gate below verifies the new signature against the new key, never
1531            // a mismatched pair.
1532            if let Some(node_key) = patch.node_key {
1533                node.node_key = node_key;
1534            }
1535            if let Some(sig) = &patch.key_signature {
1536                node.key_signature = sig.clone();
1537            }
1538
1539            // Re-run the tailnet-lock gate on the patched node: a patch that rotates the key must
1540            // satisfy the active authority, exactly like a `Delta` upsert, or it would be a
1541            // trust-enforcement bypass. fail-CLOSED — if the patched node is no longer admitted,
1542            // evict it rather than keep the stale (now-unverified) entry.
1543            if !self.tka_admits(&node) {
1544                if let Some((id, _)) = self.peer_db.remove(&patch.id) {
1545                    tracing::warn!(
1546                        control_node_id = patch.id,
1547                        "peer patch rejected by tailnet lock; evicting peer"
1548                    );
1549                    deletions.insert(id);
1550                }
1551                continue;
1552            }
1553
1554            let id = self.upsert_from_control(&node);
1555            upserts.insert(id);
1556        }
1557
1558        self.prune_endpoint_disco();
1559
1560        (upserts, deletions)
1561    }
1562
1563    /// Apply the standalone online/last-seen delta maps (`MapResponse.OnlineChange` /
1564    /// `PeerSeenChange`, channels C/D) onto the retained netmap. Returns `true` if any node was
1565    /// actually mutated (so the caller knows whether to re-publish).
1566    ///
1567    /// Mirrors Go `controlclient/map.go:updatePeersStateFromResponse` (the two channels are
1568    /// semantically DISTINCT and must not be conflated):
1569    /// - `OnlineChange` (channel C) is the sole driver of a peer's `online` flag (`mut.Online = v`).
1570    /// - `PeerSeenChange` (channel D) is the sole driver of `last_seen`: `true ⇒ LastSeen = now`,
1571    ///   `false ⇒ LastSeen = nil` (cleared). It NEVER touches `online` — "not seen recently" is not
1572    ///   the same as "offline", which only `OnlineChange` asserts.
1573    ///
1574    /// Each entry is keyed by control node id and applies to a peer already in the netmap; an unknown
1575    /// node id is ignored (these maps never create a node). `now` is the wall-clock timestamp for a
1576    /// `PeerSeenChange: true` (Go uses `clock.Now()`); the caller passes it so this stays a pure
1577    /// function of its inputs. Returns `true` if any node was actually mutated.
1578    fn apply_liveness_changes(
1579        &mut self,
1580        online_change: &std::collections::BTreeMap<ts_control::NodeId, bool>,
1581        peer_seen_change: &std::collections::BTreeMap<ts_control::NodeId, bool>,
1582        now: chrono::DateTime<chrono::Utc>,
1583    ) -> bool {
1584        let mut changed = false;
1585
1586        // Channel C — direct online flips (the only writer of `online`).
1587        for (&node_id, &online) in online_change {
1588            if let Some((_pid, existing)) = self.peer_db.get(&node_id)
1589                && existing.online != Some(online)
1590            {
1591                let mut node = existing.clone();
1592                node.online = Some(online);
1593                self.peer_db.upsert(&node);
1594                changed = true;
1595            }
1596        }
1597
1598        // Channel D — peer-seen flips (the only writer of `last_seen`; never touches `online`).
1599        // `true` ⇒ last-seen is now; `false` ⇒ last-seen cleared (Go map.go:820-830).
1600        for (&node_id, &seen) in peer_seen_change {
1601            let new_last_seen = if seen { Some(now) } else { None };
1602            if let Some((_pid, existing)) = self.peer_db.get(&node_id)
1603                && existing.last_seen != new_last_seen
1604            {
1605                let mut node = existing.clone();
1606                node.last_seen = new_last_seen;
1607                self.peer_db.upsert(&node);
1608                changed = true;
1609            }
1610        }
1611
1612        changed
1613    }
1614
1615    /// Test-only constructor: build a [`PeerTracker`] with a chosen initial TKA authority without
1616    /// going through the actor `on_start` path. Returns the tracker plus the **`watch::Sender`** for
1617    /// its enforcement-authority cell, so a test can drive the exact enable/disable transitions the
1618    /// control runner drives at runtime (`tx.send_replace(Some(..))` ⇒ enforce, `tx.send_replace(None)`
1619    /// ⇒ clear). The initial `Some` exercises the fail-closed chokepoint
1620    /// ([`tka_admits`](Self::tka_admits)); `None` is the no-lock admit-all path. The returned sender
1621    /// must be kept alive for the tracker to read updated values.
1622    #[cfg(test)]
1623    fn for_test(
1624        env: Env,
1625        tka_authority: Option<ts_tka::Authority>,
1626    ) -> (Self, watch::Sender<Option<Arc<ts_tka::Authority>>>) {
1627        let (peer_watch, _) = watch::channel(Vec::new());
1628        let (tka_tx, tka_rx) = watch::channel(tka_authority.map(Arc::new));
1629        let tracker = Self {
1630            peer_db: PeerDb::default(),
1631            seen_state_update: false,
1632            pending_requests: Vec::new(),
1633            peer_watch,
1634            user_profiles: HashMap::new(),
1635            endpoint_disco: HashMap::new(),
1636            tka_authority: tka_rx,
1637            env,
1638        };
1639        (tracker, tka_tx)
1640    }
1641
1642    fn service_pending_requests(&mut self) {
1643        if self.seen_state_update {
1644            return;
1645        }
1646
1647        self.seen_state_update = true;
1648
1649        if !self.pending_requests.is_empty() {
1650            tracing::debug!(
1651                n_pending = self.pending_requests.len(),
1652                "state update received, servicing pending requests"
1653            );
1654        }
1655
1656        for req in core::mem::take(&mut self.pending_requests) {
1657            match req {
1658                Pending::PeerByName(PeerByName { name }, reply) => {
1659                    reply.send(self.peer_by_name_opt(&name).cloned());
1660                }
1661                Pending::TailnetIp(PeerByTailnetIp { ip }, reply) => {
1662                    reply.send(self.peer_by_tailnet_ip_opt(ip).cloned());
1663                }
1664                Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, reply) => {
1665                    reply.send(
1666                        self.peer_db
1667                            .get_route(ip.into())
1668                            .map(|(_id, node)| node.clone())
1669                            .collect(),
1670                    );
1671                }
1672                Pending::Status(reply) => {
1673                    reply.send(self.status_peers_with_ids());
1674                }
1675                Pending::WhoIs(Whois { addr }, reply) => {
1676                    reply.send(self.whois_opt(addr));
1677                }
1678            }
1679        }
1680    }
1681}
1682
1683#[cfg(test)]
1684pub(crate) mod tka_tests {
1685    //! Tailnet-Lock (TKA) enforcement tests for the peer-trust chokepoint.
1686    //!
1687    //! These exercise [`PeerTracker::tka_admits`] and the `tka_admits ⇒ upsert` loop the netmap
1688    //! handler runs. The test [`ts_tka::Authority`] is built with [`ts_tka::Authority::from_state`]
1689    //! over a known Ed25519 trusted key, and the signed node-key signature CBOR is produced through
1690    //! `ts_tka`'s public `cbor` encoder + `aum_hash` (the exact same canonical bytes `ts_tka`'s own
1691    //! `direct_signature_verifies_end_to_end` test signs, with no new crypto vectors invented and no
1692    //! private `ts_tka` API used).
1693
1694    use ed25519_dalek::{Signer, SigningKey};
1695    use ts_control::{Node, StableNodeId, TailnetAddress};
1696    use ts_tka::{
1697        AumHash, Authority, Key, KeyKind, State,
1698        cbor::{self, Value},
1699    };
1700
1701    use super::*;
1702
1703    /// `SigKind::Direct` wire value (Go `SigKind`; `ts_tka::SigKind::Direct = 1`).
1704    const SIG_KIND_DIRECT: u64 = 1;
1705
1706    /// The 32-byte node key used across the signed-peer fixtures.
1707    const NODE_KEY_BYTES: [u8; 32] = [7u8; 32];
1708
1709    /// Build a real [`Env`] for the tracker. Only the bus/keys/shutdown plumbing matters here; the
1710    /// TKA gate reads neither, so the forwarding preferences are all benign defaults.
1711    pub(super) fn test_env() -> Env {
1712        let (_shutdown_tx, shutdown_rx) = watch::channel(false);
1713        Env::new(
1714            ts_keys::NodeState::generate(),
1715            shutdown_rx,
1716            crate::env::ForwarderConfig {
1717                accept_routes: false,
1718                accept_dns: true,
1719                exit_node: None,
1720                forward_routes: Vec::new(),
1721                forward_tcp_ports: Vec::new(),
1722                forward_udp_ports: Vec::new(),
1723                forward_all_ports: false,
1724                forward_exit_egress: false,
1725                block_incoming: false,
1726                exit_proxy: None,
1727                peerapi_port: None,
1728                taildrop_dir: None,
1729                enable_ipv6: false,
1730                wireguard_listen_port: None,
1731                network_monitor: false,
1732                persistent_keepalive_interval: None,
1733                ingress_active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1734            },
1735        )
1736    }
1737
1738    /// A minimal peer [`Node`] carrying `node_key` and the given `key_signature`.
1739    ///
1740    /// `pub(crate)` so the cold-start replay tests in `control_runner` build their peers the same way
1741    /// this module's TKA tests do — both run the same filter, and a second hand-rolled fixture could
1742    /// drift from it.
1743    pub(crate) fn peer_node(stable_id: &str, node_key: [u8; 32], key_signature: Vec<u8>) -> Node {
1744        Node {
1745            id: 1,
1746            stable_id: StableNodeId(stable_id.to_string()),
1747            hostname: stable_id.to_string(),
1748            user_id: 0,
1749            tailnet: Some("ts.net".to_string()),
1750            tags: Vec::new(),
1751            addresses: vec![
1752                "100.64.0.1/32".parse().unwrap(),
1753                "fd7a:115c:a1e0::1/128".parse().unwrap(),
1754            ],
1755            tailnet_address: TailnetAddress {
1756                ipv4: "100.64.0.1/32".parse().unwrap(),
1757                ipv6: "fd7a:115c:a1e0::1/128".parse().unwrap(),
1758            },
1759            node_key: node_key.into(),
1760            node_key_expiry: None,
1761            online: None,
1762            last_seen: None,
1763            key_signature,
1764            machine_key: None,
1765            disco_key: None,
1766            accepted_routes: Vec::new(),
1767            underlay_addresses: Vec::new(),
1768            derp_region: None,
1769            cap: Default::default(),
1770            cap_map: Default::default(),
1771            peerapi_port: None,
1772            peerapi_dns_proxy: false,
1773            is_wireguard_only: false,
1774            exit_node_dns_resolvers: Vec::new(),
1775            peer_relay: false,
1776            ssh_host_keys: Vec::new(),
1777            service_vips: Default::default(),
1778            unsigned_peer_api_only: false,
1779        }
1780    }
1781
1782    /// Encode a `Direct` [`ts_tka::NodeKeySignature`] CBOR exactly as `ts_tka`'s private `to_cbor`
1783    /// does (int-map keys: 1=kind, 2=pubkey, 3=key_id, 4=signature; empty byte fields omitted),
1784    /// using only the crate's *public* `cbor` encoder. `signature` of `None` produces the
1785    /// signing-digest preimage (the `SigHash` form).
1786    fn direct_sig_cbor(node_key: &[u8], key_id: &[u8], signature: Option<&[u8]>) -> Vec<u8> {
1787        let mut pairs = alloc_pairs(node_key, key_id);
1788        if let Some(sig) = signature {
1789            pairs.push((4, Some(Value::Bytes(sig.to_vec()))));
1790        }
1791        cbor::int_map(pairs).to_vec()
1792    }
1793
1794    fn alloc_pairs(node_key: &[u8], key_id: &[u8]) -> Vec<(u64, Option<Value>)> {
1795        vec![
1796            (1, Some(Value::Uint(SIG_KIND_DIRECT))),
1797            (2, Some(Value::Bytes(node_key.to_vec()))),
1798            (3, Some(Value::Bytes(key_id.to_vec()))),
1799        ]
1800    }
1801
1802    /// Build a TKA [`Authority`] that trusts `signing.verifying_key()`, plus a valid `Direct`
1803    /// node-key signature CBOR authorizing [`NODE_KEY_BYTES`] under it.
1804    fn authority_and_valid_sig() -> (Authority, Vec<u8>) {
1805        // A fixed, known Ed25519 trusted key (mirrors ts_tka's own end-to-end test seed).
1806        let signing = SigningKey::from_bytes(&[42u8; 32]);
1807        let trusted_pub = signing.verifying_key().to_bytes().to_vec();
1808
1809        let authority = Authority::from_state(
1810            AumHash([0; 32]),
1811            State {
1812                keys: vec![Key {
1813                    kind: KeyKind::Ed25519,
1814                    votes: 1,
1815                    public: trusted_pub.clone(),
1816                }],
1817            },
1818        );
1819
1820        // SigHash preimage = canonical CBOR with the signature field omitted; sign its blake2s hash.
1821        let preimage = direct_sig_cbor(&NODE_KEY_BYTES, &trusted_pub, None);
1822        let sig_hash = ts_tka::aum_hash(&preimage).0;
1823        let signature = signing.sign(&sig_hash).to_bytes().to_vec();
1824
1825        let signed_cbor = direct_sig_cbor(&NODE_KEY_BYTES, &trusted_pub, Some(&signature));
1826        // Sanity: the authority accepts the signature we just built (same path the gate uses).
1827        assert!(
1828            authority
1829                .node_key_authorized(&NODE_KEY_BYTES, &signed_cbor)
1830                .is_ok()
1831        );
1832
1833        (authority, signed_cbor)
1834    }
1835
1836    #[tokio::test]
1837    async fn tka_inactive_upserts_all_peers() {
1838        // No authority ⇒ enforcement inactive ⇒ both a signed and an unsigned peer are admitted.
1839        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
1840
1841        let signed = peer_node("signed", [1u8; 32], vec![0xde, 0xad, 0xbe, 0xef]);
1842        let unsigned = peer_node("unsigned", [2u8; 32], vec![]);
1843
1844        assert!(tracker.tka_admits(&signed));
1845        assert!(tracker.tka_admits(&unsigned));
1846
1847        tracker.peer_db.upsert(&signed);
1848        tracker.peer_db.upsert(&unsigned);
1849        assert_eq!(tracker.peer_db.peers().len(), 2);
1850    }
1851
1852    #[tokio::test]
1853    async fn tka_active_rejects_unsigned_peer() {
1854        // Authority present + peer presents no signature ⇒ rejected (fail-closed), not in peer_db.
1855        let (authority, _sig) = authority_and_valid_sig();
1856        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1857
1858        let unsigned = peer_node("unsigned", NODE_KEY_BYTES, vec![]);
1859        assert!(!tracker.tka_admits(&unsigned));
1860
1861        // Mirror the handler's `if !tka_admits { continue }` loop.
1862        if tracker.tka_admits(&unsigned) {
1863            tracker.peer_db.upsert(&unsigned);
1864        }
1865        assert_eq!(tracker.peer_db.peers().len(), 0);
1866        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1867    }
1868
1869    #[tokio::test]
1870    async fn tka_active_rejects_unsigned_peer_api_only_peer() {
1871        // `UnsignedPeerAPIOnly` buys NO admission exemption here: Go admits such a peer unsigned
1872        // under an active lock (peerAPI-only, no network access), this fork drops it like any other
1873        // unsigned peer. Pins the documented parity gap (`docs/PARITY_ROADMAP.md`, and the
1874        // `ts_control::Node::unsigned_peer_api_only` field docs) so implementing the carve-out has
1875        // to update the prose that promises no peerAPI access today.
1876        let (authority, _sig) = authority_and_valid_sig();
1877        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1878
1879        let mut peer_api_only = peer_node("peerapi-only", NODE_KEY_BYTES, vec![]);
1880        peer_api_only.unsigned_peer_api_only = true;
1881
1882        assert!(
1883            !tracker.tka_admits(&peer_api_only),
1884            "unsigned_peer_api_only must not exempt a peer from the tailnet-lock admission gate"
1885        );
1886
1887        // Mirror the handler's `if !tka_admits { continue }` loop: nothing reaches the peer db, so
1888        // the peer is not reachable for peerAPI either.
1889        if tracker.tka_admits(&peer_api_only) {
1890            tracker.peer_db.upsert(&peer_api_only);
1891        }
1892        assert_eq!(tracker.peer_db.peers().len(), 0);
1893        assert!(tracker.peer_db.get(&peer_api_only.node_key).is_none());
1894    }
1895
1896    #[tokio::test]
1897    async fn tka_empty_keyset_authority_admits_unsigned_peer_api_only_peer() {
1898        // The other side of `tka_active_rejects_unsigned_peer_api_only_peer`: "an authority is
1899        // present" is NOT on its own enough to drop an unsigned peer. The brick-guard fires first,
1900        // so an authority carrying no trusted keys enforces nothing and admits even the peer class
1901        // a keyed lock would reject. Pins the qualifier on the
1902        // `ts_control::Node::unsigned_peer_api_only` field docs — remove the guard and this fails.
1903        use ts_tka::{AumHash, Authority, State};
1904        let empty_auth = Authority::from_state(AumHash([0u8; 32]), State { keys: Vec::new() });
1905        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(empty_auth));
1906
1907        let mut peer_api_only = peer_node("peerapi-only", NODE_KEY_BYTES, vec![]);
1908        peer_api_only.unsigned_peer_api_only = true;
1909
1910        assert!(
1911            tracker.tka_admits(&peer_api_only),
1912            "an empty-keyset authority must not enforce, not even against an unsigned peer"
1913        );
1914
1915        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer_api_only.clone()]));
1916        assert!(
1917            tracker.peer_db.get(&peer_api_only.node_key).is_some(),
1918            "the peer reaches the peer db, so the gate's drop is keyset-conditional"
1919        );
1920    }
1921
1922    #[tokio::test]
1923    async fn tka_active_rejects_bad_signature() {
1924        // Authority present + a signature that fails to verify ⇒ rejected, not in peer_db.
1925        let (authority, mut sig) = authority_and_valid_sig();
1926        // Tamper the last byte (the trailing signature byte) so verification fails.
1927        let last = sig.len() - 1;
1928        sig[last] ^= 0xff;
1929
1930        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1931        let bad = peer_node("bad", NODE_KEY_BYTES, sig);
1932        assert!(!tracker.tka_admits(&bad));
1933
1934        if tracker.tka_admits(&bad) {
1935            tracker.peer_db.upsert(&bad);
1936        }
1937        assert_eq!(tracker.peer_db.peers().len(), 0);
1938    }
1939
1940    #[tokio::test]
1941    async fn tka_active_admits_authorized_peer() {
1942        // Authority present + correctly-signed node key ⇒ admitted and upserted.
1943        let (authority, sig) = authority_and_valid_sig();
1944        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1945
1946        let good = peer_node("good", NODE_KEY_BYTES, sig);
1947        assert!(tracker.tka_admits(&good));
1948
1949        if tracker.tka_admits(&good) {
1950            tracker.peer_db.upsert(&good);
1951        }
1952        assert_eq!(tracker.peer_db.peers().len(), 1);
1953        assert!(tracker.peer_db.get(&good.node_key).is_some());
1954    }
1955
1956    // ---------------------------------------------------------------------------------------------
1957    // Tests that drive REAL `PeerUpdate`s through the shared handler body
1958    // ([`PeerTracker::apply_peer_update`], the single source of truth the actor's netmap `handle`
1959    // also calls), so the two real upsert sites (`Full` and `Delta { upsert }`) are exercised via
1960    // the actual enforcement path — not by hand-mirroring `if !tka_admits { continue }`.
1961    // ---------------------------------------------------------------------------------------------
1962
1963    #[tokio::test]
1964    async fn tka_active_delta_upsert_rejects_unauthorized() {
1965        // Drive a real `Delta { upsert }` whose peer carries no signature. The Delta upsert site
1966        // must reject it under an active authority ⇒ not present in peer_db after the handler runs.
1967        let (authority, _sig) = authority_and_valid_sig();
1968        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1969
1970        let unsigned = peer_node("unsigned", NODE_KEY_BYTES, vec![]);
1971        let update = ts_control::PeerUpdate::Delta {
1972            upsert: vec![unsigned.clone()],
1973            remove: Vec::new(),
1974        };
1975
1976        tracker.apply_peer_update(&update);
1977
1978        assert_eq!(tracker.peer_db.peers().len(), 0);
1979        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
1980    }
1981
1982    #[tokio::test]
1983    async fn tka_active_delta_upsert_admits_authorized() {
1984        // Drive a real `Delta { upsert }` with a correctly-signed peer ⇒ present in peer_db.
1985        let (authority, sig) = authority_and_valid_sig();
1986        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
1987
1988        let good = peer_node("good", NODE_KEY_BYTES, sig);
1989        let update = ts_control::PeerUpdate::Delta {
1990            upsert: vec![good.clone()],
1991            remove: Vec::new(),
1992        };
1993
1994        tracker.apply_peer_update(&update);
1995
1996        assert_eq!(tracker.peer_db.peers().len(), 1);
1997        assert!(tracker.peer_db.get(&good.node_key).is_some());
1998    }
1999
2000    #[tokio::test]
2001    async fn tka_active_full_admits_only_authorized_in_mixed_batch() {
2002        // Drive a real `Full` carrying a MIX of authorized + unauthorized peers. Only the
2003        // correctly-signed peer survives the Full upsert site; the unsigned and bad-sig peers are
2004        // dropped fail-closed.
2005        let (authority, sig) = authority_and_valid_sig();
2006        // A bad-sig variant of the same authorized signature (tamper the trailing byte).
2007        let mut bad_sig = sig.clone();
2008        let last = bad_sig.len() - 1;
2009        bad_sig[last] ^= 0xff;
2010
2011        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2012
2013        // Only the authorized peer carries NODE_KEY_BYTES (the key the authority signed); the
2014        // rejected peers use distinct node keys so the survivor is unambiguous.
2015        let good = peer_node("good", NODE_KEY_BYTES, sig);
2016        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2017        let bad = peer_node("bad", [9u8; 32], bad_sig);
2018
2019        let update =
2020            ts_control::PeerUpdate::Full(vec![good.clone(), unsigned.clone(), bad.clone()]);
2021
2022        tracker.apply_peer_update(&update);
2023
2024        assert_eq!(tracker.peer_db.peers().len(), 1);
2025        assert!(tracker.peer_db.get(&good.node_key).is_some());
2026        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
2027        assert!(tracker.peer_db.get(&bad.node_key).is_none());
2028    }
2029
2030    /// End-to-end through the REAL enforcement-authority transport (the `watch` cell the control
2031    /// runner writes), not a direct field poke: writing `Some(authority)` flips enforcement on so a
2032    /// mixed batch drops the unsigned/bad peers, and a subsequent `None` (lock disabled) clears
2033    /// enforcement so a peer DROPPED while enforced is re-admitted. Exercises the exact `borrow`-based
2034    /// read path `tka_admits` uses — a broken receiver wiring would pass every for_test-field test but
2035    /// fail here.
2036    #[tokio::test]
2037    async fn tka_authority_watch_enables_then_clears_enforcement() {
2038        let (authority, sig) = authority_and_valid_sig();
2039        let mut bad_sig = sig.clone();
2040        let last = bad_sig.len() - 1;
2041        bad_sig[last] ^= 0xff;
2042
2043        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
2044
2045        // 1) No authority yet ⇒ admit-all (Go b.tka == nil).
2046        let good = peer_node("good", NODE_KEY_BYTES, sig.clone());
2047        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2048        let bad = peer_node("bad", [9u8; 32], bad_sig);
2049        let batch = ts_control::PeerUpdate::Full(vec![good.clone(), unsigned.clone(), bad.clone()]);
2050        tracker.apply_peer_update(&batch);
2051        assert_eq!(tracker.peer_db.peers().len(), 3, "no lock ⇒ admit all");
2052
2053        // 2) Publish the verified authority over the watch cell (exactly what the control runner does
2054        //    on a successful sync) ⇒ enforcement ON. A re-applied Full now drops unsigned + bad.
2055        tka_tx.send_replace(Some(Arc::new(authority)));
2056        tracker.apply_peer_update(&batch);
2057        assert_eq!(
2058            tracker.peer_db.peers().len(),
2059            1,
2060            "lock active ⇒ only the signed peer survives"
2061        );
2062        assert!(tracker.peer_db.get(&good.node_key).is_some());
2063        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
2064        assert!(tracker.peer_db.get(&bad.node_key).is_none());
2065
2066        // 3) Lock disabled (None) ⇒ enforcement cleared ⇒ a peer that was DROPPED while enforced is
2067        //    re-admitted by a fresh netmap. Assert the specific previously-dropped key returns (not
2068        //    merely a count), so this proves the drop→clear→re-admit transition, not "admit-all-fresh".
2069        tka_tx.send_replace(None);
2070        tracker.apply_peer_update(&batch);
2071        assert_eq!(
2072            tracker.peer_db.peers().len(),
2073            3,
2074            "lock disabled ⇒ admit all again"
2075        );
2076        assert!(
2077            tracker.peer_db.get(&unsigned.node_key).is_some(),
2078            "the peer dropped under enforcement must come back once the lock is cleared"
2079        );
2080        assert!(tracker.peer_db.get(&bad.node_key).is_some());
2081    }
2082
2083    /// The ordering gap this closes. A peer admitted BEFORE the lock synced must be re-checked the
2084    /// moment the authority is installed — not left in the db until control happens to send another
2085    /// `Full`. Go never has this problem: `SetControlClientStatus` runs `tkaSyncIfNeeded` and then
2086    /// `tkaFilterNetmapLocked(st.NetMap)` on the SAME netmap in one pass, so the netmap that
2087    /// announced the lock is itself filtered. Here the sync is a spawned task, so the netmap lands
2088    /// first and `tka_reevaluate_peer_db` is what restores Go's ordering.
2089    ///
2090    /// Note this test applies NO second netmap: the eviction must come from the authority install
2091    /// alone, which is exactly what was missing before.
2092    #[tokio::test]
2093    async fn tka_authority_install_reevaluates_already_admitted_peers() {
2094        let (authority, sig) = authority_and_valid_sig();
2095        let mut bad_sig = sig.clone();
2096        let last = bad_sig.len() - 1;
2097        bad_sig[last] ^= 0xff;
2098
2099        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
2100
2101        // 1) A netmap arrives while nothing is synced ⇒ enforcement inactive ⇒ all three admitted.
2102        let good = peer_node("good", NODE_KEY_BYTES, sig);
2103        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2104        let bad = peer_node("bad", [9u8; 32], bad_sig);
2105        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2106            good.clone(),
2107            unsigned.clone(),
2108            bad.clone(),
2109        ]));
2110        assert_eq!(tracker.peer_db.peers().len(), 3, "no lock yet ⇒ admit all");
2111        let unsigned_id = tracker
2112            .peer_db
2113            .get(&unsigned.node_key)
2114            .expect("unsigned peer admitted while no lock is synced")
2115            .0;
2116        let bad_id = tracker
2117            .peer_db
2118            .get(&bad.node_key)
2119            .expect("bad-sig peer admitted while no lock is synced")
2120            .0;
2121
2122        // 2) The sync completes and the control runner installs the verified authority.
2123        tka_tx.send_replace(Some(Arc::new(authority)));
2124        let evicted = tracker.tka_reevaluate_peer_db();
2125
2126        assert_eq!(
2127            evicted,
2128            HashSet::from_iter([unsigned_id, bad_id]),
2129            "the unsigned and bad-signature peers are the ones reported evicted"
2130        );
2131        assert_eq!(tracker.peer_db.peers().len(), 1);
2132        assert!(
2133            tracker.peer_db.get(&good.node_key).is_some(),
2134            "the authorized peer stays admitted"
2135        );
2136        assert!(tracker.peer_db.get(&unsigned.node_key).is_none());
2137        assert!(tracker.peer_db.get(&bad.node_key).is_none());
2138
2139        // 3) Idempotent: a second pass over the now-clean db evicts nobody.
2140        assert!(tracker.tka_reevaluate_peer_db().is_empty());
2141    }
2142
2143    /// With no authority the re-evaluation evicts nobody — enforcement is inactive and every peer is
2144    /// admitted, exactly Go's `b.tka == nil` early return. Covers both "never synced" and "the lock
2145    /// was disabled after enforcing", the two ways the cell holds `None`.
2146    #[tokio::test]
2147    async fn tka_reevaluate_without_authority_evicts_nothing() {
2148        let (authority, sig) = authority_and_valid_sig();
2149        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
2150
2151        let good = peer_node("good", NODE_KEY_BYTES, sig);
2152        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2153        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2154            good.clone(),
2155            unsigned.clone(),
2156        ]));
2157
2158        // Never synced.
2159        assert!(tracker.tka_reevaluate_peer_db().is_empty());
2160        assert_eq!(tracker.peer_db.peers().len(), 2);
2161
2162        // Enforced, then disabled: the disable must not evict the peer the lock had authorized, and
2163        // must not start dropping the unsigned one either.
2164        tka_tx.send_replace(Some(Arc::new(authority)));
2165        assert_eq!(tracker.tka_reevaluate_peer_db().len(), 1);
2166        tka_tx.send_replace(None);
2167        assert!(tracker.tka_reevaluate_peer_db().is_empty());
2168        assert!(tracker.peer_db.get(&good.node_key).is_some());
2169    }
2170
2171    /// The re-evaluation runs the WHOLE Go `tkaFilterNetmapLocked` pass, not just the per-peer
2172    /// signature check: a peer presenting a node key that a newer rotation superseded is evicted too,
2173    /// even though its own `Direct` signature still verifies against the authority. Both peers are
2174    /// already in the db when the authority lands, so the cross-peer rotation filter has to run over
2175    /// the db contents — which is why `tka_keep_verdicts` is shared with the `Full` path rather than
2176    /// re-derived here.
2177    #[tokio::test]
2178    async fn tka_reevaluate_applies_the_cross_peer_rotation_filter() {
2179        use ed25519_dalek::SigningKey;
2180        use ts_tka::NodeKeySignature;
2181
2182        let trusted = SigningKey::from_bytes(&[42u8; 32]);
2183        let authority = Authority::from_state(
2184            AumHash([0; 32]),
2185            State {
2186                keys: vec![Key {
2187                    kind: KeyKind::Ed25519,
2188                    votes: 1,
2189                    public: trusted.verifying_key().to_bytes().to_vec(),
2190                }],
2191            },
2192        );
2193        // `stale` holds the pivot key with a valid Direct signature; `rotated` holds a key whose
2194        // rotation chain rotated the pivot key AWAY, which obsoletes `stale`.
2195        let pivot = SigningKey::from_bytes(&[9u8; 32]);
2196        let pivot_pub: [u8; 32] = pivot.verifying_key().to_bytes();
2197        let stale = peer_node(
2198            "stale",
2199            pivot_pub,
2200            NodeKeySignature::sign_direct(&pivot_pub, &trusted).serialize(),
2201        );
2202        let new_key = [4u8; 32];
2203        let rotated = peer_node(
2204            "rotated",
2205            new_key,
2206            NodeKeySignature::sign_rotation(&new_key, &trusted, &pivot).serialize(),
2207        );
2208
2209        // Both admitted while nothing is synced.
2210        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
2211        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2212            stale.clone(),
2213            rotated.clone(),
2214        ]));
2215        assert_eq!(tracker.peer_db.peers().len(), 2, "no lock yet ⇒ admit all");
2216
2217        tka_tx.send_replace(Some(Arc::new(authority)));
2218        let evicted = tracker.tka_reevaluate_peer_db();
2219
2220        assert_eq!(
2221            evicted.len(),
2222            1,
2223            "only the rotation-obsolete peer is evicted"
2224        );
2225        assert!(
2226            tracker.peer_db.get(&rotated.node_key).is_some(),
2227            "the freshly-rotated peer stays"
2228        );
2229        assert!(
2230            tracker.peer_db.get(&stale.node_key).is_none(),
2231            "the peer whose key a rotation superseded is evicted, though its own signature verifies"
2232        );
2233    }
2234
2235    /// A `StateUpdate` carrying nothing but a `Full` peer set — the netmap shape the live-actor test
2236    /// publishes on the bus.
2237    fn netmap_with_peers(peers: Vec<Node>) -> ts_control::StateUpdate {
2238        ts_control::StateUpdate {
2239            session_handle: None,
2240            seq: 0,
2241            keep_alive: false,
2242            derp: None,
2243            node: None,
2244            peer_update: Some(ts_control::PeerUpdate::Full(peers)),
2245            peer_patches: Vec::new(),
2246            user_profiles: Vec::new(),
2247            ping: None,
2248            packetfilter: None,
2249            cap_grants: None,
2250            pop_browser_url: None,
2251            dial_plan: None,
2252            dns_config: None,
2253            ssh_policy: None,
2254            tka: None,
2255            online_change: Default::default(),
2256            peer_seen_change: Default::default(),
2257        }
2258    }
2259
2260    /// Poll a live [`PeerTracker`] until it holds exactly `want` peers, bounded by a timeout so a
2261    /// broken wiring fails the test instead of hanging the suite.
2262    async fn await_peer_count(tracker: &ActorRef<PeerTracker>, want: usize) -> Vec<Node> {
2263        let settled = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2264            loop {
2265                let peers = tracker.ask(AllPeers).await.expect("peer tracker is alive");
2266                if peers.len() == want {
2267                    return peers;
2268                }
2269                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
2270            }
2271        })
2272        .await;
2273        settled.unwrap_or_else(|_| panic!("peer tracker never settled at {want} peer(s)"))
2274    }
2275
2276    /// End-to-end through the LIVE actor, which is the only thing that proves the wiring: the peer
2277    /// tracker watches its own enforcement cell, so the control runner's `send_replace` re-filters
2278    /// the peer db with no further netmap. If the watch task were never spawned (or the message not
2279    /// handled) the unsigned peer would stay admitted forever — a hole every `for_test` unit test
2280    /// above would still pass over, because they call the re-evaluation by hand.
2281    #[tokio::test]
2282    async fn tka_authority_change_refilters_through_the_live_actor() {
2283        use kameo::actor::Spawn as _;
2284
2285        let (authority, sig) = authority_and_valid_sig();
2286        let env = test_env();
2287        let (tka_tx, tka_rx) = watch::channel(None);
2288        let tracker = PeerTracker::spawn((env.clone(), tka_rx));
2289
2290        // Await one reply first: the actor's `on_start` (which registers it on the bus) has then
2291        // completed, so the netmap published below cannot race the subscription.
2292        assert!(
2293            tracker
2294                .ask(AllPeers)
2295                .await
2296                .expect("peer tracker started")
2297                .is_empty()
2298        );
2299
2300        let good = peer_node("good", NODE_KEY_BYTES, sig);
2301        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2302        env.publish(Arc::new(netmap_with_peers(vec![
2303            good.clone(),
2304            unsigned.clone(),
2305        ])))
2306        .await
2307        .expect("publish netmap");
2308
2309        // No lock synced ⇒ both peers land.
2310        await_peer_count(&tracker, 2).await;
2311
2312        // The control runner installs the verified authority. No netmap follows.
2313        tka_tx.send_replace(Some(Arc::new(authority)));
2314
2315        let peers = await_peer_count(&tracker, 1).await;
2316        assert_eq!(
2317            peers[0].stable_id, good.stable_id,
2318            "only the authorized peer survives the authority install"
2319        );
2320    }
2321
2322    /// Degenerate input: two DISTINCT nodes sharing one `stable_id` in a single `Full`, one with a
2323    /// valid signature and one unsigned, under an active lock. Each node is judged by its OWN verdict
2324    /// (the per-node `admits` vector), so the unsigned node is never admitted on the strength of its
2325    /// signed twin. The single-verify `Full` refactor keeps this per-node semantics (a stable_id-set
2326    /// alone would have admitted whichever node was upserted last). Malformed control input; asserted
2327    /// only to lock the verdict-per-node behavior against regression.
2328    #[tokio::test]
2329    async fn tka_full_duplicate_stable_id_judges_each_node_on_its_own_signature() {
2330        let (authority, sig) = authority_and_valid_sig();
2331        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2332
2333        // Both carry stable_id "dup"; the signed one authorizes NODE_KEY_BYTES, the other is unsigned
2334        // and uses a different node key. Order them unsigned-last so a last-writer-wins stable_id set
2335        // would (wrongly) leave the unsigned node's key in the db.
2336        let signed = peer_node("dup", NODE_KEY_BYTES, sig);
2337        let unsigned = peer_node("dup", [8u8; 32], vec![]);
2338        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2339            signed.clone(),
2340            unsigned.clone(),
2341        ]));
2342
2343        // The unsigned node's own verdict failed, so its key must NOT be present, regardless of the
2344        // shared stable_id. (The signed twin retained the stable_id; the db holds the signed key.)
2345        assert!(
2346            tracker.peer_db.get(&unsigned.node_key).is_none(),
2347            "a node whose own signature fails must not be admitted via a stable_id twin"
2348        );
2349        assert!(tracker.peer_db.get(&signed.node_key).is_some());
2350    }
2351
2352    /// Full-path consistency under two KEPT nodes sharing a `stable_id`: `peer_db.upsert` is
2353    /// last-writer-wins on `stable_id`, so the db ends holding exactly one node for that id (the last
2354    /// kept), and `retain` never evicts that just-upserted id (`retained_ids` contains the shared id
2355    /// because at least one of its nodes was kept). No lock here, so both nodes are "kept". This pins
2356    /// the published-state invariant the whole-surface audit flagged: `retain` and the upsert loop
2357    /// agree on the surviving stable_id. Malformed control input; asserted for robustness.
2358    #[tokio::test]
2359    async fn tka_full_duplicate_stable_id_both_kept_is_consistent() {
2360        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2361        let first = peer_node("dup", [1u8; 32], vec![]);
2362        let last = peer_node("dup", [2u8; 32], vec![]);
2363        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2364            first.clone(),
2365            last.clone(),
2366        ]));
2367
2368        // Exactly one db entry for the shared stable_id, holding the LAST node (upsert is
2369        // last-writer-wins on stable_id); the first node's key was transparently superseded.
2370        assert_eq!(
2371            tracker.peer_db.peers().len(),
2372            1,
2373            "one entry for the shared stable_id"
2374        );
2375        assert!(
2376            tracker.peer_db.get(&last.node_key).is_some(),
2377            "the db holds the last-upserted node for the shared id"
2378        );
2379        assert!(
2380            tracker.peer_db.get(&first.node_key).is_none(),
2381            "the first node's key was superseded by the last at the shared id"
2382        );
2383    }
2384
2385    /// A peer admitted in one `Full`, then in a later `Full` presenting a key that a co-resident
2386    /// peer's rotation chain has rotated away, is EVICTED — the cross-peer rotation filter applies on
2387    /// every resync, not only at first admission. Exercises the rotation filter through two
2388    /// sequential `Full` updates with real signing.
2389    #[tokio::test]
2390    async fn tka_full_rotation_obsolete_evicts_on_resync() {
2391        use ed25519_dalek::SigningKey;
2392        use ts_tka::NodeKeySignature;
2393
2394        let trusted = SigningKey::from_bytes(&[42u8; 32]);
2395        let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
2396        let authority = Authority::from_state(
2397            AumHash([0; 32]),
2398            State {
2399                keys: vec![Key {
2400                    kind: KeyKind::Ed25519,
2401                    votes: 1,
2402                    public: trusted_pub.clone(),
2403                }],
2404            },
2405        );
2406        let pivot = SigningKey::from_bytes(&[9u8; 32]);
2407        let pivot_pub: [u8; 32] = pivot.verifying_key().to_bytes();
2408
2409        // First Full: the soon-to-be-stale peer presents the pivot key with a valid Direct sig.
2410        let stale_sig = NodeKeySignature::sign_direct(&pivot_pub, &trusted).serialize();
2411        let stale_peer = peer_node("stale", pivot_pub, stale_sig);
2412        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2413        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![stale_peer.clone()]));
2414        assert!(
2415            tracker.peer_db.get(&stale_peer.node_key).is_some(),
2416            "the stale peer is admitted while no rotation has superseded it yet"
2417        );
2418
2419        // Second Full: a freshly-rotated peer (whose chain rotated AWAY the pivot key) joins, and the
2420        // stale peer is re-included. The rotation filter now obsoletes the pivot key ⇒ stale evicted.
2421        let new_key = [4u8; 32];
2422        let new_sig = NodeKeySignature::sign_rotation(&new_key, &trusted, &pivot).serialize();
2423        let new_peer = peer_node("rotated", new_key, new_sig);
2424        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2425            new_peer.clone(),
2426            stale_peer.clone(),
2427        ]));
2428        assert!(
2429            tracker.peer_db.get(&new_peer.node_key).is_some(),
2430            "the freshly-rotated peer is admitted"
2431        );
2432        assert!(
2433            tracker.peer_db.get(&stale_peer.node_key).is_none(),
2434            "the stale peer is EVICTED on the resync once a rotation supersedes its key"
2435        );
2436    }
2437
2438    /// The empty-trusted-key-state brick-guard: an authority with no keys must NOT drop the whole
2439    /// netmap (a `ts_tka` invariant violation / replayer edge). A verified chain always carries ≥1
2440    /// key, so this never weakens a genuine lock — it only prevents a black-hole. Uses ≥2 peers
2441    /// (one signed, one unsigned) to prove it admits **all**, not accidentally just one.
2442    #[tokio::test]
2443    async fn tka_empty_keyset_authority_admits_all() {
2444        use ts_tka::{AumHash, Authority, State};
2445        let empty_auth = Authority::from_state(AumHash([0u8; 32]), State { keys: Vec::new() });
2446        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(empty_auth));
2447        let signed = peer_node("signed", [7u8; 32], vec![0xde, 0xad]);
2448        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2449        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
2450            signed.clone(),
2451            unsigned.clone(),
2452        ]));
2453        assert_eq!(
2454            tracker.peer_db.peers().len(),
2455            2,
2456            "an empty-keyset authority must admit ALL peers (brick-guard), not enforce"
2457        );
2458    }
2459
2460    /// Signature-replay / `NodeKeyMismatch`: a structurally-valid signature that authorizes
2461    /// `NODE_KEY_BYTES` must NOT admit a DIFFERENT node key carrying that same signature blob. This is
2462    /// the highest-value bypass — if the sig↔node-key binding in `verify_signature` were dropped, this
2463    /// is the only test that would catch it (the other "bad" peers only flip a byte ⇒ `BadSignature`).
2464    #[tokio::test]
2465    async fn tka_active_rejects_valid_sig_for_wrong_node_key() {
2466        let (authority, sig) = authority_and_valid_sig();
2467        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2468
2469        // The signature authorizes NODE_KEY_BYTES; attach it to an imposter with a different key.
2470        let imposter = peer_node("imposter", [0x55u8; 32], sig);
2471        assert!(
2472            !tracker.tka_admits(&imposter),
2473            "a signature bound to one node key must not authorize a different node key"
2474        );
2475        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![imposter.clone()]));
2476        assert!(tracker.peer_db.get(&imposter.node_key).is_none());
2477    }
2478
2479    /// `UntrustedKey`: a signature produced by a well-formed Ed25519 key that is NOT in the
2480    /// authority's trusted-key state must be rejected — distinct from a tampered-byte `BadSignature`.
2481    #[tokio::test]
2482    async fn tka_active_rejects_sig_from_untrusted_key() {
2483        use ed25519_dalek::{Signer, SigningKey};
2484        let (authority, _sig) = authority_and_valid_sig();
2485        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2486
2487        // Sign a valid CBOR with a DIFFERENT key (not the one the authority trusts). The key_id in
2488        // the signature names this untrusted key, so `get_key` misses ⇒ UntrustedKey.
2489        let rogue = SigningKey::from_bytes(&[99u8; 32]);
2490        let rogue_pub = rogue.verifying_key().to_bytes().to_vec();
2491        let preimage = direct_sig_cbor(&NODE_KEY_BYTES, &rogue_pub, None);
2492        let sig_hash = ts_tka::aum_hash(&preimage).0;
2493        let signature = rogue.sign(&sig_hash).to_bytes().to_vec();
2494        let rogue_cbor = direct_sig_cbor(&NODE_KEY_BYTES, &rogue_pub, Some(&signature));
2495
2496        let peer = peer_node("rogue-signed", NODE_KEY_BYTES, rogue_cbor);
2497        assert!(
2498            !tracker.tka_admits(&peer),
2499            "a signature from a key outside the trusted set must be rejected"
2500        );
2501        // Drive the real upsert path too (match the sibling replay test's depth): an untrusted-key
2502        // signature must keep the peer out of the db, not merely fail the verdict in isolation.
2503        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
2504        assert!(tracker.peer_db.get(&peer.node_key).is_none());
2505    }
2506
2507    /// Bus-enable analogue for `Delta`: enforcement engaged via the watch cell must also gate a
2508    /// `Delta { upsert }` (not only `Full`). Closes the "authority arrived over the transport AND the
2509    /// next update is a Delta" combination.
2510    #[tokio::test]
2511    async fn tka_watch_enable_enforces_delta_upsert() {
2512        let (authority, sig) = authority_and_valid_sig();
2513        let (mut tracker, tka_tx) = PeerTracker::for_test(test_env(), None);
2514        tka_tx.send_replace(Some(Arc::new(authority)));
2515
2516        let good = peer_node("good", NODE_KEY_BYTES, sig);
2517        let unsigned = peer_node("unsigned", [8u8; 32], vec![]);
2518        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
2519            remove: vec![],
2520            upsert: vec![good.clone(), unsigned.clone()],
2521        });
2522        assert!(tracker.peer_db.get(&good.node_key).is_some());
2523        assert!(
2524            tracker.peer_db.get(&unsigned.node_key).is_none(),
2525            "delta upsert under an active lock must drop the unsigned peer"
2526        );
2527    }
2528
2529    /// A `Delta` re-upsert of an ALREADY-ADMITTED peer whose signature is now invalid must EVICT the
2530    /// stale entry (revocation-via-delta), not leave it admitted. Go re-filters the whole netmap each
2531    /// response, so a now-unsigned peer would not survive there either.
2532    #[tokio::test]
2533    async fn tka_delta_reupsert_with_invalid_sig_evicts_existing() {
2534        let (authority, sig) = authority_and_valid_sig();
2535        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2536
2537        // Admit the signed peer.
2538        let good = peer_node("good", NODE_KEY_BYTES, sig.clone());
2539        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
2540        assert!(tracker.peer_db.get(&good.node_key).is_some());
2541
2542        // Re-upsert the SAME stable_id (now with no signature) via a delta ⇒ evicted, not retained.
2543        let revoked = peer_node("good", NODE_KEY_BYTES, vec![]);
2544        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
2545            remove: vec![],
2546            upsert: vec![revoked],
2547        });
2548        assert!(
2549            tracker.peer_db.get(&good.node_key).is_none(),
2550            "a delta re-upsert that fails the lock must evict the previously-admitted peer"
2551        );
2552    }
2553
2554    #[tokio::test]
2555    async fn tka_full_resync_revocation_behavior() {
2556        // Revocation-on-resync: admit a peer, then re-include the SAME stable_id in a `Full` with a
2557        // now-invalid signature. Per the Logic review finding, the pre-fix `retain` kept the stale
2558        // (previously-admitted) entry because membership was decided purely by stable_id.
2559        //
2560        // FIXED (not merely documented): the `Full` `retain` now keys on `tka_admits`-passing
2561        // stable_ids, so a peer whose re-included signature no longer verifies under the active
2562        // authority is EVICTED. This test asserts eviction. The inactive (authority=None) path is
2563        // provably unchanged — `tka_admits` always returns `true` there, so the retained set equals
2564        // the set of re-included stable_ids exactly (see `tka_inactive_full_resync_keeps_*`).
2565        let (authority, sig) = authority_and_valid_sig();
2566        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2567
2568        // 1) Admit the peer with a valid signature via a real `Full`.
2569        let good = peer_node("revoked", NODE_KEY_BYTES, sig.clone());
2570        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
2571        assert_eq!(tracker.peer_db.peers().len(), 1);
2572        assert!(tracker.peer_db.get(&good.node_key).is_some());
2573
2574        // 2) Re-sync the SAME stable_id, but with a now-invalid signature (tamper trailing byte).
2575        let mut bad_sig = sig;
2576        let last = bad_sig.len() - 1;
2577        bad_sig[last] ^= 0xff;
2578        let revoked = peer_node("revoked", NODE_KEY_BYTES, bad_sig);
2579        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![revoked.clone()]));
2580
2581        // Eviction: the stale entry is dropped because its re-included signature fails the gate.
2582        assert_eq!(tracker.peer_db.peers().len(), 0);
2583        assert!(tracker.peer_db.get(&revoked.node_key).is_none());
2584    }
2585
2586    #[tokio::test]
2587    async fn tka_inactive_full_resync_keeps_reincluded_peer() {
2588        // Guard the inactive (authority=None) path against the revocation fix: with no authority,
2589        // a peer re-included in a `Full` survives regardless of its signature bytes — byte-for-byte
2590        // pre-TKA behavior, proving the `Full` `retain` change does not regress the always-taken
2591        // branch this wave.
2592        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2593
2594        let peer = peer_node("p", NODE_KEY_BYTES, vec![0xde, 0xad]);
2595        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
2596        assert_eq!(tracker.peer_db.peers().len(), 1);
2597
2598        // Re-sync the same stable_id with garbage signature bytes; inactive enforcement keeps it.
2599        let resynced = peer_node("p", NODE_KEY_BYTES, vec![0x00]);
2600        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![resynced.clone()]));
2601        assert_eq!(tracker.peer_db.peers().len(), 1);
2602        assert!(tracker.peer_db.get(&resynced.node_key).is_some());
2603    }
2604
2605    /// A `Patch` for a peer already in the netmap merges only the fields it carries — here new UDP
2606    /// endpoints and a new home DERP — leaving the rest of the node intact. This is the fix for
2607    /// dropped `peers_changed_patch`: without it the netmap keeps stale endpoints and the peer can
2608    /// never re-handshake after it moves.
2609    #[tokio::test]
2610    async fn patch_merges_endpoints_and_derp_into_existing_peer() {
2611        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2612
2613        // Seed a peer (id == 1, per `peer_node`) with no endpoints / no DERP.
2614        let peer = peer_node("mover", [1u8; 32], vec![]);
2615        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer.clone()]));
2616        let (_pid, before) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2617        assert!(before.underlay_addresses.is_empty());
2618        assert!(before.derp_region.is_none());
2619
2620        // Patch in fresh reachability (the idle-peer-reconnect case).
2621        let new_ep: std::net::SocketAddr = "203.0.113.7:41641".parse().unwrap();
2622        let patch = ts_control::PeerChange {
2623            id: 1,
2624            derp_region: Some(ts_derp::RegionId(core::num::NonZeroU32::new(5).unwrap())),
2625            cap: None,
2626            cap_map: None,
2627            underlay_addresses: Some(vec![new_ep]),
2628            node_key: None,
2629            key_signature: None,
2630            disco_key: None,
2631            node_key_expiry: None,
2632            online: None,
2633            last_seen: None,
2634        };
2635        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
2636
2637        assert_eq!(upserts.len(), 1);
2638        assert_eq!(deletions.len(), 0);
2639        // Same peer, now carrying the patched endpoint + DERP; node key untouched.
2640        assert_eq!(tracker.peer_db.peers().len(), 1);
2641        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2642        assert_eq!(after.underlay_addresses, vec![new_ep]);
2643        assert_eq!(
2644            after.derp_region,
2645            Some(ts_derp::RegionId(core::num::NonZeroU32::new(5).unwrap()))
2646        );
2647        assert_eq!(after.node_key, peer.node_key);
2648    }
2649
2650    /// Regression for `tsr-5u0`: when a whole-node set (`Delta`/`Full`) and a patch co-occur in one
2651    /// response, the patch is applied *on top of* the node the set just upserted — mirroring the
2652    /// handler's apply-order (peer set first, then `peer_patches`). Before the fix the patch shared
2653    /// the single `peer_update` slot and the co-occurring set silently dropped it, so a peer brought
2654    /// in by the delta kept stale (empty) reachability.
2655    #[tokio::test]
2656    async fn patch_applies_on_top_of_co_occurring_delta() {
2657        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2658
2659        // The whole-node delta upserts a brand-new peer (id == 1) with no reachability.
2660        let peer = peer_node("mover", [1u8; 32], vec![]);
2661        let (set_upserts, _) = tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
2662            upsert: vec![peer.clone()],
2663            remove: vec![],
2664        });
2665        assert_eq!(set_upserts.len(), 1, "delta upserts the new peer");
2666
2667        // The patch from the SAME response then sets that peer's endpoints + DERP. This is exactly
2668        // the consumer order the handler runs (apply_peer_update then apply_peer_patches).
2669        let new_ep: std::net::SocketAddr = "203.0.113.7:41641".parse().unwrap();
2670        let patch = ts_control::PeerChange {
2671            id: 1,
2672            derp_region: Some(ts_derp::RegionId(core::num::NonZeroU32::new(7).unwrap())),
2673            cap: None,
2674            cap_map: None,
2675            underlay_addresses: Some(vec![new_ep]),
2676            node_key: None,
2677            key_signature: None,
2678            disco_key: None,
2679            node_key_expiry: None,
2680            online: None,
2681            last_seen: None,
2682        };
2683        let (patch_upserts, patch_deletions) =
2684            tracker.apply_peer_patches(std::slice::from_ref(&patch));
2685
2686        assert_eq!(
2687            patch_upserts.len(),
2688            1,
2689            "patch re-upserts the just-added peer"
2690        );
2691        assert_eq!(patch_deletions.len(), 0);
2692        // The peer added by the delta now carries the patched reachability — the patch was NOT lost.
2693        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2694        assert_eq!(after.underlay_addresses, vec![new_ep]);
2695        assert_eq!(
2696            after.derp_region,
2697            Some(ts_derp::RegionId(core::num::NonZeroU32::new(7).unwrap()))
2698        );
2699    }
2700
2701    /// A `Patch` whose node id is not in the current netmap is ignored (the wire contract: a patch
2702    /// never creates a node). No upsert, no deletion, peer set unchanged.
2703    #[tokio::test]
2704    async fn patch_for_unknown_node_is_ignored() {
2705        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2706        let known = peer_node("known", [1u8; 32], vec![]); // id == 1
2707        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![known]));
2708
2709        let patch = ts_control::PeerChange {
2710            id: 999, // not in the netmap
2711            derp_region: None,
2712            cap: None,
2713            cap_map: None,
2714            underlay_addresses: Some(vec!["198.51.100.9:1".parse().unwrap()]),
2715            node_key: None,
2716            key_signature: None,
2717            disco_key: None,
2718            node_key_expiry: None,
2719            online: None,
2720            last_seen: None,
2721        };
2722        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
2723
2724        assert_eq!(upserts.len(), 0);
2725        assert_eq!(deletions.len(), 0);
2726        assert_eq!(tracker.peer_db.peers().len(), 1);
2727        assert!(tracker.peer_db.get(&(999 as ts_control::NodeId)).is_none());
2728    }
2729
2730    /// An expiry-only `Patch` updates `node_key_expiry` on the matching peer (Go
2731    /// `PeerChange.KeyExpiry`), rather than being silently dropped until the next full resync.
2732    #[tokio::test]
2733    async fn patch_updates_node_key_expiry() {
2734        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2735        let peer = peer_node("expiring", [1u8; 32], vec![]); // id == 1, node_key_expiry: None
2736        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2737
2738        let expiry = "2027-01-01T00:00:00Z"
2739            .parse::<chrono::DateTime<chrono::Utc>>()
2740            .unwrap();
2741        let patch = ts_control::PeerChange {
2742            id: 1,
2743            derp_region: None,
2744            cap: None,
2745            cap_map: None,
2746            underlay_addresses: None,
2747            node_key: None,
2748            key_signature: None,
2749            disco_key: None,
2750            node_key_expiry: Some(expiry),
2751            online: None,
2752            last_seen: None,
2753        };
2754        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2755
2756        let (_pid, after) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2757        assert_eq!(after.node_key_expiry, Some(expiry));
2758    }
2759
2760    /// Channel B: a `PeerChange.online` patch flips a peer's online state without a full node.
2761    #[tokio::test]
2762    async fn patch_updates_online() {
2763        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2764        let peer = peer_node("p", [1u8; 32], vec![]); // id == 1, online: None
2765        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2766        assert_eq!(
2767            tracker
2768                .peer_db
2769                .get(&(1 as ts_control::NodeId))
2770                .unwrap()
2771                .1
2772                .online,
2773            None
2774        );
2775
2776        let mut patch = ts_control::PeerChange {
2777            id: 1,
2778            derp_region: None,
2779            cap: None,
2780            cap_map: None,
2781            underlay_addresses: None,
2782            node_key: None,
2783            key_signature: None,
2784            disco_key: None,
2785            node_key_expiry: None,
2786            online: Some(true),
2787            last_seen: None,
2788        };
2789        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2790        assert_eq!(
2791            tracker
2792                .peer_db
2793                .get(&(1 as ts_control::NodeId))
2794                .unwrap()
2795                .1
2796                .online,
2797            Some(true),
2798            "PeerChange.online=Some(true) marks the peer online"
2799        );
2800
2801        // A subsequent patch flips it offline.
2802        patch.online = Some(false);
2803        tracker.apply_peer_patches(std::slice::from_ref(&patch));
2804        assert_eq!(
2805            tracker
2806                .peer_db
2807                .get(&(1 as ts_control::NodeId))
2808                .unwrap()
2809                .1
2810                .online,
2811            Some(false)
2812        );
2813    }
2814
2815    /// Channel C/D (Go `map.go:updatePeersStateFromResponse`): `online_change` is the sole driver of
2816    /// `online`; `peer_seen_change` is the sole driver of `last_seen` (true ⇒ now, false ⇒ cleared)
2817    /// and must NEVER touch `online`. Both apply to a peer already in the netmap and ignore unknown
2818    /// ids. This pins the fix for the prior bug where channel D wrote `online=false` (conflating
2819    /// "not seen recently" with "offline" — distinct signals in Go).
2820    #[tokio::test]
2821    async fn liveness_change_maps_apply_online() {
2822        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2823        let peer = peer_node("p", [1u8; 32], vec![]); // id == 1
2824        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2825        // A fixed timestamp (chrono is built without its `clock` feature, so no `Utc::now()`).
2826        let now = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
2827
2828        // Channel C: online_change sets online=true.
2829        let mut online_change = std::collections::BTreeMap::new();
2830        online_change.insert(1 as ts_control::NodeId, true);
2831        online_change.insert(999 as ts_control::NodeId, true); // unknown id — ignored
2832        let changed = tracker.apply_liveness_changes(&online_change, &Default::default(), now);
2833        assert!(changed);
2834        assert_eq!(
2835            tracker
2836                .peer_db
2837                .get(&(1 as ts_control::NodeId))
2838                .unwrap()
2839                .1
2840                .online,
2841            Some(true)
2842        );
2843
2844        // Channel D: peer_seen_change=true sets last_seen=now and leaves online UNTOUCHED.
2845        let mut seen_true = std::collections::BTreeMap::new();
2846        seen_true.insert(1 as ts_control::NodeId, true);
2847        let changed = tracker.apply_liveness_changes(&Default::default(), &seen_true, now);
2848        assert!(changed);
2849        {
2850            let (_id, node) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2851            assert_eq!(
2852                node.last_seen,
2853                Some(now),
2854                "peer_seen_change=true sets last_seen=now"
2855            );
2856            assert_eq!(
2857                node.online,
2858                Some(true),
2859                "channel D must NOT touch online (still true from channel C)"
2860            );
2861        }
2862
2863        // Channel D: peer_seen_change=false clears last_seen, still leaving online untouched.
2864        let mut seen_false = std::collections::BTreeMap::new();
2865        seen_false.insert(1 as ts_control::NodeId, false);
2866        let changed = tracker.apply_liveness_changes(&Default::default(), &seen_false, now);
2867        assert!(changed);
2868        {
2869            let (_id, node) = tracker.peer_db.get(&(1 as ts_control::NodeId)).unwrap();
2870            assert_eq!(
2871                node.last_seen, None,
2872                "peer_seen_change=false clears last_seen"
2873            );
2874            assert_eq!(node.online, Some(true), "channel D must NOT mark offline");
2875        }
2876        assert_eq!(
2877            tracker.peer_db.peers().len(),
2878            1,
2879            "the node is retained, not removed"
2880        );
2881
2882        // No-op when nothing matches / changes.
2883        assert!(!tracker.apply_liveness_changes(&Default::default(), &Default::default(), now));
2884    }
2885
2886    /// Security: a `Patch` that rotates the node key must re-satisfy the tailnet-lock authority,
2887    /// exactly like a `Delta` upsert. A key-rotation patch whose new signature does NOT verify
2888    /// evicts the peer (fail-closed) rather than leaving a now-unverified entry — closing what would
2889    /// otherwise be a trust-enforcement bypass via the patch path.
2890    #[tokio::test]
2891    async fn patch_key_rotation_failing_tka_evicts_peer() {
2892        let (authority, sig) = authority_and_valid_sig();
2893        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
2894
2895        // Admit a correctly-signed peer (id == 1).
2896        let good = peer_node("rotator", NODE_KEY_BYTES, sig.clone());
2897        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![good.clone()]));
2898        assert_eq!(tracker.peer_db.peers().len(), 1);
2899
2900        // Patch a new node key whose signature is garbage under the active authority.
2901        let patch = ts_control::PeerChange {
2902            id: 1,
2903            derp_region: None,
2904            cap: None,
2905            cap_map: None,
2906            underlay_addresses: None,
2907            node_key: Some([0x33u8; 32].into()),
2908            key_signature: Some(vec![0x00, 0x01, 0x02]),
2909            disco_key: None,
2910            node_key_expiry: None,
2911            online: None,
2912            last_seen: None,
2913        };
2914        let (upserts, deletions) = tracker.apply_peer_patches(std::slice::from_ref(&patch));
2915
2916        assert_eq!(upserts.len(), 0);
2917        assert_eq!(deletions.len(), 1);
2918        assert_eq!(tracker.peer_db.peers().len(), 0);
2919    }
2920
2921    /// A node's `user_id` joins against the accumulated UserProfiles table to resolve the owning
2922    /// user's profile in `WhoIs.user_profile`. With no matching profile, it is `None` (the
2923    /// pre-existing behavior); once a profile arrives, the same node resolves to it. This
2924    /// proves the accumulate-then-join path the netmap handler builds.
2925    fn profile(id: ts_control::UserId, login: &str) -> ts_control::UserProfile {
2926        ts_control::UserProfile {
2927            id,
2928            login_name: login.to_string(),
2929            display_name: None,
2930            groups: Vec::new(),
2931        }
2932    }
2933
2934    #[tokio::test]
2935    async fn whois_resolves_user_from_accumulated_profiles() {
2936        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
2937
2938        // A peer owned by user id 42 at 100.64.0.1 (the peer_node fixture's address).
2939        let mut peer = peer_node("p", NODE_KEY_BYTES, Vec::new());
2940        peer.user_id = 42;
2941        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![peer]));
2942        let addr = "100.64.0.1:0".parse().unwrap();
2943
2944        // No profile yet: the node resolves but its owner is unknown.
2945        let who = tracker.whois_opt(addr).expect("peer is known");
2946        assert_eq!(who.user_profile, None);
2947        assert_eq!(who.user(), None);
2948
2949        // Profile for a DIFFERENT user must not match.
2950        tracker
2951            .user_profiles
2952            .insert(7, profile(7, "someone-else@example.com"));
2953        assert_eq!(tracker.whois_opt(addr).unwrap().user(), None);
2954
2955        // The owning user's profile arrives (as the netmap handler would accumulate it): now the
2956        // login resolves.
2957        tracker
2958            .user_profiles
2959            .insert(42, profile(42, "alice@example.com"));
2960        assert_eq!(
2961            tracker.whois_opt(addr).unwrap().user(),
2962            Some("alice@example.com".to_string())
2963        );
2964    }
2965
2966    /// The whole carry, end to end: a real `MapResponse` body — the JSON control writes on the map
2967    /// poll — decoded by the production wire types and the production `From` impls, accumulated by
2968    /// the production profile merge, and read back out of `whois`.
2969    ///
2970    /// `Groups` is why `WhoIs` carries the profile rather than one display label: it is the only
2971    /// attribute of an owning user that a node cannot re-derive from anything else in the netmap,
2972    /// so an embedder authorising an inbound connection on group membership has no other source
2973    /// for it. Every hop here is production code; the only thing the test assembles is the
2974    /// `StateUpdate` struct itself (`ts_control`'s frame decode is not public, and its own tests
2975    /// pin the body-to-`StateUpdate` half).
2976    fn state_update_from_body(body: &str) -> ts_control::StateUpdate {
2977        let wire: ts_control_serde::MapResponse<'_> =
2978            serde_json::from_str(body).expect("a real MapResponse body decodes");
2979        let peers = wire
2980            .peers
2981            .as_ref()
2982            .expect("the fixture carries a full peer set")
2983            .iter()
2984            .map(ts_control::Node::from)
2985            .collect();
2986        ts_control::StateUpdate {
2987            user_profiles: wire
2988                .user_profiles
2989                .iter()
2990                .map(ts_control::UserProfile::from)
2991                .collect(),
2992            ..netmap_with_peers(peers)
2993        }
2994    }
2995
2996    /// The body control sends for a tailnet with one peer owned by user 42, whose profile carries
2997    /// `Groups`. `groups` is spliced in so the present and absent cases share one fixture.
2998    fn netmap_body_with_profile_groups(groups: &str) -> String {
2999        format!(
3000            r#"{{
3001                "MapSessionHandle": "sess-1",
3002                "Seq": 9,
3003                "Peers": [{{
3004                    "ID": 2,
3005                    "StableID": "peer-2",
3006                    "Name": "peer.example.ts.net.",
3007                    "Addresses": ["100.64.0.1/32", "fd7a:115c:a1e0::1/128"],
3008                    "User": 42
3009                }}],
3010                "UserProfiles": [{{
3011                    "ID": 42,
3012                    "LoginName": "alice@example.com",
3013                    "DisplayName": "Alice Smith"{groups}
3014                }}]
3015            }}"#
3016        )
3017    }
3018
3019    #[tokio::test]
3020    async fn whois_carries_user_groups_from_a_real_map_response() {
3021        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
3022        let update = state_update_from_body(&netmap_body_with_profile_groups(
3023            r#", "Groups": ["engineering@example.com", "group:eng"]"#,
3024        ));
3025
3026        tracker.accumulate_user_profiles(&update.user_profiles);
3027        tracker.apply_peer_update(update.peer_update.as_ref().expect("a full peer set"));
3028
3029        let who = tracker
3030            .whois_opt("100.64.0.1:0".parse().unwrap())
3031            .expect("the peer owns that address");
3032
3033        let profile = who.user_profile.as_ref().expect("user 42's profile");
3034        assert_eq!(profile.id, 42);
3035        assert_eq!(profile.login_name, "alice@example.com");
3036        assert_eq!(profile.display_name.as_deref(), Some("Alice Smith"));
3037        assert_eq!(
3038            who.user_groups(),
3039            ["engineering@example.com", "group:eng"],
3040            "the groups control reported reach the embedder in the order control sent them"
3041        );
3042        // The flattened label the pre-widening `WhoIs.user` field carried is unchanged.
3043        assert_eq!(who.user(), Some("alice@example.com".to_string()));
3044    }
3045
3046    /// The absent case, which is what every control server that does not send the field looks
3047    /// like: no `Groups` key at all. That must yield a profile with an EMPTY group list — never a
3048    /// missing profile, and never a failed decode that would drop the owner identity entirely.
3049    #[tokio::test]
3050    async fn a_map_response_without_groups_yields_an_empty_group_list() {
3051        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
3052        let update = state_update_from_body(&netmap_body_with_profile_groups(""));
3053
3054        tracker.accumulate_user_profiles(&update.user_profiles);
3055        tracker.apply_peer_update(update.peer_update.as_ref().expect("a full peer set"));
3056
3057        let who = tracker
3058            .whois_opt("100.64.0.1:0".parse().unwrap())
3059            .expect("the peer owns that address");
3060
3061        assert!(
3062            who.user_profile.is_some(),
3063            "an omitted Groups must not cost us the profile"
3064        );
3065        assert_eq!(who.user(), Some("alice@example.com".to_string()));
3066        assert!(who.user_groups().is_empty());
3067    }
3068
3069    /// Control sends profiles incrementally, so a later response restating user 42 replaces the
3070    /// held copy wholesale — including a group list that SHRANK. A membership control has revoked
3071    /// must stop being reported, or an embedder authorising on it keeps honouring it forever.
3072    #[tokio::test]
3073    async fn a_restated_profile_replaces_the_held_group_list() {
3074        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
3075        let first = state_update_from_body(&netmap_body_with_profile_groups(
3076            r#", "Groups": ["group:eng", "group:oncall"]"#,
3077        ));
3078        tracker.accumulate_user_profiles(&first.user_profiles);
3079        tracker.apply_peer_update(first.peer_update.as_ref().expect("a full peer set"));
3080
3081        let second = state_update_from_body(&netmap_body_with_profile_groups(
3082            r#", "Groups": ["group:eng"]"#,
3083        ));
3084        tracker.accumulate_user_profiles(&second.user_profiles);
3085
3086        let who = tracker
3087            .whois_opt("100.64.0.1:0".parse().unwrap())
3088            .expect("the peer owns that address");
3089        assert_eq!(who.user_groups(), ["group:eng"]);
3090    }
3091
3092    /// `UserProfile::best_label` prefers the login name, falling back to display name, else `None`.
3093    #[test]
3094    fn user_profile_best_label_prefers_login() {
3095        assert_eq!(
3096            profile(1, "alice@example.com").best_label(),
3097            Some("alice@example.com".to_string())
3098        );
3099        let display_only = ts_control::UserProfile {
3100            id: 2,
3101            login_name: String::new(),
3102            display_name: Some("Bob".to_string()),
3103            groups: Vec::new(),
3104        };
3105        assert_eq!(display_only.best_label(), Some("Bob".to_string()));
3106        let empty = ts_control::UserProfile {
3107            id: 3,
3108            login_name: String::new(),
3109            display_name: None,
3110            groups: Vec::new(),
3111        };
3112        assert_eq!(empty.best_label(), None);
3113    }
3114
3115    // ----- tsr-jo1: RotationTracker (Go ipnlocal.rotationTracker.obsoleteKeys) -----
3116
3117    /// A `RotationDetails` for a `Direct`-rooted chain with the given prior keys + wrapping key.
3118    fn rot_details(
3119        prev: &[&[u8]],
3120        wrapping: &[u8],
3121        kind: ts_tka::SigKind,
3122    ) -> ts_tka::RotationDetails {
3123        ts_tka::RotationDetails {
3124            prev_node_keys: prev.iter().map(|p| p.to_vec()).collect(),
3125            initial_sig_kind: kind,
3126            initial_wrapping_pubkey: wrapping.to_vec(),
3127        }
3128    }
3129
3130    /// Rule 1: every prior node key named by any rotation chain is obsolete, regardless of the
3131    /// chain's root kind (Go's ungated `obsolete.AddSlice(d.PrevNodeKeys)`).
3132    #[test]
3133    fn rotation_tracker_prev_keys_always_obsolete() {
3134        let mut t = RotationTracker::default();
3135        // A Direct-rooted chain that rotated away OLD1, and a Credential-rooted one that rotated OLD2.
3136        t.add(
3137            b"newA".to_vec(),
3138            &rot_details(&[b"OLD1"], b"wrapA", ts_tka::SigKind::Direct),
3139        );
3140        t.add(
3141            b"newB".to_vec(),
3142            &rot_details(&[b"OLD2"], b"wrapB", ts_tka::SigKind::Credential),
3143        );
3144        let obsolete = t.obsolete_keys();
3145        assert!(
3146            obsolete.contains(b"OLD1".as_slice()),
3147            "Direct chain's prior key obsolete"
3148        );
3149        assert!(
3150            obsolete.contains(b"OLD2".as_slice()),
3151            "Credential chain's prior key obsolete too (rule 1 is ungated)"
3152        );
3153        // The current keys themselves are not obsolete (only one peer per wrapping key here).
3154        assert!(!obsolete.contains(b"newA".as_slice()));
3155        assert!(!obsolete.contains(b"newB".as_slice()));
3156    }
3157
3158    /// Rule 2: among `Direct`-rooted chains sharing a wrapping key, only the longest survives; the
3159    /// shorter (older) clone's key is obsolete.
3160    #[test]
3161    fn rotation_tracker_unequal_chain_keeps_longest() {
3162        let mut t = RotationTracker::default();
3163        // Same wrapping key; "long" has 2 prior keys, "short" has 1 ⇒ "short" is the older clone.
3164        t.add(
3165            b"long".to_vec(),
3166            &rot_details(&[b"p1", b"p2"], b"wrap", ts_tka::SigKind::Direct),
3167        );
3168        t.add(
3169            b"short".to_vec(),
3170            &rot_details(&[b"q1"], b"wrap", ts_tka::SigKind::Direct),
3171        );
3172        let obsolete = t.obsolete_keys();
3173        assert!(
3174            obsolete.contains(b"short".as_slice()),
3175            "the shorter-chain clone is obsolete"
3176        );
3177        assert!(
3178            !obsolete.contains(b"long".as_slice()),
3179            "the longest-chain peer survives"
3180        );
3181    }
3182
3183    /// Rule 2 tie: two `Direct`-rooted chains sharing a wrapping key with EQUAL chain length cannot
3184    /// be disambiguated ⇒ BOTH are dropped (Go's safety branch).
3185    #[test]
3186    fn rotation_tracker_equal_chain_drops_both() {
3187        let mut t = RotationTracker::default();
3188        t.add(
3189            b"cloneA".to_vec(),
3190            &rot_details(&[b"p1"], b"wrap", ts_tka::SigKind::Direct),
3191        );
3192        t.add(
3193            b"cloneB".to_vec(),
3194            &rot_details(&[b"p2"], b"wrap", ts_tka::SigKind::Direct),
3195        );
3196        let obsolete = t.obsolete_keys();
3197        assert!(
3198            obsolete.contains(b"cloneA".as_slice()),
3199            "tied clone A dropped"
3200        );
3201        assert!(
3202            obsolete.contains(b"cloneB".as_slice()),
3203            "tied clone B dropped"
3204        );
3205    }
3206
3207    /// `Credential`-rooted chains sharing a wrapping key are EXEMPT from rule 2 (reusable-authkey
3208    /// carve-out): both are kept even with equal chain length.
3209    #[test]
3210    fn rotation_tracker_credential_root_clones_both_kept() {
3211        let mut t = RotationTracker::default();
3212        t.add(
3213            b"credA".to_vec(),
3214            &rot_details(&[b"p1"], b"wrap", ts_tka::SigKind::Credential),
3215        );
3216        t.add(
3217            b"credB".to_vec(),
3218            &rot_details(&[b"p2"], b"wrap", ts_tka::SigKind::Credential),
3219        );
3220        let obsolete = t.obsolete_keys();
3221        assert!(
3222            !obsolete.contains(b"credA".as_slice()),
3223            "credential-rooted clone A kept"
3224        );
3225        assert!(
3226            !obsolete.contains(b"credB".as_slice()),
3227            "credential-rooted clone B kept"
3228        );
3229    }
3230
3231    /// A peer that another chain already rotated away does not also act as a surviving clone: it is
3232    /// removed from its wrapping-key group before the longest-survivor pick (Go's `DeleteFunc`).
3233    #[test]
3234    fn rotation_tracker_already_obsolete_peer_not_a_survivor() {
3235        let mut t = RotationTracker::default();
3236        // "victim" is rotated away by "rotator" (different wrapping key), AND shares wrapping key
3237        // "w" with "other". Because "victim" is already obsolete, only "other" is in play for "w" and
3238        // survives (no spurious tie-drop of "other").
3239        t.add(
3240            b"rotator".to_vec(),
3241            &rot_details(&[b"victim"], b"wRot", ts_tka::SigKind::Direct),
3242        );
3243        t.add(
3244            b"victim".to_vec(),
3245            &rot_details(&[b"x"], b"w", ts_tka::SigKind::Direct),
3246        );
3247        t.add(
3248            b"other".to_vec(),
3249            &rot_details(&[b"y"], b"w", ts_tka::SigKind::Direct),
3250        );
3251        let obsolete = t.obsolete_keys();
3252        assert!(
3253            obsolete.contains(b"victim".as_slice()),
3254            "victim rotated away by rotator"
3255        );
3256        assert!(
3257            !obsolete.contains(b"other".as_slice()),
3258            "other survives — victim was removed from the group before the tie check"
3259        );
3260    }
3261
3262    /// Empty tracker (no rotation-signed peers) ⇒ no obsolete keys (the non-rotation netmap path).
3263    #[test]
3264    fn rotation_tracker_empty_is_noop() {
3265        let t = RotationTracker::default();
3266        assert!(t.obsolete_keys().is_empty());
3267    }
3268
3269    /// End-to-end through the real `Full` path: a peer presenting a freshly-rotated key (a Rotation
3270    /// chain) is admitted, while a second peer still presenting the rotated-AWAY pivot key — even with
3271    /// that key's own still-valid Direct signature — is DROPPED by the cross-peer rotation filter.
3272    /// This is the gap closed here: Go `tkaFilterNetmapLocked` drops the stale clone; we used to admit
3273    /// it. Uses real `ts_tka` signing (`sign_direct` + `sign_rotation`) so the whole
3274    /// verify → details → filter pipeline runs.
3275    ///
3276    /// Construction: the trusted key signs an inner `Direct` over the PIVOT keypair's public key; the
3277    /// pivot key then signs an outer `Rotation` authorizing `new_key`. That chain's `prev_node_keys`
3278    /// names the pivot pubkey — so a peer presenting the pivot pubkey as its node key is the
3279    /// rotated-away key the filter must drop.
3280    #[tokio::test]
3281    async fn tka_full_drops_rotated_away_key_e2e() {
3282        use ed25519_dalek::SigningKey;
3283        use ts_tka::NodeKeySignature;
3284
3285        let trusted = SigningKey::from_bytes(&[42u8; 32]);
3286        let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
3287        let authority = Authority::from_state(
3288            AumHash([0; 32]),
3289            State {
3290                keys: vec![Key {
3291                    kind: KeyKind::Ed25519,
3292                    votes: 1,
3293                    public: trusted_pub.clone(),
3294                }],
3295            },
3296        );
3297
3298        // The rotation pivot: a keypair whose public key the inner Direct authorizes and whose
3299        // private key signs the outer rotation wrap. This pivot pubkey IS the key being rotated away.
3300        let pivot = SigningKey::from_bytes(&[9u8; 32]);
3301        let pivot_pub: [u8; 32] = pivot.verifying_key().to_bytes();
3302
3303        let new_key = [4u8; 32]; // the freshly-rotated node key
3304
3305        // Fresh peer: a Rotation chain authorizing `new_key`, inner Direct over the pivot signed by
3306        // trusted, outer wrap signed by the pivot. Its prev_node_keys names `pivot_pub`.
3307        let new_sig = NodeKeySignature::sign_rotation(&new_key, &trusted, &pivot).serialize();
3308        let new_peer = peer_node("rotated", new_key, new_sig);
3309
3310        // Stale peer: still presents the pivot pubkey (the rotated-away key) with its own valid
3311        // Direct signature — valid in isolation, but obsoleted by the fresh peer's rotation chain.
3312        let stale_sig = NodeKeySignature::sign_direct(&pivot_pub, &trusted).serialize();
3313        let stale_peer = peer_node("stale", pivot_pub, stale_sig);
3314
3315        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), Some(authority));
3316        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![
3317            new_peer.clone(),
3318            stale_peer.clone(),
3319        ]));
3320
3321        assert!(
3322            tracker.peer_db.get(&new_peer.node_key).is_some(),
3323            "the freshly-rotated peer is admitted"
3324        );
3325        assert!(
3326            tracker.peer_db.get(&stale_peer.node_key).is_none(),
3327            "the peer presenting the rotated-away key is dropped (Go tkaFilterNetmapLocked)"
3328        );
3329    }
3330}
3331
3332#[cfg(test)]
3333mod tsmp_disco_key_tests {
3334    //! Receive side of the TSMP disco-key advertisement, at the point the key is *learned*.
3335    //!
3336    //! These exercise [`PeerTracker::learn_disco_key`] — the fork's stand-in for Go
3337    //! `magicsock.Conn.HandleDiscoKeyAdvertisement` — which is the single place an advertisement
3338    //! reaches peer state. The wire decode and the "consumed, not delivered" drop are covered in
3339    //! `ts_packet::tsmp` and `ts_dataplane` respectively.
3340
3341    use ts_keys::DiscoPublicKey;
3342
3343    use super::{
3344        tka_tests::{peer_node, test_env},
3345        *,
3346    };
3347
3348    /// The key a peer advertises, and a second one for the re-advertise case.
3349    const ADVERTISED: [u8; 32] = [0xa5u8; 32];
3350    const READVERTISED: [u8; 32] = [0x5au8; 32];
3351    /// The (staler) key control has for that same peer, and the one control eventually catches up
3352    /// to.
3353    const FROM_CONTROL: [u8; 32] = [0xc0u8; 32];
3354    const CONTROL_CAUGHT_UP: [u8; 32] = [0x0cu8; 32];
3355
3356    /// The node key of the single peer these tests use.
3357    const PEER_NODE_KEY: [u8; 32] = [1u8; 32];
3358
3359    /// A tracker holding one peer with no disco key yet, plus that peer's [`PeerId`].
3360    fn tracker_with_peer() -> (PeerTracker, PeerId) {
3361        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
3362        let node = peer_node("peer", PEER_NODE_KEY, Vec::new());
3363        let id = tracker.peer_db.upsert(&node);
3364        (tracker, id)
3365    }
3366
3367    /// The peer as CONTROL describes it: the same node, carrying whatever disco key the netmap says
3368    /// it has (`None` for a peer control has no disco key for at all).
3369    fn node_from_control(disco_key: Option<[u8; 32]>) -> Node {
3370        let mut node = peer_node("peer", PEER_NODE_KEY, Vec::new());
3371        node.disco_key = disco_key.map(DiscoPublicKey::from);
3372        node
3373    }
3374
3375    /// A netmap `Full` carrying just this peer, as control currently describes it.
3376    fn control_full(disco_key: Option<[u8; 32]>) -> ts_control::PeerUpdate {
3377        ts_control::PeerUpdate::Full(vec![node_from_control(disco_key)])
3378    }
3379
3380    /// A tracker whose single peer arrived through the netmap carrying `disco_key`, exactly as the
3381    /// actor's handler applies it. Returns the peer's [`PeerId`] too.
3382    fn tracker_with_control_peer(disco_key: Option<[u8; 32]>) -> (PeerTracker, PeerId) {
3383        let (mut tracker, _tka_tx) = PeerTracker::for_test(test_env(), None);
3384        let node = node_from_control(disco_key);
3385        tracker.apply_peer_update(&control_full(disco_key));
3386        let id = tracker
3387            .peer_db
3388            .has(&node.node_key)
3389            .expect("control delivered it");
3390        (tracker, id)
3391    }
3392
3393    /// The disco key the peer db currently holds for `peer` — the effective key every direct-path
3394    /// consumer resolves against.
3395    fn effective_key(tracker: &PeerTracker, peer: PeerId) -> Option<DiscoPublicKey> {
3396        tracker
3397            .peer_db
3398            .get(&peer)
3399            .expect("peer still present")
3400            .1
3401            .disco_key
3402    }
3403
3404    /// The happy path: an advertised key is applied to the peer AND lands in the disco index, which
3405    /// is what the direct-path machinery (`direct::DiscoPeerLookup`) reads. Re-advertising the same
3406    /// key is a no-op; advertising a different one replaces it, retracting the old index entry.
3407    #[tokio::test]
3408    async fn advertisement_learns_the_peers_disco_key() {
3409        let (mut tracker, peer) = tracker_with_peer();
3410        let key = DiscoPublicKey::from(ADVERTISED);
3411
3412        assert!(
3413            tracker.learn_disco_key(peer, key),
3414            "a first advertisement changes the peer db"
3415        );
3416        assert_eq!(
3417            tracker
3418                .peer_db
3419                .get(&peer)
3420                .expect("peer still present")
3421                .1
3422                .disco_key,
3423            Some(key),
3424            "the advertised disco key is learned"
3425        );
3426        assert_eq!(
3427            tracker.peer_db.has(&key),
3428            Some(peer),
3429            "and is reachable through the disco index the direct path resolves against"
3430        );
3431
3432        assert!(
3433            !tracker.learn_disco_key(peer, key),
3434            "re-advertising the same key is a no-op (Go counts it 'unchanged' and returns)"
3435        );
3436
3437        let rotated = DiscoPublicKey::from(READVERTISED);
3438        assert!(tracker.learn_disco_key(peer, rotated));
3439        assert_eq!(
3440            tracker
3441                .peer_db
3442                .get(&peer)
3443                .expect("peer still present")
3444                .1
3445                .disco_key,
3446            Some(rotated),
3447            "a later advertisement replaces the key without a netmap update"
3448        );
3449        assert_eq!(tracker.peer_db.has(&rotated), Some(peer));
3450        assert_eq!(
3451            tracker.peer_db.has(&key),
3452            None,
3453            "the superseded key no longer resolves to the peer"
3454        );
3455    }
3456
3457    /// The refusals, each of which must leave the peer db untouched: the zero key is never learned,
3458    /// and an advertisement never creates a peer.
3459    #[tokio::test]
3460    async fn refused_advertisements_change_nothing() {
3461        let (mut tracker, peer) = tracker_with_peer();
3462
3463        assert!(
3464            !tracker.learn_disco_key(peer, DiscoPublicKey::from([0u8; 32])),
3465            "the zero key is never learned"
3466        );
3467        assert_eq!(
3468            tracker
3469                .peer_db
3470                .get(&peer)
3471                .expect("peer still present")
3472                .1
3473                .disco_key,
3474            None,
3475            "a zero-key advertisement must not bind the peer to an unusable key"
3476        );
3477
3478        // An advertisement for a peer control has never told us about. Go logs "endpoint not found
3479        // for node" and returns; it must not conjure a peer into existence.
3480        let unknown = PeerId(4242);
3481        assert_eq!(tracker.peer_db.get(&unknown), None, "precondition");
3482        assert!(
3483            !tracker.learn_disco_key(unknown, DiscoPublicKey::from(ADVERTISED)),
3484            "an advertisement for an unknown peer is ignored"
3485        );
3486        assert_eq!(
3487            tracker.peer_db.peers().len(),
3488            1,
3489            "an advertisement never creates a peer — only control does"
3490        );
3491        assert_eq!(
3492            tracker.peer_db.has(&DiscoPublicKey::from(ADVERTISED)),
3493            None,
3494            "and never indexes a key against a peer that does not exist"
3495        );
3496    }
3497
3498    /// The feature's motivating case, end to end: the peer told us a key control has not caught up
3499    /// with, and then control polls again with the SAME stale key it had before. The advertisement
3500    /// must survive.
3501    ///
3502    /// Go keeps the two keys apart on the endpoint (`endpointDisco.controlKey` /
3503    /// `tsmpKey`), and `updateFromNode` only rewrites the control side when control's key actually
3504    /// changed — so a netmap restating the old key never touches the active TSMP key. With a single
3505    /// field the next map poll silently reverted the peer to control's stale key, which is precisely
3506    /// the state the advertisement exists to escape.
3507    #[tokio::test]
3508    async fn netmap_restating_controls_stale_key_keeps_the_tsmp_key() {
3509        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3510        let advertised = DiscoPublicKey::from(ADVERTISED);
3511        assert_eq!(
3512            effective_key(&tracker, peer),
3513            Some(DiscoPublicKey::from(FROM_CONTROL)),
3514            "precondition: the peer starts on the key control gave us"
3515        );
3516
3517        assert!(tracker.learn_disco_key(peer, advertised));
3518        assert_eq!(effective_key(&tracker, peer), Some(advertised));
3519
3520        // Control polls again, still behind: a `Full` resync, then a `Delta` re-upsert, both
3521        // carrying the key control already sent.
3522        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
3523        assert_eq!(
3524            effective_key(&tracker, peer),
3525            Some(advertised),
3526            "a Full restating control's stale key must not undo the TSMP-learned key"
3527        );
3528        tracker.apply_peer_update(&ts_control::PeerUpdate::Delta {
3529            upsert: vec![node_from_control(Some(FROM_CONTROL))],
3530            remove: vec![],
3531        });
3532        assert_eq!(
3533            effective_key(&tracker, peer),
3534            Some(advertised),
3535            "and neither must a Delta re-upsert of the same node"
3536        );
3537        assert_eq!(
3538            tracker.peer_db.has(&advertised),
3539            Some(peer),
3540            "the direct path still resolves the peer by the key it advertised"
3541        );
3542        assert_eq!(
3543            tracker.peer_db.has(&DiscoPublicKey::from(FROM_CONTROL)),
3544            None,
3545            "and control's superseded key does not resolve to it"
3546        );
3547
3548        // Control finally changes its mind. The new key is recorded in control's slot, but the key
3549        // the peer itself told us stays active — upstream returns to control's key only when disco
3550        // is received under it (`endpoint.checkAndUpdateDiscoKey`).
3551        tracker.apply_peer_update(&control_full(Some(CONTROL_CAUGHT_UP)));
3552        assert_eq!(
3553            effective_key(&tracker, peer),
3554            Some(advertised),
3555            "a control-side key change must not preempt an active TSMP-learned key"
3556        );
3557        assert_eq!(
3558            tracker.control_disco_key(&PEER_NODE_KEY.into()),
3559            Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP)),
3560            "but control's new key IS recorded in control's slot"
3561        );
3562    }
3563
3564    /// An advertisement that merely restates the key control already gave us is still *new*
3565    /// information — it is the peer itself confirming the key — so Go records it as the TSMP key and
3566    /// makes it active. Its "unchanged" early return compares `epDisco.keyFromTSMP()`, the
3567    /// TSMP-learned key specifically, never the effective one.
3568    ///
3569    /// The observable consequence, asserted here: once the peer has confirmed the key, control
3570    /// dropping it (a netmap node with no disco key) leaves the confirmed key in place instead of
3571    /// blinding the direct path.
3572    #[tokio::test]
3573    async fn advertisement_restating_controls_key_is_recorded_as_the_tsmp_key() {
3574        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3575        let key = DiscoPublicKey::from(FROM_CONTROL);
3576
3577        assert!(
3578            tracker.learn_disco_key(peer, key),
3579            "an advertisement of the key control already sent is recorded, not dropped"
3580        );
3581        assert_eq!(
3582            tracker
3583                .endpoint_disco
3584                .get(&PEER_NODE_KEY.into())
3585                .and_then(EndpointDisco::key_from_tsmp),
3586            Some(key),
3587            "it lands in the TSMP slot (Go epDisco.tsmpKey), not only in control's"
3588        );
3589        assert!(
3590            !tracker.learn_disco_key(peer, key),
3591            "re-advertising it now IS unchanged, and is refused"
3592        );
3593
3594        // Control drops the peer's disco key. The key the peer itself confirmed stays active.
3595        tracker.apply_peer_update(&control_full(None));
3596        assert_eq!(
3597            effective_key(&tracker, peer),
3598            Some(key),
3599            "a control key going away hands the active slot to the TSMP-learned key"
3600        );
3601        assert_eq!(tracker.peer_db.has(&key), Some(peer));
3602    }
3603
3604    /// A `PeersChangedPatch` is a control write like any other: one that says nothing about the
3605    /// disco key must leave an active TSMP key alone, and one that carries a new key is control
3606    /// catching up, so it wins.
3607    ///
3608    /// The patch path is the subtle one — it starts from the db node, which carries the *effective*
3609    /// key, so without re-deriving what control last said it would hand the TSMP key back as if
3610    /// control had sent it.
3611    #[tokio::test]
3612    async fn patch_without_a_disco_key_leaves_the_tsmp_key_active() {
3613        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3614        let advertised = DiscoPublicKey::from(ADVERTISED);
3615        assert!(tracker.learn_disco_key(peer, advertised));
3616
3617        // A reachability-only patch (the idle-peer-reconnect case) for the same node.
3618        let endpoint: std::net::SocketAddr = "203.0.113.9:41641".parse().unwrap();
3619        let mut patch = ts_control::PeerChange {
3620            id: 1,
3621            derp_region: None,
3622            cap: None,
3623            cap_map: None,
3624            underlay_addresses: Some(vec![endpoint]),
3625            node_key: None,
3626            key_signature: None,
3627            disco_key: None,
3628            node_key_expiry: None,
3629            online: None,
3630            last_seen: None,
3631        };
3632        tracker.apply_peer_patches(std::slice::from_ref(&patch));
3633        assert_eq!(
3634            effective_key(&tracker, peer),
3635            Some(advertised),
3636            "a patch that never mentions the disco key must not revert it to control's"
3637        );
3638        assert_eq!(
3639            tracker
3640                .peer_db
3641                .get(&peer)
3642                .expect("peer still present")
3643                .1
3644                .underlay_addresses,
3645            vec![endpoint],
3646            "and the patch it DID carry still applied"
3647        );
3648
3649        // Now control changes the key through the patch channel. Same rule as the netmap path: the
3650        // key lands in control's slot, and the active TSMP key is left alone.
3651        patch.disco_key = Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP));
3652        tracker.apply_peer_patches(std::slice::from_ref(&patch));
3653        assert_eq!(
3654            effective_key(&tracker, peer),
3655            Some(advertised),
3656            "a patch carrying a new disco key does not preempt the active TSMP-learned key either"
3657        );
3658        assert_eq!(
3659            tracker.control_disco_key(&PEER_NODE_KEY.into()),
3660            Some(DiscoPublicKey::from(CONTROL_CAUGHT_UP)),
3661            "the patched key is still recorded as what control now says"
3662        );
3663    }
3664
3665    /// The rule this whole pair of slots exists to express: once the peer has told us its key over
3666    /// TSMP, control changing its mind is *recorded* but does not take the active slot back — Go
3667    /// `endpoint.updateDiscoKey`'s `epDisco.tsmpActive = old.tsmpActive || key.IsZero()`.
3668    ///
3669    /// Control is the slower source; a key the peer sent us itself is the better evidence. Upstream
3670    /// hands the slot back only when disco is actually *received* under control's key
3671    /// (`endpoint.checkAndUpdateDiscoKey`). Here the peer re-advertising is the path back, and it is
3672    /// asserted at the end so the sticky rule cannot be read as "the TSMP key is now permanent".
3673    #[tokio::test]
3674    async fn a_control_key_change_does_not_preempt_an_active_tsmp_key() {
3675        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3676        let advertised = DiscoPublicKey::from(ADVERTISED);
3677        let caught_up = DiscoPublicKey::from(CONTROL_CAUGHT_UP);
3678        assert!(tracker.learn_disco_key(peer, advertised));
3679
3680        tracker.apply_peer_update(&control_full(Some(CONTROL_CAUGHT_UP)));
3681        assert_eq!(
3682            effective_key(&tracker, peer),
3683            Some(advertised),
3684            "the TSMP-learned key stays active across a control-side change"
3685        );
3686        assert_eq!(
3687            tracker.peer_db.has(&advertised),
3688            Some(peer),
3689            "so the direct path still resolves the peer by the key it advertised"
3690        );
3691        assert_eq!(
3692            tracker.peer_db.has(&caught_up),
3693            None,
3694            "and control's new key is not what we send to"
3695        );
3696        assert_eq!(
3697            tracker.control_disco_key(&PEER_NODE_KEY.into()),
3698            Some(caught_up),
3699            "control's new key is recorded all the same — it is not discarded, just not active"
3700        );
3701
3702        // Control changing its mind a second time, and then dropping the key entirely, changes
3703        // nothing about which key is active.
3704        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
3705        tracker.apply_peer_update(&control_full(None));
3706        assert_eq!(
3707            effective_key(&tracker, peer),
3708            Some(advertised),
3709            "neither a second control change nor control dropping the key moves the active slot"
3710        );
3711
3712        // The peer itself is what moves it: it advertises the key control had been trying to give
3713        // us, and that advertisement is what we act on.
3714        assert!(tracker.learn_disco_key(peer, caught_up));
3715        assert_eq!(
3716            effective_key(&tracker, peer),
3717            Some(caught_up),
3718            "a peer re-advertising moves the active key, because the peer is the evidence"
3719        );
3720    }
3721
3722    /// The sticky flag must not strand a peer that never had a TSMP key: control sending nothing
3723    /// leaves no key material at all, and the key control sends next must become the active one.
3724    ///
3725    /// This is the case Go covers by nil-ing the endpoint's `disco` pointer when both keys are
3726    /// zero; here [`PeerTracker::upsert_from_control`] drops the entry, so the "no control key means
3727    /// the TSMP slot is active" flag cannot survive to shadow a later control key with nothing.
3728    #[tokio::test]
3729    async fn a_first_control_key_is_active_even_after_control_sent_none() {
3730        let (mut tracker, peer) = tracker_with_control_peer(None);
3731        assert_eq!(effective_key(&tracker, peer), None, "precondition");
3732        assert!(
3733            tracker.endpoint_disco.is_empty(),
3734            "a peer with no key material from either source costs no entry"
3735        );
3736
3737        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
3738        assert_eq!(
3739            effective_key(&tracker, peer),
3740            Some(DiscoPublicKey::from(FROM_CONTROL)),
3741            "control's first key is active — there is no TSMP key for it to defer to"
3742        );
3743        assert_eq!(
3744            tracker.peer_db.has(&DiscoPublicKey::from(FROM_CONTROL)),
3745            Some(peer)
3746        );
3747    }
3748
3749    /// The other half of `tsmpActive = old.tsmpActive || key.IsZero()`, in the one state where the
3750    /// left operand is false *and* a TSMP key exists: after disco was received under control's key,
3751    /// which is upstream's only route back to control holding the active slot
3752    /// (`endpoint.checkAndUpdateDiscoKey`).
3753    ///
3754    /// Two things follow, and neither is obvious from the sticky rule alone. Control's later changes
3755    /// **do** land, because what is sticky is the flag, not the TSMP key — so this is not "the TSMP
3756    /// key wins forever", and a peer that genuinely rotated is not stranded. And control *dropping*
3757    /// its key does not leave the peer with no disco key at all: the `key.IsZero()` operand hands the
3758    /// slot to the TSMP key still sitting in the other slot, which is why Go only nils the endpoint's
3759    /// `disco` pointer when **both** keys are zero.
3760    #[tokio::test]
3761    async fn control_regains_the_slot_by_being_received_under_and_then_keeps_it() {
3762        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3763        let from_control = DiscoPublicKey::from(FROM_CONTROL);
3764        let advertised = DiscoPublicKey::from(ADVERTISED);
3765        let caught_up = DiscoPublicKey::from(CONTROL_CAUGHT_UP);
3766
3767        // Get into the state: the peer advertises, then sends disco under control's key anyway, so
3768        // control's key is active again with the TSMP key demoted but retained.
3769        assert!(tracker.learn_disco_key(peer, advertised));
3770        assert!(tracker.observe_disco_key(peer, from_control));
3771        assert_eq!(
3772            effective_key(&tracker, peer),
3773            Some(from_control),
3774            "precondition: control holds the active slot because we received under its key"
3775        );
3776        assert_eq!(
3777            ingress_match(&tracker, advertised),
3778            Some((peer, peer_db::DiscoKeyMatch::Inactive)),
3779            "precondition: the TSMP key is demoted, not discarded"
3780        );
3781
3782        // Control changes its key. With the TSMP key demoted the sticky operand is false, so this
3783        // one does take the active slot — the flag is what is sticky, not the TSMP key.
3784        tracker.apply_peer_update(&control_full(Some(CONTROL_CAUGHT_UP)));
3785        assert_eq!(
3786            effective_key(&tracker, peer),
3787            Some(caught_up),
3788            "a demoted TSMP key does not block control's next key from becoming active"
3789        );
3790        assert_eq!(
3791            ingress_match(&tracker, advertised),
3792            Some((peer, peer_db::DiscoKeyMatch::Inactive)),
3793            "and the TSMP key is still the peer's other known key for ingress"
3794        );
3795        assert_eq!(
3796            ingress_match(&tracker, from_control),
3797            None,
3798            "control's superseded key is not a third slot"
3799        );
3800
3801        // Control drops its key entirely. `key.IsZero()` is the operand that carries the peer here:
3802        // the retained TSMP key becomes active rather than the peer losing disco altogether.
3803        tracker.apply_peer_update(&control_full(None));
3804        assert_eq!(
3805            effective_key(&tracker, peer),
3806            Some(advertised),
3807            "control dropping its key falls back to the TSMP key, not to no key"
3808        );
3809        assert_eq!(
3810            ingress_match(&tracker, advertised),
3811            Some((peer, peer_db::DiscoKeyMatch::Active))
3812        );
3813        assert_eq!(
3814            tracker.control_disco_key(&PEER_NODE_KEY.into()),
3815            None,
3816            "control's slot is cleared, so there is no second key to accept"
3817        );
3818        assert_eq!(
3819            ingress_match(&tracker, caught_up),
3820            None,
3821            "the key control withdrew stops resolving on ingress"
3822        );
3823    }
3824
3825    /// The TSMP-learned key lives exactly as long as Go's endpoint does: it is dropped when the peer
3826    /// leaves the netmap, and it is not carried across a node-key rotation (Go builds the rotated
3827    /// peer a brand-new endpoint, with a brand-new `endpointDisco`).
3828    #[tokio::test]
3829    async fn tsmp_key_does_not_outlive_the_peer_or_its_node_key() {
3830        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3831        assert!(tracker.learn_disco_key(peer, DiscoPublicKey::from(ADVERTISED)));
3832
3833        // The peer leaves the netmap, then comes back on control's key.
3834        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![]));
3835        assert!(tracker.peer_db.peers().is_empty());
3836        assert!(
3837            tracker.endpoint_disco.is_empty(),
3838            "the departed peer's disco state goes with it"
3839        );
3840        tracker.apply_peer_update(&control_full(Some(FROM_CONTROL)));
3841        let readded = node_from_control(Some(FROM_CONTROL));
3842        let peer = tracker.peer_db.has(&readded.node_key).expect("re-added");
3843        assert_eq!(
3844            effective_key(&tracker, peer),
3845            Some(DiscoPublicKey::from(FROM_CONTROL)),
3846            "a peer that left and rejoined starts from control's key again"
3847        );
3848
3849        // Learn a key again, then rotate the node key underneath it.
3850        assert!(tracker.learn_disco_key(peer, DiscoPublicKey::from(READVERTISED)));
3851        let mut rotated = node_from_control(Some(FROM_CONTROL));
3852        rotated.node_key = [2u8; 32].into();
3853        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![rotated.clone()]));
3854        let peer = tracker
3855            .peer_db
3856            .has(&rotated.node_key)
3857            .expect("rotated peer");
3858        assert_eq!(
3859            effective_key(&tracker, peer),
3860            Some(DiscoPublicKey::from(FROM_CONTROL)),
3861            "a key learned under the old node key is not carried onto the new one"
3862        );
3863        assert_eq!(
3864            tracker.endpoint_disco.len(),
3865            1,
3866            "and the old node key's state is pruned"
3867        );
3868    }
3869
3870    /// How the peer db resolves `key` for an inbound disco frame: the peer it belongs to and which
3871    /// of that peer's two slots it matched.
3872    fn ingress_match(
3873        tracker: &PeerTracker,
3874        key: DiscoPublicKey,
3875    ) -> Option<(PeerId, peer_db::DiscoKeyMatch)> {
3876        tracker
3877            .peer_db
3878            .peer_by_known_disco_key(&key)
3879            .map(|(id, _node, matched)| (id, matched))
3880    }
3881
3882    /// The bead's case, end to end: the peer advertised K2 over TSMP so we send to K2, but it is
3883    /// still sending disco under the K1 control gave us. That frame must resolve to the peer, and
3884    /// receiving under K1 must make K1 the key we send to — because it is demonstrably what the
3885    /// peer uses.
3886    ///
3887    /// Go: every inbound disco comparison goes through `endpoint.checkAndUpdateDiscoKey`, which
3888    /// accepts either slot and compare-and-swaps `tsmpActive` when the key seen is the inactive one.
3889    #[tokio::test]
3890    async fn disco_under_the_inactive_key_is_accepted_and_makes_that_key_active() {
3891        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3892        let from_control = DiscoPublicKey::from(FROM_CONTROL);
3893        let advertised = DiscoPublicKey::from(ADVERTISED);
3894
3895        assert!(tracker.learn_disco_key(peer, advertised));
3896        assert_eq!(
3897            effective_key(&tracker, peer),
3898            Some(advertised),
3899            "precondition: we are sending to the TSMP-learned key"
3900        );
3901        assert_eq!(
3902            ingress_match(&tracker, from_control),
3903            Some((peer, peer_db::DiscoKeyMatch::Inactive)),
3904            "control's key is still the peer's other known key, and still resolves on ingress"
3905        );
3906
3907        // Disco arrives under control's key: accepted, and it becomes the active one.
3908        assert!(
3909            tracker.observe_disco_key(peer, from_control),
3910            "receiving under the inactive key switches the active key"
3911        );
3912        assert_eq!(
3913            effective_key(&tracker, peer),
3914            Some(from_control),
3915            "we now send to the key the peer is demonstrably using"
3916        );
3917        assert_eq!(
3918            tracker.peer_db.has(&from_control),
3919            Some(peer),
3920            "and it is the key the send-side disco index carries"
3921        );
3922        assert_eq!(
3923            ingress_match(&tracker, from_control),
3924            Some((peer, peer_db::DiscoKeyMatch::Active))
3925        );
3926        assert_eq!(
3927            ingress_match(&tracker, advertised),
3928            Some((peer, peer_db::DiscoKeyMatch::Inactive)),
3929            "the TSMP key is retained in the other slot, so ingress under it still resolves"
3930        );
3931
3932        assert!(
3933            !tracker.observe_disco_key(peer, from_control),
3934            "a second frame under the now-active key changes nothing (and forces no republish)"
3935        );
3936
3937        // And it switches back: the peer resumes sending under the key it advertised.
3938        assert!(tracker.observe_disco_key(peer, advertised));
3939        assert_eq!(effective_key(&tracker, peer), Some(advertised));
3940        assert_eq!(
3941            ingress_match(&tracker, from_control),
3942            Some((peer, peer_db::DiscoKeyMatch::Inactive))
3943        );
3944    }
3945
3946    /// The refusal that is the whole security value of the check: a key belonging to NEITHER slot
3947    /// is rejected, leaving the peer on the key it was on. Plus the two other refusals Go has —
3948    /// an unknown peer, and a peer with no disco key material at all (`epDisco == nil`).
3949    #[tokio::test]
3950    async fn disco_under_a_key_in_neither_slot_is_refused() {
3951        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3952        let from_control = DiscoPublicKey::from(FROM_CONTROL);
3953        let advertised = DiscoPublicKey::from(ADVERTISED);
3954        let third = DiscoPublicKey::from(READVERTISED);
3955
3956        assert!(tracker.learn_disco_key(peer, advertised));
3957
3958        assert!(
3959            !tracker.observe_disco_key(peer, third),
3960            "a third key is refused: a peer must not move itself onto a key nobody told us about"
3961        );
3962        assert_eq!(
3963            effective_key(&tracker, peer),
3964            Some(advertised),
3965            "and the peer stays on the key it was on"
3966        );
3967        assert_eq!(
3968            ingress_match(&tracker, third),
3969            None,
3970            "the refused key never becomes resolvable"
3971        );
3972        assert_eq!(
3973            ingress_match(&tracker, from_control),
3974            Some((peer, peer_db::DiscoKeyMatch::Inactive)),
3975            "the two real slots are untouched"
3976        );
3977
3978        // An unknown peer: like a TSMP advertisement, this never creates one.
3979        assert!(!tracker.observe_disco_key(PeerId(4242), from_control));
3980        assert_eq!(tracker.peer_db.peers().len(), 1);
3981
3982        // A peer with no disco key from either source — Go returns false on `epDisco == nil`.
3983        let (mut bare, bare_peer) = tracker_with_control_peer(None);
3984        assert_eq!(effective_key(&bare, bare_peer), None, "precondition");
3985        assert!(
3986            !bare.observe_disco_key(bare_peer, from_control),
3987            "a peer with no known disco key has no slot for this key to match"
3988        );
3989        assert_eq!(effective_key(&bare, bare_peer), None);
3990    }
3991
3992    /// A peer that has only ever had one key registers no inactive key at all, so the second index
3993    /// stays empty and an inbound frame under any other key is refused.
3994    #[tokio::test]
3995    async fn a_single_key_peer_has_no_second_slot() {
3996        let (tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
3997        let from_control = DiscoPublicKey::from(FROM_CONTROL);
3998
3999        assert_eq!(
4000            ingress_match(&tracker, from_control),
4001            Some((peer, peer_db::DiscoKeyMatch::Active))
4002        );
4003        assert_eq!(
4004            ingress_match(&tracker, DiscoPublicKey::from(ADVERTISED)),
4005            None,
4006            "no second key was ever learned, so nothing else resolves to this peer"
4007        );
4008    }
4009
4010    /// An advertisement that merely restates control's key must not leave the peer with the same
4011    /// key in both slots pretending to be two — `inactive_key` reports `None` when the inactive
4012    /// slot holds the active key, so ingress sees exactly one key.
4013    #[tokio::test]
4014    async fn the_same_key_in_both_slots_is_one_key() {
4015        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
4016        let from_control = DiscoPublicKey::from(FROM_CONTROL);
4017
4018        assert!(tracker.learn_disco_key(peer, from_control));
4019        assert_eq!(
4020            tracker
4021                .endpoint_disco
4022                .get(&PEER_NODE_KEY.into())
4023                .and_then(EndpointDisco::inactive_key),
4024            None,
4025            "both slots hold the same key, so there is no second key"
4026        );
4027        assert_eq!(
4028            ingress_match(&tracker, from_control),
4029            Some((peer, peer_db::DiscoKeyMatch::Active))
4030        );
4031        assert!(
4032            !tracker.observe_disco_key(peer, from_control),
4033            "and receiving under it is a no-op, not a switch"
4034        );
4035    }
4036
4037    /// A peer that leaves the netmap takes BOTH its keys with it: the inactive-key index must not
4038    /// keep attributing frames to a peer that is gone.
4039    #[tokio::test]
4040    async fn a_departed_peer_stops_resolving_under_either_key() {
4041        let (mut tracker, peer) = tracker_with_control_peer(Some(FROM_CONTROL));
4042        let advertised = DiscoPublicKey::from(ADVERTISED);
4043        assert!(tracker.learn_disco_key(peer, advertised));
4044        assert!(ingress_match(&tracker, DiscoPublicKey::from(FROM_CONTROL)).is_some());
4045
4046        tracker.apply_peer_update(&ts_control::PeerUpdate::Full(vec![]));
4047        assert_eq!(ingress_match(&tracker, advertised), None);
4048        assert_eq!(
4049            ingress_match(&tracker, DiscoPublicKey::from(FROM_CONTROL)),
4050            None,
4051            "the inactive key is retracted with the peer, not left dangling"
4052        );
4053    }
4054}