Skip to main content

async_snmp/v3/
engine.rs

1//! Engine discovery and time synchronization (RFC 3414 Section 4).
2//!
3//! `SNMPv3` discovers an authoritative engine's identity before authenticated
4//! traffic, then establishes boots/time only from an HMAC-verified message.
5//! This module keeps those trust domains separate and provides:
6//!
7//! - `EngineCache`: Thread-safe target identities and per-engine trusted time
8//! - `EngineState`: Discovered identity with optional trusted time
9//! - Discovery response parsing
10//!
11//! # Discovery Flow
12//!
13//! 1. Client sends discovery request (noAuthNoPriv, empty engine ID)
14//! 2. Agent responds with Report PDU containing usmStatsUnknownEngineIDs
15//! 3. The client adopts only the response's engine ID and message-size limit
16//! 4. Its first authenticated request uses boots/time zero
17//! 5. An HMAC-verified response or Report establishes trusted boots/time
18//!
19//! # Time Synchronization
20//!
21//! Per RFC 3414 Section 2.3, a non-authoritative engine (client) maintains:
22//! - `snmpEngineBoots`: Boot counter from authoritative engine
23//! - `snmpEngineTime`: Time value from authoritative engine
24//! - `latestReceivedEngineTime`: Highest time received (anti-replay)
25//!
26//! The time window is 150 seconds. Messages outside this window are rejected.
27
28use std::collections::HashMap;
29use std::net::SocketAddr;
30use std::sync::RwLock;
31use std::time::{Duration, Instant};
32
33use bytes::Bytes;
34
35use crate::error::{Error, Result};
36use crate::v3::UsmSecurityParams;
37
38/// Time window in seconds (RFC 3414 Section 2.2.3).
39pub const TIME_WINDOW: u32 = 150;
40
41/// Maximum valid snmpEngineTime value (RFC 3414 Section 2.2.1).
42///
43/// Per RFC 3414, snmpEngineTime is a 31-bit value (0..2,147,483,647).
44/// When the value reaches this maximum, the authoritative engine should
45/// reset it to zero and increment snmpEngineBoots.
46pub const MAX_ENGINE_TIME: u32 = 2_147_483_647;
47
48/// Default msgMaxSize for UDP transport (65535 - 20 IPv4 - 8 UDP = 65507).
49pub const DEFAULT_MSG_MAX_SIZE: u32 = 65507;
50
51/// Compute engine boots and time from a base boots value and total elapsed
52/// seconds since engine start.
53///
54/// Per RFC 3414 Section 2.3, engine time spans the complete 31-bit range
55/// `0..=MAX_ENGINE_TIME`. On the following second, boots increments and time
56/// wraps to zero. The boots value is capped at `MAX_ENGINE_TIME` (the
57/// "latched" state per RFC 3414 Section 2.2.3).
58#[must_use]
59pub fn compute_engine_boots_time(boots_base: u32, total_elapsed_secs: u64) -> (u32, u32) {
60    let cycle = u64::from(MAX_ENGINE_TIME) + 1;
61    let additional_boots = total_elapsed_secs / cycle;
62    let current_time = (total_elapsed_secs % cycle) as u32;
63    let boots = (u64::from(boots_base) + additional_boots).min(u64::from(MAX_ENGINE_TIME)) as u32;
64    (boots, current_time)
65}
66
67/// Minimum valid SnmpEngineID length in octets (RFC 3411 Section 5).
68pub const MIN_ENGINE_ID_LEN: usize = 5;
69
70/// Maximum valid SnmpEngineID length in octets (RFC 3411 Section 5).
71pub const MAX_ENGINE_ID_LEN: usize = 32;
72
73/// Private Enterprise Number used in generated engine IDs.
74///
75/// 32473 is the IANA example PEN reserved for documentation and testing
76/// (RFC 5612), used here as a stand-in since the crate has no registered
77/// enterprise number of its own.
78const GENERATED_ENGINE_ID_PEN: u32 = 32473;
79
80/// Format octet value 5: "administratively assigned octets" (RFC 3411
81/// Section 5), a variable-length opaque local identifier.
82const ENGINE_ID_FORMAT_OCTETS: u8 = 5;
83
84/// Number of random octets appended to a generated engine ID.
85const GENERATED_ENGINE_ID_RANDOM_LEN: usize = 12;
86
87/// Generate a locally-unique authoritative SnmpEngineID (RFC 3411 Section 5).
88///
89/// Layout: a 4-octet enterprise number with the high bit set, followed by a
90/// format octet of 5 ("administratively assigned octets"), followed by 12
91/// random octets from the OS CSPRNG. The total length is 17 octets, within
92/// the RFC 3411 5..32 range. The random suffix ensures two instances started
93/// in the same second (or on the same host) do not collide, which would
94/// otherwise yield identical localized keys under shared credentials.
95#[must_use]
96pub fn generate_engine_id() -> Bytes {
97    let mut id = Vec::with_capacity(5 + GENERATED_ENGINE_ID_RANDOM_LEN);
98    // High bit of the first octet signals the RFC 3411 variable-length format.
99    let enterprise = 0x8000_0000_u32 | GENERATED_ENGINE_ID_PEN;
100    id.extend_from_slice(&enterprise.to_be_bytes());
101    id.push(ENGINE_ID_FORMAT_OCTETS);
102    let mut random = [0_u8; GENERATED_ENGINE_ID_RANDOM_LEN];
103    getrandom::fill(&mut random).expect("getrandom failed");
104    id.extend_from_slice(&random);
105    Bytes::from(id)
106}
107
108/// Validate a user-configured SnmpEngineID (RFC 3411 Section 5).
109///
110/// Rejects IDs whose length is outside the 5..32 octet range, IDs that are
111/// all zero, and IDs that are all 0xff. All three are invalid or reserved
112/// per RFC 3411 and would break USM key localization or engine discovery.
113pub fn validate_engine_id(engine_id: &[u8]) -> Result<()> {
114    let len = engine_id.len();
115    if !(MIN_ENGINE_ID_LEN..=MAX_ENGINE_ID_LEN).contains(&len) {
116        return Err(Error::Config(
117            format!(
118                "engine ID length {len} out of range (must be {MIN_ENGINE_ID_LEN}..={MAX_ENGINE_ID_LEN} octets)"
119            )
120            .into(),
121        )
122        .boxed());
123    }
124    if engine_id.iter().all(|&b| b == 0x00) {
125        return Err(Error::Config("engine ID must not be all zero".into()).boxed());
126    }
127    if engine_id.iter().all(|&b| b == 0xff) {
128        return Err(Error::Config("engine ID must not be all 0xff".into()).boxed());
129    }
130    Ok(())
131}
132
133/// USM statistics OIDs used in Report PDUs.
134pub mod report_oids {
135    use crate::Oid;
136    use crate::oid;
137
138    /// 1.3.6.1.6.3.15.1.1.1.0 - usmStatsUnsupportedSecLevels
139    #[must_use]
140    pub fn unsupported_sec_levels() -> Oid {
141        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0)
142    }
143
144    /// 1.3.6.1.6.3.15.1.1.2.0 - usmStatsNotInTimeWindows
145    #[must_use]
146    pub fn not_in_time_windows() -> Oid {
147        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 2, 0)
148    }
149
150    /// 1.3.6.1.6.3.15.1.1.3.0 - usmStatsUnknownUserNames
151    #[must_use]
152    pub fn unknown_user_names() -> Oid {
153        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 3, 0)
154    }
155
156    /// 1.3.6.1.6.3.15.1.1.4.0 - usmStatsUnknownEngineIDs
157    #[must_use]
158    pub fn unknown_engine_ids() -> Oid {
159        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 4, 0)
160    }
161
162    /// 1.3.6.1.6.3.15.1.1.5.0 - usmStatsWrongDigests
163    #[must_use]
164    pub fn wrong_digests() -> Oid {
165        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0)
166    }
167
168    /// 1.3.6.1.6.3.15.1.1.6.0 - usmStatsDecryptionErrors
169    #[must_use]
170    pub fn decryption_errors() -> Oid {
171        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 6, 0)
172    }
173}
174
175/// HMAC-established notion of an authoritative engine's boots/time tuple.
176///
177/// Discovery never constructs this value. It is created and advanced only by
178/// RFC 3414 Section 3.2 Step 7(b) after a message's HMAC has been verified.
179#[derive(Debug, Clone)]
180pub struct TrustedEngineTime {
181    boots: u32,
182    received_time_base: u32,
183    received_at: Instant,
184    latest_received_time: u32,
185}
186
187impl TrustedEngineTime {
188    fn new_at(boots: u32, time: u32, now: Instant) -> Self {
189        Self {
190            boots,
191            received_time_base: time,
192            received_at: now,
193            latest_received_time: time,
194        }
195    }
196
197    /// The boots value at the last trusted high-water update.
198    #[must_use]
199    pub fn boots(&self) -> u32 {
200        self.boots
201    }
202
203    /// The engine time at the last trusted high-water update.
204    #[must_use]
205    pub fn received_time_base(&self) -> u32 {
206        self.received_time_base
207    }
208
209    /// The greatest authenticated engine time received for the current boots.
210    #[must_use]
211    pub fn latest_received_time(&self) -> u32 {
212        self.latest_received_time
213    }
214
215    fn estimated_at(&self, now: Instant) -> (u32, u32) {
216        if self.boots == MAX_ENGINE_TIME {
217            return (
218                MAX_ENGINE_TIME,
219                self.received_time_base.min(MAX_ENGINE_TIME),
220            );
221        }
222
223        let elapsed = now
224            .checked_duration_since(self.received_at)
225            .unwrap_or_default()
226            .as_secs();
227        let total_time = u64::from(self.received_time_base).saturating_add(elapsed);
228        let cycle = u64::from(MAX_ENGINE_TIME) + 1;
229        let additional_boots = total_time / cycle;
230        let engine_time = (total_time % cycle) as u32;
231        let engine_boots =
232            (u64::from(self.boots) + additional_boots).min(u64::from(MAX_ENGINE_TIME)) as u32;
233        (engine_boots, engine_time)
234    }
235
236    fn roll_forward_at(&mut self, now: Instant) {
237        let (estimated_boots, estimated_time) = self.estimated_at(now);
238        if estimated_boots > self.boots {
239            self.boots = estimated_boots;
240            self.received_time_base = estimated_time;
241            self.received_at = now;
242            self.latest_received_time = estimated_time;
243        }
244    }
245
246    fn update_at(&mut self, response_boots: u32, response_time: u32, now: Instant) -> bool {
247        self.roll_forward_at(now);
248        if response_boots > self.boots
249            || (response_boots == self.boots && response_time > self.latest_received_time)
250        {
251            self.boots = response_boots;
252            self.received_time_base = response_time;
253            self.received_at = now;
254            self.latest_received_time = response_time;
255            true
256        } else {
257            false
258        }
259    }
260}
261
262/// Identity discovered for a remote authoritative engine, with optional
263/// HMAC-established trusted time.
264#[derive(Debug, Clone)]
265pub struct EngineState {
266    /// Authoritative engine ID.
267    pub(crate) engine_id: Bytes,
268    /// Maximum message size the remote engine can accept.
269    pub msg_max_size: u32,
270    trusted_time: Option<TrustedEngineTime>,
271}
272
273impl EngineState {
274    /// Create engine state whose boots/time are already authenticated.
275    pub fn new(engine_id: Bytes, engine_boots: u32, engine_time: u32) -> Self {
276        Self::with_msg_max_size(engine_id, engine_boots, engine_time, DEFAULT_MSG_MAX_SIZE)
277    }
278
279    /// Create an identity learned through unauthenticated discovery.
280    #[must_use]
281    pub fn discovered(engine_id: Bytes, msg_max_size: u32) -> Self {
282        Self {
283            engine_id,
284            msg_max_size,
285            trusted_time: None,
286        }
287    }
288
289    /// Create authenticated state with explicit msgMaxSize.
290    pub fn with_msg_max_size(
291        engine_id: Bytes,
292        engine_boots: u32,
293        engine_time: u32,
294        msg_max_size: u32,
295    ) -> Self {
296        Self {
297            engine_id,
298            msg_max_size,
299            trusted_time: Some(TrustedEngineTime::new_at(
300                engine_boots,
301                engine_time,
302                Instant::now(),
303            )),
304        }
305    }
306
307    /// Create authenticated state with msgMaxSize capped to a session limit.
308    pub fn with_msg_max_size_capped(
309        engine_id: Bytes,
310        engine_boots: u32,
311        engine_time: u32,
312        reported_msg_max_size: u32,
313        session_max: u32,
314    ) -> Self {
315        Self::with_msg_max_size(
316            engine_id,
317            engine_boots,
318            engine_time,
319            cap_msg_max_size(reported_msg_max_size, session_max),
320        )
321    }
322
323    /// Return the authoritative engine ID.
324    #[must_use]
325    pub fn engine_id(&self) -> &Bytes {
326        &self.engine_id
327    }
328
329    /// Return the HMAC-established trusted time, if synchronization occurred.
330    #[must_use]
331    pub fn trusted_time(&self) -> Option<&TrustedEngineTime> {
332        self.trusted_time.as_ref()
333    }
334
335    /// Return the progressing trusted boots/time pair, or `(0, 0)` before the
336    /// first authenticated message establishes a notion.
337    #[must_use]
338    pub fn estimated_boots_time(&self) -> (u32, u32) {
339        self.estimated_boots_time_at(Instant::now())
340    }
341
342    pub(crate) fn estimated_boots_time_at(&self, now: Instant) -> (u32, u32) {
343        self.trusted_time
344            .as_ref()
345            .map_or((0, 0), |time| time.estimated_at(now))
346    }
347
348    pub(crate) fn last_trusted_update_at(&self) -> Option<Instant> {
349        self.trusted_time.as_ref().map(|time| time.received_at)
350    }
351
352    /// Retained convenience accessor for the estimated time component.
353    #[must_use]
354    pub fn estimated_time(&self) -> u32 {
355        self.estimated_boots_time().1
356    }
357
358    /// Apply a forward-only authenticated high-water update.
359    ///
360    /// The caller must have verified the message HMAC and engine identity.
361    pub fn update_time(&mut self, response_boots: u32, response_time: u32) -> bool {
362        self.update_time_at(response_boots, response_time, Instant::now())
363    }
364
365    fn update_time_at(&mut self, response_boots: u32, response_time: u32, now: Instant) -> bool {
366        match self.trusted_time.as_mut() {
367            Some(time) => time.update_at(response_boots, response_time, now),
368            None => {
369                self.trusted_time = Some(TrustedEngineTime::new_at(
370                    response_boots,
371                    response_time,
372                    now,
373                ));
374                true
375            }
376        }
377    }
378
379    /// Merge only a newer trusted notion from another clone of this identity.
380    pub(crate) fn merge_from(&mut self, other: &Self) -> bool {
381        if self.engine_id != other.engine_id {
382            return false;
383        }
384        self.msg_max_size = self.msg_max_size.min(other.msg_max_size);
385        let Some(other_time) = &other.trusted_time else {
386            return false;
387        };
388        match self.trusted_time.as_mut() {
389            Some(time) => time.update_at(
390                other_time.boots,
391                other_time.latest_received_time,
392                other_time.received_at,
393            ),
394            None => {
395                self.trusted_time = Some(other_time.clone());
396                true
397            }
398        }
399    }
400
401    /// Apply RFC 3414 Step 7(b) and evaluate the asymmetric time window.
402    /// The caller must first verify the message HMAC and engine identity.
403    pub fn check_and_update_timeliness(&mut self, msg_boots: u32, msg_time: u32) -> bool {
404        self.check_and_update_timeliness_at(msg_boots, msg_time, Instant::now())
405    }
406
407    fn check_and_update_timeliness_at(
408        &mut self,
409        msg_boots: u32,
410        msg_time: u32,
411        now: Instant,
412    ) -> bool {
413        self.update_time_at(msg_boots, msg_time, now);
414        let (local_boots, local_time) = self.estimated_boots_time_at(now);
415        local_boots != MAX_ENGINE_TIME
416            && msg_boots >= local_boots
417            && (msg_boots != local_boots || msg_time >= local_time.saturating_sub(TIME_WINDOW))
418    }
419
420    /// Check the authoritative-role symmetric window against trusted time.
421    #[must_use]
422    pub fn is_in_time_window(&self, msg_boots: u32, msg_time: u32) -> bool {
423        let (local_boots, local_time) = self.estimated_boots_time();
424        in_authoritative_time_window(local_boots, local_time, msg_boots, msg_time)
425    }
426}
427
428fn cap_msg_max_size(reported: u32, session_max: u32) -> u32 {
429    if reported > session_max {
430        tracing::debug!(target: "async_snmp::v3", { reported, session_max }, "capping msgMaxSize to session limit");
431        session_max
432    } else {
433        reported
434    }
435}
436
437/// Time window check when the local engine's boots/time are the reference
438/// (RFC 3414 Section 2.2.3, applied by Section 3.2 Step 7a in the
439/// authoritative role).
440///
441/// The message is in the window only if local boots is not latched at
442/// [`MAX_ENGINE_TIME`], the message boots equals local boots, and the message
443/// time is within [`TIME_WINDOW`] seconds of local time (symmetric).
444///
445/// For messages from a remote authoritative engine (Step 7b), use
446/// [`EngineState::check_and_update_timeliness`] instead: that check is
447/// asymmetric and self-updating.
448pub fn in_authoritative_time_window(
449    local_boots: u32,
450    local_time: u32,
451    msg_boots: u32,
452    msg_time: u32,
453) -> bool {
454    local_boots != MAX_ENGINE_TIME
455        && msg_boots == local_boots
456        && msg_time.abs_diff(local_time) <= TIME_WINDOW
457}
458
459/// Default TTL for engine cache entries (5 minutes).
460///
461/// Entries not refreshed by a successful authenticated exchange within
462/// this duration are considered stale for future cache lookups. This avoids
463/// handing an old target mapping to newly constructed clients indefinitely;
464/// an existing client retains its established identity until
465/// [`Client::rediscover_engine`](crate::Client::rediscover_engine) is called.
466const DEFAULT_ENGINE_CACHE_TTL: Duration = Duration::from_secs(300);
467
468#[derive(Debug)]
469struct CachedTarget {
470    engine_id: Bytes,
471    msg_max_size: u32,
472    refreshed_at: Instant,
473}
474
475#[derive(Debug, Default)]
476struct EngineCacheInner {
477    targets: HashMap<SocketAddr, CachedTarget>,
478    trusted_times: HashMap<Bytes, TrustedEngineTime>,
479}
480
481/// Thread-safe cache of discovered `SNMPv3` engine state.
482///
483/// Target addresses map to discovered identities and remote message-size
484/// limits. Trusted time is keyed separately by authoritative engine ID, so
485/// clients reaching the same engine through multiple targets converge on one
486/// high-water value. Whole-state inserts merge monotonically and cannot replace
487/// a newer trusted tuple with a stale clone.
488///
489/// # Entry lifetime
490///
491/// Each target identity has a refresh timestamp. Every accepted HMAC-verified
492/// message refreshes it, including an older in-window message that does not
493/// advance trusted time. Entries older than the configured TTL
494/// (default 5 minutes) are removed by [`get`](Self::get).
495///
496/// Expiry prevents a shared entry from being handed indefinitely to newly
497/// constructed clients after a target is replaced. It does not silently clear
498/// an existing client's established identity; call
499/// [`Client::rediscover_engine`](crate::Client::rediscover_engine) to replace it
500/// intentionally.
501///
502/// Actively polled authenticated targets refresh their entry on every accepted
503/// HMAC-verified response or Report, so the TTL has no effect during normal
504/// authenticated operation.
505///
506/// # Capacity
507///
508/// The cache is unbounded by default. Each entry is roughly 100-150 bytes,
509/// so even 100k targets uses only ~10-15 MB. For deployments that scan
510/// very large address ranges, [`with_max_capacity`](Self::with_max_capacity)
511/// sets a hard limit with oldest-entry eviction.
512///
513/// # Example
514///
515/// ```rust,no_run
516/// use async_snmp::{Auth, AuthProtocol, Client, EngineCache};
517/// use std::sync::Arc;
518///
519/// # async fn example() -> async_snmp::Result<()> {
520/// let cache = Arc::new(EngineCache::new());
521///
522/// let client1 = Client::builder("192.168.1.1:161",
523///     Auth::usm("admin").auth(AuthProtocol::Sha1, "authpass"))
524///     .engine_cache(cache.clone())
525///     .connect()
526///     .await?;
527///
528/// let client2 = Client::builder("192.168.1.2:161",
529///     Auth::usm("admin").auth(AuthProtocol::Sha1, "authpass"))
530///     .engine_cache(cache.clone())
531///     .connect()
532///     .await?;
533/// # Ok(())
534/// # }
535/// ```
536#[derive(Debug)]
537pub struct EngineCache {
538    inner: RwLock<EngineCacheInner>,
539    max_capacity: Option<usize>,
540    ttl: Duration,
541}
542
543impl Default for EngineCache {
544    fn default() -> Self {
545        Self::new()
546    }
547}
548
549impl EngineCache {
550    /// Create a new empty engine cache with default settings.
551    #[must_use]
552    pub fn new() -> Self {
553        Self {
554            inner: RwLock::new(EngineCacheInner::default()),
555            max_capacity: None,
556            ttl: DEFAULT_ENGINE_CACHE_TTL,
557        }
558    }
559
560    /// Set a maximum capacity. When full, the oldest entry is evicted on insert.
561    #[must_use]
562    pub fn with_max_capacity(mut self, max_capacity: usize) -> Self {
563        self.max_capacity = Some(max_capacity.max(1));
564        self
565    }
566
567    /// Set the TTL for cache entries. Entries not refreshed within this
568    /// duration are removed on lookup, triggering re-discovery.
569    #[must_use]
570    pub fn with_ttl(mut self, ttl: Duration) -> Self {
571        self.ttl = ttl;
572        self
573    }
574
575    /// Get cached engine state for a target.
576    ///
577    /// Returns `None` if the entry does not exist or has expired.
578    /// Expired entries are removed from the cache.
579    pub fn get(&self, target: &SocketAddr) -> Option<EngineState> {
580        self.get_at(target, Instant::now())
581    }
582
583    fn get_at(&self, target: &SocketAddr, now: Instant) -> Option<EngineState> {
584        let mut inner = self.inner.write().ok()?;
585        let cached = inner.targets.get(target)?;
586        if now
587            .checked_duration_since(cached.refreshed_at)
588            .unwrap_or_default()
589            > self.ttl
590        {
591            let engine_id = cached.engine_id.clone();
592            inner.targets.remove(target);
593            remove_orphaned_time(&mut inner, &engine_id);
594            return None;
595        }
596        compose_cached_state(&inner, target)
597    }
598
599    /// Store engine state for a target.
600    ///
601    /// If a max capacity is set and the cache is full, the least recently
602    /// refreshed target identity is evicted.
603    pub fn insert(&self, target: SocketAddr, state: EngineState) {
604        self.insert_at(target, state, Instant::now());
605    }
606
607    fn insert_at(&self, target: SocketAddr, state: EngineState, now: Instant) {
608        let _ = self.store_at(target, state, now, false);
609    }
610
611    /// Replace one target identity after an explicit, validated rediscovery.
612    ///
613    /// Unlike ordinary inserts, this deliberately overrides an active
614    /// conflicting mapping. Holding the cache write lock makes the replacement
615    /// win over stale clients that reinsert the old identity while discovery is
616    /// in flight; subsequent ordinary inserts cannot replace the new mapping.
617    /// The returned state includes trusted time already shared under the new
618    /// authoritative engine ID.
619    pub(crate) fn replace_target(
620        &self,
621        target: SocketAddr,
622        state: EngineState,
623    ) -> Result<EngineState> {
624        self.store_at(target, state, Instant::now(), true)
625            .ok_or_else(|| Error::Config("engine cache lock poisoned".into()).boxed())
626    }
627
628    fn store_at(
629        &self,
630        target: SocketAddr,
631        state: EngineState,
632        now: Instant,
633        replace_identity: bool,
634    ) -> Option<EngineState> {
635        let mut inner = self.inner.write().ok()?;
636
637        if !replace_identity
638            && let Some(existing) = inner.targets.get(&target)
639            && existing.engine_id != state.engine_id
640            && now
641                .checked_duration_since(existing.refreshed_at)
642                .unwrap_or_default()
643                <= self.ttl
644        {
645            return compose_cached_state(&inner, &target);
646        }
647
648        if let Some(cap) = self.max_capacity
649            && !inner.targets.contains_key(&target)
650            && inner.targets.len() >= cap
651            && let Some((oldest_target, oldest_engine)) = inner
652                .targets
653                .iter()
654                .min_by_key(|(_, cached)| cached.refreshed_at)
655                .map(|(target, cached)| (*target, cached.engine_id.clone()))
656        {
657            inner.targets.remove(&oldest_target);
658            remove_orphaned_time(&mut inner, &oldest_engine);
659        }
660
661        let replaced_engine = inner
662            .targets
663            .get(&target)
664            .filter(|cached| cached.engine_id != state.engine_id)
665            .map(|cached| cached.engine_id.clone());
666        if let Some(trusted) = &state.trusted_time {
667            merge_trusted_time(&mut inner.trusted_times, &state.engine_id, trusted);
668        }
669        inner.targets.insert(
670            target,
671            CachedTarget {
672                engine_id: state.engine_id,
673                msg_max_size: state.msg_max_size,
674                refreshed_at: now,
675            },
676        );
677        if let Some(replaced_engine) = replaced_engine {
678            remove_orphaned_time(&mut inner, &replaced_engine);
679        }
680        compose_cached_state(&inner, &target)
681    }
682
683    /// Update time for an existing entry after authenticating a message.
684    ///
685    /// The caller must have verified the message HMAC and engine identity.
686    /// Returns true if the entry was updated, false if not found or not updated.
687    pub fn update_time(
688        &self,
689        target: &SocketAddr,
690        response_boots: u32,
691        response_time: u32,
692    ) -> bool {
693        self.update_time_at(target, response_boots, response_time, Instant::now())
694    }
695
696    fn update_time_at(
697        &self,
698        target: &SocketAddr,
699        response_boots: u32,
700        response_time: u32,
701        now: Instant,
702    ) -> bool {
703        let Ok(mut inner) = self.inner.write() else {
704            return false;
705        };
706        let Some(engine_id) = inner
707            .targets
708            .get(target)
709            .map(|cached| cached.engine_id.clone())
710        else {
711            return false;
712        };
713        let changed = match inner.trusted_times.get_mut(&engine_id) {
714            Some(time) => time.update_at(response_boots, response_time, now),
715            None => {
716                inner.trusted_times.insert(
717                    engine_id,
718                    TrustedEngineTime::new_at(response_boots, response_time, now),
719                );
720                true
721            }
722        };
723        if let Some(cached) = inner.targets.get_mut(target) {
724            cached.refreshed_at = now;
725        }
726        changed
727    }
728
729    /// Atomically apply authenticated timeliness processing to shared state.
730    ///
731    /// The live client's trusted notion is merged before evaluating the
732    /// message, so a rebuilt cache cannot weaken that client's time window.
733    /// Timely authenticated messages refresh the target TTL, including older
734    /// in-window messages that do not advance the high-water mark. Rejected
735    /// messages still apply required forward-only high-water processing but do
736    /// not refresh the target mapping.
737    pub(crate) fn check_and_update_timeliness(
738        &self,
739        target: &SocketAddr,
740        local_state: &EngineState,
741        engine_id: &[u8],
742        msg_boots: u32,
743        msg_time: u32,
744    ) -> Option<(bool, EngineState)> {
745        self.check_and_update_timeliness_at(
746            target,
747            local_state,
748            engine_id,
749            msg_boots,
750            msg_time,
751            Instant::now(),
752        )
753    }
754
755    /// Evaluate authenticated timeliness against a coherent live/cache
756    /// snapshot without publishing the message tuple or refreshing cache TTL.
757    ///
758    /// Used while a response to a packet-local compatibility correction is
759    /// still provisional. The returned state may be merged only after full
760    /// response correlation succeeds.
761    pub(crate) fn timeliness_candidate(
762        &self,
763        target: &SocketAddr,
764        local_state: &EngineState,
765        engine_id: &[u8],
766        msg_boots: u32,
767        msg_time: u32,
768    ) -> Option<(bool, EngineState)> {
769        let inner = self.inner.read().ok()?;
770        let cached_engine_id = &inner.targets.get(target)?.engine_id;
771        if cached_engine_id.as_ref() != engine_id || local_state.engine_id.as_ref() != engine_id {
772            return None;
773        }
774        let mut candidate = local_state.clone();
775        candidate.merge_from(&compose_cached_state(&inner, target)?);
776        let timely = candidate.check_and_update_timeliness(msg_boots, msg_time);
777        Some((timely, candidate))
778    }
779
780    fn check_and_update_timeliness_at(
781        &self,
782        target: &SocketAddr,
783        local_state: &EngineState,
784        engine_id: &[u8],
785        msg_boots: u32,
786        msg_time: u32,
787        now: Instant,
788    ) -> Option<(bool, EngineState)> {
789        let mut inner = self.inner.write().ok()?;
790        let cached_engine_id = inner.targets.get(target)?.engine_id.clone();
791        if cached_engine_id.as_ref() != engine_id || local_state.engine_id.as_ref() != engine_id {
792            return None;
793        }
794        if let Some(local_time) = &local_state.trusted_time {
795            merge_trusted_time(&mut inner.trusted_times, &cached_engine_id, local_time);
796        }
797        let time = inner
798            .trusted_times
799            .entry(cached_engine_id.clone())
800            .or_insert_with(|| TrustedEngineTime::new_at(msg_boots, msg_time, now));
801        time.update_at(msg_boots, msg_time, now);
802        let (local_boots, local_time) = time.estimated_at(now);
803        let timely = local_boots != MAX_ENGINE_TIME
804            && msg_boots >= local_boots
805            && (msg_boots != local_boots || msg_time >= local_time.saturating_sub(TIME_WINDOW));
806        if timely {
807            inner.targets.get_mut(target)?.refreshed_at = now;
808        }
809        let state = compose_cached_state(&inner, target)?;
810        Some((timely, state))
811    }
812
813    /// Remove cached identity for a target. Shared trusted time remains while
814    /// another target still maps to the same authoritative engine.
815    pub fn remove(&self, target: &SocketAddr) -> Option<EngineState> {
816        let mut inner = self.inner.write().ok()?;
817        let state = compose_cached_state(&inner, target)?;
818        let cached = inner.targets.remove(target)?;
819        remove_orphaned_time(&mut inner, &cached.engine_id);
820        Some(state)
821    }
822
823    /// Clear all cached identities and trusted time.
824    pub fn clear(&self) {
825        if let Ok(mut inner) = self.inner.write() {
826            inner.targets.clear();
827            inner.trusted_times.clear();
828        }
829    }
830
831    /// Get the number of cached target identities (including expired entries).
832    pub fn len(&self) -> usize {
833        self.inner.read().map_or(0, |inner| inner.targets.len())
834    }
835
836    /// Check if the cache is empty.
837    pub fn is_empty(&self) -> bool {
838        self.len() == 0
839    }
840}
841
842fn compose_cached_state(inner: &EngineCacheInner, target: &SocketAddr) -> Option<EngineState> {
843    let cached = inner.targets.get(target)?;
844    Some(EngineState {
845        engine_id: cached.engine_id.clone(),
846        msg_max_size: cached.msg_max_size,
847        trusted_time: inner.trusted_times.get(&cached.engine_id).cloned(),
848    })
849}
850
851fn merge_trusted_time(
852    trusted_times: &mut HashMap<Bytes, TrustedEngineTime>,
853    engine_id: &Bytes,
854    incoming: &TrustedEngineTime,
855) {
856    match trusted_times.get_mut(engine_id) {
857        Some(current) => {
858            current.update_at(
859                incoming.boots,
860                incoming.latest_received_time,
861                incoming.received_at,
862            );
863        }
864        None => {
865            trusted_times.insert(engine_id.clone(), incoming.clone());
866        }
867    }
868}
869
870fn remove_orphaned_time(inner: &mut EngineCacheInner, engine_id: &Bytes) {
871    if !inner
872        .targets
873        .values()
874        .any(|cached| cached.engine_id == engine_id)
875    {
876        inner.trusted_times.remove(engine_id);
877    }
878}
879
880/// Extract engine identity from a discovery response's USM security parameters.
881///
882/// The discovery response carries boots/time too, but this parser deliberately
883/// discards them because the discovery message is unauthenticated.
884pub fn parse_discovery_response(security_params: &Bytes) -> Result<EngineState> {
885    parse_discovery_response_with_limits(
886        security_params,
887        DEFAULT_MSG_MAX_SIZE,
888        DEFAULT_MSG_MAX_SIZE,
889    )
890}
891
892/// Extract engine identity with explicit msgMaxSize and session limit.
893///
894/// The `reported_msg_max_size` comes from the V3 message header (`MsgGlobalData`).
895/// The `session_max` is our transport's maximum message size.
896/// Values are capped to prevent issues with non-compliant agents.
897pub fn parse_discovery_response_with_limits(
898    security_params: &Bytes,
899    reported_msg_max_size: u32,
900    session_max: u32,
901) -> Result<EngineState> {
902    let usm = UsmSecurityParams::decode(security_params.clone())?;
903
904    // RFC 3411 Section 5: a valid SnmpEngineID is 5..=32 octets and is neither
905    // all-zero nor all-0xff. Reject discovery responses carrying an engine ID
906    // outside those bounds (including the empty ID) rather than caching it and
907    // deriving unusable localized keys from it.
908    if validate_engine_id(&usm.engine_id).is_err() {
909        tracing::debug!(target: "async_snmp::engine", { length = usm.engine_id.len() }, "discovery response contained invalid engine ID");
910        return Err(Error::MalformedResponse {
911            target: SocketAddr::from(([0, 0, 0, 0], 0)),
912        }
913        .boxed());
914    }
915
916    Ok(EngineState::discovered(
917        usm.engine_id,
918        cap_msg_max_size(reported_msg_max_size, session_max),
919    ))
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925
926    #[test]
927    fn test_generate_engine_id_is_valid_and_well_formed() {
928        let id = generate_engine_id();
929
930        // Valid length within RFC 3411 5..32 range.
931        assert!((MIN_ENGINE_ID_LEN..=MAX_ENGINE_ID_LEN).contains(&id.len()));
932        validate_engine_id(&id).expect("generated engine ID must validate");
933
934        // High bit of the first octet set -> variable-length format.
935        assert_eq!(id[0] & 0x80, 0x80);
936        // Enterprise number matches the generator's PEN.
937        let enterprise = u32::from_be_bytes([id[0], id[1], id[2], id[3]]);
938        assert_eq!(enterprise, 0x8000_0000 | GENERATED_ENGINE_ID_PEN);
939        // Format octet is "administratively assigned octets".
940        assert_eq!(id[4], ENGINE_ID_FORMAT_OCTETS);
941        // Random suffix present.
942        assert_eq!(id.len(), 5 + GENERATED_ENGINE_ID_RANDOM_LEN);
943    }
944
945    #[test]
946    fn test_generate_engine_id_distinct_across_generations() {
947        let a = generate_engine_id();
948        let b = generate_engine_id();
949        assert_ne!(a, b, "two generated engine IDs must not collide");
950    }
951
952    #[test]
953    fn test_validate_engine_id_rejects_invalid() {
954        // Too short.
955        assert!(validate_engine_id(&[0x80, 0x00, 0x00, 0x01]).is_err());
956        // Too long.
957        assert!(validate_engine_id(&[0x11; MAX_ENGINE_ID_LEN + 1]).is_err());
958        // All zero.
959        assert!(validate_engine_id(&[0x00; 8]).is_err());
960        // All 0xff.
961        assert!(validate_engine_id(&[0xff; 8]).is_err());
962    }
963
964    #[test]
965    fn test_validate_engine_id_accepts_valid() {
966        // Minimum length. The RFC 3411 format layouts are a recommended
967        // generation algorithm, not additional syntax constraints.
968        validate_engine_id(&[0x80, 0x00, 0x00, 0x00, 0x01]).unwrap();
969        // Maximum length, including an enterprise-defined legacy value.
970        validate_engine_id(&[0x22; MAX_ENGINE_ID_LEN]).unwrap();
971        // Typical configured text value.
972        validate_engine_id(b"my-engine").unwrap();
973    }
974
975    #[test]
976    fn test_engine_state_estimated_time() {
977        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
978
979        // Estimated time should be at least engine_time
980        let estimated = state.estimated_time();
981        assert!(estimated >= 1000);
982    }
983
984    #[test]
985    fn test_engine_state_update_time() {
986        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
987
988        // Same boots, newer time -> should update
989        assert!(state.update_time(1, 1100));
990        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1100);
991
992        // Same boots, older time -> should NOT update
993        assert!(!state.update_time(1, 1050));
994        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1100);
995
996        // New boot cycle -> should update
997        assert!(state.update_time(2, 500));
998        assert_eq!(state.trusted_time().unwrap().boots(), 2);
999        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 500);
1000    }
1001
1002    /// Test anti-replay protection via latestReceivedEngineTime (RFC 3414 Section 3.2 Step 7b).
1003    ///
1004    /// The anti-replay mechanism rejects messages with engine time values that are
1005    /// not newer than the latest received time. This prevents replay attacks where
1006    /// an attacker captures and re-sends old authenticated messages.
1007    #[test]
1008    fn test_anti_replay_rejects_old_time() {
1009        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1010        assert!(state.update_time(1, 1500));
1011
1012        // Attempt to replay a message from time 1400 (before latest)
1013        // update_time returns false, indicating the update was rejected
1014        assert!(
1015            !state.update_time(1, 1400),
1016            "Should reject replay: time 1400 < latest 1500"
1017        );
1018        assert_eq!(
1019            state.trusted_time().unwrap().latest_received_time(),
1020            1500,
1021            "Latest should not change"
1022        );
1023
1024        // Even time 1500 (equal) should be rejected - must be strictly greater
1025        assert!(
1026            !state.update_time(1, 1500),
1027            "Should reject replay: time 1500 == latest 1500"
1028        );
1029        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1500);
1030
1031        // Time 1501 (newer) should be accepted
1032        assert!(
1033            state.update_time(1, 1501),
1034            "Should accept: time 1501 > latest 1500"
1035        );
1036        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1501);
1037    }
1038
1039    /// Test anti-replay across boot cycles.
1040    ///
1041    /// A new boot cycle (higher boots value) always resets the `latest_received_engine_time`
1042    /// since the agent has rebooted and time values are relative to the boot.
1043    #[test]
1044    fn test_anti_replay_new_boot_cycle_resets() {
1045        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1046        assert!(state.update_time(1, 5000));
1047
1048        // New boot cycle with lower time value - should accept
1049        // because the engine rebooted (boots increased)
1050        assert!(
1051            state.update_time(2, 100),
1052            "New boot cycle should accept even with lower time"
1053        );
1054        assert_eq!(state.trusted_time().unwrap().boots(), 2);
1055        assert_eq!(state.trusted_time().unwrap().received_time_base(), 100);
1056        assert_eq!(
1057            state.trusted_time().unwrap().latest_received_time(),
1058            100,
1059            "Latest should reset to new time"
1060        );
1061
1062        // Now subsequent updates in the new boot cycle follow normal rules
1063        assert!(
1064            !state.update_time(2, 50),
1065            "Should reject older time in same boot cycle"
1066        );
1067        assert!(state.update_time(2, 150), "Should accept newer time");
1068        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 150);
1069    }
1070
1071    /// Test anti-replay rejects old boot cycles.
1072    ///
1073    /// An attacker cannot replay messages from a previous boot cycle.
1074    #[test]
1075    fn test_anti_replay_rejects_old_boot_cycle() {
1076        let mut state = EngineState::new(Bytes::from_static(b"engine"), 5, 1000);
1077
1078        // Attempt to use old boot cycle (boots=4) - should reject
1079        assert!(
1080            !state.update_time(4, 9999),
1081            "Should reject old boot cycle even with high time"
1082        );
1083        assert_eq!(
1084            state.trusted_time().unwrap().boots(),
1085            5,
1086            "Boots should not change"
1087        );
1088        assert_eq!(
1089            state.trusted_time().unwrap().latest_received_time(),
1090            1000,
1091            "Latest should not change"
1092        );
1093
1094        // Attempt boots=0 - should reject
1095        assert!(!state.update_time(0, 9999), "Should reject boots=0 replay");
1096    }
1097
1098    /// Test anti-replay with exact boundary values.
1099    #[test]
1100    fn test_anti_replay_boundary_values() {
1101        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 0);
1102
1103        // Start with time=0
1104        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 0);
1105
1106        // Time=1 should be accepted (> 0)
1107        assert!(state.update_time(1, 1));
1108        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1);
1109
1110        // Time=0 should be rejected (< 1)
1111        assert!(!state.update_time(1, 0));
1112
1113        // The largest pre-rollover time can be accepted.
1114        assert!(state.update_time(1, MAX_ENGINE_TIME - 1));
1115        assert_eq!(
1116            state.trusted_time().unwrap().latest_received_time(),
1117            MAX_ENGINE_TIME - 1
1118        );
1119
1120        // The maximum is the final representable high-water value in this boot.
1121        assert!(state.update_time(1, MAX_ENGINE_TIME));
1122        assert_eq!(state.estimated_boots_time(), (1, MAX_ENGINE_TIME));
1123        assert!(!state.update_time(1, MAX_ENGINE_TIME));
1124    }
1125
1126    #[test]
1127    fn test_engine_state_time_window() {
1128        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1129
1130        // Same boots, within window
1131        assert!(state.is_in_time_window(1, 1000));
1132        assert!(state.is_in_time_window(1, 1100)); // +100s
1133        assert!(state.is_in_time_window(1, 900)); // -100s
1134
1135        // Different boots -> out of window
1136        assert!(!state.is_in_time_window(2, 1000));
1137        assert!(!state.is_in_time_window(0, 1000));
1138
1139        // Way outside time window
1140        assert!(!state.is_in_time_window(1, 2000)); // +1000s > 150s
1141    }
1142
1143    /// Test the exact 150-second time window boundary per RFC 3414 Section 2.2.3.
1144    ///
1145    /// The time window is exactly 150 seconds. Messages with time difference
1146    /// of exactly 150 seconds should be accepted, but 151 seconds should fail.
1147    #[test]
1148    fn test_time_window_150s_exact_boundary() {
1149        // Use high engine_time to avoid underflow complications
1150        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 10000);
1151
1152        // At exactly +150 seconds from engine_time (10000 + 150 = 10150)
1153        // The is_in_time_window compares against estimated_time(), which adds
1154        // elapsed time. For a fresh EngineState, elapsed should be ~0.
1155        // So msg_time of 10150 should be within window (diff = 150 <= TIME_WINDOW)
1156        assert!(
1157            state.is_in_time_window(1, 10150),
1158            "Message at exactly +150s boundary should be in window"
1159        );
1160
1161        // At exactly +151 seconds (diff = 151 > TIME_WINDOW = 150)
1162        assert!(
1163            !state.is_in_time_window(1, 10151),
1164            "Message at +151s should be outside window"
1165        );
1166
1167        // At exactly -150 seconds (10000 - 150 = 9850)
1168        assert!(
1169            state.is_in_time_window(1, 9850),
1170            "Message at exactly -150s boundary should be in window"
1171        );
1172
1173        // At exactly -151 seconds (10000 - 151 = 9849)
1174        assert!(
1175            !state.is_in_time_window(1, 9849),
1176            "Message at -151s should be outside window"
1177        );
1178    }
1179
1180    /// Test time window with maximum engine boots value (2_147_483_647).
1181    ///
1182    /// Per RFC 3414 Section 2.2.3, when snmpEngineBoots is 2_147_483_647 (latched),
1183    /// all messages should be rejected as outside the time window.
1184    #[test]
1185    fn test_time_window_boots_latched() {
1186        // Maximum boots value indicates the engine has been rebooted too many times
1187        // and should reject all authenticated messages
1188        let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
1189
1190        // Even with matching boots and same time, should fail when latched
1191        assert!(
1192            !state.is_in_time_window(2_147_483_647, 1000),
1193            "Latched boots should reject all messages"
1194        );
1195
1196        // Any other time should also fail
1197        assert!(!state.is_in_time_window(2_147_483_647, 1100));
1198        assert!(!state.is_in_time_window(2_147_483_647, 900));
1199    }
1200
1201    /// Test time window edge cases with boot counter differences.
1202    ///
1203    /// Boot counter must match exactly; any difference means out of window.
1204    #[test]
1205    fn test_time_window_boots_mismatch() {
1206        let state = EngineState::new(Bytes::from_static(b"engine"), 100, 1000);
1207
1208        // Boots too high
1209        assert!(!state.is_in_time_window(101, 1000));
1210        assert!(!state.is_in_time_window(200, 1000));
1211
1212        // Boots too low (replay from previous boot cycle)
1213        assert!(!state.is_in_time_window(99, 1000));
1214        assert!(!state.is_in_time_window(0, 1000));
1215    }
1216
1217    /// Non-authoritative timeliness (RFC 3414 Section 3.2 Step 7b): a message
1218    /// with time within the window is accepted without updating the LCD.
1219    #[test]
1220    fn test_check_and_update_timeliness_within_window_accepted() {
1221        let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1222
1223        // Older time but within 150s of our notion: accepted, latest unchanged
1224        assert!(state.check_and_update_timeliness(3, 900));
1225        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1000);
1226
1227        // Exactly at the boundary (1000 - 150 = 850): accepted
1228        assert!(state.check_and_update_timeliness(3, 850));
1229    }
1230
1231    #[test]
1232    fn test_check_and_update_timeliness_controllable_boundary_without_rollback() {
1233        let now = Instant::now();
1234        let mut at_boundary = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1235        at_boundary.trusted_time.as_mut().unwrap().received_at = now;
1236        assert!(at_boundary.check_and_update_timeliness_at(3, 950, now + Duration::from_secs(100)));
1237        assert_eq!(
1238            at_boundary.trusted_time().unwrap().latest_received_time(),
1239            1000,
1240            "an older in-window message must not lower the high-water mark"
1241        );
1242
1243        let mut outside = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1244        outside.trusted_time.as_mut().unwrap().received_at = now;
1245        assert!(!outside.check_and_update_timeliness_at(3, 949, now + Duration::from_secs(100)));
1246        assert_eq!(outside.trusted_time().unwrap().latest_received_time(), 1000);
1247    }
1248
1249    #[test]
1250    fn test_check_and_update_timeliness_newer_time_updates_lcd() {
1251        let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1252
1253        assert!(state.check_and_update_timeliness(3, 1200));
1254        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1200);
1255        assert_eq!(state.trusted_time().unwrap().received_time_base(), 1200);
1256    }
1257
1258    #[test]
1259    fn test_check_and_update_timeliness_stale_time_rejected() {
1260        let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1261
1262        // 500 < 1000 - 150: replayed/stale message
1263        assert!(!state.check_and_update_timeliness(3, 500));
1264        // Just past the boundary
1265        assert!(!state.check_and_update_timeliness(3, 849));
1266    }
1267
1268    #[test]
1269    fn test_check_and_update_timeliness_old_boots_rejected() {
1270        let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1271
1272        assert!(!state.check_and_update_timeliness(2, 5000));
1273        assert_eq!(
1274            state.trusted_time().unwrap().boots(),
1275            3,
1276            "old boot cycle must not update LCD"
1277        );
1278    }
1279
1280    #[test]
1281    fn test_check_and_update_timeliness_reboot_accepted() {
1282        let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
1283
1284        // Sender rebooted: higher boots with low time is accepted and updates LCD
1285        assert!(state.check_and_update_timeliness(4, 10));
1286        assert_eq!(state.trusted_time().unwrap().boots(), 4);
1287        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 10);
1288
1289        // Messages from the previous boot cycle are now rejected
1290        assert!(!state.check_and_update_timeliness(3, 99999));
1291    }
1292
1293    #[test]
1294    fn test_check_and_update_timeliness_latched_boots_rejected() {
1295        let mut state = EngineState::new(Bytes::from_static(b"engine"), MAX_ENGINE_TIME, 1000);
1296
1297        assert!(!state.check_and_update_timeliness(MAX_ENGINE_TIME, 1000));
1298    }
1299
1300    #[test]
1301    fn test_engine_cache_basic_operations() {
1302        let cache = EngineCache::new();
1303        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1304
1305        // Initially empty
1306        assert!(cache.is_empty());
1307        assert!(cache.get(&addr).is_none());
1308
1309        // Insert
1310        let state = EngineState::new(Bytes::from_static(b"engine1"), 1, 1000);
1311        cache.insert(addr, state);
1312
1313        assert_eq!(cache.len(), 1);
1314        assert!(!cache.is_empty());
1315
1316        // Get
1317        let retrieved = cache.get(&addr).unwrap();
1318        assert_eq!(retrieved.engine_id.as_ref(), b"engine1");
1319        assert_eq!(retrieved.trusted_time().unwrap().boots(), 1);
1320
1321        // Update time
1322        assert!(cache.update_time(&addr, 1, 1100));
1323
1324        // Remove
1325        let removed = cache.remove(&addr).unwrap();
1326        assert_eq!(removed.trusted_time().unwrap().latest_received_time(), 1100);
1327        assert!(cache.is_empty());
1328    }
1329
1330    #[test]
1331    fn test_engine_cache_explicit_replacement_latches_new_identity() {
1332        let cache = EngineCache::new();
1333        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1334        let shared_addr: SocketAddr = "192.168.1.2:161".parse().unwrap();
1335        let old = EngineState::discovered(Bytes::from_static(b"old-engine"), 1400);
1336        let new = EngineState::discovered(Bytes::from_static(b"new-engine"), 1500);
1337        let shared = EngineState::new(Bytes::from_static(b"new-engine"), 7, 500);
1338
1339        cache.insert(addr, old.clone());
1340        cache.insert(shared_addr, shared);
1341        let replaced = cache.replace_target(addr, new).unwrap();
1342        cache.insert(addr, old);
1343
1344        assert_eq!(replaced.engine_id().as_ref(), b"new-engine");
1345        let trusted = replaced.trusted_time().unwrap();
1346        assert_eq!((trusted.boots(), trusted.latest_received_time()), (7, 500));
1347
1348        let cached = cache.get(&addr).unwrap();
1349        assert_eq!(cached.engine_id().as_ref(), b"new-engine");
1350        assert_eq!(cached.msg_max_size, 1500);
1351    }
1352
1353    #[test]
1354    fn test_engine_cache_shares_trusted_time_by_engine_id() {
1355        let cache = EngineCache::new();
1356        let addr1: SocketAddr = "192.168.1.1:161".parse().unwrap();
1357        let addr2: SocketAddr = "192.168.1.2:161".parse().unwrap();
1358        let engine_id = Bytes::from_static(b"shared-engine");
1359
1360        cache.insert(addr1, EngineState::discovered(engine_id.clone(), 1400));
1361        cache.insert(addr2, EngineState::discovered(engine_id, 1500));
1362        assert!(cache.update_time(&addr1, 4, 500));
1363
1364        let state2 = cache.get(&addr2).unwrap();
1365        let trusted = state2.trusted_time().unwrap();
1366        assert_eq!((trusted.boots(), trusted.latest_received_time()), (4, 500));
1367        assert_eq!(state2.msg_max_size, 1500);
1368    }
1369
1370    #[test]
1371    fn test_engine_cache_stale_clone_cannot_overwrite_newer_time() {
1372        let cache = EngineCache::new();
1373        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1374        let engine_id = Bytes::from_static(b"engine1");
1375
1376        cache.insert(addr, EngineState::new(engine_id.clone(), 7, 500));
1377        cache.insert(addr, EngineState::new(engine_id.clone(), 6, 9000));
1378        cache.insert(addr, EngineState::discovered(engine_id, 1400));
1379
1380        let state = cache.get(&addr).unwrap();
1381        let trusted = state.trusted_time().unwrap();
1382        assert_eq!((trusted.boots(), trusted.latest_received_time()), (7, 500));
1383    }
1384
1385    #[test]
1386    fn test_engine_cache_concurrent_updates_converge_monotonically() {
1387        use std::sync::Arc;
1388
1389        let cache = Arc::new(EngineCache::new());
1390        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1391        cache.insert(
1392            addr,
1393            EngineState::discovered(Bytes::from_static(b"engine1"), 1400),
1394        );
1395
1396        let older = Arc::clone(&cache);
1397        let newer = Arc::clone(&cache);
1398        let older_task = std::thread::spawn(move || {
1399            for _ in 0..100 {
1400                older.update_time(&addr, 4, 9000);
1401            }
1402        });
1403        let newer_task = std::thread::spawn(move || {
1404            for _ in 0..100 {
1405                newer.update_time(&addr, 5, 10);
1406            }
1407        });
1408        older_task.join().unwrap();
1409        newer_task.join().unwrap();
1410
1411        let state = cache.get(&addr).unwrap();
1412        let trusted = state.trusted_time().unwrap();
1413        assert_eq!((trusted.boots(), trusted.latest_received_time()), (5, 10));
1414    }
1415
1416    #[test]
1417    fn test_engine_cache_ttl_expiry() {
1418        let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
1419        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1420        let now = Instant::now();
1421
1422        let state = EngineState::new(Bytes::from_static(b"engine1"), 1, 1000);
1423        cache.insert_at(addr, state, now);
1424        assert!(cache.get_at(&addr, now + Duration::from_secs(5)).is_some());
1425        assert!(
1426            cache.get_at(&addr, now + Duration::from_secs(6)).is_none(),
1427            "expired entry should return None"
1428        );
1429        assert!(cache.is_empty(), "expired entry should be removed");
1430    }
1431
1432    #[test]
1433    fn test_engine_cache_ttl_refresh_on_every_accepted_authenticated_message() {
1434        let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
1435        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1436        let now = Instant::now();
1437        let engine_id = Bytes::from_static(b"engine1");
1438        let local_state = EngineState::new(engine_id.clone(), 1, 1000);
1439
1440        cache.insert_at(addr, local_state.clone(), now);
1441        let (timely, _) = cache
1442            .check_and_update_timeliness_at(
1443                &addr,
1444                &local_state,
1445                &engine_id,
1446                1,
1447                900,
1448                now + Duration::from_secs(4),
1449            )
1450            .unwrap();
1451        assert!(timely, "older in-window input remains acceptable");
1452        assert!(
1453            cache.get_at(&addr, now + Duration::from_secs(8)).is_some(),
1454            "accepted authenticated input must refresh TTL without advancing high-water"
1455        );
1456    }
1457
1458    #[test]
1459    fn test_engine_cache_live_state_prevents_rebuilt_cache_from_accepting_old_boots() {
1460        let cache = EngineCache::new();
1461        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1462        let now = Instant::now();
1463        let engine_id = Bytes::from_static(b"engine1");
1464        let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
1465        local_state.trusted_time.as_mut().unwrap().received_at = now;
1466
1467        cache.insert_at(addr, EngineState::discovered(engine_id.clone(), 1400), now);
1468        let (timely, canonical) = cache
1469            .check_and_update_timeliness_at(
1470                &addr,
1471                &local_state,
1472                &engine_id,
1473                4,
1474                5000,
1475                now + Duration::from_secs(1),
1476            )
1477            .unwrap();
1478
1479        assert!(!timely, "rebuilt cache must not weaken live client state");
1480        let trusted = canonical.trusted_time().unwrap();
1481        assert_eq!((trusted.boots(), trusted.latest_received_time()), (5, 1000));
1482    }
1483
1484    #[test]
1485    fn test_engine_cache_rejected_message_does_not_refresh_existing_entry() {
1486        let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
1487        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1488        let now = Instant::now();
1489        let engine_id = Bytes::from_static(b"engine1");
1490        let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
1491        local_state.trusted_time.as_mut().unwrap().received_at = now;
1492
1493        cache.insert_at(addr, local_state.clone(), now);
1494        let (timely, _) = cache
1495            .check_and_update_timeliness_at(
1496                &addr,
1497                &local_state,
1498                &engine_id,
1499                4,
1500                5000,
1501                now + Duration::from_secs(4),
1502            )
1503            .unwrap();
1504        assert!(!timely);
1505        assert!(cache.get_at(&addr, now + Duration::from_secs(6)).is_none());
1506    }
1507
1508    #[test]
1509    fn test_engine_cache_rejected_message_does_not_resurrect_expired_entry() {
1510        let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
1511        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1512        let now = Instant::now();
1513        let engine_id = Bytes::from_static(b"engine1");
1514        let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
1515        local_state.trusted_time.as_mut().unwrap().received_at = now;
1516
1517        cache.insert_at(addr, local_state.clone(), now);
1518        let (timely, _) = cache
1519            .check_and_update_timeliness_at(
1520                &addr,
1521                &local_state,
1522                &engine_id,
1523                4,
1524                5000,
1525                now + Duration::from_secs(6),
1526            )
1527            .unwrap();
1528        assert!(!timely);
1529        assert!(cache.get_at(&addr, now + Duration::from_secs(6)).is_none());
1530        assert!(cache.is_empty());
1531    }
1532
1533    #[test]
1534    fn test_engine_cache_max_capacity_eviction() {
1535        let cache = EngineCache::new().with_max_capacity(2);
1536        let addr1: SocketAddr = "192.168.1.1:161".parse().unwrap();
1537        let addr2: SocketAddr = "192.168.1.2:161".parse().unwrap();
1538        let addr3: SocketAddr = "192.168.1.3:161".parse().unwrap();
1539
1540        let now = Instant::now();
1541        cache.insert_at(
1542            addr1,
1543            EngineState::new(Bytes::from_static(b"e1"), 1, 100),
1544            now,
1545        );
1546        cache.insert_at(
1547            addr2,
1548            EngineState::new(Bytes::from_static(b"e2"), 1, 200),
1549            now + Duration::from_secs(1),
1550        );
1551
1552        assert_eq!(cache.len(), 2);
1553
1554        // Third insert should evict the least recently refreshed target.
1555        cache.insert_at(
1556            addr3,
1557            EngineState::new(Bytes::from_static(b"e3"), 1, 300),
1558            now + Duration::from_secs(2),
1559        );
1560        assert_eq!(cache.len(), 2);
1561        assert!(
1562            cache.get(&addr1).is_none(),
1563            "oldest entry should be evicted"
1564        );
1565        assert!(cache.get(&addr2).is_some());
1566        assert!(cache.get(&addr3).is_some());
1567    }
1568
1569    #[test]
1570    fn test_parse_discovery_response() {
1571        let usm = UsmSecurityParams::new(b"test-engine-id".as_slice(), 42, 12345, b"".as_slice());
1572        let encoded = usm.encode();
1573
1574        let state = parse_discovery_response(&encoded).unwrap();
1575        assert_eq!(state.engine_id.as_ref(), b"test-engine-id");
1576        assert!(state.trusted_time().is_none());
1577        assert_eq!(state.estimated_boots_time(), (0, 0));
1578    }
1579
1580    #[test]
1581    fn test_parse_discovery_response_empty_engine_id() {
1582        let usm = UsmSecurityParams::empty();
1583        let encoded = usm.encode();
1584
1585        let result = parse_discovery_response(&encoded);
1586        assert!(matches!(
1587            *result.unwrap_err(),
1588            Error::MalformedResponse { .. }
1589        ));
1590    }
1591
1592    #[test]
1593    fn test_parse_discovery_response_rejects_invalid_engine_id() {
1594        // Too short (< 5 octets).
1595        let usm = UsmSecurityParams::new(b"abcd".as_slice(), 1, 1, b"".as_slice());
1596        assert!(matches!(
1597            *parse_discovery_response(&usm.encode()).unwrap_err(),
1598            Error::MalformedResponse { .. }
1599        ));
1600
1601        // All-zero engine ID of otherwise valid length.
1602        let usm = UsmSecurityParams::new([0u8; 8].as_slice(), 1, 1, b"".as_slice());
1603        assert!(matches!(
1604            *parse_discovery_response(&usm.encode()).unwrap_err(),
1605            Error::MalformedResponse { .. }
1606        ));
1607
1608        // All-0xff engine ID of otherwise valid length.
1609        let usm = UsmSecurityParams::new([0xffu8; 8].as_slice(), 1, 1, b"".as_slice());
1610        assert!(matches!(
1611            *parse_discovery_response(&usm.encode()).unwrap_err(),
1612            Error::MalformedResponse { .. }
1613        ));
1614    }
1615
1616    // ========================================================================
1617    // Engine Boots Overflow Tests (RFC 3414 Section 2.2.3)
1618    // ========================================================================
1619
1620    /// Test that `update_time` accepts transition to maximum boots value.
1621    ///
1622    /// When the engine reboots and boots reaches 2_147_483_647 (`i32::MAX`),
1623    /// the update should be accepted since it's a valid new boot cycle.
1624    #[test]
1625    fn test_engine_boots_transition_to_max() {
1626        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_646, 1000);
1627
1628        // Boot cycle to max value should be accepted
1629        assert!(
1630            state.update_time(2_147_483_647, 100),
1631            "Transition to boots=2_147_483_647 should be accepted"
1632        );
1633        assert_eq!(state.trusted_time().unwrap().boots(), 2_147_483_647);
1634        assert_eq!(state.trusted_time().unwrap().received_time_base(), 100);
1635    }
1636
1637    /// Test `update_time` behavior when boots is latched.
1638    ///
1639    /// The `update_time` function still tracks received times for anti-replay
1640    /// purposes. The security rejection happens in `is_in_time_window()`.
1641    /// However, when boots=2_147_483_647, there's no valid "higher" boots value,
1642    /// so boot cycle transitions are impossible.
1643    #[test]
1644    fn test_engine_boots_latched_update_behavior() {
1645        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
1646
1647        // Time tracking still works for same boots
1648        assert!(
1649            state.update_time(2_147_483_647, 2000),
1650            "Time tracking updates should still work"
1651        );
1652        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 2000);
1653
1654        // Old time rejected per normal anti-replay
1655        assert!(!state.update_time(2_147_483_647, 1500));
1656        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 2000);
1657
1658        // The key security check is in is_in_time_window
1659        assert!(
1660            !state.is_in_time_window(2_147_483_647, 2000),
1661            "Latched state should still reject all messages"
1662        );
1663    }
1664
1665    /// Test that time window rejects all messages when boots is latched.
1666    ///
1667    /// This is the key security property: once an engine's boots counter
1668    /// reaches its maximum value, all authenticated messages should be
1669    /// rejected to prevent replay attacks.
1670    #[test]
1671    fn test_engine_boots_latched_time_window_always_fails() {
1672        let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
1673
1674        // All time values should fail when latched
1675        assert!(!state.is_in_time_window(2_147_483_647, 0));
1676        assert!(!state.is_in_time_window(2_147_483_647, 1000));
1677        assert!(!state.is_in_time_window(2_147_483_647, 1001));
1678        assert!(!state.is_in_time_window(2_147_483_647, u32::MAX));
1679
1680        // Even previous boots values should fail
1681        assert!(!state.is_in_time_window(2_147_483_646, 1000));
1682        assert!(!state.is_in_time_window(0, 1000));
1683    }
1684
1685    /// Test creating `EngineState` directly with latched boots value.
1686    ///
1687    /// An agent that has been running for a very long time might already
1688    /// be in the latched state when we first discover it.
1689    #[test]
1690    fn test_engine_state_created_latched() {
1691        let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 5000);
1692
1693        assert_eq!(state.trusted_time().unwrap().boots(), 2_147_483_647);
1694        assert_eq!(state.trusted_time().unwrap().received_time_base(), 5000);
1695        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 5000);
1696
1697        // Should immediately be in latched state
1698        assert!(
1699            !state.is_in_time_window(2_147_483_647, 5000),
1700            "Newly created latched engine should reject all messages"
1701        );
1702    }
1703
1704    /// Test that boots values near the maximum work correctly.
1705    ///
1706    /// Verify normal operation just before reaching the latch point.
1707    #[test]
1708    fn test_engine_boots_near_max_operates_normally() {
1709        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_645, 1000);
1710
1711        // Normal time window checks should work
1712        assert!(state.is_in_time_window(2_147_483_645, 1000));
1713        assert!(state.is_in_time_window(2_147_483_645, 1100));
1714        assert!(!state.is_in_time_window(2_147_483_645, 1200)); // Outside 150s window
1715
1716        // Should accept boot to 2_147_483_646
1717        assert!(state.update_time(2_147_483_646, 500));
1718        assert_eq!(state.trusted_time().unwrap().boots(), 2_147_483_646);
1719        assert!(state.is_in_time_window(2_147_483_646, 500));
1720
1721        // Should accept boot to 2_147_483_647 (becomes latched)
1722        assert!(state.update_time(2_147_483_647, 100));
1723        assert_eq!(state.trusted_time().unwrap().boots(), 2_147_483_647);
1724
1725        // Now latched - all messages rejected
1726        assert!(!state.is_in_time_window(2_147_483_647, 100));
1727    }
1728
1729    /// Test that `update_time` correctly handles the comparison when
1730    /// current boots is high but not yet latched.
1731    #[test]
1732    fn test_engine_boots_high_value_update_logic() {
1733        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_640, 1000);
1734
1735        // Old boot cycles should be rejected
1736        assert!(!state.update_time(2147483639, 9999));
1737        assert!(!state.update_time(0, 9999));
1738
1739        // Same boot, older time should be rejected
1740        assert!(!state.update_time(2_147_483_640, 500));
1741
1742        // Same boot, newer time should be accepted
1743        assert!(state.update_time(2_147_483_640, 1500));
1744        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 1500);
1745
1746        // New boot should be accepted
1747        assert!(state.update_time(2_147_483_641, 100));
1748        assert_eq!(state.trusted_time().unwrap().boots(), 2_147_483_641);
1749    }
1750
1751    /// Test `EngineCache` behavior with latched engines.
1752    ///
1753    /// Even when latched, time tracking updates are accepted (for anti-replay).
1754    /// The security rejection is enforced by `is_in_time_window()`, not `update_time()`.
1755    #[test]
1756    fn test_engine_cache_latched_engine() {
1757        let cache = EngineCache::new();
1758        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
1759
1760        // Insert latched engine
1761        cache.insert(
1762            addr,
1763            EngineState::new(Bytes::from_static(b"latched"), 2_147_483_647, 1000),
1764        );
1765
1766        // Time tracking still works
1767        assert!(
1768            cache.update_time(&addr, 2_147_483_647, 2000),
1769            "Time tracking should update even for latched engine"
1770        );
1771
1772        // Verify state was updated
1773        let state = cache.get(&addr).unwrap();
1774        assert_eq!(state.trusted_time().unwrap().latest_received_time(), 2000);
1775
1776        // But the key security property: is_in_time_window rejects
1777        assert!(
1778            !state.is_in_time_window(2_147_483_647, 2000),
1779            "Latched engine should reject all time window checks"
1780        );
1781    }
1782
1783    // ========================================================================
1784    // msgMaxSize Capping Tests
1785    // ========================================================================
1786    //
1787    // Per net-snmp behavior, agent-reported msgMaxSize values should be capped
1788    // to the session's maximum to prevent buffer issues with non-compliant agents.
1789
1790    /// Test that `EngineState` stores the agent's advertised msgMaxSize.
1791    ///
1792    /// The `msg_max_size` field tracks the maximum message size the remote engine
1793    /// can accept, as reported in `SNMPv3` message headers.
1794    #[test]
1795    fn test_engine_state_stores_msg_max_size() {
1796        let state = EngineState::with_msg_max_size(Bytes::from_static(b"engine"), 1, 1000, 65507);
1797        assert_eq!(state.msg_max_size, 65507);
1798    }
1799
1800    /// Test that the default constructor uses the maximum UDP message size.
1801    ///
1802    /// When msgMaxSize is not provided (e.g., during basic discovery),
1803    /// default to the maximum safe UDP datagram size (65507 bytes).
1804    #[test]
1805    fn test_engine_state_default_msg_max_size() {
1806        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1807        assert_eq!(
1808            state.msg_max_size, DEFAULT_MSG_MAX_SIZE,
1809            "Default msg_max_size should be the maximum UDP datagram size"
1810        );
1811    }
1812
1813    /// Test that msgMaxSize is capped to session maximum.
1814    ///
1815    /// Non-compliant agents may advertise msgMaxSize values larger than they
1816    /// (or we) can actually handle. Values exceeding the session maximum are
1817    /// silently capped to prevent buffer issues.
1818    #[test]
1819    fn test_engine_state_msg_max_size_capped_to_session_max() {
1820        // Agent advertises 2GB, but we cap to 65507 (our session max)
1821        let state = EngineState::with_msg_max_size_capped(
1822            Bytes::from_static(b"engine"),
1823            1,
1824            1000,
1825            2_000_000_000, // Agent claims 2GB
1826            65507,         // Our session maximum
1827        );
1828        assert_eq!(
1829            state.msg_max_size, 65507,
1830            "msg_max_size should be capped to session maximum"
1831        );
1832    }
1833
1834    /// Test that msgMaxSize within session maximum is not modified.
1835    ///
1836    /// When the agent advertises a reasonable value below our maximum,
1837    /// it should be stored as-is without capping.
1838    #[test]
1839    fn test_engine_state_msg_max_size_within_limit_not_capped() {
1840        let state = EngineState::with_msg_max_size_capped(
1841            Bytes::from_static(b"engine"),
1842            1,
1843            1000,
1844            1472,  // Agent claims 1472 (Ethernet MTU - headers)
1845            65507, // Our session maximum
1846        );
1847        assert_eq!(
1848            state.msg_max_size, 1472,
1849            "msg_max_size within limit should not be capped"
1850        );
1851    }
1852
1853    /// Test msgMaxSize capping at exact boundary.
1854    ///
1855    /// When agent's msgMaxSize exactly equals session maximum, no capping occurs.
1856    #[test]
1857    fn test_engine_state_msg_max_size_at_exact_boundary() {
1858        let state = EngineState::with_msg_max_size_capped(
1859            Bytes::from_static(b"engine"),
1860            1,
1861            1000,
1862            65507, // Exactly at session max
1863            65507, // Our session maximum
1864        );
1865        assert_eq!(state.msg_max_size, 65507);
1866    }
1867
1868    /// Test msgMaxSize capping with TCP transport maximum.
1869    ///
1870    /// TCP transports may have higher limits. Verify capping works with
1871    /// the larger TCP message size limit.
1872    #[test]
1873    fn test_engine_state_msg_max_size_tcp_limit() {
1874        const TCP_MAX: u32 = 0x7FFF_FFFF; // net-snmp TCP maximum
1875
1876        // Agent claims i32::MAX, we have same limit
1877        let state = EngineState::with_msg_max_size_capped(
1878            Bytes::from_static(b"engine"),
1879            1,
1880            1000,
1881            TCP_MAX,
1882            TCP_MAX,
1883        );
1884        assert_eq!(state.msg_max_size, TCP_MAX);
1885
1886        // Agent claims more than i32::MAX (wrapped negative), cap to limit
1887        let state = EngineState::with_msg_max_size_capped(
1888            Bytes::from_static(b"engine"),
1889            1,
1890            1000,
1891            u32::MAX, // Larger than any valid msgMaxSize
1892            TCP_MAX,
1893        );
1894        assert_eq!(
1895            state.msg_max_size, TCP_MAX,
1896            "Values exceeding session max should be capped"
1897        );
1898    }
1899
1900    /// Test that `EngineState::new` uses the default `msg_max_size` constant.
1901    #[test]
1902    fn test_engine_state_new_uses_default_constant() {
1903        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1904
1905        // DEFAULT_MSG_MAX_SIZE is the maximum UDP payload (65507)
1906        assert_eq!(state.msg_max_size, DEFAULT_MSG_MAX_SIZE);
1907    }
1908
1909    // ========================================================================
1910    // Engine Time Overflow Tests (RFC 3414 Section 2.2.1)
1911    // ========================================================================
1912    //
1913    // Per RFC 3414, snmpEngineTime is a 31-bit value (0..2_147_483_647).
1914    // When the time value would exceed this, it must not go beyond MAX_ENGINE_TIME.
1915
1916    /// Test that `estimated_time` caps at `MAX_ENGINE_TIME` (2^31-1).
1917    ///
1918    /// Per RFC 3414 Section 2.2.1, snmpEngineTime is 31-bit (0..2_147_483_647).
1919    /// If time would exceed this value, it should cap at `MAX_ENGINE_TIME` rather
1920    /// than continuing to `u32::MAX`.
1921    #[test]
1922    fn test_estimated_time_caps_at_max_engine_time() {
1923        // Create state with engine_time near the maximum
1924        let state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME - 10);
1925
1926        // Even though we're adding elapsed time, result should never exceed MAX_ENGINE_TIME
1927        let estimated = state.estimated_time();
1928        assert!(
1929            estimated <= MAX_ENGINE_TIME,
1930            "estimated_time() should never exceed MAX_ENGINE_TIME ({MAX_ENGINE_TIME}), got {estimated}"
1931        );
1932    }
1933
1934    /// The maximum time value is representable; rollover occurs one second later.
1935    #[test]
1936    fn test_estimated_pair_rolls_after_max_engine_time() {
1937        let now = Instant::now();
1938        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 0);
1939        state.trusted_time.as_mut().unwrap().received_at = now;
1940
1941        assert_eq!(
1942            state.estimated_boots_time_at(now + Duration::from_secs(u64::from(MAX_ENGINE_TIME))),
1943            (1, MAX_ENGINE_TIME)
1944        );
1945        assert_eq!(
1946            state
1947                .estimated_boots_time_at(now + Duration::from_secs(u64::from(MAX_ENGINE_TIME) + 1)),
1948            (2, 0)
1949        );
1950    }
1951
1952    #[test]
1953    fn test_max_engine_time_tuple_remains_timely() {
1954        let now = Instant::now();
1955        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME);
1956        state.trusted_time.as_mut().unwrap().received_at = now;
1957
1958        assert!(state.check_and_update_timeliness_at(1, MAX_ENGINE_TIME, now));
1959        assert_eq!(state.estimated_boots_time_at(now), (1, MAX_ENGINE_TIME));
1960    }
1961
1962    /// Test that `engine_time` values beyond `MAX_ENGINE_TIME` are invalid.
1963    ///
1964    /// This verifies the constant value is correct per RFC 3414.
1965    #[test]
1966    fn test_max_engine_time_constant() {
1967        // RFC 3414 specifies 31-bit (0..2_147_483_647), which is i32::MAX
1968        assert_eq!(MAX_ENGINE_TIME, 2_147_483_647);
1969        assert_eq!(MAX_ENGINE_TIME, i32::MAX as u32);
1970    }
1971
1972    /// Test that normal time estimation works below `MAX_ENGINE_TIME`.
1973    ///
1974    /// For typical time values well below the maximum, estimation should
1975    /// work normally without artificial capping.
1976    #[test]
1977    fn test_estimated_time_normal_operation() {
1978        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1979
1980        // For a fresh state, elapsed should be ~0, so estimated should be ~engine_time
1981        let estimated = state.estimated_time();
1982        assert!(
1983            estimated >= 1000,
1984            "estimated_time() should be at least engine_time"
1985        );
1986        // Should not hit the cap
1987        assert!(
1988            estimated < MAX_ENGINE_TIME,
1989            "Normal time values should not hit MAX_ENGINE_TIME cap"
1990        );
1991    }
1992}