umsh_mac/
cache.rs

1use heapless::Deque;
2
3// Replay detection is a Security-chapter concept and lives in
4// umsh-crypto so the device can share it without depending on
5// the MAC; re-exported here so this crate's public API is unchanged.
6pub use umsh_crypto::replay::{RecentMic, ReplayVerdict, ReplayWindow};
7
8/// Duplicate-suppression key derived from an accepted packet.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum DupCacheKey {
11    /// Authenticated routable packet keyed by its MIC bytes.
12    Mic {
13        bytes: [u8; 16],
14        len: u8,
15        route_retry: bool,
16    },
17    /// MIC-less routable packet keyed by a stable local hash over non-dynamic
18    /// fields.
19    Hash32(u32),
20}
21
22/// How long a duplicate key stays suppressed.
23///
24/// Capacity alone is not an expiry policy. A packet whose identity is
25/// derived from its contents rather than a MIC — a beacon, whose body is
26/// empty and whose non-dynamic options never change — hashes to the same key
27/// on every repetition, so a quiet mesh that never pushes 64 further keys
28/// through the ring would suppress that node's beacons forever. An hour is
29/// long enough that a burst of retransmissions of one packet is still
30/// collapsed, and short enough that a node re-announcing itself is heard
31/// again well within the time anyone would wait for it.
32pub const DUP_CACHE_TTL_MS: u64 = 60 * 60 * 1000;
33
34/// Fixed-capacity cache of recently observed duplicate keys.
35///
36/// Entries leave either by age ([`DUP_CACHE_TTL_MS`]) or by eviction of the
37/// oldest when the ring is full, whichever comes first.
38#[derive(Clone, Debug)]
39pub struct DuplicateCache<const N: usize = 64> {
40    /// Insertion-ordered, so the oldest entry is always at the front and
41    /// expiry can be pruned from that end without a scan.
42    entries: Deque<(DupCacheKey, u64), N>,
43}
44
45impl<const N: usize> Default for DuplicateCache<N> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl<const N: usize> DuplicateCache<N> {
52    /// Create an empty duplicate cache.
53    pub fn new() -> Self {
54        Self {
55            entries: Deque::new(),
56        }
57    }
58
59    /// Return whether `key` is present and has not aged out.
60    pub fn contains(&self, key: &DupCacheKey, now_ms: u64) -> bool {
61        self.entries
62            .iter()
63            .any(|(entry, inserted_ms)| entry == key && !Self::is_expired(*inserted_ms, now_ms))
64    }
65
66    /// Insert `key`, dropping aged-out entries and evicting the oldest
67    /// survivor if the ring is still full.
68    ///
69    /// A repeat of a key already held does not refresh its timestamp: the
70    /// entry ages from when the packet was *first* seen, so a node repeating
71    /// itself inside the window cannot hold its own suppression open.
72    pub fn insert(&mut self, key: DupCacheKey, now_ms: u64) {
73        self.expire(now_ms);
74        if self.contains(&key, now_ms) {
75            return;
76        }
77        if self.entries.is_full() {
78            let _ = self.entries.pop_front();
79        }
80        let _ = self.entries.push_back((key, now_ms));
81    }
82
83    /// Drop every entry older than [`DUP_CACHE_TTL_MS`].
84    pub fn expire(&mut self, now_ms: u64) {
85        while let Some((_, inserted_ms)) = self.entries.front() {
86            if !Self::is_expired(*inserted_ms, now_ms) {
87                return;
88            }
89            let _ = self.entries.pop_front();
90        }
91    }
92
93    /// A clock that has gone backwards (a resynchronized monotonic source)
94    /// leaves the entry looking younger than it is, never older, so
95    /// suppression can only be held slightly too long — never released early.
96    fn is_expired(inserted_ms: u64, now_ms: u64) -> bool {
97        now_ms.saturating_sub(inserted_ms) >= DUP_CACHE_TTL_MS
98    }
99
100    /// Return the number of tracked entries.
101    pub fn len(&self) -> usize {
102        self.entries.len()
103    }
104
105    /// Return whether the cache is empty.
106    pub fn is_empty(&self) -> bool {
107        self.entries.is_empty()
108    }
109}