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-sdkis the safe Rust API applications should use.ireasoning-snmp-sdk-typescontains the public data models re-exported by the safe crate.ireasoning-snmp-sdk-sysconnects 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:
| Target | Static library | Dynamic 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:
| Field | Meaning |
|---|---|
sdk_version: String | Native SDK semantic version. |
abi_major: u32 | Native ABI major version. Version 1.0 uses 1. |
json_schema_version: u32 | Complex-payload schema version. Version 1.0 uses 1. |
target: String | Architecture and operating-system identity compiled into the library. |
profile: String | Native 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
| Field | Meaning |
|---|---|
kind: ErrorKind | Stable category suitable for program control flow. |
message: String | Human-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:
| Kind | Typical cause |
|---|---|
Argument | Invalid OID, empty input, zero receive timeout, or malformed binding data. |
Configuration | Unsupported version/operation combination, invalid security configuration, limit violation, or oversized outbound message. |
Timeout | No matching response or notification before the timeout. |
Transport | DNS, socket, TCP framing, worker, or other I/O failure. |
SnmpResponse | Agent Response-PDU error, malformed SNMP input, or protocol mismatch. |
Usm | SNMPv3 report, authentication, privacy, timeliness, or notification security failure. |
Mib | MIB I/O, parsing, import, ambiguity, or resolution failure. |
IncompatibleAbi | Native ABI or JSON schema is incompatible with the bindings. |
InvalidHandle | A stale or wrong-type native handle was detected. This normally indicates an SDK integration defect. |
Callback | A traversal callback failed or panicked. |
InternalPanic | A 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:
| Variant | Rust payload | Use |
|---|---|---|
Integer | i32 | ASN.1 INTEGER and enumerations. |
OctetString | Vec<u8> | OCTET STRING, display strings, and BITS. |
Null | none | Retrieval request placeholder; not legal for SET. |
ObjectIdentifier | Oid | OBJECT IDENTIFIER. |
IpAddress | [u8; 4] | SNMP IPv4 application value. |
Counter32 | u32 | Counter32. |
Gauge32 | u32 | Gauge32 or Unsigned32 wire value. |
TimeTicks | u32 | Hundredths of a second. |
Opaque | Vec<u8> | Opaque application bytes. |
Counter64 | u64 | Counter64; not legal in an SNMPv1 SET request. |
NoSuchObject | none | SNMPv2 exception response. |
NoSuchInstance | none | SNMPv2 exception response. |
EndOfMibView | none | SNMPv2 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:
| Field | Meaning |
|---|---|
pdu_type: PduType | Get, GetNext, Set, GetBulk, Response, Inform, V2Trap, or Report. Successful manager operations normally return Response. |
request_id: i32 | Correlation identifier generated by the native client. |
error_status: i32 | Numeric agent error status. Nonzero response statuses are normally returned as SdkError, rather than as a successful Response. |
error_index: i32 | Agent-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:
| Field | Default | Meaning |
|---|---|---|
host: String | supplied | DNS name or numeric address. It must not be empty. For IPv6, pass the address without URL brackets. |
port: u16 | 161 | Remote SNMP port. |
credentials: Credentials | supplied | SNMP version and community or USM credentials. |
transport: Transport | Udp | Transport::Udp or Transport::Tcp. TCP uses BER-length message framing. |
timeout_ms: u64 | 5000 | Timeout for each transport attempt in milliseconds. It must be greater than zero. |
retries: u32 | 0 | Additional attempts after the first timeout. Retries reuse the same encoded request and request ID. |
max_message_size: usize | 32768 | Maximum 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 byv1andv2c.Credentials::V3(UsmCredentials)is the serialized SNMPv3 form created byv3.
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:
| Field | Meaning |
|---|---|
username | USM security name; it must contain 1–32 bytes. |
auth_protocol / auth_password | Both must be present or both absent. Passwords must not be empty. |
privacy_protocol / privacy_password | Both must be present or both absent. Privacy additionally requires authentication. |
context_name | Scoped-PDU context name. |
context_engine_id | Optional explicit scoped-PDU engine ID. |
key_expansion_with_engine_id | Java-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>
oidsis 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
SdkErrorwithkind == SnmpResponse.
BlockingClient::get_next
pub fn get_next(&mut self, oids: &[Oid]) -> Result<Response>
oidsis 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>
varbindsmust 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
Valuesection. Counter64is 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_repeatersis the number of leading OIDs for which the agent should return a single successor.max_repetitionsis 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>
oidsmust be nonempty.optionscontrols 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
noSuchNameand SNMPv2 exception values terminate traversal.
TraversalOptions
| Field | Default | Meaning |
|---|---|---|
mode: TraversalMode | GetNext | GetNext or GetBulk { max_repetitions }. |
max_requests: usize | 10_000 | Maximum request/response rounds. Must be greater than zero. |
max_varbinds: usize | 1_000_000 | Maximum 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 ofEndOfMibView,OutsideSubtree,EmptyResponse, orCallbackStopped.
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:
| Field | Meaning |
|---|---|
bind_address: IpAddr | Local IPv4 or IPv6 interface address. Use an unspecified address for all interfaces. |
port: u16 | Local UDP port. |
max_message_size: usize | Maximum datagram size; it must be at least 484 bytes. |
max_engine_cache_entries: usize | Bound 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: u64 | Bounded 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>>,
}
credentialssupplies username, authentication, privacy, and Java-compatible key expansion.authoritative_engine_idoptionally restricts this user entry to one sender engine. Leave itNoneto 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_idmust contain 5–32 octets and should be stable across restarts.engine_bootsmust not exceedi32::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:
| Field | Meaning |
|---|---|
source: SocketAddr | Sender address. |
local_address: SocketAddr | Receiver address that accepted the datagram. |
version: SnmpVersion | V1, V2c, or V3. |
kind: NotificationKind | V1Trap, V2Trap, or Inform. |
security: NotificationSecurity | Redacted 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; orUsm { username, authoritative_engine_id, security_level }, wheresecurity_levelisNoAuthNoPriv,AuthNoPriv, orAuthPriv.
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
Nonewhen 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
| Field | Default | Meaning |
|---|---|---|
strict: bool | true | Strict mode returns typed errors for unresolved/cyclic imports and required parse failures. Lenient mode keeps placeholders and diagnostics when possible. |
depth: ParseDepth | Full | ModuleName, Imports, or Full. |
resolve_syntax: bool | false | Resolve 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_nameis recorded in diagnostics and the returned module.readeris 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.0andifDescr.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 == trueforces numericnameoutput.- 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)indexesMibTree::nodesand 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
| Field | Meaning |
|---|---|
name | Module definition name. |
source | File path or caller-supplied source name. |
metadata | MODULE-IDENTITY metadata and revisions. |
imports / exports | Parsed import/export declarations. |
tree | Owned arena OID tree. |
traps | SMIv1 TRAP-TYPE and SMIv2 notification metadata. |
textual_conventions | Name-to-Syntax map. |
display_hints | Parsed display hints. |
diagnostics | Typed 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:
| Field | Meaning |
|---|---|
id, parent, children | Arena traversal links. |
name, full_name, oid, module | Symbolic/numeric identity and owning module. |
node_type: NodeType | Root, ObjectType, ObjectIdentity, Notification, NotificationGroup, ModuleIdentity, Compliance, ObjectGroup, Other, or Unknown. |
description, access, status, units, default_value | Parsed SMI clauses. |
syntax | Optional resolved/derived syntax. |
indexes, table_indexes | Entry/table index definitions, including implied. |
augments, augments_table | Augmentation metadata. |
objects, object_oids | Notification/group object references and resolved OIDs. |
is_table | Table classification. |
row_status_oid, entry_status | RowStatus-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:WarningorError;code: DiagnosticCode:Syntax,InvalidHeader,InvalidEncoding,UnresolvedImport,UnresolvedSymbol,AmbiguousSymbol,DuplicateOid,ImportCycle, orIo;message,source, optional module/imported-module names; and- one-based
lineandcolumn.
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
BlockingClientorBlockingTrapReceiveroperation requires mutable access. AsyncClientandAsyncTrapReceiverclones share and serialize access to the same native object.- Create separate clients or receivers when independent parallel state is required.
- Each
MibRegistryowns 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
Debugoutput 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
- Use the SDK archive for the application's exact target triple.
- Select exactly one of static or dynamic linkage.
- Call
build_info()during startup diagnostics if early compatibility reporting is desired. - Ensure dynamic libraries are installed where the platform loader can find them.
- Set realistic
timeout_ms,retries, packet limits, and traversal limits. - Store SNMP credentials outside source and logs.
- For SNMPv3 INFORM reception, persist a stable authoritative engine ID and increment engine boots before binding after each restart.
- Use nonprivileged receiver ports unless deployment explicitly grants access to UDP 162.
- Prefer streaming traversal for large device trees.
- 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.