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::{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
73pub(crate) use types::DerivedKeys;
75pub use types::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 {
144 Self {
145 bind_addr: "0.0.0.0:162".to_string(),
146 usm_users: HashMap::new(),
147 }
148 }
149
150 pub fn bind(mut self, addr: impl Into<String>) -> Self {
154 self.bind_addr = addr.into();
155 self
156 }
157
158 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
179 where
180 F: FnOnce(UsmUserConfig) -> UsmUserConfig,
181 {
182 let username_bytes: Bytes = username.into();
183 let config = configure(UsmUserConfig::new(username_bytes.clone()));
184 self.usm_users.insert(username_bytes, config);
185 self
186 }
187
188 pub async fn build(self) -> Result<NotificationReceiver> {
192 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| Error::Io {
193 target: None,
194 source: std::io::Error::new(
195 std::io::ErrorKind::InvalidInput,
196 format!("invalid bind address: {}", self.bind_addr),
197 ),
198 })?;
199
200 let socket = bind_udp_socket(bind_addr).await.map_err(|e| Error::Io {
201 target: Some(bind_addr),
202 source: e,
203 })?;
204
205 let local_addr = socket.local_addr().map_err(|e| Error::Io {
206 target: Some(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> {
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 pub fn local_addr(&self) -> SocketAddr {
466 self.inner.local_addr
467 }
468
469 #[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, Err(e) => {
496 tracing::warn!(snmp.source = %source, error = %e, "failed to parse notification");
498 continue;
499 }
500 }
501 }
502 }
503
504 async fn parse_and_respond(
508 &self,
509 data: Bytes,
510 source: SocketAddr,
511 ) -> Result<Option<Notification>> {
512 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}