Skip to main content

Developer documentation

Build SNMP manager applications in Rust using the iReasoning binary SDK.

iReasoning SNMP SDK for Rust

Developer Guide and API Reference — SDK 1.0, ABI v1

This guide explains how a Rust application uses the binary iReasoning SNMP SDK. It covers the complete supported safe Rust API: SNMP GET, GET-NEXT, SET, GET-BULK, walk and subtree traversal, SNMP notification reception, and SMIv1/SMIv2 MIB parsing and lookup.

The SDK is distributed as compiled native libraries plus three small source crates:

  • ireasoning-snmp-sdk is the safe Rust API applications should use.
  • ireasoning-snmp-sdk-types contains the public data models re-exported by the safe crate.
  • ireasoning-snmp-sdk-sys connects the safe crate to the native static or dynamic library.

Applications do not compile or receive the proprietary SNMP and MIB implementation sources. Rust layouts never cross the binary boundary. The native library uses ABI-major-versioned C symbols and versioned JSON payloads internally, while ireasoning-snmp-sdk presents ordinary typed Rust functions.

1. Requirements and supported targets

  • Rust 1.85 or newer.
  • A target-specific SDK archive matching the final application target.
  • No JDK, JNI runtime, native cryptography package, or external ASN.1 library.
  • Tokio is required by the safe crate for the async façade. Blocking-only applications do not need to create a Tokio runtime.

Supported SDK targets are:

TargetStatic libraryDynamic library
x86_64-unknown-linux-gnu.a.so
aarch64-unknown-linux-gnu.a.so
x86_64-apple-darwin.a.dylib
aarch64-apple-darwin.a.dylib
x86_64-pc-windows-msvc.lib.dll

2. Installing the SDK

Unpack the SDK archive under a stable directory in the application repository, for example:

my-application/
├── Cargo.toml
├── src/
└── vendor/
    └── ireasoning-snmp-sdk/
        ├── lib/
        ├── include/
        ├── docs/
        └── rust/
            └── crates/
                ├── sdk/
                ├── sdk-sys/
                └── sdk-types/

Add the safe crate to the application:

[dependencies]
ireasoning-snmp-sdk = {
    path = "vendor/ireasoning-snmp-sdk/rust/crates/sdk"
}

Static linking is the distribution default. To link the dynamic library instead:

[dependencies]
ireasoning-snmp-sdk = {
    path = "vendor/ireasoning-snmp-sdk/rust/crates/sdk",
    default-features = false,
    features = ["dynamic"]
}

The packaged sdk-sys crate normally finds the native libraries in its adjacent native directory. If the libraries are installed elsewhere, set IREASONING_SNMP_SDK_DIR while building:

IREASONING_SNMP_SDK_DIR=/opt/ireasoning-snmp-sdk/lib cargo build --release

For dynamic linking, also make the shared library available to the operating-system loader at deployment time. Typical mechanisms are LD_LIBRARY_PATH or an rpath on Linux, DYLD_LIBRARY_PATH or an application-bundle location on macOS, and PATH or the executable directory on Windows.

Do not enable static and dynamic together. Use one target archive and one linkage mode for a final executable.

Applications that use the async examples should also declare Tokio directly so their own crate can use the runtime macro:

[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Generated Rust API documentation is included at docs/ireasoning_snmp_sdk/index.html in each archive. This guide explains behavior and integration; the generated pages are useful for type-by-type navigation in an editor or browser.

3. First program

The following program performs an SNMPv2c GET:

use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, Oid, Result,
};

fn main() -> Result<()> {
    let config = ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    );
    let mut client = BlockingClient::connect(config)?;

    let oid = ".1.3.6.1.2.1.1.3.0".parse::<Oid>().unwrap();
    let response = client.get(&[oid])?;

    for varbind in response.pdu.varbinds {
        println!("{} = {:?}", varbind.oid, varbind.value);
    }
    Ok(())
}

The ? operator works because all operational functions return the SDK alias Result<T>, which is std::result::Result<T, SdkError>.

4. ABI and build information

build_info

pub fn build_info() -> Result<BuildInfo>

Validates the linked native ABI and JSON schema, then returns the native build identity. The safe bindings perform this check automatically before creating or using a native handle, so calling it explicitly is optional but useful for startup diagnostics.

BuildInfo fields are:

FieldMeaning
sdk_version: StringNative SDK semantic version.
abi_major: u32Native ABI major version. Version 1.0 uses 1.
json_schema_version: u32Complex-payload schema version. Version 1.0 uses 1.
target: StringArchitecture and operating-system identity compiled into the library.
profile: StringNative build profile, normally release.

JSON_SCHEMA_VERSION is a public constant containing the schema version expected by the Rust bindings.

Example:

use ireasoning_snmp_sdk::{build_info, Result};

fn main() -> Result<()> {
    let info = build_info()?;
    println!(
        "SDK {} / ABI {} / schema {} / {}",
        info.sdk_version,
        info.abi_major,
        info.json_schema_version,
        info.target,
    );
    Ok(())
}

An incompatible library produces ErrorKind::IncompatibleAbi before an operational handle is created.

5. Error handling

Result<T>

pub type Result<T> = std::result::Result<T, SdkError>;

SdkError

FieldMeaning
kind: ErrorKindStable category suitable for program control flow.
message: StringHuman-readable, credential-redacted description.
status: Option<i32>SNMP Response-PDU error status when kind is SnmpResponse.
index: Option<i32>One-based offending varbind index supplied by the agent, when available.

SdkError implements std::error::Error and Display. Its Debug and Display output do not include communities, passwords, localized keys, or privacy salts.

ErrorKind values are:

KindTypical cause
ArgumentInvalid OID, empty input, zero receive timeout, or malformed binding data.
ConfigurationUnsupported version/operation combination, invalid security configuration, limit violation, or oversized outbound message.
TimeoutNo matching response or notification before the timeout.
TransportDNS, socket, TCP framing, worker, or other I/O failure.
SnmpResponseAgent Response-PDU error, malformed SNMP input, or protocol mismatch.
UsmSNMPv3 report, authentication, privacy, timeliness, or notification security failure.
MibMIB I/O, parsing, import, ambiguity, or resolution failure.
IncompatibleAbiNative ABI or JSON schema is incompatible with the bindings.
InvalidHandleA stale or wrong-type native handle was detected. This normally indicates an SDK integration defect.
CallbackA traversal callback failed or panicked.
InternalPanicA panic or invariant failure was contained inside the SDK boundary.

