Skip to main content

async_snmp/notification/
mod.rs

1//! SNMP Notification Receiver (RFC 3413).
2//!
3//! This module provides functionality for receiving SNMP notifications:
4//! - TrapV1 (SNMPv1 format, different PDU structure)
5//! - TrapV2/SNMPv2-Trap (SNMPv2c/v3 format)
6//! - InformRequest (confirmed notification, requires response)
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use async_snmp::notification::{NotificationReceiver, Notification};
12//! use std::net::SocketAddr;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<async_snmp::Error>> {
16//!     let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
17//!
18//!     loop {
19//!         match receiver.recv().await {
20//!             Ok((notification, source)) => {
21//!                 println!("Received notification from {}: {:?}", source, notification);
22//!             }
23//!             Err(e) => {
24//!                 eprintln!("Error receiving notification: {}", e);
25//!             }
26//!         }
27//!     }
28//! }
29//! ```
30//!
31//! # V3 Authenticated Informs
32//!
33//! To receive and respond to authenticated V3 InformRequests, configure USM credentials:
34//!
35//! ```rust,no_run
36//! use async_snmp::notification::NotificationReceiver;
37//! use async_snmp::{AuthProtocol, PrivProtocol};
38//!
39//! # async fn example() -> Result<(), Box<async_snmp::Error>> {
40//! let receiver = NotificationReceiver::builder()
41//!     .bind("0.0.0.0:162")
42//!     .usm_user("informuser", |u| {
43//!         u.auth(AuthProtocol::Sha1, b"authpass123")
44//!          .privacy(PrivProtocol::Aes128, b"privpass123")
45//!     })
46//!     .build()
47//!     .await?;
48//! # Ok(())
49//! # }
50//! ```
51
52mod handlers;
53mod types;
54mod varbind;
55
56use std::collections::HashMap;
57use std::net::SocketAddr;
58use std::sync::Arc;
59use std::sync::atomic::{AtomicU32, Ordering};
60use std::time::Instant;
61
62use bytes::Bytes;
63use tokio::net::UdpSocket;
64use tracing::instrument;
65
66use crate::ber::Decoder;
67use crate::error::internal::DecodeErrorKind;
68use crate::error::{Error, Result};
69use crate::oid::Oid;
70use crate::pdu::TrapV1Pdu;
71use crate::util::bind_udp_socket;
72use crate::v3::SaltCounter;
73use crate::varbind::VarBind;
74use crate::version::Version;
75
76// Re-exports
77pub use types::{DerivedKeys, UsmConfig};
78pub use varbind::validate_notification_varbinds;
79
80/// Well-known OIDs for notification varbinds.
81pub mod oids {
82    use crate::oid;
83
84    /// sysUpTime.0 - first varbind in v2c/v3 notifications
85    pub fn sys_uptime() -> crate::Oid {
86        oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
87    }
88
89    /// snmpTrapOID.0 - second varbind in v2c/v3 notifications (contains trap type)
90    pub fn snmp_trap_oid() -> crate::Oid {
91        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
92    }
93
94    /// snmpTrapEnterprise.0 - optional, enterprise OID for enterprise-specific traps
95    pub fn snmp_trap_enterprise() -> crate::Oid {
96        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
97    }
98
99    /// snmpTrapAddress.0 - agent address from v1 trap (RFC 3584 Section 3)
100    pub fn snmp_trap_address() -> crate::Oid {
101        oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
102    }
103
104    /// Standard trap OID prefix (snmpTraps)
105    pub fn snmp_traps() -> crate::Oid {
106        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
107    }
108
109    /// coldStart trap OID (snmpTraps.1)
110    pub fn cold_start() -> crate::Oid {
111        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
112    }
113
114    /// warmStart trap OID (snmpTraps.2)
115    pub fn warm_start() -> crate::Oid {
116        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
117    }
118
119    /// linkDown trap OID (snmpTraps.3)
120    pub fn link_down() -> crate::Oid {
121        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
122    }
123
124    /// linkUp trap OID (snmpTraps.4)
125    pub fn link_up() -> crate::Oid {
126        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
127    }
128
129    /// authenticationFailure trap OID (snmpTraps.5)
130    pub fn auth_failure() -> crate::Oid {
131        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
132    }
133
134    /// egpNeighborLoss trap OID (snmpTraps.6)
135    pub fn egp_neighbor_loss() -> crate::Oid {
136        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
137    }
138}
139
140/// Builder for `NotificationReceiver`.
141///
142/// Allows configuration of bind address and USM credentials for V3 support.
143pub struct NotificationReceiverBuilder {
144    bind_addr: String,
145    usm_users: HashMap<Bytes, UsmConfig>,
146    engine_id: Option<Vec<u8>>,
147    engine_boots: u32,
148}
149
150impl NotificationReceiverBuilder {
151    /// Create a new builder with default settings.
152    ///
153    /// Defaults:
154    /// - Bind address: `0.0.0.0:162` (UDP, standard SNMP trap port)
155    /// - No USM users (v3 notifications rejected until users are added)
156    pub fn new() -> Self {
157        Self {
158            bind_addr: "0.0.0.0:162".to_string(),
159            usm_users: HashMap::new(),
160            engine_id: None,
161            engine_boots: 1,
162        }
163    }
164
165    /// Set the UDP bind address.
166    ///
167    /// Default is `0.0.0.0:162` (UDP, standard SNMP trap port).
168    pub fn bind(mut self, addr: impl Into<String>) -> Self {
169        self.bind_addr = addr.into();
170        self
171    }
172
173    /// Add a USM user for V3 authentication.
174    ///
175    /// # Example
176    ///
177    /// ```rust,no_run
178    /// use async_snmp::notification::NotificationReceiver;
179    /// use async_snmp::{AuthProtocol, PrivProtocol};
180    ///
181    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
182    /// let receiver = NotificationReceiver::builder()
183    ///     .bind("0.0.0.0:162")
184    ///     .usm_user("trapuser", |u| {
185    ///         u.auth(AuthProtocol::Sha1, b"authpassword")
186    ///          .privacy(PrivProtocol::Aes128, b"privpassword")
187    ///     })
188    ///     .build()
189    ///     .await?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
194    where
195        F: FnOnce(UsmConfig) -> UsmConfig,
196    {
197        let username_bytes: Bytes = username.into();
198        let config = configure(UsmConfig::new(username_bytes.clone()));
199        self.usm_users.insert(username_bytes, config);
200        self
201    }
202
203    /// Set the engine ID for SNMPv3.
204    ///
205    /// If not set, a default engine ID will be generated based on the
206    /// RFC 3411 format using enterprise number and timestamp.
207    pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
208        self.engine_id = Some(engine_id.into());
209        self
210    }
211
212    /// Set the initial engine boots value.
213    ///
214    /// This should be persisted across restarts and incremented each time
215    /// the receiver starts. Default is 1.
216    pub fn engine_boots(mut self, boots: u32) -> Self {
217        self.engine_boots = boots;
218        self
219    }
220
221    /// Build the notification receiver.
222    pub async fn build(self) -> Result<NotificationReceiver> {
223        let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
224            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
225        })?;
226
227        let socket = bind_udp_socket(bind_addr, None, None, false)
228            .await
229            .map_err(|e| Error::Network {
230                target: bind_addr,
231                source: e,
232            })?;
233
234        let local_addr = socket.local_addr().map_err(|e| Error::Network {
235            target: bind_addr,
236            source: e,
237        })?;
238
239        let engine_id: Bytes = self.engine_id.map(Bytes::from).unwrap_or_else(|| {
240            let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
241            let timestamp = std::time::SystemTime::now()
242                .duration_since(std::time::UNIX_EPOCH)
243                .unwrap_or_default()
244                .as_secs();
245            id.extend_from_slice(&timestamp.to_be_bytes());
246            Bytes::from(id)
247        });
248
249        Ok(NotificationReceiver {
250            inner: Arc::new(ReceiverInner {
251                socket,
252                local_addr,
253                usm_users: self.usm_users,
254                engine_id,
255                salt_counter: SaltCounter::new(),
256                engine_boots_base: self.engine_boots,
257                engine_start: Instant::now(),
258                usm_unknown_engine_ids: AtomicU32::new(0),
259            }),
260        })
261    }
262}
263
264impl Default for NotificationReceiverBuilder {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270/// Received SNMP notification.
271///
272/// This enum represents all types of SNMP notifications that can be received:
273/// - SNMPv1 Trap (different PDU structure)
274/// - SNMPv2c/v3 Trap (standard PDU with sysUpTime.0 and snmpTrapOID.0)
275/// - InformRequest (confirmed notification, response will be sent automatically)
276#[derive(Debug, Clone)]
277pub enum Notification {
278    /// SNMPv1 Trap with unique PDU structure.
279    TrapV1 {
280        /// Community string used for authentication
281        community: Bytes,
282        /// The trap PDU
283        trap: TrapV1Pdu,
284    },
285
286    /// SNMPv2c Trap (unconfirmed notification).
287    TrapV2c {
288        /// Community string used for authentication
289        community: Bytes,
290        /// sysUpTime.0 value (hundredths of seconds since agent init)
291        uptime: u32,
292        /// snmpTrapOID.0 value (trap type identifier)
293        trap_oid: Oid,
294        /// Additional variable bindings
295        varbinds: Vec<VarBind>,
296        /// Original request ID (for logging/correlation)
297        request_id: i32,
298    },
299
300    /// SNMPv3 Trap (unconfirmed notification).
301    TrapV3 {
302        /// Username from USM
303        username: Bytes,
304        /// Context engine ID
305        context_engine_id: Bytes,
306        /// Context name
307        context_name: Bytes,
308        /// sysUpTime.0 value
309        uptime: u32,
310        /// snmpTrapOID.0 value
311        trap_oid: Oid,
312        /// Additional variable bindings
313        varbinds: Vec<VarBind>,
314        /// Original request ID
315        request_id: i32,
316    },
317
318    /// InformRequest (confirmed notification) - v2c.
319    ///
320    /// A response is automatically sent when this notification is received.
321    InformV2c {
322        /// Community string
323        community: Bytes,
324        /// sysUpTime.0 value
325        uptime: u32,
326        /// snmpTrapOID.0 value
327        trap_oid: Oid,
328        /// Additional variable bindings
329        varbinds: Vec<VarBind>,
330        /// Request ID (used in response)
331        request_id: i32,
332    },
333
334    /// InformRequest (confirmed notification) - v3.
335    ///
336    /// A response is automatically sent when this notification is received.
337    InformV3 {
338        /// Username from USM
339        username: Bytes,
340        /// Context engine ID
341        context_engine_id: Bytes,
342        /// Context name
343        context_name: Bytes,
344        /// sysUpTime.0 value
345        uptime: u32,
346        /// snmpTrapOID.0 value
347        trap_oid: Oid,
348        /// Additional variable bindings
349        varbinds: Vec<VarBind>,
350        /// Request ID
351        request_id: i32,
352    },
353}
354
355impl Notification {
356    /// Get the trap/notification OID.
357    ///
358    /// For TrapV1, this is derived from enterprise + generic/specific trap.
359    /// For v2c/v3, this is the snmpTrapOID.0 value.
360    pub fn trap_oid(&self) -> Result<Oid> {
361        match self {
362            Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
363            Notification::TrapV2c { trap_oid, .. }
364            | Notification::TrapV3 { trap_oid, .. }
365            | Notification::InformV2c { trap_oid, .. }
366            | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
367        }
368    }
369
370    /// Get the uptime value (sysUpTime.0 or time_stamp for v1).
371    pub fn uptime(&self) -> u32 {
372        match self {
373            Notification::TrapV1 { trap, .. } => trap.time_stamp,
374            Notification::TrapV2c { uptime, .. }
375            | Notification::TrapV3 { uptime, .. }
376            | Notification::InformV2c { uptime, .. }
377            | Notification::InformV3 { uptime, .. } => *uptime,
378        }
379    }
380
381    /// Get the variable bindings.
382    pub fn varbinds(&self) -> &[VarBind] {
383        match self {
384            Notification::TrapV1 { trap, .. } => &trap.varbinds,
385            Notification::TrapV2c { varbinds, .. }
386            | Notification::TrapV3 { varbinds, .. }
387            | Notification::InformV2c { varbinds, .. }
388            | Notification::InformV3 { varbinds, .. } => varbinds,
389        }
390    }
391
392    /// Check if this is a confirmed notification (InformRequest).
393    pub fn is_confirmed(&self) -> bool {
394        matches!(
395            self,
396            Notification::InformV2c { .. } | Notification::InformV3 { .. }
397        )
398    }
399
400    /// Get the SNMP version of this notification.
401    pub fn version(&self) -> Version {
402        match self {
403            Notification::TrapV1 { .. } => Version::V1,
404            Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
405            Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
406        }
407    }
408}
409
410/// SNMP Notification Receiver.
411///
412/// Listens for incoming SNMP notifications (traps and informs) on a UDP socket.
413/// For InformRequest notifications, automatically sends a Response-PDU.
414///
415/// # V3 Authentication
416///
417/// To receive authenticated V3 notifications, use the builder pattern to configure
418/// USM credentials:
419///
420/// ```rust,no_run
421/// use async_snmp::notification::NotificationReceiver;
422/// use async_snmp::{AuthProtocol, PrivProtocol};
423///
424/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
425/// let receiver = NotificationReceiver::builder()
426///     .bind("0.0.0.0:162")
427///     .usm_user("trapuser", |u| {
428///         u.auth(AuthProtocol::Sha1, b"authpassword")
429///     })
430///     .build()
431///     .await?;
432/// # Ok(())
433/// # }
434/// ```
435pub struct NotificationReceiver {
436    inner: Arc<ReceiverInner>,
437}
438
439struct ReceiverInner {
440    socket: UdpSocket,
441    local_addr: SocketAddr,
442    /// Configured USM users for V3 authentication
443    usm_users: HashMap<Bytes, UsmConfig>,
444    /// Engine ID for V3 discovery responses
445    engine_id: Bytes,
446    /// Salt counter for privacy operations
447    salt_counter: SaltCounter,
448    /// Initial engine boots value at startup, used to compute overflow-adjusted boots.
449    engine_boots_base: u32,
450    /// Time when the receiver was started, used to compute engine time.
451    engine_start: Instant,
452    /// usmStatsUnknownEngineIDs counter
453    usm_unknown_engine_ids: AtomicU32,
454}
455
456impl NotificationReceiver {
457    /// Create a builder for configuring the notification receiver.
458    ///
459    /// Use this to configure USM credentials for V3 authentication.
460    pub fn builder() -> NotificationReceiverBuilder {
461        NotificationReceiverBuilder::new()
462    }
463
464    /// Bind to a local address.
465    ///
466    /// The standard SNMP notification port is 162.
467    /// For V3 authentication support, use `NotificationReceiver::builder()` instead.
468    ///
469    /// # Example
470    ///
471    /// ```rust,no_run
472    /// use async_snmp::notification::NotificationReceiver;
473    ///
474    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
475    /// // Bind to the standard trap port (requires root/admin on most systems)
476    /// let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
477    ///
478    /// // Or use an unprivileged port for testing
479    /// let receiver = NotificationReceiver::bind("0.0.0.0:1162").await?;
480    /// # Ok(())
481    /// # }
482    /// ```
483    pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
484        let addr_str = addr.as_ref();
485        let bind_addr: SocketAddr = addr_str
486            .parse()
487            .map_err(|_| Error::Config(format!("invalid bind address: {}", addr_str).into()))?;
488
489        let socket = bind_udp_socket(bind_addr, None, None, false)
490            .await
491            .map_err(|e| Error::Network {
492                target: bind_addr,
493                source: e,
494            })?;
495
496        let local_addr = socket.local_addr().map_err(|e| Error::Network {
497            target: bind_addr,
498            source: e,
499        })?;
500
501        let engine_id: Bytes = {
502            let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
503            let timestamp = std::time::SystemTime::now()
504                .duration_since(std::time::UNIX_EPOCH)
505                .unwrap_or_default()
506                .as_secs();
507            id.extend_from_slice(&timestamp.to_be_bytes());
508            Bytes::from(id)
509        };
510
511        Ok(Self {
512            inner: Arc::new(ReceiverInner {
513                socket,
514                local_addr,
515                usm_users: HashMap::new(),
516                engine_id,
517                salt_counter: SaltCounter::new(),
518                engine_boots_base: 1,
519                engine_start: Instant::now(),
520                usm_unknown_engine_ids: AtomicU32::new(0),
521            }),
522        })
523    }
524
525    /// Get the local address this receiver is bound to.
526    pub fn local_addr(&self) -> SocketAddr {
527        self.inner.local_addr
528    }
529
530    /// Get the engine ID.
531    pub fn engine_id(&self) -> &[u8] {
532        &self.inner.engine_id
533    }
534
535    /// Get the usmStatsUnknownEngineIDs counter value.
536    pub fn usm_unknown_engine_ids(&self) -> u32 {
537        self.inner.usm_unknown_engine_ids.load(Ordering::Relaxed)
538    }
539
540    /// Receive a notification.
541    ///
542    /// This method blocks until a notification is received. For InformRequest
543    /// notifications, a Response-PDU is automatically sent back to the sender.
544    ///
545    /// Returns the notification and the source address.
546    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
547    pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
548        let mut buf = vec![0u8; 65535];
549
550        loop {
551            let (len, source) =
552                self.inner
553                    .socket
554                    .recv_from(&mut buf)
555                    .await
556                    .map_err(|e| Error::Network {
557                        target: self.inner.local_addr,
558                        source: e,
559                    })?;
560
561            let data = Bytes::copy_from_slice(&buf[..len]);
562
563            match self.parse_and_respond(data, source).await {
564                Ok(Some(notification)) => return Ok((notification, source)),
565                Ok(None) => continue, // Not a notification PDU, ignore
566                Err(e) => {
567                    // Log parsing error but continue receiving
568                    tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
569                    continue;
570                }
571            }
572        }
573    }
574
575    /// Parse received data and send response if needed.
576    ///
577    /// Returns `None` if the message is not a notification PDU.
578    async fn parse_and_respond(
579        &self,
580        data: Bytes,
581        source: SocketAddr,
582    ) -> Result<Option<Notification>> {
583        // First, peek at the version to determine message type
584        let mut decoder = Decoder::with_target(data.clone(), source);
585        let mut seq = decoder.read_sequence()?;
586        let version_num = seq.read_integer()?;
587        let version = Version::from_i32(version_num).ok_or_else(|| {
588            tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
589            Error::MalformedResponse { target: source }.boxed()
590        })?;
591        drop(seq);
592        drop(decoder);
593
594        match version {
595            Version::V1 => self.handle_v1(data, source).await,
596            Version::V2c => self.handle_v2c(data, source).await,
597            Version::V3 => self.handle_v3(data, source).await,
598        }
599    }
600}
601
602impl Clone for NotificationReceiver {
603    fn clone(&self) -> Self {
604        Self {
605            inner: Arc::clone(&self.inner),
606        }
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613    use crate::message::SecurityLevel;
614    use crate::oid;
615    use crate::pdu::GenericTrap;
616    use crate::v3::AuthProtocol;
617
618    #[test]
619    fn test_notification_trap_v1() {
620        let trap = TrapV1Pdu::new(
621            oid!(1, 3, 6, 1, 4, 1, 9999),
622            [192, 168, 1, 1],
623            GenericTrap::LinkDown,
624            0,
625            12345,
626            vec![],
627        );
628
629        let notification = Notification::TrapV1 {
630            community: Bytes::from_static(b"public"),
631            trap,
632        };
633
634        assert!(!notification.is_confirmed());
635        assert_eq!(notification.version(), Version::V1);
636        assert_eq!(notification.uptime(), 12345);
637        assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
638    }
639
640    #[test]
641    fn test_notification_trap_v2c() {
642        let notification = Notification::TrapV2c {
643            community: Bytes::from_static(b"public"),
644            uptime: 54321,
645            trap_oid: oids::link_up(),
646            varbinds: vec![],
647            request_id: 1,
648        };
649
650        assert!(!notification.is_confirmed());
651        assert_eq!(notification.version(), Version::V2c);
652        assert_eq!(notification.uptime(), 54321);
653        assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
654    }
655
656    #[test]
657    fn test_notification_inform() {
658        let notification = Notification::InformV2c {
659            community: Bytes::from_static(b"public"),
660            uptime: 11111,
661            trap_oid: oids::cold_start(),
662            varbinds: vec![],
663            request_id: 42,
664        };
665
666        assert!(notification.is_confirmed());
667        assert_eq!(notification.version(), Version::V2c);
668    }
669
670    #[test]
671    fn test_notification_receiver_builder_default() {
672        let builder = NotificationReceiverBuilder::new();
673        assert_eq!(builder.bind_addr, "0.0.0.0:162");
674        assert!(builder.usm_users.is_empty());
675    }
676
677    #[test]
678    fn test_notification_receiver_builder_with_user() {
679        let builder = NotificationReceiverBuilder::new()
680            .bind("0.0.0.0:1162")
681            .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
682
683        assert_eq!(builder.bind_addr, "0.0.0.0:1162");
684        assert_eq!(builder.usm_users.len(), 1);
685
686        let user = builder
687            .usm_users
688            .get(&Bytes::from_static(b"trapuser"))
689            .unwrap();
690        assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
691    }
692
693    #[test]
694    fn test_notification_v3_inform() {
695        let notification = Notification::InformV3 {
696            username: Bytes::from_static(b"testuser"),
697            context_engine_id: Bytes::from_static(b"engine123"),
698            context_name: Bytes::new(),
699            uptime: 99999,
700            trap_oid: oids::warm_start(),
701            varbinds: vec![],
702            request_id: 100,
703        };
704
705        assert!(notification.is_confirmed());
706        assert_eq!(notification.version(), Version::V3);
707        assert_eq!(notification.uptime(), 99999);
708        assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
709    }
710
711    #[test]
712    fn test_notification_trap_v1_enterprise_specific_oid() {
713        let trap = TrapV1Pdu::new(
714            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
715            [192, 168, 1, 1],
716            GenericTrap::EnterpriseSpecific,
717            42,
718            12345,
719            vec![],
720        );
721
722        let notification = Notification::TrapV1 {
723            community: Bytes::from_static(b"public"),
724            trap,
725        };
726
727        assert_eq!(
728            notification.trap_oid().unwrap(),
729            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
730        );
731    }
732
733    #[test]
734    fn test_compute_engine_boots_time_basic() {
735        let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
736        assert_eq!(boots, 1);
737        assert_eq!(time, 1000);
738    }
739
740    #[test]
741    fn test_compute_engine_boots_time_zero_elapsed() {
742        let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
743        assert_eq!(boots, 1);
744        assert_eq!(time, 0);
745    }
746
747    #[test]
748    fn test_builder_engine_boots_default() {
749        let builder = NotificationReceiverBuilder::new();
750        assert_eq!(builder.engine_boots, 1);
751    }
752
753    #[test]
754    fn test_builder_engine_boots_custom() {
755        let builder = NotificationReceiverBuilder::new().engine_boots(5);
756        assert_eq!(builder.engine_boots, 5);
757    }
758
759    /// Build an authenticated V3 InformRequest message with the given
760    /// engine_boots and engine_time in the USM parameters.
761    fn build_authed_v3_inform(
762        engine_id: &[u8],
763        engine_boots: u32,
764        engine_time: u32,
765        username: &[u8],
766        auth_password: &[u8],
767        auth_protocol: AuthProtocol,
768    ) -> Bytes {
769        use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
770        use crate::pdu::{Pdu, PduType};
771        use crate::v3::auth::authenticate_message;
772        use crate::v3::{LocalizedKey, UsmSecurityParams};
773        use crate::value::Value;
774
775        let auth_key =
776            LocalizedKey::from_password(auth_protocol, auth_password, engine_id).unwrap();
777        let mac_len = auth_key.mac_len();
778
779        // Build an InformRequest PDU with sysUpTime.0 and snmpTrapOID.0
780        let pdu = Pdu {
781            pdu_type: PduType::InformRequest,
782            request_id: 1,
783            error_status: 0,
784            error_index: 0,
785            varbinds: vec![
786                VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
787                VarBind::new(
788                    oids::snmp_trap_oid(),
789                    Value::ObjectIdentifier(oids::cold_start()),
790                ),
791            ],
792        };
793
794        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthNoPriv, false));
795
796        let usm_params = UsmSecurityParams::new(
797            Bytes::copy_from_slice(engine_id),
798            engine_boots,
799            engine_time,
800            Bytes::copy_from_slice(username),
801        )
802        .with_auth_placeholder(mac_len);
803
804        let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
805        let msg = V3Message::new(global, usm_params.encode(), scoped);
806        let mut msg_bytes = msg.encode().to_vec();
807
808        // Compute and insert HMAC
809        let (auth_offset, auth_len) =
810            UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
811        authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
812
813        Bytes::from(msg_bytes)
814    }
815
816    #[tokio::test]
817    async fn test_v3_inform_outside_time_window_rejected() {
818        let receiver = NotificationReceiver::builder()
819            .bind("127.0.0.1:0")
820            .engine_id(b"test-engine".to_vec())
821            .engine_boots(1)
822            .usm_user("informuser", |u| {
823                u.auth(AuthProtocol::Sha1, b"authpass12345678")
824            })
825            .build()
826            .await
827            .unwrap();
828
829        let engine_id = b"test-engine";
830        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
831
832        // Engine time far in the future (5000 seconds, well beyond 150-second window)
833        let msg = build_authed_v3_inform(
834            engine_id,
835            1,    // correct boots
836            5000, // way outside time window (receiver started ~0 seconds ago)
837            b"informuser",
838            b"authpass12345678",
839            AuthProtocol::Sha1,
840        );
841
842        let result = receiver.handle_v3(msg, source).await;
843        assert!(
844            result.is_err(),
845            "message with engine_time=5000 should be rejected (outside 150s window)"
846        );
847    }
848
849    #[tokio::test]
850    async fn test_v3_inform_wrong_boots_rejected() {
851        let receiver = NotificationReceiver::builder()
852            .bind("127.0.0.1:0")
853            .engine_id(b"test-engine".to_vec())
854            .engine_boots(1)
855            .usm_user("informuser", |u| {
856                u.auth(AuthProtocol::Sha1, b"authpass12345678")
857            })
858            .build()
859            .await
860            .unwrap();
861
862        let engine_id = b"test-engine";
863        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
864
865        // Wrong engine boots (receiver has boots=1)
866        let msg = build_authed_v3_inform(
867            engine_id,
868            2, // wrong boots
869            0, // time is fine
870            b"informuser",
871            b"authpass12345678",
872            AuthProtocol::Sha1,
873        );
874
875        let result = receiver.handle_v3(msg, source).await;
876        assert!(
877            result.is_err(),
878            "message with wrong engine_boots should be rejected"
879        );
880    }
881
882    #[tokio::test]
883    async fn test_v3_inform_within_time_window_accepted() {
884        let receiver = NotificationReceiver::builder()
885            .bind("127.0.0.1:0")
886            .engine_id(b"test-engine".to_vec())
887            .engine_boots(1)
888            .usm_user("informuser", |u| {
889                u.auth(AuthProtocol::Sha1, b"authpass12345678")
890            })
891            .build()
892            .await
893            .unwrap();
894
895        let engine_id = b"test-engine";
896        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
897
898        // Engine time within the window (receiver started ~0 seconds ago, engine_time=0 is fine)
899        let msg = build_authed_v3_inform(
900            engine_id,
901            1, // correct boots
902            0, // within window
903            b"informuser",
904            b"authpass12345678",
905            AuthProtocol::Sha1,
906        );
907
908        let result = receiver.handle_v3(msg, source).await;
909        // Should succeed (or at least not fail due to time window).
910        // The Inform response send will fail since source is fake, but
911        // the time window check itself should pass. The error if any
912        // should be a network error from trying to send the response,
913        // not an Auth error.
914        match result {
915            Ok(Some(_)) => {} // unexpected but ok (socket might succeed on loopback)
916            Err(e) => {
917                let err_str = format!("{}", e);
918                assert!(
919                    !err_str.contains("Auth"),
920                    "should not be an auth error for valid time window, got: {}",
921                    err_str
922                );
923            }
924            Ok(None) => panic!("should not return None for a valid InformRequest"),
925        }
926    }
927
928    /// Build a V3 discovery request message (empty engine ID, noAuthNoPriv).
929    fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
930        use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
931        use crate::pdu::{Pdu, PduType};
932        use crate::v3::UsmSecurityParams;
933
934        let pdu = Pdu {
935            pdu_type: PduType::GetRequest,
936            request_id: 0,
937            error_status: 0,
938            error_index: 0,
939            varbinds: vec![],
940        };
941
942        let global = MsgGlobalData::new(
943            msg_id,
944            65507,
945            MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
946        );
947
948        let usm_params = UsmSecurityParams::new(
949            Bytes::new(), // empty engine ID = discovery
950            0,
951            0,
952            Bytes::new(), // empty username
953        );
954
955        let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
956        let msg = V3Message::new(global, usm_params.encode(), scoped);
957        msg.encode()
958    }
959
960    #[tokio::test]
961    async fn test_v3_discovery_gets_response() {
962        let receiver = NotificationReceiver::builder()
963            .bind("127.0.0.1:0")
964            .engine_id(b"test-discovery-engine".to_vec())
965            .build()
966            .await
967            .unwrap();
968
969        let recv_addr = receiver.local_addr();
970
971        // Bind a separate socket to send discovery and receive the response
972        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
973        let client_addr = client.local_addr().unwrap();
974
975        let discovery_msg = build_v3_discovery_request(42, true);
976        client.send_to(&discovery_msg, recv_addr).await.unwrap();
977
978        // The receiver needs to be running recv() to handle the message.
979        // Instead, call handle_v3 directly with the client address as source.
980        let result = receiver.handle_v3(discovery_msg, client_addr).await;
981
982        // Discovery should return Ok(None) - not a notification
983        assert!(result.is_ok());
984        assert!(result.unwrap().is_none());
985
986        // Counter should be incremented
987        assert_eq!(receiver.usm_unknown_engine_ids(), 1);
988    }
989
990    #[tokio::test]
991    async fn test_v3_discovery_non_reportable_ignored() {
992        let receiver = NotificationReceiver::builder()
993            .bind("127.0.0.1:0")
994            .engine_id(b"test-discovery-engine".to_vec())
995            .build()
996            .await
997            .unwrap();
998
999        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1000        let discovery_msg = build_v3_discovery_request(42, false);
1001
1002        let result = receiver.handle_v3(discovery_msg, source).await;
1003
1004        // Non-reportable discovery should return Ok(None) without incrementing counter
1005        assert!(result.is_ok());
1006        assert!(result.unwrap().is_none());
1007        assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1008    }
1009
1010    #[tokio::test]
1011    async fn test_v3_engine_id_mismatch_ignored() {
1012        let receiver = NotificationReceiver::builder()
1013            .bind("127.0.0.1:0")
1014            .engine_id(b"my-receiver-engine".to_vec())
1015            .engine_boots(1)
1016            .usm_user("informuser", |u| {
1017                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1018            })
1019            .build()
1020            .await
1021            .unwrap();
1022
1023        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1024
1025        // Build a message with a DIFFERENT engine ID
1026        let msg = build_authed_v3_inform(
1027            b"wrong-engine-id",
1028            1,
1029            0,
1030            b"informuser",
1031            b"authpass12345678",
1032            AuthProtocol::Sha1,
1033        );
1034
1035        let result = receiver.handle_v3(msg, source).await;
1036        assert!(result.is_ok());
1037        assert!(
1038            result.unwrap().is_none(),
1039            "engine ID mismatch should return None"
1040        );
1041    }
1042
1043    #[test]
1044    fn test_auto_generated_engine_id_non_empty() {
1045        let builder = NotificationReceiverBuilder::new();
1046        // engine_id field should be None (auto-generate on build)
1047        assert!(builder.engine_id.is_none());
1048    }
1049
1050    #[tokio::test]
1051    async fn test_bind_generates_engine_id() {
1052        let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1053        assert!(!receiver.engine_id().is_empty());
1054        // RFC 3411 format: starts with 0x80 enterprise indicator
1055        assert_eq!(receiver.engine_id()[0], 0x80);
1056    }
1057
1058    #[tokio::test]
1059    async fn test_builder_generates_engine_id() {
1060        let receiver = NotificationReceiver::builder()
1061            .bind("127.0.0.1:0")
1062            .build()
1063            .await
1064            .unwrap();
1065        assert!(!receiver.engine_id().is_empty());
1066        assert_eq!(receiver.engine_id()[0], 0x80);
1067    }
1068
1069    #[tokio::test]
1070    async fn test_builder_custom_engine_id() {
1071        let receiver = NotificationReceiver::builder()
1072            .bind("127.0.0.1:0")
1073            .engine_id(b"custom-engine".to_vec())
1074            .build()
1075            .await
1076            .unwrap();
1077        assert_eq!(receiver.engine_id(), b"custom-engine");
1078    }
1079}