1mod auth;
4mod builder;
5mod retry;
6mod v3;
7mod walk;
8
9pub use auth::{Auth, CommunityVersion};
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::v3::{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 ClientEngine {
144 state: EngineState,
145 derived_keys: DerivedKeys,
146}
147
148struct ClientInner<T: Transport> {
149 transport: T,
150 config: ClientConfig,
151 engine: RwLock<Option<ClientEngine>>,
153 salt_counter: SaltCounter,
155 engine_cache: Option<Arc<EngineCache>>,
157 discovery_lock: AsyncMutex<()>,
159 local_derived_keys: RwLock<Option<DerivedKeys>>,
161 #[cfg(test)]
162 deferred_authenticated_update_hook: RwLock<Option<Arc<dyn Fn() + Send + Sync>>>,
163}
164
165#[derive(Clone)]
169pub struct ClientConfig {
170 pub version: Version,
172 pub community: Bytes,
174 pub timeout: Duration,
176 pub retry: Retry,
178 pub max_oids_per_request: usize,
180 pub v3_security: Option<UsmConfig>,
182 pub allow_unauthenticated_v3_time_correction: bool,
191 pub walk_mode: WalkMode,
193 pub oid_ordering: OidOrdering,
195 pub max_walk_results: Option<usize>,
197 pub max_repetitions: u32,
199 pub local_authoritative_engine: Option<crate::v3::AuthoritativeEngine>,
205}
206
207impl Default for ClientConfig {
208 fn default() -> Self {
212 Self {
213 version: Version::V2c,
214 community: Bytes::from_static(b"public"),
215 timeout: DEFAULT_TIMEOUT,
216 retry: Retry::default(),
217 max_oids_per_request: DEFAULT_MAX_OIDS_PER_REQUEST,
218 v3_security: None,
219 allow_unauthenticated_v3_time_correction: false,
220 walk_mode: WalkMode::Auto,
221 oid_ordering: OidOrdering::Strict,
222 max_walk_results: None,
223 max_repetitions: DEFAULT_MAX_REPETITIONS,
224 local_authoritative_engine: None,
225 }
226 }
227}
228
229impl ClientConfig {
230 #[must_use]
238 fn sanitized(mut self) -> Self {
239 if self.max_oids_per_request == 0 {
240 self.max_oids_per_request = DEFAULT_MAX_OIDS_PER_REQUEST;
241 }
242 self
243 }
244}
245
246impl<T: Transport> Client<T> {
247 pub fn new(transport: T, config: ClientConfig) -> Self {
254 Self {
255 inner: Arc::new(ClientInner {
256 transport,
257 config: config.sanitized(),
258 engine: RwLock::new(None),
259 salt_counter: SaltCounter::new(),
260 engine_cache: None,
261 discovery_lock: AsyncMutex::new(()),
262 local_derived_keys: RwLock::new(None),
263 #[cfg(test)]
264 deferred_authenticated_update_hook: RwLock::new(None),
265 }),
266 }
267 }
268
269 pub fn with_engine_cache(
271 transport: T,
272 config: ClientConfig,
273 engine_cache: Arc<EngineCache>,
274 ) -> Self {
275 Self {
276 inner: Arc::new(ClientInner {
277 transport,
278 config: config.sanitized(),
279 engine: RwLock::new(None),
280 salt_counter: SaltCounter::new(),
281 engine_cache: Some(engine_cache),
282 discovery_lock: AsyncMutex::new(()),
283 local_derived_keys: RwLock::new(None),
284 #[cfg(test)]
285 deferred_authenticated_update_hook: RwLock::new(None),
286 }),
287 }
288 }
289
290 #[must_use]
295 pub fn peer_addr(&self) -> SocketAddr {
296 self.inner.transport.peer_addr()
297 }
298
299 fn next_request_id(&self) -> i32 {
303 self.inner.transport.alloc_request_id()
304 }
305
306 fn is_v3(&self) -> bool {
308 self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
309 }
310
311 #[instrument(
313 level = "debug",
314 skip(self, data),
315 fields(
316 snmp.target = %self.peer_addr(),
317 snmp.request_id = request_id,
318 snmp.attempt = tracing::field::Empty,
319 snmp.elapsed_ms = tracing::field::Empty,
320 )
321 )]
322 async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
323 let start = Instant::now();
324 let mut last_error: Option<Box<Error>> = None;
325 let max_attempts = if self.inner.transport.is_reliable() {
326 0
327 } else {
328 self.inner.config.retry.max_attempts
329 };
330
331 for attempt in 0..=max_attempts {
332 Span::current().record("snmp.attempt", attempt);
333 if attempt > 0 {
334 tracing::debug!(target: "async_snmp::client", "retrying request");
335 }
336
337 self.inner
339 .transport
340 .register_request(request_id, self.inner.config.timeout);
341
342 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending request");
347 match self.inner.transport.request(data, request_id).await {
348 Ok((response_data, _source)) => {
349 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received response");
350
351 let response = Message::decode(response_data)?;
353
354 let response_version = response.version();
356 let expected_version = self.inner.config.version;
357 if response_version != expected_version {
358 tracing::warn!(target: "async_snmp::client", { ?expected_version, ?response_version, peer = %self.peer_addr() }, "version mismatch in response");
359 return Err(Error::MalformedResponse {
360 target: self.peer_addr(),
361 }
362 .boxed());
363 }
364
365 if let Message::Community(ref m) = response
369 && m.community != self.inner.config.community
370 {
371 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "community mismatch in response");
372 }
373
374 let Some(response_pdu) = response.into_pdu() else {
375 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "received TrapV1 in response to request");
376 return Err(Error::MalformedResponse {
377 target: self.peer_addr(),
378 }
379 .boxed());
380 };
381
382 if response_pdu.pdu_type != PduType::Response {
385 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), pdu_type = ?response_pdu.pdu_type }, "non-Response PDU in response");
386 return Err(Error::MalformedResponse {
387 target: self.peer_addr(),
388 }
389 .boxed());
390 }
391
392 if response_pdu.request_id != request_id {
394 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");
395 return Err(Error::MalformedResponse {
396 target: self.peer_addr(),
397 }
398 .boxed());
399 }
400
401 if let Some(err) = pdu_to_snmp_error(&response_pdu, self.peer_addr()) {
403 Span::current()
404 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
405 return Err(err);
406 }
407
408 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
409 return Ok(response_pdu);
410 }
411 Err(e) if matches!(*e, Error::Timeout { .. }) => {
412 last_error = Some(e);
413 if attempt < max_attempts {
415 let delay = self.inner.config.retry.compute_delay(attempt);
416 if !delay.is_zero() {
417 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
418 tokio::time::sleep(delay).await;
419 }
420 }
421 }
423 Err(e) => {
424 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
425 return Err(e);
426 }
427 }
428 }
429
430 let _ = last_error;
436 let elapsed = start.elapsed();
437 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
438 tracing::debug!(target: "async_snmp::client", { request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
439 Err(Error::Timeout {
440 target: self.peer_addr(),
441 elapsed,
442 retries: max_attempts,
443 }
444 .boxed())
445 }
446
447 async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
449 if self.is_v3() {
451 return self.send_v3_and_recv(pdu).await;
452 }
453
454 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len() }, "sending {} request", pdu.pdu_type);
455
456 let request_id = pdu.request_id;
457 let message = CommunityMessage::new(
458 self.inner.config.version,
459 self.inner.config.community.clone(),
460 pdu,
461 );
462 let data = message.encode();
463 let response = self.send_and_recv(request_id, &data).await?;
464
465 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);
466
467 Ok(response)
468 }
469
470 async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
472 if self.is_v3() {
474 let pdu = Pdu::get_bulk(
476 pdu.request_id,
477 pdu.non_repeaters,
478 pdu.max_repetitions,
479 pdu.varbinds,
480 );
481 return self.send_v3_and_recv(pdu).await;
482 }
483
484 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");
485
486 let request_id = pdu.request_id;
487 let data = CommunityMessage::encode_bulk(
488 self.inner.config.version,
489 self.inner.config.community.clone(),
490 &pdu,
491 );
492 let response = self.send_and_recv(request_id, &data).await?;
493
494 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);
495
496 Ok(response)
497 }
498
499 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
501 pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
502 let request_id = self.next_request_id();
503 let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
504 let response = self.send_request(pdu).await?;
505
506 response.varbinds.into_iter().next().ok_or_else(|| {
507 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GET response");
508 Error::MalformedResponse {
509 target: self.peer_addr(),
510 }
511 .boxed()
512 })
513 }
514
515 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
536 pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
537 self.get_or_getnext_many(oids, "GET", Pdu::get_request)
538 .await
539 }
540
541 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
543 pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
544 let request_id = self.next_request_id();
545 let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
546 let response = self.send_request(pdu).await?;
547
548 response.varbinds.into_iter().next().ok_or_else(|| {
549 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GETNEXT response");
550 Error::MalformedResponse {
551 target: self.peer_addr(),
552 }
553 .boxed()
554 })
555 }
556
557 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
577 pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
578 self.get_or_getnext_many(oids, "GETNEXT", Pdu::get_next_request)
579 .await
580 }
581
582 async fn get_or_getnext_many(
587 &self,
588 oids: &[Oid],
589 op_name: &'static str,
590 op: fn(i32, &[Oid]) -> Pdu,
591 ) -> Result<Vec<VarBind>> {
592 if oids.is_empty() {
593 return Ok(Vec::new());
594 }
595
596 let max_per_request = self.inner.config.max_oids_per_request;
597 let mut all_results = Vec::with_capacity(oids.len());
598
599 for chunk in oids.chunks(max_per_request) {
600 self.send_batch_with_bisect(chunk, op_name, op, &mut all_results)
601 .await?;
602 }
603
604 Ok(all_results)
605 }
606
607 fn send_batch_with_bisect<'a>(
613 &'a self,
614 oids: &'a [Oid],
615 op_name: &'static str,
616 op: fn(i32, &[Oid]) -> Pdu,
617 results: &'a mut Vec<VarBind>,
618 ) -> Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
619 Box::pin(async move {
620 let request_id = self.next_request_id();
621 let pdu = op(request_id, oids);
622 match self.send_request(pdu).await {
623 Ok(response) => {
624 if response.varbinds.len() > oids.len() {
625 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");
626 return Err(Error::MalformedResponse {
627 target: self.peer_addr(),
628 }
629 .boxed());
630 } else if response.varbinds.len() < oids.len() {
631 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");
632 return Err(Error::MalformedResponse {
633 target: self.peer_addr(),
634 }
635 .boxed());
636 }
637 results.extend(response.varbinds);
638 Ok(())
639 }
640 Err(e)
641 if oids.len() > 1
642 && matches!(
643 &*e,
644 Error::Snmp {
645 status: ErrorStatus::TooBig,
646 ..
647 }
648 ) =>
649 {
650 let mid = oids.len() / 2;
651 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");
652 self.send_batch_with_bisect(&oids[..mid], op_name, op, results)
653 .await?;
654 self.send_batch_with_bisect(&oids[mid..], op_name, op, results)
655 .await?;
656 Ok(())
657 }
658 Err(e) => Err(e),
659 }
660 })
661 }
662
663 #[instrument(skip(self, value), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
665 pub async fn set(&self, oid: &Oid, value: Value) -> Result<VarBind> {
666 let request_id = self.next_request_id();
667 let varbind = VarBind::new(oid.clone(), value);
668 let pdu = Pdu::set_request(request_id, vec![varbind]);
669 let response = self.send_request(pdu).await?;
670
671 response.varbinds.into_iter().next().ok_or_else(|| {
672 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty SET response");
673 Error::MalformedResponse {
674 target: self.peer_addr(),
675 }
676 .boxed()
677 })
678 }
679
680 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = varbinds.len()))]
706 pub async fn set_many(&self, varbinds: &[(Oid, Value)]) -> Result<Vec<VarBind>> {
707 if varbinds.is_empty() {
708 return Ok(Vec::new());
709 }
710
711 let max_per_request = self.inner.config.max_oids_per_request;
712
713 if varbinds.len() > max_per_request {
714 return Err(Error::Config(
715 format!(
716 "set_many: {} varbinds exceeds max_oids_per_request ({}); \
717 SET must be atomic and cannot be split across PDUs",
718 varbinds.len(),
719 max_per_request,
720 )
721 .into(),
722 )
723 .boxed());
724 }
725
726 let request_id = self.next_request_id();
727 let vbs: Vec<VarBind> = varbinds
728 .iter()
729 .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
730 .collect();
731 let expected_count = vbs.len();
732 let pdu = Pdu::set_request(request_id, vbs);
733 let response = self.send_request(pdu).await?;
734 if response.varbinds.len() > expected_count {
735 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has more varbinds than requested");
736 return Err(Error::MalformedResponse {
737 target: self.peer_addr(),
738 }
739 .boxed());
740 } else if response.varbinds.len() < expected_count {
741 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has fewer varbinds than requested");
742 }
743 Ok(response.varbinds)
744 }
745
746 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
766 pub async fn send_trap(
767 &self,
768 trap_oid: &Oid,
769 uptime: u32,
770 varbinds: Vec<VarBind>,
771 ) -> Result<()> {
772 if self.inner.config.version == Version::V1 {
773 let local_ip = match self.inner.transport.local_addr().ip() {
776 std::net::IpAddr::V4(v4) => v4.octets(),
777 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
778 };
779 let pdu = Pdu::trap_v2(0, uptime, trap_oid, varbinds);
782 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
783 Error::Config("cannot convert trap to v1 (Counter64 varbind?)".into()).boxed()
784 })?;
785 return self.send_v1_trap(trap).await;
786 }
787
788 let request_id = self.next_request_id();
789 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
790
791 if self.is_v3() {
792 self.ensure_local_keys_derived()?;
793 let msg_id = self.next_request_id();
794 let data = self.build_v3_trap_message(&pdu, msg_id)?;
795 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.varbind_count = pdu.varbinds.len(), snmp.bytes = data.len() }, "sending V3 trap");
796 self.inner.transport.send(&data).await?;
797 } else {
798 let message = CommunityMessage::new(
799 self.inner.config.version,
800 self.inner.config.community.clone(),
801 pdu,
802 );
803 let data = message.encode();
804 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.bytes = data.len() }, "sending v2c trap");
805 self.inner.transport.send(&data).await?;
806 }
807
808 Ok(())
809 }
810
811 #[instrument(skip(self, trap), err, fields(snmp.target = %self.peer_addr(), snmp.generic_trap = %trap.generic_trap))]
841 pub async fn send_v1_trap(&self, trap: TrapV1Pdu) -> Result<()> {
842 if self.inner.config.version != Version::V1 {
843 return Err(Error::Config("send_v1_trap requires a V1 client".into()).boxed());
844 }
845
846 let message = CommunityMessage::v1_trap(self.inner.config.community.clone(), trap);
847 let data = message.encode();
848 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV1", snmp.bytes = data.len() }, "sending v1 trap");
849 self.inner.transport.send(&data).await?;
850
851 Ok(())
852 }
853
854 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
869 pub async fn send_inform(
870 &self,
871 trap_oid: &Oid,
872 uptime: u32,
873 varbinds: Vec<VarBind>,
874 ) -> Result<()> {
875 if self.inner.config.version == Version::V1 {
876 return Err(Error::Config("v1 inform sending not supported".into()).boxed());
877 }
878
879 let request_id = self.next_request_id();
880 let pdu = Pdu::inform_request(request_id, uptime, trap_oid, varbinds);
881 let _response = self.send_request(pdu).await?;
882 Ok(())
883 }
884
885 #[instrument(skip(self, oids), err, fields(
918 snmp.target = %self.peer_addr(),
919 snmp.oid_count = oids.len(),
920 snmp.non_repeaters = non_repeaters,
921 snmp.max_repetitions = max_repetitions
922 ))]
923 pub async fn get_bulk(
924 &self,
925 oids: &[Oid],
926 non_repeaters: i32,
927 max_repetitions: i32,
928 ) -> Result<Vec<VarBind>> {
929 let request_id = self.next_request_id();
930 let pdu = GetBulkPdu::new(request_id, non_repeaters, max_repetitions, oids);
931 let response = self.send_bulk_request(pdu).await?;
932 Ok(response.varbinds)
933 }
934
935 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
961 pub fn walk(&self, oid: Oid) -> Result<WalkStream<T>>
962 where
963 T: 'static,
964 {
965 let ordering = self.inner.config.oid_ordering;
966 let max_results = self.inner.config.max_walk_results;
967 let walk_mode = self.inner.config.walk_mode;
968 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
969 let version = self.inner.config.version;
970
971 WalkStream::new(
972 self.clone(),
973 oid,
974 version,
975 walk_mode,
976 ordering,
977 max_results,
978 max_repetitions,
979 )
980 }
981
982 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
1005 pub fn walk_getnext(&self, oid: Oid) -> Walk<T>
1006 where
1007 T: 'static,
1008 {
1009 let ordering = self.inner.config.oid_ordering;
1010 let max_results = self.inner.config.max_walk_results;
1011 Walk::new(self.clone(), oid, ordering, max_results)
1012 }
1013
1014 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid, snmp.max_repetitions = max_repetitions))]
1040 pub fn bulk_walk(&self, oid: Oid, max_repetitions: i32) -> BulkWalk<T>
1041 where
1042 T: 'static,
1043 {
1044 let ordering = self.inner.config.oid_ordering;
1045 let max_results = self.inner.config.max_walk_results;
1046 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1047 }
1048
1049 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
1067 pub fn bulk_walk_default(&self, oid: Oid) -> BulkWalk<T>
1068 where
1069 T: 'static,
1070 {
1071 let ordering = self.inner.config.oid_ordering;
1072 let max_results = self.inner.config.max_walk_results;
1073 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
1074 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use crate::message::CommunityMessage;
1082 use crate::oid::Oid;
1083 use crate::pdu::{Pdu, PduType};
1084 use crate::varbind::VarBind;
1085 use crate::version::Version;
1086 use bytes::Bytes;
1087 use std::collections::VecDeque;
1088 use std::net::SocketAddr;
1089 use std::sync::{Arc, Mutex};
1090
1091 #[test]
1098 fn test_max_repetitions_to_wire_saturates_above_i32_max() {
1099 assert_eq!(max_repetitions_to_wire(u32::MAX), i32::MAX);
1101 assert_eq!(
1102 max_repetitions_to_wire(i32::MAX as u32 + 1),
1103 i32::MAX,
1104 "one past i32::MAX must saturate, not wrap negative"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_max_repetitions_to_wire_passes_through_in_range_values() {
1110 assert_eq!(max_repetitions_to_wire(0), 0);
1111 assert_eq!(max_repetitions_to_wire(25), 25);
1112 assert_eq!(max_repetitions_to_wire(i32::MAX as u32), i32::MAX);
1113 }
1114
1115 #[derive(Clone)]
1121 struct TruncatingTransport {
1122 response_varbind_count: usize,
1124 pending: Arc<Mutex<VecDeque<i32>>>,
1127 }
1128
1129 impl TruncatingTransport {
1130 fn new(response_varbind_count: usize) -> Self {
1131 Self {
1132 response_varbind_count,
1133 pending: Arc::new(Mutex::new(VecDeque::new())),
1134 }
1135 }
1136 }
1137
1138 impl Transport for TruncatingTransport {
1139 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1140 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1142 {
1143 let mut q = self.pending.lock().unwrap();
1144 q.push_back(request_id);
1145 }
1146 async { Ok(()) }
1147 }
1148
1149 fn recv(
1150 &self,
1151 _request_id: i32,
1152 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1153 let request_id = {
1154 let mut q = self.pending.lock().unwrap();
1155 q.pop_front().unwrap_or(1)
1156 };
1157 let n = self.response_varbind_count;
1158 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1159
1160 async move {
1161 let varbinds: Vec<VarBind> = (0..n)
1163 .map(|i| {
1164 VarBind::new(
1165 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1166 crate::value::Value::Null,
1167 )
1168 })
1169 .collect();
1170
1171 let pdu = Pdu {
1172 pdu_type: PduType::Response,
1173 request_id,
1174 error_status: 0,
1175 error_index: 0,
1176 varbinds,
1177 };
1178
1179 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1180 let encoded = msg.encode();
1181 Ok((encoded, peer))
1182 }
1183 }
1184
1185 fn peer_addr(&self) -> SocketAddr {
1186 "127.0.0.1:161".parse().unwrap()
1187 }
1188
1189 fn local_addr(&self) -> SocketAddr {
1190 "127.0.0.1:0".parse().unwrap()
1191 }
1192
1193 fn is_reliable(&self) -> bool {
1194 true
1195 }
1196 }
1197
1198 fn make_client(response_varbind_count: usize) -> Client<TruncatingTransport> {
1199 let transport = TruncatingTransport::new(response_varbind_count);
1200 let config = ClientConfig {
1201 version: Version::V2c,
1202 max_oids_per_request: 10,
1203 retry: crate::client::retry::Retry::none(),
1204 ..Default::default()
1205 };
1206 Client::new(transport, config)
1207 }
1208
1209 #[tokio::test]
1210 async fn new_clamps_zero_max_oids_per_request() {
1211 let transport = TruncatingTransport::new(3);
1215 let config = ClientConfig {
1216 version: Version::V2c,
1217 max_oids_per_request: 0,
1218 retry: crate::client::retry::Retry::none(),
1219 ..Default::default()
1220 };
1221 let client = Client::new(transport, config);
1222 assert_eq!(
1223 client.inner.config.max_oids_per_request,
1224 DEFAULT_MAX_OIDS_PER_REQUEST
1225 );
1226
1227 let oids = [
1228 Oid::from_slice(&[1, 3, 6, 1, 1]),
1229 Oid::from_slice(&[1, 3, 6, 1, 2]),
1230 Oid::from_slice(&[1, 3, 6, 1, 3]),
1231 ];
1232 let result = client.get_many(&oids).await;
1234 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1235 }
1236
1237 #[tokio::test]
1238 async fn get_many_rejects_truncated_response() {
1239 let client = make_client(1);
1242 let oids = [
1243 Oid::from_slice(&[1, 3, 6, 1, 1]),
1244 Oid::from_slice(&[1, 3, 6, 1, 2]),
1245 Oid::from_slice(&[1, 3, 6, 1, 3]),
1246 ];
1247
1248 let err = client.get_many(&oids).await.unwrap_err();
1249 assert!(
1250 matches!(*err, Error::MalformedResponse { .. }),
1251 "expected MalformedResponse, got: {err}"
1252 );
1253 }
1254
1255 #[tokio::test]
1256 async fn get_many_rejects_inflated_response() {
1257 let client = make_client(5);
1259 let oids = [
1260 Oid::from_slice(&[1, 3, 6, 1, 1]),
1261 Oid::from_slice(&[1, 3, 6, 1, 2]),
1262 Oid::from_slice(&[1, 3, 6, 1, 3]),
1263 ];
1264
1265 let err = client.get_many(&oids).await.unwrap_err();
1266 assert!(
1267 matches!(*err, Error::MalformedResponse { .. }),
1268 "expected MalformedResponse, got: {err}"
1269 );
1270 }
1271
1272 #[tokio::test]
1273 async fn get_many_accepts_correct_response_count() {
1274 let client = make_client(3);
1276 let oids = [
1277 Oid::from_slice(&[1, 3, 6, 1, 1]),
1278 Oid::from_slice(&[1, 3, 6, 1, 2]),
1279 Oid::from_slice(&[1, 3, 6, 1, 3]),
1280 ];
1281
1282 let result = client.get_many(&oids).await;
1283 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1284 assert_eq!(result.unwrap().len(), 3);
1285 }
1286
1287 #[tokio::test]
1288 async fn get_next_many_rejects_truncated_response() {
1289 let client = make_client(1);
1292 let oids = [
1293 Oid::from_slice(&[1, 3, 6, 1, 1]),
1294 Oid::from_slice(&[1, 3, 6, 1, 2]),
1295 Oid::from_slice(&[1, 3, 6, 1, 3]),
1296 ];
1297
1298 let err = client.get_next_many(&oids).await.unwrap_err();
1299 assert!(
1300 matches!(*err, Error::MalformedResponse { .. }),
1301 "expected MalformedResponse, got: {err}"
1302 );
1303 }
1304
1305 #[tokio::test]
1306 async fn get_next_many_rejects_inflated_response() {
1307 let client = make_client(5);
1309 let oids = [
1310 Oid::from_slice(&[1, 3, 6, 1, 1]),
1311 Oid::from_slice(&[1, 3, 6, 1, 2]),
1312 Oid::from_slice(&[1, 3, 6, 1, 3]),
1313 ];
1314
1315 let err = client.get_next_many(&oids).await.unwrap_err();
1316 assert!(
1317 matches!(*err, Error::MalformedResponse { .. }),
1318 "expected MalformedResponse, got: {err}"
1319 );
1320 }
1321
1322 #[tokio::test]
1323 async fn get_next_many_accepts_correct_response_count() {
1324 let client = make_client(3);
1326 let oids = [
1327 Oid::from_slice(&[1, 3, 6, 1, 1]),
1328 Oid::from_slice(&[1, 3, 6, 1, 2]),
1329 Oid::from_slice(&[1, 3, 6, 1, 3]),
1330 ];
1331
1332 let result = client.get_next_many(&oids).await;
1333 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1334 assert_eq!(result.unwrap().len(), 3);
1335 }
1336
1337 #[tokio::test]
1338 async fn set_many_warns_on_truncated_response() {
1339 let client = make_client(1);
1341 let varbinds = [
1342 (
1343 Oid::from_slice(&[1, 3, 6, 1, 1]),
1344 crate::value::Value::Integer(1),
1345 ),
1346 (
1347 Oid::from_slice(&[1, 3, 6, 1, 2]),
1348 crate::value::Value::Integer(2),
1349 ),
1350 (
1351 Oid::from_slice(&[1, 3, 6, 1, 3]),
1352 crate::value::Value::Integer(3),
1353 ),
1354 ];
1355
1356 let result = client.set_many(&varbinds).await;
1357 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1358 assert_eq!(result.unwrap().len(), 1);
1359 }
1360
1361 #[tokio::test]
1362 async fn set_many_rejects_inflated_response() {
1363 let client = make_client(5);
1365 let varbinds = [
1366 (
1367 Oid::from_slice(&[1, 3, 6, 1, 1]),
1368 crate::value::Value::Integer(1),
1369 ),
1370 (
1371 Oid::from_slice(&[1, 3, 6, 1, 2]),
1372 crate::value::Value::Integer(2),
1373 ),
1374 (
1375 Oid::from_slice(&[1, 3, 6, 1, 3]),
1376 crate::value::Value::Integer(3),
1377 ),
1378 ];
1379
1380 let err = client.set_many(&varbinds).await.unwrap_err();
1381 assert!(
1382 matches!(*err, Error::MalformedResponse { .. }),
1383 "expected MalformedResponse, got: {err}"
1384 );
1385 }
1386
1387 #[tokio::test]
1388 async fn set_many_accepts_correct_response_count() {
1389 let client = make_client(3);
1391 let varbinds = [
1392 (
1393 Oid::from_slice(&[1, 3, 6, 1, 1]),
1394 crate::value::Value::Integer(1),
1395 ),
1396 (
1397 Oid::from_slice(&[1, 3, 6, 1, 2]),
1398 crate::value::Value::Integer(2),
1399 ),
1400 (
1401 Oid::from_slice(&[1, 3, 6, 1, 3]),
1402 crate::value::Value::Integer(3),
1403 ),
1404 ];
1405
1406 let result = client.set_many(&varbinds).await;
1407 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1408 assert_eq!(result.unwrap().len(), 3);
1409 }
1410
1411 #[derive(Clone)]
1416 struct TooBigTransport {
1417 max_varbinds: usize,
1419 pending: Arc<Mutex<VecDeque<(i32, usize)>>>,
1420 }
1421
1422 impl TooBigTransport {
1423 fn new(max_varbinds: usize) -> Self {
1424 Self {
1425 max_varbinds,
1426 pending: Arc::new(Mutex::new(VecDeque::new())),
1427 }
1428 }
1429 }
1430
1431 impl Transport for TooBigTransport {
1432 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1433 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1434 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
1436 let varbind_count = msg.pdu.standard().unwrap().varbinds.len();
1437 {
1438 let mut q = self.pending.lock().unwrap();
1439 q.push_back((request_id, varbind_count));
1440 }
1441 async { Ok(()) }
1442 }
1443
1444 fn recv(
1445 &self,
1446 _request_id: i32,
1447 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1448 let (request_id, varbind_count) = {
1449 let mut q = self.pending.lock().unwrap();
1450 q.pop_front().unwrap_or((1, 0))
1451 };
1452 let max = self.max_varbinds;
1453 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1454
1455 async move {
1456 let pdu = if varbind_count > max {
1457 Pdu {
1459 pdu_type: PduType::Response,
1460 request_id,
1461 error_status: ErrorStatus::TooBig.as_i32(),
1462 error_index: 0,
1463 varbinds: vec![],
1464 }
1465 } else {
1466 let varbinds: Vec<VarBind> = (0..varbind_count)
1468 .map(|i| {
1469 VarBind::new(
1470 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1471 crate::value::Value::Integer(i as i32),
1472 )
1473 })
1474 .collect();
1475 Pdu {
1476 pdu_type: PduType::Response,
1477 request_id,
1478 error_status: 0,
1479 error_index: 0,
1480 varbinds,
1481 }
1482 };
1483
1484 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1485 Ok((msg.encode(), peer))
1486 }
1487 }
1488
1489 fn peer_addr(&self) -> SocketAddr {
1490 "127.0.0.1:161".parse().unwrap()
1491 }
1492
1493 fn local_addr(&self) -> SocketAddr {
1494 "127.0.0.1:0".parse().unwrap()
1495 }
1496
1497 fn is_reliable(&self) -> bool {
1498 true
1499 }
1500 }
1501
1502 #[tokio::test]
1503 async fn get_many_bisects_on_too_big() {
1504 let transport = TooBigTransport::new(3);
1509 let config = ClientConfig {
1510 version: Version::V2c,
1511 max_oids_per_request: 10,
1512 retry: crate::client::retry::Retry::none(),
1513 ..Default::default()
1514 };
1515 let client = Client::new(transport, config);
1516
1517 let oids: Vec<Oid> = (0..8u32)
1518 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1519 .collect();
1520
1521 let result = client.get_many(&oids).await.unwrap();
1522 assert_eq!(result.len(), 8);
1523 }
1524
1525 #[tokio::test]
1526 async fn get_many_single_oid_too_big_is_unrecoverable() {
1527 let transport = TooBigTransport::new(0);
1529 let config = ClientConfig {
1530 version: Version::V2c,
1531 max_oids_per_request: 10,
1532 retry: crate::client::retry::Retry::none(),
1533 ..Default::default()
1534 };
1535 let client = Client::new(transport, config);
1536
1537 let oids = [Oid::from_slice(&[1, 3, 6, 1, 1])];
1538 let err = client.get_many(&oids).await.unwrap_err();
1539 assert!(
1540 matches!(
1541 &*err,
1542 Error::Snmp {
1543 status: ErrorStatus::TooBig,
1544 ..
1545 }
1546 ),
1547 "expected TooBig, got: {err}"
1548 );
1549 }
1550
1551 #[tokio::test]
1552 async fn get_next_many_bisects_on_too_big() {
1553 let transport = TooBigTransport::new(3);
1555 let config = ClientConfig {
1556 version: Version::V2c,
1557 max_oids_per_request: 10,
1558 retry: crate::client::retry::Retry::none(),
1559 ..Default::default()
1560 };
1561 let client = Client::new(transport, config);
1562
1563 let oids: Vec<Oid> = (0..8u32)
1564 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1565 .collect();
1566
1567 let result = client.get_next_many(&oids).await.unwrap();
1568 assert_eq!(result.len(), 8);
1569 }
1570
1571 #[tokio::test]
1573 async fn get_many_batched_rejects_truncated_response() {
1574 let transport = TruncatingTransport::new(1);
1578 let config = ClientConfig {
1579 version: Version::V2c,
1580 max_oids_per_request: 10,
1581 retry: crate::client::retry::Retry::none(),
1582 ..Default::default()
1583 };
1584 let client = Client::new(transport, config);
1585
1586 let oids: Vec<Oid> = (0..12u32)
1587 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1588 .collect();
1589
1590 let err = client.get_many(&oids).await.unwrap_err();
1591 assert!(
1592 matches!(*err, Error::MalformedResponse { .. }),
1593 "expected MalformedResponse, got: {err}"
1594 );
1595 }
1596
1597 #[tokio::test]
1598 async fn get_many_batched_rejects_inflated_response() {
1599 let transport = TruncatingTransport::new(12);
1601 let config = ClientConfig {
1602 version: Version::V2c,
1603 max_oids_per_request: 10,
1604 retry: crate::client::retry::Retry::none(),
1605 ..Default::default()
1606 };
1607 let client = Client::new(transport, config);
1608
1609 let oids: Vec<Oid> = (0..12u32)
1610 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1611 .collect();
1612
1613 let err = client.get_many(&oids).await.unwrap_err();
1614 assert!(
1615 matches!(*err, Error::MalformedResponse { .. }),
1616 "expected MalformedResponse, got: {err}"
1617 );
1618 }
1619
1620 #[derive(Clone)]
1626 struct AdversarialTransport {
1627 pdu_type: PduType,
1628 community: &'static [u8],
1629 respond_as_v1: bool,
1630 pending: Arc<Mutex<VecDeque<i32>>>,
1631 }
1632
1633 impl AdversarialTransport {
1634 fn new(pdu_type: PduType, community: &'static [u8], respond_as_v1: bool) -> Self {
1635 Self {
1636 pdu_type,
1637 community,
1638 respond_as_v1,
1639 pending: Arc::new(Mutex::new(VecDeque::new())),
1640 }
1641 }
1642 }
1643
1644 impl Transport for AdversarialTransport {
1645 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1646 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1647 self.pending.lock().unwrap().push_back(request_id);
1648 async { Ok(()) }
1649 }
1650
1651 fn recv(
1652 &self,
1653 _request_id: i32,
1654 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1655 let request_id = self.pending.lock().unwrap().pop_front().unwrap_or(1);
1656 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1657 let pdu = Pdu {
1658 pdu_type: self.pdu_type,
1659 request_id,
1660 error_status: 0,
1661 error_index: 0,
1662 varbinds: vec![VarBind::new(
1663 Oid::from_slice(&[1, 3, 6, 1, 1]),
1664 crate::value::Value::Null,
1665 )],
1666 };
1667 let community = Bytes::from_static(self.community);
1668 let msg = if self.respond_as_v1 {
1669 CommunityMessage::v1(community, pdu)
1670 } else {
1671 CommunityMessage::v2c(community, pdu)
1672 };
1673 let encoded = msg.encode();
1674 async move { Ok((encoded, peer)) }
1675 }
1676
1677 fn peer_addr(&self) -> SocketAddr {
1678 "127.0.0.1:161".parse().unwrap()
1679 }
1680
1681 fn local_addr(&self) -> SocketAddr {
1682 "127.0.0.1:0".parse().unwrap()
1683 }
1684
1685 fn is_reliable(&self) -> bool {
1686 true
1687 }
1688 }
1689
1690 fn adversarial_client(
1691 pdu_type: PduType,
1692 community: &'static [u8],
1693 respond_as_v1: bool,
1694 ) -> Client<AdversarialTransport> {
1695 let transport = AdversarialTransport::new(pdu_type, community, respond_as_v1);
1696 let config = ClientConfig {
1697 version: Version::V2c,
1698 retry: crate::client::retry::Retry::none(),
1699 ..Default::default()
1700 };
1701 Client::new(transport, config)
1702 }
1703
1704 #[tokio::test]
1707 async fn response_validation_accepts_well_formed_response() {
1708 let client = adversarial_client(PduType::Response, b"public", false);
1709 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1710 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1711 }
1712
1713 #[tokio::test]
1716 async fn response_validation_rejects_echoed_request_pdu() {
1717 let client = adversarial_client(PduType::GetRequest, b"public", false);
1718 let err = client
1719 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1720 .await
1721 .unwrap_err();
1722 assert!(
1723 matches!(*err, Error::MalformedResponse { .. }),
1724 "expected MalformedResponse, got: {err}"
1725 );
1726 }
1727
1728 #[tokio::test]
1730 async fn response_validation_accepts_community_mismatch() {
1731 let client = adversarial_client(PduType::Response, b"other", false);
1732 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1733 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1734 }
1735
1736 #[tokio::test]
1738 async fn response_validation_rejects_version_mismatch() {
1739 let client = adversarial_client(PduType::Response, b"public", true);
1740 let err = client
1741 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1742 .await
1743 .unwrap_err();
1744 assert!(
1745 matches!(*err, Error::MalformedResponse { .. }),
1746 "expected MalformedResponse, got: {err}"
1747 );
1748 }
1749}