async_snmp/v3/
engine.rs

1//! Engine discovery and time synchronization (RFC 3414 Section 4).
2//!
3//! SNMPv3 requires knowing the authoritative engine's ID, boots counter,
4//! and time value before authenticated messages can be sent. This module
5//! provides:
6//!
7//! - `EngineCache`: Thread-safe cache of discovered engine state
8//! - `EngineState`: Per-engine state (ID, boots, 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. Response's USM params contain the engine ID, boots, and time
16//! 4. Client caches these values for subsequent authenticated requests
17//!
18//! # Time Synchronization
19//!
20//! Per RFC 3414 Section 2.3, a non-authoritative engine (client) maintains:
21//! - `snmpEngineBoots`: Boot counter from authoritative engine
22//! - `snmpEngineTime`: Time value from authoritative engine
23//! - `latestReceivedEngineTime`: Highest time received (anti-replay)
24//!
25//! The time window is 150 seconds. Messages outside this window are rejected.
26
27use std::collections::HashMap;
28use std::net::SocketAddr;
29use std::sync::RwLock;
30use std::time::Instant;
31
32use bytes::Bytes;
33
34use crate::error::{Error, Result};
35use crate::v3::UsmSecurityParams;
36
37/// Time window in seconds (RFC 3414 Section 2.2.3).
38pub const TIME_WINDOW: u32 = 150;
39
40/// Maximum valid snmpEngineTime value (RFC 3414 Section 2.2.1).
41///
42/// Per RFC 3414, snmpEngineTime is a 31-bit value (0..2147483647).
43/// When the value reaches this maximum, the authoritative engine should
44/// reset it to zero and increment snmpEngineBoots.
45pub const MAX_ENGINE_TIME: u32 = 2147483647;
46
47/// Default msgMaxSize for UDP transport (65535 - 20 IPv4 - 8 UDP = 65507).
48pub const DEFAULT_MSG_MAX_SIZE: u32 = 65507;
49
50/// USM statistics OIDs used in Report PDUs.
51pub mod report_oids {
52    use crate::Oid;
53    use crate::oid;
54
55    /// 1.3.6.1.6.3.15.1.1.1.0 - usmStatsUnsupportedSecLevels
56    pub fn unsupported_sec_levels() -> Oid {
57        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0)
58    }
59
60    /// 1.3.6.1.6.3.15.1.1.2.0 - usmStatsNotInTimeWindows
61    pub fn not_in_time_windows() -> Oid {
62        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 2, 0)
63    }
64
65    /// 1.3.6.1.6.3.15.1.1.3.0 - usmStatsUnknownUserNames
66    pub fn unknown_user_names() -> Oid {
67        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 3, 0)
68    }
69
70    /// 1.3.6.1.6.3.15.1.1.4.0 - usmStatsUnknownEngineIDs
71    pub fn unknown_engine_ids() -> Oid {
72        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 4, 0)
73    }
74
75    /// 1.3.6.1.6.3.15.1.1.5.0 - usmStatsWrongDigests
76    pub fn wrong_digests() -> Oid {
77        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0)
78    }
79
80    /// 1.3.6.1.6.3.15.1.1.6.0 - usmStatsDecryptionErrors
81    pub fn decryption_errors() -> Oid {
82        oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 6, 0)
83    }
84}
85
86/// Discovered engine state.
87#[derive(Debug, Clone)]
88pub struct EngineState {
89    /// Authoritative engine ID
90    pub engine_id: Bytes,
91    /// Engine boot count
92    pub engine_boots: u32,
93    /// Engine time at last sync
94    pub engine_time: u32,
95    /// Local time when engine_time was received
96    pub synced_at: Instant,
97    /// Latest received engine time (for anti-replay, RFC 3414 Section 2.3)
98    pub latest_received_engine_time: u32,
99    /// Maximum message size the remote engine can accept (from msgMaxSize header).
100    pub msg_max_size: u32,
101}
102
103impl EngineState {
104    /// Create new engine state from discovery response.
105    pub fn new(engine_id: Bytes, engine_boots: u32, engine_time: u32) -> Self {
106        Self {
107            engine_id,
108            engine_boots,
109            engine_time,
110            synced_at: Instant::now(),
111            latest_received_engine_time: engine_time,
112            msg_max_size: DEFAULT_MSG_MAX_SIZE,
113        }
114    }
115
116    /// Create with explicit msgMaxSize from agent's header.
117    pub fn with_msg_max_size(
118        engine_id: Bytes,
119        engine_boots: u32,
120        engine_time: u32,
121        msg_max_size: u32,
122    ) -> Self {
123        Self {
124            engine_id,
125            engine_boots,
126            engine_time,
127            synced_at: Instant::now(),
128            latest_received_engine_time: engine_time,
129            msg_max_size,
130        }
131    }
132
133    /// Create with msgMaxSize capped to session maximum.
134    ///
135    /// Non-compliant agents may advertise msgMaxSize values larger than they
136    /// can handle. This caps the value to a known safe session limit.
137    pub fn with_msg_max_size_capped(
138        engine_id: Bytes,
139        engine_boots: u32,
140        engine_time: u32,
141        reported_msg_max_size: u32,
142        session_max: u32,
143    ) -> Self {
144        let msg_max_size = if reported_msg_max_size > session_max {
145            tracing::debug!(target: "async_snmp::v3", { reported = reported_msg_max_size, session_max = session_max }, "capping msgMaxSize to session limit");
146            session_max
147        } else {
148            reported_msg_max_size
149        };
150
151        Self {
152            engine_id,
153            engine_boots,
154            engine_time,
155            synced_at: Instant::now(),
156            latest_received_engine_time: engine_time,
157            msg_max_size,
158        }
159    }
160
161    /// Get the estimated current engine time.
162    ///
163    /// This adds elapsed local time to the synced engine time.
164    /// Per RFC 3414 Section 2.2.1, the result is capped at MAX_ENGINE_TIME (2^31-1).
165    pub fn estimated_time(&self) -> u32 {
166        let elapsed = self.synced_at.elapsed().as_secs() as u32;
167        self.engine_time
168            .saturating_add(elapsed)
169            .min(MAX_ENGINE_TIME)
170    }
171
172    /// Update time from a response.
173    ///
174    /// Per RFC 3414 Section 3.2 Step 7b, only update if:
175    /// - Response boots > local boots, OR
176    /// - Response boots == local boots AND response time > latest_received_engine_time
177    pub fn update_time(&mut self, response_boots: u32, response_time: u32) -> bool {
178        if response_boots > self.engine_boots {
179            // New boot cycle
180            self.engine_boots = response_boots;
181            self.engine_time = response_time;
182            self.synced_at = Instant::now();
183            self.latest_received_engine_time = response_time;
184            true
185        } else if response_boots == self.engine_boots
186            && response_time > self.latest_received_engine_time
187        {
188            // Same boot cycle, newer time
189            self.engine_time = response_time;
190            self.synced_at = Instant::now();
191            self.latest_received_engine_time = response_time;
192            true
193        } else {
194            false
195        }
196    }
197
198    /// Check if a message time is within the time window.
199    ///
200    /// Per RFC 3414 Section 2.2.3, a message is outside the window if:
201    /// - Local boots is 2147483647 (latched), OR
202    /// - Message boots differs from local boots, OR
203    /// - |message_time - local_time| > 150 seconds
204    pub fn is_in_time_window(&self, msg_boots: u32, msg_time: u32) -> bool {
205        // Check for latched boots (max value)
206        if self.engine_boots == 2147483647 {
207            return false;
208        }
209
210        // Boots must match
211        if msg_boots != self.engine_boots {
212            return false;
213        }
214
215        // Time must be within window
216        let local_time = self.estimated_time();
217        let diff = msg_time.abs_diff(local_time);
218
219        diff <= TIME_WINDOW
220    }
221}
222
223/// Thread-safe cache of discovered engine state.
224///
225/// Use this to share engine discovery results across multiple clients
226/// targeting different engines.
227///
228/// # Example
229///
230/// ```ignore
231/// let cache = EngineCache::new();
232///
233/// let client1 = Client::builder("192.168.1.1:161")
234///     .username("admin")
235///     .auth(AuthProtocol::Sha1, "authpass")
236///     .engine_cache(cache.clone())
237///     .connect()
238///     .await?;
239///
240/// let client2 = Client::builder("192.168.1.2:161")
241///     .username("admin")
242///     .auth(AuthProtocol::Sha1, "authpass")
243///     .engine_cache(cache.clone())
244///     .connect()
245///     .await?;
246/// ```
247#[derive(Debug, Default)]
248pub struct EngineCache {
249    engines: RwLock<HashMap<SocketAddr, EngineState>>,
250}
251
252impl EngineCache {
253    /// Create a new empty engine cache.
254    pub fn new() -> Self {
255        Self {
256            engines: RwLock::new(HashMap::new()),
257        }
258    }
259
260    /// Get cached engine state for a target.
261    pub fn get(&self, target: &SocketAddr) -> Option<EngineState> {
262        self.engines.read().ok()?.get(target).cloned()
263    }
264
265    /// Store engine state for a target.
266    pub fn insert(&self, target: SocketAddr, state: EngineState) {
267        if let Ok(mut engines) = self.engines.write() {
268            engines.insert(target, state);
269        }
270    }
271
272    /// Update time for an existing entry.
273    ///
274    /// Returns true if the entry was updated, false if not found or not updated.
275    pub fn update_time(
276        &self,
277        target: &SocketAddr,
278        response_boots: u32,
279        response_time: u32,
280    ) -> bool {
281        if let Ok(mut engines) = self.engines.write()
282            && let Some(state) = engines.get_mut(target)
283        {
284            return state.update_time(response_boots, response_time);
285        }
286        false
287    }
288
289    /// Remove cached state for a target.
290    pub fn remove(&self, target: &SocketAddr) -> Option<EngineState> {
291        self.engines.write().ok()?.remove(target)
292    }
293
294    /// Clear all cached state.
295    pub fn clear(&self) {
296        if let Ok(mut engines) = self.engines.write() {
297            engines.clear();
298        }
299    }
300
301    /// Get the number of cached engines.
302    pub fn len(&self) -> usize {
303        self.engines.read().map(|e| e.len()).unwrap_or(0)
304    }
305
306    /// Check if the cache is empty.
307    pub fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310}
311
312impl Clone for EngineCache {
313    fn clone(&self) -> Self {
314        // Clone creates a new cache with the same contents
315        let engines = self.engines.read().map(|e| e.clone()).unwrap_or_default();
316        Self {
317            engines: RwLock::new(engines),
318        }
319    }
320}
321
322/// Extract engine state from a discovery response's USM security parameters.
323///
324/// The discovery response (Report PDU) contains the authoritative engine's
325/// ID, boots, and time in the USM security parameters field.
326pub fn parse_discovery_response(security_params: &Bytes) -> Result<EngineState> {
327    parse_discovery_response_with_limits(
328        security_params,
329        DEFAULT_MSG_MAX_SIZE,
330        DEFAULT_MSG_MAX_SIZE,
331    )
332}
333
334/// Extract engine state with explicit msgMaxSize and session limit.
335///
336/// The `reported_msg_max_size` comes from the V3 message header (MsgGlobalData).
337/// The `session_max` is our transport's maximum message size.
338/// Values are capped to prevent issues with non-compliant agents.
339pub fn parse_discovery_response_with_limits(
340    security_params: &Bytes,
341    reported_msg_max_size: u32,
342    session_max: u32,
343) -> Result<EngineState> {
344    let usm = UsmSecurityParams::decode(security_params.clone())?;
345
346    if usm.engine_id.is_empty() {
347        tracing::debug!(target: "async_snmp::engine", "discovery response contained empty engine ID");
348        return Err(Error::MalformedResponse {
349            target: SocketAddr::from(([0, 0, 0, 0], 0)),
350        }
351        .boxed());
352    }
353
354    Ok(EngineState::with_msg_max_size_capped(
355        usm.engine_id,
356        usm.engine_boots,
357        usm.engine_time,
358        reported_msg_max_size,
359        session_max,
360    ))
361}
362
363/// Check if a Report PDU indicates "unknown engine ID" (discovery response).
364///
365/// Returns true if the PDU contains usmStatsUnknownEngineIDs varbind.
366pub fn is_unknown_engine_id_report(pdu: &crate::pdu::Pdu) -> bool {
367    use crate::pdu::PduType;
368
369    if pdu.pdu_type != PduType::Report {
370        return false;
371    }
372
373    let unknown_engine_ids_oid = report_oids::unknown_engine_ids();
374    pdu.varbinds
375        .iter()
376        .any(|vb| vb.oid == unknown_engine_ids_oid)
377}
378
379/// Check if a Report PDU indicates "not in time window".
380///
381/// Returns true if the PDU contains usmStatsNotInTimeWindows varbind.
382pub fn is_not_in_time_window_report(pdu: &crate::pdu::Pdu) -> bool {
383    use crate::pdu::PduType;
384
385    if pdu.pdu_type != PduType::Report {
386        return false;
387    }
388
389    let not_in_time_windows_oid = report_oids::not_in_time_windows();
390    pdu.varbinds
391        .iter()
392        .any(|vb| vb.oid == not_in_time_windows_oid)
393}
394
395/// Check if a Report PDU indicates "wrong digest" (authentication failure).
396///
397/// Returns true if the PDU contains usmStatsWrongDigests varbind.
398pub fn is_wrong_digest_report(pdu: &crate::pdu::Pdu) -> bool {
399    use crate::pdu::PduType;
400
401    if pdu.pdu_type != PduType::Report {
402        return false;
403    }
404
405    let wrong_digests_oid = report_oids::wrong_digests();
406    pdu.varbinds.iter().any(|vb| vb.oid == wrong_digests_oid)
407}
408
409/// Check if a Report PDU indicates "unsupported security level".
410///
411/// Returns true if the PDU contains usmStatsUnsupportedSecLevels varbind.
412pub fn is_unsupported_sec_level_report(pdu: &crate::pdu::Pdu) -> bool {
413    use crate::pdu::PduType;
414
415    if pdu.pdu_type != PduType::Report {
416        return false;
417    }
418
419    let oid = report_oids::unsupported_sec_levels();
420    pdu.varbinds.iter().any(|vb| vb.oid == oid)
421}
422
423/// Check if a Report PDU indicates "unknown user name".
424///
425/// Returns true if the PDU contains usmStatsUnknownUserNames varbind.
426pub fn is_unknown_user_name_report(pdu: &crate::pdu::Pdu) -> bool {
427    use crate::pdu::PduType;
428
429    if pdu.pdu_type != PduType::Report {
430        return false;
431    }
432
433    let oid = report_oids::unknown_user_names();
434    pdu.varbinds.iter().any(|vb| vb.oid == oid)
435}
436
437/// Check if a Report PDU indicates "decryption error".
438///
439/// Returns true if the PDU contains usmStatsDecryptionErrors varbind.
440pub fn is_decryption_error_report(pdu: &crate::pdu::Pdu) -> bool {
441    use crate::pdu::PduType;
442
443    if pdu.pdu_type != PduType::Report {
444        return false;
445    }
446
447    let oid = report_oids::decryption_errors();
448    pdu.varbinds.iter().any(|vb| vb.oid == oid)
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_engine_state_estimated_time() {
457        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
458
459        // Estimated time should be at least engine_time
460        let estimated = state.estimated_time();
461        assert!(estimated >= 1000);
462    }
463
464    #[test]
465    fn test_engine_state_update_time() {
466        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
467
468        // Same boots, newer time -> should update
469        assert!(state.update_time(1, 1100));
470        assert_eq!(state.latest_received_engine_time, 1100);
471
472        // Same boots, older time -> should NOT update
473        assert!(!state.update_time(1, 1050));
474        assert_eq!(state.latest_received_engine_time, 1100);
475
476        // New boot cycle -> should update
477        assert!(state.update_time(2, 500));
478        assert_eq!(state.engine_boots, 2);
479        assert_eq!(state.latest_received_engine_time, 500);
480    }
481
482    /// Test anti-replay protection via latestReceivedEngineTime (RFC 3414 Section 3.2 Step 7b).
483    ///
484    /// The anti-replay mechanism rejects messages with engine time values that are
485    /// not newer than the latest received time. This prevents replay attacks where
486    /// an attacker captures and re-sends old authenticated messages.
487    #[test]
488    fn test_anti_replay_rejects_old_time() {
489        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
490        state.latest_received_engine_time = 1500; // Simulate having received up to time 1500
491
492        // Attempt to replay a message from time 1400 (before latest)
493        // update_time returns false, indicating the update was rejected
494        assert!(
495            !state.update_time(1, 1400),
496            "Should reject replay: time 1400 < latest 1500"
497        );
498        assert_eq!(
499            state.latest_received_engine_time, 1500,
500            "Latest should not change"
501        );
502
503        // Even time 1500 (equal) should be rejected - must be strictly greater
504        assert!(
505            !state.update_time(1, 1500),
506            "Should reject replay: time 1500 == latest 1500"
507        );
508        assert_eq!(state.latest_received_engine_time, 1500);
509
510        // Time 1501 (newer) should be accepted
511        assert!(
512            state.update_time(1, 1501),
513            "Should accept: time 1501 > latest 1500"
514        );
515        assert_eq!(state.latest_received_engine_time, 1501);
516    }
517
518    /// Test anti-replay across boot cycles.
519    ///
520    /// A new boot cycle (higher boots value) always resets the latest_received_engine_time
521    /// since the agent has rebooted and time values are relative to the boot.
522    #[test]
523    fn test_anti_replay_new_boot_cycle_resets() {
524        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
525        state.latest_received_engine_time = 5000; // High value from long uptime
526
527        // New boot cycle with lower time value - should accept
528        // because the engine rebooted (boots increased)
529        assert!(
530            state.update_time(2, 100),
531            "New boot cycle should accept even with lower time"
532        );
533        assert_eq!(state.engine_boots, 2);
534        assert_eq!(state.engine_time, 100);
535        assert_eq!(
536            state.latest_received_engine_time, 100,
537            "Latest should reset to new time"
538        );
539
540        // Now subsequent updates in the new boot cycle follow normal rules
541        assert!(
542            !state.update_time(2, 50),
543            "Should reject older time in same boot cycle"
544        );
545        assert!(state.update_time(2, 150), "Should accept newer time");
546        assert_eq!(state.latest_received_engine_time, 150);
547    }
548
549    /// Test anti-replay rejects old boot cycles.
550    ///
551    /// An attacker cannot replay messages from a previous boot cycle.
552    #[test]
553    fn test_anti_replay_rejects_old_boot_cycle() {
554        let mut state = EngineState::new(Bytes::from_static(b"engine"), 5, 1000);
555        state.latest_received_engine_time = 1000;
556
557        // Attempt to use old boot cycle (boots=4) - should reject
558        assert!(
559            !state.update_time(4, 9999),
560            "Should reject old boot cycle even with high time"
561        );
562        assert_eq!(state.engine_boots, 5, "Boots should not change");
563        assert_eq!(
564            state.latest_received_engine_time, 1000,
565            "Latest should not change"
566        );
567
568        // Attempt boots=0 - should reject
569        assert!(!state.update_time(0, 9999), "Should reject boots=0 replay");
570    }
571
572    /// Test anti-replay with exact boundary values.
573    #[test]
574    fn test_anti_replay_boundary_values() {
575        let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 0);
576
577        // Start with time=0
578        assert_eq!(state.latest_received_engine_time, 0);
579
580        // Time=1 should be accepted (> 0)
581        assert!(state.update_time(1, 1));
582        assert_eq!(state.latest_received_engine_time, 1);
583
584        // Time=0 should be rejected (< 1)
585        assert!(!state.update_time(1, 0));
586
587        // Large time value should work
588        assert!(state.update_time(1, u32::MAX - 1));
589        assert_eq!(state.latest_received_engine_time, u32::MAX - 1);
590
591        // u32::MAX should still work
592        assert!(state.update_time(1, u32::MAX));
593        assert_eq!(state.latest_received_engine_time, u32::MAX);
594
595        // Nothing can be newer than u32::MAX in the same boot cycle
596        assert!(!state.update_time(1, u32::MAX));
597    }
598
599    #[test]
600    fn test_engine_state_time_window() {
601        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
602
603        // Same boots, within window
604        assert!(state.is_in_time_window(1, 1000));
605        assert!(state.is_in_time_window(1, 1100)); // +100s
606        assert!(state.is_in_time_window(1, 900)); // -100s
607
608        // Different boots -> out of window
609        assert!(!state.is_in_time_window(2, 1000));
610        assert!(!state.is_in_time_window(0, 1000));
611
612        // Way outside time window
613        assert!(!state.is_in_time_window(1, 2000)); // +1000s > 150s
614    }
615
616    /// Test the exact 150-second time window boundary per RFC 3414 Section 2.2.3.
617    ///
618    /// The time window is exactly 150 seconds. Messages with time difference
619    /// of exactly 150 seconds should be accepted, but 151 seconds should fail.
620    #[test]
621    fn test_time_window_150s_exact_boundary() {
622        // Use high engine_time to avoid underflow complications
623        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 10000);
624
625        // At exactly +150 seconds from engine_time (10000 + 150 = 10150)
626        // The is_in_time_window compares against estimated_time(), which adds
627        // elapsed time. For a fresh EngineState, elapsed should be ~0.
628        // So msg_time of 10150 should be within window (diff = 150 <= TIME_WINDOW)
629        assert!(
630            state.is_in_time_window(1, 10150),
631            "Message at exactly +150s boundary should be in window"
632        );
633
634        // At exactly +151 seconds (diff = 151 > TIME_WINDOW = 150)
635        assert!(
636            !state.is_in_time_window(1, 10151),
637            "Message at +151s should be outside window"
638        );
639
640        // At exactly -150 seconds (10000 - 150 = 9850)
641        assert!(
642            state.is_in_time_window(1, 9850),
643            "Message at exactly -150s boundary should be in window"
644        );
645
646        // At exactly -151 seconds (10000 - 151 = 9849)
647        assert!(
648            !state.is_in_time_window(1, 9849),
649            "Message at -151s should be outside window"
650        );
651    }
652
653    /// Test time window with maximum engine boots value (2147483647).
654    ///
655    /// Per RFC 3414 Section 2.2.3, when snmpEngineBoots is 2147483647 (latched),
656    /// all messages should be rejected as outside the time window.
657    #[test]
658    fn test_time_window_boots_latched() {
659        // Maximum boots value indicates the engine has been rebooted too many times
660        // and should reject all authenticated messages
661        let state = EngineState::new(Bytes::from_static(b"engine"), 2147483647, 1000);
662
663        // Even with matching boots and same time, should fail when latched
664        assert!(
665            !state.is_in_time_window(2147483647, 1000),
666            "Latched boots should reject all messages"
667        );
668
669        // Any other time should also fail
670        assert!(!state.is_in_time_window(2147483647, 1100));
671        assert!(!state.is_in_time_window(2147483647, 900));
672    }
673
674    /// Test time window edge cases with boot counter differences.
675    ///
676    /// Boot counter must match exactly; any difference means out of window.
677    #[test]
678    fn test_time_window_boots_mismatch() {
679        let state = EngineState::new(Bytes::from_static(b"engine"), 100, 1000);
680
681        // Boots too high
682        assert!(!state.is_in_time_window(101, 1000));
683        assert!(!state.is_in_time_window(200, 1000));
684
685        // Boots too low (replay from previous boot cycle)
686        assert!(!state.is_in_time_window(99, 1000));
687        assert!(!state.is_in_time_window(0, 1000));
688    }
689
690    #[test]
691    fn test_engine_cache_basic_operations() {
692        let cache = EngineCache::new();
693        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
694
695        // Initially empty
696        assert!(cache.is_empty());
697        assert!(cache.get(&addr).is_none());
698
699        // Insert
700        let state = EngineState::new(Bytes::from_static(b"engine1"), 1, 1000);
701        cache.insert(addr, state);
702
703        assert_eq!(cache.len(), 1);
704        assert!(!cache.is_empty());
705
706        // Get
707        let retrieved = cache.get(&addr).unwrap();
708        assert_eq!(retrieved.engine_id.as_ref(), b"engine1");
709        assert_eq!(retrieved.engine_boots, 1);
710
711        // Update time
712        assert!(cache.update_time(&addr, 1, 1100));
713
714        // Remove
715        let removed = cache.remove(&addr).unwrap();
716        assert_eq!(removed.latest_received_engine_time, 1100);
717        assert!(cache.is_empty());
718    }
719
720    #[test]
721    fn test_engine_cache_clone() {
722        let cache1 = EngineCache::new();
723        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
724
725        cache1.insert(
726            addr,
727            EngineState::new(Bytes::from_static(b"engine1"), 1, 1000),
728        );
729
730        // Clone should have same content
731        let cache2 = cache1.clone();
732        assert_eq!(cache2.len(), 1);
733        assert!(cache2.get(&addr).is_some());
734
735        // But modifications are independent
736        cache2.clear();
737        assert_eq!(cache1.len(), 1);
738        assert_eq!(cache2.len(), 0);
739    }
740
741    #[test]
742    fn test_parse_discovery_response() {
743        let usm = UsmSecurityParams::new(b"test-engine-id".as_slice(), 42, 12345, b"".as_slice());
744        let encoded = usm.encode();
745
746        let state = parse_discovery_response(&encoded).unwrap();
747        assert_eq!(state.engine_id.as_ref(), b"test-engine-id");
748        assert_eq!(state.engine_boots, 42);
749        assert_eq!(state.engine_time, 12345);
750    }
751
752    #[test]
753    fn test_parse_discovery_response_empty_engine_id() {
754        let usm = UsmSecurityParams::empty();
755        let encoded = usm.encode();
756
757        let result = parse_discovery_response(&encoded);
758        assert!(matches!(
759            *result.unwrap_err(),
760            Error::MalformedResponse { .. }
761        ));
762    }
763
764    #[test]
765    fn test_is_unknown_engine_id_report() {
766        use crate::Value;
767        use crate::VarBind;
768        use crate::pdu::{Pdu, PduType};
769
770        // Report with usmStatsUnknownEngineIDs
771        let mut pdu = Pdu {
772            pdu_type: PduType::Report,
773            request_id: 1,
774            error_status: 0,
775            error_index: 0,
776            varbinds: vec![VarBind {
777                oid: report_oids::unknown_engine_ids(),
778                value: Value::Counter32(1),
779            }],
780        };
781
782        assert!(is_unknown_engine_id_report(&pdu));
783
784        // Different report type
785        pdu.varbinds[0].oid = report_oids::not_in_time_windows();
786        assert!(!is_unknown_engine_id_report(&pdu));
787
788        // Not a Report PDU
789        pdu.pdu_type = PduType::Response;
790        assert!(!is_unknown_engine_id_report(&pdu));
791    }
792
793    // ========================================================================
794    // Engine Boots Overflow Tests (RFC 3414 Section 2.2.3)
795    // ========================================================================
796
797    /// Test that update_time accepts transition to maximum boots value.
798    ///
799    /// When the engine reboots and boots reaches 2147483647 (i32::MAX),
800    /// the update should be accepted since it's a valid new boot cycle.
801    #[test]
802    fn test_engine_boots_transition_to_max() {
803        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2147483646, 1000);
804
805        // Boot cycle to max value should be accepted
806        assert!(
807            state.update_time(2147483647, 100),
808            "Transition to boots=2147483647 should be accepted"
809        );
810        assert_eq!(state.engine_boots, 2147483647);
811        assert_eq!(state.engine_time, 100);
812    }
813
814    /// Test update_time behavior when boots is latched.
815    ///
816    /// The update_time function still tracks received times for anti-replay
817    /// purposes. The security rejection happens in is_in_time_window().
818    /// However, when boots=2147483647, there's no valid "higher" boots value,
819    /// so boot cycle transitions are impossible.
820    #[test]
821    fn test_engine_boots_latched_update_behavior() {
822        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2147483647, 1000);
823
824        // Time tracking still works for same boots
825        assert!(
826            state.update_time(2147483647, 2000),
827            "Time tracking updates should still work"
828        );
829        assert_eq!(state.latest_received_engine_time, 2000);
830
831        // Old time rejected per normal anti-replay
832        assert!(!state.update_time(2147483647, 1500));
833        assert_eq!(state.latest_received_engine_time, 2000);
834
835        // The key security check is in is_in_time_window
836        assert!(
837            !state.is_in_time_window(2147483647, 2000),
838            "Latched state should still reject all messages"
839        );
840    }
841
842    /// Test that time window rejects all messages when boots is latched.
843    ///
844    /// This is the key security property: once an engine's boots counter
845    /// reaches its maximum value, all authenticated messages should be
846    /// rejected to prevent replay attacks.
847    #[test]
848    fn test_engine_boots_latched_time_window_always_fails() {
849        let state = EngineState::new(Bytes::from_static(b"engine"), 2147483647, 1000);
850
851        // All time values should fail when latched
852        assert!(!state.is_in_time_window(2147483647, 0));
853        assert!(!state.is_in_time_window(2147483647, 1000));
854        assert!(!state.is_in_time_window(2147483647, 1001));
855        assert!(!state.is_in_time_window(2147483647, u32::MAX));
856
857        // Even previous boots values should fail
858        assert!(!state.is_in_time_window(2147483646, 1000));
859        assert!(!state.is_in_time_window(0, 1000));
860    }
861
862    /// Test creating EngineState directly with latched boots value.
863    ///
864    /// An agent that has been running for a very long time might already
865    /// be in the latched state when we first discover it.
866    #[test]
867    fn test_engine_state_created_latched() {
868        let state = EngineState::new(Bytes::from_static(b"engine"), 2147483647, 5000);
869
870        assert_eq!(state.engine_boots, 2147483647);
871        assert_eq!(state.engine_time, 5000);
872        assert_eq!(state.latest_received_engine_time, 5000);
873
874        // Should immediately be in latched state
875        assert!(
876            !state.is_in_time_window(2147483647, 5000),
877            "Newly created latched engine should reject all messages"
878        );
879    }
880
881    /// Test that boots values near the maximum work correctly.
882    ///
883    /// Verify normal operation just before reaching the latch point.
884    #[test]
885    fn test_engine_boots_near_max_operates_normally() {
886        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2147483645, 1000);
887
888        // Normal time window checks should work
889        assert!(state.is_in_time_window(2147483645, 1000));
890        assert!(state.is_in_time_window(2147483645, 1100));
891        assert!(!state.is_in_time_window(2147483645, 1200)); // Outside 150s window
892
893        // Should accept boot to 2147483646
894        assert!(state.update_time(2147483646, 500));
895        assert_eq!(state.engine_boots, 2147483646);
896        assert!(state.is_in_time_window(2147483646, 500));
897
898        // Should accept boot to 2147483647 (becomes latched)
899        assert!(state.update_time(2147483647, 100));
900        assert_eq!(state.engine_boots, 2147483647);
901
902        // Now latched - all messages rejected
903        assert!(!state.is_in_time_window(2147483647, 100));
904    }
905
906    /// Test that update_time correctly handles the comparison when
907    /// current boots is high but not yet latched.
908    #[test]
909    fn test_engine_boots_high_value_update_logic() {
910        let mut state = EngineState::new(Bytes::from_static(b"engine"), 2147483640, 1000);
911
912        // Old boot cycles should be rejected
913        assert!(!state.update_time(2147483639, 9999));
914        assert!(!state.update_time(0, 9999));
915
916        // Same boot, older time should be rejected
917        assert!(!state.update_time(2147483640, 500));
918
919        // Same boot, newer time should be accepted
920        assert!(state.update_time(2147483640, 1500));
921        assert_eq!(state.latest_received_engine_time, 1500);
922
923        // New boot should be accepted
924        assert!(state.update_time(2147483641, 100));
925        assert_eq!(state.engine_boots, 2147483641);
926    }
927
928    /// Test EngineCache behavior with latched engines.
929    ///
930    /// Even when latched, time tracking updates are accepted (for anti-replay).
931    /// The security rejection is enforced by is_in_time_window(), not update_time().
932    #[test]
933    fn test_engine_cache_latched_engine() {
934        let cache = EngineCache::new();
935        let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
936
937        // Insert latched engine
938        cache.insert(
939            addr,
940            EngineState::new(Bytes::from_static(b"latched"), 2147483647, 1000),
941        );
942
943        // Time tracking still works
944        assert!(
945            cache.update_time(&addr, 2147483647, 2000),
946            "Time tracking should update even for latched engine"
947        );
948
949        // Verify state was updated
950        let state = cache.get(&addr).unwrap();
951        assert_eq!(state.latest_received_engine_time, 2000);
952
953        // But the key security property: is_in_time_window rejects
954        assert!(
955            !state.is_in_time_window(2147483647, 2000),
956            "Latched engine should reject all time window checks"
957        );
958    }
959
960    // ========================================================================
961    // msgMaxSize Capping Tests
962    // ========================================================================
963    //
964    // Per net-snmp behavior, agent-reported msgMaxSize values should be capped
965    // to the session's maximum to prevent buffer issues with non-compliant agents.
966
967    /// Test that EngineState stores the agent's advertised msgMaxSize.
968    ///
969    /// The msg_max_size field tracks the maximum message size the remote engine
970    /// can accept, as reported in SNMPv3 message headers.
971    #[test]
972    fn test_engine_state_stores_msg_max_size() {
973        let state = EngineState::with_msg_max_size(Bytes::from_static(b"engine"), 1, 1000, 65507);
974        assert_eq!(state.msg_max_size, 65507);
975    }
976
977    /// Test that the default constructor uses the maximum UDP message size.
978    ///
979    /// When msgMaxSize is not provided (e.g., during basic discovery),
980    /// default to the maximum safe UDP datagram size (65507 bytes).
981    #[test]
982    fn test_engine_state_default_msg_max_size() {
983        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
984        assert_eq!(
985            state.msg_max_size, DEFAULT_MSG_MAX_SIZE,
986            "Default msg_max_size should be the maximum UDP datagram size"
987        );
988    }
989
990    /// Test that msgMaxSize is capped to session maximum.
991    ///
992    /// Non-compliant agents may advertise msgMaxSize values larger than they
993    /// (or we) can actually handle. Values exceeding the session maximum are
994    /// silently capped to prevent buffer issues.
995    #[test]
996    fn test_engine_state_msg_max_size_capped_to_session_max() {
997        // Agent advertises 2GB, but we cap to 65507 (our session max)
998        let state = EngineState::with_msg_max_size_capped(
999            Bytes::from_static(b"engine"),
1000            1,
1001            1000,
1002            2_000_000_000, // Agent claims 2GB
1003            65507,         // Our session maximum
1004        );
1005        assert_eq!(
1006            state.msg_max_size, 65507,
1007            "msg_max_size should be capped to session maximum"
1008        );
1009    }
1010
1011    /// Test that msgMaxSize within session maximum is not modified.
1012    ///
1013    /// When the agent advertises a reasonable value below our maximum,
1014    /// it should be stored as-is without capping.
1015    #[test]
1016    fn test_engine_state_msg_max_size_within_limit_not_capped() {
1017        let state = EngineState::with_msg_max_size_capped(
1018            Bytes::from_static(b"engine"),
1019            1,
1020            1000,
1021            1472,  // Agent claims 1472 (Ethernet MTU - headers)
1022            65507, // Our session maximum
1023        );
1024        assert_eq!(
1025            state.msg_max_size, 1472,
1026            "msg_max_size within limit should not be capped"
1027        );
1028    }
1029
1030    /// Test msgMaxSize capping at exact boundary.
1031    ///
1032    /// When agent's msgMaxSize exactly equals session maximum, no capping occurs.
1033    #[test]
1034    fn test_engine_state_msg_max_size_at_exact_boundary() {
1035        let state = EngineState::with_msg_max_size_capped(
1036            Bytes::from_static(b"engine"),
1037            1,
1038            1000,
1039            65507, // Exactly at session max
1040            65507, // Our session maximum
1041        );
1042        assert_eq!(state.msg_max_size, 65507);
1043    }
1044
1045    /// Test msgMaxSize capping with TCP transport maximum.
1046    ///
1047    /// TCP transports may have higher limits. Verify capping works with
1048    /// the larger TCP message size limit.
1049    #[test]
1050    fn test_engine_state_msg_max_size_tcp_limit() {
1051        const TCP_MAX: u32 = 0x7FFFFFFF; // net-snmp TCP maximum
1052
1053        // Agent claims i32::MAX, we have same limit
1054        let state = EngineState::with_msg_max_size_capped(
1055            Bytes::from_static(b"engine"),
1056            1,
1057            1000,
1058            TCP_MAX,
1059            TCP_MAX,
1060        );
1061        assert_eq!(state.msg_max_size, TCP_MAX);
1062
1063        // Agent claims more than i32::MAX (wrapped negative), cap to limit
1064        let state = EngineState::with_msg_max_size_capped(
1065            Bytes::from_static(b"engine"),
1066            1,
1067            1000,
1068            u32::MAX, // Larger than any valid msgMaxSize
1069            TCP_MAX,
1070        );
1071        assert_eq!(
1072            state.msg_max_size, TCP_MAX,
1073            "Values exceeding session max should be capped"
1074        );
1075    }
1076
1077    /// Test that EngineState::new uses the default msg_max_size constant.
1078    #[test]
1079    fn test_engine_state_new_uses_default_constant() {
1080        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1081
1082        // DEFAULT_MSG_MAX_SIZE is the maximum UDP payload (65507)
1083        assert_eq!(state.msg_max_size, DEFAULT_MSG_MAX_SIZE);
1084    }
1085
1086    // ========================================================================
1087    // Engine Time Overflow Tests (RFC 3414 Section 2.2.1)
1088    // ========================================================================
1089    //
1090    // Per RFC 3414, snmpEngineTime is a 31-bit value (0..2147483647).
1091    // When the time value would exceed this, it must not go beyond MAX_ENGINE_TIME.
1092
1093    /// Test that estimated_time caps at MAX_ENGINE_TIME (2^31-1).
1094    ///
1095    /// Per RFC 3414 Section 2.2.1, snmpEngineTime is 31-bit (0..2147483647).
1096    /// If time would exceed this value, it should cap at MAX_ENGINE_TIME rather
1097    /// than continuing to u32::MAX.
1098    #[test]
1099    fn test_estimated_time_caps_at_max_engine_time() {
1100        // Create state with engine_time near the maximum
1101        let state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME - 10);
1102
1103        // Even though we're adding elapsed time, result should never exceed MAX_ENGINE_TIME
1104        let estimated = state.estimated_time();
1105        assert!(
1106            estimated <= MAX_ENGINE_TIME,
1107            "estimated_time() should never exceed MAX_ENGINE_TIME ({}), got {}",
1108            MAX_ENGINE_TIME,
1109            estimated
1110        );
1111    }
1112
1113    /// Test that estimated_time at MAX_ENGINE_TIME stays at MAX_ENGINE_TIME.
1114    ///
1115    /// When engine_time is already at the maximum, adding more elapsed time
1116    /// should not increase it further.
1117    #[test]
1118    fn test_estimated_time_at_max_stays_at_max() {
1119        let state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME);
1120
1121        // Should stay at MAX_ENGINE_TIME
1122        let estimated = state.estimated_time();
1123        assert_eq!(
1124            estimated, MAX_ENGINE_TIME,
1125            "estimated_time() at max should stay at MAX_ENGINE_TIME"
1126        );
1127    }
1128
1129    /// Test that engine_time values beyond MAX_ENGINE_TIME are invalid.
1130    ///
1131    /// This verifies the constant value is correct per RFC 3414.
1132    #[test]
1133    fn test_max_engine_time_constant() {
1134        // RFC 3414 specifies 31-bit (0..2147483647), which is i32::MAX
1135        assert_eq!(MAX_ENGINE_TIME, 2147483647);
1136        assert_eq!(MAX_ENGINE_TIME, i32::MAX as u32);
1137    }
1138
1139    /// Test that normal time estimation works below MAX_ENGINE_TIME.
1140    ///
1141    /// For typical time values well below the maximum, estimation should
1142    /// work normally without artificial capping.
1143    #[test]
1144    fn test_estimated_time_normal_operation() {
1145        let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
1146
1147        // For a fresh state, elapsed should be ~0, so estimated should be ~engine_time
1148        let estimated = state.estimated_time();
1149        assert!(
1150            estimated >= 1000,
1151            "estimated_time() should be at least engine_time"
1152        );
1153        // Should not hit the cap
1154        assert!(
1155            estimated < MAX_ENGINE_TIME,
1156            "Normal time values should not hit MAX_ENGINE_TIME cap"
1157        );
1158    }
1159}