Skip to main content

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