Crate async_snmp

Crate async_snmp 

Source
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

§Quick Start

use async_snmp::{Auth, Client, oid};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<async_snmp::Error>> {
    // SNMPv2c client
    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(AuthProtocol::Sha256, "authpass123")
            .privacy(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(())
}

§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 (default behavior)
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 UdpTransport recommended
  • ~100,000s+ targets: Multiple UdpTransport instances, 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 dual-stack socket shared across all clients
    let transport = UdpTransport::bind("[::]:0")
        .await
        .expect("failed to bind");

    let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);

    // Create clients for each target
    let clients: Vec<_> = targets.iter()
        .map(|t| {
            Client::builder(*t, Auth::v2c("public"))
                .build_with(&transport)
        })
        .collect::<Result<_, _>>()
        .expect("failed to build clients");

    // 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 time

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")
    .with_privacy(PrivProtocol::Aes128, b"privpassword");

// 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").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)?;

    let result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await?;
    println!("{}: {:?}", target, result.value);
}
OptimizationWithoutWithSavings
MasterKeys850μs/engine1μs/engine~99.9%
EngineCache1 RTT/engine0 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

LevelWhat’s Logged
ERRORSocket errors, fatal transport failures
WARNAuth failures, parse errors, source address mismatches
INFOConnect/disconnect, walk completion
DEBUGRequest/response flow, engine discovery, retries
TRACEAuth verification, raw packet data

§Structured Fields

All fields use the snmp. prefix for easy filtering:

FieldDescription
snmp.targetTarget address for outgoing requests
snmp.sourceSource address of incoming messages
snmp.request_idSNMP request identifier
snmp.retriesCurrent retry attempt number
snmp.elapsed_msRequest duration in milliseconds
snmp.pdu_typePDU type (Get, GetNext, etc.)
snmp.varbind_countNumber of varbinds in request/response
snmp.error_statusSNMP error status from response
snmp.error_indexIndex of problematic varbind
snmp.non_repeatersGETBULK non-repeaters parameter
snmp.max_repetitionsGETBULK max-repetitions parameter
snmp.usernameSNMPv3 USM username
snmp.security_levelSNMPv3 security level
snmp.engine_idSNMPv3 engine identifier (hex)
snmp.local_addrLocal bind address

§Filtering by Target

Tracing targets follow a stable naming scheme (not tied to internal module paths):

Target PrefixWhat’s Included
async_snmpEverything
async_snmp::clientClient operations, requests, retries
async_snmp::agentAgent request/response handling
async_snmp::berBER encoding/decoding
async_snmp::v3SNMPv3 message processing
async_snmp::transportUDP/TCP transport layer
async_snmp::notificationTrap/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

ProblemSolution
GETBULK returns errors or garbageUse WalkMode::GetNext
OIDs returned out of orderUse OidOrdering::AllowNonIncreasing
Walk never terminatesSet ClientBuilder::max_walk_results
Slow responses cause timeoutsReduce 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

  • cli - Builds command-line utilities (asnmp-get, asnmp-walk, asnmp-set)
  • tls - (Placeholder) SNMP over TLS per RFC 6353
  • dtls - (Placeholder) SNMP over DTLS per RFC 6353

Re-exports§

pub use agent::Agent;
pub use agent::AgentBuilder;
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::UsmAuth;
pub use client::UsmBuilder;
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::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::UsmConfig;
pub use notification::UsmUserConfig;
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::AuthProtocol;
pub use v3::EngineCache;
pub use v3::LocalizedKey;
pub use v3::MasterKey;
pub use v3::MasterKeys;
pub use v3::ParseProtocolError;
pub use v3::PrivProtocol;
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.
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
SNMPv3 security module.
value
SNMP value types.
varbind
Variable binding (VarBind) type.
version
SNMP version enumeration.

Macros§

oid
Macro to create an OID at compile time.

Type Aliases§

TcpClient
Type alias for a client using a TCP connection.
UdpClient
Type alias for a client using UDP transport.