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