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 use types::{DerivedKeys, UsmConfig, UsmUserConfig};
76pub use varbind::validate_notification_varbinds;
77
78pub mod oids {
80 use crate::oid;
81
82 pub fn sys_uptime() -> crate::Oid {
84 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
85 }
86
87 pub fn snmp_trap_oid() -> crate::Oid {
89 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
90 }
91
92 pub fn snmp_trap_enterprise() -> crate::Oid {
94 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
95 }
96
97 pub fn snmp_traps() -> crate::Oid {
99 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
100 }
101
102 pub fn cold_start() -> crate::Oid {
104 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
105 }
106
107 pub fn warm_start() -> crate::Oid {
109 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
110 }
111
112 pub fn link_down() -> crate::Oid {
114 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
115 }
116
117 pub fn link_up() -> crate::Oid {
119 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
120 }
121
122 pub fn auth_failure() -> crate::Oid {
124 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
125 }
126
127 pub fn egp_neighbor_loss() -> crate::Oid {
129 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
130 }
131}
132
133pub struct NotificationReceiverBuilder {
137 bind_addr: String,
138 usm_users: HashMap<Bytes, UsmUserConfig>,
139}
140
141impl NotificationReceiverBuilder {
142 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 pub fn bind(mut self, addr: impl Into<String>) -> Self {
158 self.bind_addr = addr.into();
159 self
160 }
161
162 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 pub async fn build(self) -> Result<NotificationReceiver> {
194 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
195 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
196 })?;
197
198 let socket = bind_udp_socket(bind_addr, None)
199 .await
200 .map_err(|e| Error::Network {
201 target: bind_addr,
202 source: e,
203 })?;
204
205 let local_addr = socket.local_addr().map_err(|e| Error::Network {
206 target: bind_addr,
207 source: e,
208 })?;
209
210 Ok(NotificationReceiver {
211 inner: Arc::new(ReceiverInner {
212 socket,
213 local_addr,
214 usm_users: self.usm_users,
215 salt_counter: SaltCounter::new(),
216 }),
217 })
218 }
219}
220
221impl Default for NotificationReceiverBuilder {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227#[derive(Debug, Clone)]
234pub enum Notification {
235 TrapV1 {
237 community: Bytes,
239 trap: TrapV1Pdu,
241 },
242
243 TrapV2c {
245 community: Bytes,
247 uptime: u32,
249 trap_oid: Oid,
251 varbinds: Vec<VarBind>,
253 request_id: i32,
255 },
256
257 TrapV3 {
259 username: Bytes,
261 context_engine_id: Bytes,
263 context_name: Bytes,
265 uptime: u32,
267 trap_oid: Oid,
269 varbinds: Vec<VarBind>,
271 request_id: i32,
273 },
274
275 InformV2c {
279 community: Bytes,
281 uptime: u32,
283 trap_oid: Oid,
285 varbinds: Vec<VarBind>,
287 request_id: i32,
289 },
290
291 InformV3 {
295 username: Bytes,
297 context_engine_id: Bytes,
299 context_name: Bytes,
301 uptime: u32,
303 trap_oid: Oid,
305 varbinds: Vec<VarBind>,
307 request_id: i32,
309 },
310}
311
312impl Notification {
313 pub fn trap_oid(&self) -> &Oid {
318 match self {
319 Notification::TrapV1 { trap, .. } => &trap.enterprise,
320 Notification::TrapV2c { trap_oid, .. }
321 | Notification::TrapV3 { trap_oid, .. }
322 | Notification::InformV2c { trap_oid, .. }
323 | Notification::InformV3 { trap_oid, .. } => trap_oid,
324 }
325 }
326
327 pub fn uptime(&self) -> u32 {
329 match self {
330 Notification::TrapV1 { trap, .. } => trap.time_stamp,
331 Notification::TrapV2c { uptime, .. }
332 | Notification::TrapV3 { uptime, .. }
333 | Notification::InformV2c { uptime, .. }
334 | Notification::InformV3 { uptime, .. } => *uptime,
335 }
336 }
337
338 pub fn varbinds(&self) -> &[VarBind] {
340 match self {
341 Notification::TrapV1 { trap, .. } => &trap.varbinds,
342 Notification::TrapV2c { varbinds, .. }
343 | Notification::TrapV3 { varbinds, .. }
344 | Notification::InformV2c { varbinds, .. }
345 | Notification::InformV3 { varbinds, .. } => varbinds,
346 }
347 }
348
349 pub fn is_confirmed(&self) -> bool {
351 matches!(
352 self,
353 Notification::InformV2c { .. } | Notification::InformV3 { .. }
354 )
355 }
356
357 pub fn version(&self) -> Version {
359 match self {
360 Notification::TrapV1 { .. } => Version::V1,
361 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
362 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
363 }
364 }
365}
366
367pub struct NotificationReceiver {
393 inner: Arc<ReceiverInner>,
394}
395
396struct ReceiverInner {
397 socket: UdpSocket,
398 local_addr: SocketAddr,
399 usm_users: HashMap<Bytes, UsmUserConfig>,
401 salt_counter: SaltCounter,
403}
404
405impl NotificationReceiver {
406 pub fn builder() -> NotificationReceiverBuilder {
410 NotificationReceiverBuilder::new()
411 }
412
413 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
433 let addr_str = addr.as_ref();
434 let bind_addr: SocketAddr = addr_str
435 .parse()
436 .map_err(|_| Error::Config(format!("invalid bind address: {}", addr_str).into()))?;
437
438 let socket = bind_udp_socket(bind_addr, None)
439 .await
440 .map_err(|e| Error::Network {
441 target: bind_addr,
442 source: e,
443 })?;
444
445 let local_addr = socket.local_addr().map_err(|e| Error::Network {
446 target: bind_addr,
447 source: e,
448 })?;
449
450 Ok(Self {
451 inner: Arc::new(ReceiverInner {
452 socket,
453 local_addr,
454 usm_users: HashMap::new(),
455 salt_counter: SaltCounter::new(),
456 }),
457 })
458 }
459
460 pub fn local_addr(&self) -> SocketAddr {
462 self.inner.local_addr
463 }
464
465 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
472 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
473 let mut buf = vec![0u8; 65535];
474
475 loop {
476 let (len, source) =
477 self.inner
478 .socket
479 .recv_from(&mut buf)
480 .await
481 .map_err(|e| Error::Network {
482 target: self.inner.local_addr,
483 source: e,
484 })?;
485
486 let data = Bytes::copy_from_slice(&buf[..len]);
487
488 match self.parse_and_respond(data, source).await {
489 Ok(Some(notification)) => return Ok((notification, source)),
490 Ok(None) => continue, Err(e) => {
492 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
494 continue;
495 }
496 }
497 }
498 }
499
500 async fn parse_and_respond(
504 &self,
505 data: Bytes,
506 source: SocketAddr,
507 ) -> Result<Option<Notification>> {
508 let mut decoder = Decoder::with_target(data.clone(), source);
510 let mut seq = decoder.read_sequence()?;
511 let version_num = seq.read_integer()?;
512 let version = Version::from_i32(version_num).ok_or_else(|| {
513 tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
514 Error::MalformedResponse { target: source }.boxed()
515 })?;
516 drop(seq);
517 drop(decoder);
518
519 match version {
520 Version::V1 => self.handle_v1(data, source).await,
521 Version::V2c => self.handle_v2c(data, source).await,
522 Version::V3 => self.handle_v3(data, source).await,
523 }
524 }
525}
526
527impl Clone for NotificationReceiver {
528 fn clone(&self) -> Self {
529 Self {
530 inner: Arc::clone(&self.inner),
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use crate::message::SecurityLevel;
539 use crate::oid;
540 use crate::pdu::GenericTrap;
541 use crate::v3::AuthProtocol;
542
543 #[test]
544 fn test_notification_trap_v1() {
545 let trap = TrapV1Pdu::new(
546 oid!(1, 3, 6, 1, 4, 1, 9999),
547 [192, 168, 1, 1],
548 GenericTrap::LinkDown,
549 0,
550 12345,
551 vec![],
552 );
553
554 let notification = Notification::TrapV1 {
555 community: Bytes::from_static(b"public"),
556 trap,
557 };
558
559 assert!(!notification.is_confirmed());
560 assert_eq!(notification.version(), Version::V1);
561 assert_eq!(notification.uptime(), 12345);
562 }
563
564 #[test]
565 fn test_notification_trap_v2c() {
566 let notification = Notification::TrapV2c {
567 community: Bytes::from_static(b"public"),
568 uptime: 54321,
569 trap_oid: oids::link_up(),
570 varbinds: vec![],
571 request_id: 1,
572 };
573
574 assert!(!notification.is_confirmed());
575 assert_eq!(notification.version(), Version::V2c);
576 assert_eq!(notification.uptime(), 54321);
577 assert_eq!(notification.trap_oid(), &oids::link_up());
578 }
579
580 #[test]
581 fn test_notification_inform() {
582 let notification = Notification::InformV2c {
583 community: Bytes::from_static(b"public"),
584 uptime: 11111,
585 trap_oid: oids::cold_start(),
586 varbinds: vec![],
587 request_id: 42,
588 };
589
590 assert!(notification.is_confirmed());
591 assert_eq!(notification.version(), Version::V2c);
592 }
593
594 #[test]
595 fn test_notification_receiver_builder_default() {
596 let builder = NotificationReceiverBuilder::new();
597 assert_eq!(builder.bind_addr, "0.0.0.0:162");
598 assert!(builder.usm_users.is_empty());
599 }
600
601 #[test]
602 fn test_notification_receiver_builder_with_user() {
603 let builder = NotificationReceiverBuilder::new()
604 .bind("0.0.0.0:1162")
605 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
606
607 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
608 assert_eq!(builder.usm_users.len(), 1);
609
610 let user = builder
611 .usm_users
612 .get(&Bytes::from_static(b"trapuser"))
613 .unwrap();
614 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
615 }
616
617 #[test]
618 fn test_notification_v3_inform() {
619 let notification = Notification::InformV3 {
620 username: Bytes::from_static(b"testuser"),
621 context_engine_id: Bytes::from_static(b"engine123"),
622 context_name: Bytes::new(),
623 uptime: 99999,
624 trap_oid: oids::warm_start(),
625 varbinds: vec![],
626 request_id: 100,
627 };
628
629 assert!(notification.is_confirmed());
630 assert_eq!(notification.version(), Version::V3);
631 assert_eq!(notification.uptime(), 99999);
632 assert_eq!(notification.trap_oid(), &oids::warm_start());
633 }
634}