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>;