1use crate::ber::Decoder;
7use crate::error::internal::{AuthErrorKind, CryptoErrorKind};
8use crate::error::{Error, Result};
9use crate::format::hex;
10use crate::message::{RawMsgData, RawV3Message, ScopedPdu, SecurityLevel, V3Message};
11use crate::pdu::{Pdu, PduType};
12use crate::transport::Transport;
13use crate::v3::{
14 EngineCache, EngineState, ReportStatus, UsmSecurityParams, auth::verify_message,
15 classify_report,
16};
17use bytes::Bytes;
18use std::net::SocketAddr;
19use std::time::Instant;
20use tracing::{Span, instrument};
21
22use super::{Client, ClientEngine};
23
24struct EncodedV3Request {
25 data: Vec<u8>,
26 context_engine_id: Bytes,
27 context_name: Bytes,
28}
29
30struct PacketLocalEngineTime {
31 engine_id: Bytes,
32 boots: u32,
33 time: u32,
34}
35
36fn check_and_update_engine_timeliness(
37 state: &mut EngineState,
38 cache: Option<&EngineCache>,
39 target: SocketAddr,
40 engine_id: &[u8],
41 msg_boots: u32,
42 msg_time: u32,
43) -> bool {
44 if let Some(cache) = cache
45 && let Some((timely, cached_state)) =
46 cache.check_and_update_timeliness(&target, state, engine_id, msg_boots, msg_time)
47 {
48 state.merge_from(&cached_state);
49 return timely;
50 }
51
52 let timely = state.check_and_update_timeliness(msg_boots, msg_time);
53 if timely && let Some(cache) = cache {
54 cache.insert(target, state.clone());
55 }
56 timely
57}
58
59impl<T: Transport> Client<T> {
61 #[instrument(level = "debug", skip(self), fields(snmp.target = %self.peer_addr()))]
63 pub(super) async fn ensure_engine_discovered(&self) -> Result<()> {
64 {
66 let engine = self
67 .inner
68 .engine
69 .read()
70 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
71 if engine.is_some() {
72 return Ok(());
73 }
74 }
75
76 let _guard = self.inner.discovery_lock.lock().await;
79 self.discover_engine_locked(false).await
80 }
81
82 pub async fn rediscover_engine(&self) -> Result<()> {
99 if !self.is_v3() {
100 return Err(Error::Config("engine discovery requires SNMPv3".into()).boxed());
101 }
102
103 let _guard = self.inner.discovery_lock.lock().await;
104 self.discover_engine_locked(true).await
105 }
106
107 async fn discover_engine_locked(&self, replace_cached_identity: bool) -> Result<()> {
109 if !replace_cached_identity {
113 let engine = self
114 .inner
115 .engine
116 .read()
117 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
118 if engine.is_some() {
119 return Ok(());
120 }
121 }
122
123 if !replace_cached_identity
126 && let Some(cache) = &self.inner.engine_cache
127 && let Some(cached_state) = cache.get(&self.peer_addr())
128 {
129 tracing::debug!(target: "async_snmp::client", "using cached engine state");
130 let security = self
131 .inner
132 .config
133 .v3_security
134 .as_ref()
135 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
136 let derived_keys = security
137 .derive_keys(&cached_state.engine_id)
138 .map_err(|e| Error::Config(e.to_string().into()).boxed())?;
139 let mut engine = self
140 .inner
141 .engine
142 .write()
143 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
144 *engine = Some(ClientEngine {
145 state: cached_state,
146 derived_keys,
147 });
148 return Ok(());
149 }
150
151 tracing::debug!(target: "async_snmp::client", "performing engine discovery");
153
154 let max_attempts = if self.inner.transport.is_reliable() {
155 0
156 } else {
157 self.inner.config.retry.max_attempts
158 };
159
160 let mut last_error: Option<Box<Error>> = None;
161 let mut response_data_opt: Option<(Bytes, SocketAddr, i32)> = None;
162
163 'discovery: for attempt in 0..=max_attempts {
164 if attempt > 0 {
165 tracing::debug!(target: "async_snmp::client", "retrying engine discovery");
166 }
167
168 let msg_id = self.next_request_id();
169 let discovery_msg = V3Message::discovery_request(msg_id);
170 let discovery_data = discovery_msg.encode();
171
172 self.inner
173 .transport
174 .register_request(msg_id, self.inner.config.timeout);
175
176 match self.inner.transport.request(&discovery_data, msg_id).await {
177 Ok((data, source)) => {
178 response_data_opt = Some((data, source, msg_id));
179 break 'discovery;
180 }
181 Err(e) if matches!(*e, Error::Timeout { .. }) => {
182 last_error = Some(e);
183 if attempt < max_attempts {
184 let delay = self.inner.config.retry.compute_delay(attempt);
185 if !delay.is_zero() {
186 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
187 tokio::time::sleep(delay).await;
188 }
189 }
190 }
192 Err(e) => return Err(e),
193 }
194 }
195
196 let (response_data, _source, expected_msg_id) = response_data_opt.ok_or_else(|| {
197 last_error.unwrap_or_else(|| {
198 Error::Timeout {
199 target: self.peer_addr(),
200 elapsed: std::time::Duration::ZERO,
201 retries: max_attempts,
202 }
203 .boxed()
204 })
205 })?;
206
207 let response = RawV3Message::decode(response_data)?;
212 let engine_state = self.validate_discovery_response(&response, expected_msg_id)?;
213 tracing::debug!(target: "async_snmp::client", { snmp.engine_id = %hex::Bytes(&engine_state.engine_id), snmp.msg_max_size = engine_state.msg_max_size }, "discovered engine identity");
214
215 let security = self
216 .inner
217 .config
218 .v3_security
219 .as_ref()
220 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
221
222 let replacement_keys = if replace_cached_identity {
227 Some(
228 security
229 .derive_keys(&engine_state.engine_id)
230 .map_err(|e| Error::Config(e.to_string().into()).boxed())?,
231 )
232 } else {
233 None
234 };
235
236 if let Some(derived_keys) = replacement_keys {
237 let mut engine = self
244 .inner
245 .engine
246 .write()
247 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
248 let engine_state = if let Some(cache) = &self.inner.engine_cache {
249 cache.replace_target(self.peer_addr(), engine_state)?
250 } else {
251 engine_state
252 };
253 *engine = Some(ClientEngine {
254 state: engine_state,
255 derived_keys,
256 });
257 return Ok(());
258 }
259
260 let engine_state = if let Some(cache) = &self.inner.engine_cache {
265 cache.insert(self.peer_addr(), engine_state.clone());
266 cache.get(&self.peer_addr()).unwrap_or(engine_state)
267 } else {
268 engine_state
269 };
270 let derived_keys = security
271 .derive_keys(&engine_state.engine_id)
272 .map_err(|e| Error::Config(e.to_string().into()).boxed())?;
273 let mut engine = self
274 .inner
275 .engine
276 .write()
277 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
278 *engine = Some(ClientEngine {
279 state: engine_state,
280 derived_keys,
281 });
282
283 Ok(())
284 }
285
286 fn validate_discovery_response(
287 &self,
288 response: &RawV3Message,
289 expected_msg_id: i32,
290 ) -> Result<EngineState> {
291 let malformed = || {
292 Error::MalformedResponse {
293 target: self.peer_addr(),
294 }
295 .boxed()
296 };
297
298 if response.security_level() != SecurityLevel::NoAuthNoPriv {
303 return Err(malformed());
304 }
305
306 let usm = UsmSecurityParams::decode(response.security_params.clone())?;
310 let engine_state = crate::v3::parse_discovery_response_with_limits(
311 &response.security_params,
312 response.global_data.msg_max_size as u32,
313 self.inner.transport.max_message_size(),
314 )?;
315 if !usm.username.is_empty() || !usm.auth_params.is_empty() || !usm.priv_params.is_empty() {
316 return Err(malformed());
317 }
318
319 let RawMsgData::Plaintext(bytes) = &response.msg_data else {
320 return Err(malformed());
321 };
322 let mut decoder = Decoder::with_target(bytes.clone(), self.peer_addr());
323 let scoped_pdu = ScopedPdu::decode(&mut decoder)?;
324
325 if response.global_data.msg_id != expected_msg_id {
328 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected_msg_id, actual_msg_id = response.global_data.msg_id }, "msgID mismatch in discovery response");
329 return Err(malformed());
330 }
331
332 if scoped_pdu.context_engine_id != engine_state.engine_id
333 || !scoped_pdu.context_name.is_empty()
334 {
335 return Err(malformed());
336 }
337
338 if !matches!(
339 classify_report(&scoped_pdu.pdu),
340 Ok(ReportStatus::UnknownEngineId { .. })
341 ) {
342 return Err(malformed());
343 }
344
345 Ok(engine_state)
346 }
347
348 fn refresh_engine_from_cache(&self) -> Result<()> {
349 let Some(cache) = &self.inner.engine_cache else {
350 return Ok(());
351 };
352 let Some(cached_state) = cache.get(&self.peer_addr()) else {
353 return Ok(());
354 };
355 let mut engine = self
356 .inner
357 .engine
358 .write()
359 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
360 if let Some(engine) = engine.as_mut() {
361 engine.state.merge_from(&cached_state);
362 }
363 Ok(())
364 }
365
366 fn build_v3_message(
371 &self,
372 pdu: &Pdu,
373 msg_id: i32,
374 engine_time_override: Option<&PacketLocalEngineTime>,
375 ) -> Result<EncodedV3Request> {
376 let security = self
377 .inner
378 .config
379 .v3_security
380 .as_ref()
381 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
382
383 self.refresh_engine_from_cache()?;
384 let engine = self
385 .inner
386 .engine
387 .read()
388 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
389 let engine = engine
390 .as_ref()
391 .ok_or_else(|| Error::Config("engine not discovered".into()).boxed())?;
392
393 let context_engine_id = engine.state.engine_id.clone();
394 let context_name = security.configured_context_name().clone();
395 let (engine_boots, engine_time) = if let Some(engine_time) = engine_time_override {
396 if engine_time.engine_id != engine.state.engine_id {
400 return Err(Error::MalformedResponse {
401 target: self.peer_addr(),
402 }
403 .boxed());
404 }
405 (engine_time.boots, engine_time.time)
406 } else {
407 engine.state.estimated_boots_time()
408 };
409 let data = crate::v3::encode::encode_v3_message(
410 pdu,
411 msg_id,
412 &context_engine_id,
413 engine_boots,
414 engine_time,
415 security,
416 Some(&engine.derived_keys),
417 &self.inner.salt_counter,
418 true, self.inner.transport.max_message_size(),
424 )?;
425
426 Ok(EncodedV3Request {
427 data,
428 context_engine_id,
429 context_name,
430 })
431 }
432
433 fn verify_response_security(
440 &self,
441 response_data: &[u8],
442 response_usm: &UsmSecurityParams,
443 received_level: SecurityLevel,
444 ) -> Result<()> {
445 let security = self
446 .inner
447 .config
448 .v3_security
449 .as_ref()
450 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
451
452 if response_usm.username != security.username() {
453 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "USM security name does not select the configured user");
454 return Err(Error::Auth {
455 target: self.peer_addr(),
456 }
457 .boxed());
458 }
459
460 {
461 let engine = self
462 .inner
463 .engine
464 .read()
465 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
466 let engine_matches = engine
467 .as_ref()
468 .is_some_and(|engine| engine.state.engine_id == response_usm.engine_id);
469 if !engine_matches {
470 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "USM authoritative engine does not select the cached localized keys");
471 return Err(Error::Auth {
472 target: self.peer_addr(),
473 }
474 .boxed());
475 }
476 }
477
478 if !received_level.requires_auth() {
479 if security.security_level().requires_auth()
480 && !self.inner.config.allow_unauthenticated_v3_time_correction
481 {
482 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "unauthenticated reply on authenticated session");
483 return Err(Error::Auth {
484 target: self.peer_addr(),
485 }
486 .boxed());
487 }
488 return Ok(());
489 }
490
491 tracing::trace!(target: "async_snmp::client", "verifying HMAC authentication on response");
492
493 let engine = self
494 .inner
495 .engine
496 .read()
497 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
498 let derived = &engine
499 .as_ref()
500 .ok_or_else(|| Error::Config("engine not discovered".into()).boxed())?
501 .derived_keys;
502 let auth_key = derived.auth_key.as_ref().ok_or_else(|| {
503 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::NoAuthKey }, "authentication failed");
504 Error::Auth {
505 target: self.peer_addr(),
506 }
507 .boxed()
508 })?;
509
510 if received_level.requires_priv() && derived.priv_key.is_none() {
511 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %CryptoErrorKind::NoPrivKey }, "received security level is unsupported");
512 return Err(Error::Auth {
513 target: self.peer_addr(),
514 }
515 .boxed());
516 }
517
518 let (offset, len) = UsmSecurityParams::find_auth_params_offset(response_data).ok_or_else(
519 || {
520 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::AuthParamsNotFound }, "authentication failed");
521 Error::Auth {
522 target: self.peer_addr(),
523 }
524 .boxed()
525 },
526 )?;
527
528 if !verify_message(auth_key, response_data, offset, len)
529 .map_err(|e| Error::Config(e.to_string().into()).boxed())?
530 {
531 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::HmacMismatch }, "authentication failed");
532 return Err(Error::Auth {
533 target: self.peer_addr(),
534 }
535 .boxed());
536 }
537
538 tracing::trace!(target: "async_snmp::client", { auth_params_offset = offset, auth_params_len = len }, "HMAC verification successful");
539 Ok(())
540 }
541
542 fn decrypt_scoped_pdu(
544 &self,
545 ciphertext: &Bytes,
546 usm_params: &UsmSecurityParams,
547 ) -> Result<ScopedPdu> {
548 tracing::trace!(target: "async_snmp::client", { ciphertext_len = ciphertext.len() }, "decrypting response");
549
550 let engine = self
551 .inner
552 .engine
553 .read()
554 .map_err(|_| Error::Config("engine lock poisoned".into()).boxed())?;
555 let priv_key = engine
556 .as_ref()
557 .and_then(|engine| engine.derived_keys.priv_key.as_ref())
558 .ok_or_else(|| {
559 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %CryptoErrorKind::NoPrivKey }, "decryption failed");
560 Error::Auth {
561 target: self.peer_addr(),
562 }
563 .boxed()
564 })?;
565
566 let plaintext = priv_key
567 .decrypt(
568 ciphertext,
569 usm_params.engine_boots,
570 usm_params.engine_time,
571 &usm_params.priv_params,
572 )
573 .map_err(|e| {
574 tracing::warn!(target: "async_snmp::crypto", { peer = %self.peer_addr(), error = %e }, "decryption failed");
575 Error::Auth {
576 target: self.peer_addr(),
577 }
578 .boxed()
579 })?;
580
581 tracing::trace!(target: "async_snmp::client", { plaintext_len = plaintext.len() }, "decrypted response");
582
583 let mut decoder = Decoder::with_target(plaintext, self.peer_addr());
584 ScopedPdu::decode(&mut decoder)
585 }
586
587 #[instrument(
589 level = "debug",
590 skip(self, pdu),
591 fields(
592 snmp.target = %self.peer_addr(),
593 snmp.request_id = pdu.request_id,
594 snmp.security_level = ?self.inner.config.v3_security.as_ref().map(crate::UsmConfig::security_level),
595 snmp.attempt = tracing::field::Empty,
596 snmp.protocol_correction = tracing::field::Empty,
597 snmp.elapsed_ms = tracing::field::Empty,
598 )
599 )]
600 pub(super) async fn send_v3_and_recv(&self, pdu: Pdu) -> Result<Pdu> {
601 let start = Instant::now();
602
603 self.ensure_engine_discovered().await?;
605
606 let security = self
607 .inner
608 .config
609 .v3_security
610 .as_ref()
611 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
612 let security_level = security.security_level();
613
614 let max_timeout_retries = if self.inner.transport.is_reliable() {
615 0
616 } else {
617 self.inner.config.retry.max_attempts
618 };
619 let mut timeout_retries = 0;
620 let mut correction_used = false;
621 let mut packet_local_engine_time = None;
622 let mut pdu = pdu;
623 let mut msg_id_window: Vec<i32> = Vec::new();
627
628 loop {
629 Span::current().record("snmp.attempt", timeout_retries);
630 Span::current().record("snmp.protocol_correction", correction_used);
631
632 let msg_id = self.next_request_id();
637 let engine_time_override = packet_local_engine_time.take();
638 let request = self.build_v3_message(&pdu, msg_id, engine_time_override.as_ref())?;
639
640 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len(), snmp.msg_id = msg_id }, "sending V3 {} request", pdu.pdu_type);
641 tracing::trace!(target: "async_snmp::client", { snmp.bytes = request.data.len() }, "sending V3 request");
642
643 self.inner
645 .transport
646 .register_request(msg_id, self.inner.config.timeout);
647 for &prior in &msg_id_window {
648 self.inner.transport.register_request_alias(
649 prior,
650 msg_id,
651 self.inner.config.timeout,
652 );
653 }
654 msg_id_window.push(msg_id);
655
656 match self.inner.transport.request(&request.data, msg_id).await {
659 Ok((response_data, _source)) => {
660 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received V3 response");
661
662 let raw = RawV3Message::decode(response_data.clone())?;
668 let received_level = raw.security_level();
669 let response_usm = UsmSecurityParams::decode(raw.security_params.clone())?;
670
671 self.verify_response_security(&response_data, &response_usm, received_level)?;
675
676 let mut deferred_authenticated_update = false;
683 if received_level.requires_auth() {
684 let timely = if engine_time_override.is_some() {
685 let local_state = {
692 let engine = self.inner.engine.read().map_err(|_| {
693 Error::Config("engine lock poisoned".into()).boxed()
694 })?;
695 engine
696 .as_ref()
697 .ok_or_else(|| {
698 Error::Config("engine not discovered".into()).boxed()
699 })?
700 .state
701 .clone()
702 };
703 let timely = self
704 .inner
705 .engine_cache
706 .as_deref()
707 .and_then(|cache| {
708 cache.timeliness_candidate(
709 &self.peer_addr(),
710 &local_state,
711 &response_usm.engine_id,
712 response_usm.engine_boots,
713 response_usm.engine_time,
714 )
715 })
716 .map_or_else(
717 || {
718 local_state.clone().check_and_update_timeliness(
719 response_usm.engine_boots,
720 response_usm.engine_time,
721 )
722 },
723 |(timely, _candidate)| timely,
724 );
725 deferred_authenticated_update = true;
726 timely
727 } else {
728 let mut engine = self.inner.engine.write().map_err(|_| {
729 Error::Config("engine lock poisoned".into()).boxed()
730 })?;
731 let engine = engine.as_mut().ok_or_else(|| {
732 Error::Config("engine not discovered".into()).boxed()
733 })?;
734 check_and_update_engine_timeliness(
735 &mut engine.state,
736 self.inner.engine_cache.as_deref(),
737 self.peer_addr(),
738 &response_usm.engine_id,
739 response_usm.engine_boots,
740 response_usm.engine_time,
741 )
742 };
743
744 if !timely {
745 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), msg_boots = response_usm.engine_boots, msg_time = response_usm.engine_time }, "message outside time window");
746 return Err(Error::Auth {
747 target: self.peer_addr(),
748 }
749 .boxed());
750 }
751 }
752
753 #[cfg(test)]
754 if deferred_authenticated_update {
755 let hook = self
756 .inner
757 .deferred_authenticated_update_hook
758 .read()
759 .expect("deferred update hook lock poisoned")
760 .clone();
761 if let Some(hook) = hook {
762 hook();
763 }
764 }
765
766 let scoped_pdu = match &raw.msg_data {
769 RawMsgData::Plaintext(bytes) => {
770 let mut decoder = Decoder::with_target(bytes.clone(), self.peer_addr());
771 ScopedPdu::decode(&mut decoder)?
772 }
773 RawMsgData::Encrypted(ciphertext) => {
774 self.decrypt_scoped_pdu(ciphertext, &response_usm)?
775 }
776 };
777
778 if !msg_id_window.contains(&raw.global_data.msg_id) {
785 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected_msg_ids = ?msg_id_window, actual_msg_id = raw.global_data.msg_id }, "msgID mismatch in response");
786 return Err(Error::MalformedResponse {
787 target: self.peer_addr(),
788 }
789 .boxed());
790 }
791
792 if scoped_pdu.pdu.pdu_type == PduType::Report {
797 let status = classify_report(&scoped_pdu.pdu).map_err(|_| {
798 Error::MalformedResponse {
799 target: self.peer_addr(),
800 }
801 .boxed()
802 })?;
803
804 if matches!(status, ReportStatus::NotInTimeWindow { .. })
805 && received_level.requires_auth()
806 && !correction_used
807 {
808 correction_used = true;
814 pdu.request_id = self.next_request_id();
815 msg_id_window.clear();
816 Span::current().record("snmp.protocol_correction", true);
817 tracing::debug!(target: "async_snmp::client", { snmp.report_status = %status }, "sending SNMPv3 protocol correction");
818 continue;
819 }
820
821 if matches!(status, ReportStatus::NotInTimeWindow { .. })
822 && received_level == SecurityLevel::NoAuthNoPriv
823 && security_level.requires_auth()
824 && self.inner.config.allow_unauthenticated_v3_time_correction
825 && response_usm.auth_params.is_empty()
826 && response_usm.priv_params.is_empty()
827 && !correction_used
828 {
829 correction_used = true;
836 packet_local_engine_time = Some(PacketLocalEngineTime {
837 engine_id: response_usm.engine_id.clone(),
838 boots: response_usm.engine_boots,
839 time: response_usm.engine_time,
840 });
841 pdu.request_id = self.next_request_id();
842 msg_id_window.clear();
843 Span::current().record("snmp.protocol_correction", true);
844 tracing::debug!(target: "async_snmp::client", { snmp.report_status = %status }, "sending packet-local SNMPv3 compatibility correction");
845 continue;
846 }
847
848 return Err(Error::Report {
853 target: self.peer_addr(),
854 status: Box::new(status),
855 }
856 .boxed());
857 }
858
859 if received_level != security_level {
861 tracing::warn!(target: "async_snmp::client", {
862 peer = %self.peer_addr(),
863 expected = ?security_level,
864 actual = ?received_level
865 }, "security level mismatch in response");
866 return Err(Error::MalformedResponse {
867 target: self.peer_addr(),
868 }
869 .boxed());
870 }
871
872 {
874 let engine =
875 self.inner.engine.read().map_err(|_| {
876 Error::Config("engine lock poisoned".into()).boxed()
877 })?;
878 if let Some(ref engine) = *engine
879 && response_usm.engine_id != engine.state.engine_id
880 {
881 tracing::warn!(target: "async_snmp::client", {
882 peer = %self.peer_addr()
883 }, "engine ID mismatch in response");
884 return Err(Error::MalformedResponse {
885 target: self.peer_addr(),
886 }
887 .boxed());
888 }
889 }
890
891 if response_usm.username != security.username() {
893 tracing::warn!(target: "async_snmp::client", {
894 peer = %self.peer_addr()
895 }, "username mismatch in response");
896 return Err(Error::MalformedResponse {
897 target: self.peer_addr(),
898 }
899 .boxed());
900 }
901
902 if scoped_pdu.context_engine_id != request.context_engine_id
905 || scoped_pdu.context_name != request.context_name
906 {
907 tracing::warn!(target: "async_snmp::client", {
908 peer = %self.peer_addr()
909 }, "scoped context mismatch in response");
910 return Err(Error::MalformedResponse {
911 target: self.peer_addr(),
912 }
913 .boxed());
914 }
915
916 let response_pdu = scoped_pdu.pdu;
917
918 if response_pdu.pdu_type != PduType::Response {
922 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), pdu_type = ?response_pdu.pdu_type }, "non-Response PDU in response");
923 return Err(Error::MalformedResponse {
924 target: self.peer_addr(),
925 }
926 .boxed());
927 }
928
929 if response_pdu.request_id != pdu.request_id {
931 tracing::warn!(target: "async_snmp::client", { expected_request_id = pdu.request_id, actual_request_id = response_pdu.request_id, peer = %self.peer_addr() }, "request ID mismatch in response");
932 return Err(Error::MalformedResponse {
933 target: self.peer_addr(),
934 }
935 .boxed());
936 }
937
938 if deferred_authenticated_update {
944 let timely = {
945 let mut engine = self.inner.engine.write().map_err(|_| {
946 Error::Config("engine lock poisoned".into()).boxed()
947 })?;
948 let engine = engine.as_mut().ok_or_else(|| {
949 Error::Config("engine not discovered".into()).boxed()
950 })?;
951 if engine.state.engine_id != response_usm.engine_id {
952 return Err(Error::MalformedResponse {
953 target: self.peer_addr(),
954 }
955 .boxed());
956 }
957 check_and_update_engine_timeliness(
958 &mut engine.state,
959 self.inner.engine_cache.as_deref(),
960 self.peer_addr(),
961 &response_usm.engine_id,
962 response_usm.engine_boots,
963 response_usm.engine_time,
964 )
965 };
966 if !timely {
967 return Err(Error::Auth {
968 target: self.peer_addr(),
969 }
970 .boxed());
971 }
972 }
973
974 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?response_pdu.pdu_type, snmp.varbind_count = response_pdu.varbinds.len(), snmp.error_status = response_pdu.error_status, snmp.error_index = response_pdu.error_index }, "received V3 {} response", response_pdu.pdu_type);
975
976 if let Some(err) = super::pdu_to_snmp_error(&response_pdu, self.peer_addr()) {
978 Span::current()
979 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
980 return Err(err);
981 }
982
983 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
984 return Ok(response_pdu);
985 }
986 Err(e) if matches!(*e, Error::Timeout { .. }) => {
987 if engine_time_override.is_some() || timeout_retries >= max_timeout_retries {
990 break;
991 }
992
993 let delay = self.inner.config.retry.compute_delay(timeout_retries);
994 timeout_retries += 1;
995 tracing::debug!(target: "async_snmp::client", { timeout_retries, delay_ms = delay.as_millis() as u64 }, "retransmitting V3 request after timeout");
1002 if !delay.is_zero() {
1003 tokio::time::sleep(delay).await;
1004 }
1005 }
1006 Err(e) => {
1007 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
1008 return Err(e);
1009 }
1010 }
1011 }
1012
1013 let elapsed = start.elapsed();
1016 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
1017 tracing::debug!(target: "async_snmp::client", { request_id = pdu.request_id, peer = %self.peer_addr(), ?elapsed, retries = timeout_retries }, "request timed out");
1018 Err(Error::Timeout {
1019 target: self.peer_addr(),
1020 elapsed,
1021 retries: timeout_retries,
1022 }
1023 .boxed())
1024 }
1025
1026 pub(super) fn ensure_local_keys_derived(&self) -> Result<()> {
1028 {
1030 let keys =
1031 self.inner.local_derived_keys.read().map_err(|_| {
1032 Error::Config("local_derived_keys lock poisoned".into()).boxed()
1033 })?;
1034 if keys.is_some() {
1035 return Ok(());
1036 }
1037 }
1038
1039 let local_engine = self.local_engine_for_trap()?;
1040
1041 let security = self
1042 .inner
1043 .config
1044 .v3_security
1045 .as_ref()
1046 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
1047
1048 let keys = security
1049 .derive_keys(local_engine.engine_id())
1050 .map_err(|e| Error::Config(e.to_string().into()).boxed())?;
1051
1052 let mut derived = self
1053 .inner
1054 .local_derived_keys
1055 .write()
1056 .map_err(|_| Error::Config("local_derived_keys lock poisoned".into()).boxed())?;
1057 *derived = Some(keys);
1058
1059 Ok(())
1060 }
1061
1062 fn local_engine_for_trap(&self) -> Result<&crate::v3::AuthoritativeEngine> {
1063 self.inner
1064 .config
1065 .local_authoritative_engine
1066 .as_ref()
1067 .ok_or_else(|| {
1068 Error::Config(
1069 "local authoritative engine state required for V3 trap sending".into(),
1070 )
1071 .boxed()
1072 })
1073 }
1074
1075 pub(super) fn build_v3_trap_message(&self, pdu: &Pdu, msg_id: i32) -> Result<Vec<u8>> {
1081 let security = self
1082 .inner
1083 .config
1084 .v3_security
1085 .as_ref()
1086 .ok_or_else(|| Error::Config("V3 security not configured".into()).boxed())?;
1087
1088 let local_engine = self.local_engine_for_trap()?;
1089
1090 let derived = self
1091 .inner
1092 .local_derived_keys
1093 .read()
1094 .map_err(|_| Error::Config("local_derived_keys lock poisoned".into()).boxed())?;
1095
1096 let (engine_boots, engine_time) = local_engine.current_boots_time()?;
1097
1098 crate::v3::encode::encode_v3_message(
1099 pdu,
1100 msg_id,
1101 local_engine.engine_id(),
1102 engine_boots,
1103 engine_time,
1104 security,
1105 derived.as_ref(),
1106 &self.inner.salt_counter,
1107 false, crate::v3::DEFAULT_MSG_MAX_SIZE,
1109 )
1110 }
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use crate::UsmConfig;
1117 use crate::client::ClientConfig;
1118 use crate::message::V3MessageData;
1119 use crate::oid;
1120 use crate::transport::Transport;
1121 use bytes::Bytes;
1122 use std::future::ready;
1123 use std::net::{Ipv4Addr, SocketAddr};
1124 use std::sync::Arc;
1125 use std::sync::atomic::{AtomicU32, Ordering};
1126 use std::time::Duration;
1127
1128 #[derive(Clone)]
1129 struct TestTransport {
1130 peer: SocketAddr,
1131 }
1132
1133 impl TestTransport {
1134 fn new() -> Self {
1135 Self {
1136 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1137 }
1138 }
1139 }
1140
1141 impl Transport for TestTransport {
1142 fn send(&self, _data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1143 ready(Ok(()))
1144 }
1145
1146 fn recv(
1147 &self,
1148 _request_id: i32,
1149 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1150 ready(Err(Error::Config(
1151 "test transport does not receive data".into(),
1152 )
1153 .boxed()))
1154 }
1155
1156 fn peer_addr(&self) -> SocketAddr {
1157 self.peer
1158 }
1159
1160 fn local_addr(&self) -> SocketAddr {
1161 SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
1162 }
1163
1164 fn is_reliable(&self) -> bool {
1165 false
1166 }
1167
1168 fn register_request(&self, _request_id: i32, _timeout: Duration) {}
1169 }
1170
1171 #[tokio::test]
1172 async fn direct_config_requires_authoritative_state_before_sending_v3_trap() {
1173 let config = ClientConfig {
1174 version: crate::Version::V3,
1175 v3_security: Some(UsmConfig::new("trapuser")),
1176 ..ClientConfig::default()
1177 };
1178 let client = Client::new(TestTransport::new(), config);
1179
1180 let err = client
1181 .send_trap(&oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), 0, vec![])
1182 .await
1183 .unwrap_err();
1184 assert!(matches!(*err, Error::Config(_)));
1185 }
1186
1187 #[test]
1188 fn test_rejected_message_does_not_reinsert_missing_cache_entry() {
1189 let cache = EngineCache::new();
1190 let target = SocketAddr::from((Ipv4Addr::LOCALHOST, 161));
1191 let engine_id = Bytes::from_static(b"engine");
1192 let mut state = EngineState::new(engine_id.clone(), 5, 1000);
1193
1194 let timely = check_and_update_engine_timeliness(
1195 &mut state,
1196 Some(&cache),
1197 target,
1198 &engine_id,
1199 4,
1200 5000,
1201 );
1202
1203 assert!(!timely);
1204 assert!(cache.get(&target).is_none());
1205 assert!(cache.is_empty());
1206 }
1207
1208 #[test]
1209 fn test_deferred_time_revalidation_rejects_concurrently_stale_response() {
1210 let cache = EngineCache::new();
1211 let target = SocketAddr::from((Ipv4Addr::LOCALHOST, 161));
1212 let engine_id = Bytes::from_static(b"engine");
1213 let mut state = EngineState::new(engine_id.clone(), 1, 1000);
1214 cache.insert(target, state.clone());
1215
1216 let (initially_timely, _) = cache
1217 .timeliness_candidate(&target, &state, &engine_id, 1, 1100)
1218 .expect("cached identity");
1219 assert!(initially_timely);
1220
1221 assert!(check_and_update_engine_timeliness(
1222 &mut state,
1223 Some(&cache),
1224 target,
1225 &engine_id,
1226 1,
1227 1400,
1228 ));
1229 assert!(!check_and_update_engine_timeliness(
1230 &mut state,
1231 Some(&cache),
1232 target,
1233 &engine_id,
1234 1,
1235 1100,
1236 ));
1237 }
1238
1239 #[test]
1240 fn test_build_v3_message_uses_configured_context_name() {
1241 let transport = TestTransport::new();
1242 let config = ClientConfig {
1243 version: crate::version::Version::V3,
1244 v3_security: Some(UsmConfig::new("user").context_name("ctx")),
1245 ..ClientConfig::default()
1246 };
1247 let client = Client::new(transport, config);
1248
1249 {
1250 let security = client.inner.config.v3_security.as_ref().unwrap();
1251 let state = EngineState::new(Bytes::from_static(b"engine"), 1, 42);
1252 let derived_keys = security.derive_keys(&state.engine_id).unwrap();
1253 *client.inner.engine.write().expect("engine lock poisoned") = Some(ClientEngine {
1254 state,
1255 derived_keys,
1256 });
1257 }
1258
1259 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
1260
1261 let encoded = client
1262 .build_v3_message(&pdu, 456, None)
1263 .expect("v3 message should encode");
1264 let decoded =
1265 V3Message::decode(Bytes::from(encoded.data)).expect("v3 message should decode");
1266 let scoped = match decoded.data {
1267 V3MessageData::Plaintext(scoped) => scoped,
1268 V3MessageData::Encrypted(_) => panic!("expected plaintext scoped PDU"),
1269 };
1270
1271 assert_eq!(scoped.context_name.as_ref(), b"ctx");
1272 }
1273
1274 #[test]
1275 fn test_packet_local_time_rejects_changed_engine_generation() {
1276 let transport = TestTransport::new();
1277 let config = ClientConfig {
1278 version: crate::version::Version::V3,
1279 v3_security: Some(UsmConfig::new("user")),
1280 ..ClientConfig::default()
1281 };
1282 let client = Client::new(transport, config);
1283
1284 {
1285 let security = client.inner.config.v3_security.as_ref().unwrap();
1286 let state = EngineState::new(Bytes::from_static(b"engine-a"), 1, 42);
1287 let derived_keys = security.derive_keys(&state.engine_id).unwrap();
1288 *client.inner.engine.write().expect("engine lock poisoned") = Some(ClientEngine {
1289 state,
1290 derived_keys,
1291 });
1292 }
1293
1294 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
1295 let packet_time = PacketLocalEngineTime {
1296 engine_id: Bytes::from_static(b"engine-b"),
1297 boots: 9,
1298 time: 99,
1299 };
1300 let err = client
1301 .build_v3_message(&pdu, 456, Some(&packet_time))
1302 .err()
1303 .expect("changed engine generation must fail");
1304
1305 assert!(matches!(*err, Error::MalformedResponse { .. }));
1306 }
1307
1308 #[derive(Clone)]
1311 struct RetryTestTransport {
1312 peer: SocketAddr,
1313 recv_count: Arc<AtomicU32>,
1314 engine_id: Bytes,
1315 }
1316
1317 impl RetryTestTransport {
1318 fn new(engine_id: Bytes) -> Self {
1319 Self {
1320 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1321 recv_count: Arc::new(AtomicU32::new(0)),
1322 engine_id,
1323 }
1324 }
1325 }
1326
1327 impl Transport for RetryTestTransport {
1328 fn send(&self, _data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1329 ready(Ok(()))
1330 }
1331
1332 fn recv(
1333 &self,
1334 request_id: i32,
1335 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1336 let count = self.recv_count.fetch_add(1, Ordering::Relaxed);
1337 let peer = self.peer;
1338 let engine_id = self.engine_id.clone();
1339 async move {
1340 if count == 0 {
1341 Err(Error::Timeout {
1343 target: peer,
1344 elapsed: Duration::from_secs(5),
1345 retries: 0,
1346 }
1347 .boxed())
1348 } else {
1349 Ok((build_discovery_response(&engine_id, request_id), peer))
1350 }
1351 }
1352 }
1353
1354 fn peer_addr(&self) -> SocketAddr {
1355 self.peer
1356 }
1357
1358 fn local_addr(&self) -> SocketAddr {
1359 SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
1360 }
1361
1362 fn is_reliable(&self) -> bool {
1363 false
1364 }
1365
1366 fn register_request(&self, _request_id: i32, _timeout: Duration) {}
1367 }
1368
1369 fn build_discovery_response(engine_id: &[u8], msg_id: i32) -> Bytes {
1371 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1372 use crate::pdu::{Pdu, PduType};
1373 use crate::v3::UsmSecurityParams;
1374 use crate::value::Value;
1375 use crate::varbind::VarBind;
1376
1377 let report_pdu = Pdu {
1378 pdu_type: PduType::Report,
1379 request_id: 1,
1380 error_status: 0,
1381 error_index: 0,
1382 varbinds: vec![VarBind::new(
1383 crate::oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 4, 0),
1384 Value::Counter32(0),
1385 )],
1386 };
1387
1388 let global = MsgGlobalData::new(
1389 msg_id,
1390 65507,
1391 MsgFlags::new(crate::message::SecurityLevel::NoAuthNoPriv, false),
1392 );
1393 let usm = UsmSecurityParams::new(Bytes::copy_from_slice(engine_id), 1, 100, Bytes::new());
1394 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), report_pdu);
1395
1396 V3Message::new(global, usm.encode(), scoped).encode()
1397 }
1398
1399 #[tokio::test]
1400 async fn test_discovery_retries_on_timeout() {
1401 let engine_id = b"test-engine";
1402 let transport = RetryTestTransport::new(Bytes::copy_from_slice(engine_id));
1403 let recv_count = transport.recv_count.clone();
1404
1405 let config = ClientConfig {
1406 version: crate::version::Version::V3,
1407 v3_security: Some(UsmConfig::new("user")),
1408 retry: crate::client::Retry::fixed(1, Duration::ZERO),
1409 ..ClientConfig::default()
1410 };
1411 let client = Client::new(transport, config);
1412
1413 client
1414 .ensure_engine_discovered()
1415 .await
1416 .expect("discovery should succeed after retry");
1417
1418 assert_eq!(recv_count.load(Ordering::Relaxed), 2);
1420
1421 let engine = client.inner.engine.read().expect("engine lock poisoned");
1423 assert!(engine.is_some());
1424 let state = &engine.as_ref().unwrap().state;
1425 assert_eq!(state.engine_id.as_ref(), engine_id);
1426 assert!(state.trusted_time().is_none());
1427 }
1428
1429 #[tokio::test]
1430 async fn test_discovery_fails_when_all_retries_timeout() {
1431 #[derive(Clone)]
1433 struct AlwaysTimeoutTransport {
1434 peer: SocketAddr,
1435 }
1436 impl Transport for AlwaysTimeoutTransport {
1437 fn send(&self, _data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1438 ready(Ok(()))
1439 }
1440 fn recv(
1441 &self,
1442 _request_id: i32,
1443 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1444 let peer = self.peer;
1445 async move {
1446 Err(Error::Timeout {
1447 target: peer,
1448 elapsed: Duration::from_secs(5),
1449 retries: 0,
1450 }
1451 .boxed())
1452 }
1453 }
1454 fn peer_addr(&self) -> SocketAddr {
1455 self.peer
1456 }
1457 fn local_addr(&self) -> SocketAddr {
1458 SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
1459 }
1460 fn is_reliable(&self) -> bool {
1461 false
1462 }
1463 fn register_request(&self, _request_id: i32, _timeout: Duration) {}
1464 }
1465
1466 let transport = AlwaysTimeoutTransport {
1467 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1468 };
1469 let config = ClientConfig {
1470 version: crate::version::Version::V3,
1471 v3_security: Some(UsmConfig::new("user")),
1472 retry: crate::client::Retry::fixed(2, Duration::ZERO),
1473 ..ClientConfig::default()
1474 };
1475 let client = Client::new(transport, config);
1476
1477 let result = client.ensure_engine_discovered().await;
1478 assert!(
1479 matches!(*result.unwrap_err(), Error::Timeout { .. }),
1480 "should return Timeout after all retries exhausted"
1481 );
1482 }
1483}
1484
1485#[cfg(test)]
1486mod response_validation_tests {
1487 use super::*;
1488 use crate::UsmConfig;
1489 use crate::client::ClientConfig;
1490 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, SecurityLevel, V3MessageData};
1491 use crate::oid;
1492 use crate::v3::auth::authenticate_message;
1493 use crate::v3::{AuthProtocol, EngineState, LocalizedKey};
1494 use bytes::Bytes;
1495 use std::future::ready;
1496 use std::net::{Ipv4Addr, SocketAddr};
1497 use std::sync::Arc;
1498 use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
1499 use std::time::Duration;
1500
1501 #[derive(Clone)]
1503 struct CannedTransport {
1504 peer: SocketAddr,
1505 response: Bytes,
1506 max_size: u32,
1507 }
1508
1509 impl CannedTransport {
1510 fn new(response: Bytes) -> Self {
1511 Self {
1512 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1513 response,
1514 max_size: crate::v3::DEFAULT_MSG_MAX_SIZE,
1515 }
1516 }
1517 }
1518
1519 impl Transport for CannedTransport {
1520 fn send(&self, _data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1521 ready(Ok(()))
1522 }
1523
1524 fn max_message_size(&self) -> u32 {
1525 self.max_size
1526 }
1527
1528 fn recv(
1529 &self,
1530 _request_id: i32,
1531 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1532 ready(Ok((self.response.clone(), self.peer)))
1533 }
1534
1535 fn peer_addr(&self) -> SocketAddr {
1536 self.peer
1537 }
1538
1539 fn local_addr(&self) -> SocketAddr {
1540 SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
1541 }
1542
1543 fn is_reliable(&self) -> bool {
1544 true
1545 }
1546
1547 fn alloc_request_id(&self) -> i32 {
1548 99
1550 }
1551
1552 fn register_request(&self, _request_id: i32, _timeout: Duration) {}
1553 }
1554
1555 #[derive(Clone)]
1556 struct DeferredUpdateTransport {
1557 peer: SocketAddr,
1558 response_number: Arc<AtomicU32>,
1559 next_request_id: Arc<AtomicI32>,
1560 }
1561
1562 impl DeferredUpdateTransport {
1563 fn new() -> Self {
1564 Self {
1565 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1566 response_number: Arc::new(AtomicU32::new(0)),
1567 next_request_id: Arc::new(AtomicI32::new(100)),
1568 }
1569 }
1570 }
1571
1572 impl Transport for DeferredUpdateTransport {
1573 async fn send(&self, _data: &[u8]) -> Result<()> {
1574 Ok(())
1575 }
1576
1577 async fn recv(&self, _request_id: i32) -> Result<(Bytes, SocketAddr)> {
1578 Err(Error::Config("DeferredUpdateTransport uses request()".into()).boxed())
1579 }
1580
1581 async fn request(&self, data: &[u8], _request_id: i32) -> Result<(Bytes, SocketAddr)> {
1582 let response_number = self.response_number.fetch_add(1, Ordering::SeqCst);
1583 let response = build_deferred_update_response(data, response_number);
1584 Ok((response, self.peer))
1585 }
1586
1587 fn peer_addr(&self) -> SocketAddr {
1588 self.peer
1589 }
1590
1591 fn local_addr(&self) -> SocketAddr {
1592 SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
1593 }
1594
1595 fn alloc_request_id(&self) -> i32 {
1596 self.next_request_id.fetch_add(1, Ordering::Relaxed)
1597 }
1598
1599 fn is_reliable(&self) -> bool {
1600 false
1601 }
1602 }
1603
1604 const ENGINE_ID: &[u8] = b"engine";
1605
1606 fn build_response(
1610 pdu_type: PduType,
1611 request_id: i32,
1612 engine_boots: u32,
1613 engine_time: u32,
1614 auth_password: Option<&[u8]>,
1615 ) -> Bytes {
1616 let security_level = if auth_password.is_some() {
1617 SecurityLevel::AuthNoPriv
1618 } else {
1619 SecurityLevel::NoAuthNoPriv
1620 };
1621 let global = MsgGlobalData::new(99, 65507, MsgFlags::new(security_level, false));
1622 let mut usm = UsmSecurityParams::new(
1623 Bytes::from_static(ENGINE_ID),
1624 engine_boots,
1625 engine_time,
1626 Bytes::from_static(b"user"),
1627 );
1628 let auth_key = auth_password.map(|password| {
1629 LocalizedKey::from_password(AuthProtocol::Sha1, password, ENGINE_ID).unwrap()
1630 });
1631 if let Some(key) = &auth_key {
1632 usm = usm.with_auth_placeholder(key.mac_len());
1633 }
1634 let scoped = ScopedPdu::new(
1635 Bytes::from_static(ENGINE_ID),
1636 Bytes::new(),
1637 Pdu {
1638 pdu_type,
1639 request_id,
1640 error_status: 0,
1641 error_index: 0,
1642 varbinds: vec![],
1643 },
1644 );
1645 let msg = V3Message::new(global, usm.encode(), scoped);
1646 match auth_key {
1647 Some(key) => {
1648 let mut bytes = msg.encode().to_vec();
1649 let (offset, len) = UsmSecurityParams::find_auth_params_offset(&bytes).unwrap();
1650 authenticate_message(&key, &mut bytes, offset, len).unwrap();
1651 Bytes::from(bytes)
1652 }
1653 None => msg.encode(),
1654 }
1655 }
1656
1657 fn build_deferred_update_response(request_data: &[u8], response_number: u32) -> Bytes {
1658 let request = V3Message::decode(Bytes::copy_from_slice(request_data)).unwrap();
1659 let scoped_request = match request.data {
1660 V3MessageData::Plaintext(scoped) => scoped,
1661 V3MessageData::Encrypted(_) => panic!("expected authNoPriv request"),
1662 };
1663 let (level, engine_time, pdu) = match response_number {
1664 0 => (
1665 SecurityLevel::NoAuthNoPriv,
1666 1100,
1667 Pdu {
1668 pdu_type: PduType::Report,
1669 request_id: 0,
1670 error_status: 0,
1671 error_index: 0,
1672 varbinds: vec![crate::VarBind::new(
1673 crate::v3::report_oids::not_in_time_windows(),
1674 crate::Value::Counter32(1),
1675 )],
1676 },
1677 ),
1678 1 => (
1679 SecurityLevel::AuthNoPriv,
1680 1100,
1681 Pdu {
1682 pdu_type: PduType::Response,
1683 request_id: scoped_request.pdu.request_id,
1684 error_status: 0,
1685 error_index: 0,
1686 varbinds: vec![],
1687 },
1688 ),
1689 2 => (
1690 SecurityLevel::AuthNoPriv,
1691 1400,
1692 Pdu {
1693 pdu_type: PduType::Response,
1694 request_id: scoped_request.pdu.request_id,
1695 error_status: 0,
1696 error_index: 0,
1697 varbinds: vec![],
1698 },
1699 ),
1700 _ => panic!("unexpected deferred-update response {response_number}"),
1701 };
1702 let global = MsgGlobalData::new(
1703 request.global_data.msg_id,
1704 65507,
1705 MsgFlags::new(level, false),
1706 );
1707 let auth_key = (level == SecurityLevel::AuthNoPriv).then(|| {
1708 LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", ENGINE_ID).unwrap()
1709 });
1710 let mut usm = UsmSecurityParams::new(
1711 Bytes::from_static(ENGINE_ID),
1712 1,
1713 engine_time,
1714 Bytes::from_static(b"user"),
1715 );
1716 if let Some(key) = &auth_key {
1717 usm = usm.with_auth_placeholder(key.mac_len());
1718 }
1719 let scoped = ScopedPdu::new(
1720 scoped_request.context_engine_id,
1721 scoped_request.context_name,
1722 pdu,
1723 );
1724 let mut response = V3Message::new(global, usm.encode(), scoped)
1725 .encode()
1726 .to_vec();
1727 if let Some(key) = auth_key {
1728 let (offset, len) = UsmSecurityParams::find_auth_params_offset(&response).unwrap();
1729 authenticate_message(&key, &mut response, offset, len).unwrap();
1730 }
1731 Bytes::from(response)
1732 }
1733
1734 fn canned_client(
1737 response: Bytes,
1738 engine_boots: u32,
1739 engine_time: u32,
1740 security: UsmConfig,
1741 ) -> Client<CannedTransport> {
1742 let config = ClientConfig {
1743 version: crate::version::Version::V3,
1744 v3_security: Some(security.clone()),
1745 ..ClientConfig::default()
1746 };
1747 let client = Client::new(CannedTransport::new(response), config);
1748 {
1749 let state = EngineState::new(Bytes::from_static(ENGINE_ID), engine_boots, engine_time);
1750 let derived_keys = security.derive_keys(ENGINE_ID).unwrap();
1751 *client.inner.engine.write().unwrap() = Some(ClientEngine {
1752 state,
1753 derived_keys,
1754 });
1755 }
1756 client
1757 }
1758
1759 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1760 async fn v3_deferred_update_revalidates_after_concurrent_advancement() {
1761 let transport = DeferredUpdateTransport::new();
1762 let security = UsmConfig::new("user").auth(AuthProtocol::Sha1, "authpass12345678");
1763 let cache = Arc::new(EngineCache::new());
1764 let config = ClientConfig {
1765 version: crate::version::Version::V3,
1766 retry: crate::client::Retry::none(),
1767 v3_security: Some(security.clone()),
1768 allow_unauthenticated_v3_time_correction: true,
1769 ..ClientConfig::default()
1770 };
1771 let client = Client::with_engine_cache(transport, config, cache.clone());
1772 let state = EngineState::new(Bytes::from_static(ENGINE_ID), 1, 1000);
1773 let derived_keys = security.derive_keys(ENGINE_ID).unwrap();
1774 cache.insert(client.peer_addr(), state.clone());
1775 *client.inner.engine.write().unwrap() = Some(ClientEngine {
1776 state,
1777 derived_keys,
1778 });
1779
1780 let (candidate_checked_tx, candidate_checked_rx) = std::sync::mpsc::channel();
1781 let (advancement_complete_tx, advancement_complete_rx) = std::sync::mpsc::channel();
1782 let advancement_complete_rx = std::sync::Mutex::new(advancement_complete_rx);
1783 *client
1784 .inner
1785 .deferred_authenticated_update_hook
1786 .write()
1787 .unwrap() = Some(Arc::new(move || {
1788 candidate_checked_tx
1789 .send(())
1790 .expect("candidate-check waiter dropped");
1791 advancement_complete_rx
1792 .lock()
1793 .expect("advancement-complete lock poisoned")
1794 .recv_timeout(Duration::from_secs(5))
1795 .expect("concurrent advancement did not complete");
1796 }));
1797
1798 let stale_client = client.clone();
1799 let stale_request = tokio::spawn(async move {
1800 stale_client
1801 .send_v3_and_recv(Pdu::get_request(1, &[oid!(1, 3, 6, 1, 1)]))
1802 .await
1803 });
1804 tokio::task::spawn_blocking(move || {
1805 candidate_checked_rx.recv_timeout(Duration::from_secs(5))
1806 })
1807 .await
1808 .expect("candidate-check waiter panicked")
1809 .expect("provisional timeliness check did not complete");
1810
1811 client
1812 .send_v3_and_recv(Pdu::get_request(2, &[oid!(1, 3, 6, 1, 1)]))
1813 .await
1814 .expect("concurrent response should advance trusted time");
1815 advancement_complete_tx
1816 .send(())
1817 .expect("advancement-complete waiter dropped");
1818
1819 let err = stale_request.await.unwrap().unwrap_err();
1820 assert!(matches!(*err, Error::Auth { .. }));
1821 let engine = client.inner.engine.read().unwrap();
1822 let trusted = engine.as_ref().unwrap().state.trusted_time().unwrap();
1823 assert_eq!(trusted.latest_received_time(), 1400);
1824 assert_eq!(
1825 cache
1826 .get(&client.peer_addr())
1827 .unwrap()
1828 .trusted_time()
1829 .unwrap()
1830 .latest_received_time(),
1831 1400
1832 );
1833 }
1834
1835 #[tokio::test]
1839 async fn v3_advertises_local_receive_capacity_not_remote() {
1840 let security = UsmConfig::new("user").auth(AuthProtocol::Sha1, "authpass12345678");
1841 let transport = CannedTransport {
1842 peer: SocketAddr::from((Ipv4Addr::LOCALHOST, 161)),
1843 response: Bytes::new(),
1844 max_size: 1400,
1845 };
1846 let config = ClientConfig {
1847 version: crate::version::Version::V3,
1848 v3_security: Some(security.clone()),
1849 ..ClientConfig::default()
1850 };
1851 let client = Client::new(transport, config);
1852 {
1853 let state =
1855 EngineState::with_msg_max_size(Bytes::from_static(ENGINE_ID), 5, 1000, 9000);
1856 let derived_keys = security.derive_keys(ENGINE_ID).unwrap();
1857 *client.inner.engine.write().unwrap() = Some(ClientEngine {
1858 state,
1859 derived_keys,
1860 });
1861 }
1862
1863 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 1)]);
1864 let request = client.build_v3_message(&pdu, 1, None).unwrap();
1865 let msg = V3Message::decode(Bytes::from(request.data)).unwrap();
1866 assert_eq!(
1867 msg.global_data.msg_max_size, 1400,
1868 "request must advertise the local transport capacity, not the remote's cached 9000"
1869 );
1870 }
1871
1872 #[tokio::test]
1877 async fn v3_noauth_client_rejects_received_auth_response() {
1878 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 1)]);
1879 let response = build_response(PduType::Response, 123, 1, 1001, Some(b"authpass12345678"));
1880 let client = canned_client(response, 1, 1000, UsmConfig::new("user"));
1881
1882 let err = client.send_v3_and_recv(pdu).await.unwrap_err();
1883 assert!(
1884 matches!(*err, Error::Auth { .. }),
1885 "expected Auth error for unverifiable received auth, got: {err}"
1886 );
1887 }
1888
1889 #[tokio::test]
1892 async fn v3_rejects_echoed_request_pdu() {
1893 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 1)]);
1894 let response = build_response(PduType::GetRequest, 123, 1, 1001, None);
1895 let client = canned_client(response, 1, 1000, UsmConfig::new("user"));
1896
1897 let err = client.send_v3_and_recv(pdu).await.unwrap_err();
1898 assert!(
1899 matches!(*err, Error::MalformedResponse { .. }),
1900 "expected MalformedResponse, got: {err}"
1901 );
1902 }
1903
1904 #[tokio::test]
1907 async fn v3_accepts_timely_authenticated_response() {
1908 let security = UsmConfig::new("user").auth(AuthProtocol::Sha1, "authpass12345678");
1909 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 1)]);
1910 let response = build_response(PduType::Response, 123, 1, 1200, Some(b"authpass12345678"));
1911 let client = canned_client(response, 1, 1000, security);
1912
1913 let result = client.send_v3_and_recv(pdu).await;
1914 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1915 }
1916
1917 #[tokio::test]
1921 async fn v3_rejects_stale_authenticated_response() {
1922 let security = UsmConfig::new("user").auth(AuthProtocol::Sha1, "authpass12345678");
1923 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 1)]);
1924 let response = build_response(PduType::Response, 123, 1, 500, Some(b"authpass12345678"));
1926 let client = canned_client(response, 1, 1000, security);
1927
1928 let err = client.send_v3_and_recv(pdu).await.unwrap_err();
1929 assert!(
1930 matches!(*err, Error::Auth { .. }),
1931 "expected Auth error, got: {err}"
1932 );
1933 }
1934}