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