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 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<(), 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(|_| Error::Io {
195            target: None,
196            source: std::io::Error::new(
197                std::io::ErrorKind::InvalidInput,
198                format!("invalid bind address: {}", self.bind_addr),
199            ),
200        })?;
201
202        let socket = bind_udp_socket(bind_addr).await.map_err(|e| Error::Io {
203            target: Some(bind_addr),
204            source: e,
205        })?;
206
207        let local_addr = socket.local_addr().map_err(|e| Error::Io {
208            target: Some(bind_addr),
209            source: e,
210        })?;
211
212        Ok(NotificationReceiver {
213            inner: Arc::new(ReceiverInner {
214                socket,
215                local_addr,
216                usm_users: self.usm_users,
217                salt_counter: SaltCounter::new(),
218            }),
219        })
220    }
221}
222
223impl Default for NotificationReceiverBuilder {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Received SNMP notification.
230///
231/// This enum represents all types of SNMP notifications that can be received:
232/// - SNMPv1 Trap (different PDU structure)
233/// - SNMPv2c/v3 Trap (standard PDU with sysUpTime.0 and snmpTrapOID.0)
234/// - InformRequest (confirmed notification, response will be sent automatically)
235#[derive(Debug, Clone)]
236pub enum Notification {
237    /// SNMPv1 Trap with unique PDU structure.
238    TrapV1 {
239        /// Community string used for authentication
240        community: Bytes,
241        /// The trap PDU
242        trap: TrapV1Pdu,
243    },
244
245    /// SNMPv2c Trap (unconfirmed notification).
246    TrapV2c {
247        /// Community string used for authentication
248        community: Bytes,
249        /// sysUpTime.0 value (hundredths of seconds since agent init)
250        uptime: u32,
251        /// snmpTrapOID.0 value (trap type identifier)
252        trap_oid: Oid,
253        /// Additional variable bindings
254        varbinds: Vec<VarBind>,
255        /// Original request ID (for logging/correlation)
256        request_id: i32,
257    },
258
259    /// SNMPv3 Trap (unconfirmed notification).
260    TrapV3 {
261        /// Username from USM
262        username: Bytes,
263        /// Context engine ID
264        context_engine_id: Bytes,
265        /// Context name
266        context_name: Bytes,
267        /// sysUpTime.0 value
268        uptime: u32,
269        /// snmpTrapOID.0 value
270        trap_oid: Oid,
271        /// Additional variable bindings
272        varbinds: Vec<VarBind>,
273        /// Original request ID
274        request_id: i32,
275    },
276
277    /// InformRequest (confirmed notification) - v2c.
278    ///
279    /// A response is automatically sent when this notification is received.
280    InformV2c {
281        /// Community string
282        community: Bytes,
283        /// sysUpTime.0 value
284        uptime: u32,
285        /// snmpTrapOID.0 value
286        trap_oid: Oid,
287        /// Additional variable bindings
288        varbinds: Vec<VarBind>,
289        /// Request ID (used in response)
290        request_id: i32,
291    },
292
293    /// InformRequest (confirmed notification) - v3.
294    ///
295    /// A response is automatically sent when this notification is received.
296    InformV3 {
297        /// Username from USM
298        username: Bytes,
299        /// Context engine ID
300        context_engine_id: Bytes,
301        /// Context name
302        context_name: Bytes,
303        /// sysUpTime.0 value
304        uptime: u32,
305        /// snmpTrapOID.0 value
306        trap_oid: Oid,
307        /// Additional variable bindings
308        varbinds: Vec<VarBind>,
309        /// Request ID
310        request_id: i32,
311    },
312}
313
314impl Notification {
315    /// Get the trap/notification OID.
316    ///
317    /// For TrapV1, this is derived from enterprise + generic/specific trap.
318    /// For v2c/v3, this is the snmpTrapOID.0 value.
319    pub fn trap_oid(&self) -> &Oid {
320        match self {
321            Notification::TrapV1 { trap, .. } => &trap.enterprise,
322            Notification::TrapV2c { trap_oid, .. }
323            | Notification::TrapV3 { trap_oid, .. }
324            | Notification::InformV2c { trap_oid, .. }
325            | Notification::InformV3 { trap_oid, .. } => trap_oid,
326        }
327    }
328
329    /// Get the uptime value (sysUpTime.0 or time_stamp for v1).
330    pub fn uptime(&self) -> u32 {
331        match self {
332            Notification::TrapV1 { trap, .. } => trap.time_stamp,
333            Notification::TrapV2c { uptime, .. }
334            | Notification::TrapV3 { uptime, .. }
335            | Notification::InformV2c { uptime, .. }
336            | Notification::InformV3 { uptime, .. } => *uptime,
337        }
338    }
339
340    /// Get the variable bindings.
341    pub fn varbinds(&self) -> &[VarBind] {
342        match self {
343            Notification::TrapV1 { trap, .. } => &trap.varbinds,
344            Notification::TrapV2c { varbinds, .. }
345            | Notification::TrapV3 { varbinds, .. }
346            | Notification::InformV2c { varbinds, .. }
347            | Notification::InformV3 { varbinds, .. } => varbinds,
348        }
349    }
350
351    /// Check if this is a confirmed notification (InformRequest).
352    pub fn is_confirmed(&self) -> bool {
353        matches!(
354            self,
355            Notification::InformV2c { .. } | Notification::InformV3 { .. }
356        )
357    }
358
359    /// Get the SNMP version of this notification.
360    pub fn version(&self) -> Version {
361        match self {
362            Notification::TrapV1 { .. } => Version::V1,
363            Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
364            Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
365        }
366    }
367}
368
369/// SNMP Notification Receiver.
370///
371/// Listens for incoming SNMP notifications (traps and informs) on a UDP socket.
372/// For InformRequest notifications, automatically sends a Response-PDU.
373///
374/// # V3 Authentication
375///
376/// To receive authenticated V3 notifications, use the builder pattern to configure
377/// USM credentials:
378///
379/// ```rust,no_run
380/// use async_snmp::notification::NotificationReceiver;
381/// use async_snmp::{AuthProtocol, PrivProtocol};
382///
383/// # async fn example() -> Result<(), async_snmp::Error> {
384/// let receiver = NotificationReceiver::builder()
385///     .bind("0.0.0.0:162")
386///     .usm_user("trapuser", |u| {
387///         u.auth(AuthProtocol::Sha1, b"authpassword")
388///     })
389///     .build()
390///     .await?;
391/// # Ok(())
392/// # }
393/// ```
394pub struct NotificationReceiver {
395    inner: Arc<ReceiverInner>,
396}
397
398struct ReceiverInner {
399    socket: UdpSocket,
400    local_addr: SocketAddr,
401    /// Configured USM users for V3 authentication
402    usm_users: HashMap<Bytes, UsmUserConfig>,
403    /// Salt counter for privacy operations
404    salt_counter: SaltCounter,
405}
406
407impl NotificationReceiver {
408    /// Create a builder for configuring the notification receiver.
409    ///
410    /// Use this to configure USM credentials for V3 authentication.
411    pub fn builder() -> NotificationReceiverBuilder {
412        NotificationReceiverBuilder::new()
413    }
414
415    /// Bind to a local address.
416    ///
417    /// The standard SNMP notification port is 162.
418    /// For V3 authentication support, use `NotificationReceiver::builder()` instead.
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}