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