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, SecurityLevel, V3Message};
11use crate::pdu::{Pdu, PduType};
12use crate::transport::Transport;
13use crate::v3::{AuthProtocol, PrivProtocol};
14use crate::v3::{
15 LocalizedKey, PrivKey, UsmSecurityParams,
16 auth::{authenticate_message, verify_message},
17 is_not_in_time_window_report, is_unknown_engine_id_report,
18};
19use bytes::Bytes;
20use std::time::Instant;
21use tracing::{Span, instrument};
22
23use super::Client;
24
25#[derive(Clone)]
37pub struct V3SecurityConfig {
38 pub username: Bytes,
40 pub auth: Option<(AuthProtocol, Vec<u8>)>,
42 pub privacy: Option<(PrivProtocol, Vec<u8>)>,
44 pub master_keys: Option<crate::v3::MasterKeys>,
46}
47
48impl V3SecurityConfig {
49 pub fn new(username: impl Into<Bytes>) -> Self {
51 Self {
52 username: username.into(),
53 auth: None,
54 privacy: None,
55 master_keys: None,
56 }
57 }
58
59 pub fn auth(mut self, protocol: AuthProtocol, password: impl Into<Vec<u8>>) -> Self {
61 self.auth = Some((protocol, password.into()));
62 self
63 }
64
65 pub fn privacy(mut self, protocol: PrivProtocol, password: impl Into<Vec<u8>>) -> Self {
67 self.privacy = Some((protocol, password.into()));
68 self
69 }
70
71 pub fn with_master_keys(mut self, master_keys: crate::v3::MasterKeys) -> Self {
77 self.master_keys = Some(master_keys);
78 self
79 }
80
81 pub fn security_level(&self) -> SecurityLevel {
83 if let Some(ref master_keys) = self.master_keys {
85 if master_keys.priv_protocol().is_some() {
86 return SecurityLevel::AuthPriv;
87 }
88 return SecurityLevel::AuthNoPriv;
89 }
90
91 match (&self.auth, &self.privacy) {
92 (None, _) => SecurityLevel::NoAuthNoPriv,
93 (Some(_), None) => SecurityLevel::AuthNoPriv,
94 (Some(_), Some(_)) => SecurityLevel::AuthPriv,
95 }
96 }
97
98 pub fn derive_keys(&self, engine_id: &[u8]) -> V3DerivedKeys {
104 if let Some(ref master_keys) = self.master_keys {
106 tracing::trace!(target: "async_snmp::client", { engine_id_len = engine_id.len(), auth_protocol = ?master_keys.auth_protocol(), priv_protocol = ?master_keys.priv_protocol() }, "localizing from cached master keys");
107 let (auth_key, priv_key) = master_keys.localize(engine_id);
108 tracing::trace!(target: "async_snmp::client", "key localization complete");
109 return V3DerivedKeys {
110 auth_key: Some(auth_key),
111 priv_key,
112 };
113 }
114
115 tracing::trace!(target: "async_snmp::client", { engine_id_len = engine_id.len(), has_auth = self.auth.is_some(), has_priv = self.privacy.is_some() }, "deriving localized keys from passwords");
117
118 let auth_key = self.auth.as_ref().map(|(protocol, password)| {
119 tracing::trace!(target: "async_snmp::client", { auth_protocol = ?protocol }, "deriving auth key");
120 LocalizedKey::from_password(*protocol, password, engine_id)
121 });
122
123 let priv_key = match (&self.auth, &self.privacy) {
124 (Some((auth_protocol, _)), Some((priv_protocol, priv_password))) => {
125 tracing::trace!(target: "async_snmp::client", { priv_protocol = ?priv_protocol }, "deriving privacy key");
126 Some(PrivKey::from_password(
127 *auth_protocol,
128 *priv_protocol,
129 priv_password,
130 engine_id,
131 ))
132 }
133 _ => None,
134 };
135
136 tracing::trace!(target: "async_snmp::client", "key derivation complete");
137 V3DerivedKeys { auth_key, priv_key }
138 }
139}
140
141impl std::fmt::Debug for V3SecurityConfig {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.debug_struct("V3SecurityConfig")
144 .field("username", &String::from_utf8_lossy(&self.username))
145 .field("auth", &self.auth.as_ref().map(|(p, _)| p))
146 .field("privacy", &self.privacy.as_ref().map(|(p, _)| p))
147 .finish()
148 }
149}
150
151pub struct V3DerivedKeys {
153 pub auth_key: Option<LocalizedKey>,
154 pub priv_key: Option<PrivKey>,
155}
156
157impl<T: Transport> Client<T> {
159 #[instrument(level = "debug", skip(self), fields(snmp.target = %self.peer_addr()))]
161 pub(super) async fn ensure_engine_discovered(&self) -> Result<()> {
162 {
164 let state = self.inner.engine_state.read().unwrap();
165 if state.is_some() {
166 return Ok(());
167 }
168 }
169
170 if let Some(cache) = &self.inner.engine_cache
172 && let Some(cached_state) = cache.get(&self.peer_addr())
173 {
174 tracing::debug!(target: "async_snmp::client", "using cached engine state");
175 let mut state = self.inner.engine_state.write().unwrap();
176 *state = Some(cached_state.clone());
177 if let Some(security) = &self.inner.config.v3_security {
179 let keys = security.derive_keys(&cached_state.engine_id);
180 let mut derived = self.inner.derived_keys.write().unwrap();
181 *derived = Some(keys);
182 }
183 return Ok(());
184 }
185
186 tracing::debug!(target: "async_snmp::client", "performing engine discovery");
188 let msg_id = self.next_request_id();
189 let discovery_msg = V3Message::discovery_request(msg_id);
190 let discovery_data = discovery_msg.encode();
191
192 self.inner
194 .transport
195 .register_request(msg_id, self.inner.config.timeout);
196 self.inner.transport.send(&discovery_data).await?;
197 let (response_data, _source) = self.inner.transport.recv(msg_id).await?;
198
199 let response = V3Message::decode(response_data)?;
201
202 let reported_msg_max_size = response.global_data.msg_max_size as u32;
203 let session_max = self.inner.transport.max_message_size();
204 let engine_state = crate::v3::parse_discovery_response_with_limits(
205 &response.security_params,
206 reported_msg_max_size,
207 session_max,
208 )?;
209 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");
210
211 if let Some(security) = &self.inner.config.v3_security {
213 let keys = security.derive_keys(&engine_state.engine_id);
214 let mut derived = self.inner.derived_keys.write().unwrap();
215 *derived = Some(keys);
216 }
217
218 {
220 let mut state = self.inner.engine_state.write().unwrap();
221 *state = Some(engine_state.clone());
222 }
223
224 if let Some(cache) = &self.inner.engine_cache {
226 cache.insert(self.peer_addr(), engine_state);
227 }
228
229 Ok(())
230 }
231
232 pub(super) fn build_v3_message(&self, pdu: &Pdu, msg_id: i32) -> Result<Vec<u8>> {
237 let security = self.inner.config.v3_security.as_ref().ok_or_else(|| {
238 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoSecurityConfig }, "V3 security not configured");
239 Error::Config("V3 security not configured".into()).boxed()
240 })?;
241
242 let engine_state = self.inner.engine_state.read().unwrap();
243 let engine_state = engine_state.as_ref().ok_or_else(|| {
244 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::EngineNotDiscovered }, "engine not discovered");
245 Error::Config("engine not discovered".into()).boxed()
246 })?;
247
248 let derived = self.inner.derived_keys.read().unwrap();
249
250 let security_level = security.security_level();
251
252 let scoped_pdu = ScopedPdu::new(
254 engine_state.engine_id.clone(),
255 Bytes::new(), pdu.clone(),
257 );
258
259 let engine_boots = engine_state.engine_boots;
261 let engine_time = engine_state.estimated_time();
262
263 let (msg_data, priv_params) = if security_level.requires_priv() {
265 tracing::trace!(target: "async_snmp::client", "encrypting scoped PDU");
266
267 let derived_ref = derived.as_ref().ok_or_else(|| {
270 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::KeysNotDerived }, "keys not derived");
271 Error::Config("keys not derived".into()).boxed()
272 })?;
273 let mut priv_key = derived_ref
274 .priv_key
275 .as_ref()
276 .ok_or_else(|| {
277 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoPrivKey }, "privacy key not available");
278 Error::Config("privacy key not available".into()).boxed()
279 })?
280 .clone();
281
282 let scoped_pdu_bytes = scoped_pdu.encode_to_bytes();
284
285 let (ciphertext, salt) = priv_key
287 .encrypt(
288 &scoped_pdu_bytes,
289 engine_boots,
290 engine_time,
291 Some(&self.inner.salt_counter),
292 )
293 .map_err(|e| {
294 tracing::warn!(target: "async_snmp::crypto", { peer = %self.peer_addr(), error = %e }, "encryption failed");
295 Error::Auth {
296 target: self.peer_addr(),
297 }
298 .boxed()
299 })?;
300
301 tracing::trace!(target: "async_snmp::client", { plaintext_len = scoped_pdu_bytes.len(), ciphertext_len = ciphertext.len() }, "encrypted scoped PDU");
302
303 (crate::message::V3MessageData::Encrypted(ciphertext), salt)
304 } else {
305 (
306 crate::message::V3MessageData::Plaintext(scoped_pdu),
307 Bytes::new(),
308 )
309 };
310
311 let mac_len = if security_level.requires_auth() {
313 derived
314 .as_ref()
315 .and_then(|d| d.auth_key.as_ref())
316 .map(|k| k.mac_len())
317 .unwrap_or(12)
318 } else {
319 0
320 };
321
322 let mut usm_params = UsmSecurityParams::new(
323 engine_state.engine_id.clone(),
324 engine_boots,
325 engine_time,
326 security.username.clone(),
327 );
328
329 if security_level.requires_auth() {
330 usm_params = usm_params.with_auth_placeholder(mac_len);
331 }
332
333 if security_level.requires_priv() {
334 usm_params = usm_params.with_priv_params(priv_params);
335 }
336
337 let usm_encoded = usm_params.encode();
338
339 let msg_flags = MsgFlags::new(security_level, true); let global_data = MsgGlobalData::new(msg_id, 65507, msg_flags);
342
343 let msg = match msg_data {
345 crate::message::V3MessageData::Plaintext(scoped_pdu) => {
346 V3Message::new(global_data, usm_encoded, scoped_pdu)
347 }
348 crate::message::V3MessageData::Encrypted(ciphertext) => {
349 V3Message::new_encrypted(global_data, usm_encoded, ciphertext)
350 }
351 };
352
353 let mut encoded = msg.encode().to_vec();
354
355 if security_level.requires_auth() {
357 tracing::trace!(target: "async_snmp::client", "applying HMAC authentication");
358
359 let auth_key = derived
360 .as_ref()
361 .and_then(|d| d.auth_key.as_ref())
362 .ok_or_else(|| {
363 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::MissingAuthKey }, "auth key not available for encoding");
364 Error::Config("auth key not available".into()).boxed()
365 })?;
366
367 if let Some((offset, len)) = UsmSecurityParams::find_auth_params_offset(&encoded) {
369 authenticate_message(auth_key, &mut encoded, offset, len);
370 tracing::trace!(target: "async_snmp::client", { auth_params_offset = offset, auth_params_len = len }, "applied HMAC authentication");
371 } else {
372 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::MissingAuthParams }, "could not find auth params position");
373 return Err(Error::Config("could not find auth params position".into()).boxed());
374 }
375 }
376
377 Ok(encoded)
378 }
379
380 #[instrument(
382 level = "debug",
383 skip(self, pdu),
384 fields(
385 snmp.target = %self.peer_addr(),
386 snmp.request_id = pdu.request_id,
387 snmp.security_level = ?self.inner.config.v3_security.as_ref().map(|s| s.security_level()),
388 snmp.attempt = tracing::field::Empty,
389 snmp.elapsed_ms = tracing::field::Empty,
390 )
391 )]
392 pub(super) async fn send_v3_and_recv(&self, pdu: Pdu) -> Result<Pdu> {
393 let start = Instant::now();
394
395 self.ensure_engine_discovered().await?;
397
398 let security = self.inner.config.v3_security.as_ref().ok_or_else(|| {
399 tracing::debug!(target: "async_snmp::client", { kind = %EncodeErrorKind::NoSecurityConfig }, "V3 security not configured");
400 Error::Config("V3 security not configured".into()).boxed()
401 })?;
402 let security_level = security.security_level();
403
404 let mut last_error: Option<Box<Error>> = None;
405 let max_attempts = if self.inner.transport.is_reliable() {
406 0
407 } else {
408 self.inner.config.retry.max_attempts
409 };
410
411 for attempt in 0..=max_attempts {
412 Span::current().record("snmp.attempt", attempt);
413 if attempt > 0 {
414 tracing::debug!(target: "async_snmp::client", "retrying V3 request");
415 }
416
417 let msg_id = self.next_request_id();
419 let data = self.build_v3_message(&pdu, msg_id)?;
420
421 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);
422 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending V3 request");
423
424 self.inner
426 .transport
427 .register_request(msg_id, self.inner.config.timeout);
428
429 self.inner.transport.send(&data).await?;
431
432 match self.inner.transport.recv(msg_id).await {
434 Ok((response_data, _source)) => {
435 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received V3 response");
436
437 if security_level.requires_auth() {
439 tracing::trace!(target: "async_snmp::client", "verifying HMAC authentication on response");
440
441 let derived = self.inner.derived_keys.read().unwrap();
442 let auth_key = derived
443 .as_ref()
444 .and_then(|d| d.auth_key.as_ref())
445 .ok_or_else(|| {
446 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::NoAuthKey }, "authentication failed");
447 Error::Auth {
448 target: self.peer_addr(),
449 }
450 .boxed()
451 })?;
452
453 if let Some((offset, len)) =
454 UsmSecurityParams::find_auth_params_offset(&response_data)
455 {
456 if !verify_message(auth_key, &response_data, offset, len) {
457 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::HmacMismatch }, "authentication failed");
458 return Err(Error::Auth {
459 target: self.peer_addr(),
460 }
461 .boxed());
462 }
463 tracing::trace!(target: "async_snmp::client", { auth_params_offset = offset, auth_params_len = len }, "HMAC verification successful");
464 } else {
465 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %AuthErrorKind::AuthParamsNotFound }, "authentication failed");
466 return Err(Error::Auth {
467 target: self.peer_addr(),
468 }
469 .boxed());
470 }
471 }
472
473 let response = V3Message::decode(response_data.clone())?;
475
476 if let Some(scoped_pdu) = response.scoped_pdu()
478 && scoped_pdu.pdu.pdu_type == PduType::Report
479 {
480 if is_not_in_time_window_report(&scoped_pdu.pdu) {
482 tracing::debug!(target: "async_snmp::client", "not in time window, resyncing");
483 let usm_params =
485 UsmSecurityParams::decode(response.security_params.clone())?;
486 {
487 let mut state = self.inner.engine_state.write().unwrap();
488 if let Some(ref mut s) = *state {
489 s.update_time(usm_params.engine_boots, usm_params.engine_time);
490 }
491 }
492 last_error = Some(
493 Error::Auth {
494 target: self.peer_addr(),
495 }
496 .boxed(),
497 );
498 if attempt < max_attempts {
500 let delay = self.inner.config.retry.compute_delay(attempt);
501 if !delay.is_zero() {
502 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
503 tokio::time::sleep(delay).await;
504 }
505 }
506 continue;
507 }
508
509 if is_unknown_engine_id_report(&scoped_pdu.pdu) {
511 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "unknown engine ID");
512 return Err(Error::Auth {
513 target: self.peer_addr(),
514 }
515 .boxed());
516 }
517
518 return Err(Error::Snmp {
520 target: self.peer_addr(),
521 status: ErrorStatus::GenErr,
522 index: 0,
523 oid: scoped_pdu.pdu.varbinds.first().map(|vb| vb.oid.clone()),
524 }
525 .boxed());
526 }
527
528 let response_security_params = response.security_params.clone();
530
531 let response_pdu = if security_level.requires_priv() {
533 match response.data {
534 crate::message::V3MessageData::Encrypted(ciphertext) => {
535 tracing::trace!(target: "async_snmp::client", { ciphertext_len = ciphertext.len() }, "decrypting response");
536
537 let derived = self.inner.derived_keys.read().unwrap();
539 let priv_key = derived
540 .as_ref()
541 .and_then(|d| d.priv_key.as_ref())
542 .ok_or_else(|| {
543 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %CryptoErrorKind::NoPrivKey }, "decryption failed");
544 Error::Auth {
545 target: self.peer_addr(),
546 }
547 .boxed()
548 })?;
549
550 let usm_params =
551 UsmSecurityParams::decode(response_security_params.clone())?;
552 let plaintext = priv_key
553 .decrypt(
554 &ciphertext,
555 usm_params.engine_boots,
556 usm_params.engine_time,
557 &usm_params.priv_params,
558 )
559 .map_err(|e| {
560 tracing::warn!(target: "async_snmp::crypto", { peer = %self.peer_addr(), error = %e }, "decryption failed");
561 Error::Auth {
562 target: self.peer_addr(),
563 }
564 .boxed()
565 })?;
566
567 tracing::trace!(target: "async_snmp::client", { plaintext_len = plaintext.len() }, "decrypted response");
568
569 let mut decoder = Decoder::with_target(plaintext, self.peer_addr());
571 let scoped_pdu = ScopedPdu::decode(&mut decoder)?;
572 scoped_pdu.pdu
573 }
574 crate::message::V3MessageData::Plaintext(scoped_pdu) => scoped_pdu.pdu,
575 }
576 } else {
577 response.into_pdu().ok_or_else(|| {
578 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::MissingPdu }, "missing PDU in response");
579 Error::MalformedResponse {
580 target: self.peer_addr(),
581 }
582 .boxed()
583 })?
584 };
585
586 if response_pdu.request_id != pdu.request_id {
588 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");
589 return Err(Error::MalformedResponse {
590 target: self.peer_addr(),
591 }
592 .boxed());
593 }
594
595 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);
596
597 {
599 let usm_params = UsmSecurityParams::decode(response_security_params)?;
600 let mut state = self.inner.engine_state.write().unwrap();
601 if let Some(ref mut s) = *state {
602 s.update_time(usm_params.engine_boots, usm_params.engine_time);
603 }
604 }
605
606 if response_pdu.is_error() {
608 let status = response_pdu.error_status_enum();
609 let oid = (response_pdu.error_index as usize)
611 .checked_sub(1)
612 .and_then(|idx| response_pdu.varbinds.get(idx))
613 .map(|vb| vb.oid.clone());
614
615 Span::current()
616 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
617 return Err(Error::Snmp {
618 target: self.peer_addr(),
619 status,
620 index: response_pdu.error_index.max(0) as u32,
621 oid,
622 }
623 .boxed());
624 }
625
626 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
627 return Ok(response_pdu);
628 }
629 Err(e) if matches!(*e, Error::Timeout { .. }) => {
630 last_error = Some(e);
631 if attempt < max_attempts {
633 let delay = self.inner.config.retry.compute_delay(attempt);
634 if !delay.is_zero() {
635 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
636 tokio::time::sleep(delay).await;
637 }
638 }
639 continue;
640 }
641 Err(e) => {
642 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
643 return Err(e);
644 }
645 }
646 }
647
648 let elapsed = start.elapsed();
650 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
651 tracing::debug!(target: "async_snmp::client", { request_id = pdu.request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
652 Err(last_error.unwrap_or_else(|| {
653 Error::Timeout {
654 target: self.peer_addr(),
655 elapsed,
656 retries: max_attempts,
657 }
658 .boxed()
659 }))
660 }
661}