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<(), 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<(), 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::{DecodeErrorKind, Error, Result};
66use crate::oid::Oid;
67use crate::pdu::TrapV1Pdu;
68use crate::util::bind_udp_socket;
69use crate::v3::SaltCounter;
70use crate::varbind::VarBind;
71use crate::version::Version;
72
73// Re-exports
74pub(crate) use types::DerivedKeys;
75pub use types::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.
143    pub fn new() -> Self {
144        Self {
145            bind_addr: "0.0.0.0:162".to_string(),
146            usm_users: HashMap::new(),
147        }
148    }
149
150    /// Set the bind address.
151    ///
152    /// Default is "0.0.0.0:162" (standard trap port).
153    pub fn bind(mut self, addr: impl Into<String>) -> Self {
154        self.bind_addr = addr.into();
155        self
156    }
157
158    /// Add a USM user for V3 authentication.
159    ///
160    /// # Example
161    ///
162    /// ```rust,no_run
163    /// use async_snmp::notification::NotificationReceiver;
164    /// use async_snmp::{AuthProtocol, PrivProtocol};
165    ///
166    /// # async fn example() -> Result<(), async_snmp::Error> {
167    /// let receiver = NotificationReceiver::builder()
168    ///     .bind("0.0.0.0:162")
169    ///     .usm_user("trapuser", |u| {
170    ///         u.auth(AuthProtocol::Sha1, b"authpassword")
171    ///          .privacy(PrivProtocol::Aes128, b"privpassword")
172    ///     })
173    ///     .build()
174    ///     .await?;
175    /// # Ok(())
176    /// # }
177    /// ```
178    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
179    where
180        F: FnOnce(UsmUserConfig) -> UsmUserConfig,
181    {
182        let username_bytes: Bytes = username.into();
183        let config = configure(UsmUserConfig::new(username_bytes.clone()));
184        self.usm_users.insert(username_bytes, config);
185        self
186    }
187
188    /// Build the notification receiver.
189    ///
190    /// For IPv6 bind addresses, the socket has `IPV6_V6ONLY` set to true.
191    pub async fn build(self) -> Result<NotificationReceiver> {
192        let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| Error::Io {
193            target: None,
194            source: std::io::Error::new(
195                std::io::ErrorKind::InvalidInput,
196                format!("invalid bind address: {}", self.bind_addr),
197            ),
198        })?;
199
200        let socket = bind_udp_socket(bind_addr).await.map_err(|e| Error::Io {
201            target: Some(bind_addr),
202            source: e,
203        })?;
204
205        let local_addr = socket.local_addr().map_err(|e| Error::Io {
206            target: Some(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<(), 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    /// For IPv6 bind addresses, the socket has `IPV6_V6ONLY` set to true.
419    ///
420    /// # Example
421    ///
422    /// ```rust,no_run
423    /// use async_snmp::notification::NotificationReceiver;
424    ///
425    /// # async fn example() -> Result<(), async_snmp::Error> {
426    /// // Bind to the standard trap port (requires root/admin on most systems)
427    /// let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
428    ///
429    /// // Or use an unprivileged port for testing
430    /// let receiver = NotificationReceiver::bind("0.0.0.0:1162").await?;
431    /// # Ok(())
432    /// # }
433    /// ```
434    pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
435        let addr_str = addr.as_ref();
436        let bind_addr: SocketAddr = addr_str.parse().map_err(|_| Error::Io {
437            target: None,
438            source: std::io::Error::new(
439                std::io::ErrorKind::InvalidInput,
440                format!("invalid bind address: {}", addr_str),
441            ),
442        })?;
443
444        let socket = bind_udp_socket(bind_addr).await.map_err(|e| Error::Io {
445            target: Some(bind_addr),
446            source: e,
447        })?;
448
449        let local_addr = socket.local_addr().map_err(|e| Error::Io {
450            target: Some(bind_addr),
451            source: e,
452        })?;
453
454        Ok(Self {
455            inner: Arc::new(ReceiverInner {
456                socket,
457                local_addr,
458                usm_users: HashMap::new(),
459                salt_counter: SaltCounter::new(),
460            }),
461        })
462    }
463
464    /// Get the local address this receiver is bound to.
465    pub fn local_addr(&self) -> SocketAddr {
466        self.inner.local_addr
467    }
468
469    /// Receive a notification.
470    ///
471    /// This method blocks until a notification is received. For InformRequest
472    /// notifications, a Response-PDU is automatically sent back to the sender.
473    ///
474    /// Returns the notification and the source address.
475    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
476    pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
477        let mut buf = vec![0u8; 65535];
478
479        loop {
480            let (len, source) =
481                self.inner
482                    .socket
483                    .recv_from(&mut buf)
484                    .await
485                    .map_err(|e| Error::Io {
486                        target: None,
487                        source: e,
488                    })?;
489
490            let data = Bytes::copy_from_slice(&buf[..len]);
491
492            match self.parse_and_respond(data, source).await {
493                Ok(Some(notification)) => return Ok((notification, source)),
494                Ok(None) => continue, // Not a notification PDU, ignore
495                Err(e) => {
496                    // Log parsing error but continue receiving
497                    tracing::warn!(snmp.source = %source, error = %e, "failed to parse notification");
498                    continue;
499                }
500            }
501        }
502    }
503
504    /// Parse received data and send response if needed.
505    ///
506    /// Returns `None` if the message is not a notification PDU.
507    async fn parse_and_respond(
508        &self,
509        data: Bytes,
510        source: SocketAddr,
511    ) -> Result<Option<Notification>> {
512        // First, peek at the version to determine message type
513        let mut decoder = Decoder::new(data.clone());
514        let mut seq = decoder.read_sequence()?;
515        let version_num = seq.read_integer()?;
516        let version = Version::from_i32(version_num).ok_or_else(|| {
517            Error::decode(seq.offset(), DecodeErrorKind::UnknownVersion(version_num))
518        })?;
519        drop(seq);
520        drop(decoder);
521
522        match version {
523            Version::V1 => self.handle_v1(data, source).await,
524            Version::V2c => self.handle_v2c(data, source).await,
525            Version::V3 => self.handle_v3(data, source).await,
526        }
527    }
528}
529
530impl Clone for NotificationReceiver {
531    fn clone(&self) -> Self {
532        Self {
533            inner: Arc::clone(&self.inner),
534        }
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::message::SecurityLevel;
542    use crate::oid;
543    use crate::pdu::GenericTrap;
544    use crate::v3::AuthProtocol;
545
546    #[test]
547    fn test_notification_trap_v1() {
548        let trap = TrapV1Pdu::new(
549            oid!(1, 3, 6, 1, 4, 1, 9999),
550            [192, 168, 1, 1],
551            GenericTrap::LinkDown,
552            0,
553            12345,
554            vec![],
555        );
556
557        let notification = Notification::TrapV1 {
558            community: Bytes::from_static(b"public"),
559            trap,
560        };
561
562        assert!(!notification.is_confirmed());
563        assert_eq!(notification.version(), Version::V1);
564        assert_eq!(notification.uptime(), 12345);
565    }
566
567    #[test]
568    fn test_notification_trap_v2c() {
569        let notification = Notification::TrapV2c {
570            community: Bytes::from_static(b"public"),
571            uptime: 54321,
572            trap_oid: oids::link_up(),
573            varbinds: vec![],
574            request_id: 1,
575        };
576
577        assert!(!notification.is_confirmed());
578        assert_eq!(notification.version(), Version::V2c);
579        assert_eq!(notification.uptime(), 54321);
580        assert_eq!(notification.trap_oid(), &oids::link_up());
581    }
582
583    #[test]
584    fn test_notification_inform() {
585        let notification = Notification::InformV2c {
586            community: Bytes::from_static(b"public"),
587            uptime: 11111,
588            trap_oid: oids::cold_start(),
589            varbinds: vec![],
590            request_id: 42,
591        };
592
593        assert!(notification.is_confirmed());
594        assert_eq!(notification.version(), Version::V2c);
595    }
596
597    #[test]
598    fn test_notification_receiver_builder_default() {
599        let builder = NotificationReceiverBuilder::new();
600        assert_eq!(builder.bind_addr, "0.0.0.0:162");
601        assert!(builder.usm_users.is_empty());
602    }
603
604    #[test]
605    fn test_notification_receiver_builder_with_user() {
606        let builder = NotificationReceiverBuilder::new()
607            .bind("0.0.0.0:1162")
608            .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
609
610        assert_eq!(builder.bind_addr, "0.0.0.0:1162");
611        assert_eq!(builder.usm_users.len(), 1);
612
613        let user = builder
614            .usm_users
615            .get(&Bytes::from_static(b"trapuser"))
616            .unwrap();
617        assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
618    }
619
620    #[test]
621    fn test_notification_v3_inform() {
622        let notification = Notification::InformV3 {
623            username: Bytes::from_static(b"testuser"),
624            context_engine_id: Bytes::from_static(b"engine123"),
625            context_name: Bytes::new(),
626            uptime: 99999,
627            trap_oid: oids::warm_start(),
628            varbinds: vec![],
629            request_id: 100,
630        };
631
632        assert!(notification.is_confirmed());
633        assert_eq!(notification.version(), Version::V3);
634        assert_eq!(notification.uptime(), 99999);
635        assert_eq!(notification.trap_oid(), &oids::warm_start());
636    }
637}