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}