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