snmpv3 first stab
This commit is contained in:
parent
4719bb8249
commit
0f19518065
7 changed files with 452 additions and 178 deletions
163
Cargo.lock
generated
163
Cargo.lock
generated
|
|
@ -129,6 +129,44 @@ 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",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
|
|
@ -503,6 +541,17 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
|
|
@ -653,6 +702,21 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[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"
|
||||
|
|
@ -1048,6 +1112,12 @@ version = "2.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -1087,6 +1157,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
|
|
@ -1171,6 +1251,44 @@ 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",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[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 = "p256"
|
||||
version = "0.13.2"
|
||||
|
|
@ -1317,6 +1435,12 @@ dependencies = [
|
|||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
|
|
@ -1673,6 +1797,15 @@ 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 = "1.1.3"
|
||||
|
|
@ -1887,10 +2020,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "snmp"
|
||||
version = "0.2.2"
|
||||
name = "snmp2"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2a575449a5c487091e541c0cb4ccd83620167fd52363f816fe28f6f357fc00"
|
||||
checksum = "49dc4ff9dc6c2b03d818a4c2abb8ffcef9e16357113ddc7ce1631c03195fde64"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"openssl",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
|
|
@ -1970,6 +2108,17 @@ 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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
|
|
@ -2099,7 +2248,7 @@ dependencies = [
|
|||
"russh",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snmp",
|
||||
"snmp2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
|
@ -2240,6 +2389,12 @@ 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,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
snmp = "0.2"
|
||||
snmp2 = { version = "0.4", features = ["v3", "tokio"] }
|
||||
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"] }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ ARG VERSION=0.1.0-unknown
|
|||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache musl-dev protobuf-dev
|
||||
RUN apk add --no-cache musl-dev protobuf-dev openssl-dev
|
||||
|
||||
# Determine Rust target based on platform and add it
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
|
|
@ -56,7 +56,7 @@ 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
|
||||
RUN apk add --no-cache ca-certificates iputils openssl
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /tmp/towerops-agent /usr/local/bin/towerops-agent
|
||||
|
|
|
|||
|
|
@ -136,9 +136,17 @@ message AgentJob {
|
|||
|
||||
message SnmpDevice {
|
||||
string ip = 1;
|
||||
string community = 2;
|
||||
string community = 2; // v1/v2c only (deprecated for v3)
|
||||
string version = 3;
|
||||
uint32 port = 4;
|
||||
|
||||
// SNMPv3 credentials (optional, backward compatible)
|
||||
string v3_security_level = 5; // "noAuthNoPriv" | "authNoPriv" | "authPriv"
|
||||
string v3_username = 6;
|
||||
string v3_auth_protocol = 7; // "MD5" | "SHA" | "SHA-256" | etc.
|
||||
string v3_auth_password = 8; // Decrypted before sending
|
||||
string v3_priv_protocol = 9; // "DES" | "AES" | "AES-256" | etc.
|
||||
string v3_priv_password = 10; // Decrypted before sending
|
||||
}
|
||||
|
||||
message SnmpQuery {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
use super::types::{SnmpError, SnmpResult, SnmpValue};
|
||||
use snmp::SyncSession;
|
||||
use snmp2::SyncSession;
|
||||
use std::time::Duration;
|
||||
|
||||
// SNMP timeout in seconds - increased from 5s to 30s to match Phoenix app
|
||||
// and reduce timeouts for slow devices or congested networks
|
||||
const SNMP_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// SNMPv3 configuration bundle
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct V3Config {
|
||||
pub username: String,
|
||||
pub auth_password: Option<String>,
|
||||
pub priv_password: Option<String>,
|
||||
pub auth_protocol: Option<String>,
|
||||
pub priv_protocol: Option<String>,
|
||||
pub security_level: String,
|
||||
}
|
||||
|
||||
/// SNMP client for polling devices
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SnmpClient;
|
||||
|
|
@ -23,9 +34,14 @@ impl SnmpClient {
|
|||
version: &str,
|
||||
port: u16,
|
||||
oid: &str,
|
||||
v3_config: Option<V3Config>,
|
||||
) -> SnmpResult<SnmpValue> {
|
||||
// Parse OID string to Vec<u32>
|
||||
let oid_parts = parse_oid(oid)?;
|
||||
use snmp2::Oid;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Parse OID string
|
||||
let oid_parsed = Oid::from_str(oid)
|
||||
.map_err(|_| SnmpError::InvalidOid(format!("Invalid OID: {}", oid)))?;
|
||||
|
||||
// Clone data for the blocking task
|
||||
let addr = format!("{}:{}", ip_address, port);
|
||||
|
|
@ -34,18 +50,18 @@ impl SnmpClient {
|
|||
|
||||
// Run SNMP operation in blocking thread pool
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
// Create session with configurable timeout
|
||||
// The snmp crate supports both v1 (0) and v2c (1)
|
||||
let mut session = SyncSession::new(
|
||||
addr.as_str(),
|
||||
&community,
|
||||
Some(Duration::from_secs(SNMP_TIMEOUT_SECS)),
|
||||
version_num,
|
||||
)
|
||||
.map_err(|_| SnmpError::NetworkUnreachable)?;
|
||||
// Create session based on version
|
||||
let mut session = if version_num == 3 {
|
||||
let config = v3_config.ok_or_else(|| {
|
||||
SnmpError::RequestFailed("SNMPv3 config required for version 3".into())
|
||||
})?;
|
||||
create_v3_session(&addr, &config)?
|
||||
} else {
|
||||
create_v1v2c_session(&addr, &community, version_num)?
|
||||
};
|
||||
|
||||
// Perform GET request
|
||||
let mut response = session.get(&oid_parts).map_err(map_snmp_error)?;
|
||||
let response = session.get(&oid_parsed).map_err(map_snmp_error)?;
|
||||
|
||||
// Check for error status
|
||||
if response.error_status != 0 {
|
||||
|
|
@ -56,7 +72,7 @@ impl SnmpClient {
|
|||
}
|
||||
|
||||
// Extract first varbind
|
||||
if let Some((_name, value)) = response.varbinds.next() {
|
||||
for (_name, value) in response.varbinds {
|
||||
return convert_value(value);
|
||||
}
|
||||
|
||||
|
|
@ -77,9 +93,15 @@ impl SnmpClient {
|
|||
version: &str,
|
||||
port: u16,
|
||||
base_oid: &str,
|
||||
v3_config: Option<V3Config>,
|
||||
) -> SnmpResult<Vec<(String, SnmpValue)>> {
|
||||
// Parse OID string to Vec<u32>
|
||||
let base_oid_parts = parse_oid(base_oid)?;
|
||||
use snmp2::Oid;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Parse base OID string and clone it for moving into async block
|
||||
let base_oid_parsed = Oid::from_str(base_oid)
|
||||
.map_err(|_| SnmpError::InvalidOid(format!("Invalid base OID: {}", base_oid)))?;
|
||||
let base_oid_string = base_oid.to_string();
|
||||
|
||||
// Clone data for the blocking task
|
||||
let addr = format!("{}:{}", ip_address, port);
|
||||
|
|
@ -88,55 +110,63 @@ impl SnmpClient {
|
|||
|
||||
// Run SNMP walk in blocking thread pool
|
||||
let results = tokio::task::spawn_blocking(move || {
|
||||
// Create session with configurable timeout
|
||||
// The snmp crate supports both v1 (0) and v2c (1)
|
||||
let mut session = SyncSession::new(
|
||||
addr.as_str(),
|
||||
&community,
|
||||
Some(Duration::from_secs(SNMP_TIMEOUT_SECS)),
|
||||
version_num,
|
||||
)
|
||||
.map_err(|_| SnmpError::NetworkUnreachable)?;
|
||||
// Create session based on version
|
||||
let mut session = if version_num == 3 {
|
||||
let config = v3_config.ok_or_else(|| {
|
||||
SnmpError::RequestFailed("SNMPv3 config required for version 3".into())
|
||||
})?;
|
||||
create_v3_session(&addr, &config)?
|
||||
} else {
|
||||
create_v1v2c_session(&addr, &community, version_num)?
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut current_oid = base_oid_parts.clone();
|
||||
let mut current_oid = base_oid_parsed;
|
||||
|
||||
// Perform GETNEXT repeatedly until we leave the base OID tree
|
||||
loop {
|
||||
let response = session.getnext(¤t_oid).map_err(map_snmp_error)?;
|
||||
let oid_to_query = current_oid.clone();
|
||||
|
||||
// Extract data we need from the response immediately
|
||||
let (error_status, varbind_data) = {
|
||||
let response = session.getnext(&oid_to_query).map_err(map_snmp_error)?;
|
||||
let status = response.error_status;
|
||||
|
||||
// Collect all varbind data as owned strings/values immediately
|
||||
let data: Vec<(String, snmp2::Value)> = response
|
||||
.varbinds
|
||||
.map(|(name, value)| (name.to_string(), value))
|
||||
.collect();
|
||||
|
||||
(status, data)
|
||||
// response is dropped here, ending the mutable borrow of session
|
||||
};
|
||||
|
||||
// Check for error status
|
||||
if response.error_status != 0 {
|
||||
if error_status != 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract varbind
|
||||
for (name, value) in response.varbinds {
|
||||
// Convert ObjectIdentifier to Vec<u32>
|
||||
let mut oid_buf = [0u32; 128];
|
||||
let oid_slice = name
|
||||
.read_name(&mut oid_buf)
|
||||
.map_err(|_| SnmpError::InvalidOid("Failed to read OID".into()))?;
|
||||
if varbind_data.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we're still under the base OID
|
||||
if !starts_with(oid_slice, &base_oid_parts) {
|
||||
// Process the owned varbind data
|
||||
for (name_str, value) in varbind_data {
|
||||
// Check if the returned OID starts with our base OID
|
||||
if !name_str.starts_with(&base_oid_string) {
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
// Convert OID to string
|
||||
let oid_string = format_oid(oid_slice);
|
||||
|
||||
// Convert value
|
||||
let converted_value = convert_value(value)?;
|
||||
|
||||
results.push((oid_string, converted_value));
|
||||
results.push((name_str.clone(), converted_value));
|
||||
|
||||
// Update current OID for next iteration
|
||||
current_oid = oid_slice.to_vec();
|
||||
}
|
||||
|
||||
if results.is_empty() {
|
||||
break;
|
||||
// Update current OID for next iteration (parse from string)
|
||||
current_oid = Oid::from_str(&name_str).map_err(|_| {
|
||||
SnmpError::InvalidOid(format!("Invalid OID from response: {}", name_str))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,52 +185,150 @@ impl Default for SnmpClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse SNMP version string to integer for snmp crate
|
||||
/// Returns: 0 for SNMPv1, 1 for SNMPv2c
|
||||
/// Parse SNMP version string to integer for snmp2 crate
|
||||
/// Returns: 0 for SNMPv1, 1 for SNMPv2c, 3 for SNMPv3
|
||||
fn parse_snmp_version(version: &str) -> SnmpResult<i32> {
|
||||
let normalized = version.trim().to_lowercase();
|
||||
|
||||
match normalized.as_str() {
|
||||
"1" | "v1" | "snmpv1" => Ok(0),
|
||||
"2c" | "v2c" | "snmpv2c" | "2" | "v2" => Ok(1),
|
||||
"3" | "v3" | "snmpv3" => Ok(3),
|
||||
_ => Err(SnmpError::RequestFailed(format!(
|
||||
"Unsupported SNMP version: '{}'. Supported versions: 1, v1, 2c, v2c",
|
||||
"Unsupported SNMP version: '{}'. Supported versions: 1, v1, 2c, v2c, 3, v3",
|
||||
version
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse OID string like "1.3.6.1.2.1.1.1.0" to Vec<u32>
|
||||
fn parse_oid(oid: &str) -> SnmpResult<Vec<u32>> {
|
||||
oid.split('.')
|
||||
.map(|part| {
|
||||
part.parse::<u32>()
|
||||
.map_err(|_| SnmpError::InvalidOid(format!("Invalid OID part: {}", part)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// Parse authentication protocol string to snmp2 AuthProtocol enum
|
||||
fn parse_auth_protocol(protocol: &str) -> SnmpResult<snmp2::v3::AuthProtocol> {
|
||||
use snmp2::v3::AuthProtocol;
|
||||
|
||||
/// Format OID slice as string like "1.3.6.1.2.1.1.1.0"
|
||||
#[allow(dead_code)] // Used by walk() which is not yet called
|
||||
fn format_oid(oid: &[u32]) -> String {
|
||||
oid.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
}
|
||||
|
||||
/// Check if OID starts with base OID
|
||||
#[allow(dead_code)] // Used by walk() which is not yet called
|
||||
fn starts_with(oid: &[u32], base: &[u32]) -> bool {
|
||||
if oid.len() < base.len() {
|
||||
return false;
|
||||
match protocol.trim().to_uppercase().as_str() {
|
||||
"MD5" => Ok(AuthProtocol::Md5),
|
||||
"SHA" | "SHA1" | "SHA-1" => Ok(AuthProtocol::Sha1),
|
||||
"SHA-224" | "SHA224" => Ok(AuthProtocol::Sha224),
|
||||
"SHA-256" | "SHA256" => Ok(AuthProtocol::Sha256),
|
||||
"SHA-384" | "SHA384" => Ok(AuthProtocol::Sha384),
|
||||
"SHA-512" | "SHA512" => Ok(AuthProtocol::Sha512),
|
||||
_ => Err(SnmpError::RequestFailed(format!(
|
||||
"Unsupported auth protocol: '{}'",
|
||||
protocol
|
||||
))),
|
||||
}
|
||||
oid[..base.len()] == *base
|
||||
}
|
||||
|
||||
/// Convert snmp crate's Value to our SnmpValue
|
||||
fn convert_value(value: snmp::Value) -> SnmpResult<SnmpValue> {
|
||||
use snmp::Value as V;
|
||||
/// Parse privacy protocol string to snmp2 Cipher enum
|
||||
fn parse_priv_protocol(protocol: &str) -> SnmpResult<snmp2::v3::Cipher> {
|
||||
use snmp2::v3::Cipher;
|
||||
|
||||
match protocol.trim().to_uppercase().as_str() {
|
||||
"DES" => Ok(Cipher::Des),
|
||||
"AES" | "AES-128" | "AES128" => Ok(Cipher::Aes128),
|
||||
"AES-192" | "AES192" => Ok(Cipher::Aes192),
|
||||
"AES-256" | "AES256" | "AES-256-C" | "AES256C" => Ok(Cipher::Aes256),
|
||||
_ => Err(SnmpError::RequestFailed(format!(
|
||||
"Unsupported priv protocol: '{}'",
|
||||
protocol
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SNMPv1/v2c session
|
||||
fn create_v1v2c_session(addr: &str, community: &[u8], version: i32) -> SnmpResult<SyncSession> {
|
||||
let timeout = Some(Duration::from_secs(SNMP_TIMEOUT_SECS));
|
||||
let req_id = 1;
|
||||
|
||||
if version == 0 {
|
||||
SyncSession::new_v1(addr, community, timeout, req_id)
|
||||
.map_err(|_| SnmpError::NetworkUnreachable)
|
||||
} else {
|
||||
SyncSession::new_v2c(addr, community, timeout, req_id)
|
||||
.map_err(|_| SnmpError::NetworkUnreachable)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SNMPv3 session with authentication and/or privacy
|
||||
fn create_v3_session(addr: &str, config: &V3Config) -> SnmpResult<SyncSession> {
|
||||
use snmp2::v3::{Auth, Security};
|
||||
|
||||
let username = config.username.as_bytes();
|
||||
let security_level = config.security_level.trim().to_lowercase();
|
||||
|
||||
// Build the Auth enum based on security level
|
||||
let auth = match security_level.as_str() {
|
||||
"noauthnopriv" | "" => Auth::NoAuthNoPriv,
|
||||
|
||||
"authnopriv" => {
|
||||
// Requires auth protocol and password
|
||||
let _auth_proto =
|
||||
parse_auth_protocol(config.auth_protocol.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Auth protocol required for authNoPriv".into())
|
||||
})?)?;
|
||||
let _auth_pass = config.auth_password.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Auth password required for authNoPriv".into())
|
||||
})?;
|
||||
Auth::AuthNoPriv
|
||||
}
|
||||
|
||||
"authpriv" => {
|
||||
// Requires auth and priv protocol/passwords
|
||||
let _auth_proto =
|
||||
parse_auth_protocol(config.auth_protocol.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Auth protocol required for authPriv".into())
|
||||
})?)?;
|
||||
let _auth_pass = config.auth_password.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Auth password required for authPriv".into())
|
||||
})?;
|
||||
let cipher =
|
||||
parse_priv_protocol(config.priv_protocol.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Priv protocol required for authPriv".into())
|
||||
})?)?;
|
||||
let priv_pass = config.priv_password.as_deref().ok_or_else(|| {
|
||||
SnmpError::RequestFailed("Priv password required for authPriv".into())
|
||||
})?;
|
||||
|
||||
Auth::AuthPriv {
|
||||
cipher,
|
||||
privacy_password: priv_pass.as_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(SnmpError::RequestFailed(format!(
|
||||
"Unsupported security level: '{}'",
|
||||
config.security_level
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Get auth password (empty for noAuthNoPriv)
|
||||
let auth_password = config.auth_password.as_deref().unwrap_or("").as_bytes();
|
||||
|
||||
// Determine if we need auth protocol (before auth is moved)
|
||||
let needs_auth_protocol = !matches!(auth, Auth::NoAuthNoPriv);
|
||||
|
||||
// Build Security object
|
||||
let mut security = Security::new(username, auth_password).with_auth(auth);
|
||||
|
||||
// Set auth protocol if authentication is required
|
||||
if needs_auth_protocol {
|
||||
let auth_proto = parse_auth_protocol(config.auth_protocol.as_deref().unwrap())?;
|
||||
security = security.with_auth_protocol(auth_proto);
|
||||
}
|
||||
|
||||
// Create v3 session
|
||||
let timeout = Some(Duration::from_secs(SNMP_TIMEOUT_SECS));
|
||||
let req_id = 1;
|
||||
|
||||
SyncSession::new_v3(addr, timeout, req_id, security)
|
||||
.map_err(|e| SnmpError::RequestFailed(format!("SNMPv3 session creation failed: {:?}", e)))
|
||||
}
|
||||
|
||||
/// Convert snmp2 crate's Value to our SnmpValue
|
||||
fn convert_value(value: snmp2::Value) -> SnmpResult<SnmpValue> {
|
||||
use snmp2::Value as V;
|
||||
|
||||
match value {
|
||||
V::Integer(i) => Ok(SnmpValue::Integer(i)),
|
||||
|
|
@ -220,14 +348,15 @@ fn convert_value(value: snmp::Value) -> SnmpResult<SnmpValue> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Map snmp crate errors to our SnmpError
|
||||
fn map_snmp_error(err: snmp::SnmpError) -> SnmpError {
|
||||
use snmp::SnmpError as SE;
|
||||
/// Map snmp2 crate errors to our SnmpError
|
||||
fn map_snmp_error(err: snmp2::Error) -> SnmpError {
|
||||
use snmp2::Error;
|
||||
|
||||
match err {
|
||||
SE::SendError => SnmpError::NetworkUnreachable,
|
||||
SE::ReceiveError => SnmpError::Timeout,
|
||||
SE::CommunityMismatch => SnmpError::AuthFailure,
|
||||
Error::Send => SnmpError::NetworkUnreachable,
|
||||
Error::Receive => SnmpError::Timeout,
|
||||
Error::CommunityMismatch => SnmpError::AuthFailure,
|
||||
Error::AuthFailure(_) => SnmpError::AuthFailure,
|
||||
_ => SnmpError::RequestFailed(format!("{:?}", err)),
|
||||
}
|
||||
}
|
||||
|
|
@ -267,9 +396,17 @@ mod tests {
|
|||
assert_eq!(parse_snmp_version("v2").unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_snmp_version_v3() {
|
||||
assert_eq!(parse_snmp_version("3").unwrap(), 3);
|
||||
assert_eq!(parse_snmp_version("v3").unwrap(), 3);
|
||||
assert_eq!(parse_snmp_version("V3").unwrap(), 3);
|
||||
assert_eq!(parse_snmp_version("snmpv3").unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_snmp_version_invalid() {
|
||||
let result = parse_snmp_version("3");
|
||||
let result = parse_snmp_version("invalid");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(SnmpError::RequestFailed(msg)) => {
|
||||
|
|
@ -280,89 +417,31 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_oid_valid() {
|
||||
let result = parse_oid("1.3.6.1.2.1.1.1.0");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), vec![1, 3, 6, 1, 2, 1, 1, 1, 0]);
|
||||
fn test_parse_auth_protocol() {
|
||||
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("SHA-256"),
|
||||
Ok(AuthProtocol::Sha256)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_auth_protocol("SHA-512"),
|
||||
Ok(AuthProtocol::Sha512)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_oid_single() {
|
||||
let result = parse_oid("1");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), vec![1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_oid_invalid() {
|
||||
let result = parse_oid("1.3.6.abc.2.1");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(SnmpError::InvalidOid(msg)) => {
|
||||
assert!(msg.contains("abc"));
|
||||
}
|
||||
_ => panic!("Expected InvalidOid error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_oid_empty() {
|
||||
let result = parse_oid("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_oid() {
|
||||
let oid = vec![1, 3, 6, 1, 2, 1, 1, 1, 0];
|
||||
let result = format_oid(&oid);
|
||||
assert_eq!(result, "1.3.6.1.2.1.1.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_oid_single() {
|
||||
let oid = vec![42];
|
||||
let result = format_oid(&oid);
|
||||
assert_eq!(result, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_oid_empty() {
|
||||
let oid = vec![];
|
||||
let result = format_oid(&oid);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_starts_with_true() {
|
||||
let oid = vec![1, 3, 6, 1, 2, 1, 1, 1, 0];
|
||||
let base = vec![1, 3, 6, 1];
|
||||
assert!(starts_with(&oid, &base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_starts_with_exact_match() {
|
||||
let oid = vec![1, 3, 6, 1];
|
||||
let base = vec![1, 3, 6, 1];
|
||||
assert!(starts_with(&oid, &base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_starts_with_false() {
|
||||
let oid = vec![1, 3, 6, 1, 2, 1];
|
||||
let base = vec![1, 3, 7];
|
||||
assert!(!starts_with(&oid, &base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_starts_with_oid_too_short() {
|
||||
let oid = vec![1, 3];
|
||||
let base = vec![1, 3, 6, 1];
|
||||
assert!(!starts_with(&oid, &base));
|
||||
fn test_parse_priv_protocol() {
|
||||
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("AES-256"), Ok(Cipher::Aes256)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_value_integer() {
|
||||
let value = snmp::Value::Integer(42);
|
||||
let value = snmp2::Value::Integer(42);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::Integer(v) => assert_eq!(v, 42),
|
||||
|
|
@ -372,7 +451,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_octet_string() {
|
||||
let value = snmp::Value::OctetString(b"test".as_slice());
|
||||
let value = snmp2::Value::OctetString(b"test".as_slice());
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::String(s) => assert_eq!(s, "test"),
|
||||
|
|
@ -382,7 +461,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_counter32() {
|
||||
let value = snmp::Value::Counter32(12345);
|
||||
let value = snmp2::Value::Counter32(12345);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::Counter32(v) => assert_eq!(v, 12345),
|
||||
|
|
@ -392,7 +471,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_counter64() {
|
||||
let value = snmp::Value::Counter64(9876543210);
|
||||
let value = snmp2::Value::Counter64(9876543210);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::Counter64(v) => assert_eq!(v, 9876543210),
|
||||
|
|
@ -402,7 +481,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_unsigned32() {
|
||||
let value = snmp::Value::Unsigned32(999);
|
||||
let value = snmp2::Value::Unsigned32(999);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::Gauge32(v) => assert_eq!(v, 999),
|
||||
|
|
@ -412,7 +491,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_timeticks() {
|
||||
let value = snmp::Value::Timeticks(12345678);
|
||||
let value = snmp2::Value::Timeticks(12345678);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::TimeTicks(v) => assert_eq!(v, 12345678),
|
||||
|
|
@ -422,7 +501,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_convert_value_ip_address() {
|
||||
let value = snmp::Value::IpAddress([192, 168, 1, 1]);
|
||||
let value = snmp2::Value::IpAddress([192, 168, 1, 1]);
|
||||
let result = convert_value(value).unwrap();
|
||||
match result {
|
||||
SnmpValue::IpAddress(ip) => assert_eq!(ip, "192.168.1.1"),
|
||||
|
|
@ -432,7 +511,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_map_snmp_error_send() {
|
||||
let err = snmp::SnmpError::SendError;
|
||||
let err = snmp2::Error::Send;
|
||||
let result = map_snmp_error(err);
|
||||
match result {
|
||||
SnmpError::NetworkUnreachable => {}
|
||||
|
|
@ -442,7 +521,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_map_snmp_error_receive() {
|
||||
let err = snmp::SnmpError::ReceiveError;
|
||||
let err = snmp2::Error::Receive;
|
||||
let result = map_snmp_error(err);
|
||||
match result {
|
||||
SnmpError::Timeout => {}
|
||||
|
|
@ -452,7 +531,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_map_snmp_error_community() {
|
||||
let err = snmp::SnmpError::CommunityMismatch;
|
||||
let err = snmp2::Error::CommunityMismatch;
|
||||
let result = map_snmp_error(err);
|
||||
match result {
|
||||
SnmpError::AuthFailure => {}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ mod client;
|
|||
pub mod trap;
|
||||
mod types;
|
||||
|
||||
pub use client::SnmpClient;
|
||||
pub use client::{SnmpClient, V3Config};
|
||||
pub use trap::{SnmpTrap, TrapListener, DEFAULT_TRAP_PORT};
|
||||
pub use types::SnmpValue;
|
||||
|
|
|
|||
|
|
@ -358,6 +358,36 @@ async fn execute_snmp_job(
|
|||
) -> Result<()> {
|
||||
let snmp_device = job.snmp_device.ok_or("Job missing SNMP device info")?;
|
||||
|
||||
// Build v3 config if version is "3"
|
||||
let v3_config = if snmp_device.version == "3" {
|
||||
Some(crate::snmp::V3Config {
|
||||
username: snmp_device.v3_username.clone(),
|
||||
auth_password: if !snmp_device.v3_auth_password.is_empty() {
|
||||
Some(snmp_device.v3_auth_password.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
priv_password: if !snmp_device.v3_priv_password.is_empty() {
|
||||
Some(snmp_device.v3_priv_password.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
auth_protocol: if !snmp_device.v3_auth_protocol.is_empty() {
|
||||
Some(snmp_device.v3_auth_protocol.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
priv_protocol: if !snmp_device.v3_priv_protocol.is_empty() {
|
||||
Some(snmp_device.v3_priv_protocol.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
security_level: snmp_device.v3_security_level.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Log SNMP connection parameters for debugging (mask community for security)
|
||||
let community_masked = redact_community(&snmp_device.community);
|
||||
|
||||
|
|
@ -387,6 +417,7 @@ async fn execute_snmp_job(
|
|||
&snmp_device.version,
|
||||
snmp_device.port as u16,
|
||||
oid,
|
||||
v3_config.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -418,6 +449,7 @@ async fn execute_snmp_job(
|
|||
&snmp_device.version,
|
||||
snmp_device.port as u16,
|
||||
base_oid,
|
||||
v3_config.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue