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<T: Transport> Client<T> {
219 pub fn new(transport: T, config: ClientConfig) -> Self {
226 Self {
227 inner: Arc::new(ClientInner {
228 transport,
229 config,
230 engine_state: RwLock::new(None),
231 derived_keys: RwLock::new(None),
232 salt_counter: SaltCounter::new(),
233 engine_cache: None,
234 discovery_lock: AsyncMutex::new(()),
235 local_engine_start: Instant::now(),
236 local_derived_keys: RwLock::new(None),
237 }),
238 }
239 }
240
241 pub fn with_engine_cache(
243 transport: T,
244 config: ClientConfig,
245 engine_cache: Arc<EngineCache>,
246 ) -> Self {
247 Self {
248 inner: Arc::new(ClientInner {
249 transport,
250 config,
251 engine_state: RwLock::new(None),
252 derived_keys: RwLock::new(None),
253 salt_counter: SaltCounter::new(),
254 engine_cache: Some(engine_cache),
255 discovery_lock: AsyncMutex::new(()),
256 local_engine_start: Instant::now(),
257 local_derived_keys: RwLock::new(None),
258 }),
259 }
260 }
261
262 #[must_use]
267 pub fn peer_addr(&self) -> SocketAddr {
268 self.inner.transport.peer_addr()
269 }
270
271 fn next_request_id(&self) -> i32 {
275 self.inner.transport.alloc_request_id()
276 }
277
278 fn is_v3(&self) -> bool {
280 self.inner.config.version == Version::V3 && self.inner.config.v3_security.is_some()
281 }
282
283 #[instrument(
285 level = "debug",
286 skip(self, data),
287 fields(
288 snmp.target = %self.peer_addr(),
289 snmp.request_id = request_id,
290 snmp.attempt = tracing::field::Empty,
291 snmp.elapsed_ms = tracing::field::Empty,
292 )
293 )]
294 async fn send_and_recv(&self, request_id: i32, data: &[u8]) -> Result<Pdu> {
295 let start = Instant::now();
296 let mut last_error: Option<Box<Error>> = None;
297 let max_attempts = if self.inner.transport.is_reliable() {
298 0
299 } else {
300 self.inner.config.retry.max_attempts
301 };
302
303 for attempt in 0..=max_attempts {
304 Span::current().record("snmp.attempt", attempt);
305 if attempt > 0 {
306 tracing::debug!(target: "async_snmp::client", "retrying request");
307 }
308
309 self.inner
311 .transport
312 .register_request(request_id, self.inner.config.timeout);
313
314 tracing::trace!(target: "async_snmp::client", { snmp.bytes = data.len() }, "sending request");
316 self.inner.transport.send(data).await?;
317
318 match self.inner.transport.recv(request_id).await {
320 Ok((response_data, _source)) => {
321 tracing::trace!(target: "async_snmp::client", { snmp.bytes = response_data.len() }, "received response");
322
323 let response = Message::decode(response_data)?;
325
326 let response_version = response.version();
328 let expected_version = self.inner.config.version;
329 if response_version != expected_version {
330 tracing::warn!(target: "async_snmp::client", { ?expected_version, ?response_version, peer = %self.peer_addr() }, "version mismatch in response");
331 return Err(Error::MalformedResponse {
332 target: self.peer_addr(),
333 }
334 .boxed());
335 }
336
337 if let Message::Community(ref m) = response
341 && m.community != self.inner.config.community
342 {
343 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "community mismatch in response");
344 }
345
346 let Some(response_pdu) = response.into_pdu() else {
347 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr() }, "received TrapV1 in response to request");
348 return Err(Error::MalformedResponse {
349 target: self.peer_addr(),
350 }
351 .boxed());
352 };
353
354 if response_pdu.pdu_type != PduType::Response {
357 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), pdu_type = ?response_pdu.pdu_type }, "non-Response PDU in response");
358 return Err(Error::MalformedResponse {
359 target: self.peer_addr(),
360 }
361 .boxed());
362 }
363
364 if response_pdu.request_id != request_id {
366 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");
367 return Err(Error::MalformedResponse {
368 target: self.peer_addr(),
369 }
370 .boxed());
371 }
372
373 if let Some(err) = pdu_to_snmp_error(&response_pdu, self.peer_addr()) {
375 Span::current()
376 .record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
377 return Err(err);
378 }
379
380 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
381 return Ok(response_pdu);
382 }
383 Err(e) if matches!(*e, Error::Timeout { .. }) => {
384 last_error = Some(e);
385 if attempt < max_attempts {
387 let delay = self.inner.config.retry.compute_delay(attempt);
388 if !delay.is_zero() {
389 tracing::debug!(target: "async_snmp::client", { delay_ms = delay.as_millis() as u64 }, "backing off");
390 tokio::time::sleep(delay).await;
391 }
392 }
393 }
395 Err(e) => {
396 Span::current().record("snmp.elapsed_ms", start.elapsed().as_millis() as u64);
397 return Err(e);
398 }
399 }
400 }
401
402 let elapsed = start.elapsed();
404 Span::current().record("snmp.elapsed_ms", elapsed.as_millis() as u64);
405 tracing::debug!(target: "async_snmp::client", { request_id, peer = %self.peer_addr(), ?elapsed, retries = max_attempts }, "request timed out");
406 Err(last_error.unwrap_or_else(|| {
407 Error::Timeout {
408 target: self.peer_addr(),
409 elapsed,
410 retries: max_attempts,
411 }
412 .boxed()
413 }))
414 }
415
416 async fn send_request(&self, pdu: Pdu) -> Result<Pdu> {
418 if self.is_v3() {
420 return self.send_v3_and_recv(pdu).await;
421 }
422
423 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = ?pdu.pdu_type, snmp.varbind_count = pdu.varbinds.len() }, "sending {} request", pdu.pdu_type);
424
425 let request_id = pdu.request_id;
426 let message = CommunityMessage::new(
427 self.inner.config.version,
428 self.inner.config.community.clone(),
429 pdu,
430 );
431 let data = message.encode();
432 let response = self.send_and_recv(request_id, &data).await?;
433
434 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);
435
436 Ok(response)
437 }
438
439 async fn send_bulk_request(&self, pdu: GetBulkPdu) -> Result<Pdu> {
441 if self.is_v3() {
443 let pdu = Pdu::get_bulk(
445 pdu.request_id,
446 pdu.non_repeaters,
447 pdu.max_repetitions,
448 pdu.varbinds,
449 );
450 return self.send_v3_and_recv(pdu).await;
451 }
452
453 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");
454
455 let request_id = pdu.request_id;
456 let data = CommunityMessage::encode_bulk(
457 self.inner.config.version,
458 self.inner.config.community.clone(),
459 &pdu,
460 );
461 let response = self.send_and_recv(request_id, &data).await?;
462
463 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);
464
465 Ok(response)
466 }
467
468 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
470 pub async fn get(&self, oid: &Oid) -> Result<VarBind> {
471 let request_id = self.next_request_id();
472 let pdu = Pdu::get_request(request_id, std::slice::from_ref(oid));
473 let response = self.send_request(pdu).await?;
474
475 response.varbinds.into_iter().next().ok_or_else(|| {
476 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GET response");
477 Error::MalformedResponse {
478 target: self.peer_addr(),
479 }
480 .boxed()
481 })
482 }
483
484 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
505 pub async fn get_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
506 self.get_or_getnext_many(oids, "GET", Pdu::get_request)
507 .await
508 }
509
510 #[instrument(skip(self), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
512 pub async fn get_next(&self, oid: &Oid) -> Result<VarBind> {
513 let request_id = self.next_request_id();
514 let pdu = Pdu::get_next_request(request_id, std::slice::from_ref(oid));
515 let response = self.send_request(pdu).await?;
516
517 response.varbinds.into_iter().next().ok_or_else(|| {
518 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty GETNEXT response");
519 Error::MalformedResponse {
520 target: self.peer_addr(),
521 }
522 .boxed()
523 })
524 }
525
526 #[instrument(skip(self, oids), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = oids.len()))]
546 pub async fn get_next_many(&self, oids: &[Oid]) -> Result<Vec<VarBind>> {
547 self.get_or_getnext_many(oids, "GETNEXT", Pdu::get_next_request)
548 .await
549 }
550
551 async fn get_or_getnext_many(
556 &self,
557 oids: &[Oid],
558 op_name: &'static str,
559 op: fn(i32, &[Oid]) -> Pdu,
560 ) -> Result<Vec<VarBind>> {
561 if oids.is_empty() {
562 return Ok(Vec::new());
563 }
564
565 let max_per_request = self.inner.config.max_oids_per_request;
566 let mut all_results = Vec::with_capacity(oids.len());
567
568 for chunk in oids.chunks(max_per_request) {
569 self.send_batch_with_bisect(chunk, op_name, op, &mut all_results)
570 .await?;
571 }
572
573 Ok(all_results)
574 }
575
576 fn send_batch_with_bisect<'a>(
582 &'a self,
583 oids: &'a [Oid],
584 op_name: &'static str,
585 op: fn(i32, &[Oid]) -> Pdu,
586 results: &'a mut Vec<VarBind>,
587 ) -> Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
588 Box::pin(async move {
589 let request_id = self.next_request_id();
590 let pdu = op(request_id, oids);
591 match self.send_request(pdu).await {
592 Ok(response) => {
593 if response.varbinds.len() > oids.len() {
594 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");
595 return Err(Error::MalformedResponse {
596 target: self.peer_addr(),
597 }
598 .boxed());
599 } else if response.varbinds.len() < oids.len() {
600 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");
601 }
602 results.extend(response.varbinds);
603 Ok(())
604 }
605 Err(e)
606 if oids.len() > 1
607 && matches!(
608 &*e,
609 Error::Snmp {
610 status: ErrorStatus::TooBig,
611 ..
612 }
613 ) =>
614 {
615 let mid = oids.len() / 2;
616 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");
617 self.send_batch_with_bisect(&oids[..mid], op_name, op, results)
618 .await?;
619 self.send_batch_with_bisect(&oids[mid..], op_name, op, results)
620 .await?;
621 Ok(())
622 }
623 Err(e) => Err(e),
624 }
625 })
626 }
627
628 #[instrument(skip(self, value), err, fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
630 pub async fn set(&self, oid: &Oid, value: Value) -> Result<VarBind> {
631 let request_id = self.next_request_id();
632 let varbind = VarBind::new(oid.clone(), value);
633 let pdu = Pdu::set_request(request_id, vec![varbind]);
634 let response = self.send_request(pdu).await?;
635
636 response.varbinds.into_iter().next().ok_or_else(|| {
637 tracing::debug!(target: "async_snmp::client", { peer = %self.peer_addr(), kind = %DecodeErrorKind::EmptyResponse }, "empty SET response");
638 Error::MalformedResponse {
639 target: self.peer_addr(),
640 }
641 .boxed()
642 })
643 }
644
645 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.oid_count = varbinds.len()))]
671 pub async fn set_many(&self, varbinds: &[(Oid, Value)]) -> Result<Vec<VarBind>> {
672 if varbinds.is_empty() {
673 return Ok(Vec::new());
674 }
675
676 let max_per_request = self.inner.config.max_oids_per_request;
677
678 if varbinds.len() > max_per_request {
679 return Err(Error::Config(
680 format!(
681 "set_many: {} varbinds exceeds max_oids_per_request ({}); \
682 SET must be atomic and cannot be split across PDUs",
683 varbinds.len(),
684 max_per_request,
685 )
686 .into(),
687 )
688 .boxed());
689 }
690
691 let request_id = self.next_request_id();
692 let vbs: Vec<VarBind> = varbinds
693 .iter()
694 .map(|(oid, value)| VarBind::new(oid.clone(), value.clone()))
695 .collect();
696 let expected_count = vbs.len();
697 let pdu = Pdu::set_request(request_id, vbs);
698 let response = self.send_request(pdu).await?;
699 if response.varbinds.len() > expected_count {
700 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has more varbinds than requested");
701 return Err(Error::MalformedResponse {
702 target: self.peer_addr(),
703 }
704 .boxed());
705 } else if response.varbinds.len() < expected_count {
706 tracing::warn!(target: "async_snmp::client", { peer = %self.peer_addr(), expected = expected_count, actual = response.varbinds.len() }, "SET response has fewer varbinds than requested");
707 }
708 Ok(response.varbinds)
709 }
710
711 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
730 pub async fn send_trap(
731 &self,
732 trap_oid: &Oid,
733 uptime: u32,
734 varbinds: Vec<VarBind>,
735 ) -> Result<()> {
736 if self.inner.config.version == Version::V1 {
737 let local_ip = match self.inner.transport.local_addr().ip() {
740 std::net::IpAddr::V4(v4) => v4.octets(),
741 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
742 };
743 let pdu = Pdu::trap_v2(0, uptime, trap_oid, varbinds);
746 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
747 Error::Config("cannot convert trap to v1 (Counter64 varbind?)".into()).boxed()
748 })?;
749 return self.send_v1_trap(trap).await;
750 }
751
752 let request_id = self.next_request_id();
753 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
754
755 if self.is_v3() {
756 self.ensure_local_keys_derived()?;
757 let msg_id = self.next_request_id();
758 let data = self.build_v3_trap_message(&pdu, msg_id)?;
759 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.varbind_count = pdu.varbinds.len(), snmp.bytes = data.len() }, "sending V3 trap");
760 self.inner.transport.send(&data).await?;
761 } else {
762 let message = CommunityMessage::new(
763 self.inner.config.version,
764 self.inner.config.community.clone(),
765 pdu,
766 );
767 let data = message.encode();
768 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV2", snmp.bytes = data.len() }, "sending v2c trap");
769 self.inner.transport.send(&data).await?;
770 }
771
772 Ok(())
773 }
774
775 #[instrument(skip(self, trap), err, fields(snmp.target = %self.peer_addr(), snmp.generic_trap = %trap.generic_trap))]
805 pub async fn send_v1_trap(&self, trap: TrapV1Pdu) -> Result<()> {
806 if self.inner.config.version != Version::V1 {
807 return Err(Error::Config("send_v1_trap requires a V1 client".into()).boxed());
808 }
809
810 let message = CommunityMessage::v1_trap(self.inner.config.community.clone(), trap);
811 let data = message.encode();
812 tracing::debug!(target: "async_snmp::client", { snmp.pdu_type = "TrapV1", snmp.bytes = data.len() }, "sending v1 trap");
813 self.inner.transport.send(&data).await?;
814
815 Ok(())
816 }
817
818 #[instrument(skip(self, varbinds), err, fields(snmp.target = %self.peer_addr(), snmp.trap_oid = %trap_oid))]
833 pub async fn send_inform(
834 &self,
835 trap_oid: &Oid,
836 uptime: u32,
837 varbinds: Vec<VarBind>,
838 ) -> Result<()> {
839 if self.inner.config.version == Version::V1 {
840 return Err(Error::Config("v1 inform sending not supported".into()).boxed());
841 }
842
843 let request_id = self.next_request_id();
844 let pdu = Pdu::inform_request(request_id, uptime, trap_oid, varbinds);
845 let _response = self.send_request(pdu).await?;
846 Ok(())
847 }
848
849 #[instrument(skip(self, oids), err, fields(
882 snmp.target = %self.peer_addr(),
883 snmp.oid_count = oids.len(),
884 snmp.non_repeaters = non_repeaters,
885 snmp.max_repetitions = max_repetitions
886 ))]
887 pub async fn get_bulk(
888 &self,
889 oids: &[Oid],
890 non_repeaters: i32,
891 max_repetitions: i32,
892 ) -> Result<Vec<VarBind>> {
893 let request_id = self.next_request_id();
894 let pdu = GetBulkPdu::new(request_id, non_repeaters, max_repetitions, oids);
895 let response = self.send_bulk_request(pdu).await?;
896 Ok(response.varbinds)
897 }
898
899 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
925 pub fn walk(&self, oid: Oid) -> Result<WalkStream<T>>
926 where
927 T: 'static,
928 {
929 let ordering = self.inner.config.oid_ordering;
930 let max_results = self.inner.config.max_walk_results;
931 let walk_mode = self.inner.config.walk_mode;
932 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
933 let version = self.inner.config.version;
934
935 WalkStream::new(
936 self.clone(),
937 oid,
938 version,
939 walk_mode,
940 ordering,
941 max_results,
942 max_repetitions,
943 )
944 }
945
946 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
969 pub fn walk_getnext(&self, oid: Oid) -> Walk<T>
970 where
971 T: 'static,
972 {
973 let ordering = self.inner.config.oid_ordering;
974 let max_results = self.inner.config.max_walk_results;
975 Walk::new(self.clone(), oid, ordering, max_results)
976 }
977
978 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid, snmp.max_repetitions = max_repetitions))]
1004 pub fn bulk_walk(&self, oid: Oid, max_repetitions: i32) -> BulkWalk<T>
1005 where
1006 T: 'static,
1007 {
1008 let ordering = self.inner.config.oid_ordering;
1009 let max_results = self.inner.config.max_walk_results;
1010 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1011 }
1012
1013 #[instrument(skip(self), fields(snmp.target = %self.peer_addr(), snmp.oid = %oid))]
1031 pub fn bulk_walk_default(&self, oid: Oid) -> BulkWalk<T>
1032 where
1033 T: 'static,
1034 {
1035 let ordering = self.inner.config.oid_ordering;
1036 let max_results = self.inner.config.max_walk_results;
1037 let max_repetitions = max_repetitions_to_wire(self.inner.config.max_repetitions);
1038 BulkWalk::new(self.clone(), oid, max_repetitions, ordering, max_results)
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045 use crate::message::CommunityMessage;
1046 use crate::oid::Oid;
1047 use crate::pdu::{Pdu, PduType};
1048 use crate::varbind::VarBind;
1049 use crate::version::Version;
1050 use bytes::Bytes;
1051 use std::collections::VecDeque;
1052 use std::net::SocketAddr;
1053 use std::sync::{Arc, Mutex};
1054
1055 #[test]
1062 fn test_max_repetitions_to_wire_saturates_above_i32_max() {
1063 assert_eq!(max_repetitions_to_wire(u32::MAX), i32::MAX);
1065 assert_eq!(
1066 max_repetitions_to_wire(i32::MAX as u32 + 1),
1067 i32::MAX,
1068 "one past i32::MAX must saturate, not wrap negative"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_max_repetitions_to_wire_passes_through_in_range_values() {
1074 assert_eq!(max_repetitions_to_wire(0), 0);
1075 assert_eq!(max_repetitions_to_wire(25), 25);
1076 assert_eq!(max_repetitions_to_wire(i32::MAX as u32), i32::MAX);
1077 }
1078
1079 #[derive(Clone)]
1085 struct TruncatingTransport {
1086 response_varbind_count: usize,
1088 pending: Arc<Mutex<VecDeque<i32>>>,
1091 }
1092
1093 impl TruncatingTransport {
1094 fn new(response_varbind_count: usize) -> Self {
1095 Self {
1096 response_varbind_count,
1097 pending: Arc::new(Mutex::new(VecDeque::new())),
1098 }
1099 }
1100 }
1101
1102 impl Transport for TruncatingTransport {
1103 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1104 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1106 {
1107 let mut q = self.pending.lock().unwrap();
1108 q.push_back(request_id);
1109 }
1110 async { Ok(()) }
1111 }
1112
1113 fn recv(
1114 &self,
1115 _request_id: i32,
1116 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1117 let request_id = {
1118 let mut q = self.pending.lock().unwrap();
1119 q.pop_front().unwrap_or(1)
1120 };
1121 let n = self.response_varbind_count;
1122 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1123
1124 async move {
1125 let varbinds: Vec<VarBind> = (0..n)
1127 .map(|i| {
1128 VarBind::new(
1129 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1130 crate::value::Value::Null,
1131 )
1132 })
1133 .collect();
1134
1135 let pdu = Pdu {
1136 pdu_type: PduType::Response,
1137 request_id,
1138 error_status: 0,
1139 error_index: 0,
1140 varbinds,
1141 };
1142
1143 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1144 let encoded = msg.encode();
1145 Ok((encoded, peer))
1146 }
1147 }
1148
1149 fn peer_addr(&self) -> SocketAddr {
1150 "127.0.0.1:161".parse().unwrap()
1151 }
1152
1153 fn local_addr(&self) -> SocketAddr {
1154 "127.0.0.1:0".parse().unwrap()
1155 }
1156
1157 fn is_reliable(&self) -> bool {
1158 true
1159 }
1160 }
1161
1162 fn make_client(response_varbind_count: usize) -> Client<TruncatingTransport> {
1163 let transport = TruncatingTransport::new(response_varbind_count);
1164 let config = ClientConfig {
1165 version: Version::V2c,
1166 max_oids_per_request: 10,
1167 retry: crate::client::retry::Retry::none(),
1168 ..Default::default()
1169 };
1170 Client::new(transport, config)
1171 }
1172
1173 #[tokio::test]
1174 async fn get_many_warns_on_truncated_response() {
1175 let client = make_client(1);
1177 let oids = [
1178 Oid::from_slice(&[1, 3, 6, 1, 1]),
1179 Oid::from_slice(&[1, 3, 6, 1, 2]),
1180 Oid::from_slice(&[1, 3, 6, 1, 3]),
1181 ];
1182
1183 let result = client.get_many(&oids).await;
1184 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1185 assert_eq!(result.unwrap().len(), 1);
1186 }
1187
1188 #[tokio::test]
1189 async fn get_many_rejects_inflated_response() {
1190 let client = make_client(5);
1192 let oids = [
1193 Oid::from_slice(&[1, 3, 6, 1, 1]),
1194 Oid::from_slice(&[1, 3, 6, 1, 2]),
1195 Oid::from_slice(&[1, 3, 6, 1, 3]),
1196 ];
1197
1198 let err = client.get_many(&oids).await.unwrap_err();
1199 assert!(
1200 matches!(*err, Error::MalformedResponse { .. }),
1201 "expected MalformedResponse, got: {err}"
1202 );
1203 }
1204
1205 #[tokio::test]
1206 async fn get_many_accepts_correct_response_count() {
1207 let client = make_client(3);
1209 let oids = [
1210 Oid::from_slice(&[1, 3, 6, 1, 1]),
1211 Oid::from_slice(&[1, 3, 6, 1, 2]),
1212 Oid::from_slice(&[1, 3, 6, 1, 3]),
1213 ];
1214
1215 let result = client.get_many(&oids).await;
1216 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1217 assert_eq!(result.unwrap().len(), 3);
1218 }
1219
1220 #[tokio::test]
1221 async fn get_next_many_warns_on_truncated_response() {
1222 let client = make_client(1);
1224 let oids = [
1225 Oid::from_slice(&[1, 3, 6, 1, 1]),
1226 Oid::from_slice(&[1, 3, 6, 1, 2]),
1227 Oid::from_slice(&[1, 3, 6, 1, 3]),
1228 ];
1229
1230 let result = client.get_next_many(&oids).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 get_next_many_rejects_inflated_response() {
1237 let client = make_client(5);
1239 let oids = [
1240 Oid::from_slice(&[1, 3, 6, 1, 1]),
1241 Oid::from_slice(&[1, 3, 6, 1, 2]),
1242 Oid::from_slice(&[1, 3, 6, 1, 3]),
1243 ];
1244
1245 let err = client.get_next_many(&oids).await.unwrap_err();
1246 assert!(
1247 matches!(*err, Error::MalformedResponse { .. }),
1248 "expected MalformedResponse, got: {err}"
1249 );
1250 }
1251
1252 #[tokio::test]
1253 async fn get_next_many_accepts_correct_response_count() {
1254 let client = make_client(3);
1256 let oids = [
1257 Oid::from_slice(&[1, 3, 6, 1, 1]),
1258 Oid::from_slice(&[1, 3, 6, 1, 2]),
1259 Oid::from_slice(&[1, 3, 6, 1, 3]),
1260 ];
1261
1262 let result = client.get_next_many(&oids).await;
1263 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1264 assert_eq!(result.unwrap().len(), 3);
1265 }
1266
1267 #[tokio::test]
1268 async fn set_many_warns_on_truncated_response() {
1269 let client = make_client(1);
1271 let varbinds = [
1272 (
1273 Oid::from_slice(&[1, 3, 6, 1, 1]),
1274 crate::value::Value::Integer(1),
1275 ),
1276 (
1277 Oid::from_slice(&[1, 3, 6, 1, 2]),
1278 crate::value::Value::Integer(2),
1279 ),
1280 (
1281 Oid::from_slice(&[1, 3, 6, 1, 3]),
1282 crate::value::Value::Integer(3),
1283 ),
1284 ];
1285
1286 let result = client.set_many(&varbinds).await;
1287 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1288 assert_eq!(result.unwrap().len(), 1);
1289 }
1290
1291 #[tokio::test]
1292 async fn set_many_rejects_inflated_response() {
1293 let client = make_client(5);
1295 let varbinds = [
1296 (
1297 Oid::from_slice(&[1, 3, 6, 1, 1]),
1298 crate::value::Value::Integer(1),
1299 ),
1300 (
1301 Oid::from_slice(&[1, 3, 6, 1, 2]),
1302 crate::value::Value::Integer(2),
1303 ),
1304 (
1305 Oid::from_slice(&[1, 3, 6, 1, 3]),
1306 crate::value::Value::Integer(3),
1307 ),
1308 ];
1309
1310 let err = client.set_many(&varbinds).await.unwrap_err();
1311 assert!(
1312 matches!(*err, Error::MalformedResponse { .. }),
1313 "expected MalformedResponse, got: {err}"
1314 );
1315 }
1316
1317 #[tokio::test]
1318 async fn set_many_accepts_correct_response_count() {
1319 let client = make_client(3);
1321 let varbinds = [
1322 (
1323 Oid::from_slice(&[1, 3, 6, 1, 1]),
1324 crate::value::Value::Integer(1),
1325 ),
1326 (
1327 Oid::from_slice(&[1, 3, 6, 1, 2]),
1328 crate::value::Value::Integer(2),
1329 ),
1330 (
1331 Oid::from_slice(&[1, 3, 6, 1, 3]),
1332 crate::value::Value::Integer(3),
1333 ),
1334 ];
1335
1336 let result = client.set_many(&varbinds).await;
1337 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1338 assert_eq!(result.unwrap().len(), 3);
1339 }
1340
1341 #[derive(Clone)]
1346 struct TooBigTransport {
1347 max_varbinds: usize,
1349 pending: Arc<Mutex<VecDeque<(i32, usize)>>>,
1350 }
1351
1352 impl TooBigTransport {
1353 fn new(max_varbinds: usize) -> Self {
1354 Self {
1355 max_varbinds,
1356 pending: Arc::new(Mutex::new(VecDeque::new())),
1357 }
1358 }
1359 }
1360
1361 impl Transport for TooBigTransport {
1362 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1363 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1364 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
1366 let varbind_count = msg.pdu.standard().unwrap().varbinds.len();
1367 {
1368 let mut q = self.pending.lock().unwrap();
1369 q.push_back((request_id, varbind_count));
1370 }
1371 async { Ok(()) }
1372 }
1373
1374 fn recv(
1375 &self,
1376 _request_id: i32,
1377 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1378 let (request_id, varbind_count) = {
1379 let mut q = self.pending.lock().unwrap();
1380 q.pop_front().unwrap_or((1, 0))
1381 };
1382 let max = self.max_varbinds;
1383 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1384
1385 async move {
1386 let pdu = if varbind_count > max {
1387 Pdu {
1389 pdu_type: PduType::Response,
1390 request_id,
1391 error_status: ErrorStatus::TooBig.as_i32(),
1392 error_index: 0,
1393 varbinds: vec![],
1394 }
1395 } else {
1396 let varbinds: Vec<VarBind> = (0..varbind_count)
1398 .map(|i| {
1399 VarBind::new(
1400 Oid::from_slice(&[1, 3, 6, 1, i as u32]),
1401 crate::value::Value::Integer(i as i32),
1402 )
1403 })
1404 .collect();
1405 Pdu {
1406 pdu_type: PduType::Response,
1407 request_id,
1408 error_status: 0,
1409 error_index: 0,
1410 varbinds,
1411 }
1412 };
1413
1414 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1415 Ok((msg.encode(), peer))
1416 }
1417 }
1418
1419 fn peer_addr(&self) -> SocketAddr {
1420 "127.0.0.1:161".parse().unwrap()
1421 }
1422
1423 fn local_addr(&self) -> SocketAddr {
1424 "127.0.0.1:0".parse().unwrap()
1425 }
1426
1427 fn is_reliable(&self) -> bool {
1428 true
1429 }
1430 }
1431
1432 #[tokio::test]
1433 async fn get_many_bisects_on_too_big() {
1434 let transport = TooBigTransport::new(3);
1439 let config = ClientConfig {
1440 version: Version::V2c,
1441 max_oids_per_request: 10,
1442 retry: crate::client::retry::Retry::none(),
1443 ..Default::default()
1444 };
1445 let client = Client::new(transport, config);
1446
1447 let oids: Vec<Oid> = (0..8u32)
1448 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1449 .collect();
1450
1451 let result = client.get_many(&oids).await.unwrap();
1452 assert_eq!(result.len(), 8);
1453 }
1454
1455 #[tokio::test]
1456 async fn get_many_single_oid_too_big_is_unrecoverable() {
1457 let transport = TooBigTransport::new(0);
1459 let config = ClientConfig {
1460 version: Version::V2c,
1461 max_oids_per_request: 10,
1462 retry: crate::client::retry::Retry::none(),
1463 ..Default::default()
1464 };
1465 let client = Client::new(transport, config);
1466
1467 let oids = [Oid::from_slice(&[1, 3, 6, 1, 1])];
1468 let err = client.get_many(&oids).await.unwrap_err();
1469 assert!(
1470 matches!(
1471 &*err,
1472 Error::Snmp {
1473 status: ErrorStatus::TooBig,
1474 ..
1475 }
1476 ),
1477 "expected TooBig, got: {err}"
1478 );
1479 }
1480
1481 #[tokio::test]
1482 async fn get_next_many_bisects_on_too_big() {
1483 let transport = TooBigTransport::new(3);
1485 let config = ClientConfig {
1486 version: Version::V2c,
1487 max_oids_per_request: 10,
1488 retry: crate::client::retry::Retry::none(),
1489 ..Default::default()
1490 };
1491 let client = Client::new(transport, config);
1492
1493 let oids: Vec<Oid> = (0..8u32)
1494 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1495 .collect();
1496
1497 let result = client.get_next_many(&oids).await.unwrap();
1498 assert_eq!(result.len(), 8);
1499 }
1500
1501 #[tokio::test]
1503 async fn get_many_batched_warns_on_truncated_response() {
1504 let transport = TruncatingTransport::new(1);
1507 let config = ClientConfig {
1508 version: Version::V2c,
1509 max_oids_per_request: 10,
1510 retry: crate::client::retry::Retry::none(),
1511 ..Default::default()
1512 };
1513 let client = Client::new(transport, config);
1514
1515 let oids: Vec<Oid> = (0..12u32)
1516 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1517 .collect();
1518
1519 let result = client.get_many(&oids).await;
1520 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1521 assert_eq!(result.unwrap().len(), 2); }
1523
1524 #[tokio::test]
1525 async fn get_many_batched_rejects_inflated_response() {
1526 let transport = TruncatingTransport::new(12);
1528 let config = ClientConfig {
1529 version: Version::V2c,
1530 max_oids_per_request: 10,
1531 retry: crate::client::retry::Retry::none(),
1532 ..Default::default()
1533 };
1534 let client = Client::new(transport, config);
1535
1536 let oids: Vec<Oid> = (0..12u32)
1537 .map(|i| Oid::from_slice(&[1, 3, 6, 1, i]))
1538 .collect();
1539
1540 let err = client.get_many(&oids).await.unwrap_err();
1541 assert!(
1542 matches!(*err, Error::MalformedResponse { .. }),
1543 "expected MalformedResponse, got: {err}"
1544 );
1545 }
1546
1547 #[derive(Clone)]
1553 struct AdversarialTransport {
1554 pdu_type: PduType,
1555 community: &'static [u8],
1556 respond_as_v1: bool,
1557 pending: Arc<Mutex<VecDeque<i32>>>,
1558 }
1559
1560 impl AdversarialTransport {
1561 fn new(pdu_type: PduType, community: &'static [u8], respond_as_v1: bool) -> Self {
1562 Self {
1563 pdu_type,
1564 community,
1565 respond_as_v1,
1566 pending: Arc::new(Mutex::new(VecDeque::new())),
1567 }
1568 }
1569 }
1570
1571 impl Transport for AdversarialTransport {
1572 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
1573 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
1574 self.pending.lock().unwrap().push_back(request_id);
1575 async { Ok(()) }
1576 }
1577
1578 fn recv(
1579 &self,
1580 _request_id: i32,
1581 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
1582 let request_id = self.pending.lock().unwrap().pop_front().unwrap_or(1);
1583 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
1584 let pdu = Pdu {
1585 pdu_type: self.pdu_type,
1586 request_id,
1587 error_status: 0,
1588 error_index: 0,
1589 varbinds: vec![VarBind::new(
1590 Oid::from_slice(&[1, 3, 6, 1, 1]),
1591 crate::value::Value::Null,
1592 )],
1593 };
1594 let community = Bytes::from_static(self.community);
1595 let msg = if self.respond_as_v1 {
1596 CommunityMessage::v1(community, pdu)
1597 } else {
1598 CommunityMessage::v2c(community, pdu)
1599 };
1600 let encoded = msg.encode();
1601 async move { Ok((encoded, peer)) }
1602 }
1603
1604 fn peer_addr(&self) -> SocketAddr {
1605 "127.0.0.1:161".parse().unwrap()
1606 }
1607
1608 fn local_addr(&self) -> SocketAddr {
1609 "127.0.0.1:0".parse().unwrap()
1610 }
1611
1612 fn is_reliable(&self) -> bool {
1613 true
1614 }
1615 }
1616
1617 fn adversarial_client(
1618 pdu_type: PduType,
1619 community: &'static [u8],
1620 respond_as_v1: bool,
1621 ) -> Client<AdversarialTransport> {
1622 let transport = AdversarialTransport::new(pdu_type, community, respond_as_v1);
1623 let config = ClientConfig {
1624 version: Version::V2c,
1625 retry: crate::client::retry::Retry::none(),
1626 ..Default::default()
1627 };
1628 Client::new(transport, config)
1629 }
1630
1631 #[tokio::test]
1634 async fn response_validation_accepts_well_formed_response() {
1635 let client = adversarial_client(PduType::Response, b"public", false);
1636 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1637 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1638 }
1639
1640 #[tokio::test]
1643 async fn response_validation_rejects_echoed_request_pdu() {
1644 let client = adversarial_client(PduType::GetRequest, b"public", false);
1645 let err = client
1646 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1647 .await
1648 .unwrap_err();
1649 assert!(
1650 matches!(*err, Error::MalformedResponse { .. }),
1651 "expected MalformedResponse, got: {err}"
1652 );
1653 }
1654
1655 #[tokio::test]
1657 async fn response_validation_accepts_community_mismatch() {
1658 let client = adversarial_client(PduType::Response, b"other", false);
1659 let result = client.get(&Oid::from_slice(&[1, 3, 6, 1, 1])).await;
1660 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
1661 }
1662
1663 #[tokio::test]
1665 async fn response_validation_rejects_version_mismatch() {
1666 let client = adversarial_client(PduType::Response, b"public", true);
1667 let err = client
1668 .get(&Oid::from_slice(&[1, 3, 6, 1, 1]))
1669 .await
1670 .unwrap_err();
1671 assert!(
1672 matches!(*err, Error::MalformedResponse { .. }),
1673 "expected MalformedResponse, got: {err}"
1674 );
1675 }
1676}