async_snmp/
lib.rs

1// Allow large error types - the Error enum includes OIDs inline for debugging convenience.
2// Boxing them would add complexity and allocations for a marginal size reduction.
3#![allow(clippy::result_large_err)]
4
5//! # async-snmp
6//!
7//! Modern, async-first SNMP client library for Rust.
8//!
9//! ## Features
10//!
11//! - Full SNMPv1, v2c, and v3 support
12//! - Async-first API built on Tokio
13//! - Zero-copy BER encoding/decoding
14//! - Type-safe OID and value handling
15//! - Config-driven client construction
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use async_snmp::{Auth, Client, oid};
21//! use std::time::Duration;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), async_snmp::Error> {
25//!     // SNMPv2c client
26//!     let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
27//!         .timeout(Duration::from_secs(5))
28//!         .connect()
29//!         .await?;
30//!
31//!     let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
32//!     println!("sysDescr: {:?}", result.value);
33//!
34//!     Ok(())
35//! }
36//! ```
37//!
38//! ## SNMPv3 Example
39//!
40//! ```rust,no_run
41//! use async_snmp::{Auth, Client, oid, v3::{AuthProtocol, PrivProtocol}};
42//!
43//! #[tokio::main]
44//! async fn main() -> Result<(), async_snmp::Error> {
45//!     let client = Client::builder("192.168.1.1:161",
46//!         Auth::usm("admin")
47//!             .auth(AuthProtocol::Sha256, "authpass123")
48//!             .privacy(PrivProtocol::Aes128, "privpass123"))
49//!         .connect()
50//!         .await?;
51//!
52//!     let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
53//!     println!("sysDescr: {:?}", result.value);
54//!
55//!     Ok(())
56//! }
57//! ```
58//!
59//! # Advanced Topics
60//!
61//! ## Error Handling Patterns
62//!
63//! The library provides detailed error information for debugging and recovery.
64//! See the [`error`] module for complete documentation.
65//!
66//! ```rust,no_run
67//! use async_snmp::{Auth, Client, Error, ErrorStatus, Retry, oid};
68//! use std::time::Duration;
69//!
70//! async fn poll_device(addr: &str) -> Result<String, String> {
71//!     let client = Client::builder(addr, Auth::v2c("public"))
72//!         .timeout(Duration::from_secs(5))
73//!         .retry(Retry::fixed(2, Duration::ZERO))
74//!         .connect()
75//!         .await
76//!         .map_err(|e| format!("Failed to connect: {}", e))?;
77//!
78//!     match client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await {
79//!         Ok(vb) => Ok(vb.value.as_str().unwrap_or("(non-string)").to_string()),
80//!         Err(Error::Timeout { retries, .. }) => {
81//!             Err(format!("Device unreachable after {} retries", retries))
82//!         }
83//!         Err(Error::Snmp { status: ErrorStatus::NoSuchName, .. }) => {
84//!             Err("OID not supported by device".to_string())
85//!         }
86//!         Err(e) => Err(format!("SNMP error: {}", e)),
87//!     }
88//! }
89//! ```
90//!
91//! ## Retry Configuration
92//!
93//! UDP transports retry on timeout with configurable backoff strategies.
94//! TCP transports ignore retry configuration (the transport layer handles reliability).
95//!
96//! ```rust
97//! use async_snmp::{Auth, Client, Retry};
98//! use std::time::Duration;
99//!
100//! # async fn example() -> async_snmp::Result<()> {
101//! // No retries (fail immediately on timeout)
102//! let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
103//!     .retry(Retry::none())
104//!     .connect().await?;
105//!
106//! // 3 retries with no delay (default behavior)
107//! let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
108//!     .retry(Retry::fixed(3, Duration::ZERO))
109//!     .connect().await?;
110//!
111//! // Exponential backoff with jitter (1s, 2s, 4s, 5s, 5s)
112//! let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
113//!     .retry(Retry::exponential(5)
114//!         .max_delay(Duration::from_secs(5))
115//!         .jitter(0.25))  // ±25% randomization
116//!     .connect().await?;
117//! # Ok(())
118//! # }
119//! ```
120//!
121//! ## Concurrent Operations
122//!
123//! Use standard async patterns to poll multiple devices concurrently.
124//! Share a [`UdpTransport`] for polling many targets.
125//!
126//! ```rust,no_run
127//! use async_snmp::{Auth, Client, oid};
128//! use async_snmp::transport::UdpTransport;
129//! use futures::future::join_all;
130//!
131//! async fn poll_many_devices(targets: Vec<String>) -> Vec<(String, Result<String, String>)> {
132//!     // Create a shared transport for efficient socket usage
133//!     let transport = UdpTransport::bind("0.0.0.0:0")
134//!         .await
135//!         .expect("failed to bind");
136//!
137//!     let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
138//!
139//!     // Spawn concurrent requests
140//!     let futures: Vec<_> = targets.iter().map(|target| {
141//!         let handle = transport.handle(target.parse().expect("invalid addr"));
142//!         let oid = sys_descr.clone();
143//!         let target = target.clone();
144//!         async move {
145//!             let client = match Client::builder(target.clone(), Auth::v2c("public"))
146//!                 .build(handle) {
147//!                 Ok(c) => c,
148//!                 Err(e) => return (target, Err(e.to_string())),
149//!             };
150//!             let result: Result<String, String> = match client.get(&oid).await {
151//!                 Ok(vb) => Ok(vb.value.to_string()),
152//!                 Err(e) => Err(e.to_string()),
153//!             };
154//!             (target, result)
155//!         }
156//!     }).collect();
157//!
158//!     join_all(futures).await
159//! }
160//! ```
161//!
162//! ## High-Throughput SNMPv3 Polling
163//!
164//! SNMPv3 has two expensive per-connection operations:
165//! - **Password derivation**: ~850μs to derive keys from passwords (SHA-256)
166//! - **Engine discovery**: Round-trip to learn the agent's engine ID and time
167//!
168//! For polling many targets with shared credentials, cache both:
169//!
170//! ```rust,no_run
171//! use async_snmp::{Auth, AuthProtocol, Client, EngineCache, MasterKeys, PrivProtocol, oid};
172//! use async_snmp::transport::UdpTransport;
173//! use std::sync::Arc;
174//!
175//! # async fn example() -> async_snmp::Result<()> {
176//! // 1. Derive master keys once (expensive: ~850μs)
177//! let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
178//!     .with_privacy(PrivProtocol::Aes128, b"privpassword");
179//!
180//! // 2. Share engine discovery results across clients
181//! let engine_cache = Arc::new(EngineCache::new());
182//!
183//! // 3. Use shared transport for socket efficiency
184//! let transport = UdpTransport::bind("0.0.0.0:0").await?;
185//!
186//! // Poll multiple targets - only ~1μs key localization per engine
187//! for target in ["192.0.2.1:161", "192.0.2.2:161"] {
188//!     let handle = transport.handle(target.parse().unwrap());
189//!     let auth = Auth::usm("snmpuser").with_master_keys(master_keys.clone());
190//!
191//!     let client = Client::builder(target, auth)
192//!         .engine_cache(engine_cache.clone())
193//!         .build(handle)?;
194//!
195//!     let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
196//!     println!("{}: {:?}", target, result.value);
197//! }
198//! # Ok(())
199//! # }
200//! ```
201//!
202//! | Optimization | Without | With | Savings |
203//! |--------------|---------|------|---------|
204//! | `MasterKeys` | 850μs/engine | 1μs/engine | ~99.9% |
205//! | `EngineCache` | 1 RTT/engine | 0 RTT (cached) | 1 RTT |
206//!
207//! ## Graceful Shutdown
208//!
209//! Use `tokio::select!` or cancellation tokens for clean shutdown.
210//!
211//! ```rust,no_run
212//! use async_snmp::{Auth, Client, oid};
213//! use std::time::Duration;
214//! use tokio::time::interval;
215//!
216//! async fn poll_with_shutdown(
217//!     addr: &str,
218//!     mut shutdown: tokio::sync::oneshot::Receiver<()>,
219//! ) {
220//!     let client = Client::builder(addr, Auth::v2c("public"))
221//!         .connect()
222//!         .await
223//!         .expect("failed to connect");
224//!
225//!     let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
226//!     let mut poll_interval = interval(Duration::from_secs(30));
227//!
228//!     loop {
229//!         tokio::select! {
230//!             _ = &mut shutdown => {
231//!                 println!("Shutdown signal received");
232//!                 break;
233//!             }
234//!             _ = poll_interval.tick() => {
235//!                 match client.get(&sys_uptime).await {
236//!                     Ok(vb) => println!("Uptime: {:?}", vb.value),
237//!                     Err(e) => eprintln!("Poll failed: {}", e),
238//!                 }
239//!             }
240//!         }
241//!     }
242//! }
243//! ```
244//!
245//! ## Tracing Integration
246//!
247//! The library uses the `tracing` crate for structured logging. All SNMP
248//! operations emit spans and events with relevant context.
249//!
250//! ### Basic Setup
251//!
252//! ```rust,no_run
253//! use async_snmp::{Auth, Client, oid};
254//! use tracing_subscriber::EnvFilter;
255//!
256//! #[tokio::main]
257//! async fn main() {
258//!     tracing_subscriber::fmt()
259//!         .with_env_filter(
260//!             EnvFilter::from_default_env()
261//!                 .add_directive("async_snmp=debug".parse().unwrap())
262//!         )
263//!         .init();
264//!
265//!     let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
266//!         .connect()
267//!         .await
268//!         .expect("failed to connect");
269//!
270//!     // Logs: DEBUG async_snmp::client snmp.target=192.168.1.1:161 snmp.request_id=12345
271//!     let _ = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await;
272//! }
273//! ```
274//!
275//! ### Log Levels
276//!
277//! | Level | What's Logged |
278//! |-------|---------------|
279//! | ERROR | Socket errors, fatal transport failures |
280//! | WARN | Auth failures, parse errors, source address mismatches |
281//! | INFO | Connect/disconnect, walk completion |
282//! | DEBUG | Request/response flow, engine discovery, retries |
283//! | TRACE | Auth verification, raw packet data |
284//!
285//! ### Structured Fields
286//!
287//! All fields use the `snmp.` prefix for easy filtering:
288//!
289//! | Field | Description |
290//! |-------|-------------|
291//! | `snmp.target` | Target address for outgoing requests |
292//! | `snmp.source` | Source address of incoming messages |
293//! | `snmp.request_id` | SNMP request identifier |
294//! | `snmp.retries` | Current retry attempt number |
295//! | `snmp.elapsed_ms` | Request duration in milliseconds |
296//! | `snmp.pdu_type` | PDU type (Get, GetNext, etc.) |
297//! | `snmp.varbind_count` | Number of varbinds in request/response |
298//! | `snmp.error_status` | SNMP error status from response |
299//! | `snmp.error_index` | Index of problematic varbind |
300//! | `snmp.non_repeaters` | GETBULK non-repeaters parameter |
301//! | `snmp.max_repetitions` | GETBULK max-repetitions parameter |
302//! | `snmp.username` | SNMPv3 USM username |
303//! | `snmp.security_level` | SNMPv3 security level |
304//! | `snmp.engine_id` | SNMPv3 engine identifier (hex) |
305//! | `snmp.local_addr` | Local bind address |
306//!
307//! ### Filtering Examples
308//!
309//! ```bash
310//! # See all async-snmp logs at debug level
311//! RUST_LOG=async_snmp=debug cargo run
312//!
313//! # Only see retries and errors
314//! RUST_LOG=async_snmp=warn cargo run
315//!
316//! # Trace a specific module
317//! RUST_LOG=async_snmp::client=trace cargo run
318//! ```
319//!
320//! ## Agent Compatibility
321//!
322//! Real-world SNMP agents often have quirks. This library provides several
323//! options to handle non-conformant implementations.
324//!
325//! ### Walk Issues
326//!
327//! | Problem | Solution |
328//! |---------|----------|
329//! | GETBULK returns errors or garbage | Use [`WalkMode::GetNext`] |
330//! | OIDs returned out of order | Use [`OidOrdering::AllowNonIncreasing`] |
331//! | Walk never terminates | Set [`ClientBuilder::max_walk_results`] |
332//! | Slow responses cause timeouts | Reduce [`ClientBuilder::max_repetitions`] |
333//!
334//! **Warning**: [`OidOrdering::AllowNonIncreasing`] uses O(n) memory to track
335//! seen OIDs for cycle detection. Always pair it with [`ClientBuilder::max_walk_results`]
336//! to bound memory usage. The cycle detection catches duplicate OIDs, but a
337//! pathological agent could still return an infinite sequence of unique OIDs.
338//!
339//! ```rust,no_run
340//! use async_snmp::{Auth, Client, WalkMode, OidOrdering};
341//!
342//! # async fn example() -> async_snmp::Result<()> {
343//! // Configure for a problematic agent
344//! let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
345//!     .walk_mode(WalkMode::GetNext)           // Avoid buggy GETBULK
346//!     .oid_ordering(OidOrdering::AllowNonIncreasing)  // Handle out-of-order OIDs
347//!     .max_walk_results(10_000)               // IMPORTANT: bound memory usage
348//!     .max_repetitions(10)                    // Smaller responses
349//!     .connect()
350//!     .await?;
351//! # Ok(())
352//! # }
353//! ```
354//!
355//! ### Permissive Parsing
356//!
357//! The BER decoder accepts non-conformant encodings that some agents produce:
358//! - Non-minimal integer encodings (extra leading bytes)
359//! - Non-minimal OID subidentifier encodings
360//! - Truncated values (logged as warnings)
361//!
362//! This matches net-snmp's permissive behavior.
363//!
364//! ### Unknown Value Types
365//!
366//! Unrecognized BER tags are preserved as [`Value::Unknown`] rather than
367//! causing decode errors. This provides forward compatibility with new
368//! SNMP types or vendor extensions.
369
370pub mod agent;
371pub mod ber;
372pub mod client;
373pub mod error;
374pub mod format;
375pub mod handler;
376pub mod message;
377pub mod notification;
378pub mod oid;
379pub mod pdu;
380pub mod prelude;
381pub mod transport;
382pub mod v3;
383pub mod value;
384pub mod varbind;
385pub mod version;
386
387pub(crate) mod util;
388
389#[cfg(feature = "cli")]
390pub mod cli;
391
392// Re-exports for convenience
393pub use agent::{Agent, AgentBuilder, VacmBuilder, VacmConfig, View};
394pub use client::{
395    Auth, Backoff, BulkWalk, Client, ClientBuilder, ClientConfig, CommunityVersion, OidOrdering,
396    Retry, RetryBuilder, UsmAuth, UsmBuilder, V3SecurityConfig, Walk, WalkMode, WalkStream,
397};
398pub use error::{
399    AuthErrorKind, CryptoErrorKind, DecodeErrorKind, EncodeErrorKind, Error, ErrorStatus,
400    OidErrorKind, Result,
401};
402pub use handler::{
403    BoxFuture, GetNextResult, GetResult, MibHandler, OidTable, RequestContext, Response,
404    SecurityModel, SetResult,
405};
406pub use message::SecurityLevel;
407pub use notification::{
408    Notification, NotificationReceiver, NotificationReceiverBuilder, UsmUserConfig,
409    validate_notification_varbinds,
410};
411pub use oid::Oid;
412pub use pdu::{GenericTrap, Pdu, PduType, TrapV1Pdu};
413pub use transport::{TcpTransport, Transport, UdpHandle, UdpTransport};
414pub use v3::{
415    AuthProtocol, EngineCache, LocalizedKey, MasterKey, MasterKeys, ParseProtocolError,
416    PrivProtocol,
417};
418pub use value::Value;
419pub use varbind::VarBind;
420pub use version::Version;
421
422/// Type alias for a client using UDP transport.
423///
424/// This is the default and most common client type.
425pub type UdpClient = Client<UdpHandle>;
426
427/// Type alias for a client using a TCP connection.
428pub type TcpClient = Client<TcpTransport>;
429
430/// Testing utilities exposed via the `testing` feature.
431#[cfg(feature = "testing")]
432pub mod testing {
433    pub use crate::format::hex::{Bytes as HexBytes, DecodeError, decode, encode};
434}