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