async_snmp/
lib.rs

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