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