1mod 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::internal::DecodeErrorKind;
66use crate::error::{Error, Result};
67use crate::oid::Oid;
68use crate::pdu::TrapV1Pdu;
69use crate::util::bind_udp_socket;
70use crate::v3::SaltCounter;
71use crate::varbind::VarBind;
72use crate::version::Version;
73
74pub(crate) use types::DerivedKeys;
76pub use types::UsmUserConfig;
77pub use varbind::validate_notification_varbinds;
78
79pub mod oids {
81 use crate::oid;
82
83 pub fn sys_uptime() -> crate::Oid {
85 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
86 }
87
88 pub fn snmp_trap_oid() -> crate::Oid {
90 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
91 }
92
93 pub fn snmp_trap_enterprise() -> crate::Oid {
95 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
96 }
97
98 pub fn snmp_traps() -> crate::Oid {
100 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
101 }
102
103 pub fn cold_start() -> crate::Oid {
105 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
106 }
107
108 pub fn warm_start() -> crate::Oid {
110 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
111 }
112
113 pub fn link_down() -> crate::Oid {
115 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
116 }
117
118 pub fn link_up() -> crate::Oid {
120 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
121 }
122
123 pub fn auth_failure() -> crate::Oid {
125 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
126 }
127
128 pub fn egp_neighbor_loss() -> crate::Oid {
130 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
131 }
132}
133
134pub struct NotificationReceiverBuilder {
138 bind_addr: String,
139 usm_users: HashMap<Bytes, UsmUserConfig>,
140}
141
142impl NotificationReceiverBuilder {
143 pub fn new() -> Self {
149 Self {
150 bind_addr: "0.0.0.0:162".to_string(),
151 usm_users: HashMap::new(),
152 }
153 }
154
155 pub fn bind(mut self, addr: impl Into<String>) -> Self {
159 self.bind_addr = addr.into();
160 self
161 }
162
163 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
184 where
185 F: FnOnce(UsmUserConfig) -> UsmUserConfig,
186 {
187 let username_bytes: Bytes = username.into();
188 let config = configure(UsmUserConfig::new(username_bytes.clone()));
189 self.usm_users.insert(username_bytes, config);
190 self
191 }
192
193 pub async fn build(self) -> Result<NotificationReceiver> {
195 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
196 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
197 })?;
198
199 let socket = bind_udp_socket(bind_addr, None)
200 .await
201 .map_err(|e| Error::Network {
202 target: bind_addr,
203 source: e,
204 })?;
205
206 let local_addr = socket.local_addr().map_err(|e| Error::Network {
207 target: bind_addr,
208 source: e,
209 })?;
210
211 Ok(NotificationReceiver {
212 inner: Arc::new(ReceiverInner {
213 socket,
214 local_addr,
215 usm_users: self.usm_users,
216 salt_counter: SaltCounter::new(),
217 }),
218 })
219 }
220}
221
222impl Default for NotificationReceiverBuilder {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228#[derive(Debug, Clone)]
235pub enum Notification {
236 TrapV1 {
238 community: Bytes,
240 trap: TrapV1Pdu,
242 },
243
244 TrapV2c {
246 community: Bytes,
248 uptime: u32,
250 trap_oid: Oid,
252 varbinds: Vec<VarBind>,
254 request_id: i32,
256 },
257
258 TrapV3 {
260 username: Bytes,
262 context_engine_id: Bytes,
264 context_name: Bytes,
266 uptime: u32,
268 trap_oid: Oid,
270 varbinds: Vec<VarBind>,
272 request_id: i32,
274 },
275
276 InformV2c {
280 community: Bytes,
282 uptime: u32,
284 trap_oid: Oid,
286 varbinds: Vec<VarBind>,
288 request_id: i32,
290 },
291
292 InformV3 {
296 username: Bytes,
298 context_engine_id: Bytes,
300 context_name: Bytes,
302 uptime: u32,
304 trap_oid: Oid,
306 varbinds: Vec<VarBind>,
308 request_id: i32,
310 },
311}
312
313impl Notification {
314 pub fn trap_oid(&self) -> &Oid {
319 match self {
320 Notification::TrapV1 { trap, .. } => &trap.enterprise,
321 Notification::TrapV2c { trap_oid, .. }
322 | Notification::TrapV3 { trap_oid, .. }
323 | Notification::InformV2c { trap_oid, .. }
324 | Notification::InformV3 { trap_oid, .. } => trap_oid,
325 }
326 }
327
328 pub fn uptime(&self) -> u32 {
330 match self {
331 Notification::TrapV1 { trap, .. } => trap.time_stamp,
332 Notification::TrapV2c { uptime, .. }
333 | Notification::TrapV3 { uptime, .. }
334 | Notification::InformV2c { uptime, .. }
335 | Notification::InformV3 { uptime, .. } => *uptime,
336 }
337 }
338
339 pub fn varbinds(&self) -> &[VarBind] {
341 match self {
342 Notification::TrapV1 { trap, .. } => &trap.varbinds,
343 Notification::TrapV2c { varbinds, .. }
344 | Notification::TrapV3 { varbinds, .. }
345 | Notification::InformV2c { varbinds, .. }
346 | Notification::InformV3 { varbinds, .. } => varbinds,
347 }
348 }
349
350 pub fn is_confirmed(&self) -> bool {
352 matches!(
353 self,
354 Notification::InformV2c { .. } | Notification::InformV3 { .. }
355 )
356 }
357
358 pub fn version(&self) -> Version {
360 match self {
361 Notification::TrapV1 { .. } => Version::V1,
362 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
363 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
364 }
365 }
366}
367
368pub struct NotificationReceiver {
394 inner: Arc<ReceiverInner>,
395}
396
397struct ReceiverInner {
398 socket: UdpSocket,
399 local_addr: SocketAddr,
400 usm_users: HashMap<Bytes, UsmUserConfig>,
402 salt_counter: SaltCounter,
404}
405
406impl NotificationReceiver {
407 pub fn builder() -> NotificationReceiverBuilder {
411 NotificationReceiverBuilder::new()
412 }
413
414 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
434 let addr_str = addr.as_ref();
435 let bind_addr: SocketAddr = addr_str
436 .parse()
437 .map_err(|_| Error::Config(format!("invalid bind address: {}", addr_str).into()))?;
438
439 let socket = bind_udp_socket(bind_addr, None)
440 .await
441 .map_err(|e| Error::Network {
442 target: bind_addr,
443 source: e,
444 })?;
445
446 let local_addr = socket.local_addr().map_err(|e| Error::Network {
447 target: bind_addr,
448 source: e,
449 })?;
450
451 Ok(Self {
452 inner: Arc::new(ReceiverInner {
453 socket,
454 local_addr,
455 usm_users: HashMap::new(),
456 salt_counter: SaltCounter::new(),
457 }),
458 })
459 }
460
461 pub fn local_addr(&self) -> SocketAddr {
463 self.inner.local_addr
464 }
465
466 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
473 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
474 let mut buf = vec![0u8; 65535];
475
476 loop {
477 let (len, source) =
478 self.inner
479 .socket
480 .recv_from(&mut buf)
481 .await
482 .map_err(|e| Error::Network {
483 target: self.inner.local_addr,
484 source: e,
485 })?;
486
487 let data = Bytes::copy_from_slice(&buf[..len]);
488
489 match self.parse_and_respond(data, source).await {
490 Ok(Some(notification)) => return Ok((notification, source)),
491 Ok(None) => continue, Err(e) => {
493 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
495 continue;
496 }
497 }
498 }
499 }
500
501 async fn parse_and_respond(
505 &self,
506 data: Bytes,
507 source: SocketAddr,
508 ) -> Result<Option<Notification>> {
509 let mut decoder = Decoder::with_target(data.clone(), source);
511 let mut seq = decoder.read_sequence()?;
512 let version_num = seq.read_integer()?;
513 let version = Version::from_i32(version_num).ok_or_else(|| {
514 tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
515 Error::MalformedResponse { target: source }.boxed()
516 })?;
517 drop(seq);
518 drop(decoder);
519
520 match version {
521 Version::V1 => self.handle_v1(data, source).await,
522 Version::V2c => self.handle_v2c(data, source).await,
523 Version::V3 => self.handle_v3(data, source).await,
524 }
525 }
526}
527
528impl Clone for NotificationReceiver {
529 fn clone(&self) -> Self {
530 Self {
531 inner: Arc::clone(&self.inner),
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use crate::message::SecurityLevel;
540 use crate::oid;
541 use crate::pdu::GenericTrap;
542 use crate::v3::AuthProtocol;
543
544 #[test]
545 fn test_notification_trap_v1() {
546 let trap = TrapV1Pdu::new(
547 oid!(1, 3, 6, 1, 4, 1, 9999),
548 [192, 168, 1, 1],
549 GenericTrap::LinkDown,
550 0,
551 12345,
552 vec![],
553 );
554
555 let notification = Notification::TrapV1 {
556 community: Bytes::from_static(b"public"),
557 trap,
558 };
559
560 assert!(!notification.is_confirmed());
561 assert_eq!(notification.version(), Version::V1);
562 assert_eq!(notification.uptime(), 12345);
563 }
564
565 #[test]
566 fn test_notification_trap_v2c() {
567 let notification = Notification::TrapV2c {
568 community: Bytes::from_static(b"public"),
569 uptime: 54321,
570 trap_oid: oids::link_up(),
571 varbinds: vec![],
572 request_id: 1,
573 };
574
575 assert!(!notification.is_confirmed());
576 assert_eq!(notification.version(), Version::V2c);
577 assert_eq!(notification.uptime(), 54321);
578 assert_eq!(notification.trap_oid(), &oids::link_up());
579 }
580
581 #[test]
582 fn test_notification_inform() {
583 let notification = Notification::InformV2c {
584 community: Bytes::from_static(b"public"),
585 uptime: 11111,
586 trap_oid: oids::cold_start(),
587 varbinds: vec![],
588 request_id: 42,
589 };
590
591 assert!(notification.is_confirmed());
592 assert_eq!(notification.version(), Version::V2c);
593 }
594
595 #[test]
596 fn test_notification_receiver_builder_default() {
597 let builder = NotificationReceiverBuilder::new();
598 assert_eq!(builder.bind_addr, "0.0.0.0:162");
599 assert!(builder.usm_users.is_empty());
600 }
601
602 #[test]
603 fn test_notification_receiver_builder_with_user() {
604 let builder = NotificationReceiverBuilder::new()
605 .bind("0.0.0.0:1162")
606 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
607
608 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
609 assert_eq!(builder.usm_users.len(), 1);
610
611 let user = builder
612 .usm_users
613 .get(&Bytes::from_static(b"trapuser"))
614 .unwrap();
615 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
616 }
617
618 #[test]
619 fn test_notification_v3_inform() {
620 let notification = Notification::InformV3 {
621 username: Bytes::from_static(b"testuser"),
622 context_engine_id: Bytes::from_static(b"engine123"),
623 context_name: Bytes::new(),
624 uptime: 99999,
625 trap_oid: oids::warm_start(),
626 varbinds: vec![],
627 request_id: 100,
628 };
629
630 assert!(notification.is_confirmed());
631 assert_eq!(notification.version(), Version::V3);
632 assert_eq!(notification.uptime(), 99999);
633 assert_eq!(notification.trap_oid(), &oids::warm_start());
634 }
635}