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, PduType, 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 max_repetitions_to_wire(max_repetitions: u32) -> i32 {
83 i32::try_from(max_repetitions).unwrap_or(i32::MAX)
84}
85
86pub(crate) fn pdu_to_snmp_error(pdu: &Pdu, target: SocketAddr) -> Option<Box<Error>> {
91 if !pdu.is_error() {
92 return None;
93 }
94 let status = pdu.error_status_enum();
95 let oid = (pdu.error_index as usize)
96 .checked_sub(1)
97 .and_then(|idx| pdu.varbinds.get(idx))
98 .map(|vb| vb.oid.clone());
99 Some(
100 Error::Snmp {
101 target,
102 status,
103 index: pdu.error_index.try_into().unwrap_or(0),
104 oid,
105 }
106 .boxed(),
107 )
108}
109
110pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
116
117pub const DEFAULT_MAX_OIDS_PER_REQUEST: usize = 10;
122
123pub const DEFAULT_MAX_REPETITIONS: u32 = 25;
127
128pub struct Client<T: Transport = UdpHandle> {
132 inner: Arc<ClientInner<T>>,
133}
134
135impl<T: Transport> Clone for Client<T> {
136 fn clone(&self) -> Self {
137 Self {
138 inner: Arc::clone(&self.inner),
139 }
140 }
141}
142
143struct ClientInner<T: Transport> {
144 transport: T,
145 config: ClientConfig,
146 engine_state: RwLock<Option<EngineState>>,
148 derived_keys: RwLock<Option<DerivedKeys>>,
150 salt_counter: SaltCounter,
152 engine_cache: Option<Arc<EngineCache>>,
154 discovery_lock: AsyncMutex<()>,
156 local_engine_start: Instant,
158 local_derived_keys: RwLock<Option<DerivedKeys>>,
160}
161
162#[derive(Clone)]
166pub struct ClientConfig {
167 pub version: Version,
169 pub community: Bytes,
171 pub timeout: Duration,
173 pub retry: Retry,
175 pub max_oids_per_request: usize,
177 pub v3_security: Option<UsmConfig>,
179 pub walk_mode: WalkMode,
181 pub oid_ordering: OidOrdering,
183 pub max_walk_results: Option<usize>,
185 pub max_repetitions: u32,
187 pub local_engine_id: Option<Bytes>,
192 pub local_engine_boots: u32,
194}
195
196impl Default for ClientConfig {
197 fn default() -> Self {
201 Self {
202 version: Version::V2c,
203 community: Bytes::from_static(b"public"),
204 timeout: DEFAULT_TIMEOUT,
205 retry: Retry::default(),
206 max_oids_per_request: DEFAULT_MAX_OIDS_PER_REQUEST,
207 v3_security: None,
208 walk_mode: WalkMode::Auto,
209 oid_ordering: OidOrdering::Strict,
210 max_walk_results: None,
211 max_repetitions: DEFAULT_MAX_REPETITIONS,
212 local_engine_id: None,
213 local_engine_boots: 1,
214 }
215 }
216}
217
218impl ClientConfig {
219 #[must_use]
227 fn sanitized(mut self) -> Self {
228 if self.max_oids_per_request == 0 {
229 self.max_oids_per_request = DEFAULT_MAX_OIDS_PER_REQUEST;
230 }
231 self
232 }
233}
234
235impl<T: Transport> Client<T> {
236 pub fn new(transport: T, config: ClientConfig) -> Self {
243 Self {
244 inner: Arc::new(ClientInner {
245 transport,
246 config: config.sanitized(),
247 engine_state: RwLock::new(None),
248 derived_keys: RwLock::new(None),
249 salt_counter: SaltCounter::new(),
250 engine_cache: None,
251 discovery_lock: AsyncMutex::new(()),
252 local_engine_start: Instant::now(),
253 local_derived_keys: RwLock::new(None),
254 }),
255 }
256 }
257
258 pub fn with_engine_cache(
260 transport: T,
261 config: ClientConfig,
262 engine_cache: Arc<EngineCache>,
263 ) -> Self {
264 Self {
265 inner: Arc::new(ClientInner {
266 transport,
267 config: config.sanitized(),
268 engine_state: RwLock::new(None),
269 derived_keys: RwLock::new(None),
270 salt_counter: SaltCounter::new(),
271 engine_cache: Some(engine_cache),
272 discovery_lock: AsyncMutex::new(()),
273 local_engine_start: Instant::now(),
274 local_derived_keys: RwLock::new(None),
275 }),
276 }
277 }
278
279 #[must_use]
284 pub fn peer_addr(&self) -> SocketAddr {
285 self.inner.transport.peer_addr()
286 }
287
288 fn next_request_id(&self) -> i32 {
292 self.inner.transport.alloc_request_id()
293 }
294
295 fn is_v3(&self) -> bool {
297 self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
298 }
299
300 #[instrument(
302 level = "debug",
303 skip(self, data),
304 fields(
305 snmp.target = %self.peer_addr(),
306 snmp.request_id = request_id,
307 snmp.attempt = tracing::field::Empty,
308 snmp.elapsed_ms = tracing::field::Empty,
309 )
310 )]
311 async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
312 let start = Instant::now();
313 let mut last_error: Option<Box<Error>> = None;
314 let max_attempts = if self.inner.transport.is_reliable() {
315 0
316 } else {
317 self.inner.config.retry.max_attempts
318 };
319
320 for attempt in 0..=max_attempts {
321 Span::current().record("snmp.attempt", attempt);
322 if attempt > 0 {
323 tracing::debug!(target: "async_snmp::client", "retrying request");
324 }
325
326 self.inner
328 .transport
329 .register_request(request_id, self.inner.config.timeout);
330
331 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending request");
336 match self.inner.transport.request(data, request_id).await {
337 Ok((response_data, _source)) => {
338 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received response");
339
340 let response = Message::decode(response_data)?;
342
343 let response_version = response.version();
345 let expected_version = self.inner.config.version;
346 if response_version != expected_version {
347 tracing::warn!(target: "async_snmp::client", { ?expected_version, ?response_version, peer = %self.peer_addr() }, "version mismatch in response");
348 return Err(Error::MalformedResponse {
349 target: self.peer_addr(),
350 }
351 .boxed());
352 }
353
354 if let Message::Community(ref m) = response
358 && m.community != self.inner.config.community
359 {
360 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "community mismatch in response");
361 }
362
363 let Some(response_pdu) = response.into_pdu() else {
364 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "received TrapV1 in response to request");
365 return Err(Error::MalformedResponse {
366 target: self.peer_addr(),
367 }
368 .boxed());
369 };
370
371 if response_pdu.pdu_type != PduType::Response {
374 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), pdu_type = ?response_pdu.pdu_type }, "non-Response PDU in response");
375 return Err(Error::MalformedResponse {
376 target: self.peer_addr(),
377 }
378 .boxed());
379 }
380
381 if response_pdu.request_id != request_id {
383 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");
384 return Err(Error::MalformedResponse {
385 target: self.peer_addr(),
386 }
387 .boxed());
388 }
389
390 if let Some(err) = pdu_to_snmp_error(&response_pdu, self.peer_addr()) {
392 Span::current()
393 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
394 return Err(err);
395 }
396
397 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
398 return Ok(response_pdu);
399 }
400 Err(e) if matches!(*e, Error::Timeout { .. }) => {
401 last_error = Some(e);
402 if attempt < max_attempts {
404 let delay = self.inner.config.retry.compute_delay(attempt);
405 if !delay.is_zero() {
406 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
407 tokio::time::sleep(delay).await;
408 }
409 }
410 }
412 Err(e) => {
413 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
414 return Err(e);
415 }
416 }
417 }
418
419 let _ = last_error;
425 let elapsed = start.elapsed();
426 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
427 tracing::debug!(target: "async_snmp::client", { request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
428 Err(Error::Timeout {
429 target: self.peer_addr(),
430 elapsed,
431 retries: max_attempts,
432 }
433 .boxed())
434 }
435
436 async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
438 if self.is_v3() {
440 return self.send_v3_and_recv(pdu).await;
441 }
442
443 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len() }, "sending {} request", pdu.pdu_type);
444
445 let request_id = pdu.request_id;
446 let message = CommunityMessage::new(
447 self.inner.config.version,
448 self.inner.config.community.clone(),
449 pdu,
450 );
451 let data = message.encode();
452 let response = self.send_and_recv(request_id, &data).await?;
453
454 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);
455
456 Ok(response)
457 }
458
459 async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
461 if self.is_v3() {
463 let pdu = Pdu::get_bulk(
465 pdu.request_id,
466 pdu.non_repeaters,
467 pdu.max_repetitions,
468 pdu.varbinds,
469 );
470 return self.send_v3_and_recv(pdu).await;
471 }
472
473 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");
474
475 let request_id = pdu.request_id;
476 let data = CommunityMessage::encode_bulk(
477 self.inner.config.version,
478 self.inner.config.community.clone(),
479 &pdu,
480 );
481 let response = self.send_and_recv(request_id, &data).await?;
482
483 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);
484
485 Ok(response)
486 }
487
488 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
490 pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
491 let request_id = self.next_request_id();
492 let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
493 let response = self.send_request(pdu).await?;
494
495 response.varbinds.into_iter().next().ok_or_else(|| {
496 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GET response");
497 Error::MalformedResponse {
498 target: self.peer_addr(),
499 }
500 .boxed()
501 })
502 }
503
504 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
525 pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
526 self.get_or_getnext_many(oids, "GET", Pdu::get_request)
527 .await
528 }
529
530 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
532 pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
533 let request_id = self.next_request_id();
534 let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
535 let response = self.send_request(pdu).await?;
536
537 response.varbinds.into_iter().next().ok_or_else(|| {
538 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GETNEXT response");
539 Error::MalformedResponse {
540 target: self.peer_addr(),
541 }
542 .boxed()
543 })
544 }
545
546 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
566 pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
567 self.get_or_getnext_many(oids, "GETNEXT", Pdu::get_next_request)
568 .await
569 }
570
571 async fn get_or_getnext_many(
576 &self,
577 oids: &[Oid],
578 op_name: &'static str,
579 op: fn(i32, &[Oid]) -> Pdu,
580 ) -> Result<Vec<VarBind>> {
581 if oids.is_empty() {
582 return Ok(Vec::new());
583 }
584
585 let max_per_request = self.inner.config.max_oids_per_request;
586 let mut all_results = Vec::with_capacity(oids.len());
587
588 for chunk in oids.chunks(max_per_request) {
589 self.send_batch_with_bisect(chunk, op_name, op, &mut all_results)
590 .await?;
591 }
592
593 Ok(all_results)
594 }
595
596 fn send_batch_with_bisect<'a>(
602 &'a self,
603 oids: &'a [Oid],
604 op_name: &'static str,
605 op: fn(i32, &[Oid]) -> Pdu,
606 results: &'a mut Vec<VarBind>,
607 ) -> Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
608 Box::pin(async move {
609 let request_id = self.next_request_id();
610 let pdu = op(request_id, oids);
611 match self.send_request(pdu).await {
612 Ok(response) => {
613 if response.varbinds.len() > oids.len() {
614 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");
615 return Err(Error::MalformedResponse {
616 target: self.peer_addr(),
617 }
618 .boxed());
619 } else if response.varbinds.len() < oids.len() {
620 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");
621 return Err(Error::MalformedResponse {
622 target: self.peer_addr(),
623 }
624 .boxed());
625 }
626 results.extend(response.varbinds);
627 Ok(())
628 }
629 Err(e)
630 if oids.len() > 1
631 && matches!(
632 &*e,
633 Error::Snmp {
634 status: ErrorStatus::TooBig,
635 ..
636 }
637 ) =>
638 {
639 let mid = oids.len() / 2;
640 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");
641 self.send_batch_with_bisect(&oids[..mid], op_name, op, results)
642 .await?;
643 self.send_batch_with_bisect(&oids[mid..], op_name, op, results)
644 .await?;
645 Ok(())
646 }
647 Err(e) => Err(e),
648 }
649 })
650 }
651
652 #[instrument(skip(self, value), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
654 pub async fn set(&self, oid: &Oid, value: Value) -> Result<VarBind> {
655 let request_id = self.next_request_id();
656 let varbind = VarBind::new(oid.clone(), value);
657 let pdu = Pdu::set_request(request_id, vec![varbind]);
658 let response = self.send_request(pdu).await?;
659
660 response.varbinds.into_iter().next().ok_or_else(|| {
661 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty SET response");
662 Error::MalformedResponse {
663 target: self.peer_addr(),
664 }
665 .boxed()
666 })
667 }
668
669 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = varbinds.len()))]
695 pub async fn set_many(&self, varbinds: &[(Oid, Value)]) -> Result<Vec<VarBind>> {
696 if varbinds.is_empty() {
697 return Ok(Vec::new());
698 }
699
700 let max_per_request = self.inner.config.max_oids_per_request;
701
702 if varbinds.len() > max_per_request {
703 return Err(Error::Config(
704 format!(
705 "set_many: {} varbinds exceeds max_oids_per_request ({}); \
706 SET must be atomic and cannot be split across PDUs",
707 varbinds.len(),
708 max_per_request,
709 )
710 .into(),
711 )
712 .boxed());
713 }
714
715 let request_id = self.next_request_id();
716 let vbs: Vec<VarBind> = varbinds
717 .iter()
718 .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
719 .collect();
720 let expected_count = vbs.len();
721 let pdu = Pdu::set_request(request_id, vbs);
722 let response = self.send_request(pdu).await?;
723 if response.varbinds.len() > expected_count {
724 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has more varbinds than requested");
725 return Err(Error::MalformedResponse {
726 target: self.peer_addr(),
727 }
728 .boxed());
729 } else if response.varbinds.len() < expected_count {
730 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has fewer varbinds than requested");
731 }
732 Ok(response.varbinds)
733 }
734
735 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
754 pub async fn send_trap(
755 &self,
756 trap_oid: &Oid,
757 uptime: u32,
758 varbinds: Vec<VarBind>,
759 ) -> Result<()> {
760 if self.inner.config.version == Version::V1 {
761 let local_ip = match self.inner.transport.local_addr().ip() {
764 std::net::IpAddr::V4(v4) => v4.octets(),
765 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
766 };
767 let pdu = Pdu::trap_v2(0, uptime, trap_oid, varbinds);
770 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
771 Error::Config("cannot convert trap to v1 (Counter64 varbind?)".into()).boxed()
772 })?;
773 return self.send_v1_trap(trap).await;
774 }
775
776 let request_id = self.next_request_id();
777 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
778
779 if self.is_v3() {
780 self.ensure_local_keys_derived()?;
781 let msg_id = self.next_request_id();
782 let data = self.build_v3_trap_message(&pdu, msg_id)?;
783 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.varbind_count = pdu.varbinds.len(), snmp.bytes = data.len() }, "sending V3 trap");
784 self.inner.transport.send(&data).await?;
785 } else {
786 let message = CommunityMessage::new(
787 self.inner.config.version,
788 self.inner.config.community.clone(),
789 pdu,
790 );
791 let data = message.encode();
792 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.bytes = data.len() }, "sending v2c trap");
793 self.inner.transport.send(&data).await?;
794 }
795
796 Ok(())
797 }
798
799 #[instrument(skip(self, trap), err, fields(snmp.target = %self.peer_addr(), snmp.generic_trap = %trap.generic_trap))]
829 pub async fn send_v1_trap(&self, trap: TrapV1Pdu) -> Result<()> {
830 if self.inner.config.version != Version::V1 {
831 return Err(Error::Config("send_v1_trap requires a V1 client".into()).boxed());
832 }
833
834 let message = CommunityMessage::v1_trap(self.inner.config.community.clone(), trap);
835 let data = message.encode();
836 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV1", snmp.bytes = data.len() }, "sending v1 trap");
837 self.inner.transport.send(&data).await?;
838
839 Ok(())
840 }
841
842 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
857 pub async fn send_inform(
858 &self,
859 trap_oid: &Oid,
860 uptime: u32,
861 varbinds: Vec<VarBind>,
862 ) -> Result<()> {
863 if self.inner.config.version == Version::V1 {
864 return Err(Error::Config("v1 inform sending not supported".into()).boxed());
865 }
866
867 let request_id = self.next_request_id();
868 let pdu = Pdu::inform_request(request_id, uptime, trap_oid, varbinds);
869 let _response = self.send_request(pdu).await?;
870 Ok(())
871 }
872
873 #[instrument(skip(self, oids), err, fields(
906 snmp.target = %self.peer_addr(),
907 snmp.oid_count = oids.len(),
908 snmp.non_repeaters = non_repeaters,
909 snmp.max_repetitions = max_repetitions
910 ))]
911 pub async fn get_bulk(
912 &self,
913 oids: &[Oid],
914 non_repeaters: i32,
915 max_repetitions: i32,
916 ) -> Result<Vec<VarBind>> {
917 let request_id = self.next_request_id();
918 let pdu = GetBulkPdu::new(request_id, non_repeaters, max_repetitions, oids);
919 let response = self.send_bulk_request(pdu).await?;
920 Ok(response.varbinds)
921 }
922
923 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
949 pub fn walk(&self, oid: Oid) -> Result<WalkStream<T>>
950 where
951 T: 'static,
952 {
953 let ordering = self.inner.config.oid_ordering;
954 let max_results = self.inner.config.max_walk_results;
955 let walk_mode = self.inner.config.walk_mode;
956 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
957 let version = self.inner.config.version;
958
959 WalkStream::new(
960 self.clone(),
961 oid,
962 version,
963 walk_mode,
964 ordering,
965 max_results,
966 max_repetitions,
967 )
968 }
969
970 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
993 pub fn walk_getnext(&self, oid: Oid) -> Walk<T>
994 where
995 T: 'static,
996 {
997 let ordering = self.inner.config.oid_ordering;
998 let max_results = self.inner.config.max_walk_results;
999 Walk::new(self.clone(), oid, ordering, max_results)
1000 }
1001
1002 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid, snmp.max_repetitions = max_repetitions))]
1028 pub fn bulk_walk(&self, oid: Oid, max_repetitions: i32) -> BulkWalk<T>
1029 where
1030 T: 'static,
1031 {
1032 let ordering = self.inner.config.oid_ordering;
1033 let max_results = self.inner.config.max_walk_results;
1034 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1035 }
1036
1037 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
1055 pub fn bulk_walk_default(&self, oid: Oid) -> BulkWalk<T>
1056 where
1057 T: 'static,
1058 {
1059 let ordering = self.inner.config.oid_ordering;
1060 let max_results = self.inner.config.max_walk_results;
1061 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
1062 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use crate::message::CommunityMessage;
1070 use crate::oid::Oid;
1071 use crate::pdu::{Pdu, PduType};
1072 use crate::varbind::VarBind;
1073 use crate::version::Version;
1074 use bytes::Bytes;
1075 use std::collections::VecDeque;
1076 use std::net::SocketAddr;
1077 use std::sync::{Arc, Mutex};
1078
1079 #[test]
1086 fn test_max_repetitions_to_wire_saturates_above_i32_max() {
1087 assert_eq!(max_repetitions_to_wire(u32::MAX), i32::MAX);
1089 assert_eq!(
1090 max_repetitions_to_wire(i32::MAX as u32 + 1),
1091 i32::MAX,
1092 "one past i32::MAX must saturate, not wrap negative"
1093 );
1094 }
1095
1096 #[test]
1097 fn test_max_repetitions_to_wire_passes_through_in_range_values() {
1098 assert_eq!(max_repetitions_to_wire(0), 0);
1099 assert_eq!(max_repetitions_to_wire(25), 25);
1100 assert_eq!(max_repetitions_to_wire(i32::MAX as u32), i32::MAX);
1101 }
1102
1103 #[derive(Clone)]
1109 struct TruncatingTransport {
1110 response_varbind_count: usize,
1112 pending: Arc<Mutex<VecDeque<i32>>>,
1115 }
1116
1117 impl TruncatingTransport {
1118 fn new(response_varbind_count: usize) -> Self {
1119 Self {
1120 response_varbind_count,
1121 pending: Arc::new(Mutex::new(VecDeque::new())),
1122 }
1123 }
1124 }
1125
1126 impl Transport for TruncatingTransport {
1127 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1128 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1130 {
1131 let mut q = self.pending.lock().unwrap();
1132 q.push_back(request_id);
1133 }
1134 async { Ok(()) }
1135 }
1136
1137 fn recv(
1138 &self,
1139 _request_id: i32,
1140 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1141 let request_id = {
1142 let mut q = self.pending.lock().unwrap();
1143 q.pop_front().unwrap_or(1)
1144 };
1145 let n = self.response_varbind_count;
1146 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1147
1148 async move {
1149 let varbinds: Vec<VarBind> = (0..n)
1151 .map(|i| {
1152 VarBind::new(
1153 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1154 crate::value::Value::Null,
1155 )
1156 })
1157 .collect();
1158
1159 let pdu = Pdu {
1160 pdu_type: PduType::Response,
1161 request_id,
1162 error_status: 0,
1163 error_index: 0,
1164 varbinds,
1165 };
1166
1167 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1168 let encoded = msg.encode();
1169 Ok((encoded, peer))
1170 }
1171 }
1172
1173 fn peer_addr(&self) -> SocketAddr {
1174 "127.0.0.1:161".parse().unwrap()
1175 }
1176
1177 fn local_addr(&self) -> SocketAddr {
1178 "127.0.0.1:0".parse().unwrap()
1179 }
1180
1181 fn is_reliable(&self) -> bool {
1182 true
1183 }
1184 }
1185
1186 fn make_client(response_varbind_count: usize) -> Client<TruncatingTransport> {
1187 let transport = TruncatingTransport::new(response_varbind_count);
1188 let config = ClientConfig {
1189 version: Version::V2c,
1190 max_oids_per_request: 10,
1191 retry: crate::client::retry::Retry::none(),
1192 ..Default::default()
1193 };
1194 Client::new(transport, config)
1195 }
1196
1197 #[tokio::test]
1198 async fn new_clamps_zero_max_oids_per_request() {
1199 let transport = TruncatingTransport::new(3);
1203 let config = ClientConfig {
1204 version: Version::V2c,
1205 max_oids_per_request: 0,
1206 retry: crate::client::retry::Retry::none(),
1207 ..Default::default()
1208 };
1209 let client = Client::new(transport, config);
1210 assert_eq!(
1211 client.inner.config.max_oids_per_request,
1212 DEFAULT_MAX_OIDS_PER_REQUEST
1213 );
1214
1215 let oids = [
1216 Oid::from_slice(&[1, 3, 6, 1, 1]),
1217 Oid::from_slice(&[1, 3, 6, 1, 2]),
1218 Oid::from_slice(&[1, 3, 6, 1, 3]),
1219 ];
1220 let result = client.get_many(&oids).await;
1222 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1223 }
1224
1225 #[tokio::test]
1226 async fn get_many_rejects_truncated_response() {
1227 let client = make_client(1);
1230 let oids = [
1231 Oid::from_slice(&[1, 3, 6, 1, 1]),
1232 Oid::from_slice(&[1, 3, 6, 1, 2]),
1233 Oid::from_slice(&[1, 3, 6, 1, 3]),
1234 ];
1235
1236 let err = client.get_many(&oids).await.unwrap_err();
1237 assert!(
1238 matches!(*err, Error::MalformedResponse { .. }),
1239 "expected MalformedResponse, got: {err}"
1240 );
1241 }
1242
1243 #[tokio::test]
1244 async fn get_many_rejects_inflated_response() {
1245 let client = make_client(5);
1247 let oids = [
1248 Oid::from_slice(&[1, 3, 6, 1, 1]),
1249 Oid::from_slice(&[1, 3, 6, 1, 2]),
1250 Oid::from_slice(&[1, 3, 6, 1, 3]),
1251 ];
1252
1253 let err = client.get_many(&oids).await.unwrap_err();
1254 assert!(
1255 matches!(*err, Error::MalformedResponse { .. }),
1256 "expected MalformedResponse, got: {err}"
1257 );
1258 }
1259
1260 #[tokio::test]
1261 async fn get_many_accepts_correct_response_count() {
1262 let client = make_client(3);
1264 let oids = [
1265 Oid::from_slice(&[1, 3, 6, 1, 1]),
1266 Oid::from_slice(&[1, 3, 6, 1, 2]),
1267 Oid::from_slice(&[1, 3, 6, 1, 3]),
1268 ];
1269
1270 let result = client.get_many(&oids).await;
1271 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1272 assert_eq!(result.unwrap().len(), 3);
1273 }
1274
1275 #[tokio::test]
1276 async fn get_next_many_rejects_truncated_response() {
1277 let client = make_client(1);
1280 let oids = [
1281 Oid::from_slice(&[1, 3, 6, 1, 1]),
1282 Oid::from_slice(&[1, 3, 6, 1, 2]),
1283 Oid::from_slice(&[1, 3, 6, 1, 3]),
1284 ];
1285
1286 let err = client.get_next_many(&oids).await.unwrap_err();
1287 assert!(
1288 matches!(*err, Error::MalformedResponse { .. }),
1289 "expected MalformedResponse, got: {err}"
1290 );
1291 }
1292
1293 #[tokio::test]
1294 async fn get_next_many_rejects_inflated_response() {
1295 let client = make_client(5);
1297 let oids = [
1298 Oid::from_slice(&[1, 3, 6, 1, 1]),
1299 Oid::from_slice(&[1, 3, 6, 1, 2]),
1300 Oid::from_slice(&[1, 3, 6, 1, 3]),
1301 ];
1302
1303 let err = client.get_next_many(&oids).await.unwrap_err();
1304 assert!(
1305 matches!(*err, Error::MalformedResponse { .. }),
1306 "expected MalformedResponse, got: {err}"
1307 );
1308 }
1309
1310 #[tokio::test]
1311 async fn get_next_many_accepts_correct_response_count() {
1312 let client = make_client(3);
1314 let oids = [
1315 Oid::from_slice(&[1, 3, 6, 1, 1]),
1316 Oid::from_slice(&[1, 3, 6, 1, 2]),
1317 Oid::from_slice(&[1, 3, 6, 1, 3]),
1318 ];
1319
1320 let result = client.get_next_many(&oids).await;
1321 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1322 assert_eq!(result.unwrap().len(), 3);
1323 }
1324
1325 #[tokio::test]
1326 async fn set_many_warns_on_truncated_response() {
1327 let client = make_client(1);
1329 let varbinds = [
1330 (
1331 Oid::from_slice(&[1, 3, 6, 1, 1]),
1332 crate::value::Value::Integer(1),
1333 ),
1334 (
1335 Oid::from_slice(&[1, 3, 6, 1, 2]),
1336 crate::value::Value::Integer(2),
1337 ),
1338 (
1339 Oid::from_slice(&[1, 3, 6, 1, 3]),
1340 crate::value::Value::Integer(3),
1341 ),
1342 ];
1343
1344 let result = client.set_many(&varbinds).await;
1345 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1346 assert_eq!(result.unwrap().len(), 1);
1347 }
1348
1349 #[tokio::test]
1350 async fn set_many_rejects_inflated_response() {
1351 let client = make_client(5);
1353 let varbinds = [
1354 (
1355 Oid::from_slice(&[1, 3, 6, 1, 1]),
1356 crate::value::Value::Integer(1),
1357 ),
1358 (
1359 Oid::from_slice(&[1, 3, 6, 1, 2]),
1360 crate::value::Value::Integer(2),
1361 ),
1362 (
1363 Oid::from_slice(&[1, 3, 6, 1, 3]),
1364 crate::value::Value::Integer(3),
1365 ),
1366 ];
1367
1368 let err = client.set_many(&varbinds).await.unwrap_err();
1369 assert!(
1370 matches!(*err, Error::MalformedResponse { .. }),
1371 "expected MalformedResponse, got: {err}"
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn set_many_accepts_correct_response_count() {
1377 let client = make_client(3);
1379 let varbinds = [
1380 (
1381 Oid::from_slice(&[1, 3, 6, 1, 1]),
1382 crate::value::Value::Integer(1),
1383 ),
1384 (
1385 Oid::from_slice(&[1, 3, 6, 1, 2]),
1386 crate::value::Value::Integer(2),
1387 ),
1388 (
1389 Oid::from_slice(&[1, 3, 6, 1, 3]),
1390 crate::value::Value::Integer(3),
1391 ),
1392 ];
1393
1394 let result = client.set_many(&varbinds).await;
1395 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1396 assert_eq!(result.unwrap().len(), 3);
1397 }
1398
1399 #[derive(Clone)]
1404 struct TooBigTransport {
1405 max_varbinds: usize,
1407 pending: Arc<Mutex<VecDeque<(i32, usize)>>>,
1408 }
1409
1410 impl TooBigTransport {
1411 fn new(max_varbinds: usize) -> Self {
1412 Self {
1413 max_varbinds,
1414 pending: Arc::new(Mutex::new(VecDeque::new())),
1415 }
1416 }
1417 }
1418
1419 impl Transport for TooBigTransport {
1420 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1421 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1422 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
1424 let varbind_count = msg.pdu.standard().unwrap().varbinds.len();
1425 {
1426 let mut q = self.pending.lock().unwrap();
1427 q.push_back((request_id, varbind_count));
1428 }
1429 async { Ok(()) }
1430 }
1431
1432 fn recv(
1433 &self,
1434 _request_id: i32,
1435 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1436 let (request_id, varbind_count) = {
1437 let mut q = self.pending.lock().unwrap();
1438 q.pop_front().unwrap_or((1, 0))
1439 };
1440 let max = self.max_varbinds;
1441 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1442
1443 async move {
1444 let pdu = if varbind_count > max {
1445 Pdu {
1447 pdu_type: PduType::Response,
1448 request_id,
1449 error_status: ErrorStatus::TooBig.as_i32(),
1450 error_index: 0,
1451 varbinds: vec![],
1452 }
1453 } else {
1454 let varbinds: Vec<VarBind> = (0..varbind_count)
1456 .map(|i| {
1457 VarBind::new(
1458 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1459 crate::value::Value::Integer(i as i32),
1460 )
1461 })
1462 .collect();
1463 Pdu {
1464 pdu_type: PduType::Response,
1465 request_id,
1466 error_status: 0,
1467 error_index: 0,
1468 varbinds,
1469 }
1470 };
1471
1472 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1473 Ok((msg.encode(), peer))
1474 }
1475 }
1476
1477 fn peer_addr(&self) -> SocketAddr {
1478 "127.0.0.1:161".parse().unwrap()
1479 }
1480
1481 fn local_addr(&self) -> SocketAddr {
1482 "127.0.0.1:0".parse().unwrap()
1483 }
1484
1485 fn is_reliable(&self) -> bool {
1486 true
1487 }
1488 }
1489
1490 #[tokio::test]
1491 async fn get_many_bisects_on_too_big() {
1492 let transport = TooBigTransport::new(3);
1497 let config = ClientConfig {
1498 version: Version::V2c,
1499 max_oids_per_request: 10,
1500 retry: crate::client::retry::Retry::none(),
1501 ..Default::default()
1502 };
1503 let client = Client::new(transport, config);
1504
1505 let oids: Vec<Oid> = (0..8u32)
1506 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1507 .collect();
1508
1509 let result = client.get_many(&oids).await.unwrap();
1510 assert_eq!(result.len(), 8);
1511 }
1512
1513 #[tokio::test]
1514 async fn get_many_single_oid_too_big_is_unrecoverable() {
1515 let transport = TooBigTransport::new(0);
1517 let config = ClientConfig {
1518 version: Version::V2c,
1519 max_oids_per_request: 10,
1520 retry: crate::client::retry::Retry::none(),
1521 ..Default::default()
1522 };
1523 let client = Client::new(transport, config);
1524
1525 let oids = [Oid::from_slice(&[1, 3, 6, 1, 1])];
1526 let err = client.get_many(&oids).await.unwrap_err();
1527 assert!(
1528 matches!(
1529 &*err,
1530 Error::Snmp {
1531 status: ErrorStatus::TooBig,
1532 ..
1533 }
1534 ),
1535 "expected TooBig, got: {err}"
1536 );
1537 }
1538
1539 #[tokio::test]
1540 async fn get_next_many_bisects_on_too_big() {
1541 let transport = TooBigTransport::new(3);
1543 let config = ClientConfig {
1544 version: Version::V2c,
1545 max_oids_per_request: 10,
1546 retry: crate::client::retry::Retry::none(),
1547 ..Default::default()
1548 };
1549 let client = Client::new(transport, config);
1550
1551 let oids: Vec<Oid> = (0..8u32)
1552 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1553 .collect();
1554
1555 let result = client.get_next_many(&oids).await.unwrap();
1556 assert_eq!(result.len(), 8);
1557 }
1558
1559 #[tokio::test]
1561 async fn get_many_batched_rejects_truncated_response() {
1562 let transport = TruncatingTransport::new(1);
1566 let config = ClientConfig {
1567 version: Version::V2c,
1568 max_oids_per_request: 10,
1569 retry: crate::client::retry::Retry::none(),
1570 ..Default::default()
1571 };
1572 let client = Client::new(transport, config);
1573
1574 let oids: Vec<Oid> = (0..12u32)
1575 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1576 .collect();
1577
1578 let err = client.get_many(&oids).await.unwrap_err();
1579 assert!(
1580 matches!(*err, Error::MalformedResponse { .. }),
1581 "expected MalformedResponse, got: {err}"
1582 );
1583 }
1584
1585 #[tokio::test]
1586 async fn get_many_batched_rejects_inflated_response() {
1587 let transport = TruncatingTransport::new(12);
1589 let config = ClientConfig {
1590 version: Version::V2c,
1591 max_oids_per_request: 10,
1592 retry: crate::client::retry::Retry::none(),
1593 ..Default::default()
1594 };
1595 let client = Client::new(transport, config);
1596
1597 let oids: Vec<Oid> = (0..12u32)
1598 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1599 .collect();
1600
1601 let err = client.get_many(&oids).await.unwrap_err();
1602 assert!(
1603 matches!(*err, Error::MalformedResponse { .. }),
1604 "expected MalformedResponse, got: {err}"
1605 );
1606 }
1607
1608 #[derive(Clone)]
1614 struct AdversarialTransport {
1615 pdu_type: PduType,
1616 community: &'static [u8],
1617 respond_as_v1: bool,
1618 pending: Arc<Mutex<VecDeque<i32>>>,
1619 }
1620
1621 impl AdversarialTransport {
1622 fn new(pdu_type: PduType, community: &'static [u8], respond_as_v1: bool) -> Self {
1623 Self {
1624 pdu_type,
1625 community,
1626 respond_as_v1,
1627 pending: Arc::new(Mutex::new(VecDeque::new())),
1628 }
1629 }
1630 }
1631
1632 impl Transport for AdversarialTransport {
1633 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1634 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1635 self.pending.lock().unwrap().push_back(request_id);
1636 async { Ok(()) }
1637 }
1638
1639 fn recv(
1640 &self,
1641 _request_id: i32,
1642 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1643 let request_id = self.pending.lock().unwrap().pop_front().unwrap_or(1);
1644 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1645 let pdu = Pdu {
1646 pdu_type: self.pdu_type,
1647 request_id,
1648 error_status: 0,
1649 error_index: 0,
1650 varbinds: vec![VarBind::new(
1651 Oid::from_slice(&[1, 3, 6, 1, 1]),
1652 crate::value::Value::Null,
1653 )],
1654 };
1655 let community = Bytes::from_static(self.community);
1656 let msg = if self.respond_as_v1 {
1657 CommunityMessage::v1(community, pdu)
1658 } else {
1659 CommunityMessage::v2c(community, pdu)
1660 };
1661 let encoded = msg.encode();
1662 async move { Ok((encoded, peer)) }
1663 }
1664
1665 fn peer_addr(&self) -> SocketAddr {
1666 "127.0.0.1:161".parse().unwrap()
1667 }
1668
1669 fn local_addr(&self) -> SocketAddr {
1670 "127.0.0.1:0".parse().unwrap()
1671 }
1672
1673 fn is_reliable(&self) -> bool {
1674 true
1675 }
1676 }
1677
1678 fn adversarial_client(
1679 pdu_type: PduType,
1680 community: &'static [u8],
1681 respond_as_v1: bool,
1682 ) -> Client<AdversarialTransport> {
1683 let transport = AdversarialTransport::new(pdu_type, community, respond_as_v1);
1684 let config = ClientConfig {
1685 version: Version::V2c,
1686 retry: crate::client::retry::Retry::none(),
1687 ..Default::default()
1688 };
1689 Client::new(transport, config)
1690 }
1691
1692 #[tokio::test]
1695 async fn response_validation_accepts_well_formed_response() {
1696 let client = adversarial_client(PduType::Response, b"public", false);
1697 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1698 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1699 }
1700
1701 #[tokio::test]
1704 async fn response_validation_rejects_echoed_request_pdu() {
1705 let client = adversarial_client(PduType::GetRequest, b"public", false);
1706 let err = client
1707 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1708 .await
1709 .unwrap_err();
1710 assert!(
1711 matches!(*err, Error::MalformedResponse { .. }),
1712 "expected MalformedResponse, got: {err}"
1713 );
1714 }
1715
1716 #[tokio::test]
1718 async fn response_validation_accepts_community_mismatch() {
1719 let client = adversarial_client(PduType::Response, b"other", false);
1720 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1721 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1722 }
1723
1724 #[tokio::test]
1726 async fn response_validation_rejects_version_mismatch() {
1727 let client = adversarial_client(PduType::Response, b"public", true);
1728 let err = client
1729 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1730 .await
1731 .unwrap_err();
1732 assert!(
1733 matches!(*err, Error::MalformedResponse { .. }),
1734 "expected MalformedResponse, got: {err}"
1735 );
1736 }
1737}