diff --git a/src/cache/coord_cache.rs b/src/cache/coord_cache.rs index e832915e..d2acd8ae 100644 --- a/src/cache/coord_cache.rs +++ b/src/cache/coord_cache.rs @@ -17,6 +17,32 @@ pub const DEFAULT_COORD_CACHE_SIZE: usize = 50_000; /// Default TTL for coordinate cache entries (5 minutes in milliseconds). pub const DEFAULT_COORD_CACHE_TTL_MS: u64 = 300_000; +/// What a hint write did, which is the only place the precedence rule is +/// observable. +/// +/// `#[must_use]` on purpose. A hint write can be refused, and a caller that +/// drops the outcome cannot tell a stored coordinate from a rejected one. It +/// also makes the compiler, rather than review, the thing that notices when a +/// write site is left on the hint path that should have been verified. +#[must_use] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HintOutcome { + /// No entry existed; the hint was stored. + Inserted, + /// An entry existed and the hint replaced it with a different value. + /// + /// This is the security-interesting outcome. A destination's coordinates + /// changing is ordinary when it moves in the tree and is also exactly what + /// a poisoning looks like, so the two are not distinguishable here and the + /// counter is a rate to watch rather than an alarm. + Changed, + /// An entry existed and the hint carried the same value. + Unchanged, + /// An entry existed, was verified and still within its verification + /// window, so the hint was refused. + Rejected, +} + /// Coordinate cache for routing decisions. /// /// Maps node addresses to their tree coordinates, enabling data packets @@ -62,12 +88,41 @@ impl CoordCache { self.default_ttl_ms = ttl_ms; } - /// Insert or update a cache entry. - pub fn insert(&mut self, addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) { - // Update existing entry if present + /// Insert or update a cache entry from an unauthenticated hint. + /// + /// **This is the only way to write a coordinate learned off the wire, and + /// it is deliberately the obvious name.** A hint never displaces an entry + /// that a verified lookup established and whose verification has not yet + /// aged out; see [`CacheEntry::is_verified`]. Conferring trust requires + /// asking for it by name, with [`CoordCache::insert_verified`]. + pub fn insert( + &mut self, + addr: NodeAddr, + coords: TreeCoordinate, + current_time_ms: u64, + ) -> HintOutcome { + self.insert_hint_with_ttl(addr, coords, current_time_ms, self.default_ttl_ms) + } + + /// Insert or update a cache entry from a hint, with an explicit TTL. + fn insert_hint_with_ttl( + &mut self, + addr: NodeAddr, + coords: TreeCoordinate, + current_time_ms: u64, + ttl_ms: u64, + ) -> HintOutcome { if let Some(entry) = self.entries.get_mut(&addr) { - entry.update(coords, current_time_ms, self.default_ttl_ms); - return; + if entry.is_verified(current_time_ms) { + return HintOutcome::Rejected; + } + let changed = entry.coords() != &coords; + entry.update(coords, current_time_ms, ttl_ms); + return if changed { + HintOutcome::Changed + } else { + HintOutcome::Unchanged + }; } // Evict if at capacity @@ -75,15 +130,47 @@ impl CoordCache { self.evict_one(current_time_ms); } - let entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms); + // Eviction can decline to free a slot when every entry is a live + // verified one, which is the case the hint must not be allowed to + // force. Refuse rather than grow past the cap. + if self.entries.len() >= self.max_entries { + return HintOutcome::Rejected; + } + + let entry = CacheEntry::new(coords, current_time_ms, ttl_ms); + self.entries.insert(addr, entry); + HintOutcome::Inserted + } + + /// Insert or update a cache entry from a lookup whose proof was verified. + /// + /// Unconditional: a verified value displaces whatever was there, which is + /// the point — it is how a poisoned entry gets corrected. + pub fn insert_verified( + &mut self, + addr: NodeAddr, + coords: TreeCoordinate, + current_time_ms: u64, + ) { + if let Some(entry) = self.entries.get_mut(&addr) { + entry.update_verified(coords, current_time_ms, self.default_ttl_ms); + return; + } + + if self.entries.len() >= self.max_entries { + self.evict_one(current_time_ms); + } + + let entry = CacheEntry::new_verified(coords, current_time_ms, self.default_ttl_ms); self.entries.insert(addr, entry); } - /// Insert or update a cache entry with path MTU information. + /// Insert or update a verified cache entry with path MTU information. /// /// Used by discovery response handling to store the discovered path MTU - /// alongside the target's coordinates. - pub fn insert_with_path_mtu( + /// alongside the target's coordinates. Verified for the same reason + /// [`CoordCache::insert_verified`] is: the caller checked the proof. + pub fn insert_verified_with_path_mtu( &mut self, addr: NodeAddr, coords: TreeCoordinate, @@ -91,7 +178,7 @@ impl CoordCache { path_mtu: u16, ) { if let Some(entry) = self.entries.get_mut(&addr) { - entry.update(coords, current_time_ms, self.default_ttl_ms); + entry.update_verified(coords, current_time_ms, self.default_ttl_ms); entry.set_path_mtu(path_mtu); return; } @@ -100,7 +187,7 @@ impl CoordCache { self.evict_one(current_time_ms); } - let mut entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms); + let mut entry = CacheEntry::new_verified(coords, current_time_ms, self.default_ttl_ms); entry.set_path_mtu(path_mtu); self.entries.insert(addr, entry); } @@ -112,18 +199,8 @@ impl CoordCache { coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64, - ) { - if let Some(entry) = self.entries.get_mut(&addr) { - entry.update(coords, current_time_ms, ttl_ms); - return; - } - - if self.entries.len() >= self.max_entries { - self.evict_one(current_time_ms); - } - - let entry = CacheEntry::new(coords, current_time_ms, ttl_ms); - self.entries.insert(addr, entry); + ) -> HintOutcome { + self.insert_hint_with_ttl(addr, coords, current_time_ms, ttl_ms) } /// Look up coordinates for an address (without touching). @@ -253,10 +330,19 @@ impl CoordCache { return; } - // Otherwise evict LRU (oldest last_used) + // Otherwise evict the LRU among entries that are not live-verified. + // + // Restricting the victim pool is what stops a hint flood from + // manufacturing the empty slot the precedence rule depends on: without + // it, an attacker fills the cache with hints until a verified entry + // becomes the LRU, evicts it, and then plants into a slot that is now + // empty and so accepts an ordinary first write. Declining to evict is + // the correct outcome when every entry is live-verified; the caller + // refuses the hint rather than growing past the cap. let lru_key = self .entries .iter() + .filter(|(_, e)| !e.is_verified(current_time_ms)) .max_by_key(|(_, e)| e.idle_time(current_time_ms)) .map(|(k, _)| *k); @@ -299,6 +385,7 @@ impl Default for CoordCache { #[cfg(test)] mod tests { use super::*; + use crate::cache::entry::VERIFIED_TTL_MS; fn make_node_addr(val: u8) -> NodeAddr { let mut bytes = [0u8; 16]; @@ -316,7 +403,7 @@ mod tests { let addr = make_node_addr(1); let coords = make_coords(&[1, 0]); - cache.insert(addr, coords.clone(), 0); + let _ = cache.insert(addr, coords.clone(), 0); assert!(cache.contains(&addr, 0)); assert_eq!(cache.get(&addr, 0), Some(&coords)); @@ -329,7 +416,7 @@ mod tests { let addr = make_node_addr(1); let coords = make_coords(&[1, 0]); - cache.insert(addr, coords, 0); + let _ = cache.insert(addr, coords, 0); assert!(cache.contains(&addr, 500)); assert!(!cache.contains(&addr, 1500)); @@ -340,8 +427,8 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let addr = make_node_addr(1); - cache.insert(addr, make_coords(&[1, 0]), 0); - cache.insert(addr, make_coords(&[1, 2, 0]), 500); + let _ = cache.insert(addr, make_coords(&[1, 0]), 0); + let _ = cache.insert(addr, make_coords(&[1, 2, 0]), 500); assert_eq!(cache.len(), 1); let coords = cache.get(&addr, 500).unwrap(); @@ -356,14 +443,14 @@ mod tests { let addr2 = make_node_addr(2); let addr3 = make_node_addr(3); - cache.insert(addr1, make_coords(&[1, 0]), 0); - cache.insert(addr2, make_coords(&[2, 0]), 100); + let _ = cache.insert(addr1, make_coords(&[1, 0]), 0); + let _ = cache.insert(addr2, make_coords(&[2, 0]), 100); // Touch addr2 to make it more recent let _ = cache.get_and_touch(&addr2, 200); // Insert addr3, should evict addr1 (LRU) - cache.insert(addr3, make_coords(&[3, 0]), 300); + let _ = cache.insert(addr3, make_coords(&[3, 0]), 300); assert!(!cache.contains(&addr1, 300)); assert!(cache.contains(&addr2, 300)); @@ -374,11 +461,11 @@ mod tests { fn test_coord_cache_evict_expired_first() { let mut cache = CoordCache::new(2, 100); - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // At time 150, addr1 is expired, addr2 is not - cache.insert(make_node_addr(3), make_coords(&[3, 0]), 150); + let _ = cache.insert(make_node_addr(3), make_coords(&[3, 0]), 150); // addr1 should be evicted (expired), not addr2 (LRU but not expired) assert!(!cache.contains(&make_node_addr(1), 150)); @@ -390,9 +477,9 @@ mod tests { fn test_coord_cache_purge_expired() { let mut cache = CoordCache::new(100, 100); - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); // expires at 100 - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // expires at 150 - cache.insert(make_node_addr(3), make_coords(&[3, 0]), 200); // expires at 300 + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); // expires at 100 + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // expires at 150 + let _ = cache.insert(make_node_addr(3), make_coords(&[3, 0]), 200); // expires at 300 assert_eq!(cache.len(), 3); @@ -408,8 +495,8 @@ mod tests { fn test_coord_cache_stats() { let mut cache = CoordCache::new(100, 100); - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); let stats = cache.stats(150); @@ -424,7 +511,7 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let addr = make_node_addr(1); - cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200); + let _ = cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200); // Should expire at 200, not the default 1000 assert!(cache.contains(&addr, 100)); @@ -436,8 +523,8 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let addr = make_node_addr(1); - cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200); - cache.insert_with_ttl(addr, make_coords(&[1, 2, 0]), 100, 300); + let _ = cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200); + let _ = cache.insert_with_ttl(addr, make_coords(&[1, 2, 0]), 100, 300); assert_eq!(cache.len(), 1); let coords = cache.get(&addr, 100).unwrap(); @@ -452,7 +539,7 @@ mod tests { let mut cache = CoordCache::new(100, 100); let addr = make_node_addr(1); - cache.insert(addr, make_coords(&[1, 0]), 0); + let _ = cache.insert(addr, make_coords(&[1, 0]), 0); assert_eq!(cache.len(), 1); // Entry expired at time 200 @@ -467,7 +554,7 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let addr = make_node_addr(1); - cache.insert(addr, make_coords(&[1, 0]), 500); + let _ = cache.insert(addr, make_coords(&[1, 0]), 500); let entry = cache.get_entry(&addr).unwrap(); assert_eq!(entry.created_at(), 500); @@ -481,7 +568,7 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let addr = make_node_addr(1); - cache.insert(addr, make_coords(&[1, 0]), 0); + let _ = cache.insert(addr, make_coords(&[1, 0]), 0); assert_eq!(cache.len(), 1); let removed = cache.remove(&addr); @@ -498,8 +585,8 @@ mod tests { assert!(cache.is_empty()); - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); assert!(!cache.is_empty()); @@ -525,7 +612,7 @@ mod tests { cache.set_default_ttl_ms(200); assert_eq!(cache.default_ttl_ms(), 200); - cache.insert(addr, make_coords(&[1, 0]), 0); + let _ = cache.insert(addr, make_coords(&[1, 0]), 0); // New TTL applies: expires at 200 assert!(cache.contains(&addr, 100)); assert!(!cache.contains(&addr, 201)); @@ -550,7 +637,7 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let target = make_node_addr(1); - cache.insert(target, make_coords(&[1, 0]), 0); + let _ = cache.insert(target, make_coords(&[1, 0]), 0); assert_eq!(cache.len(), 1); let removed = cache.invalidate_via_node(&target); @@ -564,7 +651,7 @@ mod tests { let mut cache = CoordCache::new(100, 1000); let dest = make_node_addr(5); // Path: 5 -> 3 -> 1 -> 0 (root). Target 3 appears at depth 1. - cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); + let _ = cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); let removed = cache.invalidate_via_node(&make_node_addr(3)); assert_eq!(removed, 1); @@ -576,7 +663,7 @@ mod tests { // Entry whose ancestry does NOT contain the target must be retained. let mut cache = CoordCache::new(100, 1000); let dest = make_node_addr(5); - cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); + let _ = cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); let removed = cache.invalidate_via_node(&make_node_addr(99)); assert_eq!(removed, 0); @@ -596,8 +683,8 @@ mod tests { fn test_invalidate_other_roots_current_root_kept() { let mut cache = CoordCache::new(100, 1000); // Entries rooted at addr(0) - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); let removed = cache.invalidate_other_roots(&make_node_addr(0)); assert_eq!(removed, 0); @@ -608,10 +695,10 @@ mod tests { fn test_invalidate_other_roots_different_root_dropped() { let mut cache = CoordCache::new(100, 1000); // Three entries rooted at addr(0), one rooted at addr(9) - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); - cache.insert(make_node_addr(3), make_coords(&[3, 0]), 0); - cache.insert(make_node_addr(4), make_coords(&[4, 9]), 0); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + let _ = cache.insert(make_node_addr(3), make_coords(&[3, 0]), 0); + let _ = cache.insert(make_node_addr(4), make_coords(&[4, 9]), 0); let removed = cache.invalidate_other_roots(&make_node_addr(0)); assert_eq!(removed, 1); @@ -623,8 +710,8 @@ mod tests { #[test] fn test_invalidate_other_roots_all_match() { let mut cache = CoordCache::new(100, 1000); - cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); - cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + let _ = cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + let _ = cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); let removed = cache.invalidate_other_roots(&make_node_addr(0)); assert_eq!(removed, 0); @@ -638,4 +725,123 @@ mod tests { assert_eq!(removed, 0); assert_eq!(cache.len(), 0); } + + #[test] + fn a_hint_does_not_displace_a_live_verified_entry() { + let mut cache = CoordCache::new(100, 1000); + let addr = make_node_addr(1); + let good = make_coords(&[1, 0]); + let forged = make_coords(&[1, 2, 0]); + + cache.insert_verified(addr, good.clone(), 0); + assert_eq!(cache.insert(addr, forged, 10), HintOutcome::Rejected); + assert_eq!( + cache.get(&addr, 10), + Some(&good), + "the verified value must survive the hint" + ); + } + + #[test] + fn a_verified_write_displaces_a_hint() { + let mut cache = CoordCache::new(100, 1000); + let addr = make_node_addr(1); + let hint = make_coords(&[1, 2, 0]); + let good = make_coords(&[1, 0]); + + assert_eq!(cache.insert(addr, hint, 0), HintOutcome::Inserted); + cache.insert_verified(addr, good.clone(), 10); + assert_eq!( + cache.get(&addr, 10), + Some(&good), + "a proof must be able to correct a poisoned entry" + ); + } + + #[test] + fn verification_ages_out_so_a_stale_verified_entry_stops_refusing_hints() { + let mut cache = CoordCache::new(100, u64::MAX / 4); + let addr = make_node_addr(1); + cache.insert_verified(addr, make_coords(&[1, 0]), 0); + + // Inside the window: refused. + assert_eq!( + cache.insert(addr, make_coords(&[1, 2, 0]), VERIFIED_TTL_MS), + HintOutcome::Rejected + ); + // One millisecond past it: accepted, so a destination that genuinely + // moved is not locked out forever by a verification nobody renews. + let moved = make_coords(&[1, 3, 0]); + assert_eq!( + cache.insert(addr, moved.clone(), VERIFIED_TTL_MS + 1), + HintOutcome::Changed + ); + assert_eq!(cache.get(&addr, VERIFIED_TTL_MS + 1), Some(&moved)); + } + + #[test] + fn ordinary_traffic_does_not_extend_the_verification_window() { + // The entry TTL has to be long enough that the touches below keep the + // entry alive; the test is about the verification clock, not expiry. + let mut cache = CoordCache::new(100, VERIFIED_TTL_MS); + let addr = make_node_addr(1); + cache.insert_verified(addr, make_coords(&[1, 0]), 0); + + // Touch it repeatedly the way forwarding does, right up to the edge. + for t in [100, 1000, 100_000, VERIFIED_TTL_MS] { + let _ = cache.get_and_touch(&addr, t); + } + + // The entry is alive but its verification has aged out on its own + // clock, which is the whole point of keeping the two clocks separate. + assert_eq!( + cache.insert(addr, make_coords(&[1, 2, 0]), VERIFIED_TTL_MS + 1), + HintOutcome::Changed, + "refresh must not carry the verification forward" + ); + } + + #[test] + fn eviction_prefers_an_unverified_victim_over_a_verified_one() { + let mut cache = CoordCache::new(2, 1_000_000); + let verified = make_node_addr(1); + let hint = make_node_addr(2); + let newcomer = make_node_addr(3); + + // The verified entry is the least recently used, so an unrestricted + // LRU would take it. That is exactly the eviction an attacker would + // drive to manufacture an empty slot. + cache.insert_verified(verified, make_coords(&[1, 0]), 0); + assert_eq!( + cache.insert(hint, make_coords(&[2, 0]), 100), + HintOutcome::Inserted + ); + assert_eq!( + cache.insert(newcomer, make_coords(&[3, 0]), 200), + HintOutcome::Inserted + ); + + assert!( + cache.contains(&verified, 200), + "the verified entry must not be the eviction victim" + ); + assert!( + !cache.contains(&hint, 200), + "the unverified entry should have been evicted instead" + ); + } + + #[test] + fn a_cache_full_of_verified_entries_refuses_a_hint_rather_than_evicting_one() { + let mut cache = CoordCache::new(2, 1_000_000); + cache.insert_verified(make_node_addr(1), make_coords(&[1, 0]), 0); + cache.insert_verified(make_node_addr(2), make_coords(&[2, 0]), 0); + + assert_eq!( + cache.insert(make_node_addr(3), make_coords(&[3, 0]), 10), + HintOutcome::Rejected + ); + assert!(cache.contains(&make_node_addr(1), 10)); + assert!(cache.contains(&make_node_addr(2), 10)); + } } diff --git a/src/cache/entry.rs b/src/cache/entry.rs index 69c30ff8..8fe95ae0 100644 --- a/src/cache/entry.rs +++ b/src/cache/entry.rs @@ -2,6 +2,30 @@ use crate::proto::stp::TreeCoordinate; +/// How long a verification outranks a hint, in milliseconds. +/// +/// Deliberately independent of the entry's own TTL. An entry carrying live +/// traffic is refreshed on every use and so never expires, and if verification +/// rode that same clock a once-verified entry would outrank every hint forever +/// — including the hints that would carry a destination's genuine move. This +/// clock is never refreshed: verification ages out on its own, and the entry +/// stays usable afterwards, it just stops winning. +pub const VERIFIED_TTL_MS: u64 = 300_000; + +/// Where a cached coordinate came from, which is what decides whether it may +/// be overwritten. +/// +/// The distinction is the whole of the defence: `Verified` values arrive with +/// a proof this node checked, `Hint` values are copied off a passing packet +/// and are attacker-supplied in the general case. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CoordSource { + /// Established by a lookup whose response proof this node verified. + Verified, + /// Copied from a packet in transit. Unauthenticated. + Hint, +} + /// A cached coordinate entry. #[derive(Clone, Debug)] pub struct CacheEntry { @@ -19,10 +43,21 @@ pub struct CacheEntry { /// response is cached. `None` when populated from SessionSetup or /// other sources that don't carry path MTU information. path_mtu: Option, + /// Where the current coordinates came from. + source: CoordSource, + /// Until when a `Verified` source outranks a hint (Unix milliseconds). + /// + /// Zero for a hint. Never extended by `refresh` or `touch`; see + /// [`VERIFIED_TTL_MS`]. + verified_until: u64, } impl CacheEntry { - /// Create a new cache entry. + /// Create a new cache entry, carrying a hint. + /// + /// Hint is the safe default: a caller that means to confer trust has to say + /// so with [`CacheEntry::new_verified`], rather than trust being what you + /// get by reaching for the obvious constructor. pub fn new(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self { Self { coords, @@ -30,9 +65,44 @@ impl CacheEntry { last_used: current_time_ms, expires_at: current_time_ms.saturating_add(ttl_ms), path_mtu: None, + source: CoordSource::Hint, + verified_until: 0, } } + /// Create a new cache entry from a verified lookup. + pub fn new_verified(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self { + let mut entry = Self::new(coords, current_time_ms, ttl_ms); + entry.mark_verified(current_time_ms); + entry + } + + /// Where the current coordinates came from. + pub fn source(&self) -> CoordSource { + self.source + } + + /// Whether this entry's verification still outranks a hint at this time. + /// + /// A `Verified` entry whose `verified_until` has passed answers `false`: + /// the coordinates remain usable, they just no longer refuse an update. + pub fn is_verified(&self, current_time_ms: u64) -> bool { + self.source == CoordSource::Verified && current_time_ms <= self.verified_until + } + + /// Mark the current coordinates as verified, starting the verification + /// clock at `current_time_ms`. + pub fn mark_verified(&mut self, current_time_ms: u64) { + self.source = CoordSource::Verified; + self.verified_until = current_time_ms.saturating_add(VERIFIED_TTL_MS); + } + + /// Mark the current coordinates as an unauthenticated hint. + pub fn mark_hint(&mut self) { + self.source = CoordSource::Hint; + self.verified_until = 0; + } + /// Get the cached coordinates. pub fn coords(&self) -> &TreeCoordinate { &self.coords @@ -79,11 +149,23 @@ impl CacheEntry { self.last_used = current_time_ms; } - /// Update the coordinates and refresh timestamps. + /// Update the coordinates and refresh timestamps, as a hint. + /// + /// New coordinates are new provenance: whatever the entry held before, the + /// value now present came from this caller, so an update by the hint path + /// demotes the entry rather than inheriting the old verification. pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) { self.coords = coords; self.last_used = current_time_ms; self.expires_at = current_time_ms.saturating_add(ttl_ms); + self.mark_hint(); + } + + /// Update the coordinates from a verified lookup and restart the + /// verification clock. + pub fn update_verified(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) { + self.update(coords, current_time_ms, ttl_ms); + self.mark_verified(current_time_ms); } /// Time since last use (for LRU eviction). diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4144126c..bc8763b0 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -8,8 +8,10 @@ mod entry; use thiserror::Error; -pub use coord_cache::{CoordCache, DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS}; -pub use entry::CacheEntry; +pub use coord_cache::{ + CoordCache, DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS, HintOutcome, +}; +pub use entry::{CacheEntry, CoordSource, VERIFIED_TTL_MS}; /// Errors related to cache operations. #[derive(Debug, Error)] diff --git a/src/control/snapshots/show_routing.json b/src/control/snapshots/show_routing.json index 1a0b3e5b..99e8ddd2 100644 --- a/src/control/snapshots/show_routing.json +++ b/src/control/snapshots/show_routing.json @@ -51,6 +51,8 @@ "unbound_mtu": 0 }, "forwarding": { + "coord_hint_changed": 0, + "coord_hint_rejected": 0, "coord_warm_foreign_root": 0, "coord_warm_key_mismatch": 0, "decode_error_bytes": 0, diff --git a/src/control/snapshots/show_status.json b/src/control/snapshots/show_status.json index 3f8b1550..8bed4e5a 100644 --- a/src/control/snapshots/show_status.json +++ b/src/control/snapshots/show_status.json @@ -6,6 +6,8 @@ "estimated_mesh_size": null, "exe_path": "", "forwarding": { + "coord_hint_changed": 0, + "coord_hint_rejected": 0, "coord_warm_foreign_root": 0, "coord_warm_key_mismatch": 0, "decode_error_bytes": 0, diff --git a/src/node/dataplane/forwarding.rs b/src/node/dataplane/forwarding.rs index d7436b2b..76164e66 100644 --- a/src/node/dataplane/forwarding.rs +++ b/src/node/dataplane/forwarding.rs @@ -263,7 +263,7 @@ impl Node { if *coords.node_addr() != key { self.metrics().forwarding.record_warm_key_mismatch(); } - self.coord_cache_mut().insert(key, coords, now_ms); + self.insert_coord_hint(key, coords, now_ms); } fn try_warm_coord_cache_ref(&mut self, datagram: &SessionDatagramRef<'_>, outer_len: usize) { diff --git a/src/node/handlers/lookup.rs b/src/node/handlers/lookup.rs index 665e64ad..76ec77d1 100644 --- a/src/node/handlers/lookup.rs +++ b/src/node/handlers/lookup.rs @@ -374,10 +374,10 @@ impl Node { caching coordinates without it" ); self.metrics().errors.lookup_resp_mtu_below_floor.inc(); - self.coord_cache.insert(target, coords, now_ms); + self.coord_cache.insert_verified(target, coords, now_ms); } else { self.coord_cache - .insert_with_path_mtu(target, coords, now_ms, path_mtu); + .insert_verified_with_path_mtu(target, coords, now_ms, path_mtu); } } LookupAction::WritePathMtu { diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index 4d03749e..61180743 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -256,7 +256,7 @@ impl Node { .plan_cache_coords(*src_addr, my_addr, src_coords, dest_coords) { if let FspAction::CacheCoords { addr, coords } = action { - self.coord_cache.insert(addr, coords, now_ms); + self.insert_coord_hint(addr, coords, now_ms); } } ciphertext_offset += bytes_consumed; @@ -1055,7 +1055,7 @@ impl Node { entry.clear_handshake_payload(); entry.touch(now_ms); self.sessions.insert(*src_addr, entry); - self.coord_cache.insert(*src_addr, ack.src_coords, now_ms); + self.insert_coord_hint(*src_addr, ack.src_coords.clone(), now_ms); // Flush any queued outbound packets for this destination self.flush_pending_packets(src_addr).await; diff --git a/src/node/metrics.rs b/src/node/metrics.rs index 45f2f5b8..dc40d1ba 100644 --- a/src/node/metrics.rs +++ b/src/node/metrics.rs @@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; +use crate::cache::HintOutcome; use crate::node::reject::{BloomReject, DiscoveryReject, ForwardingReject, TreeReject}; use crate::node::stats::{ BloomStatsSnapshot, CongestionStatsSnapshot, ErrorSignalStatsSnapshot, ForwardingStatsSnapshot, @@ -81,6 +82,15 @@ pub struct ForwardingMetrics { /// as its own child. It counts a defect honest nodes make, where a sender /// whose own cache missed sends its own coordinates as the destination's. pub coord_warm_key_mismatch: Counter, + /// Hint writes that replaced an existing entry with a different value. + /// A destination moving in the tree produces this, and so does a + /// poisoning; the two are not distinguishable here, so this is a rate to + /// watch rather than an alarm. + pub coord_hint_changed: Counter, + /// Hint writes refused because the entry they targeted was verified and + /// still inside its verification window, or because the cache was full of + /// live-verified entries and declined to evict one. + pub coord_hint_rejected: Counter, pub ttl_exhausted_packets: Counter, pub ttl_exhausted_bytes: Counter, pub delivered_packets: Counter, @@ -156,6 +166,15 @@ impl ForwardingMetrics { self.coord_warm_key_mismatch.inc(); } + /// Record the outcome of a hint write against the coordinate cache. + pub fn record_hint_outcome(&self, outcome: HintOutcome) { + match outcome { + HintOutcome::Changed => self.coord_hint_changed.inc(), + HintOutcome::Rejected => self.coord_hint_rejected.inc(), + HintOutcome::Inserted | HintOutcome::Unchanged => {} + } + } + /// Record a forwarded (transit) packet of `bytes` payload. #[inline] pub fn record_forwarded(&self, bytes: usize) { @@ -226,6 +245,8 @@ impl ForwardingMetrics { warm_malformed_bytes: self.warm_malformed_bytes.get(), coord_warm_foreign_root: self.coord_warm_foreign_root.get(), coord_warm_key_mismatch: self.coord_warm_key_mismatch.get(), + coord_hint_changed: self.coord_hint_changed.get(), + coord_hint_rejected: self.coord_hint_rejected.get(), ttl_exhausted_packets: self.ttl_exhausted_packets.get(), ttl_exhausted_bytes: self.ttl_exhausted_bytes.get(), delivered_packets: self.delivered_packets.get(), diff --git a/src/node/mod.rs b/src/node/mod.rs index d1ecbd8d..3f5639a5 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -3107,6 +3107,23 @@ impl Node { /// cannot make loop-free forwarding decisions. The caller should signal /// `CoordsRequired` back to the source when `None` is returned for a /// non-local destination. + /// Write one unauthenticated coordinate hint, counting the outcome. + /// + /// Every production hint write goes through here, so the precedence rule + /// has exactly one enforcement point and the counters have exactly one + /// increment site. The verified path is deliberately not routed through + /// this: a caller that has checked a proof calls + /// `CoordCache::insert_verified` directly and says so. + pub(crate) fn insert_coord_hint( + &mut self, + addr: NodeAddr, + coords: TreeCoordinate, + now_ms: u64, + ) { + let outcome = self.coord_cache.insert(addr, coords, now_ms); + self.metrics().forwarding.record_hint_outcome(outcome); + } + pub fn find_next_hop(&mut self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> { // 1. Local delivery if dest_node_addr == self.node_addr() { diff --git a/src/node/stats.rs b/src/node/stats.rs index 23ab304f..8367d857 100644 --- a/src/node/stats.rs +++ b/src/node/stats.rs @@ -305,6 +305,8 @@ pub struct ForwardingStatsSnapshot { pub warm_malformed_bytes: u64, pub coord_warm_foreign_root: u64, pub coord_warm_key_mismatch: u64, + pub coord_hint_changed: u64, + pub coord_hint_rejected: u64, pub ttl_exhausted_packets: u64, pub ttl_exhausted_bytes: u64, pub delivered_packets: u64, diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index dda11ee1..d55f4f45 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -1315,7 +1315,7 @@ async fn test_response_path_mtu_three_node_chain() { #[tokio::test] async fn test_cache_entry_path_mtu_stored() { - // Verify that insert_with_path_mtu stores the path_mtu in the cache entry + // Verify that insert_verified_with_path_mtu stores the path_mtu in the cache entry let mut node = make_node(); let target = make_node_addr(0xBB); @@ -1323,7 +1323,7 @@ async fn test_cache_entry_path_mtu_stored() { let now_ms = 1000u64; node.coord_cache_mut() - .insert_with_path_mtu(target, coords, now_ms, 1280); + .insert_verified_with_path_mtu(target, coords, now_ms, 1280); let entry = node.coord_cache().get_entry(&target).unwrap(); assert_eq!(entry.path_mtu(), Some(1280)); @@ -1338,7 +1338,7 @@ async fn test_cache_entry_no_path_mtu_from_regular_insert() { let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap(); let now_ms = 1000u64; - node.coord_cache_mut().insert(target, coords, now_ms); + let _ = node.coord_cache_mut().insert(target, coords, now_ms); let entry = node.coord_cache().get_entry(&target).unwrap(); assert_eq!(entry.path_mtu(), None); diff --git a/src/node/tests/forwarding.rs b/src/node/tests/forwarding.rs index eaccba35..c092e2d8 100644 --- a/src/node/tests/forwarding.rs +++ b/src/node/tests/forwarding.rs @@ -204,6 +204,45 @@ async fn test_forwarding_direct_peer() { // Coordinate Cache Warming Tests // ============================================================================ +#[tokio::test] +async fn a_forged_warm_cannot_displace_a_coordinate_established_by_a_verified_lookup() { + let mut node = make_node(); + let attacker_link = make_node_addr(0xAA); + let victim_dest = make_node_addr(0x02); + let root_addr = *node.tree_state.my_coords().root_id(); + + // The state a completed lookup leaves behind. + let real_coords = TreeCoordinate::from_addrs(vec![victim_dest, root_addr]).unwrap(); + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + node.coord_cache_mut() + .insert_verified(victim_dest, real_coords.clone(), now_ms); + + // One packet, claiming to be from the destination, carrying a different + // position for it under the same root. This is the whole attack. + let forged = + TreeCoordinate::from_addrs(vec![victim_dest, make_node_addr(0x77), root_addr]).unwrap(); + let src_coords = TreeCoordinate::from_addrs(vec![victim_dest, root_addr]).unwrap(); + let payload = SessionSetup::new(src_coords, forged.clone()).encode(); + let encoded = SessionDatagram::new(victim_dest, victim_dest, payload).encode(); + + let rejected_before = node.metrics().forwarding.coord_hint_rejected.get(); + node.handle_session_datagram(&attacker_link, &encoded[1..], false) + .await; + + assert_eq!( + node.coord_cache().get(&victim_dest, now_ms), + Some(&real_coords), + "a forged warm displaced a verified coordinate" + ); + assert!( + node.metrics().forwarding.coord_hint_rejected.get() > rejected_before, + "the refusal should be counted" + ); +} + #[tokio::test] async fn warming_refuses_a_coordinate_rooted_in_a_tree_this_node_is_not_in() { let mut node = make_node(); @@ -897,7 +936,7 @@ async fn test_forwarding_with_cache_warming_enables_routing() { // Node 0 gets full cache for (addr, coords) in &all_coords { if addr != nodes[0].node.node_addr() { - nodes[0] + let _ = nodes[0] .node .coord_cache_mut() .insert(*addr, coords.clone(), now_ms); @@ -926,7 +965,7 @@ async fn test_forwarding_with_cache_warming_enables_routing() { .unwrap() .1 .clone(); - nodes[i] + let _ = nodes[i] .node .coord_cache_mut() .insert(j_addr, coords, now_ms); diff --git a/src/node/tests/probe.rs b/src/node/tests/probe.rs index f1f3138e..cbe2cf88 100644 --- a/src/node/tests/probe.rs +++ b/src/node/tests/probe.rs @@ -344,7 +344,7 @@ async fn preview_next_hop_reports_why_it_could_name_no_hop() { let alien_root = crate::NodeAddr::from_bytes([0x77; 16]); let coords = crate::proto::stp::TreeCoordinate::from_addrs(vec![stranger, alien_root]) .expect("non-empty coordinate"); - nodes[0] + let _ = nodes[0] .node .coord_cache_mut() .insert(stranger, coords, wall_ms); diff --git a/src/node/tests/routing.rs b/src/node/tests/routing.rs index 007e47e1..a9d51708 100644 --- a/src/node/tests/routing.rs +++ b/src/node/tests/routing.rs @@ -89,7 +89,7 @@ fn test_routing_bloom_filter_hit() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - node.coord_cache_mut().insert(dest, dest_coords, now_ms); + let _ = node.coord_cache_mut().insert(dest, dest_coords, now_ms); // Add dest to peer1's bloom filter only let peer1 = node.get_peer_mut(&peer1_addr).unwrap(); @@ -140,7 +140,7 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - node.coord_cache_mut().insert(dest, dest_coords, now_ms); + let _ = node.coord_cache_mut().insert(dest, dest_coords, now_ms); // Add dest to ALL peers' bloom filters for &addr in &peer_addrs { @@ -192,7 +192,7 @@ fn test_routing_tree_fallback() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - node.coord_cache_mut().insert(dest, dest_coords, now_ms); + let _ = node.coord_cache_mut().insert(dest, dest_coords, now_ms); // No bloom filter hit — should fall back to tree routing. // Our distance to dest: 2 (root → peer → dest) @@ -268,7 +268,7 @@ fn test_routing_bloom_hit_not_closer_falls_through_to_tree() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - node.coord_cache_mut().insert(dest, dest_coords, now_ms); + let _ = node.coord_cache_mut().insert(dest, dest_coords, now_ms); // dest is in bloom_peer's filter only (the "bloom hit" candidate), // but bloom_peer's tree distance (3) is NOT strictly less than our @@ -342,7 +342,8 @@ fn test_routing_refreshes_coord_cache_ttl() { .map(|d| d.as_millis() as u64) .unwrap_or(0); let short_ttl = 10_000; // 10 seconds - node.coord_cache_mut() + let _ = node + .coord_cache_mut() .insert_with_ttl(dest, dest_coords, now_ms, short_ttl); let original_expiry = node.coord_cache().get_entry(&dest).unwrap().expires_at(); @@ -438,7 +439,7 @@ fn test_routing_discovery_coord_cache() { assert!(node.find_next_hop(&dest).is_none()); // Now populate coord_cache (as discovery would do) - node.coord_cache_mut().insert(dest, dest_coords, now_ms); + let _ = node.coord_cache_mut().insert(dest, dest_coords, now_ms); // find_next_hop should succeed via coord_cache let result = node.find_next_hop(&dest); @@ -484,14 +485,14 @@ async fn test_routing_chain_topology() { let node3_addr = *nodes[3].node.node_addr(); let node3_coords = nodes[3].node.tree_state().my_coords().clone(); - nodes[0] + let _ = nodes[0] .node .coord_cache_mut() .insert(node3_addr, node3_coords, now_ms); let node0_addr = *nodes[0].node.node_addr(); let node0_coords = nodes[0].node.tree_state().my_coords().clone(); - nodes[3] + let _ = nodes[3] .node .coord_cache_mut() .insert(node0_addr, node0_coords, now_ms); @@ -551,7 +552,7 @@ async fn test_routing_bloom_preferred_over_tree() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - nodes[0] + let _ = nodes[0] .node .coord_cache_mut() .insert(dest, dest_coords, now_ms); @@ -705,7 +706,8 @@ async fn test_routing_reachability_100_nodes() { for node in &mut nodes { for (addr, coords) in &all_coords { if addr != node.node.node_addr() { - node.node + let _ = node + .node .coord_cache_mut() .insert(*addr, coords.clone(), now_ms); } @@ -840,7 +842,8 @@ async fn test_routing_stops_after_peer_removal() { for node in &mut nodes { for (addr, coords) in &all_coords { if addr != node.node.node_addr() { - node.node + let _ = node + .node .coord_cache_mut() .insert(*addr, coords.clone(), now_ms); } @@ -945,7 +948,7 @@ async fn test_routing_bloom_only_transit() { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); - nodes[0] + let _ = nodes[0] .node .coord_cache_mut() .insert(node3_addr, node3_coords, now_ms); @@ -1055,7 +1058,7 @@ async fn test_routing_source_only_coords_100_nodes() { // Inject dest coords ONLY at the source let (dest_addr, dest_coords) = &all_coords[dst]; - nodes[src] + let _ = nodes[src] .node .coord_cache_mut() .insert(*dest_addr, dest_coords.clone(), now_ms); @@ -1098,7 +1101,8 @@ async fn test_routing_source_only_coords_100_nodes() { for node in &mut nodes { for (addr, coords) in &all_coords { if addr != node.node.node_addr() { - node.node + let _ = node + .node .coord_cache_mut() .insert(*addr, coords.clone(), now_ms); } @@ -1145,7 +1149,7 @@ fn test_classify_forward_tree_up() { // Destination somewhere above us; routed via the parent. let dest = make_node_addr(50); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( dest, TreeCoordinate::from_addrs(vec![dest, root]).unwrap(), now_ms(), @@ -1175,7 +1179,7 @@ fn test_classify_forward_tree_down() { // Destination below the child; routed down to it. let dest = make_node_addr(60); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( dest, TreeCoordinate::from_addrs(vec![dest, child, me, root]).unwrap(), now_ms(), @@ -1209,7 +1213,7 @@ fn test_classify_forward_tree_down_cross() { // Destination lives elsewhere (directly under root), NOT under the child; // reachable from the child only via a cross-link. let dest = make_node_addr(60); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( dest, TreeCoordinate::from_addrs(vec![dest, root]).unwrap(), now_ms(), @@ -1241,7 +1245,7 @@ fn test_classify_forward_crosslink_descend() { // Destination is under the cross-link peer. let dest = make_node_addr(70); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( dest, TreeCoordinate::from_addrs(vec![dest, peer, sibling_parent, root]).unwrap(), now_ms(), @@ -1273,7 +1277,7 @@ fn test_classify_forward_crosslink_ascend() { // Destination lives elsewhere (under root directly), NOT under the peer. let dest = make_node_addr(80); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( dest, TreeCoordinate::from_addrs(vec![dest, root]).unwrap(), now_ms(), @@ -1382,14 +1386,14 @@ fn test_parent_loss_reparent_invalidates_coord_cache() { // via-node class: a downstream destination that routes through us. let downstream = make_node_addr(10); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( downstream, TreeCoordinate::from_addrs(vec![downstream, my_addr, root]).unwrap(), now_ms, ); // survivor: same root, does not route through us. let sibling_dest = make_node_addr(11); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( sibling_dest, TreeCoordinate::from_addrs(vec![sibling_dest, alt, root]).unwrap(), now_ms, @@ -1436,14 +1440,14 @@ fn test_parent_loss_selfroot_invalidates_coord_cache() { // via-node class: routes through us. let downstream = make_node_addr(10); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( downstream, TreeCoordinate::from_addrs(vec![downstream, my_addr, old_root]).unwrap(), now_ms, ); // other-roots class: on the old root, does not route through us. let foreign = make_node_addr(11); - node.coord_cache_mut().insert( + let _ = node.coord_cache_mut().insert( foreign, TreeCoordinate::from_addrs(vec![foreign, parent, old_root]).unwrap(), now_ms, @@ -1549,7 +1553,8 @@ fn seam_two_equidistant_peers(node: &mut Node) -> (NodeAddr, NodeAddr, NodeAddr) .update_peer(ParentDeclaration::new(far, dest, 3, 1000), far_coords); let dest_coords = TreeCoordinate::from_addrs(vec![dest, near, my_addr]).unwrap(); - node.coord_cache_mut() + let _ = node + .coord_cache_mut() .insert(dest, dest_coords, seam_now_ms()); (near, far, dest) @@ -1593,7 +1598,8 @@ fn seam_distance_ladder(node: &mut Node) -> (NodeAddr, NodeAddr, NodeAddr, NodeA .update_peer(ParentDeclaration::new(rung1, rung2, 3, 1000), rung1_coords); let dest_coords = TreeCoordinate::from_addrs(vec![dest, rung1, rung2, rung3, my_addr]).unwrap(); - node.coord_cache_mut() + let _ = node + .coord_cache_mut() .insert(dest, dest_coords, seam_now_ms()); (rung1, rung2, rung3, dest) @@ -1901,7 +1907,8 @@ fn test_seam_routing_view_reads_match_live_peer_state() { // not only the present/absent arms. let stale = make_node_addr(201); let stale_coords = TreeCoordinate::from_addrs(vec![stale, near, my_addr]).unwrap(); - node.coord_cache_mut() + let _ = node + .coord_cache_mut() .insert_with_ttl(stale, stale_coords, 1_000_000, 10); let unknown = make_node_addr(202); diff --git a/src/node/tests/session.rs b/src/node/tests/session.rs index 8e76a541..56e02382 100644 --- a/src/node/tests/session.rs +++ b/src/node/tests/session.rs @@ -3055,7 +3055,7 @@ async fn test_path_broken_naming_a_dest_with_no_session_does_not_flush_cached_co let dest = NodeAddr::from_bytes([0xCC; 16]); let reporter = NodeAddr::from_bytes([0xBB; 16]); let coords = node.tree_state().my_coords().clone(); - node.coord_cache_mut().insert(dest, coords, 1000); + let _ = node.coord_cache_mut().insert(dest, coords, 1000); let encoded = PathBroken::new(dest, reporter).encode(); node.handle_path_broken(&reporter, &encoded[5..]).await; @@ -3099,7 +3099,7 @@ async fn test_path_broken_naming_a_dest_whose_entry_is_an_unauthenticated_respon let dest = NodeAddr::from_bytes([0xCC; 16]); let reporter = NodeAddr::from_bytes([0xBB; 16]); let coords = node.tree_state().my_coords().clone(); - node.coord_cache_mut().insert(dest, coords, 1000); + let _ = node.coord_cache_mut().insert(dest, coords, 1000); // One forged SessionSetup naming `dest` would leave exactly this entry. install_halfopen(&mut node, dest); @@ -3137,7 +3137,7 @@ async fn test_path_broken_for_a_session_we_initiated_still_flushes_cached_coords let dest = *remote.node_addr(); let reporter = NodeAddr::from_bytes([0xBB; 16]); let coords = node.tree_state().my_coords().clone(); - node.coord_cache_mut().insert(dest, coords, 1000); + let _ = node.coord_cache_mut().insert(dest, coords, 1000); let encoded = PathBroken::new(dest, reporter).encode(); node.handle_path_broken(&reporter, &encoded[5..]).await; diff --git a/src/node/tests/spanning_tree.rs b/src/node/tests/spanning_tree.rs index 62069f98..2f4852e9 100644 --- a/src/node/tests/spanning_tree.rs +++ b/src/node/tests/spanning_tree.rs @@ -875,7 +875,8 @@ pub(super) fn populate_all_coord_caches(nodes: &mut [TestNode]) { for tn in nodes.iter_mut() { for (addr, coords) in &all_coords { if addr != tn.node.node_addr() { - tn.node + let _ = tn + .node .coord_cache_mut() .insert(*addr, coords.clone(), now_ms); }