1mod auth;
4mod builder;
5mod retry;
6mod v3;
7mod walk;
8
9pub use auth::{Auth, CommunityVersion, UsmAuth, UsmBuilder};
10pub use builder::{ClientBuilder, Target};
11pub use retry::{Backoff, Retry, RetryBuilder};
12
13impl Client<UdpHandle> {
15 pub fn builder(target: impl Into<Target>, auth: impl Into<Auth>) -> ClientBuilder {
43 ClientBuilder::new(target, auth)
44 }
45}
46use crate::error::internal::DecodeErrorKind;
47use crate::error::{Error, ErrorStatus, Result};
48use crate::message::{CommunityMessage, Message};
49use crate::oid::Oid;
50use crate::pdu::{GetBulkPdu, Pdu, TrapV1Pdu};
51use crate::transport::Transport;
52use crate::transport::UdpHandle;
53use crate::v3::{EngineCache, EngineState, SaltCounter};
54use crate::value::Value;
55use crate::varbind::VarBind;
56use crate::version::Version;
57use bytes::Bytes;
58use std::net::SocketAddr;
59use std::pin::Pin;
60use std::sync::Arc;
61use std::sync::RwLock;
62use std::time::{Duration, Instant};
63use tokio::sync::Mutex as AsyncMutex;
64use tracing::{Span, instrument};
65
66pub use crate::notification::{DerivedKeys, UsmConfig};
67pub use walk::{BulkWalk, OidOrdering, Walk, WalkMode, WalkStream};
68
69pub(crate) fn pdu_to_snmp_error(pdu: &Pdu, target: SocketAddr) -> Option<Box<Error>> {
78 if !pdu.is_error() {
79 return None;
80 }
81 let status = pdu.error_status_enum();
82 let oid = (pdu.error_index as usize)
83 .checked_sub(1)
84 .and_then(|idx| pdu.varbinds.get(idx))
85 .map(|vb| vb.oid.clone());
86 Some(
87 Error::Snmp {
88 target,
89 status,
90 index: pdu.error_index.try_into().unwrap_or(0),
91 oid,
92 }
93 .boxed(),
94 )
95}
96
97pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
103
104pub const DEFAULT_MAX_OIDS_PER_REQUEST: usize = 10;
109
110pub const DEFAULT_MAX_REPETITIONS: u32 = 25;
114
115pub struct Client<T: Transport = UdpHandle> {
119 inner: Arc<ClientInner<T>>,
120}
121
122impl<T: Transport> Clone for Client<T> {
123 fn clone(&self) -> Self {
124 Self {
125 inner: Arc::clone(&self.inner),
126 }
127 }
128}
129
130struct ClientInner<T: Transport> {
131 transport: T,
132 config: ClientConfig,
133 engine_state: RwLock<Option<EngineState>>,
135 derived_keys: RwLock<Option<DerivedKeys>>,
137 salt_counter: SaltCounter,
139 engine_cache: Option<Arc<EngineCache>>,
141 discovery_lock: AsyncMutex<()>,
143 local_engine_start: Instant,
145 local_derived_keys: RwLock<Option<DerivedKeys>>,
147}
148
149#[derive(Clone)]
153pub struct ClientConfig {
154 pub version: Version,
156 pub community: Bytes,
158 pub timeout: Duration,
160 pub retry: Retry,
162 pub max_oids_per_request: usize,
164 pub v3_security: Option<UsmConfig>,
166 pub walk_mode: WalkMode,
168 pub oid_ordering: OidOrdering,
170 pub max_walk_results: Option<usize>,
172 pub max_repetitions: u32,
174 pub local_engine_id: Option<Bytes>,
179 pub local_engine_boots: u32,
181}
182
183impl Default for ClientConfig {
184 fn default() -> Self {
188 Self {
189 version: Version::V2c,
190 community: Bytes::from_static(b"public"),
191 timeout: DEFAULT_TIMEOUT,
192 retry: Retry::default(),
193 max_oids_per_request: DEFAULT_MAX_OIDS_PER_REQUEST,
194 v3_security: None,
195 walk_mode: WalkMode::Auto,
196 oid_ordering: OidOrdering::Strict,
197 max_walk_results: None,
198 max_repetitions: DEFAULT_MAX_REPETITIONS,
199 local_engine_id: None,
200 local_engine_boots: 1,
201 }
202 }
203}
204
205impl<T: Transport> Client<T> {
206 pub fn new(transport: T, config: ClientConfig) -> Self {
213 Self {
214 inner: Arc::new(ClientInner {
215 transport,
216 config,
217 engine_state: RwLock::new(None),
218 derived_keys: RwLock::new(None),
219 salt_counter: SaltCounter::new(),
220 engine_cache: None,
221 discovery_lock: AsyncMutex::new(()),
222 local_engine_start: Instant::now(),
223 local_derived_keys: RwLock::new(None),
224 }),
225 }
226 }
227
228 pub fn with_engine_cache(
230 transport: T,
231 config: ClientConfig,
232 engine_cache: Arc<EngineCache>,
233 ) -> Self {
234 Self {
235 inner: Arc::new(ClientInner {
236 transport,
237 config,
238 engine_state: RwLock::new(None),
239 derived_keys: RwLock::new(None),
240 salt_counter: SaltCounter::new(),
241 engine_cache: Some(engine_cache),
242 discovery_lock: AsyncMutex::new(()),
243 local_engine_start: Instant::now(),
244 local_derived_keys: RwLock::new(None),
245 }),
246 }
247 }
248
249 #[must_use]
254 pub fn peer_addr(&self) -> SocketAddr {
255 self.inner.transport.peer_addr()
256 }
257
258 fn next_request_id(&self) -> i32 {
262 self.inner.transport.alloc_request_id()
263 }
264
265 fn is_v3(&self) -> bool {
267 self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
268 }
269
270 #[instrument(
272 level = "debug",
273 skip(self, data),
274 fields(
275 snmp.target = %self.peer_addr(),
276 snmp.request_id = request_id,
277 snmp.attempt = tracing::field::Empty,
278 snmp.elapsed_ms = tracing::field::Empty,
279 )
280 )]
281 async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
282 let start = Instant::now();
283 let mut last_error: Option<Box<Error>> = None;
284 let max_attempts = if self.inner.transport.is_reliable() {
285 0
286 } else {
287 self.inner.config.retry.max_attempts
288 };
289
290 for attempt in 0..=max_attempts {
291 Span::current().record("snmp.attempt", attempt);
292 if attempt > 0 {
293 tracing::debug!(target: "async_snmp::client", "retrying request");
294 }
295
296 self.inner
298 .transport
299 .register_request(request_id, self.inner.config.timeout);
300
301 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending request");
303 self.inner.transport.send(data).await?;
304
305 match self.inner.transport.recv(request_id).await {
307 Ok((response_data, _source)) => {
308 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received response");
309
310 let response = Message::decode(response_data)?;
312
313 let response_version = response.version();
315 let expected_version = self.inner.config.version;
316 if response_version != expected_version {
317 tracing::warn!(target: "async_snmp::client", { ?expected_version, ?response_version, peer = %self.peer_addr() }, "version mismatch in response");
318 return Err(Error::MalformedResponse {
319 target: self.peer_addr(),
320 }
321 .boxed());
322 }
323
324 let Some(response_pdu) = response.into_pdu() else {
325 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "received TrapV1 in response to request");
326 return Err(Error::MalformedResponse {
327 target: self.peer_addr(),
328 }
329 .boxed());
330 };
331
332 if response_pdu.request_id != request_id {
334 tracing::warn!(target: "async_snmp::client", { expected_request_id = request_id, actual_request_id = response_pdu.request_id, peer = %self.peer_addr() }, "request ID mismatch in response");
335 return Err(Error::MalformedResponse {
336 target: self.peer_addr(),
337 }
338 .boxed());
339 }
340
341 if let Some(err) = pdu_to_snmp_error(&response_pdu, self.peer_addr()) {
343 Span::current()
344 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
345 return Err(err);
346 }
347
348 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
349 return Ok(response_pdu);
350 }
351 Err(e) if matches!(*e, Error::Timeout { .. }) => {
352 last_error = Some(e);
353 if attempt < max_attempts {
355 let delay = self.inner.config.retry.compute_delay(attempt);
356 if !delay.is_zero() {
357 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
358 tokio::time::sleep(delay).await;
359 }
360 }
361 }
363 Err(e) => {
364 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
365 return Err(e);
366 }
367 }
368 }
369
370 let elapsed = start.elapsed();
372 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
373 tracing::debug!(target: "async_snmp::client", { request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
374 Err(last_error.unwrap_or_else(|| {
375 Error::Timeout {
376 target: self.peer_addr(),
377 elapsed,
378 retries: max_attempts,
379 }
380 .boxed()
381 }))
382 }
383
384 async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
386 if self.is_v3() {
388 return self.send_v3_and_recv(pdu).await;
389 }
390
391 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len() }, "sending {} request", pdu.pdu_type);
392
393 let request_id = pdu.request_id;
394 let message = CommunityMessage::new(
395 self.inner.config.version,
396 self.inner.config.community.clone(),
397 pdu,
398 );
399 let data = message.encode();
400 let response = self.send_and_recv(request_id, &data).await?;
401
402 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?response.pdu_type, snmp.varbind_count = response.varbinds.len(), snmp.error_status = response.error_status, snmp.error_index = response.error_index }, "received {} response", response.pdu_type);
403
404 Ok(response)
405 }
406
407 async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
409 if self.is_v3() {
411 let pdu = Pdu::get_bulk(
413 pdu.request_id,
414 pdu.non_repeaters,
415 pdu.max_repetitions,
416 pdu.varbinds,
417 );
418 return self.send_v3_and_recv(pdu).await;
419 }
420
421 tracing::debug!(target: "async_snmp::client", { snmp.non_repeaters = pdu.non_repeaters, snmp.max_repetitions = pdu.max_repetitions, snmp.varbind_count = pdu.varbinds.len() }, "sending GetBulkRequest");
422
423 let request_id = pdu.request_id;
424 let data = CommunityMessage::encode_bulk(
425 self.inner.config.version,
426 self.inner.config.community.clone(),
427 &pdu,
428 );
429 let response = self.send_and_recv(request_id, &data).await?;
430
431 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?response.pdu_type, snmp.varbind_count = response.varbinds.len(), snmp.error_status = response.error_status, snmp.error_index = response.error_index }, "received {} response", response.pdu_type);
432
433 Ok(response)
434 }
435
436 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
438 pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
439 let request_id = self.next_request_id();
440 let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
441 let response = self.send_request(pdu).await?;
442
443 response.varbinds.into_iter().next().ok_or_else(|| {
444 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GET response");
445 Error::MalformedResponse {
446 target: self.peer_addr(),
447 }
448 .boxed()
449 })
450 }
451
452 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
473 pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
474 self.get_or_getnext_many(oids, "GET", Pdu::get_request)
475 .await
476 }
477
478 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
480 pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
481 let request_id = self.next_request_id();
482 let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
483 let response = self.send_request(pdu).await?;
484
485 response.varbinds.into_iter().next().ok_or_else(|| {
486 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GETNEXT response");
487 Error::MalformedResponse {
488 target: self.peer_addr(),
489 }
490 .boxed()
491 })
492 }
493
494 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
514 pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
515 self.get_or_getnext_many(oids, "GETNEXT", Pdu::get_next_request)
516 .await
517 }
518
519 async fn get_or_getnext_many(
524 &self,
525 oids: &[Oid],
526 op_name: &'static str,
527 op: fn(i32, &[Oid]) -> Pdu,
528 ) -> Result<Vec<VarBind>> {
529 if oids.is_empty() {
530 return Ok(Vec::new());
531 }
532
533 let max_per_request = self.inner.config.max_oids_per_request;
534 let mut all_results = Vec::with_capacity(oids.len());
535
536 for chunk in oids.chunks(max_per_request) {
537 self.send_batch_with_bisect(chunk, op_name, op, &mut all_results)
538 .await?;
539 }
540
541 Ok(all_results)
542 }
543
544 fn send_batch_with_bisect<'a>(
550 &'a self,
551 oids: &'a [Oid],
552 op_name: &'static str,
553 op: fn(i32, &[Oid]) -> Pdu,
554 results: &'a mut Vec<VarBind>,
555 ) -> Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
556 Box::pin(async move {
557 let request_id = self.next_request_id();
558 let pdu = op(request_id, oids);
559 match self.send_request(pdu).await {
560 Ok(response) => {
561 if response.varbinds.len() > oids.len() {
562 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = oids.len(), actual = response.varbinds.len(), snmp.op = op_name }, "response has more varbinds than requested");
563 return Err(Error::MalformedResponse {
564 target: self.peer_addr(),
565 }
566 .boxed());
567 } else if response.varbinds.len() < oids.len() {
568 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = oids.len(), actual = response.varbinds.len(), snmp.op = op_name }, "response has fewer varbinds than requested");
569 }
570 results.extend(response.varbinds);
571 Ok(())
572 }
573 Err(e)
574 if oids.len() > 1
575 && matches!(
576 &*e,
577 Error::Snmp {
578 status: ErrorStatus::TooBig,
579 ..
580 }
581 ) =>
582 {
583 let mid = oids.len() / 2;
584 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), snmp.batch_size = oids.len(), snmp.split_at = mid, snmp.op = op_name }, "tooBig response, bisecting batch");
585 self.send_batch_with_bisect(&oids[..mid], op_name, op, results)
586 .await?;
587 self.send_batch_with_bisect(&oids[mid..], op_name, op, results)
588 .await?;
589 Ok(())
590 }
591 Err(e) => Err(e),
592 }
593 })
594 }
595
596 #[instrument(skip(self, value), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
598 pub async fn set(&self, oid: &Oid, value: Value) -> Result<VarBind> {
599 let request_id = self.next_request_id();
600 let varbind = VarBind::new(oid.clone(), value);
601 let pdu = Pdu::set_request(request_id, vec![varbind]);
602 let response = self.send_request(pdu).await?;
603
604 response.varbinds.into_iter().next().ok_or_else(|| {
605 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty SET response");
606 Error::MalformedResponse {
607 target: self.peer_addr(),
608 }
609 .boxed()
610 })
611 }
612
613 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = varbinds.len()))]
639 pub async fn set_many(&self, varbinds: &[(Oid, Value)]) -> Result<Vec<VarBind>> {
640 if varbinds.is_empty() {
641 return Ok(Vec::new());
642 }
643
644 let max_per_request = self.inner.config.max_oids_per_request;
645
646 if varbinds.len() > max_per_request {
647 return Err(Error::Config(
648 format!(
649 "set_many: {} varbinds exceeds max_oids_per_request ({}); \
650 SET must be atomic and cannot be split across PDUs",
651 varbinds.len(),
652 max_per_request,
653 )
654 .into(),
655 )
656 .boxed());
657 }
658
659 let request_id = self.next_request_id();
660 let vbs: Vec<VarBind> = varbinds
661 .iter()
662 .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
663 .collect();
664 let expected_count = vbs.len();
665 let pdu = Pdu::set_request(request_id, vbs);
666 let response = self.send_request(pdu).await?;
667 if response.varbinds.len() > expected_count {
668 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has more varbinds than requested");
669 return Err(Error::MalformedResponse {
670 target: self.peer_addr(),
671 }
672 .boxed());
673 } else if response.varbinds.len() < expected_count {
674 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has fewer varbinds than requested");
675 }
676 Ok(response.varbinds)
677 }
678
679 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
698 pub async fn send_trap(
699 &self,
700 trap_oid: &Oid,
701 uptime: u32,
702 varbinds: Vec<VarBind>,
703 ) -> Result<()> {
704 if self.inner.config.version == Version::V1 {
705 let local_ip = match self.inner.transport.local_addr().ip() {
708 std::net::IpAddr::V4(v4) => v4.octets(),
709 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
710 };
711 let pdu = Pdu::trap_v2(0, uptime, trap_oid, varbinds);
714 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
715 Error::Config("cannot convert trap to v1 (Counter64 varbind?)".into()).boxed()
716 })?;
717 return self.send_v1_trap(trap).await;
718 }
719
720 let request_id = self.next_request_id();
721 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
722
723 if self.is_v3() {
724 self.ensure_local_keys_derived()?;
725 let msg_id = self.next_request_id();
726 let data = self.build_v3_trap_message(&pdu, msg_id)?;
727 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.varbind_count = pdu.varbinds.len(), snmp.bytes = data.len() }, "sending V3 trap");
728 self.inner.transport.send(&data).await?;
729 } else {
730 let message = CommunityMessage::new(
731 self.inner.config.version,
732 self.inner.config.community.clone(),
733 pdu,
734 );
735 let data = message.encode();
736 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.bytes = data.len() }, "sending v2c trap");
737 self.inner.transport.send(&data).await?;
738 }
739
740 Ok(())
741 }
742
743 #[instrument(skip(self, trap), err, fields(snmp.target = %self.peer_addr(), snmp.generic_trap = %trap.generic_trap))]
773 pub async fn send_v1_trap(&self, trap: TrapV1Pdu) -> Result<()> {
774 if self.inner.config.version != Version::V1 {
775 return Err(Error::Config("send_v1_trap requires a V1 client".into()).boxed());
776 }
777
778 let message = CommunityMessage::v1_trap(self.inner.config.community.clone(), trap);
779 let data = message.encode();
780 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV1", snmp.bytes = data.len() }, "sending v1 trap");
781 self.inner.transport.send(&data).await?;
782
783 Ok(())
784 }
785
786 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
801 pub async fn send_inform(
802 &self,
803 trap_oid: &Oid,
804 uptime: u32,
805 varbinds: Vec<VarBind>,
806 ) -> Result<()> {
807 if self.inner.config.version == Version::V1 {
808 return Err(Error::Config("v1 inform sending not supported".into()).boxed());
809 }
810
811 let request_id = self.next_request_id();
812 let pdu = Pdu::inform_request(request_id, uptime, trap_oid, varbinds);
813 let _response = self.send_request(pdu).await?;
814 Ok(())
815 }
816
817 #[instrument(skip(self, oids), err, fields(
850 snmp.target = %self.peer_addr(),
851 snmp.oid_count = oids.len(),
852 snmp.non_repeaters = non_repeaters,
853 snmp.max_repetitions = max_repetitions
854 ))]
855 pub async fn get_bulk(
856 &self,
857 oids: &[Oid],
858 non_repeaters: i32,
859 max_repetitions: i32,
860 ) -> Result<Vec<VarBind>> {
861 let request_id = self.next_request_id();
862 let pdu = GetBulkPdu::new(request_id, non_repeaters, max_repetitions, oids);
863 let response = self.send_bulk_request(pdu).await?;
864 Ok(response.varbinds)
865 }
866
867 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
893 pub fn walk(&self, oid: Oid) -> Result<WalkStream<T>>
894 where
895 T: 'static,
896 {
897 let ordering = self.inner.config.oid_ordering;
898 let max_results = self.inner.config.max_walk_results;
899 let walk_mode = self.inner.config.walk_mode;
900 let max_repetitions = self.inner.config.max_repetitions as i32;
901 let version = self.inner.config.version;
902
903 WalkStream::new(
904 self.clone(),
905 oid,
906 version,
907 walk_mode,
908 ordering,
909 max_results,
910 max_repetitions,
911 )
912 }
913
914 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
937 pub fn walk_getnext(&self, oid: Oid) -> Walk<T>
938 where
939 T: 'static,
940 {
941 let ordering = self.inner.config.oid_ordering;
942 let max_results = self.inner.config.max_walk_results;
943 Walk::new(self.clone(), oid, ordering, max_results)
944 }
945
946 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid, snmp.max_repetitions = max_repetitions))]
972 pub fn bulk_walk(&self, oid: Oid, max_repetitions: i32) -> BulkWalk<T>
973 where
974 T: 'static,
975 {
976 let ordering = self.inner.config.oid_ordering;
977 let max_results = self.inner.config.max_walk_results;
978 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
979 }
980
981 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
999 pub fn bulk_walk_default(&self, oid: Oid) -> BulkWalk<T>
1000 where
1001 T: 'static,
1002 {
1003 let ordering = self.inner.config.oid_ordering;
1004 let max_results = self.inner.config.max_walk_results;
1005 let max_repetitions = self.inner.config.max_repetitions as i32;
1006 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013 use crate::message::CommunityMessage;
1014 use crate::oid::Oid;
1015 use crate::pdu::{Pdu, PduType};
1016 use crate::varbind::VarBind;
1017 use crate::version::Version;
1018 use bytes::Bytes;
1019 use std::collections::VecDeque;
1020 use std::net::SocketAddr;
1021 use std::sync::{Arc, Mutex};
1022
1023 #[derive(Clone)]
1029 struct TruncatingTransport {
1030 response_varbind_count: usize,
1032 pending: Arc<Mutex<VecDeque<i32>>>,
1035 }
1036
1037 impl TruncatingTransport {
1038 fn new(response_varbind_count: usize) -> Self {
1039 Self {
1040 response_varbind_count,
1041 pending: Arc::new(Mutex::new(VecDeque::new())),
1042 }
1043 }
1044 }
1045
1046 impl Transport for TruncatingTransport {
1047 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1048 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1050 {
1051 let mut q = self.pending.lock().unwrap();
1052 q.push_back(request_id);
1053 }
1054 async { Ok(()) }
1055 }
1056
1057 fn recv(
1058 &self,
1059 _request_id: i32,
1060 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1061 let request_id = {
1062 let mut q = self.pending.lock().unwrap();
1063 q.pop_front().unwrap_or(1)
1064 };
1065 let n = self.response_varbind_count;
1066 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1067
1068 async move {
1069 let varbinds: Vec<VarBind> = (0..n)
1071 .map(|i| {
1072 VarBind::new(
1073 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1074 crate::value::Value::Null,
1075 )
1076 })
1077 .collect();
1078
1079 let pdu = Pdu {
1080 pdu_type: PduType::Response,
1081 request_id,
1082 error_status: 0,
1083 error_index: 0,
1084 varbinds,
1085 };
1086
1087 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1088 let encoded = msg.encode();
1089 Ok((encoded, peer))
1090 }
1091 }
1092
1093 fn peer_addr(&self) -> SocketAddr {
1094 "127.0.0.1:161".parse().unwrap()
1095 }
1096
1097 fn local_addr(&self) -> SocketAddr {
1098 "127.0.0.1:0".parse().unwrap()
1099 }
1100
1101 fn is_reliable(&self) -> bool {
1102 true
1103 }
1104 }
1105
1106 fn make_client(response_varbind_count: usize) -> Client<TruncatingTransport> {
1107 let transport = TruncatingTransport::new(response_varbind_count);
1108 let config = ClientConfig {
1109 version: Version::V2c,
1110 max_oids_per_request: 10,
1111 retry: crate::client::retry::Retry::none(),
1112 ..Default::default()
1113 };
1114 Client::new(transport, config)
1115 }
1116
1117 #[tokio::test]
1118 async fn get_many_warns_on_truncated_response() {
1119 let client = make_client(1);
1121 let oids = [
1122 Oid::from_slice(&[1, 3, 6, 1, 1]),
1123 Oid::from_slice(&[1, 3, 6, 1, 2]),
1124 Oid::from_slice(&[1, 3, 6, 1, 3]),
1125 ];
1126
1127 let result = client.get_many(&oids).await;
1128 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1129 assert_eq!(result.unwrap().len(), 1);
1130 }
1131
1132 #[tokio::test]
1133 async fn get_many_rejects_inflated_response() {
1134 let client = make_client(5);
1136 let oids = [
1137 Oid::from_slice(&[1, 3, 6, 1, 1]),
1138 Oid::from_slice(&[1, 3, 6, 1, 2]),
1139 Oid::from_slice(&[1, 3, 6, 1, 3]),
1140 ];
1141
1142 let err = client.get_many(&oids).await.unwrap_err();
1143 assert!(
1144 matches!(*err, Error::MalformedResponse { .. }),
1145 "expected MalformedResponse, got: {err}"
1146 );
1147 }
1148
1149 #[tokio::test]
1150 async fn get_many_accepts_correct_response_count() {
1151 let client = make_client(3);
1153 let oids = [
1154 Oid::from_slice(&[1, 3, 6, 1, 1]),
1155 Oid::from_slice(&[1, 3, 6, 1, 2]),
1156 Oid::from_slice(&[1, 3, 6, 1, 3]),
1157 ];
1158
1159 let result = client.get_many(&oids).await;
1160 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1161 assert_eq!(result.unwrap().len(), 3);
1162 }
1163
1164 #[tokio::test]
1165 async fn get_next_many_warns_on_truncated_response() {
1166 let client = make_client(1);
1168 let oids = [
1169 Oid::from_slice(&[1, 3, 6, 1, 1]),
1170 Oid::from_slice(&[1, 3, 6, 1, 2]),
1171 Oid::from_slice(&[1, 3, 6, 1, 3]),
1172 ];
1173
1174 let result = client.get_next_many(&oids).await;
1175 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1176 assert_eq!(result.unwrap().len(), 1);
1177 }
1178
1179 #[tokio::test]
1180 async fn get_next_many_rejects_inflated_response() {
1181 let client = make_client(5);
1183 let oids = [
1184 Oid::from_slice(&[1, 3, 6, 1, 1]),
1185 Oid::from_slice(&[1, 3, 6, 1, 2]),
1186 Oid::from_slice(&[1, 3, 6, 1, 3]),
1187 ];
1188
1189 let err = client.get_next_many(&oids).await.unwrap_err();
1190 assert!(
1191 matches!(*err, Error::MalformedResponse { .. }),
1192 "expected MalformedResponse, got: {err}"
1193 );
1194 }
1195
1196 #[tokio::test]
1197 async fn get_next_many_accepts_correct_response_count() {
1198 let client = make_client(3);
1200 let oids = [
1201 Oid::from_slice(&[1, 3, 6, 1, 1]),
1202 Oid::from_slice(&[1, 3, 6, 1, 2]),
1203 Oid::from_slice(&[1, 3, 6, 1, 3]),
1204 ];
1205
1206 let result = client.get_next_many(&oids).await;
1207 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1208 assert_eq!(result.unwrap().len(), 3);
1209 }
1210
1211 #[tokio::test]
1212 async fn set_many_warns_on_truncated_response() {
1213 let client = make_client(1);
1215 let varbinds = [
1216 (
1217 Oid::from_slice(&[1, 3, 6, 1, 1]),
1218 crate::value::Value::Integer(1),
1219 ),
1220 (
1221 Oid::from_slice(&[1, 3, 6, 1, 2]),
1222 crate::value::Value::Integer(2),
1223 ),
1224 (
1225 Oid::from_slice(&[1, 3, 6, 1, 3]),
1226 crate::value::Value::Integer(3),
1227 ),
1228 ];
1229
1230 let result = client.set_many(&varbinds).await;
1231 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1232 assert_eq!(result.unwrap().len(), 1);
1233 }
1234
1235 #[tokio::test]
1236 async fn set_many_rejects_inflated_response() {
1237 let client = make_client(5);
1239 let varbinds = [
1240 (
1241 Oid::from_slice(&[1, 3, 6, 1, 1]),
1242 crate::value::Value::Integer(1),
1243 ),
1244 (
1245 Oid::from_slice(&[1, 3, 6, 1, 2]),
1246 crate::value::Value::Integer(2),
1247 ),
1248 (
1249 Oid::from_slice(&[1, 3, 6, 1, 3]),
1250 crate::value::Value::Integer(3),
1251 ),
1252 ];
1253
1254 let err = client.set_many(&varbinds).await.unwrap_err();
1255 assert!(
1256 matches!(*err, Error::MalformedResponse { .. }),
1257 "expected MalformedResponse, got: {err}"
1258 );
1259 }
1260
1261 #[tokio::test]
1262 async fn set_many_accepts_correct_response_count() {
1263 let client = make_client(3);
1265 let varbinds = [
1266 (
1267 Oid::from_slice(&[1, 3, 6, 1, 1]),
1268 crate::value::Value::Integer(1),
1269 ),
1270 (
1271 Oid::from_slice(&[1, 3, 6, 1, 2]),
1272 crate::value::Value::Integer(2),
1273 ),
1274 (
1275 Oid::from_slice(&[1, 3, 6, 1, 3]),
1276 crate::value::Value::Integer(3),
1277 ),
1278 ];
1279
1280 let result = client.set_many(&varbinds).await;
1281 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1282 assert_eq!(result.unwrap().len(), 3);
1283 }
1284
1285 #[derive(Clone)]
1290 struct TooBigTransport {
1291 max_varbinds: usize,
1293 pending: Arc<Mutex<VecDeque<(i32, usize)>>>,
1294 }
1295
1296 impl TooBigTransport {
1297 fn new(max_varbinds: usize) -> Self {
1298 Self {
1299 max_varbinds,
1300 pending: Arc::new(Mutex::new(VecDeque::new())),
1301 }
1302 }
1303 }
1304
1305 impl Transport for TooBigTransport {
1306 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1307 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1308 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
1310 let varbind_count = msg.pdu.standard().unwrap().varbinds.len();
1311 {
1312 let mut q = self.pending.lock().unwrap();
1313 q.push_back((request_id, varbind_count));
1314 }
1315 async { Ok(()) }
1316 }
1317
1318 fn recv(
1319 &self,
1320 _request_id: i32,
1321 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1322 let (request_id, varbind_count) = {
1323 let mut q = self.pending.lock().unwrap();
1324 q.pop_front().unwrap_or((1, 0))
1325 };
1326 let max = self.max_varbinds;
1327 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1328
1329 async move {
1330 let pdu = if varbind_count > max {
1331 Pdu {
1333 pdu_type: PduType::Response,
1334 request_id,
1335 error_status: ErrorStatus::TooBig.as_i32(),
1336 error_index: 0,
1337 varbinds: vec![],
1338 }
1339 } else {
1340 let varbinds: Vec<VarBind> = (0..varbind_count)
1342 .map(|i| {
1343 VarBind::new(
1344 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1345 crate::value::Value::Integer(i as i32),
1346 )
1347 })
1348 .collect();
1349 Pdu {
1350 pdu_type: PduType::Response,
1351 request_id,
1352 error_status: 0,
1353 error_index: 0,
1354 varbinds,
1355 }
1356 };
1357
1358 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1359 Ok((msg.encode(), peer))
1360 }
1361 }
1362
1363 fn peer_addr(&self) -> SocketAddr {
1364 "127.0.0.1:161".parse().unwrap()
1365 }
1366
1367 fn local_addr(&self) -> SocketAddr {
1368 "127.0.0.1:0".parse().unwrap()
1369 }
1370
1371 fn is_reliable(&self) -> bool {
1372 true
1373 }
1374 }
1375
1376 #[tokio::test]
1377 async fn get_many_bisects_on_too_big() {
1378 let transport = TooBigTransport::new(3);
1383 let config = ClientConfig {
1384 version: Version::V2c,
1385 max_oids_per_request: 10,
1386 retry: crate::client::retry::Retry::none(),
1387 ..Default::default()
1388 };
1389 let client = Client::new(transport, config);
1390
1391 let oids: Vec<Oid> = (0..8u32)
1392 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1393 .collect();
1394
1395 let result = client.get_many(&oids).await.unwrap();
1396 assert_eq!(result.len(), 8);
1397 }
1398
1399 #[tokio::test]
1400 async fn get_many_single_oid_too_big_is_unrecoverable() {
1401 let transport = TooBigTransport::new(0);
1403 let config = ClientConfig {
1404 version: Version::V2c,
1405 max_oids_per_request: 10,
1406 retry: crate::client::retry::Retry::none(),
1407 ..Default::default()
1408 };
1409 let client = Client::new(transport, config);
1410
1411 let oids = [Oid::from_slice(&[1, 3, 6, 1, 1])];
1412 let err = client.get_many(&oids).await.unwrap_err();
1413 assert!(
1414 matches!(
1415 &*err,
1416 Error::Snmp {
1417 status: ErrorStatus::TooBig,
1418 ..
1419 }
1420 ),
1421 "expected TooBig, got: {err}"
1422 );
1423 }
1424
1425 #[tokio::test]
1426 async fn get_next_many_bisects_on_too_big() {
1427 let transport = TooBigTransport::new(3);
1429 let config = ClientConfig {
1430 version: Version::V2c,
1431 max_oids_per_request: 10,
1432 retry: crate::client::retry::Retry::none(),
1433 ..Default::default()
1434 };
1435 let client = Client::new(transport, config);
1436
1437 let oids: Vec<Oid> = (0..8u32)
1438 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1439 .collect();
1440
1441 let result = client.get_next_many(&oids).await.unwrap();
1442 assert_eq!(result.len(), 8);
1443 }
1444
1445 #[tokio::test]
1447 async fn get_many_batched_warns_on_truncated_response() {
1448 let transport = TruncatingTransport::new(1);
1451 let config = ClientConfig {
1452 version: Version::V2c,
1453 max_oids_per_request: 10,
1454 retry: crate::client::retry::Retry::none(),
1455 ..Default::default()
1456 };
1457 let client = Client::new(transport, config);
1458
1459 let oids: Vec<Oid> = (0..12u32)
1460 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1461 .collect();
1462
1463 let result = client.get_many(&oids).await;
1464 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1465 assert_eq!(result.unwrap().len(), 2); }
1467
1468 #[tokio::test]
1469 async fn get_many_batched_rejects_inflated_response() {
1470 let transport = TruncatingTransport::new(12);
1472 let config = ClientConfig {
1473 version: Version::V2c,
1474 max_oids_per_request: 10,
1475 retry: crate::client::retry::Retry::none(),
1476 ..Default::default()
1477 };
1478 let client = Client::new(transport, config);
1479
1480 let oids: Vec<Oid> = (0..12u32)
1481 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1482 .collect();
1483
1484 let err = client.get_many(&oids).await.unwrap_err();
1485 assert!(
1486 matches!(*err, Error::MalformedResponse { .. }),
1487 "expected MalformedResponse, got: {err}"
1488 );
1489 }
1490}