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