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