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]
355 pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
356 self.engine_id = Some(engine_id.into());
357 self
358 }
359
360 #[must_use]
365 pub fn engine_boots(mut self, boots: u32) -> Self {
366 self.engine_boots = boots;
367 self
368 }
369
370 pub async fn build(self) -> Result<NotificationReceiver> {
372 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
373 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
374 })?;
375
376 let socket = bind_udp_socket(bind_addr, None, None, false)
377 .await
378 .map_err(|e| Error::Network {
379 target: bind_addr,
380 source: e,
381 })?;
382
383 let local_addr = socket.local_addr().map_err(|e| Error::Network {
384 target: bind_addr,
385 source: e,
386 })?;
387
388 let engine_id: Bytes = self.engine_id.map_or_else(
389 || {
390 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
391 let timestamp = std::time::SystemTime::now()
392 .duration_since(std::time::UNIX_EPOCH)
393 .unwrap_or_default()
394 .as_secs();
395 id.extend_from_slice(×tamp.to_be_bytes());
396 Bytes::from(id)
397 },
398 Bytes::from,
399 );
400
401 Ok(NotificationReceiver {
402 inner: Arc::new(ReceiverInner {
403 socket,
404 local_addr,
405 usm_users: self.usm_users,
406 communities: self.communities,
407 engine_id,
408 salt_counter: SaltCounter::new(),
409 engine_boots_base: self.engine_boots,
410 engine_start: Instant::now(),
411 usm_stats: UsmStats::default(),
412 remote_engines: Mutex::new(HashMap::new()),
413 }),
414 })
415 }
416}
417
418impl Default for NotificationReceiverBuilder {
419 fn default() -> Self {
420 Self::new()
421 }
422}
423
424#[derive(Debug, Clone)]
431pub enum Notification {
432 TrapV1 {
434 community: Bytes,
436 trap: TrapV1Pdu,
438 },
439
440 TrapV2c {
442 community: Bytes,
444 uptime: u32,
446 trap_oid: Oid,
448 varbinds: Vec<VarBind>,
450 request_id: i32,
452 },
453
454 TrapV3 {
456 username: Bytes,
458 context_engine_id: Bytes,
460 context_name: Bytes,
462 security_level: SecurityLevel,
466 uptime: u32,
468 trap_oid: Oid,
470 varbinds: Vec<VarBind>,
472 request_id: i32,
474 },
475
476 InformV2c {
480 community: Bytes,
482 uptime: u32,
484 trap_oid: Oid,
486 varbinds: Vec<VarBind>,
488 request_id: i32,
490 },
491
492 InformV3 {
496 username: Bytes,
498 context_engine_id: Bytes,
500 context_name: Bytes,
502 security_level: SecurityLevel,
506 uptime: u32,
508 trap_oid: Oid,
510 varbinds: Vec<VarBind>,
512 request_id: i32,
514 },
515}
516
517impl Notification {
518 pub fn trap_oid(&self) -> Result<Oid> {
523 match self {
524 Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
525 Notification::TrapV2c { trap_oid, .. }
526 | Notification::TrapV3 { trap_oid, .. }
527 | Notification::InformV2c { trap_oid, .. }
528 | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
529 }
530 }
531
532 pub fn uptime(&self) -> u32 {
534 match self {
535 Notification::TrapV1 { trap, .. } => trap.time_stamp,
536 Notification::TrapV2c { uptime, .. }
537 | Notification::TrapV3 { uptime, .. }
538 | Notification::InformV2c { uptime, .. }
539 | Notification::InformV3 { uptime, .. } => *uptime,
540 }
541 }
542
543 pub fn varbinds(&self) -> &[VarBind] {
545 match self {
546 Notification::TrapV1 { trap, .. } => &trap.varbinds,
547 Notification::TrapV2c { varbinds, .. }
548 | Notification::TrapV3 { varbinds, .. }
549 | Notification::InformV2c { varbinds, .. }
550 | Notification::InformV3 { varbinds, .. } => varbinds,
551 }
552 }
553
554 pub fn security_level(&self) -> Option<SecurityLevel> {
561 match self {
562 Notification::TrapV1 { .. }
563 | Notification::TrapV2c { .. }
564 | Notification::InformV2c { .. } => None,
565 Notification::TrapV3 { security_level, .. }
566 | Notification::InformV3 { security_level, .. } => Some(*security_level),
567 }
568 }
569
570 pub fn is_confirmed(&self) -> bool {
572 matches!(
573 self,
574 Notification::InformV2c { .. } | Notification::InformV3 { .. }
575 )
576 }
577
578 pub fn version(&self) -> Version {
580 match self {
581 Notification::TrapV1 { .. } => Version::V1,
582 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
583 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
584 }
585 }
586}
587
588pub struct NotificationReceiver {
614 inner: Arc<ReceiverInner>,
615}
616
617struct ReceiverInner {
618 socket: UdpSocket,
619 local_addr: SocketAddr,
620 usm_users: HashMap<Bytes, UsmConfig>,
622 communities: Vec<Vec<u8>>,
626 engine_id: Bytes,
628 salt_counter: SaltCounter,
630 engine_boots_base: u32,
632 engine_start: Instant,
634 usm_stats: UsmStats,
636 remote_engines: Mutex<HashMap<Bytes, EngineState>>,
643}
644
645impl NotificationReceiver {
646 #[must_use]
650 pub fn builder() -> NotificationReceiverBuilder {
651 NotificationReceiverBuilder::new()
652 }
653
654 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
680 let addr_str = addr.as_ref();
681 let bind_addr: SocketAddr = addr_str
682 .parse()
683 .map_err(|_| Error::Config(format!("invalid bind address: {addr_str}").into()))?;
684
685 let socket = bind_udp_socket(bind_addr, None, None, false)
686 .await
687 .map_err(|e| Error::Network {
688 target: bind_addr,
689 source: e,
690 })?;
691
692 let local_addr = socket.local_addr().map_err(|e| Error::Network {
693 target: bind_addr,
694 source: e,
695 })?;
696
697 let engine_id: Bytes = {
698 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
699 let timestamp = std::time::SystemTime::now()
700 .duration_since(std::time::UNIX_EPOCH)
701 .unwrap_or_default()
702 .as_secs();
703 id.extend_from_slice(×tamp.to_be_bytes());
704 Bytes::from(id)
705 };
706
707 Ok(Self {
708 inner: Arc::new(ReceiverInner {
709 socket,
710 local_addr,
711 usm_users: HashMap::new(),
712 communities: Vec::new(),
713 engine_id,
714 salt_counter: SaltCounter::new(),
715 engine_boots_base: 1,
716 engine_start: Instant::now(),
717 usm_stats: UsmStats::default(),
718 remote_engines: Mutex::new(HashMap::new()),
719 }),
720 })
721 }
722
723 #[must_use]
725 pub fn local_addr(&self) -> SocketAddr {
726 self.inner.local_addr
727 }
728
729 #[must_use]
731 pub fn engine_id(&self) -> &[u8] {
732 &self.inner.engine_id
733 }
734
735 #[must_use]
737 pub fn usm_unknown_engine_ids(&self) -> u32 {
738 self.inner
739 .usm_stats
740 .unknown_engine_ids
741 .load(Ordering::Relaxed)
742 }
743
744 #[must_use]
746 pub fn usm_unknown_usernames(&self) -> u32 {
747 self.inner
748 .usm_stats
749 .unknown_usernames
750 .load(Ordering::Relaxed)
751 }
752
753 #[must_use]
755 pub fn usm_wrong_digests(&self) -> u32 {
756 self.inner.usm_stats.wrong_digests.load(Ordering::Relaxed)
757 }
758
759 #[must_use]
761 pub fn usm_not_in_time_windows(&self) -> u32 {
762 self.inner
763 .usm_stats
764 .not_in_time_windows
765 .load(Ordering::Relaxed)
766 }
767
768 #[must_use]
770 pub fn usm_unsupported_sec_levels(&self) -> u32 {
771 self.inner
772 .usm_stats
773 .unsupported_sec_levels
774 .load(Ordering::Relaxed)
775 }
776
777 #[must_use]
779 pub fn usm_decryption_errors(&self) -> u32 {
780 self.inner
781 .usm_stats
782 .decryption_errors
783 .load(Ordering::Relaxed)
784 }
785
786 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
793 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
794 let mut buf = vec![0u8; 65535];
795
796 loop {
797 let (len, source) =
798 self.inner
799 .socket
800 .recv_from(&mut buf)
801 .await
802 .map_err(|e| Error::Network {
803 target: self.inner.local_addr,
804 source: e,
805 })?;
806
807 let data = Bytes::copy_from_slice(&buf[..len]);
808
809 match self.parse_and_respond(data, source).await {
810 Ok(Some(notification)) => return Ok((notification, source)),
811 Ok(None) => {} Err(e) => {
813 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
815 }
816 }
817 }
818 }
819
820 async fn parse_and_respond(
824 &self,
825 data: Bytes,
826 source: SocketAddr,
827 ) -> Result<Option<Notification>> {
828 match crate::message::peek_version(data.clone(), source)? {
829 Version::V1 => self.handle_v1(data, source).await,
830 Version::V2c => self.handle_v2c(data, source).await,
831 Version::V3 => self.handle_v3(data, source).await,
832 }
833 }
834}
835
836impl Clone for NotificationReceiver {
837 fn clone(&self) -> Self {
838 Self {
839 inner: Arc::clone(&self.inner),
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use crate::message::SecurityLevel;
848 use crate::oid;
849 use crate::pdu::GenericTrap;
850 use crate::v3::AuthProtocol;
851
852 #[test]
853 fn test_notification_trap_v1() {
854 let trap = TrapV1Pdu::new(
855 oid!(1, 3, 6, 1, 4, 1, 9999),
856 [192, 168, 1, 1],
857 GenericTrap::LinkDown,
858 0,
859 12345,
860 vec![],
861 );
862
863 let notification = Notification::TrapV1 {
864 community: Bytes::from_static(b"public"),
865 trap,
866 };
867
868 assert!(!notification.is_confirmed());
869 assert_eq!(notification.version(), Version::V1);
870 assert_eq!(notification.uptime(), 12345);
871 assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
872 }
873
874 #[test]
875 fn test_notification_trap_v2c() {
876 let notification = Notification::TrapV2c {
877 community: Bytes::from_static(b"public"),
878 uptime: 54321,
879 trap_oid: oids::link_up(),
880 varbinds: vec![],
881 request_id: 1,
882 };
883
884 assert!(!notification.is_confirmed());
885 assert_eq!(notification.version(), Version::V2c);
886 assert_eq!(notification.uptime(), 54321);
887 assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
888 }
889
890 #[test]
891 fn test_notification_inform() {
892 let notification = Notification::InformV2c {
893 community: Bytes::from_static(b"public"),
894 uptime: 11111,
895 trap_oid: oids::cold_start(),
896 varbinds: vec![],
897 request_id: 42,
898 };
899
900 assert!(notification.is_confirmed());
901 assert_eq!(notification.version(), Version::V2c);
902 }
903
904 #[test]
905 fn test_notification_receiver_builder_default() {
906 let builder = NotificationReceiverBuilder::new();
907 assert_eq!(builder.bind_addr, "0.0.0.0:162");
908 assert!(builder.usm_users.is_empty());
909 }
910
911 #[test]
912 fn test_notification_receiver_builder_with_user() {
913 let builder = NotificationReceiverBuilder::new()
914 .bind("0.0.0.0:1162")
915 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
916
917 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
918 assert_eq!(builder.usm_users.len(), 1);
919
920 let user = builder
921 .usm_users
922 .get(&Bytes::from_static(b"trapuser"))
923 .unwrap();
924 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
925 }
926
927 #[test]
928 fn test_notification_v3_inform() {
929 let notification = Notification::InformV3 {
930 username: Bytes::from_static(b"testuser"),
931 context_engine_id: Bytes::from_static(b"engine123"),
932 context_name: Bytes::new(),
933 security_level: SecurityLevel::AuthNoPriv,
934 uptime: 99999,
935 trap_oid: oids::warm_start(),
936 varbinds: vec![],
937 request_id: 100,
938 };
939
940 assert!(notification.is_confirmed());
941 assert_eq!(notification.version(), Version::V3);
942 assert_eq!(notification.uptime(), 99999);
943 assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
944 }
945
946 #[test]
947 fn test_notification_security_level_accessor() {
948 let trap_v3 = Notification::TrapV3 {
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::AuthPriv,
953 uptime: 1,
954 trap_oid: oids::cold_start(),
955 varbinds: vec![],
956 request_id: 1,
957 };
958 assert_eq!(trap_v3.security_level(), Some(SecurityLevel::AuthPriv));
959
960 let inform_v3 = Notification::InformV3 {
961 username: Bytes::from_static(b"testuser"),
962 context_engine_id: Bytes::from_static(b"engine123"),
963 context_name: Bytes::new(),
964 security_level: SecurityLevel::NoAuthNoPriv,
965 uptime: 1,
966 trap_oid: oids::cold_start(),
967 varbinds: vec![],
968 request_id: 1,
969 };
970 assert_eq!(
971 inform_v3.security_level(),
972 Some(SecurityLevel::NoAuthNoPriv)
973 );
974
975 let trap_v2c = Notification::TrapV2c {
976 community: Bytes::from_static(b"public"),
977 uptime: 1,
978 trap_oid: oids::cold_start(),
979 varbinds: vec![],
980 request_id: 1,
981 };
982 assert_eq!(trap_v2c.security_level(), None);
983 }
984
985 #[test]
986 fn test_notification_trap_v1_enterprise_specific_oid() {
987 let trap = TrapV1Pdu::new(
988 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
989 [192, 168, 1, 1],
990 GenericTrap::EnterpriseSpecific,
991 42,
992 12345,
993 vec![],
994 );
995
996 let notification = Notification::TrapV1 {
997 community: Bytes::from_static(b"public"),
998 trap,
999 };
1000
1001 assert_eq!(
1002 notification.trap_oid().unwrap(),
1003 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1004 );
1005 }
1006
1007 #[test]
1008 fn test_compute_engine_boots_time_basic() {
1009 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
1010 assert_eq!(boots, 1);
1011 assert_eq!(time, 1000);
1012 }
1013
1014 #[test]
1015 fn test_compute_engine_boots_time_zero_elapsed() {
1016 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
1017 assert_eq!(boots, 1);
1018 assert_eq!(time, 0);
1019 }
1020
1021 #[test]
1022 fn test_builder_engine_boots_default() {
1023 let builder = NotificationReceiverBuilder::new();
1024 assert_eq!(builder.engine_boots, 1);
1025 }
1026
1027 #[test]
1028 fn test_builder_engine_boots_custom() {
1029 let builder = NotificationReceiverBuilder::new().engine_boots(5);
1030 assert_eq!(builder.engine_boots, 5);
1031 }
1032
1033 fn build_v3_notification(
1038 pdu_type: crate::pdu::PduType,
1039 engine_id: &[u8],
1040 engine_boots: u32,
1041 engine_time: u32,
1042 username: &[u8],
1043 auth: Option<(&[u8], AuthProtocol)>,
1044 ) -> Bytes {
1045 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1046 use crate::pdu::Pdu;
1047 use crate::v3::auth::authenticate_message;
1048 use crate::v3::{LocalizedKey, UsmSecurityParams};
1049 use crate::value::Value;
1050
1051 let auth_key = auth.map(|(password, protocol)| {
1052 LocalizedKey::from_password(protocol, password, engine_id).unwrap()
1053 });
1054
1055 let pdu = Pdu {
1057 pdu_type,
1058 request_id: 1,
1059 error_status: 0,
1060 error_index: 0,
1061 varbinds: vec![
1062 VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
1063 VarBind::new(
1064 oids::snmp_trap_oid(),
1065 Value::ObjectIdentifier(oids::cold_start()),
1066 ),
1067 ],
1068 };
1069
1070 let level = if auth_key.is_some() {
1071 SecurityLevel::AuthNoPriv
1072 } else {
1073 SecurityLevel::NoAuthNoPriv
1074 };
1075 let reportable = pdu_type == crate::pdu::PduType::InformRequest;
1078 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(level, reportable));
1079
1080 let mut usm_params = UsmSecurityParams::new(
1081 Bytes::copy_from_slice(engine_id),
1082 engine_boots,
1083 engine_time,
1084 Bytes::copy_from_slice(username),
1085 );
1086 if let Some(key) = &auth_key {
1087 usm_params = usm_params.with_auth_placeholder(key.mac_len());
1088 }
1089
1090 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
1091 let msg = V3Message::new(global, usm_params.encode(), scoped);
1092 let mut msg_bytes = msg.encode().to_vec();
1093
1094 if let Some(key) = &auth_key {
1096 let (auth_offset, auth_len) =
1097 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1098 authenticate_message(key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1099 }
1100
1101 Bytes::from(msg_bytes)
1102 }
1103
1104 fn build_authed_v3_inform(
1107 engine_id: &[u8],
1108 engine_boots: u32,
1109 engine_time: u32,
1110 username: &[u8],
1111 auth_password: &[u8],
1112 auth_protocol: AuthProtocol,
1113 ) -> Bytes {
1114 build_v3_notification(
1115 crate::pdu::PduType::InformRequest,
1116 engine_id,
1117 engine_boots,
1118 engine_time,
1119 username,
1120 Some((auth_password, auth_protocol)),
1121 )
1122 }
1123
1124 fn build_authed_v3_trap(engine_id: &[u8], engine_boots: u32, engine_time: u32) -> Bytes {
1127 build_v3_notification(
1128 crate::pdu::PduType::TrapV2,
1129 engine_id,
1130 engine_boots,
1131 engine_time,
1132 b"trapuser",
1133 Some((b"authpass12345678", AuthProtocol::Sha1)),
1134 )
1135 }
1136
1137 fn build_noauth_v3_trap(engine_id: &[u8], username: &[u8]) -> Bytes {
1139 build_v3_notification(crate::pdu::PduType::TrapV2, engine_id, 0, 0, username, None)
1140 }
1141
1142 async fn remote_trap_receiver() -> NotificationReceiver {
1145 NotificationReceiver::builder()
1146 .bind("127.0.0.1:0")
1147 .engine_id(b"my-receiver-engine".to_vec())
1148 .engine_boots(1)
1149 .usm_user("trapuser", |u| {
1150 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1151 })
1152 .build()
1153 .await
1154 .unwrap()
1155 }
1156
1157 #[tokio::test]
1165 async fn test_v3_trap_from_remote_sender_engine_accepted() {
1166 let receiver = remote_trap_receiver().await;
1167 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1168
1169 let msg = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1171
1172 let result = receiver.handle_v3(msg, source).await.unwrap();
1173 match result {
1174 Some(Notification::TrapV3 {
1175 username,
1176 security_level,
1177 ..
1178 }) => {
1179 assert_eq!(username.as_ref(), b"trapuser");
1180 assert_eq!(security_level, SecurityLevel::AuthNoPriv);
1181 }
1182 other => panic!("expected TrapV3, got {other:?}"),
1183 }
1184 }
1185
1186 #[tokio::test]
1190 async fn test_v3_noauth_trap_carries_security_level() {
1191 let receiver = remote_trap_receiver().await;
1192 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1193
1194 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"trapuser");
1195 match receiver.handle_v3(msg, source).await.unwrap() {
1196 Some(Notification::TrapV3 {
1197 security_level,
1198 username,
1199 ..
1200 }) => {
1201 assert_eq!(security_level, SecurityLevel::NoAuthNoPriv);
1202 assert_eq!(username.as_ref(), b"trapuser");
1203 }
1204 other => panic!("expected TrapV3, got {other:?}"),
1205 }
1206 }
1207
1208 #[tokio::test]
1213 async fn test_v3_noauth_trap_unknown_user_rejected_and_counted() {
1214 let receiver = remote_trap_receiver().await;
1215 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1216
1217 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"nosuchuser");
1218 let result = receiver.handle_v3(msg, source).await.unwrap();
1219 assert!(result.is_none(), "unknown user must not be delivered");
1220 assert_eq!(receiver.usm_unknown_usernames(), 1);
1221 }
1222
1223 #[tokio::test]
1226 async fn test_v3_traps_from_multiple_remote_engines_accepted() {
1227 let receiver = remote_trap_receiver().await;
1228 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1229
1230 let msg_a = build_authed_v3_trap(b"sender-engine-a", 7, 123_456);
1231 let msg_b = build_authed_v3_trap(b"sender-engine-b", 2, 42);
1232
1233 assert!(
1234 receiver.handle_v3(msg_a, source).await.unwrap().is_some(),
1235 "trap from first remote engine should be accepted"
1236 );
1237 assert!(
1238 receiver.handle_v3(msg_b, source).await.unwrap().is_some(),
1239 "trap from second remote engine should be accepted"
1240 );
1241 }
1242
1243 #[tokio::test]
1248 async fn test_v3_remote_engines_table_bounded() {
1249 let receiver = remote_trap_receiver().await;
1250 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1251
1252 {
1254 let mut engines = receiver.inner.remote_engines.lock().unwrap();
1255 for i in 0..MAX_REMOTE_ENGINES {
1256 let id = Bytes::from(format!("dummy-engine-{i}"));
1257 engines.insert(id.clone(), EngineState::new(id, 1, 1));
1258 }
1259 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1260 }
1261
1262 let msg = build_authed_v3_trap(b"fresh-remote-engine", 7, 123_456);
1264 assert!(receiver.handle_v3(msg, source).await.unwrap().is_some());
1265
1266 let engines = receiver.inner.remote_engines.lock().unwrap();
1269 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1270 assert!(engines.contains_key(&Bytes::from_static(b"fresh-remote-engine")));
1271 }
1272
1273 #[tokio::test]
1278 async fn test_v3_trap_remote_engine_stale_time_rejected() {
1279 let receiver = remote_trap_receiver().await;
1280 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1281
1282 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1283 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1284
1285 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1287 assert!(
1288 receiver.handle_v3(stale, source).await.is_err(),
1289 "stale engine time should be rejected as outside the time window"
1290 );
1291 }
1292
1293 #[tokio::test]
1295 async fn test_v3_trap_remote_engine_old_boots_rejected() {
1296 let receiver = remote_trap_receiver().await;
1297 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1298
1299 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1300 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1301
1302 let old_boots = build_authed_v3_trap(b"remote-sender-engine", 6, 99_999);
1303 assert!(
1304 receiver.handle_v3(old_boots, source).await.is_err(),
1305 "older boot cycle should be rejected"
1306 );
1307 }
1308
1309 #[tokio::test]
1312 async fn test_v3_trap_remote_engine_reboot_accepted() {
1313 let receiver = remote_trap_receiver().await;
1314 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1315
1316 let before = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1317 assert!(receiver.handle_v3(before, source).await.unwrap().is_some());
1318
1319 let after_reboot = build_authed_v3_trap(b"remote-sender-engine", 8, 5);
1320 assert!(
1321 receiver
1322 .handle_v3(after_reboot, source)
1323 .await
1324 .unwrap()
1325 .is_some(),
1326 "trap after sender reboot should be accepted"
1327 );
1328
1329 let from_old_cycle = build_authed_v3_trap(b"remote-sender-engine", 7, 20_000);
1330 assert!(
1331 receiver.handle_v3(from_old_cycle, source).await.is_err(),
1332 "trap from superseded boot cycle should be rejected"
1333 );
1334 }
1335
1336 #[tokio::test]
1339 async fn test_v3_trap_remote_engine_bad_auth_rejected() {
1340 let receiver = remote_trap_receiver().await;
1341 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1342
1343 let msg = build_v3_notification(
1344 crate::pdu::PduType::TrapV2,
1345 b"remote-sender-engine",
1346 7,
1347 123_456,
1348 b"trapuser",
1349 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1350 );
1351 assert!(
1352 receiver.handle_v3(msg, source).await.is_err(),
1353 "trap with wrong auth key should be rejected"
1354 );
1355
1356 let good = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1358 assert!(receiver.handle_v3(good, source).await.unwrap().is_some());
1359 }
1360
1361 #[tokio::test]
1362 async fn test_v3_inform_outside_time_window_rejected() {
1363 let receiver = NotificationReceiver::builder()
1364 .bind("127.0.0.1:0")
1365 .engine_id(b"test-engine".to_vec())
1366 .engine_boots(1)
1367 .usm_user("informuser", |u| {
1368 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1369 })
1370 .build()
1371 .await
1372 .unwrap();
1373
1374 let engine_id = b"test-engine";
1375 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1376
1377 let msg = build_authed_v3_inform(
1379 engine_id,
1380 1, 5000, b"informuser",
1383 b"authpass12345678",
1384 AuthProtocol::Sha1,
1385 );
1386
1387 let result = receiver.handle_v3(msg, source).await;
1388 assert!(
1389 result.is_err(),
1390 "message with engine_time=5000 should be rejected (outside 150s window)"
1391 );
1392 }
1393
1394 #[tokio::test]
1395 async fn test_v3_inform_wrong_boots_rejected() {
1396 let receiver = NotificationReceiver::builder()
1397 .bind("127.0.0.1:0")
1398 .engine_id(b"test-engine".to_vec())
1399 .engine_boots(1)
1400 .usm_user("informuser", |u| {
1401 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1402 })
1403 .build()
1404 .await
1405 .unwrap();
1406
1407 let engine_id = b"test-engine";
1408 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1409
1410 let msg = build_authed_v3_inform(
1412 engine_id,
1413 2, 0, b"informuser",
1416 b"authpass12345678",
1417 AuthProtocol::Sha1,
1418 );
1419
1420 let result = receiver.handle_v3(msg, source).await;
1421 assert!(
1422 result.is_err(),
1423 "message with wrong engine_boots should be rejected"
1424 );
1425 }
1426
1427 #[tokio::test]
1428 async fn test_v3_inform_within_time_window_accepted() {
1429 let receiver = NotificationReceiver::builder()
1430 .bind("127.0.0.1:0")
1431 .engine_id(b"test-engine".to_vec())
1432 .engine_boots(1)
1433 .usm_user("informuser", |u| {
1434 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1435 })
1436 .build()
1437 .await
1438 .unwrap();
1439
1440 let engine_id = b"test-engine";
1441 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1442
1443 let msg = build_authed_v3_inform(
1445 engine_id,
1446 1, 0, b"informuser",
1449 b"authpass12345678",
1450 AuthProtocol::Sha1,
1451 );
1452
1453 let result = receiver.handle_v3(msg, source).await;
1454 match result {
1460 Ok(Some(_)) => {} Err(e) => {
1462 let err_str = format!("{e}");
1463 assert!(
1464 !err_str.contains("Auth"),
1465 "should not be an auth error for valid time window, got: {err_str}"
1466 );
1467 }
1468 Ok(None) => panic!("should not return None for a valid InformRequest"),
1469 }
1470 }
1471
1472 fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
1474 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1475 use crate::pdu::{Pdu, PduType};
1476 use crate::v3::UsmSecurityParams;
1477
1478 let pdu = Pdu {
1479 pdu_type: PduType::GetRequest,
1480 request_id: 0,
1481 error_status: 0,
1482 error_index: 0,
1483 varbinds: vec![],
1484 };
1485
1486 let global = MsgGlobalData::new(
1487 msg_id,
1488 65507,
1489 MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
1490 );
1491
1492 let usm_params = UsmSecurityParams::new(
1493 Bytes::new(), 0,
1495 0,
1496 Bytes::new(), );
1498
1499 let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
1500 let msg = V3Message::new(global, usm_params.encode(), scoped);
1501 msg.encode()
1502 }
1503
1504 #[tokio::test]
1505 async fn test_v3_discovery_gets_response() {
1506 use crate::message::V3Message;
1507 use crate::v3::UsmSecurityParams;
1508 use crate::value::Value;
1509
1510 let receiver = NotificationReceiver::builder()
1511 .bind("127.0.0.1:0")
1512 .engine_id(b"test-discovery-engine".to_vec())
1513 .build()
1514 .await
1515 .unwrap();
1516
1517 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1520 let client_addr = client.local_addr().unwrap();
1521
1522 let discovery_msg = build_v3_discovery_request(42, true);
1523 let result = receiver.handle_v3(discovery_msg, client_addr).await;
1524
1525 assert!(result.is_ok());
1527 assert!(result.unwrap().is_none());
1528
1529 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1531
1532 let mut buf = vec![0u8; 4096];
1535 let (len, _) = tokio::time::timeout(
1536 std::time::Duration::from_secs(1),
1537 client.recv_from(&mut buf),
1538 )
1539 .await
1540 .expect("expected a discovery Report")
1541 .unwrap();
1542
1543 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1544 assert_eq!(
1545 report.global_data.msg_flags.security_level,
1546 SecurityLevel::NoAuthNoPriv
1547 );
1548 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1549 assert_eq!(report_usm.engine_id.as_ref(), b"test-discovery-engine");
1550 let scoped = report.scoped_pdu().expect("report should be plaintext");
1551 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1552 assert_eq!(
1553 scoped.pdu.varbinds[0].oid,
1554 crate::v3::report_oids::unknown_engine_ids()
1555 );
1556 assert_eq!(scoped.pdu.varbinds[0].value, Value::Counter32(1));
1557 }
1558
1559 #[tokio::test]
1560 async fn test_v3_discovery_non_reportable_ignored() {
1561 let receiver = NotificationReceiver::builder()
1562 .bind("127.0.0.1:0")
1563 .engine_id(b"test-discovery-engine".to_vec())
1564 .build()
1565 .await
1566 .unwrap();
1567
1568 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1569 let discovery_msg = build_v3_discovery_request(42, false);
1570
1571 let result = receiver.handle_v3(discovery_msg, source).await;
1572
1573 assert!(result.is_ok());
1577 assert!(result.unwrap().is_none());
1578 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1579 }
1580
1581 #[tokio::test]
1587 async fn test_v3_inform_under_remote_engine_id_accepted() {
1588 let receiver = NotificationReceiver::builder()
1589 .bind("127.0.0.1:0")
1590 .engine_id(b"my-receiver-engine".to_vec())
1591 .engine_boots(1)
1592 .usm_user("informuser", |u| {
1593 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1594 })
1595 .build()
1596 .await
1597 .unwrap();
1598
1599 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1600
1601 let msg = build_authed_v3_inform(
1603 b"remote-engine-id",
1604 1,
1605 0,
1606 b"informuser",
1607 b"authpass12345678",
1608 AuthProtocol::Sha1,
1609 );
1610
1611 let result = receiver.handle_v3(msg, source).await.unwrap();
1612 assert!(
1613 matches!(result, Some(Notification::InformV3 { .. })),
1614 "authenticated inform under a remote engine ID should be accepted, got {result:?}"
1615 );
1616 }
1617
1618 #[test]
1619 fn test_auto_generated_engine_id_non_empty() {
1620 let builder = NotificationReceiverBuilder::new();
1621 assert!(builder.engine_id.is_none());
1623 }
1624
1625 #[tokio::test]
1626 async fn test_bind_generates_engine_id() {
1627 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1628 assert!(!receiver.engine_id().is_empty());
1629 assert_eq!(receiver.engine_id()[0], 0x80);
1631 }
1632
1633 #[tokio::test]
1634 async fn test_builder_generates_engine_id() {
1635 let receiver = NotificationReceiver::builder()
1636 .bind("127.0.0.1:0")
1637 .build()
1638 .await
1639 .unwrap();
1640 assert!(!receiver.engine_id().is_empty());
1641 assert_eq!(receiver.engine_id()[0], 0x80);
1642 }
1643
1644 #[tokio::test]
1645 async fn test_builder_custom_engine_id() {
1646 let receiver = NotificationReceiver::builder()
1647 .bind("127.0.0.1:0")
1648 .engine_id(b"custom-engine".to_vec())
1649 .build()
1650 .await
1651 .unwrap();
1652 assert_eq!(receiver.engine_id(), b"custom-engine");
1653 }
1654
1655 #[tokio::test]
1656 async fn test_usm_counter_accessors_default_zero() {
1657 let receiver = remote_trap_receiver().await;
1658 assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1659 assert_eq!(receiver.usm_unknown_usernames(), 0);
1660 assert_eq!(receiver.usm_wrong_digests(), 0);
1661 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1662 assert_eq!(receiver.usm_unsupported_sec_levels(), 0);
1663 assert_eq!(receiver.usm_decryption_errors(), 0);
1664 }
1665
1666 #[tokio::test]
1669 async fn test_v3_trap_wrong_digest_increments_counter() {
1670 let receiver = remote_trap_receiver().await;
1671 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1672
1673 let msg = build_v3_notification(
1674 crate::pdu::PduType::TrapV2,
1675 b"remote-sender-engine",
1676 7,
1677 123_456,
1678 b"trapuser",
1679 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1680 );
1681 assert!(receiver.handle_v3(msg, source).await.is_err());
1682 assert_eq!(receiver.usm_wrong_digests(), 1);
1683 }
1684
1685 #[tokio::test]
1688 async fn test_v3_trap_unknown_user_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"nosuchuser",
1698 Some((b"authpass12345678", AuthProtocol::Sha1)),
1699 );
1700 let result = receiver.handle_v3(msg, source).await.unwrap();
1701 assert!(result.is_none(), "unknown user must not be delivered");
1702 assert_eq!(receiver.usm_unknown_usernames(), 1);
1703 assert_eq!(receiver.usm_wrong_digests(), 0);
1704 }
1705
1706 #[tokio::test]
1710 async fn test_v3_trap_user_without_auth_key_increments_counter() {
1711 let receiver = NotificationReceiver::builder()
1712 .bind("127.0.0.1:0")
1713 .engine_id(b"my-receiver-engine".to_vec())
1714 .usm_user("plainuser", |u| u)
1715 .build()
1716 .await
1717 .unwrap();
1718 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1719
1720 let msg = build_v3_notification(
1721 crate::pdu::PduType::TrapV2,
1722 b"remote-sender-engine",
1723 7,
1724 123_456,
1725 b"plainuser",
1726 Some((b"authpass12345678", AuthProtocol::Sha1)),
1727 );
1728 let result = receiver.handle_v3(msg, source).await.unwrap();
1729 assert!(result.is_none());
1730 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
1731 assert_eq!(receiver.usm_unknown_usernames(), 0);
1732 }
1733
1734 #[tokio::test]
1737 async fn test_v3_inform_time_window_failure_increments_counter() {
1738 let receiver = NotificationReceiver::builder()
1739 .bind("127.0.0.1:0")
1740 .engine_id(b"test-engine".to_vec())
1741 .engine_boots(1)
1742 .usm_user("informuser", |u| {
1743 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1744 })
1745 .build()
1746 .await
1747 .unwrap();
1748 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1749
1750 let msg = build_authed_v3_inform(
1751 b"test-engine",
1752 1,
1753 5000,
1754 b"informuser",
1755 b"authpass12345678",
1756 AuthProtocol::Sha1,
1757 );
1758 assert!(receiver.handle_v3(msg, source).await.is_err());
1759 assert_eq!(receiver.usm_not_in_time_windows(), 1);
1760 }
1761
1762 #[tokio::test]
1768 async fn test_v3_trap_remote_stale_not_counted() {
1769 let receiver = remote_trap_receiver().await;
1770 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1771
1772 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1773 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1774
1775 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1776 assert!(receiver.handle_v3(stale, source).await.is_err());
1777 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1778 }
1779
1780 #[tokio::test]
1784 async fn test_v3_inform_remote_stale_gets_no_report() {
1785 let receiver = remote_trap_receiver().await;
1786
1787 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1788 let client_addr = client.local_addr().unwrap();
1789
1790 let fresh = build_v3_notification(
1791 crate::pdu::PduType::InformRequest,
1792 b"remote-sender-engine",
1793 7,
1794 10_000,
1795 b"trapuser",
1796 Some((b"authpass12345678", AuthProtocol::Sha1)),
1797 );
1798 assert!(
1799 receiver
1800 .handle_v3(fresh, client_addr)
1801 .await
1802 .unwrap()
1803 .is_some()
1804 );
1805
1806 let mut buf = vec![0u8; 4096];
1808 tokio::time::timeout(
1809 std::time::Duration::from_secs(1),
1810 client.recv_from(&mut buf),
1811 )
1812 .await
1813 .expect("expected the inform response")
1814 .unwrap();
1815
1816 let stale = build_v3_notification(
1817 crate::pdu::PduType::InformRequest,
1818 b"remote-sender-engine",
1819 7,
1820 5_000,
1821 b"trapuser",
1822 Some((b"authpass12345678", AuthProtocol::Sha1)),
1823 );
1824 assert!(receiver.handle_v3(stale, client_addr).await.is_err());
1825 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1826
1827 let result = tokio::time::timeout(
1828 std::time::Duration::from_millis(200),
1829 client.recv_from(&mut buf),
1830 )
1831 .await;
1832 assert!(
1833 result.is_err(),
1834 "no Report may be sent for a Step 7b timeliness failure"
1835 );
1836 }
1837
1838 fn build_v3_trap_bad_ciphertext(
1842 engine_id: &[u8],
1843 username: &[u8],
1844 auth_password: &[u8],
1845 ) -> Bytes {
1846 use crate::message::{MsgFlags, MsgGlobalData, V3Message};
1847 use crate::v3::auth::authenticate_message;
1848 use crate::v3::{LocalizedKey, UsmSecurityParams};
1849
1850 let auth_key =
1851 LocalizedKey::from_password(AuthProtocol::Sha1, auth_password, engine_id).unwrap();
1852
1853 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthPriv, false));
1854 let usm_params = UsmSecurityParams::new(
1855 Bytes::copy_from_slice(engine_id),
1856 7,
1857 123_456,
1858 Bytes::copy_from_slice(username),
1859 )
1860 .with_auth_placeholder(auth_key.mac_len())
1861 .with_priv_params(Bytes::from_static(b"bad"));
1862
1863 let msg = V3Message::new_encrypted(
1864 global,
1865 usm_params.encode(),
1866 Bytes::from_static(b"not-a-valid-ciphertext"),
1867 );
1868 let mut msg_bytes = msg.encode().to_vec();
1869 let (auth_offset, auth_len) =
1870 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1871 authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1872 Bytes::from(msg_bytes)
1873 }
1874
1875 #[tokio::test]
1878 async fn test_v3_decryption_error_increments_counter() {
1879 let receiver = NotificationReceiver::builder()
1880 .bind("127.0.0.1:0")
1881 .engine_id(b"my-receiver-engine".to_vec())
1882 .usm_user("privuser", |u| {
1883 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1884 .privacy(crate::v3::PrivProtocol::Aes128, b"privpass12345678")
1885 })
1886 .build()
1887 .await
1888 .unwrap();
1889 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1890
1891 let msg =
1892 build_v3_trap_bad_ciphertext(b"remote-sender-engine", b"privuser", b"authpass12345678");
1893 assert!(receiver.handle_v3(msg, source).await.is_err());
1894 assert_eq!(receiver.usm_decryption_errors(), 1);
1895 }
1896
1897 #[tokio::test]
1902 async fn test_v3_authpriv_for_auth_only_user_counts_unsupported_sec_level() {
1903 let receiver = remote_trap_receiver().await;
1904 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1905
1906 let msg = build_v3_trap_bad_ciphertext(
1907 b"remote-sender-engine",
1908 b"trapuser",
1909 b"wrong-password-1234",
1910 );
1911 let result = receiver.handle_v3(msg, source).await.unwrap();
1912 assert!(result.is_none());
1913 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
1914 assert_eq!(receiver.usm_wrong_digests(), 0);
1915 }
1916
1917 #[tokio::test]
1923 async fn test_v3_failed_inform_gets_authenticated_time_window_report() {
1924 use crate::message::V3Message;
1925 use crate::v3::auth::verify_message;
1926 use crate::v3::{LocalizedKey, UsmSecurityParams};
1927
1928 let receiver = NotificationReceiver::builder()
1929 .bind("127.0.0.1:0")
1930 .engine_id(b"test-engine".to_vec())
1931 .engine_boots(1)
1932 .usm_user("informuser", |u| {
1933 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1934 })
1935 .build()
1936 .await
1937 .unwrap();
1938
1939 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1940 let client_addr = client.local_addr().unwrap();
1941
1942 let msg = build_authed_v3_inform(
1943 b"test-engine",
1944 1,
1945 5000, b"informuser",
1947 b"authpass12345678",
1948 AuthProtocol::Sha1,
1949 );
1950 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
1951
1952 let mut buf = vec![0u8; 4096];
1953 let (len, _) = tokio::time::timeout(
1954 std::time::Duration::from_secs(1),
1955 client.recv_from(&mut buf),
1956 )
1957 .await
1958 .expect("expected a Report in response to the failed inform")
1959 .unwrap();
1960 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
1961
1962 let report = V3Message::decode(report_bytes.clone()).unwrap();
1963 assert_eq!(
1964 report.global_data.msg_flags.security_level,
1965 SecurityLevel::AuthNoPriv,
1966 "notInTimeWindows report must be authenticated (authNoPriv)"
1967 );
1968 assert!(!report.global_data.msg_flags.reportable);
1969
1970 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1971 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
1972
1973 let key =
1976 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
1977 .unwrap();
1978 let (auth_offset, auth_len) =
1979 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
1980 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
1981
1982 let scoped = report.scoped_pdu().expect("report should be plaintext");
1983 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1984 assert_eq!(
1985 scoped.pdu.varbinds[0].oid,
1986 crate::v3::report_oids::not_in_time_windows()
1987 );
1988 }
1989
1990 #[tokio::test]
1994 async fn test_v3_latched_boots_report_is_authenticated() {
1995 use crate::message::V3Message;
1996 use crate::v3::MAX_ENGINE_TIME;
1997 use crate::v3::auth::verify_message;
1998 use crate::v3::{LocalizedKey, UsmSecurityParams};
1999
2000 let receiver = NotificationReceiver::builder()
2001 .bind("127.0.0.1:0")
2002 .engine_id(b"test-engine".to_vec())
2003 .engine_boots(MAX_ENGINE_TIME)
2004 .usm_user("informuser", |u| {
2005 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2006 })
2007 .build()
2008 .await
2009 .unwrap();
2010
2011 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2012 let client_addr = client.local_addr().unwrap();
2013
2014 let msg = build_authed_v3_inform(
2015 b"test-engine",
2016 MAX_ENGINE_TIME,
2017 0,
2018 b"informuser",
2019 b"authpass12345678",
2020 AuthProtocol::Sha1,
2021 );
2022 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2023 assert_eq!(receiver.usm_not_in_time_windows(), 1);
2024
2025 let mut buf = vec![0u8; 4096];
2026 let (len, _) = tokio::time::timeout(
2027 std::time::Duration::from_secs(1),
2028 client.recv_from(&mut buf),
2029 )
2030 .await
2031 .expect("expected a Report in response to the failed inform")
2032 .unwrap();
2033 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2034
2035 let report = V3Message::decode(report_bytes.clone()).unwrap();
2036 assert_eq!(
2037 report.global_data.msg_flags.security_level,
2038 SecurityLevel::AuthNoPriv,
2039 "notInTimeWindows report must be authenticated (authNoPriv)"
2040 );
2041 let key =
2042 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2043 .unwrap();
2044 let (auth_offset, auth_len) =
2045 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2046 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2047
2048 let scoped = report.scoped_pdu().expect("report should be plaintext");
2049 assert_eq!(
2050 scoped.pdu.varbinds[0].oid,
2051 crate::v3::report_oids::not_in_time_windows()
2052 );
2053 }
2054
2055 #[tokio::test]
2058 async fn test_v3_failed_inform_unknown_user_gets_noauth_report() {
2059 use crate::message::V3Message;
2060 use crate::v3::UsmSecurityParams;
2061
2062 let receiver = NotificationReceiver::builder()
2063 .bind("127.0.0.1:0")
2064 .engine_id(b"test-engine".to_vec())
2065 .engine_boots(1)
2066 .usm_user("informuser", |u| {
2067 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2068 })
2069 .build()
2070 .await
2071 .unwrap();
2072
2073 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2074 let client_addr = client.local_addr().unwrap();
2075
2076 let msg = build_authed_v3_inform(
2077 b"test-engine",
2078 1,
2079 0,
2080 b"nosuchuser",
2081 b"authpass12345678",
2082 AuthProtocol::Sha1,
2083 );
2084 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
2085 assert!(result.is_none());
2086 assert_eq!(receiver.usm_unknown_usernames(), 1);
2087
2088 let mut buf = vec![0u8; 4096];
2089 let (len, _) = tokio::time::timeout(
2090 std::time::Duration::from_secs(1),
2091 client.recv_from(&mut buf),
2092 )
2093 .await
2094 .expect("expected a Report in response to the failed inform")
2095 .unwrap();
2096
2097 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
2098 assert_eq!(
2099 report.global_data.msg_flags.security_level,
2100 SecurityLevel::NoAuthNoPriv
2101 );
2102 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2103 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2104 let scoped = report.scoped_pdu().expect("report should be plaintext");
2105 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2106 assert_eq!(
2107 scoped.pdu.varbinds[0].oid,
2108 crate::v3::report_oids::unknown_user_names()
2109 );
2110 }
2111
2112 #[tokio::test]
2115 async fn test_v3_failed_trap_gets_no_report() {
2116 let receiver = remote_trap_receiver().await;
2117
2118 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2119 let client_addr = client.local_addr().unwrap();
2120
2121 let msg = build_v3_notification(
2122 crate::pdu::PduType::TrapV2,
2123 b"remote-sender-engine",
2124 7,
2125 123_456,
2126 b"trapuser",
2127 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
2128 );
2129 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2130 assert_eq!(receiver.usm_wrong_digests(), 1);
2131
2132 let mut buf = vec![0u8; 4096];
2133 let result = tokio::time::timeout(
2134 std::time::Duration::from_millis(200),
2135 client.recv_from(&mut buf),
2136 )
2137 .await;
2138 assert!(result.is_err(), "no Report may be sent for a failed trap");
2139 }
2140
2141 #[test]
2142 fn test_community_allowed() {
2143 assert!(community_allowed(&[], b"public"));
2145 assert!(community_allowed(&[], b""));
2146
2147 let configured = vec![b"public".to_vec(), b"monitor".to_vec()];
2148 assert!(community_allowed(&configured, b"public"));
2149 assert!(community_allowed(&configured, b"monitor"));
2150 assert!(!community_allowed(&configured, b"private"));
2152 assert!(!community_allowed(&configured, b"pub"));
2153 assert!(!community_allowed(&configured, b"publicx"));
2154 assert!(!community_allowed(&configured, b""));
2155 }
2156
2157 fn build_v2c_trap(community: &[u8]) -> Bytes {
2158 use crate::message::CommunityMessage;
2159 use crate::pdu::Pdu;
2160 let pdu = Pdu::trap_v2(1, 100, &oids::cold_start(), vec![]);
2161 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2162 }
2163
2164 fn build_v2c_inform(community: &[u8]) -> Bytes {
2165 use crate::message::CommunityMessage;
2166 use crate::pdu::Pdu;
2167 let pdu = Pdu::inform_request(1, 100, &oids::cold_start(), vec![]);
2168 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2169 }
2170
2171 fn build_v1_trap(community: &[u8]) -> Bytes {
2172 use crate::message::CommunityMessage;
2173 use crate::pdu::GenericTrap;
2174 let trap = TrapV1Pdu::new(
2175 oid!(1, 3, 6, 1, 4, 1, 9999),
2176 [192, 168, 1, 1],
2177 GenericTrap::ColdStart,
2178 0,
2179 12345,
2180 vec![],
2181 );
2182 CommunityMessage::v1_trap(Bytes::copy_from_slice(community), trap).encode()
2183 }
2184
2185 #[tokio::test]
2186 async fn test_v2c_trap_matching_community_accepted() {
2187 let receiver = NotificationReceiver::builder()
2188 .bind("127.0.0.1:0")
2189 .community(b"public")
2190 .build()
2191 .await
2192 .unwrap();
2193 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2194
2195 let result = receiver
2196 .handle_v2c(build_v2c_trap(b"public"), source)
2197 .await
2198 .unwrap();
2199 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2200 }
2201
2202 #[tokio::test]
2203 async fn test_v2c_trap_wrong_community_dropped() {
2204 let receiver = NotificationReceiver::builder()
2205 .bind("127.0.0.1:0")
2206 .community(b"public")
2207 .build()
2208 .await
2209 .unwrap();
2210 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2211
2212 let result = receiver
2213 .handle_v2c(build_v2c_trap(b"private"), source)
2214 .await
2215 .unwrap();
2216 assert!(result.is_none());
2217 }
2218
2219 #[tokio::test]
2220 async fn test_v2c_trap_no_allowlist_accepts_any_community() {
2221 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
2222 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2223
2224 let result = receiver
2225 .handle_v2c(build_v2c_trap(b"anything"), source)
2226 .await
2227 .unwrap();
2228 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2229 }
2230
2231 #[tokio::test]
2232 async fn test_v1_trap_wrong_community_dropped() {
2233 let receiver = NotificationReceiver::builder()
2234 .bind("127.0.0.1:0")
2235 .community(b"public")
2236 .build()
2237 .await
2238 .unwrap();
2239 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2240
2241 assert!(
2242 receiver
2243 .handle_v1(build_v1_trap(b"private"), source)
2244 .await
2245 .unwrap()
2246 .is_none()
2247 );
2248 assert!(matches!(
2249 receiver
2250 .handle_v1(build_v1_trap(b"public"), source)
2251 .await
2252 .unwrap(),
2253 Some(Notification::TrapV1 { .. })
2254 ));
2255 }
2256
2257 #[tokio::test]
2260 async fn test_v2c_inform_wrong_community_dropped_without_ack() {
2261 let receiver = NotificationReceiver::builder()
2262 .bind("127.0.0.1:0")
2263 .community(b"public")
2264 .build()
2265 .await
2266 .unwrap();
2267
2268 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2269 let client_addr = client.local_addr().unwrap();
2270
2271 let result = receiver
2272 .handle_v2c(build_v2c_inform(b"private"), client_addr)
2273 .await
2274 .unwrap();
2275 assert!(result.is_none());
2276
2277 let mut buf = vec![0u8; 4096];
2278 let recv = tokio::time::timeout(
2279 std::time::Duration::from_millis(200),
2280 client.recv_from(&mut buf),
2281 )
2282 .await;
2283 assert!(recv.is_err(), "a filtered inform must not be acknowledged");
2284 }
2285
2286 #[tokio::test]
2289 async fn test_v2c_inform_matching_community_acked() {
2290 let receiver = NotificationReceiver::builder()
2291 .bind("127.0.0.1:0")
2292 .community(b"public")
2293 .build()
2294 .await
2295 .unwrap();
2296
2297 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2298 let client_addr = client.local_addr().unwrap();
2299
2300 let result = receiver
2301 .handle_v2c(build_v2c_inform(b"public"), client_addr)
2302 .await
2303 .unwrap();
2304 assert!(matches!(result, Some(Notification::InformV2c { .. })));
2305
2306 let mut buf = vec![0u8; 4096];
2307 let (len, _) = tokio::time::timeout(
2308 std::time::Duration::from_secs(1),
2309 client.recv_from(&mut buf),
2310 )
2311 .await
2312 .expect("a matching inform must be acknowledged")
2313 .unwrap();
2314 assert!(len > 0);
2315 }
2316}