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}