Skip to main content

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