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