1mod handlers;
119mod varbind;
120
121use std::collections::HashMap;
122use std::net::SocketAddr;
123use std::sync::atomic::Ordering;
124use std::sync::{Arc, Mutex};
125use std::time::Instant;
126
127use bytes::Bytes;
128use subtle::ConstantTimeEq;
129use tokio::net::UdpSocket;
130use tracing::instrument;
131
132use crate::error::{Error, Result};
133use crate::message::SecurityLevel;
134use crate::oid::Oid;
135use crate::pdu::TrapV1Pdu;
136use crate::util::bind_udp_socket;
137use crate::v3::process::UsmStats;
138use crate::v3::{AuthoritativeEngine, EngineState, SaltCounter};
139use crate::varbind::VarBind;
140use crate::version::Version;
141
142pub use crate::v3::{DerivedKeys, UsmConfig};
144pub use varbind::validate_notification_varbinds;
145
146const MAX_REMOTE_ENGINES: usize = 8192;
152
153pub(super) fn community_allowed(configured: &[Vec<u8>], community: &[u8]) -> bool {
161 if configured.is_empty() {
162 return true;
163 }
164 let mut valid = false;
165 for candidate in configured {
166 if candidate.len() == community.len() && bool::from(candidate.as_slice().ct_eq(community)) {
167 valid = true;
168 }
169 }
170 valid
171}
172
173pub mod oids {
175 use crate::oid;
176
177 #[must_use]
179 pub fn sys_uptime() -> crate::Oid {
180 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
181 }
182
183 #[must_use]
185 pub fn snmp_trap_oid() -> crate::Oid {
186 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
187 }
188
189 #[must_use]
191 pub fn snmp_trap_enterprise() -> crate::Oid {
192 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
193 }
194
195 #[must_use]
197 pub fn snmp_trap_address() -> crate::Oid {
198 oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
199 }
200
201 #[must_use]
203 pub fn snmp_traps() -> crate::Oid {
204 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
205 }
206
207 #[must_use]
209 pub fn cold_start() -> crate::Oid {
210 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
211 }
212
213 #[must_use]
215 pub fn warm_start() -> crate::Oid {
216 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
217 }
218
219 #[must_use]
221 pub fn link_down() -> crate::Oid {
222 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
223 }
224
225 #[must_use]
227 pub fn link_up() -> crate::Oid {
228 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
229 }
230
231 #[must_use]
233 pub fn auth_failure() -> crate::Oid {
234 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
235 }
236
237 #[must_use]
239 pub fn egp_neighbor_loss() -> crate::Oid {
240 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
241 }
242}
243
244pub struct NotificationReceiverBuilder {
252 bind_addr: String,
253 usm_users: HashMap<Bytes, UsmConfig>,
254 communities: Vec<Vec<u8>>,
255 authoritative_engine: Option<AuthoritativeEngine>,
256}
257
258impl NotificationReceiverBuilder {
259 #[must_use]
266 pub fn new() -> Self {
267 Self {
268 bind_addr: "0.0.0.0:162".to_string(),
269 usm_users: HashMap::new(),
270 communities: Vec::new(),
271 authoritative_engine: None,
272 }
273 }
274
275 #[must_use]
279 pub fn bind(mut self, addr: impl Into<String>) -> Self {
280 self.bind_addr = addr.into();
281 self
282 }
283
284 #[must_use]
319 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
320 where
321 F: FnOnce(UsmConfig) -> UsmConfig,
322 {
323 let username_bytes: Bytes = username.into();
324 let config = configure(UsmConfig::new(username_bytes.clone()));
325 self.usm_users.insert(username_bytes, config);
326 self
327 }
328
329 #[must_use]
357 pub fn community(mut self, community: &[u8]) -> Self {
358 self.communities.push(community.to_vec());
359 self
360 }
361
362 #[must_use]
382 pub fn communities<I, C>(mut self, communities: I) -> Self
383 where
384 I: IntoIterator<Item = C>,
385 C: AsRef<[u8]>,
386 {
387 for c in communities {
388 self.communities.push(c.as_ref().to_vec());
389 }
390 self
391 }
392
393 #[must_use]
400 pub fn authoritative_engine(mut self, engine: AuthoritativeEngine) -> Self {
401 self.authoritative_engine = Some(engine);
402 self
403 }
404
405 #[cfg(test)]
406 pub(crate) fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
407 let boots = self
408 .authoritative_engine
409 .as_ref()
410 .map_or(1, AuthoritativeEngine::engine_boots);
411 self.authoritative_engine = Some(AuthoritativeEngine::for_test(engine_id.into(), boots));
412 self
413 }
414
415 #[cfg(test)]
416 pub(crate) fn engine_boots(mut self, boots: u32) -> Self {
417 let engine_id = self
418 .authoritative_engine
419 .as_ref()
420 .map(|engine| engine.engine_id().to_vec())
421 .unwrap_or_else(|| crate::v3::generate_engine_id().to_vec());
422 self.authoritative_engine = Some(AuthoritativeEngine::for_test(engine_id, boots));
423 self
424 }
425
426 pub async fn build(mut self) -> Result<NotificationReceiver> {
431 for config in self.usm_users.values_mut() {
434 config.precompute_master_keys();
435 }
436
437 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
438 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
439 })?;
440
441 let socket = bind_udp_socket(bind_addr, None, None, false)
442 .await
443 .map_err(|e| Error::Network {
444 target: bind_addr,
445 source: e,
446 })?;
447
448 let local_addr = socket.local_addr().map_err(|e| Error::Network {
449 target: bind_addr,
450 source: e,
451 })?;
452
453 let (authoritative_engine, engine_id, engine_boots) = match self.authoritative_engine {
454 Some(engine) => {
455 let (engine_boots, _) = engine.current_boots_time()?;
456 let engine_id = Bytes::copy_from_slice(engine.engine_id());
457 (Some(engine), engine_id, engine_boots)
458 }
459 None if !self.usm_users.is_empty() => {
460 return Err(Error::Config(
461 "authoritative engine state is required for SNMPv3 notification receiving"
462 .into(),
463 )
464 .boxed());
465 }
466 None => (None, crate::v3::generate_engine_id(), 1),
467 };
468
469 Ok(NotificationReceiver {
470 inner: Arc::new(ReceiverInner {
471 authoritative_engine,
472 socket,
473 local_addr,
474 usm_users: self.usm_users,
475 communities: self.communities,
476 engine_id,
477 salt_counter: SaltCounter::new(),
478 engine_boots_base: engine_boots,
479 engine_start: Instant::now(),
480 usm_stats: UsmStats::default(),
481 remote_engines: Mutex::new(HashMap::new()),
482 }),
483 })
484 }
485}
486
487impl Default for NotificationReceiverBuilder {
488 fn default() -> Self {
489 Self::new()
490 }
491}
492
493#[derive(Debug, Clone)]
500pub enum Notification {
501 TrapV1 {
503 community: Bytes,
505 trap: TrapV1Pdu,
507 },
508
509 TrapV2c {
511 community: Bytes,
513 uptime: u32,
515 trap_oid: Oid,
517 varbinds: Vec<VarBind>,
519 request_id: i32,
521 },
522
523 TrapV3 {
525 username: Bytes,
527 context_engine_id: Bytes,
529 context_name: Bytes,
531 security_level: SecurityLevel,
535 uptime: u32,
537 trap_oid: Oid,
539 varbinds: Vec<VarBind>,
541 request_id: i32,
543 },
544
545 InformV2c {
549 community: Bytes,
551 uptime: u32,
553 trap_oid: Oid,
555 varbinds: Vec<VarBind>,
557 request_id: i32,
559 },
560
561 InformV3 {
565 username: Bytes,
567 context_engine_id: Bytes,
569 context_name: Bytes,
571 security_level: SecurityLevel,
575 uptime: u32,
577 trap_oid: Oid,
579 varbinds: Vec<VarBind>,
581 request_id: i32,
583 },
584}
585
586impl Notification {
587 pub fn trap_oid(&self) -> Result<Oid> {
592 match self {
593 Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
594 Notification::TrapV2c { trap_oid, .. }
595 | Notification::TrapV3 { trap_oid, .. }
596 | Notification::InformV2c { trap_oid, .. }
597 | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
598 }
599 }
600
601 pub fn uptime(&self) -> u32 {
603 match self {
604 Notification::TrapV1 { trap, .. } => trap.time_stamp,
605 Notification::TrapV2c { uptime, .. }
606 | Notification::TrapV3 { uptime, .. }
607 | Notification::InformV2c { uptime, .. }
608 | Notification::InformV3 { uptime, .. } => *uptime,
609 }
610 }
611
612 pub fn varbinds(&self) -> &[VarBind] {
614 match self {
615 Notification::TrapV1 { trap, .. } => &trap.varbinds,
616 Notification::TrapV2c { varbinds, .. }
617 | Notification::TrapV3 { varbinds, .. }
618 | Notification::InformV2c { varbinds, .. }
619 | Notification::InformV3 { varbinds, .. } => varbinds,
620 }
621 }
622
623 pub fn security_level(&self) -> Option<SecurityLevel> {
630 match self {
631 Notification::TrapV1 { .. }
632 | Notification::TrapV2c { .. }
633 | Notification::InformV2c { .. } => None,
634 Notification::TrapV3 { security_level, .. }
635 | Notification::InformV3 { security_level, .. } => Some(*security_level),
636 }
637 }
638
639 pub fn is_confirmed(&self) -> bool {
641 matches!(
642 self,
643 Notification::InformV2c { .. } | Notification::InformV3 { .. }
644 )
645 }
646
647 pub fn version(&self) -> Version {
649 match self {
650 Notification::TrapV1 { .. } => Version::V1,
651 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
652 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
653 }
654 }
655}
656
657pub struct NotificationReceiver {
689 inner: Arc<ReceiverInner>,
690}
691
692struct ReceiverInner {
693 authoritative_engine: Option<AuthoritativeEngine>,
694 socket: UdpSocket,
695 local_addr: SocketAddr,
696 usm_users: HashMap<Bytes, UsmConfig>,
698 communities: Vec<Vec<u8>>,
702 engine_id: Bytes,
704 salt_counter: SaltCounter,
706 engine_boots_base: u32,
708 engine_start: Instant,
710 usm_stats: UsmStats,
712 remote_engines: Mutex<HashMap<Bytes, EngineState>>,
719}
720
721impl ReceiverInner {
722 fn authoritative_boots_time(&self) -> Result<(u32, u32)> {
724 match &self.authoritative_engine {
725 Some(engine) => engine.current_boots_time(),
726 None => {
727 let total_secs = self.engine_start.elapsed().as_secs();
728 Ok(crate::v3::compute_engine_boots_time(
729 self.engine_boots_base,
730 total_secs,
731 ))
732 }
733 }
734 }
735}
736
737impl NotificationReceiver {
738 #[must_use]
742 pub fn builder() -> NotificationReceiverBuilder {
743 NotificationReceiverBuilder::new()
744 }
745
746 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
772 let addr_str = addr.as_ref();
773 let bind_addr: SocketAddr = addr_str
774 .parse()
775 .map_err(|_| Error::Config(format!("invalid bind address: {addr_str}").into()))?;
776
777 let socket = bind_udp_socket(bind_addr, None, None, false)
778 .await
779 .map_err(|e| Error::Network {
780 target: bind_addr,
781 source: e,
782 })?;
783
784 let local_addr = socket.local_addr().map_err(|e| Error::Network {
785 target: bind_addr,
786 source: e,
787 })?;
788
789 let engine_id = crate::v3::generate_engine_id();
790
791 Ok(Self {
792 inner: Arc::new(ReceiverInner {
793 authoritative_engine: None,
794 socket,
795 local_addr,
796 usm_users: HashMap::new(),
797 communities: Vec::new(),
798 engine_id,
799 salt_counter: SaltCounter::new(),
800 engine_boots_base: 1,
801 engine_start: Instant::now(),
802 usm_stats: UsmStats::default(),
803 remote_engines: Mutex::new(HashMap::new()),
804 }),
805 })
806 }
807
808 #[must_use]
810 pub fn local_addr(&self) -> SocketAddr {
811 self.inner.local_addr
812 }
813
814 #[must_use]
820 pub fn engine_id(&self) -> &[u8] {
821 &self.inner.engine_id
822 }
823
824 #[must_use]
826 pub fn engine_boots(&self) -> u32 {
827 match self.inner.authoritative_boots_time() {
828 Ok(pair) => pair.0,
829 Err(_) => self.inner.authoritative_engine.as_ref().map_or(
830 self.inner.engine_boots_base,
831 AuthoritativeEngine::engine_boots,
832 ),
833 }
834 }
835
836 #[must_use]
838 pub fn usm_unknown_engine_ids(&self) -> u32 {
839 self.inner
840 .usm_stats
841 .unknown_engine_ids
842 .load(Ordering::Relaxed)
843 }
844
845 #[must_use]
847 pub fn usm_unknown_usernames(&self) -> u32 {
848 self.inner
849 .usm_stats
850 .unknown_usernames
851 .load(Ordering::Relaxed)
852 }
853
854 #[must_use]
856 pub fn usm_wrong_digests(&self) -> u32 {
857 self.inner.usm_stats.wrong_digests.load(Ordering::Relaxed)
858 }
859
860 #[must_use]
862 pub fn usm_not_in_time_windows(&self) -> u32 {
863 self.inner
864 .usm_stats
865 .not_in_time_windows
866 .load(Ordering::Relaxed)
867 }
868
869 #[must_use]
871 pub fn usm_unsupported_sec_levels(&self) -> u32 {
872 self.inner
873 .usm_stats
874 .unsupported_sec_levels
875 .load(Ordering::Relaxed)
876 }
877
878 #[must_use]
880 pub fn usm_decryption_errors(&self) -> u32 {
881 self.inner
882 .usm_stats
883 .decryption_errors
884 .load(Ordering::Relaxed)
885 }
886
887 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
894 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
895 let mut buf = vec![0u8; 65535];
896
897 loop {
898 let (len, source) =
899 self.inner
900 .socket
901 .recv_from(&mut buf)
902 .await
903 .map_err(|e| Error::Network {
904 target: self.inner.local_addr,
905 source: e,
906 })?;
907
908 let data = Bytes::copy_from_slice(&buf[..len]);
909
910 match self.parse_and_respond(data, source).await {
911 Ok(Some(notification)) => return Ok((notification, source)),
912 Ok(None) => {} Err(e) => {
914 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
916 }
917 }
918 }
919 }
920
921 async fn parse_and_respond(
925 &self,
926 data: Bytes,
927 source: SocketAddr,
928 ) -> Result<Option<Notification>> {
929 match crate::message::peek_version(data.clone(), source)? {
930 Version::V1 => self.handle_v1(data, source).await,
931 Version::V2c => self.handle_v2c(data, source).await,
932 Version::V3 => self.handle_v3(data, source).await,
933 }
934 }
935}
936
937impl Clone for NotificationReceiver {
938 fn clone(&self) -> Self {
939 Self {
940 inner: Arc::clone(&self.inner),
941 }
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use crate::message::SecurityLevel;
949 use crate::oid;
950 use crate::pdu::GenericTrap;
951 use crate::v3::AuthProtocol;
952
953 #[test]
954 fn test_notification_trap_v1() {
955 let trap = TrapV1Pdu::new(
956 oid!(1, 3, 6, 1, 4, 1, 9999),
957 [192, 168, 1, 1],
958 GenericTrap::LinkDown,
959 0,
960 12345,
961 vec![],
962 );
963
964 let notification = Notification::TrapV1 {
965 community: Bytes::from_static(b"public"),
966 trap,
967 };
968
969 assert!(!notification.is_confirmed());
970 assert_eq!(notification.version(), Version::V1);
971 assert_eq!(notification.uptime(), 12345);
972 assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
973 }
974
975 #[test]
976 fn test_notification_trap_v2c() {
977 let notification = Notification::TrapV2c {
978 community: Bytes::from_static(b"public"),
979 uptime: 54321,
980 trap_oid: oids::link_up(),
981 varbinds: vec![],
982 request_id: 1,
983 };
984
985 assert!(!notification.is_confirmed());
986 assert_eq!(notification.version(), Version::V2c);
987 assert_eq!(notification.uptime(), 54321);
988 assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
989 }
990
991 #[test]
992 fn test_notification_inform() {
993 let notification = Notification::InformV2c {
994 community: Bytes::from_static(b"public"),
995 uptime: 11111,
996 trap_oid: oids::cold_start(),
997 varbinds: vec![],
998 request_id: 42,
999 };
1000
1001 assert!(notification.is_confirmed());
1002 assert_eq!(notification.version(), Version::V2c);
1003 }
1004
1005 #[test]
1006 fn test_notification_receiver_builder_default() {
1007 let builder = NotificationReceiverBuilder::new();
1008 assert_eq!(builder.bind_addr, "0.0.0.0:162");
1009 assert!(builder.usm_users.is_empty());
1010 }
1011
1012 #[test]
1013 fn test_notification_receiver_builder_with_user() {
1014 let builder = NotificationReceiverBuilder::new()
1015 .bind("0.0.0.0:1162")
1016 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
1017
1018 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
1019 assert_eq!(builder.usm_users.len(), 1);
1020
1021 let user = builder
1022 .usm_users
1023 .get(&Bytes::from_static(b"trapuser"))
1024 .unwrap();
1025 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
1026 }
1027
1028 #[tokio::test]
1029 async fn test_v3_receiver_requires_authoritative_engine() {
1030 let result = NotificationReceiver::builder()
1031 .bind("127.0.0.1:0")
1032 .usm_user("user", |user| user)
1033 .build()
1034 .await;
1035
1036 let err = result.err().expect("expected build to fail");
1037 assert!(matches!(*err, Error::Config(_)));
1038 }
1039
1040 #[test]
1041 fn test_notification_v3_inform() {
1042 let notification = Notification::InformV3 {
1043 username: Bytes::from_static(b"testuser"),
1044 context_engine_id: Bytes::from_static(b"engine123"),
1045 context_name: Bytes::new(),
1046 security_level: SecurityLevel::AuthNoPriv,
1047 uptime: 99999,
1048 trap_oid: oids::warm_start(),
1049 varbinds: vec![],
1050 request_id: 100,
1051 };
1052
1053 assert!(notification.is_confirmed());
1054 assert_eq!(notification.version(), Version::V3);
1055 assert_eq!(notification.uptime(), 99999);
1056 assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
1057 }
1058
1059 #[test]
1060 fn test_notification_security_level_accessor() {
1061 let trap_v3 = Notification::TrapV3 {
1062 username: Bytes::from_static(b"testuser"),
1063 context_engine_id: Bytes::from_static(b"engine123"),
1064 context_name: Bytes::new(),
1065 security_level: SecurityLevel::AuthPriv,
1066 uptime: 1,
1067 trap_oid: oids::cold_start(),
1068 varbinds: vec![],
1069 request_id: 1,
1070 };
1071 assert_eq!(trap_v3.security_level(), Some(SecurityLevel::AuthPriv));
1072
1073 let inform_v3 = Notification::InformV3 {
1074 username: Bytes::from_static(b"testuser"),
1075 context_engine_id: Bytes::from_static(b"engine123"),
1076 context_name: Bytes::new(),
1077 security_level: SecurityLevel::NoAuthNoPriv,
1078 uptime: 1,
1079 trap_oid: oids::cold_start(),
1080 varbinds: vec![],
1081 request_id: 1,
1082 };
1083 assert_eq!(
1084 inform_v3.security_level(),
1085 Some(SecurityLevel::NoAuthNoPriv)
1086 );
1087
1088 let trap_v2c = Notification::TrapV2c {
1089 community: Bytes::from_static(b"public"),
1090 uptime: 1,
1091 trap_oid: oids::cold_start(),
1092 varbinds: vec![],
1093 request_id: 1,
1094 };
1095 assert_eq!(trap_v2c.security_level(), None);
1096 }
1097
1098 #[test]
1099 fn test_notification_trap_v1_enterprise_specific_oid() {
1100 let trap = TrapV1Pdu::new(
1101 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1102 [192, 168, 1, 1],
1103 GenericTrap::EnterpriseSpecific,
1104 42,
1105 12345,
1106 vec![],
1107 );
1108
1109 let notification = Notification::TrapV1 {
1110 community: Bytes::from_static(b"public"),
1111 trap,
1112 };
1113
1114 assert_eq!(
1115 notification.trap_oid().unwrap(),
1116 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1117 );
1118 }
1119
1120 #[test]
1121 fn test_compute_engine_boots_time_basic() {
1122 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
1123 assert_eq!(boots, 1);
1124 assert_eq!(time, 1000);
1125 }
1126
1127 #[test]
1128 fn test_compute_engine_boots_time_zero_elapsed() {
1129 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
1130 assert_eq!(boots, 1);
1131 assert_eq!(time, 0);
1132 }
1133
1134 #[test]
1135 fn test_builder_authoritative_engine_default() {
1136 let builder = NotificationReceiverBuilder::new();
1137 assert!(builder.authoritative_engine.is_none());
1138 }
1139
1140 #[test]
1141 fn test_builder_authoritative_engine_custom() {
1142 let engine = AuthoritativeEngine::for_test(b"test-engine".to_vec(), 5);
1143 let builder = NotificationReceiverBuilder::new().authoritative_engine(engine);
1144 assert_eq!(builder.authoritative_engine.unwrap().engine_boots(), 5);
1145 }
1146
1147 fn build_v3_notification(
1152 pdu_type: crate::pdu::PduType,
1153 engine_id: &[u8],
1154 engine_boots: u32,
1155 engine_time: u32,
1156 username: &[u8],
1157 auth: Option<(&[u8], AuthProtocol)>,
1158 ) -> Bytes {
1159 build_v3_notification_with_max(
1160 pdu_type,
1161 engine_id,
1162 engine_boots,
1163 engine_time,
1164 username,
1165 auth,
1166 65507,
1167 )
1168 }
1169
1170 fn build_v3_notification_with_max(
1173 pdu_type: crate::pdu::PduType,
1174 engine_id: &[u8],
1175 engine_boots: u32,
1176 engine_time: u32,
1177 username: &[u8],
1178 auth: Option<(&[u8], AuthProtocol)>,
1179 msg_max_size: i32,
1180 ) -> Bytes {
1181 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1182 use crate::pdu::Pdu;
1183 use crate::v3::auth::authenticate_message;
1184 use crate::v3::{LocalizedKey, UsmSecurityParams};
1185 use crate::value::Value;
1186
1187 let auth_key = auth.map(|(password, protocol)| {
1188 LocalizedKey::from_password(protocol, password, engine_id).unwrap()
1189 });
1190
1191 let pdu = Pdu {
1193 pdu_type,
1194 request_id: 1,
1195 error_status: 0,
1196 error_index: 0,
1197 varbinds: vec![
1198 VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
1199 VarBind::new(
1200 oids::snmp_trap_oid(),
1201 Value::ObjectIdentifier(oids::cold_start()),
1202 ),
1203 ],
1204 };
1205
1206 let level = if auth_key.is_some() {
1207 SecurityLevel::AuthNoPriv
1208 } else {
1209 SecurityLevel::NoAuthNoPriv
1210 };
1211 let reportable = pdu_type == crate::pdu::PduType::InformRequest;
1214 let global = MsgGlobalData::new(1, msg_max_size, MsgFlags::new(level, reportable));
1215
1216 let mut usm_params = UsmSecurityParams::new(
1217 Bytes::copy_from_slice(engine_id),
1218 engine_boots,
1219 engine_time,
1220 Bytes::copy_from_slice(username),
1221 );
1222 if let Some(key) = &auth_key {
1223 usm_params = usm_params.with_auth_placeholder(key.mac_len());
1224 }
1225
1226 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
1227 let msg = V3Message::new(global, usm_params.encode(), scoped);
1228 let mut msg_bytes = msg.encode().to_vec();
1229
1230 if let Some(key) = &auth_key {
1232 let (auth_offset, auth_len) =
1233 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1234 authenticate_message(key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1235 }
1236
1237 Bytes::from(msg_bytes)
1238 }
1239
1240 fn build_authed_v3_inform(
1243 engine_id: &[u8],
1244 engine_boots: u32,
1245 engine_time: u32,
1246 username: &[u8],
1247 auth_password: &[u8],
1248 auth_protocol: AuthProtocol,
1249 ) -> Bytes {
1250 build_v3_notification(
1251 crate::pdu::PduType::InformRequest,
1252 engine_id,
1253 engine_boots,
1254 engine_time,
1255 username,
1256 Some((auth_password, auth_protocol)),
1257 )
1258 }
1259
1260 fn build_authed_v3_trap(engine_id: &[u8], engine_boots: u32, engine_time: u32) -> Bytes {
1263 build_v3_notification(
1264 crate::pdu::PduType::TrapV2,
1265 engine_id,
1266 engine_boots,
1267 engine_time,
1268 b"trapuser",
1269 Some((b"authpass12345678", AuthProtocol::Sha1)),
1270 )
1271 }
1272
1273 fn build_noauth_v3_trap(engine_id: &[u8], username: &[u8]) -> Bytes {
1275 build_v3_notification(crate::pdu::PduType::TrapV2, engine_id, 0, 0, username, None)
1276 }
1277
1278 async fn remote_trap_receiver() -> NotificationReceiver {
1281 NotificationReceiver::builder()
1282 .bind("127.0.0.1:0")
1283 .engine_id(b"my-receiver-engine".to_vec())
1284 .engine_boots(1)
1285 .usm_user("trapuser", |u| {
1286 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1287 })
1288 .build()
1289 .await
1290 .unwrap()
1291 }
1292
1293 #[tokio::test]
1301 async fn test_v3_trap_from_remote_sender_engine_accepted() {
1302 let receiver = remote_trap_receiver().await;
1303 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1304
1305 let msg = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1307
1308 let result = receiver.handle_v3(msg, source).await.unwrap();
1309 match result {
1310 Some(Notification::TrapV3 {
1311 username,
1312 security_level,
1313 ..
1314 }) => {
1315 assert_eq!(username.as_ref(), b"trapuser");
1316 assert_eq!(security_level, SecurityLevel::AuthNoPriv);
1317 }
1318 other => panic!("expected TrapV3, got {other:?}"),
1319 }
1320 }
1321
1322 #[tokio::test]
1326 async fn test_v3_noauth_trap_carries_security_level() {
1327 let receiver = remote_trap_receiver().await;
1328 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1329
1330 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"trapuser");
1331 match receiver.handle_v3(msg, source).await.unwrap() {
1332 Some(Notification::TrapV3 {
1333 security_level,
1334 username,
1335 ..
1336 }) => {
1337 assert_eq!(security_level, SecurityLevel::NoAuthNoPriv);
1338 assert_eq!(username.as_ref(), b"trapuser");
1339 }
1340 other => panic!("expected TrapV3, got {other:?}"),
1341 }
1342 }
1343
1344 #[tokio::test]
1349 async fn test_v3_noauth_trap_unknown_user_rejected_and_counted() {
1350 let receiver = remote_trap_receiver().await;
1351 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1352
1353 let msg = build_noauth_v3_trap(b"remote-sender-engine", b"nosuchuser");
1354 let result = receiver.handle_v3(msg, source).await.unwrap();
1355 assert!(result.is_none(), "unknown user must not be delivered");
1356 assert_eq!(receiver.usm_unknown_usernames(), 1);
1357 }
1358
1359 #[tokio::test]
1362 async fn test_v3_traps_from_multiple_remote_engines_accepted() {
1363 let receiver = remote_trap_receiver().await;
1364 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1365
1366 let msg_a = build_authed_v3_trap(b"sender-engine-a", 7, 123_456);
1367 let msg_b = build_authed_v3_trap(b"sender-engine-b", 2, 42);
1368
1369 assert!(
1370 receiver.handle_v3(msg_a, source).await.unwrap().is_some(),
1371 "trap from first remote engine should be accepted"
1372 );
1373 assert!(
1374 receiver.handle_v3(msg_b, source).await.unwrap().is_some(),
1375 "trap from second remote engine should be accepted"
1376 );
1377 }
1378
1379 #[tokio::test]
1384 async fn test_v3_remote_engines_table_bounded() {
1385 let receiver = remote_trap_receiver().await;
1386 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1387
1388 {
1390 let mut engines = receiver.inner.remote_engines.lock().unwrap();
1391 for i in 0..MAX_REMOTE_ENGINES {
1392 let id = Bytes::from(format!("dummy-engine-{i}"));
1393 engines.insert(id.clone(), EngineState::new(id, 1, 1));
1394 }
1395 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1396 }
1397
1398 let msg = build_authed_v3_trap(b"fresh-remote-engine", 7, 123_456);
1400 assert!(receiver.handle_v3(msg, source).await.unwrap().is_some());
1401
1402 let engines = receiver.inner.remote_engines.lock().unwrap();
1405 assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1406 assert!(engines.contains_key(&Bytes::from_static(b"fresh-remote-engine")));
1407 }
1408
1409 #[tokio::test]
1414 async fn test_v3_trap_remote_engine_stale_time_rejected() {
1415 let receiver = remote_trap_receiver().await;
1416 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1417
1418 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1419 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1420
1421 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1423 assert!(
1424 receiver.handle_v3(stale, source).await.is_err(),
1425 "stale engine time should be rejected as outside the time window"
1426 );
1427 }
1428
1429 #[tokio::test]
1431 async fn test_v3_trap_remote_engine_old_boots_rejected() {
1432 let receiver = remote_trap_receiver().await;
1433 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1434
1435 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1436 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1437
1438 let old_boots = build_authed_v3_trap(b"remote-sender-engine", 6, 99_999);
1439 assert!(
1440 receiver.handle_v3(old_boots, source).await.is_err(),
1441 "older boot cycle should be rejected"
1442 );
1443 }
1444
1445 #[tokio::test]
1448 async fn test_v3_trap_remote_engine_reboot_accepted() {
1449 let receiver = remote_trap_receiver().await;
1450 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1451
1452 let before = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1453 assert!(receiver.handle_v3(before, source).await.unwrap().is_some());
1454
1455 let after_reboot = build_authed_v3_trap(b"remote-sender-engine", 8, 5);
1456 assert!(
1457 receiver
1458 .handle_v3(after_reboot, source)
1459 .await
1460 .unwrap()
1461 .is_some(),
1462 "trap after sender reboot should be accepted"
1463 );
1464
1465 let from_old_cycle = build_authed_v3_trap(b"remote-sender-engine", 7, 20_000);
1466 assert!(
1467 receiver.handle_v3(from_old_cycle, source).await.is_err(),
1468 "trap from superseded boot cycle should be rejected"
1469 );
1470 }
1471
1472 #[tokio::test]
1475 async fn test_v3_trap_remote_engine_bad_auth_rejected() {
1476 let receiver = remote_trap_receiver().await;
1477 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1478
1479 let msg = build_v3_notification(
1480 crate::pdu::PduType::TrapV2,
1481 b"remote-sender-engine",
1482 7,
1483 123_456,
1484 b"trapuser",
1485 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1486 );
1487 assert!(
1488 receiver.handle_v3(msg, source).await.is_err(),
1489 "trap with wrong auth key should be rejected"
1490 );
1491
1492 let good = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1494 assert!(receiver.handle_v3(good, source).await.unwrap().is_some());
1495 }
1496
1497 #[tokio::test]
1498 async fn test_v3_inform_outside_time_window_rejected() {
1499 let receiver = NotificationReceiver::builder()
1500 .bind("127.0.0.1:0")
1501 .engine_id(b"test-engine".to_vec())
1502 .engine_boots(1)
1503 .usm_user("informuser", |u| {
1504 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1505 })
1506 .build()
1507 .await
1508 .unwrap();
1509
1510 let engine_id = b"test-engine";
1511 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1512
1513 let msg = build_authed_v3_inform(
1515 engine_id,
1516 1, 5000, b"informuser",
1519 b"authpass12345678",
1520 AuthProtocol::Sha1,
1521 );
1522
1523 let result = receiver.handle_v3(msg, source).await;
1524 assert!(
1525 result.is_err(),
1526 "message with engine_time=5000 should be rejected (outside 150s window)"
1527 );
1528 }
1529
1530 #[tokio::test]
1531 async fn test_v3_inform_wrong_boots_rejected() {
1532 let receiver = NotificationReceiver::builder()
1533 .bind("127.0.0.1:0")
1534 .engine_id(b"test-engine".to_vec())
1535 .engine_boots(1)
1536 .usm_user("informuser", |u| {
1537 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1538 })
1539 .build()
1540 .await
1541 .unwrap();
1542
1543 let engine_id = b"test-engine";
1544 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1545
1546 let msg = build_authed_v3_inform(
1548 engine_id,
1549 2, 0, b"informuser",
1552 b"authpass12345678",
1553 AuthProtocol::Sha1,
1554 );
1555
1556 let result = receiver.handle_v3(msg, source).await;
1557 assert!(
1558 result.is_err(),
1559 "message with wrong engine_boots should be rejected"
1560 );
1561 }
1562
1563 #[tokio::test]
1564 async fn test_v3_inform_within_time_window_accepted() {
1565 let receiver = NotificationReceiver::builder()
1566 .bind("127.0.0.1:0")
1567 .engine_id(b"test-engine".to_vec())
1568 .engine_boots(1)
1569 .usm_user("informuser", |u| {
1570 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1571 })
1572 .build()
1573 .await
1574 .unwrap();
1575
1576 let engine_id = b"test-engine";
1577 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1578
1579 let msg = build_authed_v3_inform(
1581 engine_id,
1582 1, 0, b"informuser",
1585 b"authpass12345678",
1586 AuthProtocol::Sha1,
1587 );
1588
1589 let result = receiver.handle_v3(msg, source).await;
1590 match result {
1596 Ok(Some(_)) => {} Err(e) => {
1598 let err_str = format!("{e}");
1599 assert!(
1600 !err_str.contains("Auth"),
1601 "should not be an auth error for valid time window, got: {err_str}"
1602 );
1603 }
1604 Ok(None) => panic!("should not return None for a valid InformRequest"),
1605 }
1606 }
1607
1608 fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
1610 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1611 use crate::pdu::{Pdu, PduType};
1612 use crate::v3::UsmSecurityParams;
1613
1614 let pdu = Pdu {
1615 pdu_type: PduType::GetRequest,
1616 request_id: 0,
1617 error_status: 0,
1618 error_index: 0,
1619 varbinds: vec![],
1620 };
1621
1622 let global = MsgGlobalData::new(
1623 msg_id,
1624 65507,
1625 MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
1626 );
1627
1628 let usm_params = UsmSecurityParams::new(
1629 Bytes::new(), 0,
1631 0,
1632 Bytes::new(), );
1634
1635 let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
1636 let msg = V3Message::new(global, usm_params.encode(), scoped);
1637 msg.encode()
1638 }
1639
1640 #[tokio::test]
1641 async fn test_v3_discovery_gets_response() {
1642 use crate::message::V3Message;
1643 use crate::v3::UsmSecurityParams;
1644 use crate::value::Value;
1645
1646 let receiver = NotificationReceiver::builder()
1647 .bind("127.0.0.1:0")
1648 .engine_id(b"test-discovery-engine".to_vec())
1649 .build()
1650 .await
1651 .unwrap();
1652
1653 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1656 let client_addr = client.local_addr().unwrap();
1657
1658 let discovery_msg = build_v3_discovery_request(42, true);
1659 let result = receiver.handle_v3(discovery_msg, client_addr).await;
1660
1661 assert!(result.is_ok());
1663 assert!(result.unwrap().is_none());
1664
1665 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1667
1668 let mut buf = vec![0u8; 4096];
1671 let (len, _) = tokio::time::timeout(
1672 std::time::Duration::from_secs(1),
1673 client.recv_from(&mut buf),
1674 )
1675 .await
1676 .expect("expected a discovery Report")
1677 .unwrap();
1678
1679 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1680 assert_eq!(
1681 report.global_data.msg_flags.security_level,
1682 SecurityLevel::NoAuthNoPriv
1683 );
1684 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1685 assert_eq!(report_usm.engine_id.as_ref(), b"test-discovery-engine");
1686 let scoped = report.scoped_pdu().expect("report should be plaintext");
1687 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1688 assert_eq!(
1689 scoped.pdu.varbinds[0].oid,
1690 crate::v3::report_oids::unknown_engine_ids()
1691 );
1692 assert_eq!(scoped.pdu.varbinds[0].value, Value::Counter32(1));
1693 }
1694
1695 #[tokio::test]
1696 async fn test_v3_discovery_non_reportable_ignored() {
1697 let receiver = NotificationReceiver::builder()
1698 .bind("127.0.0.1:0")
1699 .engine_id(b"test-discovery-engine".to_vec())
1700 .build()
1701 .await
1702 .unwrap();
1703
1704 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1705 let discovery_msg = build_v3_discovery_request(42, false);
1706
1707 let result = receiver.handle_v3(discovery_msg, source).await;
1708
1709 assert!(result.is_ok());
1713 assert!(result.unwrap().is_none());
1714 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1715 }
1716
1717 #[tokio::test]
1723 async fn test_v3_inform_under_remote_engine_id_rejected() {
1724 let receiver = NotificationReceiver::builder()
1725 .bind("127.0.0.1:0")
1726 .engine_id(b"my-receiver-engine".to_vec())
1727 .engine_boots(1)
1728 .usm_user("informuser", |u| {
1729 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1730 })
1731 .build()
1732 .await
1733 .unwrap();
1734
1735 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1736
1737 let msg = build_authed_v3_inform(
1739 b"remote-engine-id",
1740 1,
1741 0,
1742 b"informuser",
1743 b"authpass12345678",
1744 AuthProtocol::Sha1,
1745 );
1746
1747 let result = receiver.handle_v3(msg, source).await.unwrap();
1748 assert!(
1749 result.is_none(),
1750 "inform under a foreign authoritative engine ID should be dropped, got {result:?}"
1751 );
1752 }
1753
1754 #[tokio::test]
1758 async fn test_v3_inform_under_local_engine_id_accepted() {
1759 let receiver = NotificationReceiver::builder()
1760 .bind("127.0.0.1:0")
1761 .engine_id(b"my-receiver-engine".to_vec())
1762 .engine_boots(1)
1763 .usm_user("informuser", |u| {
1764 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1765 })
1766 .build()
1767 .await
1768 .unwrap();
1769
1770 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1771
1772 let msg = build_authed_v3_inform(
1774 b"my-receiver-engine",
1775 1,
1776 0,
1777 b"informuser",
1778 b"authpass12345678",
1779 AuthProtocol::Sha1,
1780 );
1781
1782 let result = receiver.handle_v3(msg, source).await.unwrap();
1783 assert!(
1784 matches!(result, Some(Notification::InformV3 { .. })),
1785 "inform under the local engine ID should be accepted, got {result:?}"
1786 );
1787 }
1788
1789 #[tokio::test]
1792 async fn test_v3_inform_ack_advertises_local_max_size() {
1793 let receiver = NotificationReceiver::builder()
1794 .bind("127.0.0.1:0")
1795 .engine_id(b"my-receiver-engine".to_vec())
1796 .engine_boots(1)
1797 .usm_user("informuser", |u| {
1798 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1799 })
1800 .build()
1801 .await
1802 .unwrap();
1803
1804 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1805 let client_addr = client.local_addr().unwrap();
1806
1807 let msg = build_v3_notification_with_max(
1811 crate::pdu::PduType::InformRequest,
1812 b"my-receiver-engine",
1813 1,
1814 0,
1815 b"informuser",
1816 Some((b"authpass12345678", AuthProtocol::Sha1)),
1817 1400,
1818 );
1819
1820 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
1821 assert!(
1822 matches!(result, Some(Notification::InformV3 { .. })),
1823 "inform should be accepted, got {result:?}"
1824 );
1825
1826 let mut buf = vec![0u8; 4096];
1827 let (len, _) = tokio::time::timeout(
1828 std::time::Duration::from_secs(1),
1829 client.recv_from(&mut buf),
1830 )
1831 .await
1832 .expect("expected the inform acknowledgement")
1833 .unwrap();
1834
1835 use crate::message::V3Message;
1836 let ack = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1837 assert_eq!(
1838 ack.global_data.msg_max_size,
1839 crate::v3::DEFAULT_MSG_MAX_SIZE as i32,
1840 "ack must advertise the receiver's local receive capacity, not the sender's 1400"
1841 );
1842 }
1843
1844 #[tokio::test]
1848 async fn test_v3_inform_ack_uses_current_authoritative_time() {
1849 use crate::message::V3Message;
1850 use crate::v3::UsmSecurityParams;
1851
1852 let receiver = NotificationReceiver::builder()
1853 .bind("127.0.0.1:0")
1854 .engine_id(b"my-receiver-engine".to_vec())
1855 .engine_boots(7)
1856 .usm_user("informuser", |u| {
1857 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1858 })
1859 .build()
1860 .await
1861 .unwrap();
1862
1863 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1864 let client_addr = client.local_addr().unwrap();
1865
1866 let incoming_time = 149;
1869 let msg = build_authed_v3_inform(
1870 b"my-receiver-engine",
1871 7,
1872 incoming_time,
1873 b"informuser",
1874 b"authpass12345678",
1875 AuthProtocol::Sha1,
1876 );
1877
1878 let earliest = receiver.inner.authoritative_boots_time().unwrap();
1879 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
1880 let latest = receiver.inner.authoritative_boots_time().unwrap();
1881 assert!(matches!(result, Some(Notification::InformV3 { .. })));
1882
1883 let mut buf = vec![0u8; 4096];
1884 let (len, _) = tokio::time::timeout(
1885 std::time::Duration::from_secs(1),
1886 client.recv_from(&mut buf),
1887 )
1888 .await
1889 .expect("expected the inform acknowledgement")
1890 .unwrap();
1891
1892 let ack = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1893 let ack_usm = UsmSecurityParams::decode(ack.security_params).unwrap();
1894 let ack_pair = (ack_usm.engine_boots, ack_usm.engine_time);
1895
1896 assert_eq!(ack_usm.engine_id.as_ref(), receiver.engine_id());
1897 assert_ne!(ack_usm.engine_time, incoming_time);
1898 assert_eq!(ack_pair.0, 7);
1899 assert!(
1900 ack_pair.1 >= earliest.1 && ack_pair.1 <= latest.1,
1901 "ack pair {ack_pair:?} should come from one current elapsed-time sample between {earliest:?} and {latest:?}"
1902 );
1903 }
1904
1905 #[test]
1906 fn test_auto_generated_engine_id_non_empty() {
1907 let builder = NotificationReceiverBuilder::new();
1908 assert!(builder.authoritative_engine.is_none());
1909 }
1910
1911 #[tokio::test]
1912 async fn test_bind_generates_engine_id() {
1913 let first = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1914 let second = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1915
1916 crate::v3::validate_engine_id(first.engine_id()).unwrap();
1917 crate::v3::validate_engine_id(second.engine_id()).unwrap();
1918 assert_eq!(first.engine_id()[0], 0x80);
1920 assert_ne!(first.engine_id(), second.engine_id());
1921 }
1922
1923 #[tokio::test]
1924 async fn test_builder_generates_engine_id() {
1925 let receiver = NotificationReceiver::builder()
1926 .bind("127.0.0.1:0")
1927 .build()
1928 .await
1929 .unwrap();
1930 assert!(!receiver.engine_id().is_empty());
1931 assert_eq!(receiver.engine_id()[0], 0x80);
1932 }
1933
1934 #[tokio::test]
1935 async fn test_builder_custom_engine_id() {
1936 let receiver = NotificationReceiver::builder()
1937 .bind("127.0.0.1:0")
1938 .engine_id(b"custom-engine".to_vec())
1939 .build()
1940 .await
1941 .unwrap();
1942 assert_eq!(receiver.engine_id(), b"custom-engine");
1943 }
1944
1945 #[tokio::test]
1946 async fn test_usm_counter_accessors_default_zero() {
1947 let receiver = remote_trap_receiver().await;
1948 assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1949 assert_eq!(receiver.usm_unknown_usernames(), 0);
1950 assert_eq!(receiver.usm_wrong_digests(), 0);
1951 assert_eq!(receiver.usm_not_in_time_windows(), 0);
1952 assert_eq!(receiver.usm_unsupported_sec_levels(), 0);
1953 assert_eq!(receiver.usm_decryption_errors(), 0);
1954 }
1955
1956 #[tokio::test]
1959 async fn test_v3_trap_wrong_digest_increments_counter() {
1960 let receiver = remote_trap_receiver().await;
1961 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1962
1963 let msg = build_v3_notification(
1964 crate::pdu::PduType::TrapV2,
1965 b"remote-sender-engine",
1966 7,
1967 123_456,
1968 b"trapuser",
1969 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1970 );
1971 assert!(receiver.handle_v3(msg, source).await.is_err());
1972 assert_eq!(receiver.usm_wrong_digests(), 1);
1973 }
1974
1975 #[tokio::test]
1978 async fn test_v3_trap_unknown_user_increments_counter() {
1979 let receiver = remote_trap_receiver().await;
1980 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1981
1982 let msg = build_v3_notification(
1983 crate::pdu::PduType::TrapV2,
1984 b"remote-sender-engine",
1985 7,
1986 123_456,
1987 b"nosuchuser",
1988 Some((b"authpass12345678", AuthProtocol::Sha1)),
1989 );
1990 let result = receiver.handle_v3(msg, source).await.unwrap();
1991 assert!(result.is_none(), "unknown user must not be delivered");
1992 assert_eq!(receiver.usm_unknown_usernames(), 1);
1993 assert_eq!(receiver.usm_wrong_digests(), 0);
1994 }
1995
1996 #[tokio::test]
2000 async fn test_v3_trap_user_without_auth_key_increments_counter() {
2001 let receiver = NotificationReceiver::builder()
2002 .bind("127.0.0.1:0")
2003 .engine_id(b"my-receiver-engine".to_vec())
2004 .usm_user("plainuser", |u| u)
2005 .build()
2006 .await
2007 .unwrap();
2008 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2009
2010 let msg = build_v3_notification(
2011 crate::pdu::PduType::TrapV2,
2012 b"remote-sender-engine",
2013 7,
2014 123_456,
2015 b"plainuser",
2016 Some((b"authpass12345678", AuthProtocol::Sha1)),
2017 );
2018 let result = receiver.handle_v3(msg, source).await.unwrap();
2019 assert!(result.is_none());
2020 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
2021 assert_eq!(receiver.usm_unknown_usernames(), 0);
2022 }
2023
2024 #[tokio::test]
2027 async fn test_v3_inform_time_window_failure_increments_counter() {
2028 let receiver = NotificationReceiver::builder()
2029 .bind("127.0.0.1:0")
2030 .engine_id(b"test-engine".to_vec())
2031 .engine_boots(1)
2032 .usm_user("informuser", |u| {
2033 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2034 })
2035 .build()
2036 .await
2037 .unwrap();
2038 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2039
2040 let msg = build_authed_v3_inform(
2041 b"test-engine",
2042 1,
2043 5000,
2044 b"informuser",
2045 b"authpass12345678",
2046 AuthProtocol::Sha1,
2047 );
2048 assert!(receiver.handle_v3(msg, source).await.is_err());
2049 assert_eq!(receiver.usm_not_in_time_windows(), 1);
2050 }
2051
2052 #[tokio::test]
2058 async fn test_v3_trap_remote_stale_not_counted() {
2059 let receiver = remote_trap_receiver().await;
2060 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2061
2062 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
2063 assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
2064
2065 let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
2066 assert!(receiver.handle_v3(stale, source).await.is_err());
2067 assert_eq!(receiver.usm_not_in_time_windows(), 0);
2068 }
2069
2070 #[tokio::test]
2078 async fn test_v3_inform_remote_stale_gets_no_report() {
2079 let receiver = remote_trap_receiver().await;
2080
2081 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2082 let client_addr = client.local_addr().unwrap();
2083
2084 let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
2088 assert!(
2089 receiver
2090 .handle_v3(fresh, client_addr)
2091 .await
2092 .unwrap()
2093 .is_some()
2094 );
2095
2096 let mut buf = vec![0u8; 4096];
2097
2098 let stale = build_v3_notification(
2099 crate::pdu::PduType::InformRequest,
2100 b"remote-sender-engine",
2101 7,
2102 5_000,
2103 b"trapuser",
2104 Some((b"authpass12345678", AuthProtocol::Sha1)),
2105 );
2106 assert!(receiver.handle_v3(stale, client_addr).await.is_err());
2107 assert_eq!(receiver.usm_not_in_time_windows(), 0);
2108
2109 let result = tokio::time::timeout(
2110 std::time::Duration::from_millis(200),
2111 client.recv_from(&mut buf),
2112 )
2113 .await;
2114 assert!(
2115 result.is_err(),
2116 "no Report may be sent for a Step 7b timeliness failure"
2117 );
2118 }
2119
2120 fn build_v3_trap_bad_ciphertext(
2124 engine_id: &[u8],
2125 username: &[u8],
2126 auth_password: &[u8],
2127 ) -> Bytes {
2128 use crate::message::{MsgFlags, MsgGlobalData, V3Message};
2129 use crate::v3::auth::authenticate_message;
2130 use crate::v3::{LocalizedKey, UsmSecurityParams};
2131
2132 let auth_key =
2133 LocalizedKey::from_password(AuthProtocol::Sha1, auth_password, engine_id).unwrap();
2134
2135 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthPriv, false));
2136 let usm_params = UsmSecurityParams::new(
2137 Bytes::copy_from_slice(engine_id),
2138 7,
2139 123_456,
2140 Bytes::copy_from_slice(username),
2141 )
2142 .with_auth_placeholder(auth_key.mac_len())
2143 .with_priv_params(Bytes::from_static(b"bad"));
2144
2145 let msg = V3Message::new_encrypted(
2146 global,
2147 usm_params.encode(),
2148 Bytes::from_static(b"not-a-valid-ciphertext"),
2149 );
2150 let mut msg_bytes = msg.encode().to_vec();
2151 let (auth_offset, auth_len) =
2152 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
2153 authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
2154 Bytes::from(msg_bytes)
2155 }
2156
2157 #[tokio::test]
2160 async fn test_v3_decryption_error_increments_counter() {
2161 let receiver = NotificationReceiver::builder()
2162 .bind("127.0.0.1:0")
2163 .engine_id(b"my-receiver-engine".to_vec())
2164 .usm_user("privuser", |u| {
2165 u.auth_priv(
2166 AuthProtocol::Sha1,
2167 b"authpass12345678",
2168 crate::v3::PrivProtocol::Aes128,
2169 b"privpass12345678",
2170 )
2171 })
2172 .build()
2173 .await
2174 .unwrap();
2175 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2176
2177 let msg =
2178 build_v3_trap_bad_ciphertext(b"remote-sender-engine", b"privuser", b"authpass12345678");
2179 assert!(receiver.handle_v3(msg, source).await.is_err());
2180 assert_eq!(receiver.usm_decryption_errors(), 1);
2181 }
2182
2183 #[tokio::test]
2188 async fn test_v3_authpriv_for_auth_only_user_counts_unsupported_sec_level() {
2189 let receiver = remote_trap_receiver().await;
2190 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2191
2192 let msg = build_v3_trap_bad_ciphertext(
2193 b"remote-sender-engine",
2194 b"trapuser",
2195 b"wrong-password-1234",
2196 );
2197 let result = receiver.handle_v3(msg, source).await.unwrap();
2198 assert!(result.is_none());
2199 assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
2200 assert_eq!(receiver.usm_wrong_digests(), 0);
2201 }
2202
2203 #[tokio::test]
2209 async fn test_v3_failed_inform_gets_authenticated_time_window_report() {
2210 use crate::message::V3Message;
2211 use crate::v3::auth::verify_message;
2212 use crate::v3::{LocalizedKey, UsmSecurityParams};
2213
2214 let receiver = NotificationReceiver::builder()
2215 .bind("127.0.0.1:0")
2216 .engine_id(b"test-engine".to_vec())
2217 .engine_boots(1)
2218 .usm_user("informuser", |u| {
2219 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2220 })
2221 .build()
2222 .await
2223 .unwrap();
2224
2225 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2226 let client_addr = client.local_addr().unwrap();
2227
2228 let msg = build_authed_v3_inform(
2229 b"test-engine",
2230 1,
2231 5000, b"informuser",
2233 b"authpass12345678",
2234 AuthProtocol::Sha1,
2235 );
2236 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2237
2238 let mut buf = vec![0u8; 4096];
2239 let (len, _) = tokio::time::timeout(
2240 std::time::Duration::from_secs(1),
2241 client.recv_from(&mut buf),
2242 )
2243 .await
2244 .expect("expected a Report in response to the failed inform")
2245 .unwrap();
2246 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2247
2248 let report = V3Message::decode(report_bytes.clone()).unwrap();
2249 assert_eq!(
2250 report.global_data.msg_flags.security_level,
2251 SecurityLevel::AuthNoPriv,
2252 "notInTimeWindows report must be authenticated (authNoPriv)"
2253 );
2254 assert!(!report.global_data.msg_flags.reportable);
2255
2256 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2257 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2258
2259 let key =
2262 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2263 .unwrap();
2264 let (auth_offset, auth_len) =
2265 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2266 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2267
2268 let scoped = report.scoped_pdu().expect("report should be plaintext");
2269 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2270 assert_eq!(
2271 scoped.pdu.varbinds[0].oid,
2272 crate::v3::report_oids::not_in_time_windows()
2273 );
2274 }
2275
2276 #[tokio::test]
2280 async fn test_v3_latched_boots_report_is_authenticated() {
2281 use crate::message::V3Message;
2282 use crate::v3::MAX_ENGINE_TIME;
2283 use crate::v3::auth::verify_message;
2284 use crate::v3::{LocalizedKey, UsmSecurityParams};
2285
2286 let receiver = NotificationReceiver::builder()
2287 .bind("127.0.0.1:0")
2288 .engine_id(b"test-engine".to_vec())
2289 .engine_boots(MAX_ENGINE_TIME)
2290 .usm_user("informuser", |u| {
2291 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2292 })
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 msg = build_authed_v3_inform(
2301 b"test-engine",
2302 MAX_ENGINE_TIME,
2303 0,
2304 b"informuser",
2305 b"authpass12345678",
2306 AuthProtocol::Sha1,
2307 );
2308 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2309 assert_eq!(receiver.usm_not_in_time_windows(), 1);
2310
2311 let mut buf = vec![0u8; 4096];
2312 let (len, _) = tokio::time::timeout(
2313 std::time::Duration::from_secs(1),
2314 client.recv_from(&mut buf),
2315 )
2316 .await
2317 .expect("expected a Report in response to the failed inform")
2318 .unwrap();
2319 let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2320
2321 let report = V3Message::decode(report_bytes.clone()).unwrap();
2322 assert_eq!(
2323 report.global_data.msg_flags.security_level,
2324 SecurityLevel::AuthNoPriv,
2325 "notInTimeWindows report must be authenticated (authNoPriv)"
2326 );
2327 let key =
2328 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2329 .unwrap();
2330 let (auth_offset, auth_len) =
2331 UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2332 assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2333
2334 let scoped = report.scoped_pdu().expect("report should be plaintext");
2335 assert_eq!(
2336 scoped.pdu.varbinds[0].oid,
2337 crate::v3::report_oids::not_in_time_windows()
2338 );
2339 }
2340
2341 #[tokio::test]
2344 async fn test_v3_failed_inform_unknown_user_gets_noauth_report() {
2345 use crate::message::V3Message;
2346 use crate::v3::UsmSecurityParams;
2347
2348 let receiver = NotificationReceiver::builder()
2349 .bind("127.0.0.1:0")
2350 .engine_id(b"test-engine".to_vec())
2351 .engine_boots(1)
2352 .usm_user("informuser", |u| {
2353 u.auth(AuthProtocol::Sha1, b"authpass12345678")
2354 })
2355 .build()
2356 .await
2357 .unwrap();
2358
2359 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2360 let client_addr = client.local_addr().unwrap();
2361
2362 let msg = build_authed_v3_inform(
2363 b"test-engine",
2364 1,
2365 0,
2366 b"nosuchuser",
2367 b"authpass12345678",
2368 AuthProtocol::Sha1,
2369 );
2370 let result = receiver.handle_v3(msg, client_addr).await.unwrap();
2371 assert!(result.is_none());
2372 assert_eq!(receiver.usm_unknown_usernames(), 1);
2373
2374 let mut buf = vec![0u8; 4096];
2375 let (len, _) = tokio::time::timeout(
2376 std::time::Duration::from_secs(1),
2377 client.recv_from(&mut buf),
2378 )
2379 .await
2380 .expect("expected a Report in response to the failed inform")
2381 .unwrap();
2382
2383 let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
2384 assert_eq!(
2385 report.global_data.msg_flags.security_level,
2386 SecurityLevel::NoAuthNoPriv
2387 );
2388 let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2389 assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2390 let scoped = report.scoped_pdu().expect("report should be plaintext");
2391 assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2392 assert_eq!(
2393 scoped.pdu.varbinds[0].oid,
2394 crate::v3::report_oids::unknown_user_names()
2395 );
2396 }
2397
2398 #[tokio::test]
2401 async fn test_v3_failed_trap_gets_no_report() {
2402 let receiver = remote_trap_receiver().await;
2403
2404 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2405 let client_addr = client.local_addr().unwrap();
2406
2407 let msg = build_v3_notification(
2408 crate::pdu::PduType::TrapV2,
2409 b"remote-sender-engine",
2410 7,
2411 123_456,
2412 b"trapuser",
2413 Some((b"wrong-password-1234", AuthProtocol::Sha1)),
2414 );
2415 assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2416 assert_eq!(receiver.usm_wrong_digests(), 1);
2417
2418 let mut buf = vec![0u8; 4096];
2419 let result = tokio::time::timeout(
2420 std::time::Duration::from_millis(200),
2421 client.recv_from(&mut buf),
2422 )
2423 .await;
2424 assert!(result.is_err(), "no Report may be sent for a failed trap");
2425 }
2426
2427 #[test]
2428 fn test_community_allowed() {
2429 assert!(community_allowed(&[], b"public"));
2431 assert!(community_allowed(&[], b""));
2432
2433 let configured = vec![b"public".to_vec(), b"monitor".to_vec()];
2434 assert!(community_allowed(&configured, b"public"));
2435 assert!(community_allowed(&configured, b"monitor"));
2436 assert!(!community_allowed(&configured, b"private"));
2438 assert!(!community_allowed(&configured, b"pub"));
2439 assert!(!community_allowed(&configured, b"publicx"));
2440 assert!(!community_allowed(&configured, b""));
2441 }
2442
2443 fn build_v2c_trap(community: &[u8]) -> Bytes {
2444 use crate::message::CommunityMessage;
2445 use crate::pdu::Pdu;
2446 let pdu = Pdu::trap_v2(1, 100, &oids::cold_start(), vec![]);
2447 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2448 }
2449
2450 fn build_v2c_inform(community: &[u8]) -> Bytes {
2451 use crate::message::CommunityMessage;
2452 use crate::pdu::Pdu;
2453 let pdu = Pdu::inform_request(1, 100, &oids::cold_start(), vec![]);
2454 CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2455 }
2456
2457 fn build_v1_trap(community: &[u8]) -> Bytes {
2458 use crate::message::CommunityMessage;
2459 use crate::pdu::GenericTrap;
2460 let trap = TrapV1Pdu::new(
2461 oid!(1, 3, 6, 1, 4, 1, 9999),
2462 [192, 168, 1, 1],
2463 GenericTrap::ColdStart,
2464 0,
2465 12345,
2466 vec![],
2467 );
2468 CommunityMessage::v1_trap(Bytes::copy_from_slice(community), trap).encode()
2469 }
2470
2471 #[tokio::test]
2472 async fn test_v2c_trap_matching_community_accepted() {
2473 let receiver = NotificationReceiver::builder()
2474 .bind("127.0.0.1:0")
2475 .community(b"public")
2476 .build()
2477 .await
2478 .unwrap();
2479 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2480
2481 let result = receiver
2482 .handle_v2c(build_v2c_trap(b"public"), source)
2483 .await
2484 .unwrap();
2485 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2486 }
2487
2488 #[tokio::test]
2489 async fn test_v2c_trap_wrong_community_dropped() {
2490 let receiver = NotificationReceiver::builder()
2491 .bind("127.0.0.1:0")
2492 .community(b"public")
2493 .build()
2494 .await
2495 .unwrap();
2496 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2497
2498 let result = receiver
2499 .handle_v2c(build_v2c_trap(b"private"), source)
2500 .await
2501 .unwrap();
2502 assert!(result.is_none());
2503 }
2504
2505 #[tokio::test]
2506 async fn test_v2c_trap_no_allowlist_accepts_any_community() {
2507 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
2508 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2509
2510 let result = receiver
2511 .handle_v2c(build_v2c_trap(b"anything"), source)
2512 .await
2513 .unwrap();
2514 assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2515 }
2516
2517 #[tokio::test]
2518 async fn test_v1_trap_wrong_community_dropped() {
2519 let receiver = NotificationReceiver::builder()
2520 .bind("127.0.0.1:0")
2521 .community(b"public")
2522 .build()
2523 .await
2524 .unwrap();
2525 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2526
2527 assert!(
2528 receiver
2529 .handle_v1(build_v1_trap(b"private"), source)
2530 .await
2531 .unwrap()
2532 .is_none()
2533 );
2534 assert!(matches!(
2535 receiver
2536 .handle_v1(build_v1_trap(b"public"), source)
2537 .await
2538 .unwrap(),
2539 Some(Notification::TrapV1 { .. })
2540 ));
2541 }
2542
2543 #[tokio::test]
2546 async fn test_v2c_inform_wrong_community_dropped_without_ack() {
2547 let receiver = NotificationReceiver::builder()
2548 .bind("127.0.0.1:0")
2549 .community(b"public")
2550 .build()
2551 .await
2552 .unwrap();
2553
2554 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2555 let client_addr = client.local_addr().unwrap();
2556
2557 let result = receiver
2558 .handle_v2c(build_v2c_inform(b"private"), client_addr)
2559 .await
2560 .unwrap();
2561 assert!(result.is_none());
2562
2563 let mut buf = vec![0u8; 4096];
2564 let recv = tokio::time::timeout(
2565 std::time::Duration::from_millis(200),
2566 client.recv_from(&mut buf),
2567 )
2568 .await;
2569 assert!(recv.is_err(), "a filtered inform must not be acknowledged");
2570 }
2571
2572 #[tokio::test]
2575 async fn test_v2c_inform_matching_community_acked() {
2576 let receiver = NotificationReceiver::builder()
2577 .bind("127.0.0.1:0")
2578 .community(b"public")
2579 .build()
2580 .await
2581 .unwrap();
2582
2583 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2584 let client_addr = client.local_addr().unwrap();
2585
2586 let result = receiver
2587 .handle_v2c(build_v2c_inform(b"public"), client_addr)
2588 .await
2589 .unwrap();
2590 assert!(matches!(result, Some(Notification::InformV2c { .. })));
2591
2592 let mut buf = vec![0u8; 4096];
2593 let (len, _) = tokio::time::timeout(
2594 std::time::Duration::from_secs(1),
2595 client.recv_from(&mut buf),
2596 )
2597 .await
2598 .expect("a matching inform must be acknowledged")
2599 .unwrap();
2600 assert!(len > 0);
2601 }
2602}