Response errors can be handled as follows:

use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, ErrorKind, ErrorStatus,
    Oid, Result,
};

fn read_value(client: &mut BlockingClient, oid: Oid) -> Result<()> {
    match client.get(&[oid]) {
        Ok(response) => println!("{:?}", response.pdu.varbinds),
        Err(error) if error.kind == ErrorKind::SnmpResponse => {
            if let Some(code) = error.status {
                eprintln!(
                    "agent error {:?} at varbind {:?}",
                    ErrorStatus::from_code(code),
                    error.index,
                );
            } else {
                eprintln!("protocol error: {error}");
            }
        }
        Err(error) => return Err(error),
    }
    Ok(())
}
# let _ = (ClientConfig::new("localhost", Credentials::v2c("public")), read_value);

Error helper functions

SdkError::new(kind: ErrorKind, message: impl Into<String>) -> SdkError

Creates an SDK-style error without an SNMP response status or index. This is useful when an application callback needs to translate its own failure into the SDK error model.

SdkError::response(
    message: impl Into<String>,
    status: i32,
    index: i32,
) -> SdkError

Creates an ErrorKind::SnmpResponse error with a numeric Response-PDU status and one-based error index.

ErrorStatus

ErrorStatus represents all standard SNMPv1/SNMPv2 response statuses: NoError, TooBig, NoSuchName, BadValue, ReadOnly, GenErr, NoAccess, WrongType, WrongLength, WrongEncoding, WrongValue, NoCreation, InconsistentValue, ResourceUnavailable, CommitFailed, UndoFailed, AuthorizationError, NotWritable, and InconsistentName. Unrecognized numbers are retained as Unknown(i32).

ErrorStatus::from_code(code: i32) -> ErrorStatus

Maps a wire error-status number to the symbolic enum.

status.code() -> i32

Returns the wire number, preserving the value inside Unknown.

6. OIDs, values, varbinds, PDUs, and responses

Oid

Oid is an owned vector of unsigned arcs. It implements FromStr, Display, ordering, equality, hashing, cloning, and Serde serialization.

Oid::new(arcs: impl Into<Vec<u32>>) -> std::result::Result<Oid, OidParseError>

Constructs an OID from arcs. The first arc must be 0, 1, or 2; when the first arc is 0 or 1, the second must be at most 39.

oid.arcs() -> &[u32]

Returns a borrowed view of the numeric arcs.

oid.is_empty() -> bool

Returns whether the OID contains no arcs.

oid.starts_with(other: &Oid) -> bool

Tests whether other is an OID prefix of oid. This is useful for subtree checks.

oid.appended(arcs: impl IntoIterator<Item = u32>) -> Oid

Returns a cloned OID with additional arcs. The original is unchanged.

Numeric strings with or without the leading dot can be parsed:

use ireasoning_snmp_sdk::Oid;

let first: Oid = ".1.3.6.1.2.1.1.3.0".parse().unwrap();
let second: Oid = "1.3.6.1.2.1.1.5.0".parse().unwrap();
assert_eq!(first.to_string(), ".1.3.6.1.2.1.1.3.0");
assert!(second.starts_with(&".1.3.6.1.2.1".parse().unwrap()));

Symbolic strings such as sysUpTime.0 are resolved by MibRegistry::resolve_oid, not by Oid::from_str.

Value

Value preserves the SNMP syntax received from or sent to an agent:

VariantRust payloadUse
Integeri32ASN.1 INTEGER and enumerations.
OctetStringVec<u8>OCTET STRING, display strings, and BITS.
NullnoneRetrieval request placeholder; not legal for SET.
ObjectIdentifierOidOBJECT IDENTIFIER.
IpAddress[u8; 4]SNMP IPv4 application value.
Counter32u32Counter32.
Gauge32u32Gauge32 or Unsigned32 wire value.
TimeTicksu32Hundredths of a second.
OpaqueVec<u8>Opaque application bytes.
Counter64u64Counter64; not legal in an SNMPv1 SET request.
NoSuchObjectnoneSNMPv2 exception response.
NoSuchInstancenoneSNMPv2 exception response.
EndOfMibViewnoneSNMPv2 traversal termination response.
Unknown{ tag: u8, bytes: Vec<u8> }Preserved unknown tag and raw content. Not legal for SET.

Only Integer, OctetString, ObjectIdentifier, IpAddress, Counter32, Gauge32, TimeTicks, Opaque, and Counter64 are accepted by set. Null, exception values, and Unknown are rejected locally before transmission.

VarBind

pub struct VarBind {
    pub oid: Oid,
    pub value: Value,
}
VarBind::new(oid: Oid, value: Value) -> VarBind

Constructs one variable binding. SET preserves the order of the supplied slice, including duplicate OIDs.

Pdu, PduType, and Response

pub struct Response {
    pub version: SnmpVersion,
    pub pdu: Pdu,
}

Pdu fields are:

FieldMeaning
pdu_type: PduTypeGet, GetNext, Set, GetBulk, Response, Inform, V2Trap, or Report. Successful manager operations normally return Response.
request_id: i32Correlation identifier generated by the native client.
error_status: i32Numeric agent error status. Nonzero response statuses are normally returned as SdkError, rather than as a successful Response.
error_index: i32Agent-supplied one-based offending varbind index.
varbinds: Vec<VarBind>Returned variable bindings in wire order.
pdu.response_status() -> ErrorStatus

Maps pdu.error_status to ErrorStatus.

7. Client configuration and credentials

ClientConfig

ClientConfig::new(
    host: impl Into<String>,
    credentials: Credentials,
) -> ClientConfig

Creates a configuration with port 161, UDP, a 5,000 ms timeout, zero retries, and a 32 KiB maximum message size.

All configuration fields are public:

FieldDefaultMeaning
host: StringsuppliedDNS name or numeric address. It must not be empty. For IPv6, pass the address without URL brackets.
port: u16161Remote SNMP port.
credentials: CredentialssuppliedSNMP version and community or USM credentials.
transport: TransportUdpTransport::Udp or Transport::Tcp. TCP uses BER-length message framing.
timeout_ms: u645000Timeout for each transport attempt in milliseconds. It must be greater than zero.
retries: u320Additional attempts after the first timeout. Retries reuse the same encoded request and request ID.
max_message_size: usize32768Maximum inbound and outbound SNMP message size. It must be at least 484 bytes.

