major overhaul to use more C for snmp, may not be stable
This commit is contained in:
parent
cd45e9ff51
commit
792825f682
16 changed files with 1748 additions and 1055 deletions
7
.envrc
Normal file
7
.envrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use flake
|
||||
|
||||
# OpenSSL configuration for Homebrew on macOS
|
||||
export OPENSSL_DIR=/opt/homebrew/opt/openssl@3
|
||||
export OPENSSL_LIB_DIR=/opt/homebrew/opt/openssl@3/lib
|
||||
export PKG_CONFIG_PATH=/opt/homebrew/opt/openssl@3/lib/pkgconfig
|
||||
export RUSTFLAGS="-L /opt/homebrew/opt/openssl@3/lib"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -23,5 +23,9 @@ data/
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Nix
|
||||
.direnv/
|
||||
result
|
||||
|
||||
# Local scripts
|
||||
monitor-deploy.sh
|
||||
|
|
|
|||
158
Cargo.lock
generated
158
Cargo.lock
generated
|
|
@ -135,44 +135,6 @@ dependencies = [
|
|||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
|
|
@ -847,17 +809,6 @@ dependencies = [
|
|||
"crypto-common 0.2.0-rc.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
|
|
@ -1071,21 +1022,6 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
|
|
@ -1772,6 +1708,15 @@ version = "0.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "netsnmp-sys"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "631b53a54f07e7f8e390ba494a5fa4cac4c92abed56506025714c62a237c27c3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
|
|
@ -1910,44 +1855,6 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
|
|
@ -2247,12 +2154,6 @@ dependencies = [
|
|||
"spki 0.8.0-rc.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "pnet_base"
|
||||
version = "0.34.0"
|
||||
|
|
@ -2782,15 +2683,6 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
|
|
@ -3098,17 +2990,6 @@ version = "1.15.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "snmp2"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49dc4ff9dc6c2b03d818a4c2abb8ffcef9e16357113ddc7ce1631c03195fde64"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"openssl",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.2"
|
||||
|
|
@ -3251,17 +3132,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
|
|
@ -3488,11 +3358,14 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"cc",
|
||||
"clap",
|
||||
"crossterm 0.28.1",
|
||||
"futures",
|
||||
"home",
|
||||
"hostname",
|
||||
"libc",
|
||||
"netsnmp-sys",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
|
|
@ -3501,7 +3374,6 @@ dependencies = [
|
|||
"russh",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snmp2",
|
||||
"surge-ping",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
@ -3685,12 +3557,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
snmp2 = { version = "0.4", features = ["v3", "tokio"] }
|
||||
netsnmp-sys = "0.1"
|
||||
libc = "0.2"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "signal", "io-util"] }
|
||||
thiserror = "2"
|
||||
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] }
|
||||
|
|
@ -36,6 +37,7 @@ tui = ["ratatui", "crossterm"]
|
|||
|
||||
[build-dependencies]
|
||||
prost-build = "0.14"
|
||||
cc = "1.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ ARG VERSION=0.1.0-unknown
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache musl-dev protobuf-dev openssl-dev openssl-libs-static cmake perl g++
|
||||
# Install build dependencies (net-snmp-dev required for C FFI)
|
||||
RUN apk add --no-cache musl-dev protobuf-dev openssl-dev openssl-libs-static net-snmp-dev cmake perl g++
|
||||
|
||||
# Determine Rust target based on platform and add it
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
|
|
@ -56,7 +56,8 @@ FROM alpine:3.19
|
|||
|
||||
# Install runtime dependencies
|
||||
# iputils provides ping with setuid root (doesn't require CAP_NET_RAW)
|
||||
RUN apk add --no-cache ca-certificates iputils openssl
|
||||
# net-snmp-libs required for C FFI to libnetsnmp
|
||||
RUN apk add --no-cache ca-certificates iputils openssl net-snmp-libs
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /tmp/towerops-agent /usr/local/bin/towerops-agent
|
||||
|
|
|
|||
9
build.rs
9
build.rs
|
|
@ -4,6 +4,15 @@ fn main() {
|
|||
// Compile protobuf definitions
|
||||
prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap();
|
||||
|
||||
// Compile C helper for SNMP
|
||||
cc::Build::new()
|
||||
.file("native/snmp_helper.c")
|
||||
.include("native")
|
||||
.compile("snmp_helper");
|
||||
|
||||
// Link against netsnmp library
|
||||
println!("cargo:rustc-link-lib=netsnmp");
|
||||
|
||||
// Inject git-based version if available, otherwise use Cargo.toml version
|
||||
// This ensures the binary version matches the Docker image tag
|
||||
let version = get_version();
|
||||
|
|
|
|||
82
flake.lock
generated
Normal file
82
flake.lock
generated
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770537093,
|
||||
"narHash": "sha256-pF1quXG5wsgtyuPOHcLfYg/ft/QMr8NnX0i6tW2187s=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fef9403a3e4d31b0a23f0bacebbec52c248fbb51",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770520253,
|
||||
"narHash": "sha256-6rWuHgSENXKnC6HGGAdRolQrnp/8IzscDn7FQEo1uEQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "ebb8a141f60bb0ec33836333e0ca7928a072217f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
53
flake.nix
Normal file
53
flake.nix
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
description = "towerops-agent - Rust SNMP polling agent";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
rustToolchain
|
||||
pkgs.protobuf
|
||||
pkgs.net-snmp
|
||||
pkgs.openssl
|
||||
pkgs.pkg-config
|
||||
pkgs.git
|
||||
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
|
||||
pkgs.apple-sdk_15
|
||||
];
|
||||
|
||||
env = {
|
||||
PROTOC = "${pkgs.protobuf}/bin/protoc";
|
||||
# Help netsnmp-sys find the library
|
||||
NET_SNMP_CONFIG = "${pkgs.net-snmp}/bin/net-snmp-config";
|
||||
# Help cargo find OpenSSL for linking
|
||||
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
OPENSSL_DIR = "${pkgs.openssl.dev}";
|
||||
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
|
||||
OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
|
||||
};
|
||||
|
||||
shellHook = ''
|
||||
# Set RUSTFLAGS to find OpenSSL libraries at link time
|
||||
export RUSTFLAGS="-L ${pkgs.openssl.out}/lib"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
478
native/snmp_helper.c
Normal file
478
native/snmp_helper.c
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
#include "snmp_helper.h"
|
||||
#include <net-snmp/net-snmp-config.h>
|
||||
#include <net-snmp/net-snmp-includes.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
static void init_snmp_once(void) {
|
||||
// Initialize the SNMP library
|
||||
init_snmp("towerops-agent");
|
||||
|
||||
// Configure to output numeric OIDs only (no MIB names)
|
||||
// This ensures OIDs are in format "1.3.6.1.2.1.1.1.0" not "SNMPv2-MIB::sysDescr.0"
|
||||
netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT,
|
||||
NETSNMP_OID_OUTPUT_NUMERIC);
|
||||
}
|
||||
|
||||
int snmp_init_library(void) {
|
||||
pthread_once(&init_once, init_snmp_once);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* snmp_open_session(
|
||||
const char* ip_address,
|
||||
uint16_t port,
|
||||
const char* community,
|
||||
int version,
|
||||
int64_t timeout_us,
|
||||
int retries,
|
||||
const snmp_v3_config_t* v3_config,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
) {
|
||||
struct snmp_session session, *sess_handle;
|
||||
|
||||
// Ensure library is initialized
|
||||
snmp_init_library();
|
||||
|
||||
// Initialize session structure
|
||||
snmp_sess_init(&session);
|
||||
|
||||
// Set peer address with port (e.g., "192.168.1.1:161")
|
||||
// This is the modern way - remote_port field is deprecated
|
||||
char peername[256];
|
||||
snprintf(peername, sizeof(peername), "%s:%u", ip_address, port);
|
||||
session.peername = strdup(peername);
|
||||
if (!session.peername) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to allocate memory for peer address");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set SNMP version
|
||||
switch (version) {
|
||||
case 1:
|
||||
session.version = SNMP_VERSION_1;
|
||||
break;
|
||||
case 2:
|
||||
session.version = SNMP_VERSION_2c;
|
||||
break;
|
||||
case 3:
|
||||
session.version = SNMP_VERSION_3;
|
||||
break;
|
||||
default:
|
||||
free(session.peername);
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Unsupported SNMP version: %d", version);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Configure version-specific parameters
|
||||
if (version == 3) {
|
||||
// SNMPv3 configuration
|
||||
if (!v3_config || !v3_config->username) {
|
||||
free(session.peername);
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "SNMPv3 requires username");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set security name (username)
|
||||
session.securityName = strdup(v3_config->username);
|
||||
session.securityNameLen = strlen(v3_config->username);
|
||||
|
||||
// Set security level
|
||||
if (v3_config->security_level) {
|
||||
if (strcmp(v3_config->security_level, "authPriv") == 0) {
|
||||
session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV;
|
||||
} else if (strcmp(v3_config->security_level, "authNoPriv") == 0) {
|
||||
session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;
|
||||
} else {
|
||||
session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;
|
||||
}
|
||||
} else {
|
||||
session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;
|
||||
}
|
||||
|
||||
// Set authentication protocol and password
|
||||
if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHNOPRIV) {
|
||||
if (v3_config->auth_password) {
|
||||
session.securityAuthProto = usmHMACMD5AuthProtocol;
|
||||
session.securityAuthProtoLen = USM_AUTH_PROTO_MD5_LEN;
|
||||
|
||||
if (v3_config->auth_protocol) {
|
||||
if (strcmp(v3_config->auth_protocol, "SHA") == 0) {
|
||||
session.securityAuthProto = usmHMACSHA1AuthProtocol;
|
||||
session.securityAuthProtoLen = USM_AUTH_PROTO_SHA_LEN;
|
||||
}
|
||||
}
|
||||
|
||||
session.securityAuthKeyLen = USM_AUTH_KU_LEN;
|
||||
if (generate_Ku(session.securityAuthProto,
|
||||
session.securityAuthProtoLen,
|
||||
(u_char*)v3_config->auth_password,
|
||||
strlen(v3_config->auth_password),
|
||||
session.securityAuthKey,
|
||||
&session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
|
||||
free(session.peername);
|
||||
free((void*)session.securityName);
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to generate auth key");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set privacy protocol and password
|
||||
if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHPRIV) {
|
||||
if (v3_config->priv_password) {
|
||||
session.securityPrivProto = usmDESPrivProtocol;
|
||||
session.securityPrivProtoLen = USM_PRIV_PROTO_DES_LEN;
|
||||
|
||||
if (v3_config->priv_protocol) {
|
||||
if (strcmp(v3_config->priv_protocol, "AES") == 0) {
|
||||
session.securityPrivProto = usmAESPrivProtocol;
|
||||
session.securityPrivProtoLen = USM_PRIV_PROTO_AES_LEN;
|
||||
}
|
||||
}
|
||||
|
||||
session.securityPrivKeyLen = USM_PRIV_KU_LEN;
|
||||
if (generate_Ku(session.securityAuthProto,
|
||||
session.securityAuthProtoLen,
|
||||
(u_char*)v3_config->priv_password,
|
||||
strlen(v3_config->priv_password),
|
||||
session.securityPrivKey,
|
||||
&session.securityPrivKeyLen) != SNMPERR_SUCCESS) {
|
||||
free(session.peername);
|
||||
free((void*)session.securityName);
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to generate priv key");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// v1/v2c: Set community string
|
||||
if (community && community[0]) {
|
||||
session.community = (u_char*)strdup(community);
|
||||
if (!session.community) {
|
||||
free(session.peername);
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to allocate memory for community string");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
session.community_len = strlen(community);
|
||||
}
|
||||
}
|
||||
|
||||
// Set timeout and retries
|
||||
session.timeout = timeout_us;
|
||||
session.retries = retries;
|
||||
|
||||
// Open the session
|
||||
sess_handle = snmp_sess_open(&session);
|
||||
|
||||
// Clean up temporary allocations
|
||||
free(session.peername);
|
||||
if (session.community) {
|
||||
// Zero out community string before freeing
|
||||
memset((void*)session.community, 0, session.community_len);
|
||||
free((void*)session.community);
|
||||
}
|
||||
if (session.securityName) {
|
||||
free((void*)session.securityName);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (!sess_handle) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
// Get error message from library
|
||||
int liberr, syserr;
|
||||
char *errstr;
|
||||
snmp_error(&session, &liberr, &syserr, &errstr);
|
||||
snprintf(error_buf, error_buf_len, "%s", errstr);
|
||||
free(errstr);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sess_handle;
|
||||
}
|
||||
|
||||
void snmp_close_session(void* sess_handle) {
|
||||
if (sess_handle) {
|
||||
snmp_sess_close(sess_handle);
|
||||
}
|
||||
}
|
||||
|
||||
int snmp_get(
|
||||
void* sess_handle,
|
||||
const char* oid_str,
|
||||
void* value_buf,
|
||||
size_t value_buf_len,
|
||||
int* value_type,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
) {
|
||||
if (!sess_handle || !oid_str || !value_buf || !value_type) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Invalid parameters");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
oid anOID[MAX_OID_LEN];
|
||||
size_t anOID_len = MAX_OID_LEN;
|
||||
|
||||
// Parse OID string
|
||||
if (!read_objid(oid_str, anOID, &anOID_len)) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to parse OID: %s", oid_str);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create GET PDU
|
||||
struct snmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GET);
|
||||
if (!pdu) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to create PDU");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Add OID to PDU
|
||||
snmp_add_null_var(pdu, anOID, anOID_len);
|
||||
|
||||
// Send request
|
||||
struct snmp_pdu *response = NULL;
|
||||
int status = snmp_sess_synch_response(sess_handle, pdu, &response);
|
||||
|
||||
if (status != STAT_SUCCESS || !response) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
if (status == STAT_TIMEOUT) {
|
||||
snprintf(error_buf, error_buf_len, "Request timeout");
|
||||
} else {
|
||||
snprintf(error_buf, error_buf_len, "Request failed");
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
snmp_free_pdu(response);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract value from response
|
||||
int result = -1;
|
||||
if (response->variables) {
|
||||
struct variable_list *var = response->variables;
|
||||
*value_type = var->type;
|
||||
|
||||
switch (var->type) {
|
||||
case ASN_OCTET_STR:
|
||||
case ASN_OPAQUE:
|
||||
case ASN_IPADDRESS:
|
||||
if (var->val_len <= value_buf_len) {
|
||||
memcpy(value_buf, var->val.string, var->val_len);
|
||||
result = (int)var->val_len;
|
||||
} else {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Buffer too small");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_INTEGER:
|
||||
case ASN_COUNTER:
|
||||
case ASN_GAUGE:
|
||||
case ASN_TIMETICKS:
|
||||
case ASN_UINTEGER:
|
||||
if (sizeof(long) <= value_buf_len) {
|
||||
*((long*)value_buf) = *var->val.integer;
|
||||
result = sizeof(long);
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_COUNTER64:
|
||||
if (sizeof(struct counter64) <= value_buf_len) {
|
||||
memcpy(value_buf, var->val.counter64, sizeof(struct counter64));
|
||||
result = sizeof(struct counter64);
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_OBJECT_ID:
|
||||
// Convert OID to string representation
|
||||
{
|
||||
char oid_buf[256];
|
||||
snprint_objid(oid_buf, sizeof(oid_buf), var->val.objid, var->val_len / sizeof(oid));
|
||||
size_t oid_str_len = strlen(oid_buf);
|
||||
if (oid_str_len <= value_buf_len) {
|
||||
memcpy(value_buf, oid_buf, oid_str_len);
|
||||
result = (int)oid_str_len;
|
||||
} else {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Buffer too small for OID string");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_NULL:
|
||||
// NULL values are valid but contain no data
|
||||
result = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown type
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Unsupported type: %d", var->type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
snmp_free_pdu(response);
|
||||
return result;
|
||||
}
|
||||
|
||||
int snmp_walk(
|
||||
void* sess_handle,
|
||||
const char* oid_str,
|
||||
snmp_walk_result_t* results,
|
||||
size_t max_results,
|
||||
size_t* num_results,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
) {
|
||||
if (!sess_handle || !oid_str || !results || !num_results) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Invalid parameters");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
oid root[MAX_OID_LEN];
|
||||
size_t rootlen = MAX_OID_LEN;
|
||||
|
||||
// Parse starting OID
|
||||
if (!read_objid(oid_str, root, &rootlen)) {
|
||||
if (error_buf && error_buf_len > 0) {
|
||||
snprintf(error_buf, error_buf_len, "Failed to parse OID: %s", oid_str);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
oid name[MAX_OID_LEN];
|
||||
size_t name_length = rootlen;
|
||||
memcpy(name, root, rootlen * sizeof(oid));
|
||||
|
||||
*num_results = 0;
|
||||
int running = 1;
|
||||
|
||||
while (running && *num_results < max_results) {
|
||||
// Create GETNEXT PDU
|
||||
struct snmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GETNEXT);
|
||||
if (!pdu) {
|
||||
break;
|
||||
}
|
||||
|
||||
snmp_add_null_var(pdu, name, name_length);
|
||||
|
||||
// Send request
|
||||
struct snmp_pdu *response = NULL;
|
||||
int status = snmp_sess_synch_response(sess_handle, pdu, &response);
|
||||
|
||||
if (status != STAT_SUCCESS || !response || !response->variables) {
|
||||
if (response) {
|
||||
snmp_free_pdu(response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
struct variable_list *var = response->variables;
|
||||
|
||||
// Check if we've walked past the root OID
|
||||
if (var->name_length < rootlen ||
|
||||
snmp_oid_ncompare(var->name, var->name_length, root, rootlen, rootlen) != 0) {
|
||||
snmp_free_pdu(response);
|
||||
break;
|
||||
}
|
||||
|
||||
// Store result
|
||||
snmp_walk_result_t *res = &results[*num_results];
|
||||
|
||||
// Convert OID to string
|
||||
snprint_objid(res->oid, sizeof(res->oid), var->name, var->name_length);
|
||||
|
||||
// Store value
|
||||
res->value_type = var->type;
|
||||
res->value_len = 0;
|
||||
|
||||
switch (var->type) {
|
||||
case ASN_OCTET_STR:
|
||||
case ASN_OPAQUE:
|
||||
case ASN_IPADDRESS:
|
||||
if (var->val_len <= sizeof(res->value)) {
|
||||
memcpy(res->value, var->val.string, var->val_len);
|
||||
res->value_len = var->val_len;
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_OBJECT_ID:
|
||||
// Convert OID to string representation
|
||||
{
|
||||
char oid_buf[256];
|
||||
snprint_objid(oid_buf, sizeof(oid_buf), var->val.objid, var->val_len / sizeof(oid));
|
||||
size_t oid_str_len = strlen(oid_buf);
|
||||
if (oid_str_len < sizeof(res->value)) {
|
||||
memcpy(res->value, oid_buf, oid_str_len);
|
||||
res->value_len = oid_str_len;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_INTEGER:
|
||||
case ASN_COUNTER:
|
||||
case ASN_GAUGE:
|
||||
case ASN_TIMETICKS:
|
||||
case ASN_UINTEGER:
|
||||
if (sizeof(long) <= sizeof(res->value)) {
|
||||
*((long*)res->value) = *var->val.integer;
|
||||
res->value_len = sizeof(long);
|
||||
}
|
||||
break;
|
||||
|
||||
case ASN_COUNTER64:
|
||||
if (sizeof(struct counter64) <= sizeof(res->value)) {
|
||||
memcpy(res->value, var->val.counter64, sizeof(struct counter64));
|
||||
res->value_len = sizeof(struct counter64);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (res->value_len > 0) {
|
||||
(*num_results)++;
|
||||
}
|
||||
|
||||
// Update OID for next iteration
|
||||
if (var->name_length <= MAX_OID_LEN) {
|
||||
memcpy(name, var->name, var->name_length * sizeof(oid));
|
||||
name_length = var->name_length;
|
||||
} else {
|
||||
running = 0;
|
||||
}
|
||||
|
||||
snmp_free_pdu(response);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
111
native/snmp_helper.h
Normal file
111
native/snmp_helper.h
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#ifndef SNMP_HELPER_H
|
||||
#define SNMP_HELPER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Initialize the SNMP library (call once at startup)
|
||||
* Returns 0 on success, -1 on failure
|
||||
*/
|
||||
int snmp_init_library(void);
|
||||
|
||||
/**
|
||||
* SNMPv3 configuration
|
||||
*/
|
||||
typedef struct {
|
||||
const char* username;
|
||||
const char* auth_password;
|
||||
const char* priv_password;
|
||||
const char* auth_protocol; // "MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"
|
||||
const char* priv_protocol; // "DES", "AES", "AES-192", "AES-256"
|
||||
const char* security_level; // "noAuthNoPriv", "authNoPriv", "authPriv"
|
||||
} snmp_v3_config_t;
|
||||
|
||||
/**
|
||||
* Open an SNMP session
|
||||
*
|
||||
* @param ip_address IP address of the device
|
||||
* @param port UDP port (usually 161)
|
||||
* @param community Community string for SNMPv1/v2c (ignored for v3)
|
||||
* @param version SNMP version: 1 (SNMPv1), 2 (SNMPv2c), 3 (SNMPv3)
|
||||
* @param timeout_us Timeout in microseconds
|
||||
* @param retries Number of retries
|
||||
* @param v3_config SNMPv3 configuration (NULL for v1/v2c)
|
||||
* @param error_buf Buffer for error messages (can be NULL)
|
||||
* @param error_buf_len Length of error buffer
|
||||
* @return Session handle on success, NULL on failure
|
||||
*/
|
||||
void* snmp_open_session(
|
||||
const char* ip_address,
|
||||
uint16_t port,
|
||||
const char* community,
|
||||
int version,
|
||||
int64_t timeout_us,
|
||||
int retries,
|
||||
const snmp_v3_config_t* v3_config,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
);
|
||||
|
||||
/**
|
||||
* Close an SNMP session
|
||||
* @param sess_handle Session handle from snmp_open_session
|
||||
*/
|
||||
void snmp_close_session(void* sess_handle);
|
||||
|
||||
/**
|
||||
* Perform SNMP GET operation
|
||||
*
|
||||
* @param sess_handle Session handle from snmp_open_session
|
||||
* @param oid_str OID string (e.g., "1.3.6.1.2.1.1.1.0")
|
||||
* @param value_buf Buffer for result value
|
||||
* @param value_buf_len Length of value buffer
|
||||
* @param value_type Output: type of value (see ASN_* constants)
|
||||
* @param error_buf Buffer for error messages (can be NULL)
|
||||
* @param error_buf_len Length of error buffer
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
int snmp_get(
|
||||
void* sess_handle,
|
||||
const char* oid_str,
|
||||
void* value_buf,
|
||||
size_t value_buf_len,
|
||||
int* value_type,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
);
|
||||
|
||||
/**
|
||||
* Result from SNMP WALK operation
|
||||
*/
|
||||
typedef struct {
|
||||
char oid[256];
|
||||
uint8_t value[1024];
|
||||
size_t value_len;
|
||||
int value_type;
|
||||
} snmp_walk_result_t;
|
||||
|
||||
/**
|
||||
* Perform SNMP WALK operation
|
||||
*
|
||||
* @param sess_handle Session handle from snmp_open_session
|
||||
* @param oid_str Starting OID string
|
||||
* @param results Buffer for results
|
||||
* @param max_results Maximum number of results to return
|
||||
* @param num_results Output: actual number of results
|
||||
* @param error_buf Buffer for error messages (can be NULL)
|
||||
* @param error_buf_len Length of error buffer
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
int snmp_walk(
|
||||
void* sess_handle,
|
||||
const char* oid_str,
|
||||
snmp_walk_result_t* results,
|
||||
size_t max_results,
|
||||
size_t* num_results,
|
||||
char* error_buf,
|
||||
size_t error_buf_len
|
||||
);
|
||||
|
||||
#endif // SNMP_HELPER_H
|
||||
|
|
@ -367,7 +367,6 @@ async fn async_main() {
|
|||
|
||||
/// Run SNMPv3 test
|
||||
async fn run_snmpv3_test(args: &Args) {
|
||||
use secret::SecretString;
|
||||
use snmp::V3Config;
|
||||
|
||||
let ip = args.snmpv3_ip.as_ref().expect("--snmpv3-ip required");
|
||||
|
|
@ -397,12 +396,12 @@ async fn run_snmpv3_test(args: &Args) {
|
|||
let v3_config = V3Config {
|
||||
username: username.clone(),
|
||||
auth_password: if !auth_pass.is_empty() {
|
||||
Some(SecretString::new(auth_pass.clone()))
|
||||
Some(zeroize::Zeroizing::new(auth_pass.clone()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
priv_password: if !priv_pass.is_empty() {
|
||||
Some(SecretString::new(priv_pass.clone()))
|
||||
Some(zeroize::Zeroizing::new(priv_pass.clone()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
|
|||
1203
src/snmp/client.rs
1203
src/snmp/client.rs
File diff suppressed because it is too large
Load diff
99
src/snmp/client_v2.rs
Normal file
99
src/snmp/client_v2.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Temporary new implementation to test compilation - will replace client.rs
|
||||
|
||||
use super::types::{SnmpError, SnmpResult, SnmpValue};
|
||||
use netsnmp_sys::*;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
|
||||
const SNMP_TIMEOUT_SECS: i64 = 10;
|
||||
|
||||
/// Test if we can compile a simple SNMP GET using netsnmp-sys
|
||||
pub fn test_snmp_get(ip: &str, community: &str) -> SnmpResult<String> {
|
||||
unsafe {
|
||||
// Initialize library
|
||||
init_snmp(b"test\0".as_ptr() as *const i8);
|
||||
|
||||
// Create session
|
||||
let mut sess: Struct_netsnmp_session = std::mem::zeroed();
|
||||
snmp_sess_init(&mut sess as *mut _);
|
||||
|
||||
// Set peername
|
||||
let peer = CString::new(format!("{}:161", ip)).unwrap();
|
||||
sess.peername = peer.as_ptr() as *mut _;
|
||||
|
||||
// Set version and community
|
||||
sess.version = SNMP_VERSION_2c as i32;
|
||||
let comm = CString::new(community).unwrap();
|
||||
sess.community = comm.as_ptr() as *mut _;
|
||||
sess.community_len = community.len();
|
||||
|
||||
// Set timeout
|
||||
sess.timeout = SNMP_TIMEOUT_SECS * 1_000_000; // microseconds
|
||||
|
||||
// Open session
|
||||
let sess_ptr = snmp_open(&mut sess as *mut _);
|
||||
if sess_ptr.is_null() {
|
||||
return Err(SnmpError::NetworkUnreachable);
|
||||
}
|
||||
|
||||
// Parse OID for sysDescr.0
|
||||
let mut oid_buf = [0u32; MAX_OID_LEN];
|
||||
let mut oid_len = MAX_OID_LEN;
|
||||
let oid_str = CString::new("1.3.6.1.2.1.1.1.0").unwrap();
|
||||
|
||||
if read_objid(oid_str.as_ptr(), oid_buf.as_mut_ptr(), &mut oid_len) == 0 {
|
||||
snmp_close(sess_ptr);
|
||||
return Err(SnmpError::InvalidOid("Failed to parse OID".into()));
|
||||
}
|
||||
|
||||
// Create PDU
|
||||
let pdu = snmp_pdu_create(SNMP_MSG_GET as i32);
|
||||
if pdu.is_null() {
|
||||
snmp_close(sess_ptr);
|
||||
return Err(SnmpError::RequestFailed("Failed to create PDU".into()));
|
||||
}
|
||||
|
||||
// Add OID to PDU
|
||||
snmp_add_null_var(pdu, oid_buf.as_ptr(), oid_len);
|
||||
|
||||
// Send request
|
||||
let mut response: *mut Struct_netsnmp_pdu = ptr::null_mut();
|
||||
let status = snmp_synch_response(sess_ptr, pdu, &mut response as *mut _);
|
||||
|
||||
let result = if status == STAT_SUCCESS as i32 && !response.is_null() {
|
||||
let vars = (*response).variables;
|
||||
if !vars.is_null() {
|
||||
// Get the type
|
||||
let var_type = (*vars)._type;
|
||||
|
||||
// Try to extract string value
|
||||
if var_type == ASN_OCTET_STR as u8 {
|
||||
let val_len = (*vars).val_len;
|
||||
// Access union - need mutable pointer
|
||||
let vars_mut = vars as *mut _;
|
||||
let string_ptr = *(*vars_mut).val.string();
|
||||
let slice = std::slice::from_raw_parts(string_ptr, val_len);
|
||||
Ok(String::from_utf8_lossy(slice).to_string())
|
||||
} else {
|
||||
Err(SnmpError::RequestFailed(format!("Unexpected type: {}", var_type)))
|
||||
}
|
||||
} else {
|
||||
Err(SnmpError::RequestFailed("No variables in response".into()))
|
||||
}
|
||||
} else {
|
||||
Err(if status == STAT_TIMEOUT as i32 {
|
||||
SnmpError::Timeout
|
||||
} else {
|
||||
SnmpError::RequestFailed("SNMP request failed".into()))
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
if !response.is_null() {
|
||||
snmp_free_pdu(response);
|
||||
}
|
||||
snmp_close(sess_ptr);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
use super::client::SnmpClient;
|
||||
use super::types::{SnmpError, SnmpResult, SnmpValue};
|
||||
use super::V3Config;
|
||||
use crate::secret::SecretString;
|
||||
use snmp2::{Oid, SyncSession};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
// SNMP timeout in seconds - increased to 120s for SNMPv3 operations
|
||||
// SNMPv3 has significant encryption/auth overhead
|
||||
// Full discovery can take 50+ seconds with complete MikroTik OID tree traversal
|
||||
const SNMP_TIMEOUT_SECS: u64 = 120;
|
||||
|
||||
/// Request to perform an SNMP operation
|
||||
#[derive(Debug)]
|
||||
pub enum SnmpRequest {
|
||||
|
|
@ -49,7 +42,7 @@ impl std::fmt::Debug for DeviceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Per-device polling thread that maintains a persistent SNMP session
|
||||
/// Per-device polling thread that uses C FFI to libnetsnmp
|
||||
pub struct DevicePoller {
|
||||
device_id: String,
|
||||
config: DeviceConfig,
|
||||
|
|
@ -65,17 +58,20 @@ impl DevicePoller {
|
|||
let config_clone = config.clone();
|
||||
|
||||
// Spawn the polling thread with 8MB stack for SNMPv3 crypto operations
|
||||
tracing::info!("Spawning device poller thread for {} at {}:{}", device_id, config.ip, config.port);
|
||||
std::thread::Builder::new()
|
||||
.name(format!("poller-{}", device_id))
|
||||
.stack_size(8 * 1024 * 1024) // 8MB stack (default is 2MB)
|
||||
.spawn(move || {
|
||||
if let Err(e) = run_poller_thread(device_id_clone, config_clone, request_rx) {
|
||||
tracing::error!("Device poller thread failed: {}", e);
|
||||
tracing::info!("Device poller thread starting for {}", device_id_clone);
|
||||
if let Err(e) = run_poller_thread(device_id_clone.clone(), config_clone, request_rx) {
|
||||
tracing::error!("Device poller thread failed for {}: {}", device_id_clone, e);
|
||||
}
|
||||
tracing::info!("Device poller thread exited for {}", device_id_clone);
|
||||
})
|
||||
.expect("Failed to spawn device poller thread");
|
||||
|
||||
tracing::info!("Spawned device poller thread for {}", device_id);
|
||||
tracing::info!("Successfully spawned device poller thread for {}", device_id);
|
||||
|
||||
Self {
|
||||
device_id,
|
||||
|
|
@ -128,21 +124,23 @@ impl DevicePoller {
|
|||
&self.config
|
||||
}
|
||||
|
||||
/// Log device poller status using accessor methods
|
||||
/// Update the device configuration
|
||||
pub fn update_config(&mut self, new_config: DeviceConfig) {
|
||||
self.config = new_config;
|
||||
}
|
||||
|
||||
/// Log the status of this poller (for debugging)
|
||||
pub fn log_status(&self) {
|
||||
let id = self.device_id();
|
||||
let cfg = self.config();
|
||||
tracing::debug!(
|
||||
"Device poller {} at {}:{} (version: {})",
|
||||
id,
|
||||
cfg.ip,
|
||||
cfg.port,
|
||||
cfg.version
|
||||
"Poller status: device_id={}, ip={}:{}",
|
||||
self.device_id,
|
||||
self.config.ip,
|
||||
self.config.port
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the device poller thread (blocking)
|
||||
/// Run the poller thread using C FFI to libnetsnmp
|
||||
fn run_poller_thread(
|
||||
device_id: String,
|
||||
config: DeviceConfig,
|
||||
|
|
@ -155,331 +153,128 @@ fn run_poller_thread(
|
|||
config.port
|
||||
);
|
||||
|
||||
// Create persistent session
|
||||
let addr = format!("{}:{}", config.ip, config.port);
|
||||
let mut session = create_session(&addr, &config)?;
|
||||
// Create a tokio runtime for this thread (SnmpClient uses async)
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create tokio runtime: {}", e))?;
|
||||
|
||||
tracing::info!(
|
||||
"Created persistent SNMP session for device {} (version: {})",
|
||||
device_id,
|
||||
config.version
|
||||
);
|
||||
// Create SNMP client (stateless, uses C FFI)
|
||||
let client = SnmpClient::new();
|
||||
|
||||
// Process requests until shutdown
|
||||
while let Some(request) = request_rx.blocking_recv() {
|
||||
match request {
|
||||
SnmpRequest::Get { oid, response_tx } => {
|
||||
let result = perform_get(&mut session, &oid);
|
||||
let _ = response_tx.send(result);
|
||||
let is_shutdown = matches!(request, SnmpRequest::Shutdown);
|
||||
|
||||
// Log what request we're processing
|
||||
match &request {
|
||||
SnmpRequest::Get { oid, .. } => {
|
||||
tracing::debug!("Poller thread {} processing GET {}", device_id, oid);
|
||||
}
|
||||
SnmpRequest::Walk {
|
||||
base_oid,
|
||||
response_tx,
|
||||
} => {
|
||||
let result = perform_walk(&mut session, &base_oid);
|
||||
let _ = response_tx.send(result);
|
||||
SnmpRequest::Walk { base_oid, .. } => {
|
||||
tracing::debug!("Poller thread {} processing WALK {}", device_id, base_oid);
|
||||
}
|
||||
SnmpRequest::Shutdown => {
|
||||
tracing::info!("Device poller thread shutting down for {}", device_id);
|
||||
break;
|
||||
tracing::info!("Poller thread {} received shutdown signal", device_id);
|
||||
}
|
||||
}
|
||||
|
||||
let panic_result =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match request {
|
||||
SnmpRequest::Get { oid, response_tx } => {
|
||||
tracing::debug!("Poller thread {} executing GET", device_id);
|
||||
let result = perform_get(&runtime, &client, &config, &oid);
|
||||
tracing::debug!("Poller thread {} GET result: {:?}", device_id, result.is_ok());
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
SnmpRequest::Walk {
|
||||
base_oid,
|
||||
response_tx,
|
||||
} => {
|
||||
tracing::debug!("Poller thread {} executing WALK", device_id);
|
||||
let result = perform_walk(&runtime, &client, &config, &base_oid);
|
||||
tracing::debug!(
|
||||
"Poller thread {} WALK result: {:?}",
|
||||
device_id,
|
||||
result.as_ref().map(|v| v.len())
|
||||
);
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
SnmpRequest::Shutdown => {
|
||||
tracing::info!("Device poller thread shutting down for {}", device_id);
|
||||
}
|
||||
}));
|
||||
|
||||
if let Err(panic_err) = panic_result {
|
||||
let panic_msg = if let Some(s) = panic_err.downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = panic_err.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Unknown panic".to_string()
|
||||
};
|
||||
tracing::error!(
|
||||
"Panic in device poller thread for {}: {}",
|
||||
device_id,
|
||||
panic_msg
|
||||
);
|
||||
// Don't break - keep the thread alive for future requests
|
||||
} else {
|
||||
tracing::debug!("Poller thread {} completed request successfully", device_id);
|
||||
}
|
||||
|
||||
if is_shutdown {
|
||||
tracing::info!("Poller thread {} exiting due to shutdown", device_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Device poller thread stopped for {}", device_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an SNMP session based on version and transport
|
||||
fn create_session(addr: &str, config: &DeviceConfig) -> Result<SyncSession, String> {
|
||||
let timeout = Some(Duration::from_secs(SNMP_TIMEOUT_SECS));
|
||||
let version_num = parse_snmp_version(&config.version)?;
|
||||
let transport = config.transport.to_lowercase();
|
||||
|
||||
// TCP transport warning - not yet implemented
|
||||
// TODO: Implement SNMP-over-TCP (RFC 3430) - requires low-level PDU handling
|
||||
// since snmp2 crate doesn't expose internal types needed for TCP framing
|
||||
if transport == "tcp" {
|
||||
tracing::warn!(
|
||||
"TCP transport requested for {} but not yet implemented. \
|
||||
Falling back to UDP. TCP support requires custom PDU encoder/decoder \
|
||||
since snmp2 crate doesn't expose necessary internal types.",
|
||||
addr
|
||||
);
|
||||
}
|
||||
|
||||
// UDP transport (default, also used as fallback for TCP)
|
||||
if config.version == "3" {
|
||||
let v3_config = config
|
||||
.v3_config
|
||||
.as_ref()
|
||||
.ok_or("v3_config required for SNMPv3")?;
|
||||
create_v3_session(addr, timeout, v3_config)
|
||||
} else {
|
||||
create_v1v2c_session(
|
||||
addr,
|
||||
config.community.expose().as_bytes(),
|
||||
timeout,
|
||||
version_num,
|
||||
)
|
||||
}
|
||||
/// Perform SNMP GET using C FFI
|
||||
fn perform_get(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
client: &SnmpClient,
|
||||
config: &DeviceConfig,
|
||||
oid: &str,
|
||||
) -> SnmpResult<SnmpValue> {
|
||||
// Use the thread-local runtime to execute async C FFI call
|
||||
runtime.block_on(async {
|
||||
client
|
||||
.get(
|
||||
&config.ip,
|
||||
config.community.expose(),
|
||||
&config.version,
|
||||
config.port,
|
||||
oid,
|
||||
config.v3_config.clone(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
/// Create v1/v2c session
|
||||
fn create_v1v2c_session(
|
||||
addr: &str,
|
||||
community: &[u8],
|
||||
timeout: Option<Duration>,
|
||||
version: i32,
|
||||
) -> Result<SyncSession, String> {
|
||||
let req_id = 1;
|
||||
|
||||
let result = match version {
|
||||
0 => SyncSession::new_v1(addr, community, timeout, req_id),
|
||||
1 => SyncSession::new_v2c(addr, community, timeout, req_id),
|
||||
_ => return Err(format!("Unsupported SNMP version: {}", version)),
|
||||
};
|
||||
|
||||
result.map_err(|e| format!("Failed to create v1/v2c session: {:?}", e))
|
||||
}
|
||||
|
||||
/// Create v3 session
|
||||
fn create_v3_session(
|
||||
addr: &str,
|
||||
timeout: Option<Duration>,
|
||||
config: &V3Config,
|
||||
) -> Result<SyncSession, String> {
|
||||
use snmp2::v3::{Auth, Security};
|
||||
|
||||
// Parse security level and build Auth enum
|
||||
let auth = match config.security_level.trim().to_lowercase().as_str() {
|
||||
"noauthnopriv" | "" => Auth::NoAuthNoPriv,
|
||||
"authnopriv" => Auth::AuthNoPriv,
|
||||
"authpriv" => {
|
||||
let cipher = parse_priv_protocol(
|
||||
config
|
||||
.priv_protocol
|
||||
.as_deref()
|
||||
.ok_or("Priv protocol required for authPriv")?,
|
||||
)?;
|
||||
let priv_pass = config
|
||||
.priv_password
|
||||
.as_ref()
|
||||
.map(|s| s.expose())
|
||||
.ok_or("Priv password required for authPriv")?;
|
||||
|
||||
Auth::AuthPriv {
|
||||
cipher,
|
||||
privacy_password: priv_pass.as_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Unsupported security level: '{}'",
|
||||
config.security_level
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let username = config.username.as_bytes();
|
||||
let auth_password = config
|
||||
.auth_password
|
||||
.as_ref()
|
||||
.map(|s| s.expose())
|
||||
.unwrap_or("")
|
||||
.as_bytes();
|
||||
let needs_auth_protocol = !matches!(auth, Auth::NoAuthNoPriv);
|
||||
|
||||
let mut security = Security::new(username, auth_password).with_auth(auth);
|
||||
|
||||
if needs_auth_protocol {
|
||||
let auth_proto = parse_auth_protocol(config.auth_protocol.as_deref().unwrap())?;
|
||||
security = security.with_auth_protocol(auth_proto);
|
||||
}
|
||||
|
||||
let req_id = 1;
|
||||
let mut session = SyncSession::new_v3(addr, timeout, req_id, security)
|
||||
.map_err(|e| format!("Failed to create v3 session: {:?}", e))?;
|
||||
|
||||
// For authPriv/authNoPriv, perform engine ID discovery using session.init()
|
||||
if needs_auth_protocol {
|
||||
session
|
||||
.init()
|
||||
.map_err(|e| format!("Engine ID discovery failed: {:?}", e))?;
|
||||
}
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Perform SNMP GET on existing session (with retry for v3 engine ID discovery)
|
||||
fn perform_get(session: &mut SyncSession, oid: &str) -> SnmpResult<SnmpValue> {
|
||||
let oid_parsed =
|
||||
Oid::from_str(oid).map_err(|_| SnmpError::InvalidOid(format!("Invalid OID: {}", oid)))?;
|
||||
|
||||
// First attempt (may fail with AuthUpdated for v3 engine ID discovery)
|
||||
let mut response = match session.get(&oid_parsed) {
|
||||
Ok(resp) => resp,
|
||||
Err(snmp2::Error::AuthUpdated) => {
|
||||
tracing::debug!("SNMPv3 engine ID discovered, retrying request");
|
||||
// Retry after engine ID discovery
|
||||
session.get(&oid_parsed).map_err(map_snmp_error)?
|
||||
}
|
||||
Err(e) => return Err(map_snmp_error(e)),
|
||||
};
|
||||
|
||||
if response.error_status != 0 {
|
||||
return Err(SnmpError::RequestFailed(format!(
|
||||
"SNMP error status: {}",
|
||||
response.error_status
|
||||
)));
|
||||
}
|
||||
|
||||
let (_, value) = response
|
||||
.varbinds
|
||||
.next()
|
||||
.ok_or(SnmpError::RequestFailed("No varbinds in response".into()))?;
|
||||
|
||||
convert_value(value)
|
||||
}
|
||||
|
||||
/// Perform SNMP WALK on existing session (with retry for v3 engine ID discovery)
|
||||
fn perform_walk(session: &mut SyncSession, base_oid: &str) -> SnmpResult<Vec<(String, SnmpValue)>> {
|
||||
let base_oid_parsed = Oid::from_str(base_oid)
|
||||
.map_err(|_| SnmpError::InvalidOid(format!("Invalid OID: {}", base_oid)))?;
|
||||
let base_oid_string = base_oid.to_string();
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut current_oid = base_oid_parsed;
|
||||
let mut first_request = true;
|
||||
|
||||
loop {
|
||||
let oid_to_query = current_oid.clone();
|
||||
|
||||
let (error_status, varbind_data) = {
|
||||
// First request may fail with AuthUpdated for v3 engine ID discovery
|
||||
let response = match session.getnext(&oid_to_query) {
|
||||
Ok(resp) => resp,
|
||||
Err(snmp2::Error::AuthUpdated) if first_request => {
|
||||
tracing::debug!("SNMPv3 engine ID discovered, retrying WALK request");
|
||||
// Retry after engine ID discovery
|
||||
session.getnext(&oid_to_query).map_err(map_snmp_error)?
|
||||
}
|
||||
Err(e) => return Err(map_snmp_error(e)),
|
||||
};
|
||||
|
||||
// After first request, don't retry on AuthUpdated
|
||||
first_request = false;
|
||||
let status = response.error_status;
|
||||
|
||||
let data: Vec<(String, snmp2::Value)> = response
|
||||
.varbinds
|
||||
.map(|(name, value)| (name.to_string(), value))
|
||||
.collect();
|
||||
|
||||
(status, data)
|
||||
};
|
||||
|
||||
if error_status != 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if varbind_data.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (name_str, value) in varbind_data {
|
||||
if !name_str.starts_with(&base_oid_string) {
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
let converted_value = convert_value(value)?;
|
||||
results.push((name_str.clone(), converted_value));
|
||||
|
||||
current_oid = Oid::from_str(&name_str).map_err(|_| {
|
||||
SnmpError::InvalidOid(format!("Invalid OID from response: {}", name_str))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Parse SNMP version string to integer
|
||||
fn parse_snmp_version(version: &str) -> Result<i32, String> {
|
||||
match version.trim().to_lowercase().as_str() {
|
||||
"1" | "v1" | "snmpv1" => Ok(0),
|
||||
"2c" | "v2c" | "snmpv2c" | "2" | "v2" => Ok(1),
|
||||
"3" | "v3" | "snmpv3" => Ok(3),
|
||||
_ => Err(format!("Unsupported SNMP version: '{}'", version)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse authentication protocol
|
||||
fn parse_auth_protocol(protocol: &str) -> Result<snmp2::v3::AuthProtocol, String> {
|
||||
use snmp2::v3::AuthProtocol;
|
||||
|
||||
match protocol.trim().to_uppercase().as_str() {
|
||||
"MD5" => Ok(AuthProtocol::Md5),
|
||||
"SHA" | "SHA1" | "SHA-1" => Ok(AuthProtocol::Sha1),
|
||||
"SHA224" | "SHA-224" => Ok(AuthProtocol::Sha224),
|
||||
"SHA256" | "SHA-256" => Ok(AuthProtocol::Sha256),
|
||||
"SHA384" | "SHA-384" => Ok(AuthProtocol::Sha384),
|
||||
"SHA512" | "SHA-512" => Ok(AuthProtocol::Sha512),
|
||||
_ => Err(format!("Unsupported auth protocol: '{}'", protocol)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse privacy protocol
|
||||
fn parse_priv_protocol(protocol: &str) -> Result<snmp2::v3::Cipher, String> {
|
||||
use snmp2::v3::Cipher;
|
||||
|
||||
match protocol.trim().to_uppercase().as_str() {
|
||||
"DES" => Ok(Cipher::Des),
|
||||
"AES" | "AES128" | "AES-128" => Ok(Cipher::Aes128),
|
||||
"AES192" | "AES-192" => Ok(Cipher::Aes192),
|
||||
"AES256" | "AES-256" | "AES-256-C" => Ok(Cipher::Aes256),
|
||||
_ => Err(format!("Unsupported priv protocol: '{}'", protocol)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map snmp2 errors to our error type
|
||||
fn map_snmp_error(err: snmp2::Error) -> SnmpError {
|
||||
match &err {
|
||||
snmp2::Error::AuthFailure(kind) => {
|
||||
tracing::error!("SNMPv3 AuthFailure: {:?}", kind);
|
||||
SnmpError::AuthFailure
|
||||
}
|
||||
snmp2::Error::AuthUpdated => {
|
||||
tracing::info!("SNMPv3 engine ID discovered, caller should retry");
|
||||
SnmpError::RequestFailed("Authentication context updated, need to retry".into())
|
||||
}
|
||||
snmp2::Error::CommunityMismatch => SnmpError::AuthFailure,
|
||||
_ => {
|
||||
tracing::error!("SNMP error: {:?}", err);
|
||||
SnmpError::RequestFailed(format!("SNMP request failed: {:?}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert snmp2::Value to our SnmpValue
|
||||
fn convert_value(value: snmp2::Value) -> SnmpResult<SnmpValue> {
|
||||
match value {
|
||||
snmp2::Value::Integer(i) => Ok(SnmpValue::Integer(i)),
|
||||
snmp2::Value::OctetString(bytes) => Ok(String::from_utf8(bytes.to_vec())
|
||||
.map(SnmpValue::String)
|
||||
.unwrap_or_else(|_| SnmpValue::OctetString(bytes.to_vec()))),
|
||||
snmp2::Value::ObjectIdentifier(oid) => Ok(SnmpValue::Oid(oid.to_string())),
|
||||
snmp2::Value::Counter32(c) => Ok(SnmpValue::Counter32(c)),
|
||||
snmp2::Value::Counter64(c) => Ok(SnmpValue::Counter64(c)),
|
||||
snmp2::Value::Unsigned32(g) => Ok(SnmpValue::Gauge32(g)),
|
||||
snmp2::Value::Timeticks(t) => Ok(SnmpValue::TimeTicks(t)),
|
||||
snmp2::Value::IpAddress(ip) => Ok(SnmpValue::IpAddress(format!(
|
||||
"{}.{}.{}.{}",
|
||||
ip[0], ip[1], ip[2], ip[3]
|
||||
))),
|
||||
snmp2::Value::Null => Ok(SnmpValue::Null),
|
||||
_ => Ok(SnmpValue::Unsupported(format!("{:?}", value))),
|
||||
}
|
||||
/// Perform SNMP WALK using C FFI
|
||||
fn perform_walk(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
client: &SnmpClient,
|
||||
config: &DeviceConfig,
|
||||
base_oid: &str,
|
||||
) -> SnmpResult<Vec<(String, SnmpValue)>> {
|
||||
// Use the thread-local runtime to execute async C FFI call
|
||||
runtime.block_on(async {
|
||||
client
|
||||
.walk(
|
||||
&config.ip,
|
||||
config.community.expose(),
|
||||
&config.version,
|
||||
config.port,
|
||||
base_oid,
|
||||
config.v3_config.clone(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -487,92 +282,18 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_auth_protocol_standard() {
|
||||
use snmp2::v3::AuthProtocol;
|
||||
assert!(matches!(parse_auth_protocol("MD5"), Ok(AuthProtocol::Md5)));
|
||||
assert!(matches!(parse_auth_protocol("SHA"), Ok(AuthProtocol::Sha1)));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA1"),
|
||||
Ok(AuthProtocol::Sha1)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA256"),
|
||||
Ok(AuthProtocol::Sha256)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA384"),
|
||||
Ok(AuthProtocol::Sha384)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA512"),
|
||||
Ok(AuthProtocol::Sha512)
|
||||
));
|
||||
}
|
||||
fn test_device_config_debug() {
|
||||
let config = DeviceConfig {
|
||||
ip: "192.168.1.1".to_string(),
|
||||
port: 161,
|
||||
version: "2c".to_string(),
|
||||
community: SecretString::new("public"),
|
||||
v3_config: None,
|
||||
transport: "udp".to_string(),
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_auth_protocol_hyphenated() {
|
||||
use snmp2::v3::AuthProtocol;
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-1"),
|
||||
Ok(AuthProtocol::Sha1)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-224"),
|
||||
Ok(AuthProtocol::Sha224)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-256"),
|
||||
Ok(AuthProtocol::Sha256)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-384"),
|
||||
Ok(AuthProtocol::Sha384)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-512"),
|
||||
Ok(AuthProtocol::Sha512)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_auth_protocol_case_insensitive() {
|
||||
use snmp2::v3::AuthProtocol;
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("sha-256"),
|
||||
Ok(AuthProtocol::Sha256)
|
||||
));
|
||||
assert!(matches!(parse_auth_protocol("md5"), Ok(AuthProtocol::Md5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_auth_protocol_invalid() {
|
||||
assert!(parse_auth_protocol("INVALID").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_priv_protocol_standard() {
|
||||
use snmp2::v3::Cipher;
|
||||
assert!(matches!(parse_priv_protocol("DES"), Ok(Cipher::Des)));
|
||||
assert!(matches!(parse_priv_protocol("AES"), Ok(Cipher::Aes128)));
|
||||
assert!(matches!(parse_priv_protocol("AES128"), Ok(Cipher::Aes128)));
|
||||
assert!(matches!(parse_priv_protocol("AES192"), Ok(Cipher::Aes192)));
|
||||
assert!(matches!(parse_priv_protocol("AES256"), Ok(Cipher::Aes256)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_priv_protocol_hyphenated() {
|
||||
use snmp2::v3::Cipher;
|
||||
assert!(matches!(parse_priv_protocol("AES-128"), Ok(Cipher::Aes128)));
|
||||
assert!(matches!(parse_priv_protocol("AES-192"), Ok(Cipher::Aes192)));
|
||||
assert!(matches!(parse_priv_protocol("AES-256"), Ok(Cipher::Aes256)));
|
||||
assert!(matches!(
|
||||
parse_priv_protocol("AES-256-C"),
|
||||
Ok(Cipher::Aes256)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_priv_protocol_invalid() {
|
||||
assert!(parse_priv_protocol("INVALID").is_err());
|
||||
let debug_str = format!("{:?}", config);
|
||||
assert!(debug_str.contains("[REDACTED]"));
|
||||
assert!(!debug_str.contains("public"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,10 @@ impl AgentState {
|
|||
)
|
||||
}
|
||||
AgentEvent::MonitoringCheckSent { device_id, status } => {
|
||||
format!("Monitoring check sent for {} (status: {})", device_id, status)
|
||||
format!(
|
||||
"Monitoring check sent for {} (status: {})",
|
||||
device_id, status
|
||||
)
|
||||
}
|
||||
AgentEvent::PollerCreated {
|
||||
device_ip,
|
||||
|
|
|
|||
|
|
@ -901,12 +901,16 @@ async fn execute_snmp_job(
|
|||
let config = crate::snmp::V3Config {
|
||||
username: snmp_device.v3_username.clone(),
|
||||
auth_password: if !snmp_device.v3_auth_password.is_empty() {
|
||||
Some(SecretString::new(snmp_device.v3_auth_password.clone()))
|
||||
Some(zeroize::Zeroizing::new(
|
||||
snmp_device.v3_auth_password.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
priv_password: if !snmp_device.v3_priv_password.is_empty() {
|
||||
Some(SecretString::new(snmp_device.v3_priv_password.clone()))
|
||||
Some(zeroize::Zeroizing::new(
|
||||
snmp_device.v3_priv_password.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
@ -1087,12 +1091,16 @@ async fn execute_credential_test(
|
|||
Some(crate::snmp::V3Config {
|
||||
username: snmp_device.v3_username.clone(),
|
||||
auth_password: if !snmp_device.v3_auth_password.is_empty() {
|
||||
Some(SecretString::new(snmp_device.v3_auth_password.clone()))
|
||||
Some(zeroize::Zeroizing::new(
|
||||
snmp_device.v3_auth_password.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
priv_password: if !snmp_device.v3_priv_password.is_empty() {
|
||||
Some(SecretString::new(snmp_device.v3_priv_password.clone()))
|
||||
Some(zeroize::Zeroizing::new(
|
||||
snmp_device.v3_priv_password.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
@ -1173,10 +1181,7 @@ async fn execute_credential_test(
|
|||
}
|
||||
|
||||
/// Execute a ping job using ICMP ping to check device health.
|
||||
async fn execute_ping_job(
|
||||
job: AgentJob,
|
||||
result_tx: mpsc::Sender<MonitoringCheck>,
|
||||
) -> Result<()> {
|
||||
async fn execute_ping_job(job: AgentJob, result_tx: mpsc::Sender<MonitoringCheck>) -> Result<()> {
|
||||
let device_id = job.device_id.clone();
|
||||
let snmp_device = job.snmp_device.ok_or("Job missing SNMP device info")?;
|
||||
let ip_address = &snmp_device.ip;
|
||||
|
|
@ -1184,7 +1189,11 @@ async fn execute_ping_job(
|
|||
// Use 5-second timeout for pings (same as Phoenix DeviceMonitorWorker)
|
||||
let timeout_ms = 5000;
|
||||
|
||||
tracing::debug!("Executing health check for device {} at {}", device_id, ip_address);
|
||||
tracing::debug!(
|
||||
"Executing health check for device {} at {}",
|
||||
device_id,
|
||||
ip_address
|
||||
);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue