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