1mod handlers;
89mod types;
90mod varbind;
91
92use std::collections::HashMap;
93use std::net::SocketAddr;
94use std::sync::atomic::{AtomicU32, Ordering};
95use std::sync::{Arc, Mutex};
96use std::time::Instant;
97
98use bytes::Bytes;
99use subtle::ConstantTimeEq;
100use tokio::net::UdpSocket;
101use tracing::instrument;
102
103use crate::ber::Decoder;
104use crate::error::internal::DecodeErrorKind;
105use crate::error::{Error, Result};
106use crate::message::SecurityLevel;
107use crate::oid::Oid;
108use crate::pdu::TrapV1Pdu;
109use crate::util::bind_udp_socket;
110use crate::v3::{EngineState, SaltCounter};
111use crate::varbind::VarBind;
112use crate::version::Version;
113
114pub use types::{DerivedKeys, UsmConfig};
116pub use varbind::validate_notification_varbinds;
117
118const MAX_REMOTE_ENGINES: usize = 8192;
124
125pub(super) fn community_allowed(configured: &[Vec<u8>], community: &[u8]) -> bool {
133 if configured.is_empty() {
134 return true;
135 }
136 let mut valid = false;
137 for candidate in configured {
138 if candidate.len() == community.len() && bool::from(candidate.as_slice().ct_eq(community)) {
139 valid = true;
140 }
141 }
142 valid
143}
144
145pub mod oids {
147 use crate::oid;
148
149 #[must_use]
151 pub fn sys_uptime() -> crate::Oid {
152 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
153 }
154
155 #[must_use]
157 pub fn snmp_trap_oid() -> crate::Oid {
158 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
159 }
160
161 #[must_use]
163 pub fn snmp_trap_enterprise() -> crate::Oid {
164 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
165 }
166
167 #[must_use]
169 pub fn snmp_trap_address() -> crate::Oid {
170 oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
171 }
172
173 #[must_use]
175 pub fn snmp_traps() -> crate::Oid {
176 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
177 }
178
179 #[must_use]
181 pub fn cold_start() -> crate::Oid {
182 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
183 }
184
185 #[must_use]
187 pub fn warm_start() -> crate::Oid {
188 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
189 }
190
191 #[must_use]
193 pub fn link_down() -> crate::Oid {
194 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
195 }
196
197 #[must_use]
199 pub fn link_up() -> crate::Oid {
200 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
201 }
202
203 #[must_use]
205 pub fn auth_failure() -> crate::Oid {
206 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
207 }
208
209 #[must_use]
211 pub fn egp_neighbor_loss() -> crate::Oid {
212 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
213 }
214}
215
216pub struct NotificationReceiverBuilder {
223 bind_addr: String,
224 usm_users: HashMap<Bytes, UsmConfig>,
225 communities: Vec<Vec<u8>>,
226 engine_id: Option<Vec<u8>>,
227 engine_boots: u32,
228}
229
230impl NotificationReceiverBuilder {
231 #[must_use]
237 pub fn new() -> Self {
238 Self {
239 bind_addr: "0.0.0.0:162".to_string(),
240 usm_users: HashMap::new(),
241 communities: Vec::new(),
242 engine_id: None,
243 engine_boots: 1,
244 }
245 }
246
247 #[must_use]
251 pub fn bind(mut self, addr: impl Into<String>) -> Self {
252 self.bind_addr = addr.into();
253 self
254 }
255
256 #[must_use]
277 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
278 where
279 F: FnOnce(UsmConfig) -> UsmConfig,
280 {
281 let username_bytes: Bytes = username.into();
282 let config = configure(UsmConfig::new(username_bytes.clone()));
283 self.usm_users.insert(username_bytes, config);
284 self
285 }
286
287 #[must_use]
315 pub fn community(mut self, community: &[u8]) -> Self {
316 self.communities.push(community.to_vec());
317 self
318 }
319
320 #[must_use]
340 pub fn communities<I, C>(mut self, communities: I) -> Self
341 where
342 I: IntoIterator<Item = C>,
343 C: AsRef<[u8]>,
344 {
345 for c in communities {
346 self.communities.push(c.as_ref().to_vec());
347 }
348 self
349 }
350
351 #[must_use]
356 pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
357 self.engine_id = Some(engine_id.into());
358 self
359 }
360
361 #[must_use]
366 pub fn engine_boots(mut self, boots: u32) -> Self {
367 self.engine_boots = boots;
368 self
369 }
370
371 pub async fn build(self) -> Result<NotificationReceiver> {
373 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
374 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
375 })?;
376
377 let socket = bind_udp_socket(bind_addr, None, None, false)
378 .await
379 .map_err(|e| Error::Network {
380 target: bind_addr,
381 source: e,
382 })?;
383
384 let local_addr = socket.local_addr().map_err(|e| Error::Network {
385 target: bind_addr,
386 source: e,
387 })?;
388
389 let engine_id: Bytes = self.engine_id.map_or_else(
390 || {
391 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
392 let timestamp = std::time::SystemTime::now()
393 .duration_since(std::time::UNIX_EPOCH)
394 .unwrap_or_default()
395 .as_secs();
396 id.extend_from_slice(×tamp.to_be_bytes());
397 Bytes::from(id)
398 },
399 Bytes::from,
400 );
401
402 Ok(NotificationReceiver {
403 inner: Arc::new(ReceiverInner {
404 socket,
405 local_addr,
406 usm_users: self.usm_users,
407 communities: self.communities,
408 engine_id,
409 salt_counter: SaltCounter::new(),
410 engine_boots_base: self.engine_boots,
411 engine_start: Instant::now(),
412 usm_unknown_engine_ids: AtomicU32::new(0),
413 usm_unknown_usernames: AtomicU32::new(0),
414 usm_wrong_digests: AtomicU32::new(0),
415 usm_not_in_time_windows: AtomicU32::new(0),
416 usm_unsupported_sec_levels: AtomicU32::new(0),
417 usm_decryption_errors: AtomicU32::new(0),
418 remote_engines: Mutex::new(HashMap::new()),
419 }),
420 })
421 }
422}
423
424impl Default for NotificationReceiverBuilder {
425 fn default() -> Self {
426 Self::new()
427 }
428}
429
430#[derive(Debug, Clone)]
437pub enum Notification {
438 TrapV1 {
440 community: Bytes,
442 trap: TrapV1Pdu,
444 },
445
446 TrapV2c {
448 community: Bytes,
450 uptime: u32,
452 trap_oid: Oid,
454 varbinds: Vec<VarBind>,
456 request_id: i32,
458 },
459
460 TrapV3 {
462 username: Bytes,
464 context_engine_id: Bytes,
466 context_name: Bytes,
468 security_level: SecurityLevel,
472 uptime: u32,
474 trap_oid: Oid,
476 varbinds: Vec<VarBind>,
478 request_id: i32,
480 },
481
482 InformV2c {
486 community: Bytes,
488 uptime: u32,
490 trap_oid: Oid,
492 varbinds: Vec<VarBind>,
494 request_id: i32,
496 },
497
498 InformV3 {
502 username: Bytes,
504 context_engine_id: Bytes,
506 context_name: Bytes,
508 security_level: SecurityLevel,
512 uptime: u32,
514 trap_oid: Oid,
516 varbinds: Vec<VarBind>,
518 request_id: i32,
520 },
521}
522
523impl Notification {
524 pub fn trap_oid(&self) -> Result<Oid> {
529 match self {
530 Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
531 Notification::TrapV2c { trap_oid, .. }
532 | Notification::TrapV3 { trap_oid, .. }
533 | Notification::InformV2c { trap_oid, .. }
534 | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
535 }
536 }
537
538 pub fn uptime(&self) -> u32 {
540 match self {
541 Notification::TrapV1 { trap, .. } => trap.time_stamp,
542 Notification::TrapV2c { uptime, .. }
543 | Notification::TrapV3 { uptime, .. }
544 | Notification::InformV2c { uptime, .. }
545 | Notification::InformV3 { uptime, .. } => *uptime,
546 }
547 }
548
549 pub fn varbinds(&self) -> &[VarBind] {
551 match self {
552 Notification::TrapV1 { trap, .. } => &trap.varbinds,
553 Notification::TrapV2c { varbinds, .. }
554 | Notification::TrapV3 { varbinds, .. }
555 | Notification::InformV2c { varbinds, .. }
556 | Notification::InformV3 { varbinds, .. } => varbinds,
557 }
558 }
559
560 pub fn security_level(&self) -> Option<SecurityLevel> {
567 match self {
568 Notification::TrapV1 { .. }
569 | Notification::TrapV2c { .. }
570 | Notification::InformV2c { .. } => None,
571 Notification::TrapV3 { security_level, .. }
572 | Notification::InformV3 { security_level, .. } => Some(*security_level),
573 }
574 }
575
576 pub fn is_confirmed(&self) -> bool {
578 matches!(
579 self,
580 Notification::InformV2c { .. } | Notification::InformV3 { .. }
581 )
582 }
583
584 pub fn version(&self) -> Version {
586 match self {
587 Notification::TrapV1 { .. } => Version::V1,
588 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
589 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
590 }
591 }
592}
593
594pub struct NotificationReceiver {
620 inner: Arc<ReceiverInner>,
621}
622
623struct ReceiverInner {
624 socket: UdpSocket,
625 local_addr: SocketAddr,
626 usm_users: HashMap<Bytes, UsmConfig>,
628 communities: Vec<Vec<u8>>,
632 engine_id: Bytes,
634 salt_counter: SaltCounter,
636 engine_boots_base: u32,
638 engine_start: Instant,
640 usm_unknown_engine_ids: AtomicU32,
642 usm_unknown_usernames: AtomicU32,
644 usm_wrong_digests: AtomicU32,
646 usm_not_in_time_windows: AtomicU32,
648 usm_unsupported_sec_levels: AtomicU32,
650 usm_decryption_errors: AtomicU32,
652 remote_engines: Mutex<HashMap<Bytes, EngineState>>,
659}
660
661impl NotificationReceiver {
662 #[must_use]
666 pub fn builder() -> NotificationReceiverBuilder {
667 NotificationReceiverBuilder::new()
668 }
669
670 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
696 let addr_str = addr.as_ref();
697 let bind_addr: SocketAddr = addr_str
698 .parse()
699 .map_err(|_| Error::Config(format!("invalid bind address: {addr_str}").into()))?;
700
701 let socket = bind_udp_socket(bind_addr, None, None, false)
702 .await
703 .map_err(|e| Error::Network {
704 target: bind_addr,
705 source: e,
706 })?;
707
708 let local_addr = socket.local_addr().map_err(|e| Error::Network {
709 target: bind_addr,
710 source: e,
711 })?;
712
713 let engine_id: Bytes = {
714 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
715 let timestamp = std::time::SystemTime::now()
716 .duration_since(std::time::UNIX_EPOCH)
717 .unwrap_or_default()
718 .as_secs();
719 id.extend_from_slice(×tamp.to_be_bytes());
720 Bytes::from(id)
721 };
722
723 Ok(Self {
724 inner: Arc::new(ReceiverInner {
725 socket,
726 local_addr,
727 usm_users: HashMap::new(),
728 communities: Vec::new(),
729 engine_id,
730 salt_counter: SaltCounter::new(),
731 engine_boots_base: 1,
732 engine_start: Instant::now(),
733 usm_unknown_engine_ids: AtomicU32::new(0),
734 usm_unknown_usernames: AtomicU32::new(0),
735 usm_wrong_digests: AtomicU32::new(0),
736 usm_not_in_time_windows: AtomicU32::new(0),
737 usm_unsupported_sec_levels: AtomicU32::new(0),
738 usm_decryption_errors: AtomicU32::new(0),
739 remote_engines: Mutex::new(HashMap::new()),
740 }),
741 })
742 }
743
744 #[must_use]
746 pub fn local_addr(&self) -> SocketAddr {
747 self.inner.local_addr
748 }
749
750 #[must_use]
752 pub fn engine_id(&self) -> &[u8] {
753 &self.inner.engine_id
754 }
755
756 #[must_use]
758 pub fn usm_unknown_engine_ids(&self) -> u32 {
759 self.inner.usm_unknown_engine_ids.load(Ordering::Relaxed)
760 }
761
762 #[must_use]
764 pub fn usm_unknown_usernames(&self) -> u32 {
765 self.inner.usm_unknown_usernames.load(Ordering::Relaxed)
766 }
767
768 #[must_use]
770 pub fn usm_wrong_digests(&self) -> u32 {
771 self.inner.usm_wrong_digests.load(Ordering::Relaxed)
772 }
773
774 #[must_use]
776 pub fn usm_not_in_time_windows(&self) -> u32 {
777 self.inner.usm_not_in_time_windows.load(Ordering::Relaxed)
778 }
779
780 #[must_use]
782 pub fn usm_unsupported_sec_levels(&self) -> u32 {
783 self.inner
784 .usm_unsupported_sec_levels
785 .load(Ordering::Relaxed)
786 }
787
788 #[must_use]
790 pub fn usm_decryption_errors(&self) -> u32 {
791 self.inner.usm_decryption_errors.load(Ordering::Relaxed)
792 }
793
794 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
801 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
802 let mut buf = vec![0u8; 65535];
803
804 loop {
805 let (len, source) =
806 self.inner
807 .socket
808 .recv_from(&mut buf)
809 .await
810 .map_err(|e| Error::Network {
811 target: self.inner.local_addr,
812 source: e,
813 })?;
814
815 let data = Bytes::copy_from_slice(&buf[..len]);
816
817 match self.parse_and_respond(data, source).await {
818 Ok(Some(notification)) => return Ok((notification, source)),
819 Ok(None) => {} Err(e) => {
821 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
823 }
824 }
825 }
826 }
827
828 async fn parse_and_respond(
832 &self,
833 data: Bytes,
834 source: SocketAddr,
835 ) -> Result<Option<Notification>> {
836 let mut decoder = Decoder::with_target(data.clone(), source);
838 let mut seq = decoder.read_sequence()?;
839 let version_num = seq.read_integer()?;
840 let version = Version::from_i32(version_num).ok_or_else(|| {
841 tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
842 Error::MalformedResponse { target: source }.boxed()
843 })?;
844 drop(seq);
845 drop(decoder);
846
847 match version {
848 Version::V1 => self.handle_v1(data, source).await,
849 Version::V2c => self.handle_v2c(data, source).await,
850 Version::V3 => self.handle_v3(data, source).await,
851 }
852 }
853}
854
855impl Clone for NotificationReceiver {
856 fn clone(&self) -> Self {
857 Self {
858 inner: Arc::clone(&self.inner),
859 }
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866 use crate::message::SecurityLevel;
867 use crate::oid;
868 use crate::pdu::GenericTrap;
869 use crate::v3::AuthProtocol;
870
871 #[test]
872 fn test_notification_trap_v1() {
873 let trap = TrapV1Pdu::new(
874 oid!(1, 3, 6, 1, 4, 1, 9999),
875 [192, 168, 1, 1],
876 GenericTrap::LinkDown,
877 0,
878 12345,
879 vec![],
880 );
881
882 let notification = Notification::TrapV1 {
883 community: Bytes::from_static(b"public"),
884 trap,
885 };
886
887 assert!(!notification.is_confirmed());
888 assert_eq!(notification.version(), Version::V1);
889 assert_eq!(notification.uptime(), 12345);
890 assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
891 }
892
893 #[test]
894 fn test_notification_trap_v2c() {
895 let notification = Notification::TrapV2c {
896 community: Bytes::from_static(b"public"),
897 uptime: 54321,
898 trap_oid: oids::link_up(),
899 varbinds: vec![],
900 request_id: 1,
901 };
902
903 assert!(!notification.is_confirmed());
904 assert_eq!(notification.version(), Version::V2c);
905 assert_eq!(notification.uptime(), 54321);
906 assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
907 }
908
909 #[test]
910 fn test_notification_inform() {
911 let notification = Notification::InformV2c {
912 community: Bytes::from_static(b"public"),
913 uptime: 11111,
914 trap_oid: oids::cold_start(),
915 varbinds: vec![],
916 request_id: 42,
917 };
918
919 assert!(notification.is_confirmed());
920 assert_eq!(notification.version(), Version::V2c);
921 }
922
923 #[test]
924 fn test_notification_receiver_builder_default() {
925 let builder = NotificationReceiverBuilder::new();
926 assert_eq!(builder.bind_addr, "0.0.0.0:162");
927 assert!(builder.usm_users.is_empty());
928 }
929
930 #[test]
931 fn test_notification_receiver_builder_with_user() {
932 let builder = NotificationReceiverBuilder::new()
933 .bind("0.0.0.0:1162")
934 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
935
936 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
937 assert_eq!(builder.usm_users.len(), 1);
938
939 let user = builder
940 .usm_users
941 .get(&Bytes::from_static(b"trapuser"))
942 .unwrap();
943 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
944 }
945
946 #[test]
947 fn test_notification_v3_inform() {
948 let notification = Notification::InformV3 {
949 username: Bytes::from_static(b"testuser"),
950 context_engine_id: Bytes::from_static(b"engine123"),
951 context_name: Bytes::new(),
952 security_level: SecurityLevel::AuthNoPriv,
953 uptime: 99999,
954 trap_oid: oids::warm_start(),
955 varbinds: vec![],
956 request_id: 100,
957 };
958
959 assert!(notification.is_confirmed());
960 assert_eq!(notification.version(), Version::V3);
961 assert_eq!(notification.uptime(), 99999);
962 assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
963 }
964
965 #[test]
966 fn test_notification_security_level_accessor() {
967 let trap_v3 = Notification::TrapV3 {
968 username: Bytes::from_static(b"testuser"),
969 context_engine_id: Bytes::from_static(b"engine123"),
970 context_name: Bytes::new(),
971 security_level: SecurityLevel::AuthPriv,
972 uptime: 1,
973 trap_oid: oids::cold_start(),
974 varbinds: vec![],
975 request_id: 1,
976 };
977 assert_eq!(trap_v3.security_level(), Some(SecurityLevel::AuthPriv));
978
979 let inform_v3 = Notification::InformV3 {
980 username: Bytes::from_static(b"testuser"),
981 context_engine_id: Bytes::from_static(b"engine123"),
982 context_name: Bytes::new(),
983 security_level: SecurityLevel::NoAuthNoPriv,
984 uptime: 1,
985 trap_oid: oids::cold_start(),
986 varbinds: vec![],
987 request_id: 1,
988 };
989 assert_eq!(
990 inform_v3.security_level(),
991 Some(SecurityLevel::NoAuthNoPriv)
992 );
993
994 let trap_v2c = Notification::TrapV2c {
995 community: Bytes::from_static(b"public"),
996 uptime: 1,
997 trap_oid: oids::cold_start(),
998 varbinds: vec![],
999 request_id: 1,
1000 };
1001 assert_eq!(trap_v2c.security_level(), None);
1002 }
1003
1004 #[test]
1005 fn test_notification_trap_v1_enterprise_specific_oid() {
1006 let trap = TrapV1Pdu::new(
1007 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1008 [192, 168, 1, 1],
1009 GenericTrap::EnterpriseSpecific,
1010 42,
1011 12345,
1012 vec![],
1013 );
1014
1015 let notification = Notification::TrapV1 {
1016 community: Bytes::from_static(b"public"),
1017 trap,
1018 };
1019
1020 assert_eq!(
1021 notification.trap_oid().unwrap(),
1022 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1023 );
1024 }
1025
1026 #[test]
1027 fn test_compute_engine_boots_time_basic() {
1028 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
1029 assert_eq!(boots, 1);
1030 assert_eq!(time, 1000);
1031 }
1032
1033 #[test]
1034 fn test_compute_engine_boots_time_zero_elapsed() {
1035 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
1036 assert_eq!(boots, 1);
1037 assert_eq!(time, 0);
1038 }
1039
1040 #[test]
1041 fn test_builder_engine_boots_default() {
1042 let builder = NotificationReceiverBuilder::new();
1043 assert_eq!(builder.engine_boots, 1);
1044 }
1045
1046 #[test]
1047 fn test_builder_engine_boots_custom() {
1048 let builder = NotificationReceiverBuilder::new().engine_boots(5);
1049 assert_eq!(builder.engine_boots, 5);
1050 }
1051
1052 fn build_v3_notification(
1057 pdu_type: crate::pdu::PduType,
1058 engine_id: &[u8],
1059 engine_boots: u32,
1060 engine_time: u32,
1061 username: &[u8],
1062 auth: Option<(&[u8], AuthProtocol)>,
1063 ) -> Bytes {
1064 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1065 use crate::pdu::Pdu;
1066 use crate::v3::auth::authenticate_message;
1067 use crate::v3::{LocalizedKey, UsmSecurityParams};
1068 use crate::value::Value;
1069
1070 let auth_key = auth.map(|(password, protocol)| {
1071 LocalizedKey::from_password(protocol, password, engine_id).unwrap()
1072 });
1073
1074 let pdu = Pdu {
1076 pdu_type,
1077 request_id: 1,
1078 error_status: 0,
1079 error_index: 0,
1080 varbinds: vec![
1081 VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
1082 VarBind::new(
1083 oids::snmp_trap_oid(),
1084 Value::ObjectIdentifier(oids::cold_start()),
1085 ),
1086 ],
1087 };
1088
1089 let level = if auth_key.is_some() {
1090 SecurityLevel::AuthNoPriv
1091 } else {
1092 SecurityLevel::NoAuthNoPriv
1093 };
1094 let reportable = pdu_type == crate::pdu::PduType::InformRequest;
1097 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(level, reportable));
1098
1099 let mut usm_params = UsmSecurityParams::new(
1100 Bytes::copy_from_slice(engine_id),
1101 engine_boots,
1102 engine_time,
1103 Bytes::copy_from_slice(username),
1104 );
1105 if let Some(key) = &auth_key {
1106 usm_params = usm_params.with_auth_placeholder(key.mac_len());
1107 }
1108
1109 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
1110 let msg = V3Message::new(global, usm_params.encode(), scoped);
1111 let mut msg_bytes = msg.encode().to_vec();
1112
1113 if let Some(key) = &auth_key {
1115 let (auth_offset, auth_len) =
1116 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1117 authenticate_message(key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1118 }
1119
1120 Bytes::from(msg_bytes)
1121 }
1122
1123 fn build_authed_v3_inform(
1126 engine_id: &[u8],
1127 engine_boots: u32,
1128 engine_time: u32,
1129 username: &[u8],
1130 auth_password: &[u8],
1131 auth_protocol: AuthProtocol,
1132 ) -> Bytes {
1133 build_v3_notification(
1134 crate::pdu::PduType::InformRequest,
1135 engine_id,
1136 engine_boots,
1137 engine_time,
1138 username,
1139 Some((auth_password, auth_protocol)),
1140 )
1141 }
1142
1143 fn build_authed_v3_trap(engine_id: &[u8], engine_boots: u32, engine_time: u32) -> Bytes {
1146 build_v3_notification(
1147 crate::pdu::PduType::TrapV2,
1148 engine_id,
1149 engine_boots,
1150 engine_time,
1151 b"trapuser",
1152 Some((b"authpass12345678", AuthProtocol::Sha1)),
1153 )
1154 }
1155
1156 fn build_noauth_v3_trap(engine_id: &[u8], username: &[u8]) -> Bytes {
1158 build_v3_notification(crate::pdu::PduType::TrapV2, engine_id, 0, 0, username, None)
1159 }
1160
1161 async fn remote_trap_receiver() -> NotificationReceiver {
1164 NotificationReceiver::builder()
1165 .bind("127.0.0.1:0")
1166 .engine_id(b"my-receiver-engine".to_vec())
1167 .engine_boots(1)
1168 .usm_user("trapuser", |u| {
1169 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1170 })
1171 .build()
1172 .await
1173 .unwrap()
1174 }
1175
1176 #[tokio::test]
1184 async fn test_v3_trap_from_remote_sender_engine_accepted() {
1185 let receiver = remote_trap_receiver().await;
1186 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1187
1188 let msg = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1190
1191 let result = receiver.handle_v3(msg, source).await.unwrap();
1192 match result {
1193 Some(Notification::TrapV3 {
1194 username,
1195 security_level,
1196 ..
1197 }) => {
1198 assert_eq!(username.as_ref(), b"trapuser");
1199 assert_eq!(security_level, SecurityLevel::AuthNoPriv);
1200 }
1201 other => panic!("expected TrapV3, got {other:?}"),
1202 }
1203 }
1204
1205 #[tokio::test]
1209 async fn test_v3_noauth_trap_carries_security_level() {
1210 let receiver = remote_trap_receiver().await;
1211 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1212
1213 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"trapuser");
1214 match receiver.handle_v3(msg, source).await.unwrap() {
1215 Some(Notification::TrapV3 {
1216 security_level,
1217 username,
1218 ..
1219 }) => {
1220 assert_eq!(security_level, SecurityLevel::NoAuthNoPriv);
1221 assert_eq!(username.as_ref(), b"trapuser");
1222 }
1223 other => panic!("expected TrapV3, got {other:?}"),
1224 }
1225 }
1226
1227 #[tokio::test]
1232 async fn test_v3_noauth_trap_unknown_user_rejected_and_counted() {
1233 let receiver = remote_trap_receiver().await;
1234 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1235
1236 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"nosuchuser");
1237 let result = receiver.handle_v3(msg, source).await.unwrap();
1238 assert!(result.is_none(), "unknown user must not be delivered");
1239 assert_eq!(receiver.usm_unknown_usernames(), 1);
1240 }
1241
1242 #[tokio::test]
1245 async fn test_v3_traps_from_multiple_remote_engines_accepted() {
1246 let receiver = remote_trap_receiver().await;
1247 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1248
1249 let msg_a = build_authed_v3_trap(b"sender-engine-a", 7, 123_456);
1250 let msg_b = build_authed_v3_trap(b"sender-engine-b", 2, 42);
1251
1252 assert!(
1253 receiver.handle_v3(msg_a, source).await.unwrap().is_some(),
1254 "trap from first remote engine should be accepted"
1255 );
1256 assert!(
1257 receiver.handle_v3(msg_b, source).await.unwrap().is_some(),
1258 "trap from second remote engine should be accepted"
1259 );
1260 }
1261
1262 #[tokio::test]
1267 async fn test_v3_remote_engines_table_bounded() {
1268 let receiver = remote_trap_receiver().await;
1269 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1270
1271 {
1273 let mut engines = receiver.inner.remote_engines.lock().unwrap();
1274 for i in 0..MAX_REMOTE_ENGINES {
1275 let id = Bytes::from(format!("dummy-engine-{i}"));
1276 engines.insert(id.clone(), EngineState::new(id, 1, 1));
1277 }
1278 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1279 }
1280
1281 let msg = build_authed_v3_trap(b"fresh-remote-engine", 7, 123_456);
1283 assert!(receiver.handle_v3(msg, source).await.unwrap().is_some());
1284
1285 let engines = receiver.inner.remote_engines.lock().unwrap();
1288 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1289 assert!(engines.contains_key(&Bytes::from_static(b"fresh-remote-engine")));
1290 }
1291
1292 #[tokio::test]
1297 async fn test_v3_trap_remote_engine_stale_time_rejected() {
1298 let receiver = remote_trap_receiver().await;
1299 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1300
1301 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1302 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1303
1304 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1306 assert!(
1307 receiver.handle_v3(stale, source).await.is_err(),
1308 "stale engine time should be rejected as outside the time window"
1309 );
1310 }
1311
1312 #[tokio::test]
1314 async fn test_v3_trap_remote_engine_old_boots_rejected() {
1315 let receiver = remote_trap_receiver().await;
1316 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1317
1318 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1319 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1320
1321 let old_boots = build_authed_v3_trap(b"remote-sender-engine", 6, 99_999);
1322 assert!(
1323 receiver.handle_v3(old_boots, source).await.is_err(),
1324 "older boot cycle should be rejected"
1325 );
1326 }
1327
1328 #[tokio::test]
1331 async fn test_v3_trap_remote_engine_reboot_accepted() {
1332 let receiver = remote_trap_receiver().await;
1333 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1334
1335 let before = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1336 assert!(receiver.handle_v3(before, source).await.unwrap().is_some());
1337
1338 let after_reboot = build_authed_v3_trap(b"remote-sender-engine", 8, 5);
1339 assert!(
1340 receiver
1341 .handle_v3(after_reboot, source)
1342 .await
1343 .unwrap()
1344 .is_some(),
1345 "trap after sender reboot should be accepted"
1346 );
1347
1348 let from_old_cycle = build_authed_v3_trap(b"remote-sender-engine", 7, 20_000);
1349 assert!(
1350 receiver.handle_v3(from_old_cycle, source).await.is_err(),
1351 "trap from superseded boot cycle should be rejected"
1352 );
1353 }
1354
1355 #[tokio::test]
1358 async fn test_v3_trap_remote_engine_bad_auth_rejected() {
1359 let receiver = remote_trap_receiver().await;
1360 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1361
1362 let msg = build_v3_notification(
1363 crate::pdu::PduType::TrapV2,
1364 b"remote-sender-engine",
1365 7,
1366 123_456,
1367 b"trapuser",
1368 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1369 );
1370 assert!(
1371 receiver.handle_v3(msg, source).await.is_err(),
1372 "trap with wrong auth key should be rejected"
1373 );
1374
1375 let good = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1377 assert!(receiver.handle_v3(good, source).await.unwrap().is_some());
1378 }
1379
1380 #[tokio::test]
1381 async fn test_v3_inform_outside_time_window_rejected() {
1382 let receiver = NotificationReceiver::builder()
1383 .bind("127.0.0.1:0")
1384 .engine_id(b"test-engine".to_vec())
1385 .engine_boots(1)
1386 .usm_user("informuser", |u| {
1387 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1388 })
1389 .build()
1390 .await
1391 .unwrap();
1392
1393 let engine_id = b"test-engine";
1394 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1395
1396 let msg = build_authed_v3_inform(
1398 engine_id,
1399 1, 5000, b"informuser",
1402 b"authpass12345678",
1403 AuthProtocol::Sha1,
1404 );
1405
1406 let result = receiver.handle_v3(msg, source).await;
1407 assert!(
1408 result.is_err(),
1409 "message with engine_time=5000 should be rejected (outside 150s window)"
1410 );
1411 }
1412
1413 #[tokio::test]
1414 async fn test_v3_inform_wrong_boots_rejected() {
1415 let receiver = NotificationReceiver::builder()
1416 .bind("127.0.0.1:0")
1417 .engine_id(b"test-engine".to_vec())
1418 .engine_boots(1)
1419 .usm_user("informuser", |u| {
1420 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1421 })
1422 .build()
1423 .await
1424 .unwrap();
1425
1426 let engine_id = b"test-engine";
1427 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1428
1429 let msg = build_authed_v3_inform(
1431 engine_id,
1432 2, 0, b"informuser",
1435 b"authpass12345678",
1436 AuthProtocol::Sha1,
1437 );
1438
1439 let result = receiver.handle_v3(msg, source).await;
1440 assert!(
1441 result.is_err(),
1442 "message with wrong engine_boots should be rejected"
1443 );
1444 }
1445
1446 #[tokio::test]
1447 async fn test_v3_inform_within_time_window_accepted() {
1448 let receiver = NotificationReceiver::builder()
1449 .bind("127.0.0.1:0")
1450 .engine_id(b"test-engine".to_vec())
1451 .engine_boots(1)
1452 .usm_user("informuser", |u| {
1453 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1454 })
1455 .build()
1456 .await
1457 .unwrap();
1458
1459 let engine_id = b"test-engine";
1460 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1461
1462 let msg = build_authed_v3_inform(
1464 engine_id,
1465 1, 0, b"informuser",
1468 b"authpass12345678",
1469 AuthProtocol::Sha1,
1470 );
1471
1472 let result = receiver.handle_v3(msg, source).await;
1473 match result {
1479 Ok(Some(_)) => {} Err(e) => {
1481 let err_str = format!("{e}");
1482 assert!(
1483 !err_str.contains("Auth"),
1484 "should not be an auth error for valid time window, got: {err_str}"
1485 );
1486 }
1487 Ok(None) => panic!("should not return None for a valid InformRequest"),
1488 }
1489 }
1490
1491 fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
1493 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1494 use crate::pdu::{Pdu, PduType};
1495 use crate::v3::UsmSecurityParams;
1496
1497 let pdu = Pdu {
1498 pdu_type: PduType::GetRequest,
1499 request_id: 0,
1500 error_status: 0,
1501 error_index: 0,
1502 varbinds: vec![],
1503 };
1504
1505 let global = MsgGlobalData::new(
1506 msg_id,
1507 65507,
1508 MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
1509 );
1510
1511 let usm_params = UsmSecurityParams::new(
1512 Bytes::new(), 0,
1514 0,
1515 Bytes::new(), );
1517
1518 let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
1519 let msg = V3Message::new(global, usm_params.encode(), scoped);
1520 msg.encode()
1521 }
1522
1523 #[tokio::test]
1524 async fn test_v3_discovery_gets_response() {
1525 use crate::message::V3Message;
1526 use crate::v3::UsmSecurityParams;
1527 use crate::value::Value;
1528
1529 let receiver = NotificationReceiver::builder()
1530 .bind("127.0.0.1:0")
1531 .engine_id(b"test-discovery-engine".to_vec())
1532 .build()
1533 .await
1534 .unwrap();
1535
1536 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1539 let client_addr = client.local_addr().unwrap();
1540
1541 let discovery_msg = build_v3_discovery_request(42, true);
1542 let result = receiver.handle_v3(discovery_msg, client_addr).await;
1543
1544 assert!(result.is_ok());
1546 assert!(result.unwrap().is_none());
1547
1548 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1550
1551 let mut buf = vec![0u8; 4096];
1554 let (len, _) = tokio::time::timeout(
1555 std::time::Duration::from_secs(1),
1556 client.recv_from(&mut buf),
1557 )
1558 .await
1559 .expect("expected a discovery Report")
1560 .unwrap();
1561
1562 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1563 assert_eq!(
1564 report.global_data.msg_flags.security_level,
1565 SecurityLevel::NoAuthNoPriv
1566 );
1567 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1568 assert_eq!(report_usm.engine_id.as_ref(), b"test-discovery-engine");
1569 let scoped = report.scoped_pdu().expect("report should be plaintext");
1570 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1571 assert_eq!(
1572 scoped.pdu.varbinds[0].oid,
1573 crate::v3::report_oids::unknown_engine_ids()
1574 );
1575 assert_eq!(scoped.pdu.varbinds[0].value, Value::Counter32(1));
1576 }
1577
1578 #[tokio::test]
1579 async fn test_v3_discovery_non_reportable_ignored() {
1580 let receiver = NotificationReceiver::builder()
1581 .bind("127.0.0.1:0")
1582 .engine_id(b"test-discovery-engine".to_vec())
1583 .build()
1584 .await
1585 .unwrap();
1586
1587 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1588 let discovery_msg = build_v3_discovery_request(42, false);
1589
1590 let result = receiver.handle_v3(discovery_msg, source).await;
1591
1592 assert!(result.is_ok());
1596 assert!(result.unwrap().is_none());
1597 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1598 }
1599
1600 #[tokio::test]
1606 async fn test_v3_inform_under_remote_engine_id_accepted() {
1607 let receiver = NotificationReceiver::builder()
1608 .bind("127.0.0.1:0")
1609 .engine_id(b"my-receiver-engine".to_vec())
1610 .engine_boots(1)
1611 .usm_user("informuser", |u| {
1612 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1613 })
1614 .build()
1615 .await
1616 .unwrap();
1617
1618 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1619
1620 let msg = build_authed_v3_inform(
1622 b"remote-engine-id",
1623 1,
1624 0,
1625 b"informuser",
1626 b"authpass12345678",
1627 AuthProtocol::Sha1,
1628 );
1629
1630 let result = receiver.handle_v3(msg, source).await.unwrap();
1631 assert!(
1632 matches!(result, Some(Notification::InformV3 { .. })),
1633 "authenticated inform under a remote engine ID should be accepted, got {result:?}"
1634 );
1635 }
1636
1637 #[test]
1638 fn test_auto_generated_engine_id_non_empty() {
1639 let builder = NotificationReceiverBuilder::new();
1640 assert!(builder.engine_id.is_none());
1642 }
1643
1644 #[tokio::test]
1645 async fn test_bind_generates_engine_id() {
1646 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1647 assert!(!receiver.engine_id().is_empty());
1648 assert_eq!(receiver.engine_id()[0], 0x80);
1650 }
1651
1652 #[tokio::test]
1653 async fn test_builder_generates_engine_id() {
1654 let receiver = NotificationReceiver::builder()
1655 .bind("127.0.0.1:0")
1656 .build()
1657 .await
1658 .unwrap();
1659 assert!(!receiver.engine_id().is_empty());
1660 assert_eq!(receiver.engine_id()[0], 0x80);
1661 }
1662
1663 #[tokio::test]
1664 async fn test_builder_custom_engine_id() {
1665 let receiver = NotificationReceiver::builder()
1666 .bind("127.0.0.1:0")
1667 .engine_id(b"custom-engine".to_vec())
1668 .build()
1669 .await
1670 .unwrap();
1671 assert_eq!(receiver.engine_id(), b"custom-engine");
1672 }
1673
1674 #[tokio::test]
1675 async fn test_usm_counter_accessors_default_zero() {
1676 let receiver = remote_trap_receiver().await;
1677 assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1678 assert_eq!(receiver.usm_unknown_usernames(), 0);
1679 assert_eq!(receiver.usm_wrong_digests(), 0);
1680 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1681 assert_eq!(receiver.usm_unsupported_sec_levels(), 0);
1682 assert_eq!(receiver.usm_decryption_errors(), 0);
1683 }
1684
1685 #[tokio::test]
1688 async fn test_v3_trap_wrong_digest_increments_counter() {
1689 let receiver = remote_trap_receiver().await;
1690 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1691
1692 let msg = build_v3_notification(
1693 crate::pdu::PduType::TrapV2,
1694 b"remote-sender-engine",
1695 7,
1696 123_456,
1697 b"trapuser",
1698 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1699 );
1700 assert!(receiver.handle_v3(msg, source).await.is_err());
1701 assert_eq!(receiver.usm_wrong_digests(), 1);
1702 }
1703
1704 #[tokio::test]
1707 async fn test_v3_trap_unknown_user_increments_counter() {
1708 let receiver = remote_trap_receiver().await;
1709 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1710
1711 let msg = build_v3_notification(
1712 crate::pdu::PduType::TrapV2,
1713 b"remote-sender-engine",
1714 7,
1715 123_456,
1716 b"nosuchuser",
1717 Some((b"authpass12345678", AuthProtocol::Sha1)),
1718 );
1719 let result = receiver.handle_v3(msg, source).await.unwrap();
1720 assert!(result.is_none(), "unknown user must not be delivered");
1721 assert_eq!(receiver.usm_unknown_usernames(), 1);
1722 assert_eq!(receiver.usm_wrong_digests(), 0);
1723 }
1724
1725 #[tokio::test]
1729 async fn test_v3_trap_user_without_auth_key_increments_counter() {
1730 let receiver = NotificationReceiver::builder()
1731 .bind("127.0.0.1:0")
1732 .engine_id(b"my-receiver-engine".to_vec())
1733 .usm_user("plainuser", |u| u)
1734 .build()
1735 .await
1736 .unwrap();
1737 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1738
1739 let msg = build_v3_notification(
1740 crate::pdu::PduType::TrapV2,
1741 b"remote-sender-engine",
1742 7,
1743 123_456,
1744 b"plainuser",
1745 Some((b"authpass12345678", AuthProtocol::Sha1)),
1746 );
1747 let result = receiver.handle_v3(msg, source).await.unwrap();
1748 assert!(result.is_none());
1749 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
1750 assert_eq!(receiver.usm_unknown_usernames(), 0);
1751 }
1752
1753 #[tokio::test]
1756 async fn test_v3_inform_time_window_failure_increments_counter() {
1757 let receiver = NotificationReceiver::builder()
1758 .bind("127.0.0.1:0")
1759 .engine_id(b"test-engine".to_vec())
1760 .engine_boots(1)
1761 .usm_user("informuser", |u| {
1762 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1763 })
1764 .build()
1765 .await
1766 .unwrap();
1767 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1768
1769 let msg = build_authed_v3_inform(
1770 b"test-engine",
1771 1,
1772 5000,
1773 b"informuser",
1774 b"authpass12345678",
1775 AuthProtocol::Sha1,
1776 );
1777 assert!(receiver.handle_v3(msg, source).await.is_err());
1778 assert_eq!(receiver.usm_not_in_time_windows(), 1);
1779 }
1780
1781 #[tokio::test]
1787 async fn test_v3_trap_remote_stale_not_counted() {
1788 let receiver = remote_trap_receiver().await;
1789 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1790
1791 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1792 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1793
1794 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1795 assert!(receiver.handle_v3(stale, source).await.is_err());
1796 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1797 }
1798
1799 #[tokio::test]
1803 async fn test_v3_inform_remote_stale_gets_no_report() {
1804 let receiver = remote_trap_receiver().await;
1805
1806 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1807 let client_addr = client.local_addr().unwrap();
1808
1809 let fresh = build_v3_notification(
1810 crate::pdu::PduType::InformRequest,
1811 b"remote-sender-engine",
1812 7,
1813 10_000,
1814 b"trapuser",
1815 Some((b"authpass12345678", AuthProtocol::Sha1)),
1816 );
1817 assert!(
1818 receiver
1819 .handle_v3(fresh, client_addr)
1820 .await
1821 .unwrap()
1822 .is_some()
1823 );
1824
1825 let mut buf = vec![0u8; 4096];
1827 tokio::time::timeout(
1828 std::time::Duration::from_secs(1),
1829 client.recv_from(&mut buf),
1830 )
1831 .await
1832 .expect("expected the inform response")
1833 .unwrap();
1834
1835 let stale = build_v3_notification(
1836 crate::pdu::PduType::InformRequest,
1837 b"remote-sender-engine",
1838 7,
1839 5_000,
1840 b"trapuser",
1841 Some((b"authpass12345678", AuthProtocol::Sha1)),
1842 );
1843 assert!(receiver.handle_v3(stale, client_addr).await.is_err());
1844 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1845
1846 let result = tokio::time::timeout(
1847 std::time::Duration::from_millis(200),
1848 client.recv_from(&mut buf),
1849 )
1850 .await;
1851 assert!(
1852 result.is_err(),
1853 "no Report may be sent for a Step 7b timeliness failure"
1854 );
1855 }
1856
1857 fn build_v3_trap_bad_ciphertext(
1861 engine_id: &[u8],
1862 username: &[u8],
1863 auth_password: &[u8],
1864 ) -> Bytes {
1865 use crate::message::{MsgFlags, MsgGlobalData, V3Message};
1866 use crate::v3::auth::authenticate_message;
1867 use crate::v3::{LocalizedKey, UsmSecurityParams};
1868
1869 let auth_key =
1870 LocalizedKey::from_password(AuthProtocol::Sha1, auth_password, engine_id).unwrap();
1871
1872 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthPriv, false));
1873 let usm_params = UsmSecurityParams::new(
1874 Bytes::copy_from_slice(engine_id),
1875 7,
1876 123_456,
1877 Bytes::copy_from_slice(username),
1878 )
1879 .with_auth_placeholder(auth_key.mac_len())
1880 .with_priv_params(Bytes::from_static(b"bad"));
1881
1882 let msg = V3Message::new_encrypted(
1883 global,
1884 usm_params.encode(),
1885 Bytes::from_static(b"not-a-valid-ciphertext"),
1886 );
1887 let mut msg_bytes = msg.encode().to_vec();
1888 let (auth_offset, auth_len) =
1889 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1890 authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1891 Bytes::from(msg_bytes)
1892 }
1893
1894 #[tokio::test]
1897 async fn test_v3_decryption_error_increments_counter() {
1898 let receiver = NotificationReceiver::builder()
1899 .bind("127.0.0.1:0")
1900 .engine_id(b"my-receiver-engine".to_vec())
1901 .usm_user("privuser", |u| {
1902 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1903 .privacy(crate::v3::PrivProtocol::Aes128, b"privpass12345678")
1904 })
1905 .build()
1906 .await
1907 .unwrap();
1908 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1909
1910 let msg =
1911 build_v3_trap_bad_ciphertext(b"remote-sender-engine", b"privuser", b"authpass12345678");
1912 assert!(receiver.handle_v3(msg, source).await.is_err());
1913 assert_eq!(receiver.usm_decryption_errors(), 1);
1914 }
1915
1916 #[tokio::test]
1921 async fn test_v3_authpriv_for_auth_only_user_counts_unsupported_sec_level() {
1922 let receiver = remote_trap_receiver().await;
1923 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1924
1925 let msg = build_v3_trap_bad_ciphertext(
1926 b"remote-sender-engine",
1927 b"trapuser",
1928 b"wrong-password-1234",
1929 );
1930 let result = receiver.handle_v3(msg, source).await.unwrap();
1931 assert!(result.is_none());
1932 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
1933 assert_eq!(receiver.usm_wrong_digests(), 0);
1934 }
1935
1936 #[tokio::test]
1942 async fn test_v3_failed_inform_gets_authenticated_time_window_report() {
1943 use crate::message::V3Message;
1944 use crate::v3::auth::verify_message;
1945 use crate::v3::{LocalizedKey, UsmSecurityParams};
1946
1947 let receiver = NotificationReceiver::builder()
1948 .bind("127.0.0.1:0")
1949 .engine_id(b"test-engine".to_vec())
1950 .engine_boots(1)
1951 .usm_user("informuser", |u| {
1952 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1953 })
1954 .build()
1955 .await
1956 .unwrap();
1957
1958 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1959 let client_addr = client.local_addr().unwrap();
1960
1961 let msg = build_authed_v3_inform(
1962 b"test-engine",
1963 1,
1964 5000, b"informuser",
1966 b"authpass12345678",
1967 AuthProtocol::Sha1,
1968 );
1969 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
1970
1971 let mut buf = vec![0u8; 4096];
1972 let (len, _) = tokio::time::timeout(
1973 std::time::Duration::from_secs(1),
1974 client.recv_from(&mut buf),
1975 )
1976 .await
1977 .expect("expected a Report in response to the failed inform")
1978 .unwrap();
1979 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
1980
1981 let report = V3Message::decode(report_bytes.clone()).unwrap();
1982 assert_eq!(
1983 report.global_data.msg_flags.security_level,
1984 SecurityLevel::AuthNoPriv,
1985 "notInTimeWindows report must be authenticated (authNoPriv)"
1986 );
1987 assert!(!report.global_data.msg_flags.reportable);
1988
1989 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1990 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
1991
1992 let key =
1995 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
1996 .unwrap();
1997 let (auth_offset, auth_len) =
1998 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
1999 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2000
2001 let scoped = report.scoped_pdu().expect("report should be plaintext");
2002 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2003 assert_eq!(
2004 scoped.pdu.varbinds[0].oid,
2005 crate::v3::report_oids::not_in_time_windows()
2006 );
2007 }
2008
2009 #[tokio::test]
2013 async fn test_v3_latched_boots_report_is_authenticated() {
2014 use crate::message::V3Message;
2015 use crate::v3::MAX_ENGINE_TIME;
2016 use crate::v3::auth::verify_message;
2017 use crate::v3::{LocalizedKey, UsmSecurityParams};
2018
2019 let receiver = NotificationReceiver::builder()
2020 .bind("127.0.0.1:0")
2021 .engine_id(b"test-engine".to_vec())
2022 .engine_boots(MAX_ENGINE_TIME)
2023 .usm_user("informuser", |u| {
2024 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2025 })
2026 .build()
2027 .await
2028 .unwrap();
2029
2030 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2031 let client_addr = client.local_addr().unwrap();
2032
2033 let msg = build_authed_v3_inform(
2034 b"test-engine",
2035 MAX_ENGINE_TIME,
2036 0,
2037 b"informuser",
2038 b"authpass12345678",
2039 AuthProtocol::Sha1,
2040 );
2041 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2042 assert_eq!(receiver.usm_not_in_time_windows(), 1);
2043
2044 let mut buf = vec![0u8; 4096];
2045 let (len, _) = tokio::time::timeout(
2046 std::time::Duration::from_secs(1),
2047 client.recv_from(&mut buf),
2048 )
2049 .await
2050 .expect("expected a Report in response to the failed inform")
2051 .unwrap();
2052 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2053
2054 let report = V3Message::decode(report_bytes.clone()).unwrap();
2055 assert_eq!(
2056 report.global_data.msg_flags.security_level,
2057 SecurityLevel::AuthNoPriv,
2058 "notInTimeWindows report must be authenticated (authNoPriv)"
2059 );
2060 let key =
2061 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2062 .unwrap();
2063 let (auth_offset, auth_len) =
2064 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2065 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2066
2067 let scoped = report.scoped_pdu().expect("report should be plaintext");
2068 assert_eq!(
2069 scoped.pdu.varbinds[0].oid,
2070 crate::v3::report_oids::not_in_time_windows()
2071 );
2072 }
2073
2074 #[tokio::test]
2077 async fn test_v3_failed_inform_unknown_user_gets_noauth_report() {
2078 use crate::message::V3Message;
2079 use crate::v3::UsmSecurityParams;
2080
2081 let receiver = NotificationReceiver::builder()
2082 .bind("127.0.0.1:0")
2083 .engine_id(b"test-engine".to_vec())
2084 .engine_boots(1)
2085 .usm_user("informuser", |u| {
2086 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2087 })
2088 .build()
2089 .await
2090 .unwrap();
2091
2092 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2093 let client_addr = client.local_addr().unwrap();
2094
2095 let msg = build_authed_v3_inform(
2096 b"test-engine",
2097 1,
2098 0,
2099 b"nosuchuser",
2100 b"authpass12345678",
2101 AuthProtocol::Sha1,
2102 );
2103 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
2104 assert!(result.is_none());
2105 assert_eq!(receiver.usm_unknown_usernames(), 1);
2106
2107 let mut buf = vec![0u8; 4096];
2108 let (len, _) = tokio::time::timeout(
2109 std::time::Duration::from_secs(1),
2110 client.recv_from(&mut buf),
2111 )
2112 .await
2113 .expect("expected a Report in response to the failed inform")
2114 .unwrap();
2115
2116 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
2117 assert_eq!(
2118 report.global_data.msg_flags.security_level,
2119 SecurityLevel::NoAuthNoPriv
2120 );
2121 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2122 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2123 let scoped = report.scoped_pdu().expect("report should be plaintext");
2124 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2125 assert_eq!(
2126 scoped.pdu.varbinds[0].oid,
2127 crate::v3::report_oids::unknown_user_names()
2128 );
2129 }
2130
2131 #[tokio::test]
2134 async fn test_v3_failed_trap_gets_no_report() {
2135 let receiver = remote_trap_receiver().await;
2136
2137 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2138 let client_addr = client.local_addr().unwrap();
2139
2140 let msg = build_v3_notification(
2141 crate::pdu::PduType::TrapV2,
2142 b"remote-sender-engine",
2143 7,
2144 123_456,
2145 b"trapuser",
2146 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
2147 );
2148 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2149 assert_eq!(receiver.usm_wrong_digests(), 1);
2150
2151 let mut buf = vec![0u8; 4096];
2152 let result = tokio::time::timeout(
2153 std::time::Duration::from_millis(200),
2154 client.recv_from(&mut buf),
2155 )
2156 .await;
2157 assert!(result.is_err(), "no Report may be sent for a failed trap");
2158 }
2159
2160 #[test]
2161 fn test_community_allowed() {
2162 assert!(community_allowed(&[], b"public"));
2164 assert!(community_allowed(&[], b""));
2165
2166 let configured = vec![b"public".to_vec(), b"monitor".to_vec()];
2167 assert!(community_allowed(&configured, b"public"));
2168 assert!(community_allowed(&configured, b"monitor"));
2169 assert!(!community_allowed(&configured, b"private"));
2171 assert!(!community_allowed(&configured, b"pub"));
2172 assert!(!community_allowed(&configured, b"publicx"));
2173 assert!(!community_allowed(&configured, b""));
2174 }
2175
2176 fn build_v2c_trap(community: &[u8]) -> Bytes {
2177 use crate::message::CommunityMessage;
2178 use crate::pdu::Pdu;
2179 let pdu = Pdu::trap_v2(1, 100, &oids::cold_start(), vec![]);
2180 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2181 }
2182
2183 fn build_v2c_inform(community: &[u8]) -> Bytes {
2184 use crate::message::CommunityMessage;
2185 use crate::pdu::Pdu;
2186 let pdu = Pdu::inform_request(1, 100, &oids::cold_start(), vec![]);
2187 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2188 }
2189
2190 fn build_v1_trap(community: &[u8]) -> Bytes {
2191 use crate::message::CommunityMessage;
2192 use crate::pdu::GenericTrap;
2193 let trap = TrapV1Pdu::new(
2194 oid!(1, 3, 6, 1, 4, 1, 9999),
2195 [192, 168, 1, 1],
2196 GenericTrap::ColdStart,
2197 0,
2198 12345,
2199 vec![],
2200 );
2201 CommunityMessage::v1_trap(Bytes::copy_from_slice(community), trap).encode()
2202 }
2203
2204 #[tokio::test]
2205 async fn test_v2c_trap_matching_community_accepted() {
2206 let receiver = NotificationReceiver::builder()
2207 .bind("127.0.0.1:0")
2208 .community(b"public")
2209 .build()
2210 .await
2211 .unwrap();
2212 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2213
2214 let result = receiver
2215 .handle_v2c(build_v2c_trap(b"public"), source)
2216 .await
2217 .unwrap();
2218 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2219 }
2220
2221 #[tokio::test]
2222 async fn test_v2c_trap_wrong_community_dropped() {
2223 let receiver = NotificationReceiver::builder()
2224 .bind("127.0.0.1:0")
2225 .community(b"public")
2226 .build()
2227 .await
2228 .unwrap();
2229 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2230
2231 let result = receiver
2232 .handle_v2c(build_v2c_trap(b"private"), source)
2233 .await
2234 .unwrap();
2235 assert!(result.is_none());
2236 }
2237
2238 #[tokio::test]
2239 async fn test_v2c_trap_no_allowlist_accepts_any_community() {
2240 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
2241 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2242
2243 let result = receiver
2244 .handle_v2c(build_v2c_trap(b"anything"), source)
2245 .await
2246 .unwrap();
2247 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2248 }
2249
2250 #[tokio::test]
2251 async fn test_v1_trap_wrong_community_dropped() {
2252 let receiver = NotificationReceiver::builder()
2253 .bind("127.0.0.1:0")
2254 .community(b"public")
2255 .build()
2256 .await
2257 .unwrap();
2258 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2259
2260 assert!(
2261 receiver
2262 .handle_v1(build_v1_trap(b"private"), source)
2263 .await
2264 .unwrap()
2265 .is_none()
2266 );
2267 assert!(matches!(
2268 receiver
2269 .handle_v1(build_v1_trap(b"public"), source)
2270 .await
2271 .unwrap(),
2272 Some(Notification::TrapV1 { .. })
2273 ));
2274 }
2275
2276 #[tokio::test]
2279 async fn test_v2c_inform_wrong_community_dropped_without_ack() {
2280 let receiver = NotificationReceiver::builder()
2281 .bind("127.0.0.1:0")
2282 .community(b"public")
2283 .build()
2284 .await
2285 .unwrap();
2286
2287 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2288 let client_addr = client.local_addr().unwrap();
2289
2290 let result = receiver
2291 .handle_v2c(build_v2c_inform(b"private"), client_addr)
2292 .await
2293 .unwrap();
2294 assert!(result.is_none());
2295
2296 let mut buf = vec![0u8; 4096];
2297 let recv = tokio::time::timeout(
2298 std::time::Duration::from_millis(200),
2299 client.recv_from(&mut buf),
2300 )
2301 .await;
2302 assert!(recv.is_err(), "a filtered inform must not be acknowledged");
2303 }
2304
2305 #[tokio::test]
2308 async fn test_v2c_inform_matching_community_acked() {
2309 let receiver = NotificationReceiver::builder()
2310 .bind("127.0.0.1:0")
2311 .community(b"public")
2312 .build()
2313 .await
2314 .unwrap();
2315
2316 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2317 let client_addr = client.local_addr().unwrap();
2318
2319 let result = receiver
2320 .handle_v2c(build_v2c_inform(b"public"), client_addr)
2321 .await
2322 .unwrap();
2323 assert!(matches!(result, Some(Notification::InformV2c { .. })));
2324
2325 let mut buf = vec![0u8; 4096];
2326 let (len, _) = tokio::time::timeout(
2327 std::time::Duration::from_secs(1),
2328 client.recv_from(&mut buf),
2329 )
2330 .await
2331 .expect("a matching inform must be acknowledged")
2332 .unwrap();
2333 assert!(len > 0);
2334 }
2335}