Skip to main content

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