Example customization:

use ireasoning_snmp_sdk::{
    ClientConfig, Credentials, Transport,
};

let mut config = ClientConfig::new(
    "192.0.2.10",
    Credentials::v2c("monitoring"),
);
config.port = 1161;
config.transport = Transport::Tcp;
config.timeout_ms = 3_000;
config.retries = 2;
config.max_message_size = 128 * 1024;

SNMPv1 and SNMPv2c

Credentials::v1(community: impl Into<String>) -> Credentials
Credentials::v2c(community: impl Into<String>) -> Credentials

These constructors select the version and store the supplied community. Communities longer than 255 bytes are rejected. Credential Debug output is redacted, and owned community storage is zeroized when dropped.

GET-BULK and bulk traversal are not supported by SNMPv1. The library returns ErrorKind::Configuration; unlike the command-line tools, it does not silently upgrade a reusable library request to SNMPv2c.

The related enums are:

  • SnmpVersion::{V1, V2c, V3} identifies the wire protocol version.
  • Transport::{Udp, Tcp} selects the manager transport.
  • Credentials::Community { version, community } is the serialized v1/v2c form created by v1 and v2c.
  • Credentials::V3(UsmCredentials) is the serialized SNMPv3 form created by v3.

Prefer the credential constructors so the version and credential form cannot be accidentally mismatched.

SNMPv3 USM

UsmCredentials::no_auth(username: impl Into<String>) -> UsmCredentials

Creates noAuthNoPriv credentials.

UsmCredentials::authenticated(
    username: impl Into<String>,
    protocol: AuthProtocol,
    password: impl Into<String>,
) -> UsmCredentials

Creates authNoPriv credentials. protocol may be Md5, Sha1, Sha224, Sha256, Sha384, or Sha512.

credentials.with_privacy(
    protocol: PrivacyProtocol,
    password: impl Into<String>,
) -> UsmCredentials

Adds privacy and produces authPriv credentials. protocol may be Des, TripleDes, Aes128, Aes192, or Aes256. Privacy requires authentication.

credentials.with_context(
    context_name: impl Into<String>,
) -> UsmCredentials

Selects an SNMP context name. The default is the empty context.

credentials.with_context_engine_id(
    engine_id: Vec<u8>,
) -> UsmCredentials

Overrides the scoped-PDU context engine ID. When absent, the discovered authoritative engine ID is normally used.

Credentials::v3(credentials: UsmCredentials) -> Credentials

Wraps USM credentials for ClientConfig.

The UsmCredentials fields are public for serialization and advanced configuration:

FieldMeaning
usernameUSM security name; it must contain 1–32 bytes.
auth_protocol / auth_passwordBoth must be present or both absent. Passwords must not be empty.
privacy_protocol / privacy_passwordBoth must be present or both absent. Privacy additionally requires authentication.
context_nameScoped-PDU context name.
context_engine_idOptional explicit scoped-PDU engine ID.
key_expansion_with_engine_idJava-compatible AES-192/AES-256 privacy-key expansion selection. The default is true.

Prefer the constructors and builder methods over editing protocol/password option pairs manually.

Example authPriv configuration:

use ireasoning_snmp_sdk::{
    AuthProtocol, ClientConfig, Credentials, PrivacyProtocol,
    UsmCredentials,
};

let username = "monitor";
let auth_password = std::env::var("SNMP_AUTH_PASSWORD").unwrap();
let privacy_password = std::env::var("SNMP_PRIV_PASSWORD").unwrap();

let usm = UsmCredentials::authenticated(
    username,
    AuthProtocol::Sha256,
    auth_password,
)
.with_privacy(PrivacyProtocol::Aes256, privacy_password)
.with_context("production");

let config = ClientConfig::new(
    "router.example.com",
    Credentials::v3(usm),
);
# let _ = config;

The client performs discovery, password-to-key localization, engine boots/time tracking, authentication, encryption, and one bounded report-driven resynchronization automatically.

SecurityLevel::{NoAuthNoPriv, AuthNoPriv, AuthPriv} describes the effective security of received v3 notifications. Client security level is derived from which authentication and privacy settings are present rather than supplied as a separate parameter.

8. Blocking SNMP client API

BlockingClient owns a native client and a dedicated background Tokio runtime. It is safe to use in a conventional synchronous program and inside a process that already has another Tokio runtime.

Operations take &mut self, preventing concurrent or reentrant use of one handle. Create separate clients when genuinely parallel independent manager requests are needed.

BlockingClient::connect

pub fn connect(config: ClientConfig) -> Result<BlockingClient>

Validates the configuration, resolves/connects the transport, and creates the client. For SNMPv3, authoritative-engine discovery occurs on the first operation rather than necessarily during connect.

BlockingClient::get

pub fn get(&mut self, oids: &[Oid]) -> Result<Response>
  • oids is a nonempty ordered slice of object-instance OIDs.
  • One GET request contains all supplied OIDs.
  • The returned varbind order is the response wire order.
  • A nonzero agent error status is returned as SdkError with kind == SnmpResponse.

BlockingClient::get_next

pub fn get_next(&mut self, oids: &[Oid]) -> Result<Response>
  • oids is a nonempty slice of starting OIDs.
  • The agent is asked for the lexicographic successor of each OID.
  • This performs one GET-NEXT request; it does not automatically walk.

BlockingClient::set

pub fn set(&mut self, varbinds: &[VarBind]) -> Result<Response>
  • varbinds must contain at least one assignment.
  • The entire slice is sent in one atomic protocol request.
  • Assignment order and duplicate OIDs are preserved.
  • Values must be SET-compatible variants listed in the Value section.
  • Counter64 is rejected under SNMPv1 before any packet is sent.
  • Access, ranges, sizes, and cross-object consistency are authoritatively checked by the agent, not by local MIB metadata.

Example with multiple assignments:

use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, Oid, Result,
    Value, VarBind,
};

fn update(client: &mut BlockingClient) -> Result<()> {
    let assignments = vec![
        VarBind::new(
            ".1.3.6.1.2.1.1.4.0".parse::<Oid>().unwrap(),
            Value::OctetString(b"[email protected]".to_vec()),
        ),
        VarBind::new(
            ".1.3.6.1.2.1.1.6.0".parse::<Oid>().unwrap(),
            Value::OctetString(b"rack-7".to_vec()),
        ),
    ];
    let response = client.set(&assignments)?;
    println!("agent returned {} assignments", response.pdu.varbinds.len());
    Ok(())
}
# let _ = (ClientConfig::new("localhost", Credentials::v2c("private")), update);

