1use crate::ber::Decoder;
7use crate::error::internal::{AuthErrorKind, CryptoErrorKind, DecodeErrorKind, EncodeErrorKind};
8use crate::error::{Error, ErrorStatus, Result};
9use crate::format::hex;
10use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
11use crate::pdu::{Pdu, PduType};
12use crate::transport::Transport;
13use crate::v3::{
14 UsmSecurityParams,
15 auth::{authenticate_message, verify_message},
16 is_not_in_time_window_report, is_unknown_engine_id_report,
17};
18use bytes::Bytes;
19use std::time::Instant;
20use tracing::{Span, instrument};
21
22use super::Client;
23
24impl<T: Transport> Client<T> {
26 #[instrument(level = "debug", skip(self), fields(snmp.target = %self.peer_addr()))]
28 pub(super) async fn ensure_engine_discovered(&self) -> Result<()> {
29 {
31 let state = self
32 .inner
33 .engine_state
34 .read()
35 .expect("engine_state lock poisoned");
36 if state.is_some() {
37 return Ok(());
38 }
39 }
40
41 if let Some(cache) = &self.inner.engine_cache
43 && let Some(cached_state) = cache.get(&self.peer_addr())
44 {
45 tracing::debug!(target: "async_snmp::client", "using cached engine state");
46 let mut state = self
47 .inner
48 .engine_state
49 .write()
50 .expect("engine_state lock poisoned");
51 *state = Some(cached_state.clone());
52 if let Some(security) = &self.inner.config.v3_security {
54 let keys = security.derive_keys(&cached_state.engine_id);
55 let mut derived = self
56 .inner
57 .derived_keys
58 .write()
59 .expect("derived_keys lock poisoned");
60 *derived = Some(keys);
61 }
62 return Ok(());
63 }
64
65 tracing::debug!(target: "async_snmp::client", "performing engine discovery");
67 let msg_id = self.next_request_id();
68 let discovery_msg = V3Message::discovery_request(msg_id);
69 let discovery_data = discovery_msg.encode();
70
71 self.inner
73 .transport
74 .register_request(msg_id, self.inner.config.timeout);
75 self.inner.transport.send(&discovery_data).await?;
76 let (response_data, _source) = self.inner.transport.recv(msg_id).await?;
77
78 let response = V3Message::decode(response_data)?;
80
81 let reported_msg_max_size = response.global_data.msg_max_size as u32;
82 let session_max = self.inner.transport.max_message_size();
83 let engine_state = crate::v3::parse_discovery_response_with_limits(
84 &response.security_params,
85 reported_msg_max_size,
86 session_max,
87 )?;
88 tracing::debug!(target: "async_snmp::client", { snmp.engine_id = %hex::Bytes(&engine_state.engine_id), snmp.engine_boots = engine_state.engine_boots, snmp.engine_time = engine_state.engine_time, snmp.msg_max_size = engine_state.msg_max_size }, "discovered engine");
89
90 if let Some(security) = &self.inner.config.v3_security {
92 let keys = security.derive_keys(&engine_state.engine_id);
93 let mut derived = self
94 .inner
95 .derived_keys
96 .write()
97 .expect("derived_keys lock poisoned");
98 *derived = Some(keys);
99 }
100
101 {
103 let mut state = self
104 .inner
105 .engine_state
106 .write()
107 .expect("engine_state lock poisoned");
108 *state = Some(engine_state.clone());
109 }
110
111 if let Some(cache) = &self.inner.engine_cache {
113 cache.insert(self.peer_addr(), engine_state);
114 }
115
116 Ok(())
117 }
118
119 pub(super) fn build_v3_message(&self, pdu: &Pdu, msg_id: i32) -> Result<Vec<u8>> {
124 let security = self.inner.config.v3_security.as_ref().ok_or_else(|| {
125 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoSecurityConfig }, "V3 security not configured");
126 Error::Config("V3 security not configured".into()).boxed()
127 })?;
128
129 let engine_state = self
130 .inner
131 .engine_state
132 .read()
133 .expect("engine_state lock poisoned");
134 let engine_state = engine_state.as_ref().ok_or_else(|| {
135 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::EngineNotDiscovered }, "engine not discovered");
136 Error::Config("engine not discovered".into()).boxed()
137 })?;
138
139 let derived = self
140 .inner
141 .derived_keys
142 .read()
143 .expect("derived_keys lock poisoned");
144
145 let security_level = security.security_level();
146
147 let scoped_pdu = ScopedPdu::new(
149 engine_state.engine_id.clone(),
150 Bytes::new(), pdu.clone(),
152 );
153
154 let engine_boots = engine_state.engine_boots;
156 let engine_time = engine_state.estimated_time();
157
158 let (msg_data, priv_params) = if security_level.requires_priv() {
160 tracing::trace!(target: "async_snmp::client", "encrypting scoped PDU");
161
162 let derived_ref = derived.as_ref().ok_or_else(|| {
165 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::KeysNotDerived }, "keys not derived");
166 Error::Config("keys not derived".into()).boxed()
167 })?;
168 let mut priv_key = derived_ref
169 .priv_key
170 .as_ref()
171 .ok_or_else(|| {
172 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoPrivKey }, "privacy key not available");
173 Error::Config("privacy key not available".into()).boxed()
174 })?
175 .clone();
176
177 let scoped_pdu_bytes = scoped_pdu.encode_to_bytes();
179
180 let (ciphertext, salt) = priv_key
182 .encrypt(
183 &scoped_pdu_bytes,
184 engine_boots,
185 engine_time,
186 Some(&self.inner.salt_counter),
187 )
188 .map_err(|e| {
189 tracing::warn!(target: "async_snmp::crypto", { peer = %self.peer_addr(), error = %e }, "encryption failed");
190 Error::Auth {
191 target: self.peer_addr(),
192 }
193 .boxed()
194 })?;
195
196 tracing::trace!(target: "async_snmp::client", { plaintext_len = scoped_pdu_bytes.len(), ciphertext_len = ciphertext.len() }, "encrypted scoped PDU");
197
198 (crate::message::V3MessageData::Encrypted(ciphertext), salt)
199 } else {
200 (
201 crate::message::V3MessageData::Plaintext(scoped_pdu),
202 Bytes::new(),
203 )
204 };
205
206 let mac_len = if security_level.requires_auth() {
208 derived
209 .as_ref()
210 .and_then(|d| d.auth_key.as_ref())
211 .map(|k| k.mac_len())
212 .unwrap_or(12)
213 } else {
214 0
215 };
216
217 let mut usm_params = UsmSecurityParams::new(
218 engine_state.engine_id.clone(),
219 engine_boots,
220 engine_time,
221 security.username.clone(),
222 );
223
224 if security_level.requires_auth() {
225 usm_params = usm_params.with_auth_placeholder(mac_len);
226 }
227
228 if security_level.requires_priv() {
229 usm_params = usm_params.with_priv_params(priv_params);
230 }
231
232 let usm_encoded = usm_params.encode();
233
234 let msg_flags = MsgFlags::new(security_level, true); let global_data = MsgGlobalData::new(msg_id, 65507, msg_flags);
237
238 let msg = match msg_data {
240 crate::message::V3MessageData::Plaintext(scoped_pdu) => {
241 V3Message::new(global_data, usm_encoded, scoped_pdu)
242 }
243 crate::message::V3MessageData::Encrypted(ciphertext) => {
244 V3Message::new_encrypted(global_data, usm_encoded, ciphertext)
245 }
246 };
247
248 let mut encoded = msg.encode().to_vec();
249
250 if security_level.requires_auth() {
252 tracing::trace!(target: "async_snmp::client", "applying HMAC authentication");
253
254 let auth_key = derived
255 .as_ref()
256 .and_then(|d| d.auth_key.as_ref())
257 .ok_or_else(|| {
258 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::MissingAuthKey }, "auth key not available for encoding");
259 Error::Config("auth key not available".into()).boxed()
260 })?;
261
262 if let Some((offset, len)) = UsmSecurityParams::find_auth_params_offset(&encoded) {
264 authenticate_message(auth_key, &mut encoded, offset, len);
265 tracing::trace!(target: "async_snmp::client", { auth_params_offset = offset, auth_params_len = len }, "applied HMAC authentication");
266 } else {
267 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::MissingAuthParams }, "could not find auth params position");
268 return Err(Error::Config("could not find auth params position".into()).boxed());
269 }
270 }
271
272 Ok(encoded)
273 }
274
275 fn verify_response_auth(&self, response_data: &[u8]) -> Result<()> {
277 tracing::trace!(target: "async_snmp::client", "verifying HMAC authentication on response");
278
279 let derived = self
280 .inner
281 .derived_keys
282 .read()
283 .expect("derived_keys lock poisoned");
284 let auth_key = derived
285 .as_ref()
286 .and_then(|d| d.auth_key.as_ref())
287 .ok_or_else(|| {
288 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::NoAuthKey }, "authentication failed");
289 Error::Auth {
290 target: self.peer_addr(),
291 }
292 .boxed()
293 })?;
294
295 let (offset, len) = UsmSecurityParams::find_auth_params_offset(response_data).ok_or_else(
296 || {
297 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::AuthParamsNotFound }, "authentication failed");
298 Error::Auth {
299 target: self.peer_addr(),
300 }
301 .boxed()
302 },
303 )?;
304
305 if !verify_message(auth_key, response_data, offset, len) {
306 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::HmacMismatch }, "authentication failed");
307 return Err(Error::Auth {
308 target: self.peer_addr(),
309 }
310 .boxed());
311 }
312
313 tracing::trace!(target: "async_snmp::client", { auth_params_offset = offset, auth_params_len = len }, "HMAC verification successful");
314 Ok(())
315 }
316
317 fn decrypt_response_pdu(&self, response: V3Message, security_params: &Bytes) -> Result<Pdu> {
319 match response.data {
320 crate::message::V3MessageData::Encrypted(ciphertext) => {
321 tracing::trace!(target: "async_snmp::client", { ciphertext_len = ciphertext.len() }, "decrypting response");
322
323 let derived = self
324 .inner
325 .derived_keys
326 .read()
327 .expect("derived_keys lock poisoned");
328 let priv_key =
329 derived
330 .as_ref()
331 .and_then(|d| d.priv_key.as_ref())
332 .ok_or_else(|| {
333 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %CryptoErrorKind::NoPrivKey }, "decryption failed");
334 Error::Auth {
335 target: self.peer_addr(),
336 }
337 .boxed()
338 })?;
339
340 let usm_params = UsmSecurityParams::decode(security_params.clone())?;
341 let plaintext = priv_key
342 .decrypt(
343 &ciphertext,
344 usm_params.engine_boots,
345 usm_params.engine_time,
346 &usm_params.priv_params,
347 )
348 .map_err(|e| {
349 tracing::warn!(target: "async_snmp::crypto", { peer = %self.peer_addr(), error = %e }, "decryption failed");
350 Error::Auth {
351 target: self.peer_addr(),
352 }
353 .boxed()
354 })?;
355
356 tracing::trace!(target: "async_snmp::client", { plaintext_len = plaintext.len() }, "decrypted response");
357
358 let mut decoder = Decoder::with_target(plaintext, self.peer_addr());
359 let scoped_pdu = ScopedPdu::decode(&mut decoder)?;
360 Ok(scoped_pdu.pdu)
361 }
362 crate::message::V3MessageData::Plaintext(scoped_pdu) => Ok(scoped_pdu.pdu),
363 }
364 }
365
366 #[instrument(
368 level = "debug",
369 skip(self, pdu),
370 fields(
371 snmp.target = %self.peer_addr(),
372 snmp.request_id = pdu.request_id,
373 snmp.security_level = ?self.inner.config.v3_security.as_ref().map(|s| s.security_level()),
374 snmp.attempt = tracing::field::Empty,
375 snmp.elapsed_ms = tracing::field::Empty,
376 )
377 )]
378 pub(super) async fn send_v3_and_recv(&self, pdu: Pdu) -> Result<Pdu> {
379 let start = Instant::now();
380
381 self.ensure_engine_discovered().await?;
383
384 let security = self.inner.config.v3_security.as_ref().ok_or_else(|| {
385 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoSecurityConfig }, "V3 security not configured");
386 Error::Config("V3 security not configured".into()).boxed()
387 })?;
388 let security_level = security.security_level();
389
390 let mut last_error: Option<Box<Error>> = None;
391 let max_attempts = if self.inner.transport.is_reliable() {
392 0
393 } else {
394 self.inner.config.retry.max_attempts
395 };
396
397 for attempt in 0..=max_attempts {
398 Span::current().record("snmp.attempt", attempt);
399 if attempt > 0 {
400 tracing::debug!(target: "async_snmp::client", "retrying V3 request");
401 }
402
403 let msg_id = self.next_request_id();
405 let data = self.build_v3_message(&pdu, msg_id)?;
406
407 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);
408 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending V3 request");
409
410 self.inner
412 .transport
413 .register_request(msg_id, self.inner.config.timeout);
414
415 self.inner.transport.send(&data).await?;
417
418 match self.inner.transport.recv(msg_id).await {
420 Ok((response_data, _source)) => {
421 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received V3 response");
422
423 if security_level.requires_auth() {
425 self.verify_response_auth(&response_data)?;
426 }
427
428 let response = V3Message::decode(response_data)?;
430
431 if let Some(scoped_pdu) = response.scoped_pdu()
433 && scoped_pdu.pdu.pdu_type == PduType::Report
434 {
435 if is_not_in_time_window_report(&scoped_pdu.pdu) {
437 tracing::debug!(target: "async_snmp::client", "not in time window, resyncing");
438 let usm_params =
440 UsmSecurityParams::decode(response.security_params.clone())?;
441 {
442 let mut state = self
443 .inner
444 .engine_state
445 .write()
446 .expect("engine_state lock poisoned");
447 if let Some(ref mut s) = *state {
448 s.update_time(usm_params.engine_boots, usm_params.engine_time);
449 }
450 }
451 last_error = Some(
452 Error::Auth {
453 target: self.peer_addr(),
454 }
455 .boxed(),
456 );
457 if attempt < max_attempts {
459 let delay = self.inner.config.retry.compute_delay(attempt);
460 if !delay.is_zero() {
461 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
462 tokio::time::sleep(delay).await;
463 }
464 }
465 continue;
466 }
467
468 if is_unknown_engine_id_report(&scoped_pdu.pdu) {
470 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "unknown engine ID");
471 return Err(Error::Auth {
472 target: self.peer_addr(),
473 }
474 .boxed());
475 }
476
477 return Err(Error::Snmp {
479 target: self.peer_addr(),
480 status: ErrorStatus::GenErr,
481 index: 0,
482 oid: scoped_pdu.pdu.varbinds.first().map(|vb| vb.oid.clone()),
483 }
484 .boxed());
485 }
486
487 let response_security_params = response.security_params.clone();
489
490 let response_pdu = if security_level.requires_priv() {
492 self.decrypt_response_pdu(response, &response_security_params)?
493 } else {
494 response.into_pdu().ok_or_else(|| {
495 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::MissingPdu }, "missing PDU in response");
496 Error::MalformedResponse {
497 target: self.peer_addr(),
498 }
499 .boxed()
500 })?
501 };
502
503 if response_pdu.request_id != pdu.request_id {
505 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");
506 return Err(Error::MalformedResponse {
507 target: self.peer_addr(),
508 }
509 .boxed());
510 }
511
512 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);
513
514 {
516 let usm_params = UsmSecurityParams::decode(response_security_params)?;
517 let mut state = self
518 .inner
519 .engine_state
520 .write()
521 .expect("engine_state lock poisoned");
522 if let Some(ref mut s) = *state {
523 s.update_time(usm_params.engine_boots, usm_params.engine_time);
524 }
525 }
526
527 if response_pdu.is_error() {
529 let status = response_pdu.error_status_enum();
530 let oid = (response_pdu.error_index as usize)
532 .checked_sub(1)
533 .and_then(|idx| response_pdu.varbinds.get(idx))
534 .map(|vb| vb.oid.clone());
535
536 Span::current()
537 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
538 return Err(Error::Snmp {
539 target: self.peer_addr(),
540 status,
541 index: response_pdu.error_index.max(0) as u32,
542 oid,
543 }
544 .boxed());
545 }
546
547 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
548 return Ok(response_pdu);
549 }
550 Err(e) if matches!(*e, Error::Timeout { .. }) => {
551 last_error = Some(e);
552 if attempt < max_attempts {
554 let delay = self.inner.config.retry.compute_delay(attempt);
555 if !delay.is_zero() {
556 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
557 tokio::time::sleep(delay).await;
558 }
559 }
560 continue;
561 }
562 Err(e) => {
563 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
564 return Err(e);
565 }
566 }
567 }
568
569 let elapsed = start.elapsed();
571 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
572 tracing::debug!(target: "async_snmp::client", { request_id = pdu.request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
573 Err(last_error.unwrap_or_else(|| {
574 Error::Timeout {
575 target: self.peer_addr(),
576 elapsed,
577 retries: max_attempts,
578 }
579 .boxed()
580 }))
581 }
582}