async_snmp/client/
v3.rs

1//! SNMPv3-specific client functionality.
2//!
3//! This module contains V3 security configuration, key derivation, engine discovery,
4//! and V3 message building/handling.
5
6use 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
24// V3-specific Client implementation
25impl<T: Transport> Client<T> {
26    /// Ensure engine ID is discovered for V3 operations.
27    #[instrument(level = "debug", skip(self), fields(snmp.target = %self.peer_addr()))]
28    pub(super) async fn ensure_engine_discovered(&self) -> Result<()> {
29        // Check if already discovered
30        {
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        // Check shared cache first
42        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            // Derive keys for this engine
53            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        // Perform discovery
66        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        // Register request and send discovery
72        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        // Parse response
79        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        // Derive keys for this engine
91        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        // Store in local cache
102        {
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        // Store in shared cache if present
112        if let Some(cache) = &self.inner.engine_cache {
113            cache.insert(self.peer_addr(), engine_state);
114        }
115
116        Ok(())
117    }
118
119    /// Build and encode a V3 message with authentication and/or encryption.
120    ///
121    /// The `msg_id` parameter is separate from `pdu.request_id` per RFC 3412
122    /// Section 6.2: retransmissions SHOULD use a new msgID for each attempt.
123    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        // Build scoped PDU
148        let scoped_pdu = ScopedPdu::new(
149            engine_state.engine_id.clone(),
150            Bytes::new(), // empty context name
151            pdu.clone(),
152        );
153
154        // Get current engine time estimate
155        let engine_boots = engine_state.engine_boots;
156        let engine_time = engine_state.estimated_time();
157
158        // Handle encryption if needed
159        let (msg_data, priv_params) = if security_level.requires_priv() {
160            tracing::trace!(target: "async_snmp::client", "encrypting scoped PDU");
161
162            // Get mutable priv_key - we need interior mutability for salt counter
163            // Since PrivKey uses internal counter, we need to clone and use
164            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            // Encode scoped PDU
178            let scoped_pdu_bytes = scoped_pdu.encode_to_bytes();
179
180            // Encrypt
181            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        // Build USM security parameters
207        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        // Build global data
235        let msg_flags = MsgFlags::new(security_level, true); // reportable=true for requests
236        let global_data = MsgGlobalData::new(msg_id, 65507, msg_flags);
237
238        // Build complete message
239        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        // Apply authentication if needed
251        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            // Find auth params position and apply HMAC
263            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    /// Verify HMAC authentication on a V3 response message.
276    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    /// Decrypt an encrypted V3 response and extract the PDU.
318    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    /// Send a V3 request and handle the response.
367    #[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        // Ensure engine is discovered first
382        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            // RFC 3412 Section 6.2: use fresh msgID for each transmission attempt
404            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            // Register (or re-register) with fresh deadline before sending
411            self.inner
412                .transport
413                .register_request(msg_id, self.inner.config.timeout);
414
415            // Send request
416            self.inner.transport.send(&data).await?;
417
418            // Wait for response (deadline was set by register_request)
419            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                    // Verify authentication if required
424                    if security_level.requires_auth() {
425                        self.verify_response_auth(&response_data)?;
426                    }
427
428                    // Decode response
429                    let response = V3Message::decode(response_data)?;
430
431                    // Check for Report PDU (error response)
432                    if let Some(scoped_pdu) = response.scoped_pdu()
433                        && scoped_pdu.pdu.pdu_type == PduType::Report
434                    {
435                        // Check for time window error - resync and retry
436                        if is_not_in_time_window_report(&scoped_pdu.pdu) {
437                            tracing::debug!(target: "async_snmp::client", "not in time window, resyncing");
438                            // Update engine time from response
439                            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                            // Apply backoff delay before retry (if not last attempt)
458                            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                        // Check for unknown engine ID
469                        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                        // Other Report errors
478                        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                    // Extract security params before consuming response
488                    let response_security_params = response.security_params.clone();
489
490                    // Extract PDU (with decryption if required)
491                    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                    // Validate request ID
504                    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                    // Update engine time from successful response
515                    {
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                    // Check for SNMP error
528                    if response_pdu.is_error() {
529                        let status = response_pdu.error_status_enum();
530                        // error_index is 1-based; 0 means error applies to PDU, not a specific varbind
531                        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                    // Apply backoff delay before next retry (if not last attempt)
553                    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        // All retries exhausted
570        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}