BulkOptions

BulkOptions::new(
    non_repeaters: u32,
    max_repetitions: u32,
) -> BulkOptions
  • non_repeaters is the number of leading OIDs for which the agent should return a single successor.
  • max_repetitions is the requested repetition count for the remaining OIDs.
  • Both must fit a nonnegative signed 32-bit wire integer.
  • BulkOptions::default() is { non_repeaters: 0, max_repetitions: 50 }.

BlockingClient::get_bulk

pub fn get_bulk(
    &mut self,
    oids: &[Oid],
    options: BulkOptions,
) -> Result<Response>
  • oids must be nonempty.
  • options controls non-repeaters and repetitions.
  • SNMPv1 is rejected locally.
  • The response may contain fewer varbinds than requested because of agent limits, message size, or end-of-MIB values.

Example:

use ireasoning_snmp_sdk::{BlockingClient, BulkOptions, Oid, Result};

fn bulk(client: &mut BlockingClient) -> Result<()> {
    let sys_up_time = ".1.3.6.1.2.1.1.3".parse::<Oid>().unwrap();
    let if_descr = ".1.3.6.1.2.1.2.2.1.2".parse::<Oid>().unwrap();
    let response = client.get_bulk(
        &[sys_up_time, if_descr],
        BulkOptions::new(1, 20),
    )?;
    for varbind in response.pdu.varbinds {
        println!("{} = {:?}", varbind.oid, varbind.value);
    }
    Ok(())
}

9. Walk and subtree traversal

Traversal starts after the supplied root OID.

  • A walk continues lexicographically until end-of-MIB.
  • A subtree traversal stops before delivering the first OID outside root.
  • Exception varbinds are not included in results.
  • Duplicate or decreasing OIDs are rejected to prevent infinite loops.
  • SNMPv1 noSuchName and SNMPv2 exception values terminate traversal.

TraversalOptions

FieldDefaultMeaning
mode: TraversalModeGetNextGetNext or GetBulk { max_repetitions }.
max_requests: usize10_000Maximum request/response rounds. Must be greater than zero.
max_varbinds: usize1_000_000Maximum accepted result varbinds. Must be greater than zero.
TraversalOptions::default() -> TraversalOptions

Creates GET-NEXT traversal with the default safety limits.

TraversalOptions::bulk(max_repetitions: u32) -> TraversalOptions

Creates GET-BULK traversal with zero non-repeaters, the supplied repetition count, and the default safety limits. Bulk traversal is unavailable under SNMPv1.

Accumulating traversal

