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//! ## Scalable Polling (Shared Transport)
122//!
123//! For monitoring systems polling many targets, share a single [`UdpTransport`]
124//! across all clients:
125//!
126//! - **1 file descriptor** for all targets (vs 1 per target)
127//! - **Firewall session reuse** between polls to the same target
128//! - **Lower memory** from shared socket buffers
129//! - **No per-poll socket creation** overhead
130//!
131//! **Scaling guidance:**
132//! - **Most use cases**: Single shared [`UdpTransport`] recommended
133//! - **~100,000s+ targets**: Multiple [`UdpTransport`] instances, sharded by target
134//! - **Scrape isolation**: Per-client via [`.connect()`](ClientBuilder::connect) (FD + syscall overhead)
135//!
136//! ```rust,no_run
137//! use async_snmp::{Auth, Client, oid};
138//! use async_snmp::transport::UdpTransport;
139//! use futures::future::join_all;
140//!
141//! async fn poll_many_devices(targets: Vec<String>) -> Vec<(String, Result<String, String>)> {
142//!     // Single socket shared across all clients
143//!     let transport = UdpTransport::bind("0.0.0.0:0")
144//!         .await
145//!         .expect("failed to bind");
146//!
147//!     let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
148//!
149//!     // Spawn concurrent requests
150//!     let futures: Vec<_> = targets.iter().map(|target| {
151//!         let handle = transport.handle(target.parse().expect("invalid addr"));
152//!         let oid = sys_descr.clone();
153//!         let target = target.clone();
154//!         async move {
155//!             let client = match Client::builder(target.clone(), Auth::v2c("public"))
156//!                 .build(handle) {
157//!                 Ok(c) => c,
158//!                 Err(e) => return (target, Err(e.to_string())),
159//!             };
160//!             let result: Result<String, String> = match client.get(&oid).await {
161//!                 Ok(vb) => Ok(vb.value.to_string()),
162//!                 Err(e) => Err(e.to_string()),
163//!             };
164//!             (target, result)
165//!         }
166//!     }).collect();
167//!
168//!     join_all(futures).await
169//! }
170//! ```
171//!
172//! ## High-Throughput SNMPv3 Polling
173//!
174//! SNMPv3 has two expensive per-connection operations:
175//! - **Password derivation**: ~850μs to derive keys from passwords (SHA-256)
176//! - **Engine discovery**: Round-trip to learn the agent's engine ID and time
177//!
178//! For polling many targets with shared credentials, cache both:
179//!
180//! ```rust,no_run
181//! use async_snmp::{Auth, AuthProtocol, Client, EngineCache, MasterKeys, PrivProtocol, oid};
182//! use async_snmp::transport::UdpTransport;
183//! use std::sync::Arc;
184//!
185//! # async fn example() -> async_snmp::Result<()> {
186//! // 1. Derive master keys once (expensive: ~850μs)
187//! let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
188//!     .with_privacy(PrivProtocol::Aes128, b"privpassword");
189//!
190//! // 2. Share engine discovery results across clients
191//! let engine_cache = Arc::new(EngineCache::new());
192//!
193//! // 3. Use shared transport for socket efficiency
194//! let transport = UdpTransport::bind("0.0.0.0:0").await?;
195//!
196//! // Poll multiple targets - only ~1μs key localization per engine
197//! for target in ["192.0.2.1:161", "192.0.2.2:161"] {
198//!     let handle = transport.handle(target.parse().unwrap());
199//!     let auth = Auth::usm("snmpuser").with_master_keys(master_keys.clone());
200//!
201//!     let client = Client::builder(target, auth)
202//!         .engine_cache(engine_cache.clone())
203//!         .build(handle)?;
204//!
205//!     let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
206//!     println!("{}: {:?}", target, result.value);
207//! }
208//! # Ok(())
209//! # }
210//! ```
211//!
212//! | Optimization | Without | With | Savings |
213//! |--------------|---------|------|---------|
214//! | `MasterKeys` | 850μs/engine | 1μs/engine | ~99.9% |
215//! | `EngineCache` | 1 RTT/engine | 0 RTT (cached) | 1 RTT |
216//!
217//! ## Graceful Shutdown
218//!
219//! Use `tokio::select!` or cancellation tokens for clean shutdown.
220//!
221//! ```rust,no_run
222//! use async_snmp::{Auth, Client, oid};
223//! use std::time::Duration;
224//! use tokio::time::interval;
225//!
226//! async fn poll_with_shutdown(
227//!     addr: &str,
228//!     mut shutdown: tokio::sync::oneshot::Receiver<()>,
229//! ) {
230//!     let client = Client::builder(addr, Auth::v2c("public"))
231//!         .connect()
232//!         .await
233//!         .expect("failed to connect");
234//!
235//!     let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
236//!     let mut poll_interval = interval(Duration::from_secs(30));
237//!
238//!     loop {
239//!         tokio::select! {
240//!             _ = &mut shutdown => {
241//!                 println!("Shutdown signal received");
242//!                 break;
243//!             }
244//!             _ = poll_interval.tick() => {
245//!                 match client.get(&sys_uptime).await {
246//!                     Ok(vb) => println!("Uptime: {:?}", vb.value),
247//!                     Err(e) => eprintln!("Poll failed: {}", e),
248//!                 }
249//!             }
250//!         }
251//!     }
252//! }
253//! ```
254//!
255//! ## Tracing Integration
256//!
257//! The library uses the `tracing` crate for structured logging. All SNMP
258//! operations emit spans and events with relevant context.
259//!
260//! ### Basic Setup
261//!
262//! ```rust,no_run
263//! use async_snmp::{Auth, Client, oid};
264//! use tracing_subscriber::EnvFilter;
265//!
266//! #[tokio::main]
267//! async fn main() {
268//!     tracing_subscriber::fmt()
269//!         .with_env_filter(
270//!             EnvFilter::from_default_env()
271//!                 .add_directive("async_snmp=debug".parse().unwrap())
272//!         )
273//!         .init();
274//!
275//!     let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
276//!         .connect()
277//!         .await
278//!         .expect("failed to connect");
279//!
280//!     // Logs: DEBUG async_snmp::client snmp.target=192.168.1.1:161 snmp.request_id=12345
281//!     let _ = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await;
282//! }
283//! ```
284//!
285//! ### Log Levels
286//!
287//! | Level | What's Logged |
288//! |-------|---------------|
289//! | ERROR | Socket errors, fatal transport failures |
290//! | WARN | Auth failures, parse errors, source address mismatches |
291//! | INFO | Connect/disconnect, walk completion |
292//! | DEBUG | Request/response flow, engine discovery, retries |
293//! | TRACE | Auth verification, raw packet data |
294//!
295//! ### Structured Fields
296//!
297//! All fields use the `snmp.` prefix for easy filtering:
298//!
299//! | Field | Description |
300//! |-------|-------------|
301//! | `snmp.target` | Target address for outgoing requests |
302//! | `snmp.source` | Source address of incoming messages |
303//! | `snmp.request_id` | SNMP request identifier |
304//! | `snmp.retries` | Current retry attempt number |
305//! | `snmp.elapsed_ms` | Request duration in milliseconds |
306//! | `snmp.pdu_type` | PDU type (Get, GetNext, etc.) |
307//! | `snmp.varbind_count` | Number of varbinds in request/response |
308//! | `snmp.error_status` | SNMP error status from response |
309//! | `snmp.error_index` | Index of problematic varbind |
310//! | `snmp.non_repeaters` | GETBULK non-repeaters parameter |
311//! | `snmp.max_repetitions` | GETBULK max-repetitions parameter |
312//! | `snmp.username` | SNMPv3 USM username |
313//! | `snmp.security_level` | SNMPv3 security level |
314//! | `snmp.engine_id` | SNMPv3 engine identifier (hex) |
315//! | `snmp.local_addr` | Local bind address |
316//!
317//! ### Filtering Examples
318//!
319//! ```bash
320//! # See all async-snmp logs at debug level
321//! RUST_LOG=async_snmp=debug cargo run
322//!
323//! # Only see retries and errors
324//! RUST_LOG=async_snmp=warn cargo run
325//!
326//! # Trace a specific module
327//! RUST_LOG=async_snmp::client=trace cargo run
328//! ```
329//!
330//! ## Agent Compatibility
331//!
332//! Real-world SNMP agents often have quirks. This library provides several
333//! options to handle non-conformant implementations.
334//!
335//! ### Walk Issues
336//!
337//! | Problem | Solution |
338//! |---------|----------|
339//! | GETBULK returns errors or garbage | Use [`WalkMode::GetNext`] |
340//! | OIDs returned out of order | Use [`OidOrdering::AllowNonIncreasing`] |
341//! | Walk never terminates | Set [`ClientBuilder::max_walk_results`] |
342//! | Slow responses cause timeouts | Reduce [`ClientBuilder::max_repetitions`] |
343//!
344//! **Warning**: [`OidOrdering::AllowNonIncreasing`] uses O(n) memory to track
345//! seen OIDs for cycle detection. Always pair it with [`ClientBuilder::max_walk_results`]
346//! to bound memory usage. The cycle detection catches duplicate OIDs, but a
347//! pathological agent could still return an infinite sequence of unique OIDs.
348//!
349//! ```rust,no_run
350//! use async_snmp::{Auth, Client, WalkMode, OidOrdering};
351//!
352//! # async fn example() -> async_snmp::Result<()> {
353//! // Configure for a problematic agent
354//! let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
355//!     .walk_mode(WalkMode::GetNext)           // Avoid buggy GETBULK
356//!     .oid_ordering(OidOrdering::AllowNonIncreasing)  // Handle out-of-order OIDs
357//!     .max_walk_results(10_000)               // IMPORTANT: bound memory usage
358//!     .max_repetitions(10)                    // Smaller responses
359//!     .connect()
360//!     .await?;
361//! # Ok(())
362//! # }
363//! ```
364//!
365//! ### Permissive Parsing
366//!
367//! The BER decoder accepts non-conformant encodings that some agents produce:
368//! - Non-minimal integer encodings (extra leading bytes)
369//! - Non-minimal OID subidentifier encodings
370//! - Truncated values (logged as warnings)
371//!
372//! This matches net-snmp's permissive behavior.
373//!
374//! ### Unknown Value Types
375//!
376//! Unrecognized BER tags are preserved as [`Value::Unknown`] rather than
377//! causing decode errors. This provides forward compatibility with new
378//! SNMP types or vendor extensions.
379
380pub mod agent;
381pub mod ber;
382pub mod client;
383pub mod error;
384pub mod format;
385pub mod handler;
386pub mod message;
387pub mod notification;
388pub mod oid;
389pub mod pdu;
390pub mod prelude;
391pub mod transport;
392pub mod v3;
393pub mod value;
394pub mod varbind;
395pub mod version;
396
397pub(crate) mod util;
398
399#[cfg(feature = "cli")]
400pub mod cli;
401
402// Re-exports for convenience
403pub use agent::{Agent, AgentBuilder, VacmBuilder, VacmConfig, View};
404pub use client::{
405    Auth, Backoff, BulkWalk, Client, ClientBuilder, ClientConfig, CommunityVersion, OidOrdering,
406    Retry, RetryBuilder, UsmAuth, UsmBuilder, V3SecurityConfig, Walk, WalkMode, WalkStream,
407};
408pub use error::{
409    AuthErrorKind, CryptoErrorKind, DecodeErrorKind, EncodeErrorKind, Error, ErrorStatus,
410    OidErrorKind, Result,
411};
412pub use handler::{
413    BoxFuture, GetNextResult, GetResult, MibHandler, OidTable, RequestContext, Response,
414    SecurityModel, SetResult,
415};
416pub use message::SecurityLevel;
417pub use notification::{
418    Notification, NotificationReceiver, NotificationReceiverBuilder, UsmUserConfig,
419    validate_notification_varbinds,
420};
421pub use oid::Oid;
422pub use pdu::{GenericTrap, Pdu, PduType, TrapV1Pdu};
423pub use transport::{TcpTransport, Transport, UdpHandle, UdpTransport};
424pub use v3::{
425    AuthProtocol, EngineCache, LocalizedKey, MasterKey, MasterKeys, ParseProtocolError,
426    PrivProtocol,
427};
428pub use value::Value;
429pub use varbind::VarBind;
430pub use version::Version;
431
432/// Type alias for a client using UDP transport.
433///
434/// This is the default and most common client type.
435pub type UdpClient = Client<UdpHandle>;
436
437/// Type alias for a client using a TCP connection.
438pub type TcpClient = Client<TcpTransport>;