async_snmp/client/
mod.rs

1//! SNMP client implementation.
2
3mod auth;
4mod builder;
5mod v3;
6mod walk;
7
8pub use auth::{Auth, CommunityVersion, UsmAuth, UsmBuilder};
9pub use builder::ClientBuilder;
10
11// New unified entry point
12impl Client<UdpTransport> {
13    /// Create a new SNMP client builder.
14    ///
15    /// This is the single entry point for client construction, supporting all
16    /// SNMP versions (v1, v2c, v3) through the [`Auth`] enum.
17    ///
18    /// # Example
19    ///
20    /// ```rust,no_run
21    /// use async_snmp::{Auth, Client};
22    /// use std::time::Duration;
23    ///
24    /// # async fn example() -> async_snmp::Result<()> {
25    /// // Simple v2c client with default settings
26    /// let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
27    ///     .connect().await?;
28    ///
29    /// // v3 client with authentication
30    /// let client = Client::builder("192.168.1.1:161",
31    ///     Auth::usm("admin").auth(async_snmp::v3::AuthProtocol::Sha256, "password"))
32    ///     .timeout(Duration::from_secs(10))
33    ///     .retries(5)
34    ///     .connect().await?;
35    /// # Ok(())
36    /// # }
37    /// ```
38    pub fn builder(target: impl Into<String>, auth: impl Into<Auth>) -> ClientBuilder {
39        ClientBuilder::new(target, auth)
40    }
41}
42use crate::error::{DecodeErrorKind, Error, Result};
43use crate::message::{CommunityMessage, Message};
44use crate::oid::Oid;
45use crate::pdu::{GetBulkPdu, Pdu};
46use crate::transport::Transport;
47use crate::transport::UdpTransport;
48use crate::v3::{EngineCache, EngineState, SaltCounter};
49use crate::value::Value;
50use crate::varbind::VarBind;
51use crate::version::Version;
52use bytes::Bytes;
53use std::net::SocketAddr;
54use std::sync::Arc;
55use std::sync::RwLock;
56use std::sync::atomic::{AtomicI32, Ordering};
57use std::time::{Duration, Instant};
58use tracing::{Span, instrument};
59
60pub use v3::{V3DerivedKeys, V3SecurityConfig};
61pub use walk::{BulkWalk, OidOrdering, Walk, WalkMode, WalkStream};
62
63/// SNMP client.
64///
65/// Generic over transport type, with `UdpTransport` as default.
66#[derive(Clone)]
67pub struct Client<T: Transport = UdpTransport> {
68    inner: Arc<ClientInner<T>>,
69}
70
71struct ClientInner<T: Transport> {
72    transport: T,
73    config: ClientConfig,
74    request_id: AtomicI32,
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#[derive(Clone)]
87pub struct ClientConfig {
88    /// SNMP version
89    pub version: Version,
90    /// Community string (v1/v2c)
91    pub community: Bytes,
92    /// Request timeout
93    pub timeout: Duration,
94    /// Number of retries
95    pub retries: u32,
96    /// Maximum OIDs per request
97    pub max_oids_per_request: usize,
98    /// SNMPv3 security configuration
99    pub v3_security: Option<V3SecurityConfig>,
100    /// Walk operation mode (Auto, GetNext, or GetBulk)
101    pub walk_mode: WalkMode,
102    /// OID ordering behavior during walk operations
103    pub oid_ordering: OidOrdering,
104    /// Maximum results from a single walk operation (None = unlimited)
105    pub max_walk_results: Option<usize>,
106    /// Max-repetitions for GETBULK operations (default: 25)
107    pub max_repetitions: u32,
108}
109
110impl Default for ClientConfig {
111    fn default() -> Self {
112        Self {
113            version: Version::V2c,
114            community: Bytes::from_static(b"public"),
115            timeout: Duration::from_secs(5),
116            retries: 3,
117            max_oids_per_request: 10,
118            v3_security: None,
119            walk_mode: WalkMode::Auto,
120            oid_ordering: OidOrdering::Strict,
121            max_walk_results: None,
122            max_repetitions: 25,
123        }
124    }
125}
126
127impl<T: Transport> Client<T> {
128    /// Create a new client with the given transport and config.
129    pub fn new(transport: T, config: ClientConfig) -> Self {
130        Self {
131            inner: Arc::new(ClientInner {
132                transport,
133                config,
134                request_id: AtomicI32::new(1),
135                engine_state: RwLock::new(None),
136                derived_keys: RwLock::new(None),
137                salt_counter: SaltCounter::new(),
138                engine_cache: None,
139            }),
140        }
141    }
142
143    /// Create a new V3 client with a shared engine cache.
144    pub fn with_engine_cache(
145        transport: T,
146        config: ClientConfig,
147        engine_cache: Arc<EngineCache>,
148    ) -> Self {
149        Self {
150            inner: Arc::new(ClientInner {
151                transport,
152                config,
153                request_id: AtomicI32::new(1),
154                engine_state: RwLock::new(None),
155                derived_keys: RwLock::new(None),
156                salt_counter: SaltCounter::new(),
157                engine_cache: Some(engine_cache),
158            }),
159        }
160    }
161
162    /// Get the peer (target) address.
163    ///
164    /// Returns the remote address that this client sends requests to.
165    /// Named to match [`std::net::TcpStream::peer_addr()`].
166    pub fn peer_addr(&self) -> SocketAddr {
167        self.inner.transport.peer_addr()
168    }
169
170    /// Generate next request ID.
171    ///
172    /// Uses the transport's shared counter if available (for shared transports),
173    /// otherwise uses the client's own counter.
174    fn next_request_id(&self) -> i32 {
175        self.inner
176            .transport
177            .alloc_request_id()
178            .unwrap_or_else(|| self.inner.request_id.fetch_add(1, Ordering::Relaxed))
179    }
180
181    /// Check if using V3 with authentication/encryption configured.
182    fn is_v3(&self) -> bool {
183        self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
184    }
185
186    /// Send a request and wait for response (internal helper with pre-encoded data).
187    #[instrument(
188        level = "debug",
189        skip(self, data),
190        fields(
191            snmp.target = %self.peer_addr(),
192            snmp.request_id = request_id,
193            snmp.retries = tracing::field::Empty,
194            snmp.elapsed_ms = tracing::field::Empty,
195        )
196    )]
197    async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
198        let start = Instant::now();
199        let mut last_error = None;
200        let retries = if self.inner.transport.is_stream() {
201            0
202        } else {
203            self.inner.config.retries
204        };
205
206        for attempt in 0..=retries {
207            Span::current().record("snmp.retries", attempt);
208            if attempt > 0 {
209                tracing::debug!("retrying request");
210            }
211
212            // Send request
213            tracing::trace!(snmp.bytes = data.len(), "sending request");
214            self.inner.transport.send(data).await?;
215
216            // Wait for response
217            match self
218                .inner
219                .transport
220                .recv(request_id, self.inner.config.timeout)
221                .await
222            {
223                Ok((response_data, _source)) => {
224                    tracing::trace!(snmp.bytes = response_data.len(), "received response");
225
226                    // Decode response and extract PDU
227                    let response = Message::decode(response_data)?;
228
229                    // Validate response version matches request version
230                    let response_version = response.version();
231                    let expected_version = self.inner.config.version;
232                    if response_version != expected_version {
233                        return Err(Error::VersionMismatch {
234                            expected: expected_version,
235                            actual: response_version,
236                        });
237                    }
238
239                    let response_pdu = response.into_pdu();
240
241                    // Validate request ID
242                    if response_pdu.request_id != request_id {
243                        return Err(Error::RequestIdMismatch {
244                            expected: request_id,
245                            actual: response_pdu.request_id,
246                        });
247                    }
248
249                    // Check for SNMP error
250                    if response_pdu.is_error() {
251                        let status = response_pdu.error_status_enum();
252                        // error_index is 1-based; 0 means error applies to PDU, not a specific varbind
253                        let oid = (response_pdu.error_index as usize)
254                            .checked_sub(1)
255                            .and_then(|idx| response_pdu.varbinds.get(idx))
256                            .map(|vb| vb.oid.clone());
257
258                        Span::current()
259                            .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
260                        return Err(Error::Snmp {
261                            target: Some(self.peer_addr()),
262                            status,
263                            index: response_pdu.error_index as u32,
264                            oid,
265                        });
266                    }
267
268                    Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
269                    return Ok(response_pdu);
270                }
271                Err(e @ Error::Timeout { .. }) => {
272                    last_error = Some(e);
273                    continue;
274                }
275                Err(e) => {
276                    Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
277                    return Err(e);
278                }
279            }
280        }
281
282        // All retries exhausted
283        Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
284        Err(last_error.unwrap_or(Error::Timeout {
285            target: Some(self.peer_addr()),
286            elapsed: self.inner.config.timeout * (retries + 1),
287            request_id,
288            retries,
289        }))
290    }
291
292    /// Send a standard request (GET, GETNEXT, SET) and wait for response.
293    async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
294        // Dispatch to V3 handler if configured
295        if self.is_v3() {
296            return self.send_v3_and_recv(pdu).await;
297        }
298
299        tracing::debug!(
300            snmp.pdu_type = ?pdu.pdu_type,
301            snmp.varbind_count = pdu.varbinds.len(),
302            "sending {} request",
303            pdu.pdu_type
304        );
305
306        let request_id = pdu.request_id;
307        let message = CommunityMessage::new(
308            self.inner.config.version,
309            self.inner.config.community.clone(),
310            pdu,
311        );
312        let data = message.encode();
313        let response = self.send_and_recv(request_id, &data).await?;
314
315        tracing::debug!(
316            snmp.pdu_type = ?response.pdu_type,
317            snmp.varbind_count = response.varbinds.len(),
318            snmp.error_status = response.error_status,
319            snmp.error_index = response.error_index,
320            "received {} response",
321            response.pdu_type
322        );
323
324        Ok(response)
325    }
326
327    /// Send a GETBULK request and wait for response.
328    async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
329        // Dispatch to V3 handler if configured
330        if self.is_v3() {
331            // Convert GetBulkPdu to Pdu for V3 encoding
332            let pdu = Pdu::get_bulk(
333                pdu.request_id,
334                pdu.non_repeaters,
335                pdu.max_repetitions,
336                pdu.varbinds,
337            );
338            return self.send_v3_and_recv(pdu).await;
339        }
340
341        tracing::debug!(
342            snmp.non_repeaters = pdu.non_repeaters,
343            snmp.max_repetitions = pdu.max_repetitions,
344            snmp.varbind_count = pdu.varbinds.len(),
345            "sending GetBulkRequest"
346        );
347
348        let request_id = pdu.request_id;
349        let data = CommunityMessage::encode_bulk(
350            self.inner.config.version,
351            self.inner.config.community.clone(),
352            &pdu,
353        );
354        let response = self.send_and_recv(request_id, &data).await?;
355
356        tracing::debug!(
357            snmp.pdu_type = ?response.pdu_type,
358            snmp.varbind_count = response.varbinds.len(),
359            snmp.error_status = response.error_status,
360            snmp.error_index = response.error_index,
361            "received {} response",
362            response.pdu_type
363        );
364
365        Ok(response)
366    }
367
368    /// GET a single OID.
369    #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
370    pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
371        let request_id = self.next_request_id();
372        let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
373        let response = self.send_request(pdu).await?;
374
375        response
376            .varbinds
377            .into_iter()
378            .next()
379            .ok_or_else(|| Error::decode(0, DecodeErrorKind::EmptyResponse))
380    }
381
382    /// GET multiple OIDs.
383    ///
384    /// If the OID list exceeds `max_oids_per_request`, the request is
385    /// automatically split into multiple batches. Results are returned
386    /// in the same order as the input OIDs.
387    ///
388    /// # Example
389    ///
390    /// ```rust,no_run
391    /// # use async_snmp::{Auth, Client, oid};
392    /// # async fn example() -> async_snmp::Result<()> {
393    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
394    /// let results = client.get_many(&[
395    ///     oid!(1, 3, 6, 1, 2, 1, 1, 1, 0),  // sysDescr
396    ///     oid!(1, 3, 6, 1, 2, 1, 1, 3, 0),  // sysUpTime
397    ///     oid!(1, 3, 6, 1, 2, 1, 1, 5, 0),  // sysName
398    /// ]).await?;
399    /// # Ok(())
400    /// # }
401    /// ```
402    #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
403    pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
404        if oids.is_empty() {
405            return Ok(Vec::new());
406        }
407
408        let max_per_request = self.inner.config.max_oids_per_request;
409
410        // Fast path: single request if within limit
411        if oids.len() <= max_per_request {
412            let request_id = self.next_request_id();
413            let pdu = Pdu::get_request(request_id, oids);
414            let response = self.send_request(pdu).await?;
415            return Ok(response.varbinds);
416        }
417
418        // Batched path: split into chunks
419        let num_batches = oids.len().div_ceil(max_per_request);
420        tracing::debug!(
421            snmp.oid_count = oids.len(),
422            snmp.max_per_request = max_per_request,
423            snmp.batch_count = num_batches,
424            "splitting GET request into batches"
425        );
426
427        let mut all_results = Vec::with_capacity(oids.len());
428
429        for (batch_idx, chunk) in oids.chunks(max_per_request).enumerate() {
430            tracing::debug!(
431                snmp.batch = batch_idx + 1,
432                snmp.batch_total = num_batches,
433                snmp.batch_oid_count = chunk.len(),
434                "sending GET batch"
435            );
436            let request_id = self.next_request_id();
437            let pdu = Pdu::get_request(request_id, chunk);
438            let response = self.send_request(pdu).await?;
439            all_results.extend(response.varbinds);
440        }
441
442        Ok(all_results)
443    }
444
445    /// GETNEXT for a single OID.
446    #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
447    pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
448        let request_id = self.next_request_id();
449        let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
450        let response = self.send_request(pdu).await?;
451
452        response
453            .varbinds
454            .into_iter()
455            .next()
456            .ok_or_else(|| Error::decode(0, DecodeErrorKind::EmptyResponse))
457    }
458
459    /// GETNEXT for multiple OIDs.
460    ///
461    /// If the OID list exceeds `max_oids_per_request`, the request is
462    /// automatically split into multiple batches. Results are returned
463    /// in the same order as the input OIDs.
464    ///
465    /// # Example
466    ///
467    /// ```rust,no_run
468    /// # use async_snmp::{Auth, Client, oid};
469    /// # async fn example() -> async_snmp::Result<()> {
470    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
471    /// let results = client.get_next_many(&[
472    ///     oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2),  // ifDescr
473    ///     oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 3),  // ifType
474    /// ]).await?;
475    /// # Ok(())
476    /// # }
477    /// ```
478    #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
479    pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
480        if oids.is_empty() {
481            return Ok(Vec::new());
482        }
483
484        let max_per_request = self.inner.config.max_oids_per_request;
485
486        // Fast path: single request if within limit
487        if oids.len() <= max_per_request {
488            let request_id = self.next_request_id();
489            let pdu = Pdu::get_next_request(request_id, oids);
490            let response = self.send_request(pdu).await?;
491            return Ok(response.varbinds);
492        }
493
494        // Batched path: split into chunks
495        let num_batches = oids.len().div_ceil(max_per_request);
496        tracing::debug!(
497            snmp.oid_count = oids.len(),
498            snmp.max_per_request = max_per_request,
499            snmp.batch_count = num_batches,
500            "splitting GETNEXT request into batches"
501        );
502
503        let mut all_results = Vec::with_capacity(oids.len());
504
505        for (batch_idx, chunk) in oids.chunks(max_per_request).enumerate() {
506            tracing::debug!(
507                snmp.batch = batch_idx + 1,
508                snmp.batch_total = num_batches,
509                snmp.batch_oid_count = chunk.len(),
510                "sending GETNEXT batch"
511            );
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
530            .varbinds
531            .into_iter()
532            .next()
533            .ok_or_else(|| Error::decode(0, DecodeErrorKind::EmptyResponse))
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!(
578            snmp.oid_count = varbinds.len(),
579            snmp.max_per_request = max_per_request,
580            snmp.batch_count = num_batches,
581            "splitting SET request into batches"
582        );
583
584        let mut all_results = Vec::with_capacity(varbinds.len());
585
586        for (batch_idx, chunk) in varbinds.chunks(max_per_request).enumerate() {
587            tracing::debug!(
588                snmp.batch = batch_idx + 1,
589                snmp.batch_total = num_batches,
590                snmp.batch_oid_count = chunk.len(),
591                "sending SET batch"
592            );
593            let request_id = self.next_request_id();
594            let vbs: Vec<VarBind> = chunk
595                .iter()
596                .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
597                .collect();
598            let pdu = Pdu::set_request(request_id, vbs);
599            let response = self.send_request(pdu).await?;
600            all_results.extend(response.varbinds);
601        }
602
603        Ok(all_results)
604    }
605
606    /// GETBULK request (SNMPv2c/v3 only).
607    ///
608    /// Retrieves multiple variable bindings in a single request.
609    ///
610    /// # Arguments
611    ///
612    /// * `oids` - OIDs to retrieve
613    /// * `non_repeaters` - Number of OIDs to treat as non-repeating
614    /// * `max_repetitions` - Maximum iterations for repeating OIDs
615    ///
616    /// # Example
617    ///
618    /// ```rust,no_run
619    /// # use async_snmp::{Auth, Client, oid};
620    /// # async fn example() -> async_snmp::Result<()> {
621    /// # let client = Client::builder("127.0.0.1:161", Auth::v2c("public")).connect().await?;
622    /// // Get next 10 entries starting from ifDescr
623    /// let results = client.get_bulk(&[oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2)], 0, 10).await?;
624    /// for vb in results {
625    ///     println!("{}: {:?}", vb.oid, vb.value);
626    /// }
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 for better performance when walking large tables.
731    ///
732    /// Uses the client's configured `oid_ordering` and `max_walk_results` settings.
733    /// If `max_repetitions` is not specified, uses the client's configured `max_repetitions`.
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}