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;
59
60use bytes::Bytes;
61use tokio::net::UdpSocket;
62use tracing::instrument;
63
64use crate::ber::Decoder;
65use crate::error::internal::DecodeErrorKind;
66use crate::error::{Error, Result};
67use crate::oid::Oid;
68use crate::pdu::TrapV1Pdu;
69use crate::util::bind_udp_socket;
70use crate::v3::SaltCounter;
71use crate::varbind::VarBind;
72use crate::version::Version;
73
74// Re-exports
75pub use types::{DerivedKeys, UsmConfig, UsmUserConfig};
76pub use varbind::validate_notification_varbinds;
77
78/// Well-known OIDs for notification varbinds.
79pub mod oids {
80    use crate::oid;
81
82    /// sysUpTime.0 - first varbind in v2c/v3 notifications
83    pub fn sys_uptime() -> crate::Oid {
84        oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
85    }
86
87    /// snmpTrapOID.0 - second varbind in v2c/v3 notifications (contains trap type)
88    pub fn snmp_trap_oid() -> crate::Oid {
89        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
90    }
91
92    /// snmpTrapEnterprise.0 - optional, enterprise OID for enterprise-specific traps
93    pub fn snmp_trap_enterprise() -> crate::Oid {
94        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
95    }
96
97    /// Standard trap OID prefix (snmpTraps)
98    pub fn snmp_traps() -> crate::Oid {
99        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
100    }
101
102    /// coldStart trap OID (snmpTraps.1)
103    pub fn cold_start() -> crate::Oid {
104        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
105    }
106
107    /// warmStart trap OID (snmpTraps.2)
108    pub fn warm_start() -> crate::Oid {
109        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
110    }
111
112    /// linkDown trap OID (snmpTraps.3)
113    pub fn link_down() -> crate::Oid {
114        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
115    }
116
117    /// linkUp trap OID (snmpTraps.4)
118    pub fn link_up() -> crate::Oid {
119        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
120    }
121
122    /// authenticationFailure trap OID (snmpTraps.5)
123    pub fn auth_failure() -> crate::Oid {
124        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
125    }
126
127    /// egpNeighborLoss trap OID (snmpTraps.6)
128    pub fn egp_neighbor_loss() -> crate::Oid {
129        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
130    }
131}
132
133/// Builder for `NotificationReceiver`.
134///
135/// Allows configuration of bind address and USM credentials for V3 support.
136pub struct NotificationReceiverBuilder {
137    bind_addr: String,
138    usm_users: HashMap<Bytes, UsmUserConfig>,
139}
140
141impl NotificationReceiverBuilder {
142    /// Create a new builder with default settings.
143    ///
144    /// Defaults:
145    /// - Bind address: `0.0.0.0:162` (UDP, standard SNMP trap port)
146    /// - No USM users (v3 notifications rejected until users are added)
147    pub fn new() -> Self {
148        Self {
149            bind_addr: "0.0.0.0:162".to_string(),
150            usm_users: HashMap::new(),
151        }
152    }
153
154    /// Set the UDP bind address.
155    ///
156    /// Default is `0.0.0.0:162` (UDP, standard SNMP trap port).
157    pub fn bind(mut self, addr: impl Into<String>) -> Self {
158        self.bind_addr = addr.into();
159        self
160    }
161
162    /// Add a USM user for V3 authentication.
163    ///
164    /// # Example
165    ///
166    /// ```rust,no_run
167    /// use async_snmp::notification::NotificationReceiver;
168    /// use async_snmp::{AuthProtocol, PrivProtocol};
169    ///
170    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
171    /// let receiver = NotificationReceiver::builder()
172    ///     .bind("0.0.0.0:162")
173    ///     .usm_user("trapuser", |u| {
174    ///         u.auth(AuthProtocol::Sha1, b"authpassword")
175    ///          .privacy(PrivProtocol::Aes128, b"privpassword")
176    ///     })
177    ///     .build()
178    ///     .await?;
179    /// # Ok(())
180    /// # }
181    /// ```
182    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
183    where
184        F: FnOnce(UsmUserConfig) -> UsmUserConfig,
185    {
186        let username_bytes: Bytes = username.into();
187        let config = configure(UsmUserConfig::new(username_bytes.clone()));
188        self.usm_users.insert(username_bytes, config);
189        self
190    }
191
192    /// Build the notification receiver.
193    pub async fn build(self) -> Result<NotificationReceiver> {
194        let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
195            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
196        })?;
197
198        let socket = bind_udp_socket(bind_addr, None)
199            .await
200            .map_err(|e| Error::Network {
201                target: bind_addr,
202                source: e,
203            })?;
204
205        let local_addr = socket.local_addr().map_err(|e| Error::Network {
206            target: bind_addr,
207            source: e,
208        })?;
209
210        Ok(NotificationReceiver {
211            inner: Arc::new(ReceiverInner {
212                socket,
213                local_addr,
214                usm_users: self.usm_users,
215                salt_counter: SaltCounter::new(),
216            }),
217        })
218    }
219}
220
221impl Default for NotificationReceiverBuilder {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227/// Received SNMP notification.
228///
229/// This enum represents all types of SNMP notifications that can be received:
230/// - SNMPv1 Trap (different PDU structure)
231/// - SNMPv2c/v3 Trap (standard PDU with sysUpTime.0 and snmpTrapOID.0)
232/// - InformRequest (confirmed notification, response will be sent automatically)
233#[derive(Debug, Clone)]
234pub enum Notification {
235    /// SNMPv1 Trap with unique PDU structure.
236    TrapV1 {
237        /// Community string used for authentication
238        community: Bytes,
239        /// The trap PDU
240        trap: TrapV1Pdu,
241    },
242
243    /// SNMPv2c Trap (unconfirmed notification).
244    TrapV2c {
245        /// Community string used for authentication
246        community: Bytes,
247        /// sysUpTime.0 value (hundredths of seconds since agent init)
248        uptime: u32,
249        /// snmpTrapOID.0 value (trap type identifier)
250        trap_oid: Oid,
251        /// Additional variable bindings
252        varbinds: Vec<VarBind>,
253        /// Original request ID (for logging/correlation)
254        request_id: i32,
255    },
256
257    /// SNMPv3 Trap (unconfirmed notification).
258    TrapV3 {
259        /// Username from USM
260        username: Bytes,
261        /// Context engine ID
262        context_engine_id: Bytes,
263        /// Context name
264        context_name: Bytes,
265        /// sysUpTime.0 value
266        uptime: u32,
267        /// snmpTrapOID.0 value
268        trap_oid: Oid,
269        /// Additional variable bindings
270        varbinds: Vec<VarBind>,
271        /// Original request ID
272        request_id: i32,
273    },
274
275    /// InformRequest (confirmed notification) - v2c.
276    ///
277    /// A response is automatically sent when this notification is received.
278    InformV2c {
279        /// Community string
280        community: Bytes,
281        /// sysUpTime.0 value
282        uptime: u32,
283        /// snmpTrapOID.0 value
284        trap_oid: Oid,
285        /// Additional variable bindings
286        varbinds: Vec<VarBind>,
287        /// Request ID (used in response)
288        request_id: i32,
289    },
290
291    /// InformRequest (confirmed notification) - v3.
292    ///
293    /// A response is automatically sent when this notification is received.
294    InformV3 {
295        /// Username from USM
296        username: Bytes,
297        /// Context engine ID
298        context_engine_id: Bytes,
299        /// Context name
300        context_name: Bytes,
301        /// sysUpTime.0 value
302        uptime: u32,
303        /// snmpTrapOID.0 value
304        trap_oid: Oid,
305        /// Additional variable bindings
306        varbinds: Vec<VarBind>,
307        /// Request ID
308        request_id: i32,
309    },
310}
311
312impl Notification {
313    /// Get the trap/notification OID.
314    ///
315    /// For TrapV1, this is derived from enterprise + generic/specific trap.
316    /// For v2c/v3, this is the snmpTrapOID.0 value.
317    pub fn trap_oid(&self) -> &Oid {
318        match self {
319            Notification::TrapV1 { trap, .. } => &trap.enterprise,
320            Notification::TrapV2c { trap_oid, .. }
321            | Notification::TrapV3 { trap_oid, .. }
322            | Notification::InformV2c { trap_oid, .. }
323            | Notification::InformV3 { trap_oid, .. } => trap_oid,
324        }
325    }
326
327    /// Get the uptime value (sysUpTime.0 or time_stamp for v1).
328    pub fn uptime(&self) -> u32 {
329        match self {
330            Notification::TrapV1 { trap, .. } => trap.time_stamp,
331            Notification::TrapV2c { uptime, .. }
332            | Notification::TrapV3 { uptime, .. }
333            | Notification::InformV2c { uptime, .. }
334            | Notification::InformV3 { uptime, .. } => *uptime,
335        }
336    }
337
338    /// Get the variable bindings.
339    pub fn varbinds(&self) -> &[VarBind] {
340        match self {
341            Notification::TrapV1 { trap, .. } => &trap.varbinds,
342            Notification::TrapV2c { varbinds, .. }
343            | Notification::TrapV3 { varbinds, .. }
344            | Notification::InformV2c { varbinds, .. }
345            | Notification::InformV3 { varbinds, .. } => varbinds,
346        }
347    }
348
349    /// Check if this is a confirmed notification (InformRequest).
350    pub fn is_confirmed(&self) -> bool {
351        matches!(
352            self,
353            Notification::InformV2c { .. } | Notification::InformV3 { .. }
354        )
355    }
356
357    /// Get the SNMP version of this notification.
358    pub fn version(&self) -> Version {
359        match self {
360            Notification::TrapV1 { .. } => Version::V1,
361            Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
362            Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
363        }
364    }
365}
366
367/// SNMP Notification Receiver.
368///
369/// Listens for incoming SNMP notifications (traps and informs) on a UDP socket.
370/// For InformRequest notifications, automatically sends a Response-PDU.
371///
372/// # V3 Authentication
373///
374/// To receive authenticated V3 notifications, use the builder pattern to configure
375/// USM credentials:
376///
377/// ```rust,no_run
378/// use async_snmp::notification::NotificationReceiver;
379/// use async_snmp::{AuthProtocol, PrivProtocol};
380///
381/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
382/// let receiver = NotificationReceiver::builder()
383///     .bind("0.0.0.0:162")
384///     .usm_user("trapuser", |u| {
385///         u.auth(AuthProtocol::Sha1, b"authpassword")
386///     })
387///     .build()
388///     .await?;
389/// # Ok(())
390/// # }
391/// ```
392pub struct NotificationReceiver {
393    inner: Arc<ReceiverInner>,
394}
395
396struct ReceiverInner {
397    socket: UdpSocket,
398    local_addr: SocketAddr,
399    /// Configured USM users for V3 authentication
400    usm_users: HashMap<Bytes, UsmUserConfig>,
401    /// Salt counter for privacy operations
402    salt_counter: SaltCounter,
403}
404
405impl NotificationReceiver {
406    /// Create a builder for configuring the notification receiver.
407    ///
408    /// Use this to configure USM credentials for V3 authentication.
409    pub fn builder() -> NotificationReceiverBuilder {
410        NotificationReceiverBuilder::new()
411    }
412
413    /// Bind to a local address.
414    ///
415    /// The standard SNMP notification port is 162.
416    /// For V3 authentication support, use `NotificationReceiver::builder()` instead.
417    ///
418    /// # Example
419    ///
420    /// ```rust,no_run
421    /// use async_snmp::notification::NotificationReceiver;
422    ///
423    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
424    /// // Bind to the standard trap port (requires root/admin on most systems)
425    /// let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
426    ///
427    /// // Or use an unprivileged port for testing
428    /// let receiver = NotificationReceiver::bind("0.0.0.0:1162").await?;
429    /// # Ok(())
430    /// # }
431    /// ```
432    pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
433        let addr_str = addr.as_ref();
434        let bind_addr: SocketAddr = addr_str
435            .parse()
436            .map_err(|_| Error::Config(format!("invalid bind address: {}", addr_str).into()))?;
437
438        let socket = bind_udp_socket(bind_addr, None)
439            .await
440            .map_err(|e| Error::Network {
441                target: bind_addr,
442                source: e,
443            })?;
444
445        let local_addr = socket.local_addr().map_err(|e| Error::Network {
446            target: bind_addr,
447            source: e,
448        })?;
449
450        Ok(Self {
451            inner: Arc::new(ReceiverInner {
452                socket,
453                local_addr,
454                usm_users: HashMap::new(),
455                salt_counter: SaltCounter::new(),
456            }),
457        })
458    }
459
460    /// Get the local address this receiver is bound to.
461    pub fn local_addr(&self) -> SocketAddr {
462        self.inner.local_addr
463    }
464
465    /// Receive a notification.
466    ///
467    /// This method blocks until a notification is received. For InformRequest
468    /// notifications, a Response-PDU is automatically sent back to the sender.
469    ///
470    /// Returns the notification and the source address.
471    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
472    pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
473        let mut buf = vec![0u8; 65535];
474
475        loop {
476            let (len, source) =
477                self.inner
478                    .socket
479                    .recv_from(&mut buf)
480                    .await
481                    .map_err(|e| Error::Network {
482                        target: self.inner.local_addr,
483                        source: e,
484                    })?;
485
486            let data = Bytes::copy_from_slice(&buf[..len]);
487
488            match self.parse_and_respond(data, source).await {
489                Ok(Some(notification)) => return Ok((notification, source)),
490                Ok(None) => continue, // Not a notification PDU, ignore
491                Err(e) => {
492                    // Log parsing error but continue receiving
493                    tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
494                    continue;
495                }
496            }
497        }
498    }
499
500    /// Parse received data and send response if needed.
501    ///
502    /// Returns `None` if the message is not a notification PDU.
503    async fn parse_and_respond(
504        &self,
505        data: Bytes,
506        source: SocketAddr,
507    ) -> Result<Option<Notification>> {
508        // First, peek at the version to determine message type
509        let mut decoder = Decoder::with_target(data.clone(), source);
510        let mut seq = decoder.read_sequence()?;
511        let version_num = seq.read_integer()?;
512        let version = Version::from_i32(version_num).ok_or_else(|| {
513            tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
514            Error::MalformedResponse { target: source }.boxed()
515        })?;
516        drop(seq);
517        drop(decoder);
518
519        match version {
520            Version::V1 => self.handle_v1(data, source).await,
521            Version::V2c => self.handle_v2c(data, source).await,
522            Version::V3 => self.handle_v3(data, source).await,
523        }
524    }
525}
526
527impl Clone for NotificationReceiver {
528    fn clone(&self) -> Self {
529        Self {
530            inner: Arc::clone(&self.inner),
531        }
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use crate::message::SecurityLevel;
539    use crate::oid;
540    use crate::pdu::GenericTrap;
541    use crate::v3::AuthProtocol;
542
543    #[test]
544    fn test_notification_trap_v1() {
545        let trap = TrapV1Pdu::new(
546            oid!(1, 3, 6, 1, 4, 1, 9999),
547            [192, 168, 1, 1],
548            GenericTrap::LinkDown,
549            0,
550            12345,
551            vec![],
552        );
553
554        let notification = Notification::TrapV1 {
555            community: Bytes::from_static(b"public"),
556            trap,
557        };
558
559        assert!(!notification.is_confirmed());
560        assert_eq!(notification.version(), Version::V1);
561        assert_eq!(notification.uptime(), 12345);
562    }
563
564    #[test]
565    fn test_notification_trap_v2c() {
566        let notification = Notification::TrapV2c {
567            community: Bytes::from_static(b"public"),
568            uptime: 54321,
569            trap_oid: oids::link_up(),
570            varbinds: vec![],
571            request_id: 1,
572        };
573
574        assert!(!notification.is_confirmed());
575        assert_eq!(notification.version(), Version::V2c);
576        assert_eq!(notification.uptime(), 54321);
577        assert_eq!(notification.trap_oid(), &oids::link_up());
578    }
579
580    #[test]
581    fn test_notification_inform() {
582        let notification = Notification::InformV2c {
583            community: Bytes::from_static(b"public"),
584            uptime: 11111,
585            trap_oid: oids::cold_start(),
586            varbinds: vec![],
587            request_id: 42,
588        };
589
590        assert!(notification.is_confirmed());
591        assert_eq!(notification.version(), Version::V2c);
592    }
593
594    #[test]
595    fn test_notification_receiver_builder_default() {
596        let builder = NotificationReceiverBuilder::new();
597        assert_eq!(builder.bind_addr, "0.0.0.0:162");
598        assert!(builder.usm_users.is_empty());
599    }
600
601    #[test]
602    fn test_notification_receiver_builder_with_user() {
603        let builder = NotificationReceiverBuilder::new()
604            .bind("0.0.0.0:1162")
605            .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
606
607        assert_eq!(builder.bind_addr, "0.0.0.0:1162");
608        assert_eq!(builder.usm_users.len(), 1);
609
610        let user = builder
611            .usm_users
612            .get(&Bytes::from_static(b"trapuser"))
613            .unwrap();
614        assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
615    }
616
617    #[test]
618    fn test_notification_v3_inform() {
619        let notification = Notification::InformV3 {
620            username: Bytes::from_static(b"testuser"),
621            context_engine_id: Bytes::from_static(b"engine123"),
622            context_name: Bytes::new(),
623            uptime: 99999,
624            trap_oid: oids::warm_start(),
625            varbinds: vec![],
626            request_id: 100,
627        };
628
629        assert!(notification.is_confirmed());
630        assert_eq!(notification.version(), Version::V3);
631        assert_eq!(notification.uptime(), 99999);
632        assert_eq!(notification.trap_oid(), &oids::warm_start());
633    }
634}