async_snmp/client/
mod.rs

1//! SNMP client implementation.
2
3mod auth;
4mod builder;
5mod retry;
6mod v3;
7mod walk;
8
9pub use auth::{Auth, CommunityVersion, UsmAuth, UsmBuilder};
10pub use builder::ClientBuilder;
11pub use retry::{Backoff, Retry, RetryBuilder};
12
13// New unified entry point
14impl Client<UdpHandle> {
15    /// Create a new SNMP client builder.
16    ///
17    /// This is the single entry point for client construction, supporting all
18    /// SNMP versions (v1, v2c, v3) through the [`Auth`] enum.
19    ///
20    /// # Example
21    ///
22    /// ```rust,no_run
23    /// use async_snmp::{Auth, Client, Retry};
24    /// use std::time::Duration;
25    ///
26    /// # async fn example() -> async_snmp::Result<()> {
27    /// // Simple v2c client with default settings
28    /// let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
29    ///     .connect().await?;
30    ///
31    /// // v3 client with authentication
32    /// let client = Client::builder("192.168.1.1:161",
33    ///     Auth::usm("admin").auth(async_snmp::AuthProtocol::Sha256, "password"))
34    ///     .timeout(Duration::from_secs(10))
35    ///     .retry(Retry::fixed(5, Duration::ZERO))
36    ///     .connect().await?;
37    /// # Ok(())
38    /// # }
39    /// ```
40    pub fn builder(target: impl Into<String>, auth: impl Into<Auth>) -> ClientBuilder {
41        ClientBuilder::new(target, auth)
42    }
43}
44use crate::error::internal::DecodeErrorKind;
45use crate::error::{Error, Result};
46use crate::message::{CommunityMessage, Message};
47use crate::oid::Oid;
48use crate::pdu::{GetBulkPdu, Pdu};
49use crate::transport::Transport;
50use crate::transport::UdpHandle;
51use crate::v3::{EngineCache, EngineState, SaltCounter};
52use crate::value::Value;
53use crate::varbind::VarBind;
54use crate::version::Version;
55use bytes::Bytes;
56use std::net::SocketAddr;
57use std::sync::Arc;
58use std::sync::RwLock;
59use std::time::{Duration, Instant};
60use tracing::{Span, instrument};
61
62pub use crate::notification::{DerivedKeys, UsmConfig};
63pub use walk::{BulkWalk, OidOrdering, Walk, WalkMode, WalkStream};
64
65// ============================================================================
66// Default configuration constants
67// ============================================================================
68
69/// Default timeout for SNMP requests.
70pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
71
72/// Default maximum OIDs per request.
73///
74/// Requests with more OIDs than this limit are automatically split into
75/// multiple batches.
76pub const DEFAULT_MAX_OIDS_PER_REQUEST: usize = 10;
77
78/// Default max-repetitions for GETBULK operations.
79///
80/// Controls how many values are requested per GETBULK PDU during walks.
81pub const DEFAULT_MAX_REPETITIONS: u32 = 25;
82
83/// SNMP client.
84///
85/// Generic over transport type, with `UdpHandle` as default.
86#[derive(Clone)]
87pub struct Client<T: Transport = UdpHandle> {
88    inner: Arc<ClientInner<T>>,
89}
90
91struct ClientInner<T: Transport> {
92    transport: T,
93    config: ClientConfig,
94    /// Cached engine state (V3)
95    engine_state: RwLock<Option<EngineState>>,
96    /// Derived keys for this engine (V3)
97    derived_keys: RwLock<Option<DerivedKeys>>,
98    /// Salt counter for privacy (V3)
99    salt_counter: SaltCounter,
100    /// Shared engine cache (V3, optional)
101    engine_cache: Option<Arc<EngineCache>>,
102}
103
104/// Client configuration.
105///
106/// Most users should use [`ClientBuilder`] rather than constructing this directly.
107#[derive(Clone)]
108pub struct ClientConfig {
109    /// SNMP version (default: V2c)
110    pub version: Version,
111    /// Community string for v1/v2c (default: "public")
112    pub community: Bytes,
113    /// Request timeout (default: 5 seconds)
114    pub timeout: Duration,
115    /// Retry configuration (default: 3 retries, no backoff)
116    pub retry: Retry,
117    /// Maximum OIDs per request (default: 10)
118    pub max_oids_per_request: usize,
119    /// SNMPv3 security configuration (default: None)
120    pub v3_security: Option<UsmConfig>,
121    /// Walk operation mode (default: Auto)
122    pub walk_mode: WalkMode,
123    /// OID ordering behavior during walk operations (default: Strict)
124    pub oid_ordering: OidOrdering,
125    /// Maximum results from a single walk operation (default: None/unlimited)
126    pub max_walk_results: Option<usize>,
127    /// Max-repetitions for GETBULK operations (default: 25)
128    pub max_repetitions: u32,
129}
130
131impl Default for ClientConfig {
132    /// Returns configuration for SNMPv2c with community "public".
133    ///
134    /// See field documentation for all default values.
135    fn default() -> Self {
136        Self {
137            version: Version::V2c,
138            community: Bytes::from_static(b"public"),
139            timeout: DEFAULT_TIMEOUT,
140            retry: Retry::default(),
141            max_oids_per_request: DEFAULT_MAX_OIDS_PER_REQUEST,
142            v3_security: None,
143            walk_mode: WalkMode::Auto,
144            oid_ordering: OidOrdering::Strict,
145            max_walk_results: None,
146            max_repetitions: DEFAULT_MAX_REPETITIONS,
147        }
148    }
149}
150
151impl<T: Transport> Client<T> {
152    /// Create a new client with the given transport and config.
153    ///
154    /// For most use cases, prefer [`Client::builder()`] which provides a more
155    /// ergonomic API. Use this constructor when you need fine-grained control
156    /// over transport configuration (e.g., TCP connection timeout, keepalive
157    /// settings) or when using a custom [`Transport`] implementation.
158    pub fn new(transport: T, config: ClientConfig) -> Self {
159        Self {
160            inner: Arc::new(ClientInner {
161                transport,
162                config,
163                engine_state: RwLock::new(None),
164                derived_keys: RwLock::new(None),
165                salt_counter: SaltCounter::new(),
166                engine_cache: None,
167            }),
168        }
169    }
170
171    /// Create a new V3 client with a shared engine cache.
172    pub fn with_engine_cache(
173        transport: T,
174        config: ClientConfig,
175        engine_cache: Arc<EngineCache>,
176    ) -> Self {
177        Self {
178            inner: Arc::new(ClientInner {
179                transport,
180                config,
181                engine_state: RwLock::new(None),
182                derived_keys: RwLock::new(None),
183                salt_counter: SaltCounter::new(),
184                engine_cache: Some(engine_cache),
185            }),
186        }
187    }
188
189    /// Get the peer (target) address.
190    ///
191    /// Returns the remote address that this client sends requests to.
192    /// Named to match [`std::net::TcpStream::peer_addr()`].
193    pub fn peer_addr(&self) -> SocketAddr {
194        self.inner.transport.peer_addr()
195    }
196
197    /// Generate next request ID.
198    ///
199    /// Uses the transport's allocator (backed by a global counter).
200    fn next_request_id(&self) -> i32 {
201        self.inner.transport.alloc_request_id()
202    }
203
204    /// Check if using V3 with authentication/encryption configured.
205    fn is_v3(&self) -> bool {
206        self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
207    }
208
209    /// Send a request and wait for response (internal helper with pre-encoded data).
210    #[instrument(
211        level = "debug",
212        skip(self, data),
213        fields(
214            snmp.target = %self.peer_addr(),
215            snmp.request_id = request_id,
216            snmp.attempt = tracing::field::Empty,
217            snmp.elapsed_ms = tracing::field::Empty,
218        )
219    )]
220    async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
221        let start = Instant::now();
222        let mut last_error: Option<Box<Error>> = None;
223        let max_attempts = if self.inner.transport.is_reliable() {
224            0
225        } else {
226            self.inner.config.retry.max_attempts
227        };
228
229        for attempt in 0..=max_attempts {
230            Span::current().record("snmp.attempt", attempt);
231            if attempt > 0 {
232                tracing::debug!(target: "async_snmp::client", "retrying request");
233            }
234
235            // Register (or re-register) with fresh deadline before sending
236            self.inner
237                .transport
238                .register_request(request_id, self.inner.config.timeout);
239
240            // Send request
241            tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending request");
242            self.inner.transport.send(data).await?;
243
244            // Wait for response (deadline was set by register_request)
245            match self.inner.transport.recv(request_id).await {
246                Ok((response_data, _source)) => {
247                    tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received response");
248
249                    // Decode response and extract PDU
250                    let response = Message::decode(response_data)?;
251
252                    // Validate response version matches request version
253                    let response_version = response.version();
254                    let expected_version = self.inner.config.version;
255                    if response_version != expected_version {
256                        tracing::warn!(target: "async_snmp::client", { ?expected_version, ?response_version, peer = %self.peer_addr() }, "version mismatch in response");
257                        return Err(Error::MalformedResponse {
258                            target: self.peer_addr(),
259                        }
260                        .boxed());
261                    }
262
263                    let response_pdu = response.into_pdu();
264
265                    // Validate request ID
266                    if response_pdu.request_id != request_id {
267                        tracing::warn!(target: "async_snmp::client", { expected_request_id = request_id, actual_request_id = response_pdu.request_id, peer = %self.peer_addr() }, "request ID mismatch in response");
268                        return Err(Error::MalformedResponse {
269                            target: self.peer_addr(),
270                        }
271                        .boxed());
272                    }
273
274                    // Check for SNMP error
275                    if response_pdu.is_error() {
276                        let status = response_pdu.error_status_enum();
277                        // error_index is 1-based; 0 means error applies to PDU, not a specific varbind
278                        let oid = (response_pdu.error_index as usize)
279                            .checked_sub(1)
280                            .and_then(|idx| response_pdu.varbinds.get(idx))
281                            .map(|vb| vb.oid.clone());
282
283                        Span::current()
284                            .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
285                        return Err(Error::Snmp {
286                            target: self.peer_addr(),
287                            status,
288                            index: response_pdu.error_index.max(0) as u32,
289                            oid,
290                        }
291                        .boxed());
292                    }
293
294                    Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
295                    return Ok(response_pdu);
296                }
297                Err(e) if matches!(*e, Error::Timeout { .. }) => {
298                    last_error = Some(e);
299                    // Apply backoff delay before next retry (if not last attempt)
300                    if attempt < max_attempts {
301                        let delay = self.inner.config.retry.compute_delay(attempt);
302                        if !delay.is_zero() {
303                            tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
304                            tokio::time::sleep(delay).await;
305                        }
306                    }
307                    continue;
308                }
309                Err(e) => {
310                    Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
311                    return Err(e);
312                }
313            }
314        }
315
316        // All retries exhausted
317        let elapsed = start.elapsed();
318        Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
319        tracing::debug!(target: "async_snmp::client", { request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
320        Err(last_error.unwrap_or_else(|| {
321            Error::Timeout {
322                target: self.peer_addr(),
323                elapsed,
324                retries: max_attempts,
325            }
326            .boxed()
327        }))
328    }
329
330    /// Send a standard request (GET, GETNEXT, SET) and wait for response.
331    async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
332        // Dispatch to V3 handler if configured
333        if self.is_v3() {
334            return self.send_v3_and_recv(pdu).await;
335        }
336
337        tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len() }, "sending {} request", pdu.pdu_type);
338
339        let request_id = pdu.request_id;
340        let message = CommunityMessage::new(
341            self.inner.config.version,
342            self.inner.config.community.clone(),
343            pdu,
344        );
345        let data = message.encode();
346        let response = self.send_and_recv(request_id, &data).await?;
347
348        tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?response.pdu_type, snmp.varbind_count = response.varbinds.len(), snmp.error_status = response.error_status, snmp.error_index = response.error_index }, "received {} response", response.pdu_type);
349
350        Ok(response)
351    }
352
353    /// Send a GETBULK request and wait for response.
354    async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
355        // Dispatch to V3 handler if configured
356        if self.is_v3() {
357            // Convert GetBulkPdu to Pdu for V3 encoding
358            let pdu = Pdu::get_bulk(
359                pdu.request_id,
360                pdu.non_repeaters,
361                pdu.max_repetitions,
362                pdu.varbinds,
363            );
364            return self.send_v3_and_recv(pdu).await;
365        }
366
367        tracing::debug!(target: "async_snmp::client", { snmp.non_repeaters = pdu.non_repeaters, snmp.max_repetitions = pdu.max_repetitions, snmp.varbind_count = pdu.varbinds.len() }, "sending GetBulkRequest");
368
369        let request_id = pdu.request_id;
370        let data = CommunityMessage::encode_bulk(
371            self.inner.config.version,
372            self.inner.config.community.clone(),
373            &pdu,
374        );
375        let response = self.send_and_recv(request_id, &data).await?;
376
377        tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?response.pdu_type, snmp.varbind_count = response.varbinds.len(), snmp.error_status = response.error_status, snmp.error_index = response.error_index }, "received {} response", response.pdu_type);
378
379        Ok(response)
380    }
381
382    /// GET a single OID.
383    #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
384    pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
385        let request_id = self.next_request_id();
386        let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
387        let response = self.send_request(pdu).await?;
388
389        response.varbinds.into_iter().next().ok_or_else(|| {
390            tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GET response");
391            Error::MalformedResponse {
392                target: self.peer_addr(),
393            }
394            .boxed()
395        })
396    }
397
398    /// GET multiple OIDs.
399    ///
400    /// If the OID list exceeds `max_oids_per_request`, the request is
401    /// automatically split into multiple batches. Results are returned
402    /// in the same order as the input OIDs.
403    ///
404    /// # Example
405    ///
406    /// ```rust,no_run
407    /// # use async_snmp::{Auth, Client, oid};
408    /// # async fn example() -> async_snmp::Result<()> {
409    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
410    /// let results = client.get_many(&[
411    ///     oid!(1, 3, 6, 1, 2, 1, 1, 1, 0),  // sysDescr
412    ///     oid!(1, 3, 6, 1, 2, 1, 1, 3, 0),  // sysUpTime
413    ///     oid!(1, 3, 6, 1, 2, 1, 1, 5, 0),  // sysName
414    /// ]).await?;
415    /// # Ok(())
416    /// # }
417    /// ```
418    #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
419    pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
420        if oids.is_empty() {
421            return Ok(Vec::new());
422        }
423
424        let max_per_request = self.inner.config.max_oids_per_request;
425
426        // Fast path: single request if within limit
427        if oids.len() <= max_per_request {
428            let request_id = self.next_request_id();
429            let pdu = Pdu::get_request(request_id, oids);
430            let response = self.send_request(pdu).await?;
431            return Ok(response.varbinds);
432        }
433
434        // Batched path: split into chunks
435        let num_batches = oids.len().div_ceil(max_per_request);
436        tracing::debug!(target: "async_snmp::client", { snmp.oid_count = oids.len(), snmp.max_per_request = max_per_request, snmp.batch_count = num_batches }, "splitting GET request into batches");
437
438        let mut all_results = Vec::with_capacity(oids.len());
439
440        for (batch_idx, chunk) in oids.chunks(max_per_request).enumerate() {
441            tracing::debug!(target: "async_snmp::client", { snmp.batch = batch_idx + 1, snmp.batch_total = num_batches, snmp.batch_oid_count = chunk.len() }, "sending GET batch");
442            let request_id = self.next_request_id();
443            let pdu = Pdu::get_request(request_id, chunk);
444            let response = self.send_request(pdu).await?;
445            all_results.extend(response.varbinds);
446        }
447
448        Ok(all_results)
449    }
450
451    /// GETNEXT for a single OID.
452    #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
453    pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
454        let request_id = self.next_request_id();
455        let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
456        let response = self.send_request(pdu).await?;
457
458        response.varbinds.into_iter().next().ok_or_else(|| {
459            tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GETNEXT response");
460            Error::MalformedResponse {
461                target: self.peer_addr(),
462            }
463            .boxed()
464        })
465    }
466
467    /// GETNEXT for multiple OIDs.
468    ///
469    /// If the OID list exceeds `max_oids_per_request`, the request is
470    /// automatically split into multiple batches. Results are returned
471    /// in the same order as the input OIDs.
472    ///
473    /// # Example
474    ///
475    /// ```rust,no_run
476    /// # use async_snmp::{Auth, Client, oid};
477    /// # async fn example() -> async_snmp::Result<()> {
478    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
479    /// let results = client.get_next_many(&[
480    ///     oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2),  // ifDescr
481    ///     oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 3),  // ifType
482    /// ]).await?;
483    /// # Ok(())
484    /// # }
485    /// ```
486    #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
487    pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
488        if oids.is_empty() {
489            return Ok(Vec::new());
490        }
491
492        let max_per_request = self.inner.config.max_oids_per_request;
493
494        // Fast path: single request if within limit
495        if oids.len() <= max_per_request {
496            let request_id = self.next_request_id();
497            let pdu = Pdu::get_next_request(request_id, oids);
498            let response = self.send_request(pdu).await?;
499            return Ok(response.varbinds);
500        }
501
502        // Batched path: split into chunks
503        let num_batches = oids.len().div_ceil(max_per_request);
504        tracing::debug!(target: "async_snmp::client", { snmp.oid_count = oids.len(), snmp.max_per_request = max_per_request, snmp.batch_count = num_batches }, "splitting GETNEXT request into batches");
505
506        let mut all_results = Vec::with_capacity(oids.len());
507
508        for (batch_idx, chunk) in oids.chunks(max_per_request).enumerate() {
509            tracing::debug!(target: "async_snmp::client", { snmp.batch = batch_idx + 1, snmp.batch_total = num_batches, snmp.batch_oid_count = chunk.len() }, "sending GETNEXT batch");
510            let request_id = self.next_request_id();
511            let pdu = Pdu::get_next_request(request_id, chunk);
512            let response = self.send_request(pdu).await?;
513            all_results.extend(response.varbinds);
514        }
515
516        Ok(all_results)
517    }
518
519    /// SET a single OID.
520    #[instrument(skip(self, value), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
521    pub async fn set(&self, oid: &Oid, value: Value) -> Result<VarBind> {
522        let request_id = self.next_request_id();
523        let varbind = VarBind::new(oid.clone(), value);
524        let pdu = Pdu::set_request(request_id, vec![varbind]);
525        let response = self.send_request(pdu).await?;
526
527        response.varbinds.into_iter().next().ok_or_else(|| {
528            tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty SET response");
529            Error::MalformedResponse {
530                target: self.peer_addr(),
531            }
532            .boxed()
533        })
534    }
535
536    /// SET multiple OIDs.
537    ///
538    /// If the varbind list exceeds `max_oids_per_request`, the request is
539    /// automatically split into multiple batches. Results are returned
540    /// in the same order as the input varbinds.
541    ///
542    /// # Example
543    ///
544    /// ```rust,no_run
545    /// # use async_snmp::{Auth, Client, oid, Value};
546    /// # async fn example() -> async_snmp::Result<()> {
547    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("private")).connect().await?;
548    /// let results = client.set_many(&[
549    ///     (oid!(1, 3, 6, 1, 2, 1, 1, 5, 0), Value::from("new-hostname")),
550    ///     (oid!(1, 3, 6, 1, 2, 1, 1, 6, 0), Value::from("new-location")),
551    /// ]).await?;
552    /// # Ok(())
553    /// # }
554    /// ```
555    #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = varbinds.len()))]
556    pub async fn set_many(&self, varbinds: &[(Oid, Value)]) -> Result<Vec<VarBind>> {
557        if varbinds.is_empty() {
558            return Ok(Vec::new());
559        }
560
561        let max_per_request = self.inner.config.max_oids_per_request;
562
563        // Fast path: single request if within limit
564        if varbinds.len() <= max_per_request {
565            let request_id = self.next_request_id();
566            let vbs: Vec<VarBind> = varbinds
567                .iter()
568                .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
569                .collect();
570            let pdu = Pdu::set_request(request_id, vbs);
571            let response = self.send_request(pdu).await?;
572            return Ok(response.varbinds);
573        }
574
575        // Batched path: split into chunks
576        let num_batches = varbinds.len().div_ceil(max_per_request);
577        tracing::debug!(target: "async_snmp::client", { snmp.oid_count = varbinds.len(), snmp.max_per_request = max_per_request, snmp.batch_count = num_batches }, "splitting SET request into batches");
578
579        let mut all_results = Vec::with_capacity(varbinds.len());
580
581        for (batch_idx, chunk) in varbinds.chunks(max_per_request).enumerate() {
582            tracing::debug!(target: "async_snmp::client", { snmp.batch = batch_idx + 1, snmp.batch_total = num_batches, snmp.batch_oid_count = chunk.len() }, "sending SET batch");
583            let request_id = self.next_request_id();
584            let vbs: Vec<VarBind> = chunk
585                .iter()
586                .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
587                .collect();
588            let pdu = Pdu::set_request(request_id, vbs);
589            let response = self.send_request(pdu).await?;
590            all_results.extend(response.varbinds);
591        }
592
593        Ok(all_results)
594    }
595
596    /// GETBULK request (SNMPv2c/v3 only).
597    ///
598    /// Efficiently retrieves multiple variable bindings in a single request.
599    /// GETBULK splits the requested OIDs into two groups:
600    ///
601    /// - **Non-repeaters** (first N OIDs): Each gets a single GETNEXT, returning
602    ///   one value per OID. Use for scalar values like `sysUpTime.0`.
603    /// - **Repeaters** (remaining OIDs): Each gets up to `max_repetitions` GETNEXTs,
604    ///   returning multiple values per OID. Use for walking table columns.
605    ///
606    /// # Arguments
607    ///
608    /// * `oids` - OIDs to retrieve
609    /// * `non_repeaters` - How many OIDs (from the start) are non-repeating
610    /// * `max_repetitions` - Maximum rows to return for each repeating OID
611    ///
612    /// # Example
613    ///
614    /// ```rust,no_run
615    /// # use async_snmp::{Auth, Client, oid};
616    /// # async fn example() -> async_snmp::Result<()> {
617    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
618    /// // Get sysUpTime (non-repeater) plus 10 interface descriptions (repeater)
619    /// let results = client.get_bulk(
620    ///     &[oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2)],
621    ///     1,  // first OID is non-repeating
622    ///     10, // get up to 10 values for the second OID
623    /// ).await?;
624    /// // Results: [sysUpTime value, ifDescr.1, ifDescr.2, ..., ifDescr.10]
625    /// # Ok(())
626    /// # }
627    /// ```
628    #[instrument(skip(self, oids), err, fields(
629        snmp.target = %self.peer_addr(),
630        snmp.oid_count = oids.len(),
631        snmp.non_repeaters = non_repeaters,
632        snmp.max_repetitions = max_repetitions
633    ))]
634    pub async fn get_bulk(
635        &self,
636        oids: &[Oid],
637        non_repeaters: i32,
638        max_repetitions: i32,
639    ) -> Result<Vec<VarBind>> {
640        let request_id = self.next_request_id();
641        let pdu = GetBulkPdu::new(request_id, non_repeaters, max_repetitions, oids);
642        let response = self.send_bulk_request(pdu).await?;
643        Ok(response.varbinds)
644    }
645
646    /// Walk an OID subtree.
647    ///
648    /// Auto-selects the optimal walk method based on SNMP version and `WalkMode`:
649    /// - `WalkMode::Auto` (default): Uses GETNEXT for V1, GETBULK for V2c/V3
650    /// - `WalkMode::GetNext`: Always uses GETNEXT
651    /// - `WalkMode::GetBulk`: Always uses GETBULK (fails on V1)
652    ///
653    /// Returns an async stream that yields each variable binding in the subtree.
654    /// The walk terminates when an OID outside the subtree is encountered or
655    /// when `EndOfMibView` is returned.
656    ///
657    /// Uses the client's configured `oid_ordering`, `max_walk_results`, and
658    /// `max_repetitions` (for GETBULK) settings.
659    ///
660    /// # Example
661    ///
662    /// ```rust,no_run
663    /// # use async_snmp::{Auth, Client, oid};
664    /// # async fn example() -> async_snmp::Result<()> {
665    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
666    /// // Auto-selects GETBULK for V2c/V3, GETNEXT for V1
667    /// let results = client.walk(oid!(1, 3, 6, 1, 2, 1, 1))?.collect().await?;
668    /// # Ok(())
669    /// # }
670    /// ```
671    #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
672    pub fn walk(&self, oid: Oid) -> Result<WalkStream<T>>
673    where
674        T: 'static,
675    {
676        let ordering = self.inner.config.oid_ordering;
677        let max_results = self.inner.config.max_walk_results;
678        let walk_mode = self.inner.config.walk_mode;
679        let max_repetitions = self.inner.config.max_repetitions as i32;
680        let version = self.inner.config.version;
681
682        WalkStream::new(
683            self.clone(),
684            oid,
685            version,
686            walk_mode,
687            ordering,
688            max_results,
689            max_repetitions,
690        )
691    }
692
693    /// Walk an OID subtree using GETNEXT.
694    ///
695    /// This method always uses GETNEXT regardless of the client's `WalkMode` configuration.
696    /// For auto-selection based on version and mode, use [`walk()`](Self::walk) instead.
697    ///
698    /// Returns an async stream that yields each variable binding in the subtree.
699    /// The walk terminates when an OID outside the subtree is encountered or
700    /// when `EndOfMibView` is returned.
701    ///
702    /// Uses the client's configured `oid_ordering` and `max_walk_results` settings.
703    ///
704    /// # Example
705    ///
706    /// ```rust,no_run
707    /// # use async_snmp::{Auth, Client, oid};
708    /// # async fn example() -> async_snmp::Result<()> {
709    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
710    /// // Force GETNEXT even for V2c/V3 clients
711    /// let results = client.walk_getnext(oid!(1, 3, 6, 1, 2, 1, 1)).collect().await?;
712    /// # Ok(())
713    /// # }
714    /// ```
715    #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
716    pub fn walk_getnext(&self, oid: Oid) -> Walk<T>
717    where
718        T: 'static,
719    {
720        let ordering = self.inner.config.oid_ordering;
721        let max_results = self.inner.config.max_walk_results;
722        Walk::new(self.clone(), oid, ordering, max_results)
723    }
724
725    /// Walk an OID subtree using GETBULK (more efficient than GETNEXT).
726    ///
727    /// Returns an async stream that yields each variable binding in the subtree.
728    /// Uses GETBULK internally with `non_repeaters=0`, fetching `max_repetitions`
729    /// values per request for efficient table traversal.
730    ///
731    /// Uses the client's configured `oid_ordering` and `max_walk_results` settings.
732    ///
733    /// # Arguments
734    ///
735    /// * `oid` - The base OID of the subtree to walk
736    /// * `max_repetitions` - How many OIDs to fetch per request
737    ///
738    /// # Example
739    ///
740    /// ```rust,no_run
741    /// # use async_snmp::{Auth, Client, oid};
742    /// # async fn example() -> async_snmp::Result<()> {
743    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
744    /// // Walk the interfaces table efficiently
745    /// let walk = client.bulk_walk(oid!(1, 3, 6, 1, 2, 1, 2, 2), 25);
746    /// // Process with futures StreamExt
747    /// # Ok(())
748    /// # }
749    /// ```
750    #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid, snmp.max_repetitions = max_repetitions))]
751    pub fn bulk_walk(&self, oid: Oid, max_repetitions: i32) -> BulkWalk<T>
752    where
753        T: 'static,
754    {
755        let ordering = self.inner.config.oid_ordering;
756        let max_results = self.inner.config.max_walk_results;
757        BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
758    }
759
760    /// Walk an OID subtree using the client's configured `max_repetitions`.
761    ///
762    /// This is a convenience method that uses the client's `max_repetitions` setting
763    /// (default: 25) instead of requiring it as a parameter.
764    ///
765    /// # Example
766    ///
767    /// ```rust,no_run
768    /// # use async_snmp::{Auth, Client, oid};
769    /// # async fn example() -> async_snmp::Result<()> {
770    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
771    /// // Walk using configured max_repetitions
772    /// let walk = client.bulk_walk_default(oid!(1, 3, 6, 1, 2, 1, 2, 2));
773    /// // Process with futures StreamExt
774    /// # Ok(())
775    /// # }
776    /// ```
777    #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
778    pub fn bulk_walk_default(&self, oid: Oid) -> BulkWalk<T>
779    where
780        T: 'static,
781    {
782        let ordering = self.inner.config.oid_ordering;
783        let max_results = self.inner.config.max_walk_results;
784        let max_repetitions = self.inner.config.max_repetitions as i32;
785        BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
786    }
787}