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