1mod handlers;
89mod types;
90mod varbind;
91
92use std::collections::HashMap;
93use std::net::SocketAddr;
94use std::sync::atomic::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::error::{Error, Result};
104use crate::message::SecurityLevel;
105use crate::oid::Oid;
106use crate::pdu::TrapV1Pdu;
107use crate::util::bind_udp_socket;
108use crate::v3::process::UsmStats;
109use crate::v3::{EngineState, SaltCounter};
110use crate::varbind::VarBind;
111use crate::version::Version;
112
113pub use types::{DerivedKeys, UsmConfig};
115pub use varbind::validate_notification_varbinds;
116
117const MAX_REMOTE_ENGINES: usize = 8192;
123
124pub(super) fn community_allowed(configured: &[Vec<u8>], community: &[u8]) -> bool {
132 if configured.is_empty() {
133 return true;
134 }
135 let mut valid = false;
136 for candidate in configured {
137 if candidate.len() == community.len() && bool::from(candidate.as_slice().ct_eq(community)) {
138 valid = true;
139 }
140 }
141 valid
142}
143
144pub mod oids {
146 use crate::oid;
147
148 #[must_use]
150 pub fn sys_uptime() -> crate::Oid {
151 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
152 }
153
154 #[must_use]
156 pub fn snmp_trap_oid() -> crate::Oid {
157 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
158 }
159
160 #[must_use]
162 pub fn snmp_trap_enterprise() -> crate::Oid {
163 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
164 }
165
166 #[must_use]
168 pub fn snmp_trap_address() -> crate::Oid {
169 oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
170 }
171
172 #[must_use]
174 pub fn snmp_traps() -> crate::Oid {
175 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
176 }
177
178 #[must_use]
180 pub fn cold_start() -> crate::Oid {
181 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
182 }
183
184 #[must_use]
186 pub fn warm_start() -> crate::Oid {
187 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
188 }
189
190 #[must_use]
192 pub fn link_down() -> crate::Oid {
193 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
194 }
195
196 #[must_use]
198 pub fn link_up() -> crate::Oid {
199 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
200 }
201
202 #[must_use]
204 pub fn auth_failure() -> crate::Oid {
205 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
206 }
207
208 #[must_use]
210 pub fn egp_neighbor_loss() -> crate::Oid {
211 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
212 }
213}
214
215pub struct NotificationReceiverBuilder {
222 bind_addr: String,
223 usm_users: HashMap<Bytes, UsmConfig>,
224 communities: Vec<Vec<u8>>,
225 engine_id: Option<Vec<u8>>,
226 engine_boots: u32,
227}
228
229impl NotificationReceiverBuilder {
230 #[must_use]
236 pub fn new() -> Self {
237 Self {
238 bind_addr: "0.0.0.0:162".to_string(),
239 usm_users: HashMap::new(),
240 communities: Vec::new(),
241 engine_id: None,
242 engine_boots: 1,
243 }
244 }
245
246 #[must_use]
250 pub fn bind(mut self, addr: impl Into<String>) -> Self {
251 self.bind_addr = addr.into();
252 self
253 }
254
255 #[must_use]
276 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
277 where
278 F: FnOnce(UsmConfig) -> UsmConfig,
279 {
280 let username_bytes: Bytes = username.into();
281 let config = configure(UsmConfig::new(username_bytes.clone()));
282 self.usm_users.insert(username_bytes, config);
283 self
284 }
285
286 #[must_use]
314 pub fn community(mut self, community: &[u8]) -> Self {
315 self.communities.push(community.to_vec());
316 self
317 }
318
319 #[must_use]
339 pub fn communities<I, C>(mut self, communities: I) -> Self
340 where
341 I: IntoIterator<Item = C>,
342 C: AsRef<[u8]>,
343 {
344 for c in communities {
345 self.communities.push(c.as_ref().to_vec());
346 }
347 self
348 }
349
350 #[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(mut self) -> Result<NotificationReceiver> {
373 for config in self.usm_users.values_mut() {
377 config.validate()?;
378 config.precompute_master_keys();
379 }
380
381 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
382 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
383 })?;
384
385 let socket = bind_udp_socket(bind_addr, None, None, false)
386 .await
387 .map_err(|e| Error::Network {
388 target: bind_addr,
389 source: e,
390 })?;
391
392 let local_addr = socket.local_addr().map_err(|e| Error::Network {
393 target: bind_addr,
394 source: e,
395 })?;
396
397 let engine_id: Bytes = match self.engine_id {
399 Some(id) => {
400 crate::v3::validate_engine_id(&id)?;
401 Bytes::from(id)
402 }
403 None => crate::v3::generate_engine_id(),
404 };
405
406 Ok(NotificationReceiver {
407 inner: Arc::new(ReceiverInner {
408 socket,
409 local_addr,
410 usm_users: self.usm_users,
411 communities: self.communities,
412 engine_id,
413 salt_counter: SaltCounter::new(),
414 engine_boots_base: self.engine_boots,
415 engine_start: Instant::now(),
416 usm_stats: UsmStats::default(),
417 remote_engines: Mutex::new(HashMap::new()),
418 }),
419 })
420 }
421}
422
423impl Default for NotificationReceiverBuilder {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429#[derive(Debug, Clone)]
436pub enum Notification {
437 TrapV1 {
439 community: Bytes,
441 trap: TrapV1Pdu,
443 },
444
445 TrapV2c {
447 community: Bytes,
449 uptime: u32,
451 trap_oid: Oid,
453 varbinds: Vec<VarBind>,
455 request_id: i32,
457 },
458
459 TrapV3 {
461 username: Bytes,
463 context_engine_id: Bytes,
465 context_name: Bytes,
467 security_level: SecurityLevel,
471 uptime: u32,
473 trap_oid: Oid,
475 varbinds: Vec<VarBind>,
477 request_id: i32,
479 },
480
481 InformV2c {
485 community: Bytes,
487 uptime: u32,
489 trap_oid: Oid,
491 varbinds: Vec<VarBind>,
493 request_id: i32,
495 },
496
497 InformV3 {
501 username: Bytes,
503 context_engine_id: Bytes,
505 context_name: Bytes,
507 security_level: SecurityLevel,
511 uptime: u32,
513 trap_oid: Oid,
515 varbinds: Vec<VarBind>,
517 request_id: i32,
519 },
520}
521
522impl Notification {
523 pub fn trap_oid(&self) -> Result<Oid> {
528 match self {
529 Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
530 Notification::TrapV2c { trap_oid, .. }
531 | Notification::TrapV3 { trap_oid, .. }
532 | Notification::InformV2c { trap_oid, .. }
533 | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
534 }
535 }
536
537 pub fn uptime(&self) -> u32 {
539 match self {
540 Notification::TrapV1 { trap, .. } => trap.time_stamp,
541 Notification::TrapV2c { uptime, .. }
542 | Notification::TrapV3 { uptime, .. }
543 | Notification::InformV2c { uptime, .. }
544 | Notification::InformV3 { uptime, .. } => *uptime,
545 }
546 }
547
548 pub fn varbinds(&self) -> &[VarBind] {
550 match self {
551 Notification::TrapV1 { trap, .. } => &trap.varbinds,
552 Notification::TrapV2c { varbinds, .. }
553 | Notification::TrapV3 { varbinds, .. }
554 | Notification::InformV2c { varbinds, .. }
555 | Notification::InformV3 { varbinds, .. } => varbinds,
556 }
557 }
558
559 pub fn security_level(&self) -> Option<SecurityLevel> {
566 match self {
567 Notification::TrapV1 { .. }
568 | Notification::TrapV2c { .. }
569 | Notification::InformV2c { .. } => None,
570 Notification::TrapV3 { security_level, .. }
571 | Notification::InformV3 { security_level, .. } => Some(*security_level),
572 }
573 }
574
575 pub fn is_confirmed(&self) -> bool {
577 matches!(
578 self,
579 Notification::InformV2c { .. } | Notification::InformV3 { .. }
580 )
581 }
582
583 pub fn version(&self) -> Version {
585 match self {
586 Notification::TrapV1 { .. } => Version::V1,
587 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
588 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
589 }
590 }
591}
592
593pub struct NotificationReceiver {
619 inner: Arc<ReceiverInner>,
620}
621
622struct ReceiverInner {
623 socket: UdpSocket,
624 local_addr: SocketAddr,
625 usm_users: HashMap<Bytes, UsmConfig>,
627 communities: Vec<Vec<u8>>,
631 engine_id: Bytes,
633 salt_counter: SaltCounter,
635 engine_boots_base: u32,
637 engine_start: Instant,
639 usm_stats: UsmStats,
641 remote_engines: Mutex<HashMap<Bytes, EngineState>>,
648}
649
650impl NotificationReceiver {
651 #[must_use]
655 pub fn builder() -> NotificationReceiverBuilder {
656 NotificationReceiverBuilder::new()
657 }
658
659 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
685 let addr_str = addr.as_ref();
686 let bind_addr: SocketAddr = addr_str
687 .parse()
688 .map_err(|_| Error::Config(format!("invalid bind address: {addr_str}").into()))?;
689
690 let socket = bind_udp_socket(bind_addr, None, None, false)
691 .await
692 .map_err(|e| Error::Network {
693 target: bind_addr,
694 source: e,
695 })?;
696
697 let local_addr = socket.local_addr().map_err(|e| Error::Network {
698 target: bind_addr,
699 source: e,
700 })?;
701
702 let engine_id: Bytes = {
703 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
704 let timestamp = std::time::SystemTime::now()
705 .duration_since(std::time::UNIX_EPOCH)
706 .unwrap_or_default()
707 .as_secs();
708 id.extend_from_slice(×tamp.to_be_bytes());
709 Bytes::from(id)
710 };
711
712 Ok(Self {
713 inner: Arc::new(ReceiverInner {
714 socket,
715 local_addr,
716 usm_users: HashMap::new(),
717 communities: Vec::new(),
718 engine_id,
719 salt_counter: SaltCounter::new(),
720 engine_boots_base: 1,
721 engine_start: Instant::now(),
722 usm_stats: UsmStats::default(),
723 remote_engines: Mutex::new(HashMap::new()),
724 }),
725 })
726 }
727
728 #[must_use]
730 pub fn local_addr(&self) -> SocketAddr {
731 self.inner.local_addr
732 }
733
734 #[must_use]
736 pub fn engine_id(&self) -> &[u8] {
737 &self.inner.engine_id
738 }
739
740 #[must_use]
742 pub fn usm_unknown_engine_ids(&self) -> u32 {
743 self.inner
744 .usm_stats
745 .unknown_engine_ids
746 .load(Ordering::Relaxed)
747 }
748
749 #[must_use]
751 pub fn usm_unknown_usernames(&self) -> u32 {
752 self.inner
753 .usm_stats
754 .unknown_usernames
755 .load(Ordering::Relaxed)
756 }
757
758 #[must_use]
760 pub fn usm_wrong_digests(&self) -> u32 {
761 self.inner.usm_stats.wrong_digests.load(Ordering::Relaxed)
762 }
763
764 #[must_use]
766 pub fn usm_not_in_time_windows(&self) -> u32 {
767 self.inner
768 .usm_stats
769 .not_in_time_windows
770 .load(Ordering::Relaxed)
771 }
772
773 #[must_use]
775 pub fn usm_unsupported_sec_levels(&self) -> u32 {
776 self.inner
777 .usm_stats
778 .unsupported_sec_levels
779 .load(Ordering::Relaxed)
780 }
781
782 #[must_use]
784 pub fn usm_decryption_errors(&self) -> u32 {
785 self.inner
786 .usm_stats
787 .decryption_errors
788 .load(Ordering::Relaxed)
789 }
790
791 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
798 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
799 let mut buf = vec![0u8; 65535];
800
801 loop {
802 let (len, source) =
803 self.inner
804 .socket
805 .recv_from(&mut buf)
806 .await
807 .map_err(|e| Error::Network {
808 target: self.inner.local_addr,
809 source: e,
810 })?;
811
812 let data = Bytes::copy_from_slice(&buf[..len]);
813
814 match self.parse_and_respond(data, source).await {
815 Ok(Some(notification)) => return Ok((notification, source)),
816 Ok(None) => {} Err(e) => {
818 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
820 }
821 }
822 }
823 }
824
825 async fn parse_and_respond(
829 &self,
830 data: Bytes,
831 source: SocketAddr,
832 ) -> Result<Option<Notification>> {
833 match crate::message::peek_version(data.clone(), source)? {
834 Version::V1 => self.handle_v1(data, source).await,
835 Version::V2c => self.handle_v2c(data, source).await,
836 Version::V3 => self.handle_v3(data, source).await,
837 }
838 }
839}
840
841impl Clone for NotificationReceiver {
842 fn clone(&self) -> Self {
843 Self {
844 inner: Arc::clone(&self.inner),
845 }
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use crate::message::SecurityLevel;
853 use crate::oid;
854 use crate::pdu::GenericTrap;
855 use crate::v3::AuthProtocol;
856
857 #[test]
858 fn test_notification_trap_v1() {
859 let trap = TrapV1Pdu::new(
860 oid!(1, 3, 6, 1, 4, 1, 9999),
861 [192, 168, 1, 1],
862 GenericTrap::LinkDown,
863 0,
864 12345,
865 vec![],
866 );
867
868 let notification = Notification::TrapV1 {
869 community: Bytes::from_static(b"public"),
870 trap,
871 };
872
873 assert!(!notification.is_confirmed());
874 assert_eq!(notification.version(), Version::V1);
875 assert_eq!(notification.uptime(), 12345);
876 assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
877 }
878
879 #[test]
880 fn test_notification_trap_v2c() {
881 let notification = Notification::TrapV2c {
882 community: Bytes::from_static(b"public"),
883 uptime: 54321,
884 trap_oid: oids::link_up(),
885 varbinds: vec![],
886 request_id: 1,
887 };
888
889 assert!(!notification.is_confirmed());
890 assert_eq!(notification.version(), Version::V2c);
891 assert_eq!(notification.uptime(), 54321);
892 assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
893 }
894
895 #[test]
896 fn test_notification_inform() {
897 let notification = Notification::InformV2c {
898 community: Bytes::from_static(b"public"),
899 uptime: 11111,
900 trap_oid: oids::cold_start(),
901 varbinds: vec![],
902 request_id: 42,
903 };
904
905 assert!(notification.is_confirmed());
906 assert_eq!(notification.version(), Version::V2c);
907 }
908
909 #[test]
910 fn test_notification_receiver_builder_default() {
911 let builder = NotificationReceiverBuilder::new();
912 assert_eq!(builder.bind_addr, "0.0.0.0:162");
913 assert!(builder.usm_users.is_empty());
914 }
915
916 #[test]
917 fn test_notification_receiver_builder_with_user() {
918 let builder = NotificationReceiverBuilder::new()
919 .bind("0.0.0.0:1162")
920 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
921
922 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
923 assert_eq!(builder.usm_users.len(), 1);
924
925 let user = builder
926 .usm_users
927 .get(&Bytes::from_static(b"trapuser"))
928 .unwrap();
929 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
930 }
931
932 #[tokio::test]
933 async fn test_receiver_builder_rejects_privacy_without_auth() {
934 let result = NotificationReceiverBuilder::new()
935 .bind("127.0.0.1:0")
936 .usm_user("noauth", |u| {
937 u.privacy(crate::v3::PrivProtocol::Aes128, b"privpass")
938 })
939 .build()
940 .await;
941 match result {
942 Err(err) => assert!(
943 matches!(*err, Error::Config(_)),
944 "expected Config error, got {err:?}"
945 ),
946 Ok(_) => panic!("privacy without auth must be rejected"),
947 }
948 }
949
950 #[test]
951 fn test_notification_v3_inform() {
952 let notification = Notification::InformV3 {
953 username: Bytes::from_static(b"testuser"),
954 context_engine_id: Bytes::from_static(b"engine123"),
955 context_name: Bytes::new(),
956 security_level: SecurityLevel::AuthNoPriv,
957 uptime: 99999,
958 trap_oid: oids::warm_start(),
959 varbinds: vec![],
960 request_id: 100,
961 };
962
963 assert!(notification.is_confirmed());
964 assert_eq!(notification.version(), Version::V3);
965 assert_eq!(notification.uptime(), 99999);
966 assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
967 }
968
969 #[test]
970 fn test_notification_security_level_accessor() {
971 let trap_v3 = Notification::TrapV3 {
972 username: Bytes::from_static(b"testuser"),
973 context_engine_id: Bytes::from_static(b"engine123"),
974 context_name: Bytes::new(),
975 security_level: SecurityLevel::AuthPriv,
976 uptime: 1,
977 trap_oid: oids::cold_start(),
978 varbinds: vec![],
979 request_id: 1,
980 };
981 assert_eq!(trap_v3.security_level(), Some(SecurityLevel::AuthPriv));
982
983 let inform_v3 = Notification::InformV3 {
984 username: Bytes::from_static(b"testuser"),
985 context_engine_id: Bytes::from_static(b"engine123"),
986 context_name: Bytes::new(),
987 security_level: SecurityLevel::NoAuthNoPriv,
988 uptime: 1,
989 trap_oid: oids::cold_start(),
990 varbinds: vec![],
991 request_id: 1,
992 };
993 assert_eq!(
994 inform_v3.security_level(),
995 Some(SecurityLevel::NoAuthNoPriv)
996 );
997
998 let trap_v2c = Notification::TrapV2c {
999 community: Bytes::from_static(b"public"),
1000 uptime: 1,
1001 trap_oid: oids::cold_start(),
1002 varbinds: vec![],
1003 request_id: 1,
1004 };
1005 assert_eq!(trap_v2c.security_level(), None);
1006 }
1007
1008 #[test]
1009 fn test_notification_trap_v1_enterprise_specific_oid() {
1010 let trap = TrapV1Pdu::new(
1011 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1012 [192, 168, 1, 1],
1013 GenericTrap::EnterpriseSpecific,
1014 42,
1015 12345,
1016 vec![],
1017 );
1018
1019 let notification = Notification::TrapV1 {
1020 community: Bytes::from_static(b"public"),
1021 trap,
1022 };
1023
1024 assert_eq!(
1025 notification.trap_oid().unwrap(),
1026 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1027 );
1028 }
1029
1030 #[test]
1031 fn test_compute_engine_boots_time_basic() {
1032 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
1033 assert_eq!(boots, 1);
1034 assert_eq!(time, 1000);
1035 }
1036
1037 #[test]
1038 fn test_compute_engine_boots_time_zero_elapsed() {
1039 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
1040 assert_eq!(boots, 1);
1041 assert_eq!(time, 0);
1042 }
1043
1044 #[test]
1045 fn test_builder_engine_boots_default() {
1046 let builder = NotificationReceiverBuilder::new();
1047 assert_eq!(builder.engine_boots, 1);
1048 }
1049
1050 #[test]
1051 fn test_builder_engine_boots_custom() {
1052 let builder = NotificationReceiverBuilder::new().engine_boots(5);
1053 assert_eq!(builder.engine_boots, 5);
1054 }
1055
1056 fn build_v3_notification(
1061 pdu_type: crate::pdu::PduType,
1062 engine_id: &[u8],
1063 engine_boots: u32,
1064 engine_time: u32,
1065 username: &[u8],
1066 auth: Option<(&[u8], AuthProtocol)>,
1067 ) -> Bytes {
1068 build_v3_notification_with_max(
1069 pdu_type,
1070 engine_id,
1071 engine_boots,
1072 engine_time,
1073 username,
1074 auth,
1075 65507,
1076 )
1077 }
1078
1079 fn build_v3_notification_with_max(
1082 pdu_type: crate::pdu::PduType,
1083 engine_id: &[u8],
1084 engine_boots: u32,
1085 engine_time: u32,
1086 username: &[u8],
1087 auth: Option<(&[u8], AuthProtocol)>,
1088 msg_max_size: i32,
1089 ) -> Bytes {
1090 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1091 use crate::pdu::Pdu;
1092 use crate::v3::auth::authenticate_message;
1093 use crate::v3::{LocalizedKey, UsmSecurityParams};
1094 use crate::value::Value;
1095
1096 let auth_key = auth.map(|(password, protocol)| {
1097 LocalizedKey::from_password(protocol, password, engine_id).unwrap()
1098 });
1099
1100 let pdu = Pdu {
1102 pdu_type,
1103 request_id: 1,
1104 error_status: 0,
1105 error_index: 0,
1106 varbinds: vec![
1107 VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
1108 VarBind::new(
1109 oids::snmp_trap_oid(),
1110 Value::ObjectIdentifier(oids::cold_start()),
1111 ),
1112 ],
1113 };
1114
1115 let level = if auth_key.is_some() {
1116 SecurityLevel::AuthNoPriv
1117 } else {
1118 SecurityLevel::NoAuthNoPriv
1119 };
1120 let reportable = pdu_type == crate::pdu::PduType::InformRequest;
1123 let global = MsgGlobalData::new(1, msg_max_size, MsgFlags::new(level, reportable));
1124
1125 let mut usm_params = UsmSecurityParams::new(
1126 Bytes::copy_from_slice(engine_id),
1127 engine_boots,
1128 engine_time,
1129 Bytes::copy_from_slice(username),
1130 );
1131 if let Some(key) = &auth_key {
1132 usm_params = usm_params.with_auth_placeholder(key.mac_len());
1133 }
1134
1135 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
1136 let msg = V3Message::new(global, usm_params.encode(), scoped);
1137 let mut msg_bytes = msg.encode().to_vec();
1138
1139 if let Some(key) = &auth_key {
1141 let (auth_offset, auth_len) =
1142 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1143 authenticate_message(key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1144 }
1145
1146 Bytes::from(msg_bytes)
1147 }
1148
1149 fn build_authed_v3_inform(
1152 engine_id: &[u8],
1153 engine_boots: u32,
1154 engine_time: u32,
1155 username: &[u8],
1156 auth_password: &[u8],
1157 auth_protocol: AuthProtocol,
1158 ) -> Bytes {
1159 build_v3_notification(
1160 crate::pdu::PduType::InformRequest,
1161 engine_id,
1162 engine_boots,
1163 engine_time,
1164 username,
1165 Some((auth_password, auth_protocol)),
1166 )
1167 }
1168
1169 fn build_authed_v3_trap(engine_id: &[u8], engine_boots: u32, engine_time: u32) -> Bytes {
1172 build_v3_notification(
1173 crate::pdu::PduType::TrapV2,
1174 engine_id,
1175 engine_boots,
1176 engine_time,
1177 b"trapuser",
1178 Some((b"authpass12345678", AuthProtocol::Sha1)),
1179 )
1180 }
1181
1182 fn build_noauth_v3_trap(engine_id: &[u8], username: &[u8]) -> Bytes {
1184 build_v3_notification(crate::pdu::PduType::TrapV2, engine_id, 0, 0, username, None)
1185 }
1186
1187 async fn remote_trap_receiver() -> NotificationReceiver {
1190 NotificationReceiver::builder()
1191 .bind("127.0.0.1:0")
1192 .engine_id(b"my-receiver-engine".to_vec())
1193 .engine_boots(1)
1194 .usm_user("trapuser", |u| {
1195 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1196 })
1197 .build()
1198 .await
1199 .unwrap()
1200 }
1201
1202 #[tokio::test]
1210 async fn test_v3_trap_from_remote_sender_engine_accepted() {
1211 let receiver = remote_trap_receiver().await;
1212 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1213
1214 let msg = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1216
1217 let result = receiver.handle_v3(msg, source).await.unwrap();
1218 match result {
1219 Some(Notification::TrapV3 {
1220 username,
1221 security_level,
1222 ..
1223 }) => {
1224 assert_eq!(username.as_ref(), b"trapuser");
1225 assert_eq!(security_level, SecurityLevel::AuthNoPriv);
1226 }
1227 other => panic!("expected TrapV3, got {other:?}"),
1228 }
1229 }
1230
1231 #[tokio::test]
1235 async fn test_v3_noauth_trap_carries_security_level() {
1236 let receiver = remote_trap_receiver().await;
1237 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1238
1239 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"trapuser");
1240 match receiver.handle_v3(msg, source).await.unwrap() {
1241 Some(Notification::TrapV3 {
1242 security_level,
1243 username,
1244 ..
1245 }) => {
1246 assert_eq!(security_level, SecurityLevel::NoAuthNoPriv);
1247 assert_eq!(username.as_ref(), b"trapuser");
1248 }
1249 other => panic!("expected TrapV3, got {other:?}"),
1250 }
1251 }
1252
1253 #[tokio::test]
1258 async fn test_v3_noauth_trap_unknown_user_rejected_and_counted() {
1259 let receiver = remote_trap_receiver().await;
1260 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1261
1262 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"nosuchuser");
1263 let result = receiver.handle_v3(msg, source).await.unwrap();
1264 assert!(result.is_none(), "unknown user must not be delivered");
1265 assert_eq!(receiver.usm_unknown_usernames(), 1);
1266 }
1267
1268 #[tokio::test]
1271 async fn test_v3_traps_from_multiple_remote_engines_accepted() {
1272 let receiver = remote_trap_receiver().await;
1273 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1274
1275 let msg_a = build_authed_v3_trap(b"sender-engine-a", 7, 123_456);
1276 let msg_b = build_authed_v3_trap(b"sender-engine-b", 2, 42);
1277
1278 assert!(
1279 receiver.handle_v3(msg_a, source).await.unwrap().is_some(),
1280 "trap from first remote engine should be accepted"
1281 );
1282 assert!(
1283 receiver.handle_v3(msg_b, source).await.unwrap().is_some(),
1284 "trap from second remote engine should be accepted"
1285 );
1286 }
1287
1288 #[tokio::test]
1293 async fn test_v3_remote_engines_table_bounded() {
1294 let receiver = remote_trap_receiver().await;
1295 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1296
1297 {
1299 let mut engines = receiver.inner.remote_engines.lock().unwrap();
1300 for i in 0..MAX_REMOTE_ENGINES {
1301 let id = Bytes::from(format!("dummy-engine-{i}"));
1302 engines.insert(id.clone(), EngineState::new(id, 1, 1));
1303 }
1304 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1305 }
1306
1307 let msg = build_authed_v3_trap(b"fresh-remote-engine", 7, 123_456);
1309 assert!(receiver.handle_v3(msg, source).await.unwrap().is_some());
1310
1311 let engines = receiver.inner.remote_engines.lock().unwrap();
1314 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1315 assert!(engines.contains_key(&Bytes::from_static(b"fresh-remote-engine")));
1316 }
1317
1318 #[tokio::test]
1323 async fn test_v3_trap_remote_engine_stale_time_rejected() {
1324 let receiver = remote_trap_receiver().await;
1325 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1326
1327 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1328 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1329
1330 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1332 assert!(
1333 receiver.handle_v3(stale, source).await.is_err(),
1334 "stale engine time should be rejected as outside the time window"
1335 );
1336 }
1337
1338 #[tokio::test]
1340 async fn test_v3_trap_remote_engine_old_boots_rejected() {
1341 let receiver = remote_trap_receiver().await;
1342 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1343
1344 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1345 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1346
1347 let old_boots = build_authed_v3_trap(b"remote-sender-engine", 6, 99_999);
1348 assert!(
1349 receiver.handle_v3(old_boots, source).await.is_err(),
1350 "older boot cycle should be rejected"
1351 );
1352 }
1353
1354 #[tokio::test]
1357 async fn test_v3_trap_remote_engine_reboot_accepted() {
1358 let receiver = remote_trap_receiver().await;
1359 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1360
1361 let before = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1362 assert!(receiver.handle_v3(before, source).await.unwrap().is_some());
1363
1364 let after_reboot = build_authed_v3_trap(b"remote-sender-engine", 8, 5);
1365 assert!(
1366 receiver
1367 .handle_v3(after_reboot, source)
1368 .await
1369 .unwrap()
1370 .is_some(),
1371 "trap after sender reboot should be accepted"
1372 );
1373
1374 let from_old_cycle = build_authed_v3_trap(b"remote-sender-engine", 7, 20_000);
1375 assert!(
1376 receiver.handle_v3(from_old_cycle, source).await.is_err(),
1377 "trap from superseded boot cycle should be rejected"
1378 );
1379 }
1380
1381 #[tokio::test]
1384 async fn test_v3_trap_remote_engine_bad_auth_rejected() {
1385 let receiver = remote_trap_receiver().await;
1386 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1387
1388 let msg = build_v3_notification(
1389 crate::pdu::PduType::TrapV2,
1390 b"remote-sender-engine",
1391 7,
1392 123_456,
1393 b"trapuser",
1394 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1395 );
1396 assert!(
1397 receiver.handle_v3(msg, source).await.is_err(),
1398 "trap with wrong auth key should be rejected"
1399 );
1400
1401 let good = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1403 assert!(receiver.handle_v3(good, source).await.unwrap().is_some());
1404 }
1405
1406 #[tokio::test]
1407 async fn test_v3_inform_outside_time_window_rejected() {
1408 let receiver = NotificationReceiver::builder()
1409 .bind("127.0.0.1:0")
1410 .engine_id(b"test-engine".to_vec())
1411 .engine_boots(1)
1412 .usm_user("informuser", |u| {
1413 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1414 })
1415 .build()
1416 .await
1417 .unwrap();
1418
1419 let engine_id = b"test-engine";
1420 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1421
1422 let msg = build_authed_v3_inform(
1424 engine_id,
1425 1, 5000, b"informuser",
1428 b"authpass12345678",
1429 AuthProtocol::Sha1,
1430 );
1431
1432 let result = receiver.handle_v3(msg, source).await;
1433 assert!(
1434 result.is_err(),
1435 "message with engine_time=5000 should be rejected (outside 150s window)"
1436 );
1437 }
1438
1439 #[tokio::test]
1440 async fn test_v3_inform_wrong_boots_rejected() {
1441 let receiver = NotificationReceiver::builder()
1442 .bind("127.0.0.1:0")
1443 .engine_id(b"test-engine".to_vec())
1444 .engine_boots(1)
1445 .usm_user("informuser", |u| {
1446 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1447 })
1448 .build()
1449 .await
1450 .unwrap();
1451
1452 let engine_id = b"test-engine";
1453 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1454
1455 let msg = build_authed_v3_inform(
1457 engine_id,
1458 2, 0, b"informuser",
1461 b"authpass12345678",
1462 AuthProtocol::Sha1,
1463 );
1464
1465 let result = receiver.handle_v3(msg, source).await;
1466 assert!(
1467 result.is_err(),
1468 "message with wrong engine_boots should be rejected"
1469 );
1470 }
1471
1472 #[tokio::test]
1473 async fn test_v3_inform_within_time_window_accepted() {
1474 let receiver = NotificationReceiver::builder()
1475 .bind("127.0.0.1:0")
1476 .engine_id(b"test-engine".to_vec())
1477 .engine_boots(1)
1478 .usm_user("informuser", |u| {
1479 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1480 })
1481 .build()
1482 .await
1483 .unwrap();
1484
1485 let engine_id = b"test-engine";
1486 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1487
1488 let msg = build_authed_v3_inform(
1490 engine_id,
1491 1, 0, b"informuser",
1494 b"authpass12345678",
1495 AuthProtocol::Sha1,
1496 );
1497
1498 let result = receiver.handle_v3(msg, source).await;
1499 match result {
1505 Ok(Some(_)) => {} Err(e) => {
1507 let err_str = format!("{e}");
1508 assert!(
1509 !err_str.contains("Auth"),
1510 "should not be an auth error for valid time window, got: {err_str}"
1511 );
1512 }
1513 Ok(None) => panic!("should not return None for a valid InformRequest"),
1514 }
1515 }
1516
1517 fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
1519 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1520 use crate::pdu::{Pdu, PduType};
1521 use crate::v3::UsmSecurityParams;
1522
1523 let pdu = Pdu {
1524 pdu_type: PduType::GetRequest,
1525 request_id: 0,
1526 error_status: 0,
1527 error_index: 0,
1528 varbinds: vec![],
1529 };
1530
1531 let global = MsgGlobalData::new(
1532 msg_id,
1533 65507,
1534 MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
1535 );
1536
1537 let usm_params = UsmSecurityParams::new(
1538 Bytes::new(), 0,
1540 0,
1541 Bytes::new(), );
1543
1544 let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
1545 let msg = V3Message::new(global, usm_params.encode(), scoped);
1546 msg.encode()
1547 }
1548
1549 #[tokio::test]
1550 async fn test_v3_discovery_gets_response() {
1551 use crate::message::V3Message;
1552 use crate::v3::UsmSecurityParams;
1553 use crate::value::Value;
1554
1555 let receiver = NotificationReceiver::builder()
1556 .bind("127.0.0.1:0")
1557 .engine_id(b"test-discovery-engine".to_vec())
1558 .build()
1559 .await
1560 .unwrap();
1561
1562 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1565 let client_addr = client.local_addr().unwrap();
1566
1567 let discovery_msg = build_v3_discovery_request(42, true);
1568 let result = receiver.handle_v3(discovery_msg, client_addr).await;
1569
1570 assert!(result.is_ok());
1572 assert!(result.unwrap().is_none());
1573
1574 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1576
1577 let mut buf = vec![0u8; 4096];
1580 let (len, _) = tokio::time::timeout(
1581 std::time::Duration::from_secs(1),
1582 client.recv_from(&mut buf),
1583 )
1584 .await
1585 .expect("expected a discovery Report")
1586 .unwrap();
1587
1588 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1589 assert_eq!(
1590 report.global_data.msg_flags.security_level,
1591 SecurityLevel::NoAuthNoPriv
1592 );
1593 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1594 assert_eq!(report_usm.engine_id.as_ref(), b"test-discovery-engine");
1595 let scoped = report.scoped_pdu().expect("report should be plaintext");
1596 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1597 assert_eq!(
1598 scoped.pdu.varbinds[0].oid,
1599 crate::v3::report_oids::unknown_engine_ids()
1600 );
1601 assert_eq!(scoped.pdu.varbinds[0].value, Value::Counter32(1));
1602 }
1603
1604 #[tokio::test]
1605 async fn test_v3_discovery_non_reportable_ignored() {
1606 let receiver = NotificationReceiver::builder()
1607 .bind("127.0.0.1:0")
1608 .engine_id(b"test-discovery-engine".to_vec())
1609 .build()
1610 .await
1611 .unwrap();
1612
1613 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1614 let discovery_msg = build_v3_discovery_request(42, false);
1615
1616 let result = receiver.handle_v3(discovery_msg, source).await;
1617
1618 assert!(result.is_ok());
1622 assert!(result.unwrap().is_none());
1623 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1624 }
1625
1626 #[tokio::test]
1632 async fn test_v3_inform_under_remote_engine_id_rejected() {
1633 let receiver = NotificationReceiver::builder()
1634 .bind("127.0.0.1:0")
1635 .engine_id(b"my-receiver-engine".to_vec())
1636 .engine_boots(1)
1637 .usm_user("informuser", |u| {
1638 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1639 })
1640 .build()
1641 .await
1642 .unwrap();
1643
1644 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1645
1646 let msg = build_authed_v3_inform(
1648 b"remote-engine-id",
1649 1,
1650 0,
1651 b"informuser",
1652 b"authpass12345678",
1653 AuthProtocol::Sha1,
1654 );
1655
1656 let result = receiver.handle_v3(msg, source).await.unwrap();
1657 assert!(
1658 result.is_none(),
1659 "inform under a foreign authoritative engine ID should be dropped, got {result:?}"
1660 );
1661 }
1662
1663 #[tokio::test]
1667 async fn test_v3_inform_under_local_engine_id_accepted() {
1668 let receiver = NotificationReceiver::builder()
1669 .bind("127.0.0.1:0")
1670 .engine_id(b"my-receiver-engine".to_vec())
1671 .engine_boots(1)
1672 .usm_user("informuser", |u| {
1673 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1674 })
1675 .build()
1676 .await
1677 .unwrap();
1678
1679 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1680
1681 let msg = build_authed_v3_inform(
1683 b"my-receiver-engine",
1684 1,
1685 0,
1686 b"informuser",
1687 b"authpass12345678",
1688 AuthProtocol::Sha1,
1689 );
1690
1691 let result = receiver.handle_v3(msg, source).await.unwrap();
1692 assert!(
1693 matches!(result, Some(Notification::InformV3 { .. })),
1694 "inform under the local engine ID should be accepted, got {result:?}"
1695 );
1696 }
1697
1698 #[tokio::test]
1701 async fn test_v3_inform_ack_advertises_local_max_size() {
1702 let receiver = NotificationReceiver::builder()
1703 .bind("127.0.0.1:0")
1704 .engine_id(b"my-receiver-engine".to_vec())
1705 .engine_boots(1)
1706 .usm_user("informuser", |u| {
1707 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1708 })
1709 .build()
1710 .await
1711 .unwrap();
1712
1713 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1714 let client_addr = client.local_addr().unwrap();
1715
1716 let msg = build_v3_notification_with_max(
1720 crate::pdu::PduType::InformRequest,
1721 b"my-receiver-engine",
1722 1,
1723 0,
1724 b"informuser",
1725 Some((b"authpass12345678", AuthProtocol::Sha1)),
1726 1400,
1727 );
1728
1729 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
1730 assert!(
1731 matches!(result, Some(Notification::InformV3 { .. })),
1732 "inform should be accepted, got {result:?}"
1733 );
1734
1735 let mut buf = vec![0u8; 4096];
1736 let (len, _) = tokio::time::timeout(
1737 std::time::Duration::from_secs(1),
1738 client.recv_from(&mut buf),
1739 )
1740 .await
1741 .expect("expected the inform acknowledgement")
1742 .unwrap();
1743
1744 use crate::message::V3Message;
1745 let ack = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1746 assert_eq!(
1747 ack.global_data.msg_max_size,
1748 crate::v3::DEFAULT_MSG_MAX_SIZE as i32,
1749 "ack must advertise the receiver's local receive capacity, not the sender's 1400"
1750 );
1751 }
1752
1753 #[test]
1754 fn test_auto_generated_engine_id_non_empty() {
1755 let builder = NotificationReceiverBuilder::new();
1756 assert!(builder.engine_id.is_none());
1758 }
1759
1760 #[tokio::test]
1761 async fn test_bind_generates_engine_id() {
1762 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1763 assert!(!receiver.engine_id().is_empty());
1764 assert_eq!(receiver.engine_id()[0], 0x80);
1766 }
1767
1768 #[tokio::test]
1769 async fn test_builder_generates_engine_id() {
1770 let receiver = NotificationReceiver::builder()
1771 .bind("127.0.0.1:0")
1772 .build()
1773 .await
1774 .unwrap();
1775 assert!(!receiver.engine_id().is_empty());
1776 assert_eq!(receiver.engine_id()[0], 0x80);
1777 }
1778
1779 #[tokio::test]
1780 async fn test_builder_custom_engine_id() {
1781 let receiver = NotificationReceiver::builder()
1782 .bind("127.0.0.1:0")
1783 .engine_id(b"custom-engine".to_vec())
1784 .build()
1785 .await
1786 .unwrap();
1787 assert_eq!(receiver.engine_id(), b"custom-engine");
1788 }
1789
1790 #[tokio::test]
1791 async fn test_usm_counter_accessors_default_zero() {
1792 let receiver = remote_trap_receiver().await;
1793 assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1794 assert_eq!(receiver.usm_unknown_usernames(), 0);
1795 assert_eq!(receiver.usm_wrong_digests(), 0);
1796 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1797 assert_eq!(receiver.usm_unsupported_sec_levels(), 0);
1798 assert_eq!(receiver.usm_decryption_errors(), 0);
1799 }
1800
1801 #[tokio::test]
1804 async fn test_v3_trap_wrong_digest_increments_counter() {
1805 let receiver = remote_trap_receiver().await;
1806 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1807
1808 let msg = build_v3_notification(
1809 crate::pdu::PduType::TrapV2,
1810 b"remote-sender-engine",
1811 7,
1812 123_456,
1813 b"trapuser",
1814 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1815 );
1816 assert!(receiver.handle_v3(msg, source).await.is_err());
1817 assert_eq!(receiver.usm_wrong_digests(), 1);
1818 }
1819
1820 #[tokio::test]
1823 async fn test_v3_trap_unknown_user_increments_counter() {
1824 let receiver = remote_trap_receiver().await;
1825 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1826
1827 let msg = build_v3_notification(
1828 crate::pdu::PduType::TrapV2,
1829 b"remote-sender-engine",
1830 7,
1831 123_456,
1832 b"nosuchuser",
1833 Some((b"authpass12345678", AuthProtocol::Sha1)),
1834 );
1835 let result = receiver.handle_v3(msg, source).await.unwrap();
1836 assert!(result.is_none(), "unknown user must not be delivered");
1837 assert_eq!(receiver.usm_unknown_usernames(), 1);
1838 assert_eq!(receiver.usm_wrong_digests(), 0);
1839 }
1840
1841 #[tokio::test]
1845 async fn test_v3_trap_user_without_auth_key_increments_counter() {
1846 let receiver = NotificationReceiver::builder()
1847 .bind("127.0.0.1:0")
1848 .engine_id(b"my-receiver-engine".to_vec())
1849 .usm_user("plainuser", |u| u)
1850 .build()
1851 .await
1852 .unwrap();
1853 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1854
1855 let msg = build_v3_notification(
1856 crate::pdu::PduType::TrapV2,
1857 b"remote-sender-engine",
1858 7,
1859 123_456,
1860 b"plainuser",
1861 Some((b"authpass12345678", AuthProtocol::Sha1)),
1862 );
1863 let result = receiver.handle_v3(msg, source).await.unwrap();
1864 assert!(result.is_none());
1865 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
1866 assert_eq!(receiver.usm_unknown_usernames(), 0);
1867 }
1868
1869 #[tokio::test]
1872 async fn test_v3_inform_time_window_failure_increments_counter() {
1873 let receiver = NotificationReceiver::builder()
1874 .bind("127.0.0.1:0")
1875 .engine_id(b"test-engine".to_vec())
1876 .engine_boots(1)
1877 .usm_user("informuser", |u| {
1878 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1879 })
1880 .build()
1881 .await
1882 .unwrap();
1883 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1884
1885 let msg = build_authed_v3_inform(
1886 b"test-engine",
1887 1,
1888 5000,
1889 b"informuser",
1890 b"authpass12345678",
1891 AuthProtocol::Sha1,
1892 );
1893 assert!(receiver.handle_v3(msg, source).await.is_err());
1894 assert_eq!(receiver.usm_not_in_time_windows(), 1);
1895 }
1896
1897 #[tokio::test]
1903 async fn test_v3_trap_remote_stale_not_counted() {
1904 let receiver = remote_trap_receiver().await;
1905 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1906
1907 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1908 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1909
1910 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1911 assert!(receiver.handle_v3(stale, source).await.is_err());
1912 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1913 }
1914
1915 #[tokio::test]
1923 async fn test_v3_inform_remote_stale_gets_no_report() {
1924 let receiver = remote_trap_receiver().await;
1925
1926 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1927 let client_addr = client.local_addr().unwrap();
1928
1929 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1933 assert!(
1934 receiver
1935 .handle_v3(fresh, client_addr)
1936 .await
1937 .unwrap()
1938 .is_some()
1939 );
1940
1941 let mut buf = vec![0u8; 4096];
1942
1943 let stale = build_v3_notification(
1944 crate::pdu::PduType::InformRequest,
1945 b"remote-sender-engine",
1946 7,
1947 5_000,
1948 b"trapuser",
1949 Some((b"authpass12345678", AuthProtocol::Sha1)),
1950 );
1951 assert!(receiver.handle_v3(stale, client_addr).await.is_err());
1952 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1953
1954 let result = tokio::time::timeout(
1955 std::time::Duration::from_millis(200),
1956 client.recv_from(&mut buf),
1957 )
1958 .await;
1959 assert!(
1960 result.is_err(),
1961 "no Report may be sent for a Step 7b timeliness failure"
1962 );
1963 }
1964
1965 fn build_v3_trap_bad_ciphertext(
1969 engine_id: &[u8],
1970 username: &[u8],
1971 auth_password: &[u8],
1972 ) -> Bytes {
1973 use crate::message::{MsgFlags, MsgGlobalData, V3Message};
1974 use crate::v3::auth::authenticate_message;
1975 use crate::v3::{LocalizedKey, UsmSecurityParams};
1976
1977 let auth_key =
1978 LocalizedKey::from_password(AuthProtocol::Sha1, auth_password, engine_id).unwrap();
1979
1980 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthPriv, false));
1981 let usm_params = UsmSecurityParams::new(
1982 Bytes::copy_from_slice(engine_id),
1983 7,
1984 123_456,
1985 Bytes::copy_from_slice(username),
1986 )
1987 .with_auth_placeholder(auth_key.mac_len())
1988 .with_priv_params(Bytes::from_static(b"bad"));
1989
1990 let msg = V3Message::new_encrypted(
1991 global,
1992 usm_params.encode(),
1993 Bytes::from_static(b"not-a-valid-ciphertext"),
1994 );
1995 let mut msg_bytes = msg.encode().to_vec();
1996 let (auth_offset, auth_len) =
1997 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1998 authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1999 Bytes::from(msg_bytes)
2000 }
2001
2002 #[tokio::test]
2005 async fn test_v3_decryption_error_increments_counter() {
2006 let receiver = NotificationReceiver::builder()
2007 .bind("127.0.0.1:0")
2008 .engine_id(b"my-receiver-engine".to_vec())
2009 .usm_user("privuser", |u| {
2010 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2011 .privacy(crate::v3::PrivProtocol::Aes128, b"privpass12345678")
2012 })
2013 .build()
2014 .await
2015 .unwrap();
2016 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2017
2018 let msg =
2019 build_v3_trap_bad_ciphertext(b"remote-sender-engine", b"privuser", b"authpass12345678");
2020 assert!(receiver.handle_v3(msg, source).await.is_err());
2021 assert_eq!(receiver.usm_decryption_errors(), 1);
2022 }
2023
2024 #[tokio::test]
2029 async fn test_v3_authpriv_for_auth_only_user_counts_unsupported_sec_level() {
2030 let receiver = remote_trap_receiver().await;
2031 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2032
2033 let msg = build_v3_trap_bad_ciphertext(
2034 b"remote-sender-engine",
2035 b"trapuser",
2036 b"wrong-password-1234",
2037 );
2038 let result = receiver.handle_v3(msg, source).await.unwrap();
2039 assert!(result.is_none());
2040 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
2041 assert_eq!(receiver.usm_wrong_digests(), 0);
2042 }
2043
2044 #[tokio::test]
2050 async fn test_v3_failed_inform_gets_authenticated_time_window_report() {
2051 use crate::message::V3Message;
2052 use crate::v3::auth::verify_message;
2053 use crate::v3::{LocalizedKey, UsmSecurityParams};
2054
2055 let receiver = NotificationReceiver::builder()
2056 .bind("127.0.0.1:0")
2057 .engine_id(b"test-engine".to_vec())
2058 .engine_boots(1)
2059 .usm_user("informuser", |u| {
2060 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2061 })
2062 .build()
2063 .await
2064 .unwrap();
2065
2066 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2067 let client_addr = client.local_addr().unwrap();
2068
2069 let msg = build_authed_v3_inform(
2070 b"test-engine",
2071 1,
2072 5000, b"informuser",
2074 b"authpass12345678",
2075 AuthProtocol::Sha1,
2076 );
2077 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2078
2079 let mut buf = vec![0u8; 4096];
2080 let (len, _) = tokio::time::timeout(
2081 std::time::Duration::from_secs(1),
2082 client.recv_from(&mut buf),
2083 )
2084 .await
2085 .expect("expected a Report in response to the failed inform")
2086 .unwrap();
2087 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2088
2089 let report = V3Message::decode(report_bytes.clone()).unwrap();
2090 assert_eq!(
2091 report.global_data.msg_flags.security_level,
2092 SecurityLevel::AuthNoPriv,
2093 "notInTimeWindows report must be authenticated (authNoPriv)"
2094 );
2095 assert!(!report.global_data.msg_flags.reportable);
2096
2097 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2098 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2099
2100 let key =
2103 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2104 .unwrap();
2105 let (auth_offset, auth_len) =
2106 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2107 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2108
2109 let scoped = report.scoped_pdu().expect("report should be plaintext");
2110 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2111 assert_eq!(
2112 scoped.pdu.varbinds[0].oid,
2113 crate::v3::report_oids::not_in_time_windows()
2114 );
2115 }
2116
2117 #[tokio::test]
2121 async fn test_v3_latched_boots_report_is_authenticated() {
2122 use crate::message::V3Message;
2123 use crate::v3::MAX_ENGINE_TIME;
2124 use crate::v3::auth::verify_message;
2125 use crate::v3::{LocalizedKey, UsmSecurityParams};
2126
2127 let receiver = NotificationReceiver::builder()
2128 .bind("127.0.0.1:0")
2129 .engine_id(b"test-engine".to_vec())
2130 .engine_boots(MAX_ENGINE_TIME)
2131 .usm_user("informuser", |u| {
2132 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2133 })
2134 .build()
2135 .await
2136 .unwrap();
2137
2138 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2139 let client_addr = client.local_addr().unwrap();
2140
2141 let msg = build_authed_v3_inform(
2142 b"test-engine",
2143 MAX_ENGINE_TIME,
2144 0,
2145 b"informuser",
2146 b"authpass12345678",
2147 AuthProtocol::Sha1,
2148 );
2149 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2150 assert_eq!(receiver.usm_not_in_time_windows(), 1);
2151
2152 let mut buf = vec![0u8; 4096];
2153 let (len, _) = tokio::time::timeout(
2154 std::time::Duration::from_secs(1),
2155 client.recv_from(&mut buf),
2156 )
2157 .await
2158 .expect("expected a Report in response to the failed inform")
2159 .unwrap();
2160 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2161
2162 let report = V3Message::decode(report_bytes.clone()).unwrap();
2163 assert_eq!(
2164 report.global_data.msg_flags.security_level,
2165 SecurityLevel::AuthNoPriv,
2166 "notInTimeWindows report must be authenticated (authNoPriv)"
2167 );
2168 let key =
2169 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2170 .unwrap();
2171 let (auth_offset, auth_len) =
2172 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2173 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2174
2175 let scoped = report.scoped_pdu().expect("report should be plaintext");
2176 assert_eq!(
2177 scoped.pdu.varbinds[0].oid,
2178 crate::v3::report_oids::not_in_time_windows()
2179 );
2180 }
2181
2182 #[tokio::test]
2185 async fn test_v3_failed_inform_unknown_user_gets_noauth_report() {
2186 use crate::message::V3Message;
2187 use crate::v3::UsmSecurityParams;
2188
2189 let receiver = NotificationReceiver::builder()
2190 .bind("127.0.0.1:0")
2191 .engine_id(b"test-engine".to_vec())
2192 .engine_boots(1)
2193 .usm_user("informuser", |u| {
2194 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2195 })
2196 .build()
2197 .await
2198 .unwrap();
2199
2200 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2201 let client_addr = client.local_addr().unwrap();
2202
2203 let msg = build_authed_v3_inform(
2204 b"test-engine",
2205 1,
2206 0,
2207 b"nosuchuser",
2208 b"authpass12345678",
2209 AuthProtocol::Sha1,
2210 );
2211 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
2212 assert!(result.is_none());
2213 assert_eq!(receiver.usm_unknown_usernames(), 1);
2214
2215 let mut buf = vec![0u8; 4096];
2216 let (len, _) = tokio::time::timeout(
2217 std::time::Duration::from_secs(1),
2218 client.recv_from(&mut buf),
2219 )
2220 .await
2221 .expect("expected a Report in response to the failed inform")
2222 .unwrap();
2223
2224 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
2225 assert_eq!(
2226 report.global_data.msg_flags.security_level,
2227 SecurityLevel::NoAuthNoPriv
2228 );
2229 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2230 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2231 let scoped = report.scoped_pdu().expect("report should be plaintext");
2232 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2233 assert_eq!(
2234 scoped.pdu.varbinds[0].oid,
2235 crate::v3::report_oids::unknown_user_names()
2236 );
2237 }
2238
2239 #[tokio::test]
2242 async fn test_v3_failed_trap_gets_no_report() {
2243 let receiver = remote_trap_receiver().await;
2244
2245 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2246 let client_addr = client.local_addr().unwrap();
2247
2248 let msg = build_v3_notification(
2249 crate::pdu::PduType::TrapV2,
2250 b"remote-sender-engine",
2251 7,
2252 123_456,
2253 b"trapuser",
2254 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
2255 );
2256 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2257 assert_eq!(receiver.usm_wrong_digests(), 1);
2258
2259 let mut buf = vec![0u8; 4096];
2260 let result = tokio::time::timeout(
2261 std::time::Duration::from_millis(200),
2262 client.recv_from(&mut buf),
2263 )
2264 .await;
2265 assert!(result.is_err(), "no Report may be sent for a failed trap");
2266 }
2267
2268 #[test]
2269 fn test_community_allowed() {
2270 assert!(community_allowed(&[], b"public"));
2272 assert!(community_allowed(&[], b""));
2273
2274 let configured = vec![b"public".to_vec(), b"monitor".to_vec()];
2275 assert!(community_allowed(&configured, b"public"));
2276 assert!(community_allowed(&configured, b"monitor"));
2277 assert!(!community_allowed(&configured, b"private"));
2279 assert!(!community_allowed(&configured, b"pub"));
2280 assert!(!community_allowed(&configured, b"publicx"));
2281 assert!(!community_allowed(&configured, b""));
2282 }
2283
2284 fn build_v2c_trap(community: &[u8]) -> Bytes {
2285 use crate::message::CommunityMessage;
2286 use crate::pdu::Pdu;
2287 let pdu = Pdu::trap_v2(1, 100, &oids::cold_start(), vec![]);
2288 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2289 }
2290
2291 fn build_v2c_inform(community: &[u8]) -> Bytes {
2292 use crate::message::CommunityMessage;
2293 use crate::pdu::Pdu;
2294 let pdu = Pdu::inform_request(1, 100, &oids::cold_start(), vec![]);
2295 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2296 }
2297
2298 fn build_v1_trap(community: &[u8]) -> Bytes {
2299 use crate::message::CommunityMessage;
2300 use crate::pdu::GenericTrap;
2301 let trap = TrapV1Pdu::new(
2302 oid!(1, 3, 6, 1, 4, 1, 9999),
2303 [192, 168, 1, 1],
2304 GenericTrap::ColdStart,
2305 0,
2306 12345,
2307 vec![],
2308 );
2309 CommunityMessage::v1_trap(Bytes::copy_from_slice(community), trap).encode()
2310 }
2311
2312 #[tokio::test]
2313 async fn test_v2c_trap_matching_community_accepted() {
2314 let receiver = NotificationReceiver::builder()
2315 .bind("127.0.0.1:0")
2316 .community(b"public")
2317 .build()
2318 .await
2319 .unwrap();
2320 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2321
2322 let result = receiver
2323 .handle_v2c(build_v2c_trap(b"public"), source)
2324 .await
2325 .unwrap();
2326 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2327 }
2328
2329 #[tokio::test]
2330 async fn test_v2c_trap_wrong_community_dropped() {
2331 let receiver = NotificationReceiver::builder()
2332 .bind("127.0.0.1:0")
2333 .community(b"public")
2334 .build()
2335 .await
2336 .unwrap();
2337 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2338
2339 let result = receiver
2340 .handle_v2c(build_v2c_trap(b"private"), source)
2341 .await
2342 .unwrap();
2343 assert!(result.is_none());
2344 }
2345
2346 #[tokio::test]
2347 async fn test_v2c_trap_no_allowlist_accepts_any_community() {
2348 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
2349 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2350
2351 let result = receiver
2352 .handle_v2c(build_v2c_trap(b"anything"), source)
2353 .await
2354 .unwrap();
2355 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2356 }
2357
2358 #[tokio::test]
2359 async fn test_v1_trap_wrong_community_dropped() {
2360 let receiver = NotificationReceiver::builder()
2361 .bind("127.0.0.1:0")
2362 .community(b"public")
2363 .build()
2364 .await
2365 .unwrap();
2366 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2367
2368 assert!(
2369 receiver
2370 .handle_v1(build_v1_trap(b"private"), source)
2371 .await
2372 .unwrap()
2373 .is_none()
2374 );
2375 assert!(matches!(
2376 receiver
2377 .handle_v1(build_v1_trap(b"public"), source)
2378 .await
2379 .unwrap(),
2380 Some(Notification::TrapV1 { .. })
2381 ));
2382 }
2383
2384 #[tokio::test]
2387 async fn test_v2c_inform_wrong_community_dropped_without_ack() {
2388 let receiver = NotificationReceiver::builder()
2389 .bind("127.0.0.1:0")
2390 .community(b"public")
2391 .build()
2392 .await
2393 .unwrap();
2394
2395 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2396 let client_addr = client.local_addr().unwrap();
2397
2398 let result = receiver
2399 .handle_v2c(build_v2c_inform(b"private"), client_addr)
2400 .await
2401 .unwrap();
2402 assert!(result.is_none());
2403
2404 let mut buf = vec![0u8; 4096];
2405 let recv = tokio::time::timeout(
2406 std::time::Duration::from_millis(200),
2407 client.recv_from(&mut buf),
2408 )
2409 .await;
2410 assert!(recv.is_err(), "a filtered inform must not be acknowledged");
2411 }
2412
2413 #[tokio::test]
2416 async fn test_v2c_inform_matching_community_acked() {
2417 let receiver = NotificationReceiver::builder()
2418 .bind("127.0.0.1:0")
2419 .community(b"public")
2420 .build()
2421 .await
2422 .unwrap();
2423
2424 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2425 let client_addr = client.local_addr().unwrap();
2426
2427 let result = receiver
2428 .handle_v2c(build_v2c_inform(b"public"), client_addr)
2429 .await
2430 .unwrap();
2431 assert!(matches!(result, Some(Notification::InformV2c { .. })));
2432
2433 let mut buf = vec![0u8; 4096];
2434 let (len, _) = tokio::time::timeout(
2435 std::time::Duration::from_secs(1),
2436 client.recv_from(&mut buf),
2437 )
2438 .await
2439 .expect("a matching inform must be acknowledged")
2440 .unwrap();
2441 assert!(len > 0);
2442 }
2443}