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}