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