pub fn walk(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>

Returns all delivered varbinds in TraversalResult::varbinds plus a summary. Use it when the expected result comfortably fits memory.

pub fn get_subtree(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>

Works like walk, but excludes the first result outside root and then stops.

Streaming traversal

pub fn walk_with<F>(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()>;
pub fn get_subtree_with<F>(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()>;

Callback parameters and rules:

  • The callback receives one borrowed result batch at a time.
  • The batch is valid only for the callback invocation. Clone any data that must outlive it.
  • Return ControlFlow::Continue(()) to request another batch.
  • Return ControlFlow::Break(()) to stop successfully.
  • Do not call another method on the same client from the callback. Reentrant native-handle use is rejected.
  • A callback panic is contained and returned as ErrorKind::Callback.

Example streaming subtree:

use ireasoning_snmp_sdk::{
    BlockingClient, Oid, Result, TraversalOptions,
};
use std::ops::ControlFlow;

fn stream_interfaces(client: &mut BlockingClient) -> Result<()> {
    let root = ".1.3.6.1.2.1.2.2.1".parse::<Oid>().unwrap();
    let summary = client.get_subtree_with(
        &root,
        TraversalOptions::bulk(50),
        |batch| {
            for varbind in batch {
                println!("{} = {:?}", varbind.oid, varbind.value);
            }
            ControlFlow::Continue(())
        },
    )?;
    println!("{summary:?}");
    Ok(())
}

Traversal result models

TraversalResult has varbinds: Vec<VarBind> and summary: TraversalSummary.

TraversalSummary has:

  • requests: completed request count.
  • varbinds: accepted varbind count.
  • termination: TraversalTermination: one of EndOfMibView, OutsideSubtree, EmptyResponse, or CallbackStopped.

10. Async SNMP client API

AsyncClient uses bounded blocking jobs around the synchronous native ABI. It does not run a nested Tokio runtime in the calling async task. Cloning an AsyncClient shares the same underlying session, engine state, and request sequence; operations on those clones are serialized.

Cancellation is cooperative. Dropping a future does not interrupt native code mid-packet; the active attempt completes at the configured transport timeout.

Async functions

pub async fn AsyncClient::connect(
    config: ClientConfig,
) -> Result<AsyncClient>

Creates the client on Tokio's blocking pool.

pub async fn get(&self, oids: &[Oid]) -> Result<Response>
pub async fn get_next(&self, oids: &[Oid]) -> Result<Response>
pub async fn set(&self, varbinds: &[VarBind]) -> Result<Response>
pub async fn get_bulk(
    &self,
    oids: &[Oid],
    options: BulkOptions,
) -> Result<Response>

Parameters and protocol semantics are identical to the corresponding blocking functions. Input slices are cloned before the blocking job begins.

pub async fn walk(
    &self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>

pub async fn get_subtree(
    &self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>

These are the accumulating async traversal forms.

pub async fn walk_with<F>(
    &self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()> + Send + 'static;

pub async fn get_subtree_with<F>(
    &self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()> + Send + 'static;

The callback runs on a blocking worker thread, not necessarily the async runtime worker that awaited the function. Captured state must therefore be Send + 'static. Use a channel if batches must be forwarded into another async task.

Async example:

use ireasoning_snmp_sdk::{
    AsyncClient, ClientConfig, Credentials, Oid, Result,
};

#[tokio::main]
async fn main() -> Result<()> {
    let client = AsyncClient::connect(ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    ))
    .await?;

    let oids = [
        ".1.3.6.1.2.1.1.1.0".parse::<Oid>().unwrap(),
        ".1.3.6.1.2.1.1.3.0".parse::<Oid>().unwrap(),
    ];
    let response = client.get(&oids).await?;
    println!("{:?}", response.pdu.varbinds);
    Ok(())
}

11. Trap and INFORM receiver configuration

The receiver listens on UDP and supports SNMPv1 traps, SNMPv2c traps and INFORMs, and SNMPv3 traps and INFORMs. Valid INFORMs are acknowledged before their Notification is returned to the application.

TrapReceiverConfig::new

TrapReceiverConfig::new(
    bind_address: IpAddr,
    port: u16,
) -> TrapReceiverConfig

Creates receiver configuration with a 128 KiB message limit, 256 cached engine entries, no community restriction, no USM users, no authoritative receiver engine, and a 5,000 ms default receive timeout.

Use port 0 when tests need an automatically assigned local port. Port 162 may require elevated privileges on Unix-like systems.

Configuration fields are:

FieldMeaning
bind_address: IpAddrLocal IPv4 or IPv6 interface address. Use an unspecified address for all interfaces.
port: u16Local UDP port.
max_message_size: usizeMaximum datagram size; it must be at least 484 bytes.
max_engine_cache_entries: usizeBound for SNMPv3 engine timeliness and localized-key caches; it must be greater than zero.
accepted_communities: Vec<Vec<u8>>Optional exact community allowlist. Empty means accept every v1/v2c community. Values are compared in constant time and redacted from output.
users: Vec<ReceiverUsmUser>Configured SNMPv3 users. Multiple users and protocol combinations are allowed.
authoritative_engine: Option<AuthoritativeEngine>Receiver engine used for SNMPv3 INFORM discovery, REPORTs, timeliness, and acknowledgements.
receive_timeout_ms: u64Bounded wait used by recv; use a value greater than zero.
config.socket_addr() -> SocketAddr

Combines bind_address and port. Before binding, a zero port remains zero; call receiver.local_addr() after binding to discover the assigned port.

ReceiverUsmUser

pub struct ReceiverUsmUser {
    pub credentials: UsmCredentials,
    pub authoritative_engine_id: Option<Vec<u8>>,
}
  • credentials supplies username, authentication, privacy, and Java-compatible key expansion.
  • authoritative_engine_id optionally restricts this user entry to one sender engine. Leave it None to accept matching messages from any valid engine, subject to the receiver's timeliness tracking.

AuthoritativeEngine

pub struct AuthoritativeEngine {
    pub engine_id: Vec<u8>,
    pub engine_boots: u32,
}
  • engine_id must contain 5–32 octets and should be stable across restarts.
  • engine_boots must not exceed i32::MAX. Persist it and increment it before each receiver restart.
  • Engine time is derived from elapsed process time after binding.

The SDK does not persist receiver engine state itself; applications or the snmptrapd CLI are responsible for durable engine ID and boots storage.

SNMPv3 receiver example:

use ireasoning_snmp_sdk::{
    AuthProtocol, AuthoritativeEngine, PrivacyProtocol,
    ReceiverUsmUser, TrapReceiverConfig, UsmCredentials,
};
use std::net::{IpAddr, Ipv4Addr};

let user = ReceiverUsmUser {
    credentials: UsmCredentials::authenticated(
        "trap-user",
        AuthProtocol::Sha256,
        std::env::var("SNMP_AUTH_PASSWORD").unwrap(),
    )
    .with_privacy(
        PrivacyProtocol::Aes128,
        std::env::var("SNMP_PRIV_PASSWORD").unwrap(),
    ),
    authoritative_engine_id: None,
};

let mut config = TrapReceiverConfig::new(
    IpAddr::V4(Ipv4Addr::UNSPECIFIED),
    9162,
);
config.users.push(user);
config.authoritative_engine = Some(AuthoritativeEngine {
    engine_id: vec![0x80, 0x00, 0x3b, 0x29, 1, 2, 3, 4, 5],
    engine_boots: 1,
});

12. Blocking trap receiver API

BlockingTrapReceiver::bind

pub fn bind(
    config: TrapReceiverConfig,
) -> Result<BlockingTrapReceiver>

Validates configuration, binds UDP, and starts the receiver's dedicated runtime thread.

BlockingTrapReceiver::local_addr

pub fn local_addr(&self) -> Result<SocketAddr>

Returns the actual bound address. This is especially useful when configured with port 0.

BlockingTrapReceiver::recv

pub fn recv(&mut self) -> Result<Notification>

Waits up to TrapReceiverConfig::receive_timeout_ms for one valid notification. A timeout produces ErrorKind::Timeout. A malformed or unauthenticated datagram may produce a protocol/USM error for this single receive call.

BlockingTrapReceiver::recv_timeout

pub fn recv_timeout(
    &mut self,
    timeout: Duration,
) -> Result<Notification>

Uses an explicit timeout instead of the configured default. timeout must be greater than zero; sub-millisecond positive durations are rounded up to one millisecond at the ABI boundary.

BlockingTrapReceiver::run

pub fn run<F>(&mut self, callback: F) -> Result<()>
where
    F: FnMut(Notification) -> ControlFlow<()>;

Repeatedly receives notifications. Timeout, malformed notification, and USM rejection errors are treated as recoverable and the loop continues. Transport, configuration, ABI, and internal failures stop the loop.

The callback owns each Notification. Return Continue(()) to keep receiving or Break(()) to stop successfully.

Example:

use ireasoning_snmp_sdk::{
    BlockingTrapReceiver, Result, TrapReceiverConfig,
};
use std::net::{IpAddr, Ipv4Addr};
use std::ops::ControlFlow;

fn main() -> Result<()> {
    let mut config = TrapReceiverConfig::new(
        IpAddr::V4(Ipv4Addr::UNSPECIFIED),
        9162,
    );
    config.accepted_communities.push(b"public".to_vec());

    let mut receiver = BlockingTrapReceiver::bind(config)?;
    println!("listening on {}", receiver.local_addr()?);

    receiver.run(|notification| {
        println!(
            "{} {:?} oid={:?} uptime={:?}",
            notification.source,
            notification.kind,
            notification.notification_oid(),
            notification.uptime(),
        );
        ControlFlow::Continue(())
    })
}

13. Async trap receiver API

AsyncTrapReceiver is cloneable. Clones share one underlying socket and serialize receive operations. Avoid awaiting multiple concurrent recv calls on clones when notification ownership order matters.

pub async fn AsyncTrapReceiver::bind(
    config: TrapReceiverConfig,
) -> Result<AsyncTrapReceiver>

Binds on Tokio's blocking pool.

pub fn local_addr(&self) -> Result<SocketAddr>

Returns the bound address synchronously; it performs no network wait.

pub async fn recv(&self) -> Result<Notification>

Receives with the configured bounded timeout.

pub async fn recv_timeout(
    &self,
    timeout: Duration,
) -> Result<Notification>

Receives with an explicit bounded timeout.

pub async fn run<F>(&self, callback: F) -> Result<()>
where
    F: FnMut(Notification) -> ControlFlow<()> + Send + 'static;

Runs the recoverable receive loop. The callback executes in the async task after each blocking receive job completes. It must be Send + 'static because the async future may move between runtime threads.

Cancellation is bounded by the active receive timeout.

14. Notification models

Notification fields are:

FieldMeaning
source: SocketAddrSender address.
local_address: SocketAddrReceiver address that accepted the datagram.
version: SnmpVersionV1, V2c, or V3.
kind: NotificationKindV1Trap, V2Trap, or Inform.
security: NotificationSecurityRedacted community marker or USM identity/security metadata.
context_engine_id: Vec<u8>SNMPv3 scoped context engine ID; empty for community notifications.
context_name: Vec<u8>Raw context-name octets; empty for community notifications.
request_id: Option<i32>Request ID for v2/v3 notifications; absent for the distinct v1 Trap-PDU.
varbinds: Vec<VarBind>Notification variable bindings.
v1_trap: Option<V1Trap>SNMPv1-specific Trap-PDU fields.

NotificationSecurity is either:

  • Community, which intentionally does not expose the received community; or
  • Usm { username, authoritative_engine_id, security_level }, where security_level is NoAuthNoPriv, AuthNoPriv, or AuthPriv.
notification.notification_oid() -> Option<Oid>

Returns the normalized notification OID:

  • For v2/v3, it reads snmpTrapOID.0.
  • For a generic v1 trap, it returns the corresponding standard notification OID.
  • For an enterprise-specific v1 trap, it returns enterprise.0.specific-trap.
  • It returns None when the notification cannot be normalized.
notification.uptime() -> Option<u32>

Returns v1 Trap-PDU timestamp or the v2/v3 sysUpTime.0 TimeTicks value.

V1Trap contains enterprise, four-octet agent_address, generic_trap, specific_trap, and timestamp. GenericTrap distinguishes the six standard generic types, EnterpriseSpecific, and preserved Unknown(i32) values.

15. MIB parsing concepts

Use MibParser for a standalone parse and MibRegistry when imports, multiple modules, symbolic resolution, formatting, merged trees, or module lifecycle are needed.

Every registry is independent. Loading or clearing one registry has no effect on another registry in the same process.

MIB input encoding is detected as follows:

  • UTF-8 BOM selects UTF-8.
  • UTF-16LE/UTF-16BE BOMs are recognized.
  • Java-compatible zero-byte patterns detect unmarked UTF-16.
  • Other unmarked bytes are interpreted as ISO-8859-1.

ParseOptions

FieldDefaultMeaning
strict: booltrueStrict mode returns typed errors for unresolved/cyclic imports and required parse failures. Lenient mode keeps placeholders and diagnostics when possible.
depth: ParseDepthFullModuleName, Imports, or Full.
resolve_syntax: boolfalseResolve derived/textual-convention syntax into basic syntax metadata where possible.

ParseDepth::ModuleName parses only enough to identify the module. Imports also records imports/exports. Full builds metadata, textual conventions, traps, and the OID tree.

16. MibParser API

Construction

MibParser::new(options: ParseOptions) -> MibParser
MibParser::default() -> MibParser

default uses ParseOptions::default().

parser.options() -> ParseOptions

Returns the parser's copied options.

Parsing functions

parser.parse_path(
    path: impl AsRef<Path>,
) -> Result<MibModule>

Reads and parses one filesystem path. MibError details are mapped to ErrorKind::Mib.

parser.parse_reader(
    source_name: &str,
    reader: impl Read,
) -> Result<MibModule>
  • source_name is recorded in diagnostics and the returned module.
  • reader is consumed until EOF and may be a file, cursor, socket-derived buffer, or other synchronous reader.
parser.parse_bytes(
    source_name: &str,
    bytes: &[u8],
) -> Result<MibModule>

Parses an in-memory byte slice with encoding detection.

MibParser::module_name(
    source_name: &str,
    bytes: &[u8],
) -> Result<String>

Statically parses only the module header and returns the DEFINITIONS name. It is useful for indexing a MIB directory without a full parse.

Example:

use ireasoning_snmp_sdk::{
    MibParser, ParseDepth, ParseOptions, Result,
};

fn main() -> Result<()> {
    let parser = MibParser::new(ParseOptions {
        strict: false,
        depth: ParseDepth::Full,
        resolve_syntax: true,
    });
    let module = parser.parse_path("mibs/IF-MIB")?;
    println!("{}: {} nodes", module.name, module.tree.nodes.len());
    for diagnostic in module.diagnostics {
        eprintln!(
            "{}:{}:{}: {:?}: {}",
            diagnostic.source,
            diagnostic.line,
            diagnostic.column,
            diagnostic.severity,
            diagnostic.message,
        );
    }
    Ok(())
}

17. MibRegistry API

Construction and directories

MibRegistry::new() -> Result<MibRegistry>

Creates an empty independent native registry. Prefer this fallible constructor.

MibRegistry::default() also exists, but it panics if the native ABI cannot be loaded or is incompatible; it is mainly convenient after SDK deployment has already been validated.

registry.add_search_path(
    path: impl AsRef<Path>,
) -> Result<()>

Canonicalizes and adds an existing file or directory used to locate imported modules. Directories are searched recursively, hidden entries are skipped, and candidate module names are read from their headers.

registry.search_paths() -> Result<Vec<PathBuf>>

Returns owned copies of the currently configured canonical search paths.

Loading modules

registry.load_path(
    path: impl AsRef<Path>,
    options: ParseOptions,
) -> Result<ModuleId>

Loads a filesystem MIB and resolves imports by loaded module name, the source file directory, and configured search paths. Loading the same canonical path or module name returns the existing stable ModuleId.

registry.load_reader(
    source_name: &str,
    reader: impl Read,
    options: ParseOptions,
) -> Result<ModuleId>

Reads an arbitrary synchronous reader and loads the resulting module. source_name is used for metadata and diagnostics.

registry.load_bytes(
    source_name: &str,
    bytes: &[u8],
    options: ParseOptions,
) -> Result<ModuleId>

Loads in-memory bytes. Import resolution uses already loaded modules and configured paths; an in-memory source has no independently discoverable filesystem directory unless source_name names a usable path.

registry.load_mib2() -> Result<ModuleId>

Loads the embedded RFC1213 MIB using lenient full parsing with syntax resolution. Repeated calls return the existing module ID.

Inspecting loaded state

registry.is_loaded_path(
    path: impl AsRef<Path>,
) -> Result<bool>

Returns whether the canonical path is loaded. A path that cannot be canonicalized returns false rather than a MIB error.

registry.is_any_loaded() -> Result<bool>

Returns whether at least one live module is loaded.

registry.module_id(name: &str) -> Result<Option<ModuleId>>

Looks up a module name case-insensitively.

registry.module(id: ModuleId) -> Result<MibModule>

Returns an owned module copy. An unloaded, stale, or out-of-range ID returns a MIB error.

registry.module_by_name(
    name: &str,
) -> Result<Option<MibModule>>

Combines name lookup and owned module retrieval.

registry.modules() -> Result<Vec<ModuleSummary>>

Returns deterministic summaries for live modules. Each summary contains id, name, source, metadata, imports, and module display hints.

registry.display_hints() -> Result<BTreeMap<String, String>>

Returns the merged textual-convention display-hint map for loaded modules.

Removing state

registry.unload_path(
    path: impl AsRef<Path>,
) -> Result<bool>

Unloads the module associated with a canonical path. Returns true if a live module was removed.

registry.unload(id: ModuleId) -> Result<bool>

Unloads a module by stable handle. Existing copies of MibModule remain usable, but the ID no longer addresses a live registry module.

registry.clear() -> Result<()>

Removes loaded modules, indexes, loading state, and merged display hints. Configured search paths remain available for subsequent loads.

Symbolic OID resolution

registry.resolve_oid(value: &str) -> Result<Oid>

Accepts:

  • numeric OIDs such as .1.3.6.1.2.1.1.3.0;
  • unqualified symbols such as sysUpTime.0 and ifDescr.2; or
  • qualified symbols such as IF-MIB::ifDescr.2.

Numeric suffix arcs are appended to the resolved base symbol. Ambiguous unqualified names return a MIB error rather than selecting an arbitrary module.

registry.resolve_oid_in_module(
    module: &str,
    value: &str,
) -> Result<Oid>

Resolves with a preferred module. Module-local definitions and explicit imports are considered before global candidates. module must identify a loaded module.

Formatting

registry.format_oid(
    oid: &Oid,
    full_name: bool,
) -> Result<Option<String>>

Uses the longest loaded OID prefix. If full_name is false, a result resembles sysUpTime.0; if true, it resembles RFC1213-MIB::sysUpTime.0. Returns None when no loaded node prefixes the OID.

registry.format_varbind(
    varbind: &VarBind,
    numeric_oid: bool,
) -> Result<FormattedVarBind>

Returns { name, value }:

  • numeric_oid == true forces numeric name output.
  • Otherwise the best symbolic name is used, falling back to numeric.
  • Integer enum metadata formats values such as up(1) when available.
  • Other values use their normal SNMP representation.

Trees and lookup

registry.merged_tree() -> Result<MibTree>

Merges every loaded module into a new owned OID tree.

registry.merge_modules(
    ids: &[ModuleId],
) -> Result<MibTree>

Merges only the listed live module IDs. Invalid IDs are skipped by the merge operation.

registry.lookup_oid(
    oid: &Oid,
) -> Result<Option<MibLookup>>

Returns the loaded module and node with the longest matching OID prefix. The returned MibLookup owns module: String and node: MibNode.

registry.lookup_name(
    name: &str,
) -> Result<Vec<MibLookup>>

Returns every case-insensitive name match. MODULE::symbol restricts the search to one module. Multiple results are retained instead of being treated as resolution ambiguity.

registry.lookup_trap_oid(
    oid: &Oid,
) -> Result<Vec<TrapLookup>>

Returns matching SMIv1 and SMIv2 trap definitions in deterministic module/name order. SMIv1 enterprise-specific traps are normalized as enterprise.0.number.

Complete registry example:

use ireasoning_snmp_sdk::{
    MibRegistry, ParseOptions, Result, Value, VarBind,
};

fn main() -> Result<()> {
    let mut registry = MibRegistry::new()?;
    registry.add_search_path("mibs")?;
    registry.load_mib2()?;
    registry.load_path(
        "mibs/IF-MIB",
        ParseOptions {
            strict: false,
            resolve_syntax: true,
            ..ParseOptions::default()
        },
    )?;

    let oid = registry.resolve_oid("IF-MIB::ifAdminStatus.1")?;
    let varbind = VarBind::new(oid.clone(), Value::Integer(1));
    let formatted = registry.format_varbind(&varbind, false)?;
    println!("{} = {}", formatted.name, formatted.value);

    if let Some(found) = registry.lookup_oid(&oid)? {
        println!("module={} node={}", found.module, found.node.name);
    }
    Ok(())
}

18. MIB result models

All MIB models are owned and Serde serializable/deserializable. They can outlive the registry call that produced them.

Handles and trees

  • ModuleId(pub usize) is a registry-local stable module handle. Do not use an ID with another registry.
  • NodeId(pub usize) indexes MibTree::nodes and is meaningful only within that tree.

MibTree has root: NodeId and nodes: Vec<MibNode>.

tree.node(id: NodeId) -> Option<&MibNode>

Returns a node by arena ID.

tree.find_name(name: &str) -> Option<NodeId>

Returns the first case-insensitive name match in tree order.

tree.find_oid(oid: &Oid) -> Option<NodeId>

Returns the exact OID match. For longest-prefix behavior across loaded modules, use MibRegistry::lookup_oid.

MibModule

FieldMeaning
nameModule definition name.
sourceFile path or caller-supplied source name.
metadataMODULE-IDENTITY metadata and revisions.
imports / exportsParsed import/export declarations.
treeOwned arena OID tree.
trapsSMIv1 TRAP-TYPE and SMIv2 notification metadata.
textual_conventionsName-to-Syntax map.
display_hintsParsed display hints.
diagnosticsTyped warnings/errors retained by lenient parsing.

ModuleMetadata contains optional identity, last_updated, organization, contact_info, and description, plus revisions: Vec<Revision>. A Revision contains timestamp and optional description.

An Import contains the imported module name and its symbols.

MibNode

Important node fields are:

FieldMeaning
id, parent, childrenArena traversal links.
name, full_name, oid, moduleSymbolic/numeric identity and owning module.
node_type: NodeTypeRoot, ObjectType, ObjectIdentity, Notification, NotificationGroup, ModuleIdentity, Compliance, ObjectGroup, Other, or Unknown.
description, access, status, units, default_valueParsed SMI clauses.
syntaxOptional resolved/derived syntax.
indexes, table_indexesEntry/table index definitions, including implied.
augments, augments_tableAugmentation metadata.
objects, object_oidsNotification/group object references and resolved OIDs.
is_tableTable classification.
row_status_oid, entry_statusRowStatus-related metadata.

Syntax contains type_name, optional derived_type, raw declaration, size/range text, enum values, optional display_hint, and optional BaseSyntax. Basic syntax values include ObjectIdentifier, OctetString, Integer, Unsigned32, Gauge32, Counter32, Counter64, TimeTicks, IpAddress, Bits, and Opaque.

An Index contains name and the implied flag.

Traps and diagnostics

Trap contains version, name, enterprise name/OID, optional v1 number, variable names/OIDs, description, and optional tree node. TrapVersion is V1 or V2.

Diagnostic contains:

  • severity: Severity: Warning or Error;
  • code: DiagnosticCode: Syntax, InvalidHeader, InvalidEncoding, UnresolvedImport, UnresolvedSymbol, AmbiguousSymbol, DuplicateOid, ImportCycle, or Io;
  • message, source, optional module/imported-module names; and
  • one-based line and column.

ModuleSummary, MibLookup, TrapLookup, and FormattedVarBind are the compact owned result types returned by registry inspection and formatting methods. RegistrySnapshot is a serializable aggregation model containing search paths, module summaries, and display hints.

RegistryFormatRequest is the serializable { varbind: VarBind, numeric_oid: bool } model used by schema-level integrations. Safe Rust applications normally call MibRegistry::format_varbind directly instead of constructing it.

19. Using MIB names with SNMP operations

The SNMP client accepts numeric Oid values. Resolve names through a registry before sending them:

use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, MibRegistry,
    Result,
};

fn main() -> Result<()> {
    let mut mibs = MibRegistry::new()?;
    mibs.load_mib2()?;

    let oid = mibs.resolve_oid("sysUpTime.0")?;
    let mut client = BlockingClient::connect(ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    ))?;
    let response = client.get(&[oid])?;

    for varbind in response.pdu.varbinds {
        let formatted = mibs.format_varbind(&varbind, false)?;
        println!("{} = {}", formatted.name, formatted.value);
    }
    Ok(())
}

For an OID-valued SET input, resolve the value OID separately and wrap it in Value::ObjectIdentifier.

20. Thread safety, ownership, and callbacks

  • Native objects are represented internally by synchronized, type-checked handles.
  • A BlockingClient or BlockingTrapReceiver operation requires mutable access.
  • AsyncClient and AsyncTrapReceiver clones share and serialize access to the same native object.
  • Create separate clients or receivers when independent parallel state is required.
  • Each MibRegistry owns independent module state.
  • Result models are owned Rust values; they do not borrow native memory.
  • Traversal callback slices are temporary and must not escape the callback without cloning.
  • Do not re-enter the same client from its traversal callback.
  • Native handle cleanup occurs automatically in Drop.
  • Completed network operations are stored in one owned native result payload, copied once, and freed. Buffer sizing cannot repeat SET or another network operation.

21. Security guidance

  • Obtain communities and passwords from a secret manager or protected environment, not source literals.
  • Credential-bearing types redact Debug output and zero owned secret fields when dropped.
  • Temporary serialized credential buffers in the safe façade are zeroized.
  • Native errors never include communities, passwords, localized keys, or salts.
  • Do not serialize credential configuration into logs or application telemetry.
  • An empty trap community allowlist intentionally accepts all v1/v2c communities. Configure an allowlist when community filtering is required.
  • Persist only SNMPv3 authoritative receiver engine ID and boots state. Do not store credentials in the engine-state file.
  • Treat unauthenticated traps as untrusted input even if their community passes a filter.

22. Deployment checklist

  1. Use the SDK archive for the application's exact target triple.
  2. Select exactly one of static or dynamic linkage.
  3. Call build_info() during startup diagnostics if early compatibility reporting is desired.
  4. Ensure dynamic libraries are installed where the platform loader can find them.
  5. Set realistic timeout_ms, retries, packet limits, and traversal limits.
  6. Store SNMP credentials outside source and logs.
  7. For SNMPv3 INFORM reception, persist a stable authoritative engine ID and increment engine boots before binding after each restart.
  8. Use nonprivileged receiver ports unless deployment explicitly grants access to UDP 162.
  9. Prefer streaming traversal for large device trees.
  10. Load MIBs before resolving or formatting symbolic OIDs.

23. Troubleshooting

IncompatibleAbi at startup

The Rust bindings and native library came from different SDK major/schema versions. Replace all SDK files as one unit; do not mix archives.

Native library not found while linking

Confirm the target archive and linkage feature, or set IREASONING_SNMP_SDK_DIR to the directory containing the native files.

Dynamic library not found at runtime

The application linked successfully, but the operating-system loader cannot locate the .so, .dylib, or .dll. Install it beside the executable or configure the platform's loader path/rpath.

GET-BULK fails with SNMPv1

GET-BULK is an SNMPv2 operation. Use Credentials::v2c or SNMPv3.

Symbolic OID is not found

Load the defining MIB and its imports into the same MibRegistry, configure search paths, and use MODULE::symbol when duplicate names exist.

Trap receiver repeatedly times out

Verify the bound address/port using local_addr, firewall and routing rules, sender destination, and receive_timeout_ms. A timeout is normal when no notification arrives during one bounded receive window.

SNMPv3 authentication/privacy error

Verify username, protocol selection, password, sender engine restriction, authoritative receiver engine configuration for INFORMs, engine boots persistence, clock/timeliness behavior, and Java-compatible AES key-expansion selection.