fix(cache): let a verified coordinate refuse an unauthenticated hint

A coordinate learned by warming and one established by a lookup whose proof
this node checked were until now the same thing, so a forged warm silently
displaced a verified entry. Entries now carry their provenance, and a hint
does not overwrite a live verified one.

Hint is the default at every level. CacheEntry::new produces one, update
demotes to one, and CoordCache::insert takes hint semantics under the
obvious name. Conferring trust has to be asked for, by calling
insert_verified or insert_verified_with_path_mtu, which only the lookup
response handler does. That way the safe write is what a caller gets by
reaching for the name they would reach for anyway, rather than the opt-in.

HintOutcome is #[must_use], which turned out to be the useful part. It made
the compiler, not review, enumerate every production hint write: the warming
funnel, the CP-flag local delivery, and the initiator-side SessionAck. That
matched the enumeration done by reading, which is the only reason I trust
either.

Verification runs on its own clock and is never refreshed. An entry carrying
live traffic is refreshed on every use and so never expires, and if
verification rode that clock a once-verified entry would outrank every hint
forever, including the hints carrying a destination's genuine move. After
VERIFIED_TTL_MS the coordinates stay usable and simply stop winning.

Eviction will not take a live verified entry while any unverified one
remains, and declines rather than growing past the cap when they are all
verified. Without that, the precedence rule is bypassable: fill the cache
with hints until a verified entry becomes the LRU, evict it, and plant into
a slot that now accepts an ordinary first write. That is the shape that got
the earlier attempt on fix/coord-cache-provenance rejected.

Sixty-nine test call sites are seeds whose outcome cannot be Rejected, since
no verified entry exists in any of those caches; insert_verified appears
outside the cache module in exactly three places, two of them tests of their
own. They take the outcome explicitly.

Break-checked. Disabling the precedence rule reds three tests including the
end-to-end one driven through handle_session_datagram; disabling the
eviction restriction reds the two that cover it. The healthy path stays
green at 2202 tests.

Still mitigation. A destination that was never verified holds only hints, so
hint-over-hint is unchanged, and the delete-then-plant sequence still runs
against one end of a live session.
This commit is contained in:
Johnathan Corgan
2026-08-24 10:38:51 +01:00
parent 1da42867f1
commit a9ee4bf2f0
17 changed files with 485 additions and 104 deletions
+265 -59
View File
@@ -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));
}
}
+84 -2
View File
@@ -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<u16>,
/// 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).
+4 -2
View File
@@ -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)]
+2
View File
@@ -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,
+2
View File
@@ -6,6 +6,8 @@
"estimated_mesh_size": null,
"exe_path": "<redacted>",
"forwarding": {
"coord_hint_changed": 0,
"coord_hint_rejected": 0,
"coord_warm_foreign_root": 0,
"coord_warm_key_mismatch": 0,
"decode_error_bytes": 0,
+1 -1
View File
@@ -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) {
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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;
+21
View File
@@ -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(),
+17
View File
@@ -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() {
+2
View File
@@ -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,
+3 -3
View File
@@ -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);
+41 -2
View File
@@ -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);
+1 -1
View File
@@ -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);
+33 -26
View File
@@ -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);
+3 -3
View File
@@ -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;
+2 -1
View File
@@ -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);
}