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