Expand description
§async-snmp
Modern, async-first SNMP client library for Rust.
§Features
- Full
SNMPv1, v2c, and v3 support - Async-first API built on Tokio
- Zero-copy BER encoding/decoding
- Type-safe OID and value handling
- Config-driven client construction
- Trap and inform sending (agent-based multi-sink or client-based)
- Trap and inform receiving with optional community filtering and per-notification security-level reporting
- SNMP agent with async handlers, two-phase SET, VACM, and built-in MIB handlers
- Automatic tooBig recovery (GET/GETNEXT batches bisect on oversized responses)
§Quick Start
use async_snmp::{Auth, Client, oid};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<async_snmp::Error>> {
// SNMPv2c client - target accepts (host, port), a string, or a SocketAddr
let client = Client::builder(("192.168.1.1", 161), Auth::v2c("public"))
.timeout(Duration::from_secs(5))
.connect()
.await?;
let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
println!("sysDescr: {:?}", result.value);
Ok(())
}§SNMPv3 Example
use async_snmp::{Auth, Client, oid, v3::{AuthProtocol, PrivProtocol}};
#[tokio::main]
async fn main() -> Result<(), Box<async_snmp::Error>> {
let client = Client::builder(("192.168.1.1", 161),
Auth::usm("admin").auth_priv(
AuthProtocol::Sha256,
"authpass123",
PrivProtocol::Aes128,
"privpass123",
))
.connect()
.await?;
let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
println!("sysDescr: {:?}", result.value);
Ok(())
}§SNMPv3 Trust, Corrections, and Roles
Engine discovery is unauthenticated. A client accepts only a correlated,
standard usmStatsUnknownEngineIDs.0 Report and learns an engine identity
candidate and message-size limit; it discards the Report’s boots/time tuple.
Trusted time is established and advanced only after HMAC verification and
RFC 3414 Step 7(b) processing.
Incoming auth/privacy flags select the received security level. HMAC and
timeliness processing precede decryption, scoped-PDU parsing, and msgID
correlation. Ordinary Responses then require exact identity, security,
context, PDU type, and request-id matches. Reports also require a current
exchange msgID and exact shape; terminal statuses are returned as
Error::Report.
On an SNMPv3 timeout, each transmission uses a fresh outer msgID while the PDU request-id remains stable, and any msgID in the current exchange can correlate. Stable request-id reuse is a deliberate RFC 3414 Section 11.1 interoperability deviation. One authenticated time-window Report may cause a corrected request with fresh message and PDU IDs independently of timeout retry policy. Other or repeated Reports are terminal.
EngineCache maps target addresses to discovered identities while sharing
trusted time by authoritative engine ID. Cache TTL expiry affects future
lookups, not a live client’s established identity; use
Client::rediscover_engine after an intentional device replacement.
The default-off
ClientBuilder::allow_unauthenticated_v3_time_correction option supports
devices that emit an unauthenticated time-window Report. Its tuple is used
for one authenticated packet and never installed as trusted state, but an
injector can choose that packet’s time fields; enable strict UDP source
checking where possible.
Agents with USM users or V3 trap sinks, notification receivers with USM
users, and clients originating V3 traps are locally authoritative and need
a persisted AuthoritativeEngine. Polling and V3 Inform originators use
the remote responder as authoritative and do not need local engine state.
§Advanced Topics
§Error Handling Patterns
The library provides detailed error information for debugging and recovery.
See the error module for complete documentation.
use async_snmp::{Auth, Client, Error, ErrorStatus, Retry, oid};
use std::time::Duration;
async fn poll_device(addr: &str) -> Result<String, String> {
let client = Client::builder(addr, Auth::v2c("public"))
.timeout(Duration::from_secs(5))
.retry(Retry::fixed(2, Duration::ZERO))
.connect()
.await
.map_err(|e| format!("Failed to connect: {}", e))?;
match client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await {
Ok(vb) => Ok(vb.value.as_str().unwrap_or("(non-string)").to_string()),
Err(e) => match *e {
Error::Timeout { retries, .. } => {
Err(format!("Device unreachable after {} retries", retries))
}
Error::Snmp { status: ErrorStatus::NoSuchName, .. } => {
Err("OID not supported by device".to_string())
}
_ => Err(format!("SNMP error: {}", e)),
},
}
}§Retry Configuration
UDP transports retry on timeout with configurable backoff strategies. TCP transports ignore retry configuration (the transport layer handles reliability).
use async_snmp::{Auth, Client, Retry};
use std::time::Duration;
// No retries (fail immediately on timeout)
let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
.retry(Retry::none())
.connect().await?;
// 3 retries with no delay between attempts
let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
.retry(Retry::fixed(3, Duration::ZERO))
.connect().await?;
// Exponential backoff with jitter (1s, 2s, 4s, 5s, 5s)
let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
.retry(Retry::exponential(5)
.max_delay(Duration::from_secs(5))
.jitter(0.25)) // ±25% randomization
.connect().await?;§Scalable Polling (Shared Transport)
For monitoring systems polling many targets, share a single UdpTransport
across all clients:
- 1 file descriptor for all targets (vs 1 per target)
- Firewall session reuse between polls to the same target
- Lower memory from shared socket buffers
- No per-poll socket creation overhead
Scaling guidance:
- Most use cases: Single shared
UdpTransportrecommended - ~100,000s+ targets: Multiple
UdpTransportinstances, sharded by target - Scrape isolation: Per-client via
.connect()(FD + syscall overhead)
use async_snmp::{Auth, Client, oid, UdpTransport};
use futures::future::join_all;
async fn poll_many_devices(targets: Vec<&str>) -> Vec<(&str, Result<String, String>)> {
// Single socket shared across all clients
let transport = UdpTransport::bind("0.0.0.0:0")
.await
.expect("failed to bind");
let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
// Create clients for each target - (host, port) tuples work naturally
let mut clients = Vec::new();
for t in &targets {
let client = Client::builder((*t, 161), Auth::v2c("public"))
.build_with(&transport)
.await
.expect("failed to build client");
clients.push(client);
}
// Poll all targets concurrently
let results = join_all(
clients.iter().map(|c| async {
match c.get(&sys_descr).await {
Ok(vb) => Ok(vb.value.to_string()),
Err(e) => Err(e.to_string()),
}
})
).await;
targets.into_iter().zip(results).collect()
}§High-Throughput SNMPv3 Polling
SNMPv3 has two expensive per-connection operations:
- Password derivation: ~850μs to derive keys from passwords (SHA-256)
- Engine discovery: Round-trip to learn the agent’s engine ID and message-size limit
For polling many targets with shared credentials, cache both:
use async_snmp::{Auth, AuthProtocol, Client, EngineCache, MasterKeys, PrivProtocol, oid, UdpTransport};
use std::sync::Arc;
// 1. Derive master keys once (expensive: ~850μs)
let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap()
.with_privacy(PrivProtocol::Aes128, b"privpassword").unwrap();
// 2. Share engine discovery results across clients
let engine_cache = Arc::new(EngineCache::new());
// 3. Use shared transport for socket efficiency
let transport = UdpTransport::bind("0.0.0.0:0").await?;
// Poll multiple targets - only ~1μs key localization per engine
for target in ["192.0.2.1:161", "192.0.2.2:161"] {
let auth = Auth::usm("snmpuser").with_master_keys(master_keys.clone());
let client = Client::builder(target, auth)
.engine_cache(engine_cache.clone())
.build_with(&transport).await?;
let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
println!("{}: {:?}", target, result.value);
}| Optimization | Without | With | Savings |
|---|---|---|---|
MasterKeys | 850μs/engine | 1μs/engine | ~99.9% |
EngineCache | 1 RTT/engine | 0 RTT (cached) | 1 RTT |
§Graceful Shutdown
Use tokio::select! or cancellation tokens for clean shutdown.
use async_snmp::{Auth, Client, oid};
use std::time::Duration;
use tokio::time::interval;
async fn poll_with_shutdown(
addr: &str,
mut shutdown: tokio::sync::oneshot::Receiver<()>,
) {
let client = Client::builder(addr, Auth::v2c("public"))
.connect()
.await
.expect("failed to connect");
let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
let mut poll_interval = interval(Duration::from_secs(30));
loop {
tokio::select! {
_ = &mut shutdown => {
println!("Shutdown signal received");
break;
}
_ = poll_interval.tick() => {
match client.get(&sys_uptime).await {
Ok(vb) => println!("Uptime: {:?}", vb.value),
Err(e) => eprintln!("Poll failed: {}", e),
}
}
}
}
}§Tracing Integration
The library uses the tracing crate for structured logging. All SNMP
operations emit spans and events with relevant context.
§Basic Setup
use async_snmp::{Auth, Client, oid};
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("async_snmp=debug".parse().unwrap())
)
.init();
let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
.connect()
.await
.expect("failed to connect");
// Logs: DEBUG async_snmp::client snmp.target=192.168.1.1:161 snmp.request_id=12345
let _ = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await;
}§Log Levels
| Level | What’s Logged |
|---|---|
| ERROR | Socket errors, fatal transport failures |
| WARN | Auth failures, parse errors, source address mismatches |
| INFO | Connect/disconnect, walk completion |
| DEBUG | Request/response flow, engine discovery, retries |
| TRACE | Auth verification, raw packet data |
§Structured Fields
All fields use the snmp. prefix for easy filtering:
| Field | Description |
|---|---|
snmp.target | Target address for outgoing requests |
snmp.source | Source address of incoming messages |
snmp.request_id | SNMP request identifier |
snmp.retries | Current retry attempt number |
snmp.elapsed_ms | Request duration in milliseconds |
snmp.pdu_type | PDU type (Get, GetNext, etc.) |
snmp.varbind_count | Number of varbinds in request/response |
snmp.error_status | SNMP error status from response |
snmp.error_index | Index of problematic varbind |
snmp.non_repeaters | GETBULK non-repeaters parameter |
snmp.max_repetitions | GETBULK max-repetitions parameter |
snmp.username | SNMPv3 USM username |
snmp.security_level | SNMPv3 security level |
snmp.engine_id | SNMPv3 engine identifier (hex) |
snmp.local_addr | Local bind address |
§Filtering by Target
Tracing targets follow a stable naming scheme (not tied to internal module paths):
| Target Prefix | What’s Included |
|---|---|
async_snmp | Everything |
async_snmp::client | Client operations, requests, retries |
async_snmp::agent | Agent request/response handling |
async_snmp::ber | BER encoding/decoding |
async_snmp::v3 | SNMPv3 message processing |
async_snmp::transport | UDP/TCP transport layer |
async_snmp::notification | Trap/inform receiver |
# All library logs at debug level
RUST_LOG=async_snmp=debug cargo run
# Only warnings and errors
RUST_LOG=async_snmp=warn cargo run
# Trace client operations, debug everything else
RUST_LOG=async_snmp=debug,async_snmp::client=trace cargo run
# Debug just BER decoding issues
RUST_LOG=async_snmp::ber=debug cargo run§Agent Compatibility
Real-world SNMP agents often have quirks. This library provides several options to handle non-conformant implementations.
§Walk Issues
| Problem | Solution |
|---|---|
| GETBULK returns errors or garbage | Use WalkMode::GetNext |
| OIDs returned out of order | Use OidOrdering::AllowNonIncreasing |
| Walk never terminates | Set ClientBuilder::max_walk_results |
| Slow responses cause timeouts | Reduce ClientBuilder::max_repetitions |
Warning: OidOrdering::AllowNonIncreasing uses O(n) memory to track
seen OIDs for cycle detection. Always pair it with ClientBuilder::max_walk_results
to bound memory usage. The cycle detection catches duplicate OIDs, but a
pathological agent could still return an infinite sequence of unique OIDs.
use async_snmp::{Auth, Client, WalkMode, OidOrdering};
// Configure for a problematic agent
let client = Client::builder("192.168.1.1:161", Auth::v2c("public"))
.walk_mode(WalkMode::GetNext) // Avoid buggy GETBULK
.oid_ordering(OidOrdering::AllowNonIncreasing) // Handle out-of-order OIDs
.max_walk_results(10_000) // IMPORTANT: bound memory usage
.max_repetitions(10) // Smaller responses
.connect()
.await?;§Permissive Parsing
The BER decoder accepts non-conformant encodings that some agents produce:
- Non-minimal integer encodings (extra leading bytes)
- Non-minimal OID subidentifier encodings
- Truncated values (logged as warnings)
This matches net-snmp’s permissive behavior.
§Unknown Value Types
Unrecognized BER tags are preserved as Value::Unknown rather than
causing decode errors. This provides forward compatibility with new
SNMP types or vendor extensions.
§Cargo Features
agent- SNMP agent (enabled by default)crypto-rustcrypto- RustCrypto-based crypto backend (enabled by default). Supports all auth and privacy protocols.crypto-fips- FIPS 140-3 crypto backend via aws-lc-rs. Rejects MD5, DES, and 3DES. Mutually exclusive withcrypto-rustcrypto.cli- Builds command-line utilities (asnmp-get,asnmp-walk,asnmp-set)mib- MIB integration via mib-rs (OID conversions, value formatting helpers)rt-multi-thread- Multi-threaded tokio runtimetls- (Placeholder) SNMP over TLS per RFC 6353dtls- (Placeholder) SNMP over DTLS per RFC 6353
Note: crypto-rustcrypto and crypto-fips are mutually exclusive.
Exactly one must be enabled. Using --all-features will not compile;
specify features explicitly instead.
Re-exports§
pub use agent::Agent;pub use agent::AgentBuilder;pub use agent::BuiltinMib;pub use agent::VacmBuilder;pub use agent::VacmConfig;pub use agent::View;pub use client::Auth;pub use client::Backoff;pub use client::BulkWalk;pub use client::Client;pub use client::ClientBuilder;pub use client::ClientConfig;pub use client::CommunityVersion;pub use client::DEFAULT_MAX_OIDS_PER_REQUEST;pub use client::DEFAULT_MAX_REPETITIONS;pub use client::DEFAULT_TIMEOUT;pub use client::OidOrdering;pub use client::Retry;pub use client::RetryBuilder;pub use client::Target;pub use client::Walk;pub use client::WalkMode;pub use client::WalkStream;pub use error::Error;pub use error::ErrorStatus;pub use error::Result;pub use error::WalkAbortReason;pub use handler::BoxFuture;pub use handler::GetNextResult;pub use handler::GetResult;pub use handler::HandlerError;pub use handler::HandlerResult;pub use handler::MibHandler;pub use handler::OidTable;pub use handler::RequestContext;pub use handler::Response;pub use handler::SecurityModel;pub use handler::SetResult;pub use message::SecurityLevel;pub use notification::Notification;pub use notification::NotificationReceiver;pub use notification::NotificationReceiverBuilder;pub use notification::validate_notification_varbinds;pub use oid::Oid;pub use pdu::GenericTrap;pub use pdu::Pdu;pub use pdu::PduType;pub use pdu::TrapV1Pdu;pub use transport::MAX_UDP_PAYLOAD;pub use transport::TcpTransport;pub use transport::Transport;pub use transport::UdpHandle;pub use transport::UdpTransport;pub use v3::RustCryptoProvider;pub use v3::AuthProtocol;pub use v3::AuthoritativeEngine;pub use v3::CryptoError;pub use v3::CryptoProvider;pub use v3::CryptoResult;pub use v3::EngineCache;pub use v3::LocalizedKey;pub use v3::MasterKey;pub use v3::MasterKeys;pub use v3::ParseProtocolError;pub use v3::PersistedAuthoritativeEngine;pub use v3::PrivProtocol;pub use v3::UsmConfig;pub use v3::generate_engine_id;pub use value::RowStatus;pub use value::StorageType;pub use value::Value;pub use varbind::VarBind;pub use version::Version;
Modules§
- agent
- SNMP Agent (RFC 3413).
- ber
- BER (Basic Encoding Rules) codec for SNMP.
- cli
- CLI utilities for async-snmp.
- client
- SNMP client implementation.
- error
- Error types for async-snmp.
- format
- Formatting utilities for SNMP values.
- handler
- Handler types and traits for SNMP MIB operations.
- message
- SNMP message wrappers.
- mib_
support - MIB integration helpers for async-snmp.
- notification
- SNMP Notification Receiver (RFC 3413).
- oid
- Object Identifier (OID) type.
- pdu
- SNMP Protocol Data Units (PDUs).
- prelude
- Prelude module for convenient imports.
- transport
- Transport layer abstraction for SNMP communication.
- v3
SNMPv3security module.- value
- SNMP value types.
- varbind
- Variable binding (
VarBind) type. - version
- SNMP version enumeration.
Macros§
- oid
- Macro to create an OID at compile time.