1#![allow(clippy::type_complexity)]
7
8macro_rules! impl_stream_helpers {
10 ($type:ident < $($gen:tt),+ >) => {
11 impl<$($gen),+> $type<$($gen),+>
12 where
13 $($gen: crate::transport::Transport + 'static,)+
14 {
15 pub async fn next(&mut self) -> Option<crate::error::Result<crate::varbind::VarBind>> {
17 std::future::poll_fn(|cx| std::pin::Pin::new(&mut *self).poll_next(cx)).await
18 }
19
20 pub async fn collect(mut self) -> crate::error::Result<Vec<crate::varbind::VarBind>> {
22 let mut results = Vec::new();
23 while let Some(result) = self.next().await {
24 results.push(result?);
25 }
26 Ok(results)
27 }
28 }
29 };
30}
31
32use std::collections::{HashSet, VecDeque};
33use std::pin::Pin;
34use std::task::{Context, Poll};
35
36use futures_core::Stream;
37
38use crate::error::{Error, Result, WalkAbortReason};
39use crate::oid::Oid;
40use crate::transport::Transport;
41use crate::varbind::VarBind;
42use crate::version::Version;
43
44use super::Client;
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
48pub enum WalkMode {
49 #[default]
52 Auto,
53 GetNext,
55 GetBulk,
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub enum OidOrdering {
77 #[default]
82 Strict,
83
84 AllowNonIncreasing,
98}
99
100enum OidTracker {
101 Strict { last: Option<Oid> },
102 Relaxed { seen: HashSet<Oid> },
103}
104
105enum VarbindOutcome {
107 Yield,
109 Done,
111 Abort(Box<Error>),
113}
114
115fn validate_walk_varbind(
120 vb: &VarBind,
121 base_oid: &Oid,
122 oid_tracker: &mut OidTracker,
123 target: std::net::SocketAddr,
124) -> VarbindOutcome {
125 if vb.value.is_exception() {
126 return VarbindOutcome::Done;
127 }
128 if !vb.oid.starts_with(base_oid) {
129 return VarbindOutcome::Done;
130 }
131 match oid_tracker.check(&vb.oid, target) {
132 Ok(()) => VarbindOutcome::Yield,
133 Err(e) => VarbindOutcome::Abort(e),
134 }
135}
136
137impl OidTracker {
138 fn new(ordering: OidOrdering) -> Self {
139 match ordering {
140 OidOrdering::Strict => OidTracker::Strict { last: None },
141 OidOrdering::AllowNonIncreasing => OidTracker::Relaxed {
142 seen: HashSet::new(),
143 },
144 }
145 }
146
147 fn check(&mut self, oid: &Oid, target: std::net::SocketAddr) -> Result<()> {
148 match self {
149 OidTracker::Strict { last } => {
150 if let Some(prev) = last
151 && oid <= prev
152 {
153 tracing::debug!(target: "async_snmp::walk", { previous_oid = %prev, current_oid = %oid, %target }, "non-increasing OID detected");
154 return Err(Error::WalkAborted {
155 target,
156 reason: WalkAbortReason::NonIncreasing,
157 }
158 .boxed());
159 }
160 *last = Some(oid.clone());
161 Ok(())
162 }
163 OidTracker::Relaxed { seen } => {
164 if !seen.insert(oid.clone()) {
165 tracing::debug!(target: "async_snmp::walk", { %oid, %target }, "duplicate OID detected (cycle)");
166 return Err(Error::WalkAborted {
167 target,
168 reason: WalkAbortReason::Cycle,
169 }
170 .boxed());
171 }
172 Ok(())
173 }
174 }
175 }
176}
177
178pub struct Walk<T: Transport> {
182 client: Client<T>,
183 base_oid: Oid,
184 current_oid: Oid,
185 oid_tracker: OidTracker,
187 max_results: Option<usize>,
189 count: usize,
191 done: bool,
192 pending: Option<Pin<Box<dyn std::future::Future<Output = Result<VarBind>> + Send>>>,
193}
194
195impl<T: Transport> Walk<T> {
196 pub(crate) fn new(
197 client: Client<T>,
198 oid: Oid,
199 ordering: OidOrdering,
200 max_results: Option<usize>,
201 ) -> Self {
202 Self {
203 client,
204 base_oid: oid.clone(),
205 current_oid: oid,
206 oid_tracker: OidTracker::new(ordering),
207 max_results,
208 count: 0,
209 done: false,
210 pending: None,
211 }
212 }
213}
214
215impl_stream_helpers!(Walk<T>);
216
217impl<T: Transport + 'static> Stream for Walk<T> {
218 type Item = Result<VarBind>;
219
220 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
221 if self.done {
222 return Poll::Ready(None);
223 }
224
225 if let Some(max) = self.max_results
227 && self.count >= max
228 {
229 self.done = true;
230 return Poll::Ready(None);
231 }
232
233 if self.pending.is_none() {
235 let client = self.client.clone();
237 let oid = self.current_oid.clone();
238
239 let fut = Box::pin(async move { client.get_next(&oid).await });
240 self.pending = Some(fut);
241 }
242
243 let pending = self.pending.as_mut().unwrap();
245 match pending.as_mut().poll(cx) {
246 Poll::Pending => Poll::Pending,
247 Poll::Ready(result) => {
248 self.pending = None;
249
250 match result {
251 Ok(vb) => {
252 let target = self.client.peer_addr();
253 let base_oid = self.base_oid.clone();
254 match validate_walk_varbind(&vb, &base_oid, &mut self.oid_tracker, target) {
255 VarbindOutcome::Done => {
256 self.done = true;
257 return Poll::Ready(None);
258 }
259 VarbindOutcome::Abort(e) => {
260 self.done = true;
261 return Poll::Ready(Some(Err(e)));
262 }
263 VarbindOutcome::Yield => {}
264 }
265
266 self.current_oid = vb.oid.clone();
268 self.count += 1;
269
270 Poll::Ready(Some(Ok(vb)))
271 }
272 Err(e) => {
273 if self.client.inner.config.version == Version::V1
274 && matches!(
275 &*e,
276 Error::Snmp {
277 status: crate::error::ErrorStatus::NoSuchName,
278 ..
279 }
280 )
281 {
282 self.done = true;
283 return Poll::Ready(None);
284 }
285
286 self.done = true;
287 Poll::Ready(Some(Err(e)))
288 }
289 }
290 }
291 }
292 }
293}
294
295pub struct BulkWalk<T: Transport> {
299 client: Client<T>,
300 base_oid: Oid,
301 current_oid: Oid,
302 max_repetitions: i32,
303 oid_tracker: OidTracker,
305 max_results: Option<usize>,
307 count: usize,
309 done: bool,
310 buffer: VecDeque<VarBind>,
312 pending: Option<Pin<Box<dyn std::future::Future<Output = Result<Vec<VarBind>>> + Send>>>,
313}
314
315impl<T: Transport> BulkWalk<T> {
316 pub(crate) fn new(
317 client: Client<T>,
318 oid: Oid,
319 max_repetitions: i32,
320 ordering: OidOrdering,
321 max_results: Option<usize>,
322 ) -> Self {
323 Self {
324 client,
325 base_oid: oid.clone(),
326 current_oid: oid,
327 max_repetitions,
328 oid_tracker: OidTracker::new(ordering),
329 max_results,
330 count: 0,
331 done: false,
332 buffer: VecDeque::new(),
333 pending: None,
334 }
335 }
336}
337
338impl_stream_helpers!(BulkWalk<T>);
339
340impl<T: Transport + 'static> Stream for BulkWalk<T> {
341 type Item = Result<VarBind>;
342
343 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
344 loop {
345 if self.done {
346 return Poll::Ready(None);
347 }
348
349 if let Some(max) = self.max_results
351 && self.count >= max
352 {
353 self.done = true;
354 return Poll::Ready(None);
355 }
356
357 if let Some(vb) = self.buffer.pop_front() {
359 let target = self.client.peer_addr();
360 let base_oid = self.base_oid.clone();
361 match validate_walk_varbind(&vb, &base_oid, &mut self.oid_tracker, target) {
362 VarbindOutcome::Done => {
363 self.done = true;
364 return Poll::Ready(None);
365 }
366 VarbindOutcome::Abort(e) => {
367 self.done = true;
368 return Poll::Ready(Some(Err(e)));
369 }
370 VarbindOutcome::Yield => {}
371 }
372
373 self.current_oid = vb.oid.clone();
375 self.count += 1;
376
377 return Poll::Ready(Some(Ok(vb)));
378 }
379
380 if self.pending.is_none() {
382 let client = self.client.clone();
383 let oid = self.current_oid.clone();
384 let max_rep = self.max_repetitions;
385
386 let fut = Box::pin(async move { client.get_bulk(&[oid], 0, max_rep).await });
387 self.pending = Some(fut);
388 }
389
390 let pending = self.pending.as_mut().unwrap();
392 match pending.as_mut().poll(cx) {
393 Poll::Pending => return Poll::Pending,
394 Poll::Ready(result) => {
395 self.pending = None;
396
397 match result {
398 Ok(varbinds) => {
399 if varbinds.is_empty() {
400 self.done = true;
401 return Poll::Ready(None);
402 }
403
404 self.buffer = varbinds.into();
405 }
407 Err(e) => {
408 self.done = true;
409 return Poll::Ready(Some(Err(e)));
410 }
411 }
412 }
413 }
414 }
415 }
416}
417
418pub enum WalkStream<T: Transport> {
430 GetNext(Walk<T>),
432 GetBulk(BulkWalk<T>),
434}
435
436impl<T: Transport> WalkStream<T> {
437 pub(crate) fn new(
439 client: Client<T>,
440 oid: Oid,
441 version: Version,
442 walk_mode: WalkMode,
443 ordering: OidOrdering,
444 max_results: Option<usize>,
445 max_repetitions: i32,
446 ) -> Result<Self> {
447 let use_bulk = match walk_mode {
448 WalkMode::Auto => version != Version::V1,
449 WalkMode::GetNext => false,
450 WalkMode::GetBulk => {
451 if version == Version::V1 {
452 return Err(Error::Config("GETBULK is not supported in SNMPv1".into()).boxed());
453 }
454 true
455 }
456 };
457
458 Ok(if use_bulk {
459 WalkStream::GetBulk(BulkWalk::new(
460 client,
461 oid,
462 max_repetitions,
463 ordering,
464 max_results,
465 ))
466 } else {
467 WalkStream::GetNext(Walk::new(client, oid, ordering, max_results))
468 })
469 }
470}
471
472impl<T: Transport + 'static> WalkStream<T> {
473 pub async fn next(&mut self) -> Option<Result<VarBind>> {
475 std::future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
476 }
477
478 pub async fn collect(mut self) -> Result<Vec<VarBind>> {
485 let mut results = Vec::new();
486 while let Some(result) = self.next().await {
487 results.push(result?);
488 }
489 if results.is_empty() {
490 let (client, base_oid) = match &self {
491 WalkStream::GetNext(w) => (&w.client, &w.base_oid),
492 WalkStream::GetBulk(bw) => (&bw.client, &bw.base_oid),
493 };
494 match client.get(base_oid).await {
495 Ok(vb) if !vb.value.is_exception() => {
496 results.push(vb);
497 }
498 _ => {}
499 }
500 }
501 Ok(results)
502 }
503}
504
505impl<T: Transport + 'static> Stream for WalkStream<T> {
506 type Item = Result<VarBind>;
507
508 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
509 match self.get_mut() {
511 WalkStream::GetNext(walk) => Pin::new(walk).poll_next(cx),
512 WalkStream::GetBulk(bulk_walk) => Pin::new(bulk_walk).poll_next(cx),
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use crate::oid;
521 use crate::value::Value;
522
523 fn target_addr() -> std::net::SocketAddr {
524 "127.0.0.1:161".parse().unwrap()
525 }
526
527 #[test]
528 fn test_walk_terminates_on_no_such_object() {
529 let base = oid!(1, 3, 6, 1, 2, 1, 1);
530 let mut tracker = OidTracker::new(OidOrdering::Strict);
531 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::NoSuchObject);
532 assert!(matches!(
533 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
534 VarbindOutcome::Done
535 ));
536 }
537
538 #[test]
539 fn test_walk_terminates_on_no_such_instance() {
540 let base = oid!(1, 3, 6, 1, 2, 1, 1);
541 let mut tracker = OidTracker::new(OidOrdering::Strict);
542 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::NoSuchInstance);
543 assert!(matches!(
544 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
545 VarbindOutcome::Done
546 ));
547 }
548
549 #[test]
550 fn test_walk_terminates_on_end_of_mib_view() {
551 let base = oid!(1, 3, 6, 1, 2, 1, 1);
552 let mut tracker = OidTracker::new(OidOrdering::Strict);
553 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::EndOfMibView);
554 assert!(matches!(
555 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
556 VarbindOutcome::Done
557 ));
558 }
559
560 #[test]
561 fn test_walk_yields_normal_value() {
562 let base = oid!(1, 3, 6, 1, 2, 1, 1);
563 let mut tracker = OidTracker::new(OidOrdering::Strict);
564 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(42));
565 assert!(matches!(
566 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
567 VarbindOutcome::Yield
568 ));
569 }
570
571 #[test]
572 fn test_walk_strict_aborts_on_non_increasing_oid() {
573 let base = oid!(1, 3, 6, 1, 2, 1, 1);
574 let mut tracker = OidTracker::new(OidOrdering::Strict);
575
576 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
577 assert!(matches!(
578 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
579 VarbindOutcome::Yield
580 ));
581
582 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(2));
584 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
585 VarbindOutcome::Abort(e) => match *e {
586 Error::WalkAborted { reason, .. } => {
587 assert_eq!(reason, WalkAbortReason::NonIncreasing);
588 }
589 other => panic!("expected WalkAborted, got {other:?}"),
590 },
591 _ => panic!("expected Abort outcome"),
592 }
593 }
594
595 #[test]
596 fn test_walk_strict_aborts_on_equal_oid() {
597 let base = oid!(1, 3, 6, 1, 2, 1, 1);
598 let mut tracker = OidTracker::new(OidOrdering::Strict);
599
600 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
601 assert!(matches!(
602 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
603 VarbindOutcome::Yield
604 ));
605
606 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
608 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
609 VarbindOutcome::Abort(e) => match *e {
610 Error::WalkAborted { reason, .. } => {
611 assert_eq!(reason, WalkAbortReason::NonIncreasing);
612 }
613 other => panic!("expected WalkAborted, got {other:?}"),
614 },
615 _ => panic!("expected Abort outcome"),
616 }
617 }
618
619 #[test]
620 fn test_walk_relaxed_aborts_on_duplicate_oid_cycle() {
621 let base = oid!(1, 3, 6, 1, 2, 1, 1);
622 let mut tracker = OidTracker::new(OidOrdering::AllowNonIncreasing);
623
624 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
625 assert!(matches!(
626 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
627 VarbindOutcome::Yield
628 ));
629
630 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
632 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
633 VarbindOutcome::Abort(e) => match *e {
634 Error::WalkAborted { reason, .. } => {
635 assert_eq!(reason, WalkAbortReason::Cycle);
636 }
637 other => panic!("expected WalkAborted, got {other:?}"),
638 },
639 _ => panic!("expected Abort outcome"),
640 }
641 }
642
643 #[test]
644 fn test_walk_relaxed_allows_non_increasing_distinct_oid() {
645 let base = oid!(1, 3, 6, 1, 2, 1, 1);
646 let mut tracker = OidTracker::new(OidOrdering::AllowNonIncreasing);
647
648 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::Integer(1));
650 assert!(matches!(
651 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
652 VarbindOutcome::Yield
653 ));
654
655 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(2));
657 assert!(matches!(
658 validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()),
659 VarbindOutcome::Yield
660 ));
661 }
662}