SNMP
An SNMP manager for v1, v2c and v3 (SHA or MD5 with AES or DES): get, walk, get-bulk, whole tables as rows and set, with MIB names for switches, printers and UPSes; traps and informs sent, and read in a udp.listen handler.
Version 1.0.0 · 12 functions · 6 crates
Functions
What use snmp.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use snmp_get, snmp_values from snmp (Quillo 0.6).
snmp_get(host: str, oid_list: list, options: any) → listor an errorGets values (["sysDescr.0", "sysUpTime.0", "1.3.6.1.2.1.1.5.0"]) from an agent: each { oid, name, type, value }. Options: community ("public"), version ("2c", "1" or "3"), port (161), timeout_ms (3000), retries (1); for v3 user, auth (SHA, MD5, SHA-256...), auth_password, priv (AES or DES), priv_password, context.
snmp_values(host: str, oid_list: list, options: any) → anyor an errorGets values as a map from each name asked for to its value ({ "sysName.0": "plc-hall-2" }): for a few values read by name. A missing one is null.
snmp_get_next(host: str, oid_list: list, options: any) → listor an errorThe next value after each OID (get-next): { oid, name, type, value } each.
snmp_get_bulk(host: str, oid_list: list, non_repeaters: int, max_repetitions: int, options: any) → listor an errorGet-bulk (v2c and v3): the first non_repeaters OIDs once, then max_repetitions values after each of the others.
snmp_walk(host: str, oid: str, options: any) → listor an errorEvery value under an OID ("system", "ifTable", "1.3.6.1.2.1.43.11"), with get-bulk on v2c and v3 and get-next on v1; options as snmp_get, plus max values (10000).
snmp_table(host: str, table: str, options: any) → listor an errorA table walked into rows ("ifTable", "ifXTable", "prtMarkerSuppliesTable", "hrStorageTable"): each { index, <column name>: value, ... }, the index being what follows the column's OID.
snmp_set(host: str, values: list, options: any) → listor an errorSets values: [{ oid: "sysLocation.0", type: "string", value: "Hall 2" }] (types: integer, string, hex, oid, ip, counter32, gauge32, timeticks, counter64; left out, from the value): the agent's answer, each { oid, name, type, value }. Use the write community (often "private").
snmp_trap(host: str, trap_oid: str, values: list, options: any) → anyor an errorSends a trap (or with inform: true, an inform, which waits for the manager's answer) to a manager (port 162 unless given): trap_oid is the event ("linkDown", "1.3.6.1.4.1.9999.0.1"), values the varbinds after sysUpTime.0 and snmpTrapOID.0. v1 traps (version: "1") take enterprise, agent, generic and specific. v3 traps are sent from engine_id (hex).
snmp_parse(bytes: list, options: any) → anyor an errorReads an SNMP message as a udp.listen handler gets it (p.bytes): { version, community, type: "trap" | "inform" | "trap-v1" | "get" | ..., request_id, trap_oid, uptime, values }, a v1 trap's enterprise, generic and specific. For v3, options give user, auth, auth_password, priv and priv_password to check and decrypt it (else only its header is read).
snmp_response_for(bytes: list, values: list) → listor an errorThe answer to a v1 or v2c message as bytes for udp.reply: an inform acknowledged (the same request ID and values, as RFC 3416 says), or a get answered with values ([{ oid, type, value }]).
snmp_oid(name: str) → stror an errorA MIB name as its OID: "ifHCInOctets.2" is "1.3.6.1.2.1.31.1.1.1.6.2" (numbers pass through).
snmp_name(oid: str) → stror an errorAn OID as its MIB name where the plugin knows one: "1.3.6.1.2.1.43.11.1.1.9.1.1" is "prtMarkerSuppliesLevel.1.1".
Get it
$ quillo install snmp # snmp.qwer into this folder$ quillo install snmp --example # and its example, snmp.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/snmp.qwer$ curl -O https://quillolang.org/plugins/files/snmp.qweThen, in your program, all of it or only what you call:
use snmp.qweruse snmp_get, snmp_values from snmp // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
md-5 = "0.10"sha1 = "0.10"sha2 = "0.10"hmac = "0.12"aes = "0.8"des = "0.8"Example
// A network's switch and printer watched over SNMP: the switch's name and uptime, its// interfaces as rows (64-bit counters from ifXTable), the printer's toner from the// Printer-MIB, the same over SNMPv3 (SHA and AES), a location set and read back, and traps:// this program listens for them on a UDP port, answers informs, and sends its own.//// It needs an agent: SNMP_HOST (host or host:port), and SNMP_MANAGER for where traps go.// SNMP_TRAP_PORT is the port this program takes traps on (1162; 162 needs root).//// SNMP_HOST=192.168.1.2 SNMP_MANAGER=192.168.1.5 quillo run snmp.qwe then open http://localhost:8080/deviceuse "snmp.qwer"let HOST = env("SNMP_HOST", "127.0.0.1")let MANAGER = env("SNMP_MANAGER", "127.0.0.1:162")let TRAP_PORT = int(env("SNMP_TRAP_PORT", "1162"))let V3 = { user: "ops", auth: "SHA", auth_password: env("SNMP_AUTH", "opsauth123"), priv: "AES", priv_password: env("SNMP_PRIV", "opspriv123") }// Traps and informs that come in: read, kept (the last 100), informs answered.let traps = []udp.listen(TRAP_PORT, (p) => { let t = snmp_parse(p.bytes, V3) if !t { return null } if t.type == "inform" { udp.reply(p, snmp_response_for(p.bytes, [])) } traps = slice(traps + [{ from: p.from.host, version: t.version, type: t.type, trap: t.trap_name, values: t.values | map((v) => { name: v.name, value: v.value }) }], -100)})route GET /device = snmp_values(HOST, ["sysName.0", "sysLocation.0", "sysDescr.0", "sysUpTime.0", "ifNumber.0"], {})?route GET /interfaces { let names = snmp_table(HOST, "ifXTable", {})? snmp_table(HOST, "ifTable", {})? | map((row) => { let x = names | find((n) => n.index == row.index) { name: x.ifName, up: row.ifOperStatus == 1, mac: row.ifPhysAddress, mbps: x.ifHighSpeed, bytes_in: x.ifHCInOctets, alias: x.ifAlias } })}route GET /toner = snmp_table(HOST, "prtMarkerSuppliesTable", {})? | map((s) => { supply: s.prtMarkerSuppliesDescription, percent: s.prtMarkerSuppliesMaxCapacity > 0 ? s.prtMarkerSuppliesLevel * 100 / s.prtMarkerSuppliesMaxCapacity : null,})route GET /secure = { name: snmp_values(HOST, ["sysName.0"], V3)?, system_values: len(snmp_walk(HOST, "system", V3)?), wrong_password: snmp_get(HOST, ["sysName.0"], { user: "ops", auth: "SHA", auth_password: "not-the-password", priv: "AES", priv_password: "opspriv123" }).error,}route GET /set { snmp_set(HOST, [{ oid: "sysLocation.0", type: "string", value: "Hall 3, rack A" }], { community: "private" })? { location: snmp_values(HOST, ["sysLocation.0"], {})?["sysLocation.0"], read_only: snmp_set(HOST, [{ oid: "sysDescr.0", type: "string", value: "x" }], { community: "private" }).error, wrong_type: snmp_set(HOST, [{ oid: "sysName.0", type: "integer", value: 5 }], { community: "private" }).error, no_such: snmp_get(HOST, ["sysName.0", "sysLocation.7"], { version: "1" }).error, }}// Traps sent: to this program itself (v2c and v1), and to the manager: an inform it// acknowledges, and a v3 trap signed and encrypted.route GET /trap { let link = [{ oid: "ifIndex.2", type: "integer", value: 2 }, { oid: "ifDescr.2", value: "eth0" }] let sent = { self_v2c: snmp_trap("127.0.0.1", "linkDown", link, { port: TRAP_PORT }).sent, self_v1: snmp_trap("127.0.0.1", "1.3.6.1.4.1.99999", [], { port: TRAP_PORT, version: "1", generic: 6, specific: 17 }).sent, inform: snmp_trap(MANAGER, "linkUp", link, { inform: true })?.acknowledged, v3: snmp_trap(MANAGER, "1.3.6.1.4.1.99999.0.3", [{ oid: "sysLocation.0", value: "Hall 2" }], V3)?.sent, } sleep(300) { sent: sent, received: traps }}route GET /offline = { oid: snmp_oid("ifHCInOctets.2")?, name: snmp_name("1.3.6.1.2.1.43.11.1.1.9.1.1")?, unknown: snmp_oid("ifFrobnicate.1").error,}$ quillo run snmp.qwe$ curl 'localhost:8080/device'$ curl 'localhost:8080/interfaces'$ curl 'localhost:8080/toner'$ curl 'localhost:8080/secure'$ curl 'localhost:8080/set'$ curl 'localhost:8080/trap'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! An SNMP manager (v1, v2c and v3 with USM: MD5, SHA and SHA-2 authentication, DES and AES privacy): get, get-next, get-bulk, walk, whole tables and set; traps and informs sent, and received ones read for a `udp.listen` handler (informs answered); OIDs and common MIB names (system, interfaces, ifXTable, host resources, Printer-MIB, UPS-MIB, entity) both ways.//! version: 1.0.0//!//! [dependencies]//! md-5 = "0.10"//! sha1 = "0.10"//! sha2 = "0.10"//! hmac = "0.12"//! aes = "0.8"//! des = "0.8"use serde_json::{json, Map, Value};use std::collections::HashMap;use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};use std::sync::{Mutex, OnceLock};use std::time::{Duration, Instant};// ---------------------------------------------------------------------------------------------// MIB namesconst NAMES: &[(&str, &str)] = &[ ("iso", "1"), ("internet", "1.3.6.1"), ("mib-2", "1.3.6.1.2.1"), ("enterprises", "1.3.6.1.4.1"), ("snmpV2", "1.3.6.1.6"), ("system", "1.3.6.1.2.1.1"), ("sysDescr", "1.3.6.1.2.1.1.1"), ("sysObjectID", "1.3.6.1.2.1.1.2"), ("sysUpTime", "1.3.6.1.2.1.1.3"), ("sysContact", "1.3.6.1.2.1.1.4"), ("sysName", "1.3.6.1.2.1.1.5"), ("sysLocation", "1.3.6.1.2.1.1.6"), ("sysServices", "1.3.6.1.2.1.1.7"), ("sysORLastChange", "1.3.6.1.2.1.1.8"), ("interfaces", "1.3.6.1.2.1.2"), ("ifNumber", "1.3.6.1.2.1.2.1"), ("ifTable", "1.3.6.1.2.1.2.2"), ("ifEntry", "1.3.6.1.2.1.2.2.1"), ("ifIndex", "1.3.6.1.2.1.2.2.1.1"), ("ifDescr", "1.3.6.1.2.1.2.2.1.2"), ("ifType", "1.3.6.1.2.1.2.2.1.3"), ("ifMtu", "1.3.6.1.2.1.2.2.1.4"), ("ifSpeed", "1.3.6.1.2.1.2.2.1.5"), ("ifPhysAddress", "1.3.6.1.2.1.2.2.1.6"), ("ifAdminStatus", "1.3.6.1.2.1.2.2.1.7"), ("ifOperStatus", "1.3.6.1.2.1.2.2.1.8"), ("ifLastChange", "1.3.6.1.2.1.2.2.1.9"), ("ifInOctets", "1.3.6.1.2.1.2.2.1.10"), ("ifInUcastPkts", "1.3.6.1.2.1.2.2.1.11"), ("ifInNUcastPkts", "1.3.6.1.2.1.2.2.1.12"), ("ifInDiscards", "1.3.6.1.2.1.2.2.1.13"), ("ifInErrors", "1.3.6.1.2.1.2.2.1.14"), ("ifInUnknownProtos", "1.3.6.1.2.1.2.2.1.15"), ("ifOutOctets", "1.3.6.1.2.1.2.2.1.16"), ("ifOutUcastPkts", "1.3.6.1.2.1.2.2.1.17"), ("ifOutNUcastPkts", "1.3.6.1.2.1.2.2.1.18"), ("ifOutDiscards", "1.3.6.1.2.1.2.2.1.19"), ("ifOutErrors", "1.3.6.1.2.1.2.2.1.20"), ("ifOutQLen", "1.3.6.1.2.1.2.2.1.21"), ("ifSpecific", "1.3.6.1.2.1.2.2.1.22"), ("ipForwarding", "1.3.6.1.2.1.4.1"), ("ipAddrTable", "1.3.6.1.2.1.4.20"), ("ipAdEntAddr", "1.3.6.1.2.1.4.20.1.1"), ("ipAdEntIfIndex", "1.3.6.1.2.1.4.20.1.2"), ("ipAdEntNetMask", "1.3.6.1.2.1.4.20.1.3"), ("ifXTable", "1.3.6.1.2.1.31.1.1"), ("ifXEntry", "1.3.6.1.2.1.31.1.1.1"), ("ifName", "1.3.6.1.2.1.31.1.1.1.1"), ("ifInMulticastPkts", "1.3.6.1.2.1.31.1.1.1.2"), ("ifInBroadcastPkts", "1.3.6.1.2.1.31.1.1.1.3"), ("ifOutMulticastPkts", "1.3.6.1.2.1.31.1.1.1.4"), ("ifOutBroadcastPkts", "1.3.6.1.2.1.31.1.1.1.5"), ("ifHCInOctets", "1.3.6.1.2.1.31.1.1.1.6"), ("ifHCInUcastPkts", "1.3.6.1.2.1.31.1.1.1.7"), ("ifHCInMulticastPkts", "1.3.6.1.2.1.31.1.1.1.8"), ("ifHCInBroadcastPkts", "1.3.6.1.2.1.31.1.1.1.9"), ("ifHCOutOctets", "1.3.6.1.2.1.31.1.1.1.10"), ("ifHCOutUcastPkts", "1.3.6.1.2.1.31.1.1.1.11"), ("ifHCOutMulticastPkts", "1.3.6.1.2.1.31.1.1.1.12"), ("ifHCOutBroadcastPkts", "1.3.6.1.2.1.31.1.1.1.13"), ("ifLinkUpDownTrapEnable", "1.3.6.1.2.1.31.1.1.1.14"), ("ifHighSpeed", "1.3.6.1.2.1.31.1.1.1.15"), ("ifPromiscuousMode", "1.3.6.1.2.1.31.1.1.1.16"), ("ifConnectorPresent", "1.3.6.1.2.1.31.1.1.1.17"), ("ifAlias", "1.3.6.1.2.1.31.1.1.1.18"), ("hrSystemUptime", "1.3.6.1.2.1.25.1.1"), ("hrMemorySize", "1.3.6.1.2.1.25.2.2"), ("hrStorageTable", "1.3.6.1.2.1.25.2.3"), ("hrStorageEntry", "1.3.6.1.2.1.25.2.3.1"), ("hrStorageIndex", "1.3.6.1.2.1.25.2.3.1.1"), ("hrStorageType", "1.3.6.1.2.1.25.2.3.1.2"), ("hrStorageDescr", "1.3.6.1.2.1.25.2.3.1.3"), ("hrStorageAllocationUnits", "1.3.6.1.2.1.25.2.3.1.4"), ("hrStorageSize", "1.3.6.1.2.1.25.2.3.1.5"), ("hrStorageUsed", "1.3.6.1.2.1.25.2.3.1.6"), ("hrDeviceTable", "1.3.6.1.2.1.25.3.2"), ("hrDeviceEntry", "1.3.6.1.2.1.25.3.2.1"), ("hrDeviceIndex", "1.3.6.1.2.1.25.3.2.1.1"), ("hrDeviceType", "1.3.6.1.2.1.25.3.2.1.2"), ("hrDeviceDescr", "1.3.6.1.2.1.25.3.2.1.3"), ("hrDeviceStatus", "1.3.6.1.2.1.25.3.2.1.5"), ("hrProcessorLoad", "1.3.6.1.2.1.25.3.3.1.2"), ("hrPrinterStatus", "1.3.6.1.2.1.25.3.5.1.1"), ("hrPrinterDetectedErrorState", "1.3.6.1.2.1.25.3.5.1.2"), ("upsIdentManufacturer", "1.3.6.1.2.1.33.1.1.1"), ("upsIdentModel", "1.3.6.1.2.1.33.1.1.2"), ("upsBatteryStatus", "1.3.6.1.2.1.33.1.2.1"), ("upsSecondsOnBattery", "1.3.6.1.2.1.33.1.2.2"), ("upsEstimatedMinutesRemaining", "1.3.6.1.2.1.33.1.2.3"), ("upsEstimatedChargeRemaining", "1.3.6.1.2.1.33.1.2.4"), ("upsOutputSource", "1.3.6.1.2.1.33.1.4.1"), ("upsAlarmsPresent", "1.3.6.1.2.1.33.1.6.1"), ("entPhysicalTable", "1.3.6.1.2.1.47.1.1.1"), ("entPhysicalEntry", "1.3.6.1.2.1.47.1.1.1.1"), ("entPhysicalDescr", "1.3.6.1.2.1.47.1.1.1.1.2"), ("entPhysicalName", "1.3.6.1.2.1.47.1.1.1.1.7"), ("entPhysicalHardwareRev", "1.3.6.1.2.1.47.1.1.1.1.8"), ("entPhysicalFirmwareRev", "1.3.6.1.2.1.47.1.1.1.1.9"), ("entPhysicalSoftwareRev", "1.3.6.1.2.1.47.1.1.1.1.10"), ("entPhysicalSerialNum", "1.3.6.1.2.1.47.1.1.1.1.11"), ("entPhysicalMfgName", "1.3.6.1.2.1.47.1.1.1.1.12"), ("entPhysicalModelName", "1.3.6.1.2.1.47.1.1.1.1.13"), ("printmib", "1.3.6.1.2.1.43"), ("prtGeneralPrinterName", "1.3.6.1.2.1.43.5.1.1.16"), ("prtGeneralSerialNumber", "1.3.6.1.2.1.43.5.1.1.17"), ("prtInputTable", "1.3.6.1.2.1.43.8.2"), ("prtInputEntry", "1.3.6.1.2.1.43.8.2.1"), ("prtInputMaxCapacity", "1.3.6.1.2.1.43.8.2.1.9"), ("prtInputCurrentLevel", "1.3.6.1.2.1.43.8.2.1.10"), ("prtInputMediaName", "1.3.6.1.2.1.43.8.2.1.12"), ("prtInputName", "1.3.6.1.2.1.43.8.2.1.13"), ("prtMarkerLifeCount", "1.3.6.1.2.1.43.10.2.1.4"), ("prtMarkerSuppliesTable", "1.3.6.1.2.1.43.11.1"), ("prtMarkerSuppliesEntry", "1.3.6.1.2.1.43.11.1.1"), ("prtMarkerSuppliesClass", "1.3.6.1.2.1.43.11.1.1.4"), ("prtMarkerSuppliesType", "1.3.6.1.2.1.43.11.1.1.5"), ("prtMarkerSuppliesDescription", "1.3.6.1.2.1.43.11.1.1.6"), ("prtMarkerSuppliesSupplyUnit", "1.3.6.1.2.1.43.11.1.1.7"), ("prtMarkerSuppliesMaxCapacity", "1.3.6.1.2.1.43.11.1.1.8"), ("prtMarkerSuppliesLevel", "1.3.6.1.2.1.43.11.1.1.9"), ("prtMarkerColorantValue", "1.3.6.1.2.1.43.12.1.1.4"), ("prtConsoleDisplayBufferText", "1.3.6.1.2.1.43.16.5.1.2"), ("prtAlertTable", "1.3.6.1.2.1.43.18.1"), ("prtAlertEntry", "1.3.6.1.2.1.43.18.1.1"), ("prtAlertSeverityLevel", "1.3.6.1.2.1.43.18.1.1.2"), ("prtAlertGroup", "1.3.6.1.2.1.43.18.1.1.4"), ("prtAlertCode", "1.3.6.1.2.1.43.18.1.1.7"), ("prtAlertDescription", "1.3.6.1.2.1.43.18.1.1.8"), ("snmpTrapOID", "1.3.6.1.6.3.1.1.4.1"), ("snmpTrapEnterprise", "1.3.6.1.6.3.1.1.4.3"), ("snmpTraps", "1.3.6.1.6.3.1.1.5"), ("coldStart", "1.3.6.1.6.3.1.1.5.1"), ("warmStart", "1.3.6.1.6.3.1.1.5.2"), ("linkDown", "1.3.6.1.6.3.1.1.5.3"), ("linkUp", "1.3.6.1.6.3.1.1.5.4"), ("authenticationFailure", "1.3.6.1.6.3.1.1.5.5"), ("snmpEngineID", "1.3.6.1.6.3.10.2.1.1"), ("snmpEngineBoots", "1.3.6.1.6.3.10.2.1.2"), ("snmpEngineTime", "1.3.6.1.6.3.10.2.1.3"), ("usmStatsUnsupportedSecLevels", "1.3.6.1.6.3.15.1.1.1"), ("usmStatsNotInTimeWindows", "1.3.6.1.6.3.15.1.1.2"), ("usmStatsUnknownUserNames", "1.3.6.1.6.3.15.1.1.3"), ("usmStatsUnknownEngineIDs", "1.3.6.1.6.3.15.1.1.4"), ("usmStatsWrongDigests", "1.3.6.1.6.3.15.1.1.5"), ("usmStatsDecryptionErrors", "1.3.6.1.6.3.15.1.1.6"),];fn name_to_oid(n: &str) -> Option<&'static str> { NAMES.iter().find(|(k, _)| *k == n).map(|(_, v)| *v)}fn parse_oid(text: &str) -> Result<Vec<u64>, String> { let t = text.trim(); let t = match t.rsplit_once("::") { Some((_, r)) => r, None => t, }; let t = t.trim_start_matches('.'); if t.is_empty() { return Err("an empty OID".into()); } let first = t.split('.').next().unwrap_or(""); let (base, rest) = if first.chars().all(|c| c.is_ascii_digit()) { (String::new(), t.to_string()) } else { let b = name_to_oid(first).ok_or_else(|| format!("unknown MIB name `{}`: write the OID in numbers (1.3.6.1...), or one of sysDescr, ifTable, ifHCInOctets, hrPrinterStatus, prtMarkerSuppliesLevel...", first))?; (b.to_string(), t[first.len()..].trim_start_matches('.').to_string()) }; let full = match (base.is_empty(), rest.is_empty()) { (true, _) => rest, (false, true) => base, (false, false) => format!("{}.{}", base, rest), }; let arcs: Vec<u64> = full.split('.').map(|a| a.parse::<u64>().map_err(|_| format!("`{}` is not an OID: `{}` is not a number", text, a))).collect::<Result<_, _>>()?; if arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] > 39) || arcs.iter().any(|a| *a > u32::MAX as u64) { return Err(format!("`{}` is not a valid OID", text)); } Ok(arcs)}fn oid_text(o: &[u64]) -> String { o.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(".")}fn oid_name(o: &[u64]) -> String { let t = oid_text(o); let mut best: Option<(&str, &str)> = None; for (k, v) in NAMES { if (t == *v || t.starts_with(&format!("{}.", v))) && best.map(|b| v.len() > b.1.len()).unwrap_or(true) { best = Some((k, v)); } } match best { Some((k, v)) if t.len() == v.len() => k.to_string(), Some((k, v)) => format!("{}{}", k, &t[v.len()..]), None => t, }}// ---------------------------------------------------------------------------------------------// BERfn enc_len(n: usize, out: &mut Vec<u8>) { if n < 0x80 { out.push(n as u8); } else { let b = (n as u32).to_be_bytes(); let skip = b.iter().take_while(|x| **x == 0).count(); out.push(0x80 | (4 - skip) as u8); out.extend_from_slice(&b[skip..]); }}fn tlv(tag: u8, body: &[u8]) -> Vec<u8> { let mut out = vec![tag]; enc_len(body.len(), &mut out); out.extend_from_slice(body); out}fn enc_int(v: i64) -> Vec<u8> { let b = v.to_be_bytes(); let mut i = 0; while i < 7 && ((b[i] == 0 && b[i + 1] & 0x80 == 0) || (b[i] == 0xFF && b[i + 1] & 0x80 != 0)) { i += 1; } tlv(0x02, &b[i..])}fn enc_uint(tag: u8, v: u64) -> Vec<u8> { let b = v.to_be_bytes(); let mut i = 0; while i < 7 && b[i] == 0 && b[i + 1] & 0x80 == 0 { i += 1; } let mut body = b[i..].to_vec(); if body[0] & 0x80 != 0 { body.insert(0, 0); } tlv(tag, &body)}fn enc_oid(o: &[u64]) -> Vec<u8> { let mut body = vec![]; let mut push = |mut v: u64| { let mut tmp = vec![(v & 0x7F) as u8]; v >>= 7; while v > 0 { tmp.push(0x80 | (v & 0x7F) as u8); v >>= 7; } tmp.reverse(); body.extend(tmp); }; push(o[0] * 40 + o[1]); for a in &o[2..] { push(*a); } tlv(0x06, &body)}struct Rd<'a> { b: &'a [u8], at: usize,}impl<'a> Rd<'a> { fn new(b: &'a [u8]) -> Self { Rd { b, at: 0 } } fn done(&self) -> bool { self.at >= self.b.len() } /// The next TLV: (tag, body, where its whole TLV started). fn next(&mut self) -> Result<(u8, &'a [u8], usize), String> { let start = self.at; let b = self.b; if self.at + 2 > b.len() { return Err("the SNMP message is cut short".into()); } let tag = b[self.at]; let mut l = b[self.at + 1] as usize; self.at += 2; if l & 0x80 != 0 { let n = l & 0x7F; if n == 0 || n > 4 || self.at + n > b.len() { return Err("a BER length that cannot be read".into()); } l = 0; for i in 0..n { l = (l << 8) | b[self.at + i] as usize; } self.at += n; } if self.at + l > b.len() { return Err("the SNMP message is cut short".into()); } let body = &b[self.at..self.at + l]; self.at += l; Ok((tag, body, start)) } fn expect(&mut self, tag: u8, what: &str) -> Result<&'a [u8], String> { let (t, body, _) = self.next()?; if t != tag { return Err(format!("expected {} (tag 0x{:02X}), found tag 0x{:02X}", what, tag, t)); } Ok(body) } fn int(&mut self, what: &str) -> Result<i64, String> { let b = self.expect(0x02, what)?; dec_int(b) }}fn dec_int(b: &[u8]) -> Result<i64, String> { if b.is_empty() || b.len() > 8 { return Err("an INTEGER that cannot be read".into()); } let mut v: i64 = if b[0] & 0x80 != 0 { -1 } else { 0 }; for x in b { v = (v << 8) | *x as i64; } Ok(v)}fn dec_uint(b: &[u8]) -> Result<u64, String> { if b.is_empty() || b.len() > 9 || (b.len() == 9 && b[0] != 0) { return Err("an unsigned number that cannot be read".into()); } Ok(b.iter().fold(0u64, |a, x| (a << 8) | *x as u64))}fn dec_oid(b: &[u8]) -> Result<Vec<u64>, String> { if b.is_empty() { return Err("an empty OID".into()); } let mut arcs = vec![]; let mut v: u64 = 0; for (i, x) in b.iter().enumerate() { v = (v << 7) | (*x & 0x7F) as u64; if *x & 0x80 == 0 { if arcs.is_empty() { let (a, c) = if v < 40 { (0, v) } else if v < 80 { (1, v - 40) } else { (2, v - 80) }; arcs.push(a); arcs.push(c); } else { arcs.push(v); } v = 0; } else if i == b.len() - 1 { return Err("an OID cut short".into()); } } Ok(arcs)}fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect()}fn unhex(s: &str) -> Result<Vec<u8>, String> { let h: String = s.chars().filter(|c| !matches!(c, ' ' | ':' | '-' | '.')).collect(); if h.len() % 2 == 1 || !h.chars().all(|c| c.is_ascii_hexdigit()) { return Err(format!("`{}` is not hex", s)); } Ok((0..h.len() / 2).map(|i| u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap()).collect())}fn printable(b: &[u8]) -> Option<String> { let s = std::str::from_utf8(b).ok()?.trim_end_matches('\0'); if s.chars().all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t') { Some(s.to_string()) } else { None }}fn colon_hex(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect::<Vec<_>>().join(":")}/// A varbind's value read: (type, value).fn dec_value(tag: u8, b: &[u8]) -> Result<(&'static str, Value), String> { Ok(match tag { 0x02 => ("integer", json!(dec_int(b)?)), 0x04 => match printable(b) { Some(s) => ("string", json!(s)), None => ("hex", json!(colon_hex(b))), }, 0x05 => ("null", Value::Null), 0x06 => ("oid", json!(oid_text(&dec_oid(b)?))), 0x40 => { if b.len() != 4 { return Err("an IpAddress is 4 bytes".into()); } ("ip", json!(format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3]))) } 0x41 => ("counter32", json!(dec_uint(b)?)), 0x42 => ("gauge32", json!(dec_uint(b)?)), 0x43 => ("timeticks", json!(dec_uint(b)?)), 0x44 => ("opaque", json!(hex(b))), 0x46 => { let v = dec_uint(b)?; ("counter64", if v > i64::MAX as u64 { json!(v.to_string()) } else { json!(v) }) } 0x80 => ("noSuchObject", Value::Null), 0x81 => ("noSuchInstance", Value::Null), 0x82 => ("endOfMibView", Value::Null), t => ("unknown", json!(format!("tag 0x{:02X}: {}", t, hex(b)))), })}/// A value to send, from `{ type, value }` (type as net-snmp's letters or in words).fn enc_value(ty: &str, v: &Value) -> Result<Vec<u8>, String> { let int = |v: &Value| -> Result<i64, String> { v.as_i64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())).ok_or_else(|| format!("{} is not a whole number", v)) }; let uint = |v: &Value, max: u64| -> Result<u64, String> { let n = v.as_u64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())).ok_or_else(|| format!("{} is not a whole number 0 or more", v))?; if n > max { return Err(format!("{} is more than {}", n, max)); } Ok(n) }; let text = |v: &Value| -> String { v.as_str().map(|s| s.to_string()).unwrap_or_else(|| v.to_string()) }; Ok(match ty.to_lowercase().as_str() { "i" | "int" | "integer" | "integer32" => { let n = int(v)?; if n < i32::MIN as i64 || n > i32::MAX as i64 { return Err(format!("{} does not fit an INTEGER", n)); } enc_int(n) } "s" | "string" | "octet" | "octets" => tlv(0x04, text(v).as_bytes()), "x" | "hex" => tlv(0x04, &unhex(&text(v))?), "o" | "oid" => enc_oid(&parse_oid(&text(v))?), "a" | "ip" | "ipaddress" => { let ip: std::net::Ipv4Addr = text(v).parse().map_err(|_| format!("{} is not an IPv4 address", v))?; tlv(0x40, &ip.octets()) } "c" | "counter" | "counter32" => enc_uint(0x41, uint(v, u32::MAX as u64)?), "g" | "u" | "gauge" | "gauge32" | "unsigned" | "unsigned32" => enc_uint(0x42, uint(v, u32::MAX as u64)?), "t" | "timeticks" => enc_uint(0x43, uint(v, u32::MAX as u64)?), "counter64" | "c64" => enc_uint(0x46, uint(v, u64::MAX)?), "n" | "null" => tlv(0x05, &[]), "" | "auto" => match v { Value::Number(n) if n.is_i64() => enc_value("integer", v)?, Value::Number(_) => return Err(format!("{} is not a whole number: SNMP has no floats (send it as a string)", v)), Value::Null => tlv(0x05, &[]), Value::Bool(b) => enc_int(if *b { 1 } else { 2 }), // TruthValue _ => tlv(0x04, text(v).as_bytes()), }, t => return Err(format!("unknown SNMP type {:?}: use integer, string, hex, oid, ip, counter32, gauge32, timeticks, counter64 or null (or i, s, x, o, a, c, g, t)", t)), })}fn varbinds(list: &[(Vec<u64>, Vec<u8>)]) -> Vec<u8> { let mut body = vec![]; for (o, v) in list { let mut vb = enc_oid(o); vb.extend_from_slice(v); body.extend(tlv(0x30, &vb)); } tlv(0x30, &body)}fn pdu(tag: u8, id: i64, a: i64, b: i64, vbs: &[(Vec<u64>, Vec<u8>)]) -> Vec<u8> { let mut body = enc_int(id); body.extend(enc_int(a)); body.extend(enc_int(b)); body.extend(varbinds(vbs)); tlv(tag, &body)}fn pdu_kind(tag: u8) -> &'static str { match tag { 0xA0 => "get", 0xA1 => "get-next", 0xA2 => "response", 0xA3 => "set", 0xA4 => "trap-v1", 0xA5 => "get-bulk", 0xA6 => "inform", 0xA7 => "trap", 0xA8 => "report", _ => "unknown", }}fn error_name(e: i64) -> &'static str { match e { 0 => "noError", 1 => "tooBig", 2 => "noSuchName", 3 => "badValue", 4 => "readOnly", 5 => "genErr", 6 => "noAccess", 7 => "wrongType", 8 => "wrongLength", 9 => "wrongEncoding", 10 => "wrongValue", 11 => "noCreation", 12 => "inconsistentValue", 13 => "resourceUnavailable", 14 => "commitFailed", 15 => "undoFailed", 16 => "authorizationError", 17 => "notWritable", 18 => "inconsistentName", _ => "unknown error", }}#[derive(Debug, Clone)]struct Pdu { tag: u8, id: i64, err: i64, idx: i64, vbs: Vec<(Vec<u64>, u8, Vec<u8>)>, // a v1 trap's own fields trap: Option<Value>,}fn read_pdu(b: &[u8]) -> Result<Pdu, String> { let mut r = Rd::new(b); let (tag, body, _) = r.next()?; let mut p = Rd::new(body); if tag == 0xA4 { let ent = dec_oid(p.expect(0x06, "the enterprise OID")?)?; let (_, addr) = dec_value(0x40, p.expect(0x40, "the agent address")?)?; let generic = p.int("generic-trap")?; let specific = p.int("specific-trap")?; let ts = dec_uint(p.expect(0x43, "time-stamp")?)?; let vbs = read_vbs(&mut p)?; let gname = ["coldStart", "warmStart", "linkDown", "linkUp", "authenticationFailure", "egpNeighborLoss", "enterpriseSpecific"].get(generic as usize).copied().unwrap_or("unknown"); return Ok(Pdu { tag, id: 0, err: 0, idx: 0, vbs, trap: Some(json!({ "enterprise": oid_text(&ent), "agent": addr, "generic": generic, "generic_name": gname, "specific": specific, "uptime": ts })) }); } let id = p.int("request-id")?; let err = p.int("error-status")?; let idx = p.int("error-index")?; let vbs = read_vbs(&mut p)?; Ok(Pdu { tag, id, err, idx, vbs, trap: None })}fn read_vbs(p: &mut Rd) -> Result<Vec<(Vec<u64>, u8, Vec<u8>)>, String> { let list = p.expect(0x30, "the varbind list")?; let mut l = Rd::new(list); let mut out = vec![]; while !l.done() { let vb = l.expect(0x30, "a varbind")?; let mut v = Rd::new(vb); let o = dec_oid(v.expect(0x06, "an OID")?)?; let (t, body, _) = v.next()?; out.push((o, t, body.to_vec())); } Ok(out)}fn vb_json(o: &[u64], t: u8, b: &[u8]) -> Value { let name = oid_name(o); let (ty, v) = if t == 0x04 && name.starts_with("ifPhysAddress") { ("hex", json!(colon_hex(b))) } else { dec_value(t, b).unwrap_or(("unknown", Value::Null)) }; json!({ "oid": oid_text(o), "name": name, "type": ty, "value": v })}// ---------------------------------------------------------------------------------------------// USM (RFC 3414, 3826, 7860)#[derive(Clone, Copy, PartialEq, Debug)]enum Auth { Md5, Sha1, Sha224, Sha256, Sha384, Sha512,}impl Auth { fn parse(s: &str) -> Result<Auth, String> { Ok(match s.to_uppercase().replace('_', "-").as_str() { "MD5" => Auth::Md5, "SHA" | "SHA1" | "SHA-1" => Auth::Sha1, "SHA224" | "SHA-224" => Auth::Sha224, "SHA256" | "SHA-256" => Auth::Sha256, "SHA384" | "SHA-384" => Auth::Sha384, "SHA512" | "SHA-512" => Auth::Sha512, _ => return Err(format!("auth {:?}: use MD5, SHA, SHA-224, SHA-256, SHA-384 or SHA-512", s)), }) } fn mac_len(&self) -> usize { match self { Auth::Md5 | Auth::Sha1 => 12, Auth::Sha224 => 16, Auth::Sha256 => 24, Auth::Sha384 => 32, Auth::Sha512 => 48, } } fn hash(&self, parts: &[&[u8]]) -> Vec<u8> { use sha2::Digest; macro_rules! h { ($t:ty) => {{ let mut d = <$t>::new(); for p in parts { d.update(p); } d.finalize().to_vec() }}; } match self { Auth::Md5 => h!(md5::Md5), Auth::Sha1 => h!(sha1::Sha1), Auth::Sha224 => h!(sha2::Sha224), Auth::Sha256 => h!(sha2::Sha256), Auth::Sha384 => h!(sha2::Sha384), Auth::Sha512 => h!(sha2::Sha512), } } fn hmac(&self, key: &[u8], msg: &[u8]) -> Vec<u8> { use hmac::{Hmac, Mac}; macro_rules! m { ($t:ty) => {{ let mut m = <Hmac<$t> as Mac>::new_from_slice(key).expect("any key length"); m.update(msg); m.finalize().into_bytes().to_vec() }}; } let full = match self { Auth::Md5 => m!(md5::Md5), Auth::Sha1 => m!(sha1::Sha1), Auth::Sha224 => m!(sha2::Sha224), Auth::Sha256 => m!(sha2::Sha256), Auth::Sha384 => m!(sha2::Sha384), Auth::Sha512 => m!(sha2::Sha512), }; full[..self.mac_len()].to_vec() } /// RFC 3414 A.2: the password stretched to 1 MB and hashed, then localized to the engine. fn key(&self, password: &str, engine: &[u8]) -> Vec<u8> { static CACHE: OnceLock<Mutex<HashMap<(String, String, Vec<u8>), Vec<u8>>>> = OnceLock::new(); let c = CACHE.get_or_init(|| Mutex::new(HashMap::new())); let k = (format!("{:?}", self), password.to_string(), engine.to_vec()); if let Some(v) = c.lock().ok().and_then(|m| m.get(&k).cloned()) { return v; } let pw = password.as_bytes(); let mut buf = Vec::with_capacity(1_048_576); while buf.len() < 1_048_576 { let n = (1_048_576 - buf.len()).min(pw.len()); buf.extend_from_slice(&pw[..n]); } let ku = self.hash(&[&buf]); let kul = self.hash(&[&ku, engine, &ku]); if let Ok(mut m) = c.lock() { if m.len() > 256 { m.clear(); } m.insert(k, kul.clone()); } kul }}#[derive(Clone, Copy, PartialEq, Debug)]enum Priv { None, Des, Aes,}#[derive(Clone)]struct V3 { user: String, auth: Option<(Auth, String)>, privacy: Priv, priv_password: String, context: String,}fn salt() -> u64 { static N: OnceLock<Mutex<u64>> = OnceLock::new(); let m = N.get_or_init(|| { let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos() as u64).unwrap_or(1); Mutex::new(t ^ ((std::process::id() as u64) << 32)) }); let mut g = m.lock().unwrap_or_else(|e| e.into_inner()); *g = g.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); *g}fn des_cbc(key: &[u8], iv: &[u8], data: &mut [u8], encrypt: bool) { use des::cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit}; let c = des::Des::new_from_slice(&key[..8]).expect("an 8-byte key"); let mut prev = [0u8; 8]; prev.copy_from_slice(&iv[..8]); for chunk in data.chunks_mut(8) { if encrypt { for i in 0..8 { chunk[i] ^= prev[i]; } let mut b = GenericArray::clone_from_slice(chunk); c.encrypt_block(&mut b); chunk.copy_from_slice(&b); prev.copy_from_slice(chunk); } else { let saved: [u8; 8] = chunk.try_into().unwrap(); let mut b = GenericArray::clone_from_slice(chunk); c.decrypt_block(&mut b); for i in 0..8 { chunk[i] = b[i] ^ prev[i]; } prev = saved; } }}fn aes_cfb(key: &[u8], iv: &[u8], data: &mut [u8], encrypt: bool) { use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; let c = aes::Aes128::new_from_slice(&key[..16]).expect("a 16-byte key"); let mut reg = [0u8; 16]; reg.copy_from_slice(&iv[..16]); for chunk in data.chunks_mut(16) { let mut ks = GenericArray::clone_from_slice(®); c.encrypt_block(&mut ks); let n = chunk.len(); if encrypt { for i in 0..n { chunk[i] ^= ks[i]; } reg[..n].copy_from_slice(chunk); } else { let saved = chunk.to_vec(); for i in 0..n { chunk[i] ^= ks[i]; } reg[..n].copy_from_slice(&saved); } }}struct Engine { id: Vec<u8>, boots: i64, time: i64, at: Instant,}fn engines() -> &'static Mutex<HashMap<String, Engine>> { static E: OnceLock<Mutex<HashMap<String, Engine>>> = OnceLock::new(); E.get_or_init(|| Mutex::new(HashMap::new()))}/// A v3 message built: whole bytes.fn v3_message(msg_id: i64, flags: u8, engine: &[u8], boots: i64, time: i64, cred: &V3, scoped: &[u8]) -> Result<Vec<u8>, String> { let auth = if flags & 1 != 0 { cred.auth.clone() } else { None }; let mut priv_params = vec![]; let mut data = scoped.to_vec(); let msg_data = if flags & 2 != 0 { let (a, _) = auth.clone().ok_or("privacy needs authentication too")?; let k = a.key(&cred.priv_password, engine); if k.len() < 16 { return Err("this auth protocol's key is too short for privacy".into()); } let s = salt(); match cred.privacy { Priv::Des => { let salt = [&(boots as u32).to_be_bytes()[..], &(s as u32).to_be_bytes()[..]].concat(); let iv: Vec<u8> = (0..8).map(|i| k[8 + i] ^ salt[i]).collect(); while data.len() % 8 != 0 { data.push(0); } des_cbc(&k[..8], &iv, &mut data, true); priv_params = salt; } Priv::Aes => { let salt = s.to_be_bytes().to_vec(); let iv = [&(boots as u32).to_be_bytes()[..], &(time as u32).to_be_bytes()[..], &salt[..]].concat(); aes_cfb(&k[..16], &iv, &mut data, true); priv_params = salt; } Priv::None => return Err("no privacy protocol".into()), } tlv(0x04, &data) } else { data }; let mac_len = auth.as_ref().map(|(a, _)| a.mac_len()).unwrap_or(0); // USM parameters, with the place of the MAC noted let mut usm = tlv(0x04, engine); usm.extend(enc_int(boots)); usm.extend(enc_int(time)); usm.extend(tlv(0x04, cred.user.as_bytes())); let auth_at_usm = usm.len() + 2; usm.extend(tlv(0x04, &vec![0u8; mac_len])); usm.extend(tlv(0x04, &priv_params)); let usm_seq = tlv(0x30, &usm); let usm_hdr = usm_seq.len() - usm.len(); let sec = tlv(0x04, &usm_seq); let sec_hdr = sec.len() - usm_seq.len(); let mut global = enc_int(msg_id); global.extend(enc_int(65507)); global.extend(tlv(0x04, &[flags])); global.extend(enc_int(3)); let mut body = enc_int(3); body.extend(tlv(0x30, &global)); let sec_at = body.len(); body.extend(sec); body.extend(msg_data); let msg = tlv(0x30, &body); let msg_hdr = msg.len() - body.len(); let mut msg = msg; if let Some((a, pw)) = auth { let key = a.key(&pw, engine); let mac = a.hmac(&key, &msg); let at = msg_hdr + sec_at + sec_hdr + usm_hdr + auth_at_usm; msg[at..at + mac_len].copy_from_slice(&mac); } Ok(msg)}struct V3In { msg_id: i64, flags: u8, engine: Vec<u8>, boots: i64, time: i64, user: String, scoped: Vec<u8>, encrypted: bool,}/// A v3 message read; the MAC checked and the PDU decrypted when `cred` is given.fn v3_read(b: &[u8], cred: Option<&V3>) -> Result<V3In, String> { let mut r = Rd::new(b); let body = r.expect(0x30, "an SNMP message")?; let mut m = Rd::new(body); let ver = m.int("the version")?; if ver != 3 { return Err(format!("an SNMP v{} message, not v3", ver + 1)); } let g = m.expect(0x30, "msgGlobalData")?; let mut gr = Rd::new(g); let msg_id = gr.int("msgID")?; let _max = gr.int("msgMaxSize")?; let flags = gr.expect(0x04, "msgFlags")?.first().copied().unwrap_or(0); let model = gr.int("msgSecurityModel")?; if model != 3 { return Err(format!("security model {} is not USM", model)); } let sec = m.expect(0x04, "the security parameters")?; let mut ur = Rd::new(sec); let usm = ur.expect(0x30, "the USM parameters")?; let mut u = Rd::new(usm); let engine = u.expect(0x04, "the engine ID")?.to_vec(); let boots = u.int("engine boots")?; let time = u.int("engine time")?; let user = String::from_utf8_lossy(u.expect(0x04, "the user name")?).to_string(); let mac = u.expect(0x04, "the authentication parameters")?; let mac_abs = mac.as_ptr() as usize - b.as_ptr() as usize; let mac = mac.to_vec(); let privp = u.expect(0x04, "the privacy parameters")?.to_vec(); let (tag, data, start) = m.next()?; // plain: the scoped PDU's whole TLV; encrypted: the bytes inside the OCTET STRING let mut scoped = if tag == 0x30 { body[start..m.at].to_vec() } else { data.to_vec() }; let mut encrypted = false; if let Some(c) = cred { if flags & 1 != 0 { let (a, pw) = c.auth.clone().ok_or("the message is authenticated: give auth and auth_password")?; if mac.len() != a.mac_len() { return Err(format!("the message's MAC is {} bytes, not the {} of {:?}: is the auth protocol right?", mac.len(), a.mac_len(), a)); } let mut copy = b.to_vec(); for x in &mut copy[mac_abs..mac_abs + mac.len()] { *x = 0; } if a.hmac(&a.key(&pw, &engine), ©) != mac { return Err("the message's MAC is wrong: the auth password (or protocol) differs".into()); } } if flags & 2 != 0 { if tag != 0x04 { return Err("flags say encrypted, the PDU is not".into()); } let (a, _) = c.auth.clone().ok_or("an encrypted message: give auth and passwords")?; let k = a.key(&c.priv_password, &engine); match c.privacy { Priv::Des => { if privp.len() != 8 || scoped.len() % 8 != 0 { return Err("a DES-encrypted PDU that cannot be one".into()); } let iv: Vec<u8> = (0..8).map(|i| k[8 + i] ^ privp[i]).collect(); des_cbc(&k[..8], &iv, &mut scoped, false); } Priv::Aes => { if privp.len() != 8 { return Err("an AES-encrypted PDU without its salt".into()); } let iv = [&(boots as u32).to_be_bytes()[..], &(time as u32).to_be_bytes()[..], &privp[..]].concat(); aes_cfb(&k[..16], &iv, &mut scoped, false); } Priv::None => return Err("the PDU is encrypted: give priv and priv_password".into()), } // a DES PDU is padded: keep the SEQUENCE only let mut r = Rd::new(&scoped); match r.next() { Ok((0x30, _, _)) => scoped.truncate(r.at), _ => return Err("the PDU did not decrypt: the priv password (or protocol) differs".into()), } } else if tag != 0x30 { return Err("a v3 PDU that is not a scoped PDU".into()); } } else if flags & 2 != 0 { encrypted = true; } Ok(V3In { msg_id, flags, engine, boots, time, user, scoped, encrypted })}fn scoped_pdu(engine: &[u8], context: &str, p: &[u8]) -> Vec<u8> { let mut b = tlv(0x04, engine); b.extend(tlv(0x04, context.as_bytes())); b.extend_from_slice(p); tlv(0x30, &b)}fn read_scoped(s: &[u8]) -> Result<(Vec<u8>, String, Pdu), String> { let mut r = Rd::new(s); let body = r.expect(0x30, "a scoped PDU")?; let mut b = Rd::new(body); let ce = b.expect(0x04, "the context engine ID")?.to_vec(); let cn = String::from_utf8_lossy(b.expect(0x04, "the context name")?).to_string(); let rest = &body[b.at..]; Ok((ce, cn, read_pdu(rest)?))}// ---------------------------------------------------------------------------------------------// Options and the exchangestruct Opts { version: u8, // 0 = v1, 1 = v2c, 3 = v3 community: String, port: u16, timeout: Duration, retries: u32, v3: Option<V3>,}fn opts(o: &Value) -> Result<Opts, String> { let s = |k: &str| o.get(k).and_then(|v| v.as_str()).map(|s| s.to_string()); let has_user = s("user").is_some(); let version = match o.get("version").map(|v| if let Some(n) = v.as_i64() { n.to_string() } else { v.as_str().unwrap_or("").to_string() }).unwrap_or_else(|| if has_user { "3".into() } else { "2c".into() }).to_lowercase().trim_start_matches('v').to_string().as_str() { "1" => 0, "2" | "2c" => 1, "3" => 3, v => return Err(format!("version {:?}: use \"1\", \"2c\" or \"3\"", v)), }; let v3 = if version == 3 { let user = s("user").ok_or("SNMPv3 needs a user")?; let auth = match (s("auth"), s("auth_password")) { (Some(a), Some(p)) => Some((Auth::parse(&a)?, p)), (None, Some(p)) => Some((Auth::Sha1, p)), (Some(_), None) => return Err("auth needs auth_password".into()), (None, None) => None, }; if let Some((_, p)) = &auth { if p.len() < 8 { return Err("an SNMPv3 password is at least 8 characters".into()); } } let privacy = match s("priv").map(|p| p.to_uppercase()).as_deref() { None if s("priv_password").is_some() => Priv::Aes, None => Priv::None, Some("DES") => Priv::Des, Some("AES") | Some("AES128") | Some("AES-128") => Priv::Aes, Some(p) => return Err(format!("priv {:?}: use AES (AES-128) or DES", p)), }; let priv_password = s("priv_password").unwrap_or_default(); if privacy != Priv::None { if auth.is_none() { return Err("privacy needs authentication too: give auth_password".into()); } if priv_password.len() < 8 { return Err("priv_password is at least 8 characters".into()); } } Some(V3 { user, auth, privacy, priv_password, context: s("context").unwrap_or_default() }) } else { None }; Ok(Opts { version, community: s("community").unwrap_or_else(|| "public".into()), port: o.get("port").and_then(|v| v.as_u64()).unwrap_or(161).clamp(1, 65535) as u16, timeout: Duration::from_millis(o.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(3000).clamp(50, 120_000)), retries: o.get("retries").and_then(|v| v.as_u64()).unwrap_or(1).min(10) as u32, v3, })}fn sock_addr(host: &str, port: u16) -> Result<SocketAddr, String> { let h = host.trim(); let with_port = if h.starts_with('[') { if h.contains("]:") { h.to_string() } else { format!("{}:{}", h, port) } } else if h.matches(':').count() == 1 { h.to_string() } else if h.contains(':') { format!("[{}]:{}", h, port) } else { format!("{}:{}", h, port) }; let mut all: Vec<SocketAddr> = with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.collect(); all.sort_by_key(|a| !a.is_ipv4()); all.into_iter().next().ok_or(format!("{} has no address", host))}fn next_id() -> i64 { (salt() & 0x3FFF_FFFF) as i64}fn socket_for(a: &SocketAddr) -> Result<UdpSocket, String> { UdpSocket::bind(if a.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" }).map_err(|e| e.to_string())}/// Sends `msg` and waits for the packet `accept` takes, resending on silence.fn exchange(a: &SocketAddr, o: &Opts, make: &mut dyn FnMut() -> Result<Vec<u8>, String>, accept: &mut dyn FnMut(&[u8]) -> Option<Result<Vec<u8>, String>>) -> Result<Vec<u8>, String> { let s = socket_for(a)?; let mut buf = vec![0u8; 65536]; for _ in 0..=o.retries { let msg = make()?; s.send_to(&msg, a).map_err(|e| format!("sending to {}: {}", a, e))?; let until = Instant::now() + o.timeout; loop { let left = until.saturating_duration_since(Instant::now()); if left.is_zero() { break; } s.set_read_timeout(Some(left)).map_err(|e| e.to_string())?; match s.recv_from(&mut buf) { Ok((n, from)) if from.ip() == a.ip() => { if let Some(r) = accept(&buf[..n]) { return r; } } Ok(_) => {} Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => break, Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => return Err(format!("{} refused the packet: nothing listens on UDP port {}", a, a.port())), Err(e) => return Err(format!("reading from {}: {}", a, e)), } } } let hint = if o.version == 3 { "SNMP off, the agent not allowing this user, or a firewall" } else { "a wrong community (agents ignore those), SNMP off, or a firewall" }; Err(format!("no answer from {} in {} ms ({} tries): {}", a, o.timeout.as_millis(), o.retries + 1, hint))}fn community_msg(o: &Opts, p: &[u8]) -> Vec<u8> { let mut b = enc_int(o.version as i64); b.extend(tlv(0x04, o.community.as_bytes())); b.extend_from_slice(p); tlv(0x30, &b)}fn read_community(b: &[u8]) -> Result<(i64, String, Pdu), String> { let mut r = Rd::new(b); let body = r.expect(0x30, "an SNMP message")?; let mut m = Rd::new(body); let ver = m.int("the version")?; if ver == 3 { return Err("an SNMPv3 message".into()); } let comm = String::from_utf8_lossy(m.expect(0x04, "the community")?).to_string(); Ok((ver, comm, read_pdu(&body[m.at..])?))}fn report_error(p: &Pdu) -> String { let o = p.vbs.first().map(|v| oid_name(&v.0)).unwrap_or_default(); let what = if o.starts_with("usmStatsUnknownUserNames") { "the agent does not know this user" } else if o.starts_with("usmStatsWrongDigests") { "wrong auth password (or auth protocol) for this user" } else if o.starts_with("usmStatsDecryptionErrors") { "the agent could not decrypt: wrong priv password (or priv protocol)" } else if o.starts_with("usmStatsUnsupportedSecLevels") { "the agent does not allow this security level for the user (auth or priv missing, or extra)" } else if o.starts_with("usmStatsNotInTimeWindows") { "the agent's clock window was missed" } else if o.starts_with("usmStatsUnknownEngineIDs") { "the agent did not know the engine ID" } else { "the agent sent a report" }; format!("{} ({})", what, o)}/// One request: the answer's PDU.fn request(host: &str, o: &Opts, tag: u8, a: i64, b: i64, vbs: &[(Vec<u64>, Vec<u8>)]) -> Result<Pdu, String> { let addr = sock_addr(host, o.port)?; if o.version != 3 { let idc = std::cell::Cell::new(0i64); let mut make = || { let id = next_id(); idc.set(id); Ok(community_msg(o, &pdu(tag, id, a, b, vbs))) }; let mut accept = |r: &[u8]| -> Option<Result<Vec<u8>, String>> { match read_community(r) { Ok((_, _, p)) if p.id == idc.get() && p.tag == 0xA2 => Some(Ok(r.to_vec())), _ => None, } }; let raw = exchange(&addr, o, &mut make, &mut accept)?; let (_, _, p) = read_community(&raw)?; return Ok(p); } let cred = o.v3.clone().ok_or("SNMPv3 needs a user")?; let key = addr.to_string(); for attempt in 0..3 { // discover the engine (RFC 3414 4) when it is not known let known = engines().lock().ok().and_then(|m| m.get(&key).map(|e| (e.id.clone(), e.boots, e.time + e.at.elapsed().as_secs() as i64))); let (eid, boots, time) = match known { Some(k) => k, None => { let mid = std::cell::Cell::new(0i64); let anon = V3 { user: String::new(), auth: None, privacy: Priv::None, priv_password: String::new(), context: String::new() }; let mut make = || { let m = next_id(); mid.set(m); v3_message(m, 0x04, &[], 0, 0, &anon, &scoped_pdu(&[], "", &pdu(0xA0, next_id(), 0, 0, &[]))) }; let mut accept = |r: &[u8]| -> Option<Result<Vec<u8>, String>> { match v3_read(r, None) { Ok(m) if m.msg_id == mid.get() => Some(Ok(r.to_vec())), _ => None, } }; let raw = exchange(&addr, o, &mut make, &mut accept)?; let m = v3_read(&raw, None)?; if m.engine.is_empty() { return Err("the agent did not say its engine ID".into()); } if let Ok(mut e) = engines().lock() { e.insert(key.clone(), Engine { id: m.engine.clone(), boots: m.boots, time: m.time, at: Instant::now() }); } (m.engine, m.boots, m.time) } }; let flags = 0x04 | if cred.auth.is_some() { 1 } else { 0 } | if cred.privacy != Priv::None { 2 } else { 0 }; let mid = std::cell::Cell::new(0i64); let rid = std::cell::Cell::new(0i64); let mut make = || { let m = next_id(); let r = next_id(); mid.set(m); rid.set(r); v3_message(m, flags, &eid, boots, time, &cred, &scoped_pdu(&eid, &cred.context, &pdu(tag, r, a, b, vbs))) }; let mut accept = |r: &[u8]| -> Option<Result<Vec<u8>, String>> { match v3_read(r, None) { Ok(m) if m.msg_id == mid.get() => Some(Ok(r.to_vec())), _ => None, } }; let raw = exchange(&addr, o, &mut make, &mut accept)?; let head = v3_read(&raw, None)?; // a report is sent without auth when the problem is the auth itself let m = if head.flags & 1 != 0 { v3_read(&raw, Some(&cred))? } else { head }; let (_, _, p) = read_scoped(&m.scoped)?; if p.tag == 0xA8 { let o = p.vbs.first().map(|v| oid_name(&v.0)).unwrap_or_default(); if (o.starts_with("usmStatsNotInTimeWindows") || o.starts_with("usmStatsUnknownEngineIDs")) && attempt < 2 { if let Ok(mut e) = engines().lock() { if o.starts_with("usmStatsUnknownEngineIDs") { e.remove(&key); } else { e.insert(key.clone(), Engine { id: m.engine.clone(), boots: m.boots, time: m.time, at: Instant::now() }); } } continue; } return Err(report_error(&p)); } if p.id != rid.get() { return Err("the agent answered another request".into()); } return Ok(p); } Err("the agent kept refusing the time window".into())}fn check_error(p: &Pdu, asked: &[Vec<u64>]) -> Result<(), String> { if p.err != 0 { let at = if p.idx > 0 { asked.get(p.idx as usize - 1).map(|o| format!(" at {}", oid_name(o))).unwrap_or_default() } else { String::new() }; let hint = match p.err { 1 => ": ask for fewer OIDs at once", 2 => ": the agent has no such object (or this community cannot see it)", 4 | 17 => ": that object cannot be written (or this community is read-only)", 6 | 16 => ": this community or user may not do that", 7 | 8 | 10 => ": the value does not suit the object", _ => "", }; return Err(format!("{}{} (error {}){}", error_name(p.err), at, p.err, hint)); } Ok(())}fn oids(list: &[String]) -> Result<Vec<Vec<u64>>, String> { if list.is_empty() { return Err("give at least one OID".into()); } list.iter().map(|s| parse_oid(s)).collect()}fn nulls(os: &[Vec<u64>]) -> Vec<(Vec<u64>, Vec<u8>)> { os.iter().map(|o| (o.clone(), vec![0x05, 0x00])).collect()}fn result(p: &Pdu) -> Vec<Value> { p.vbs.iter().map(|(o, t, b)| vb_json(o, *t, b)).collect()}// ---------------------------------------------------------------------------------------------// The functions/// Gets values (`["sysDescr.0", "sysUpTime.0", "1.3.6.1.2.1.1.5.0"]`) from an agent: each/// `{ oid, name, type, value }`. Options: `community` ("public"), `version` ("2c", "1" or "3"),/// `port` (161), `timeout_ms` (3000), `retries` (1); for v3 `user`, `auth` (SHA, MD5, SHA-256...),/// `auth_password`, `priv` (AES or DES), `priv_password`, `context`.pub fn snmp_get(host: &str, oid_list: Vec<String>, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; let os = oids(&oid_list)?; let p = request(host, &o, 0xA0, 0, 0, &nulls(&os))?; check_error(&p, &os)?; Ok(result(&p))}/// Gets values as a map from each name asked for to its value (`{ "sysName.0": "plc-hall-2" }`):/// for a few values read by name. A missing one is null.pub fn snmp_values(host: &str, oid_list: Vec<String>, options: serde_json::Value) -> Result<serde_json::Value, String> { let got = snmp_get(host, oid_list.clone(), options)?; let mut m = Map::new(); for (k, v) in oid_list.iter().zip(got) { m.insert(k.clone(), v.get("value").cloned().unwrap_or(Value::Null)); } Ok(Value::Object(m))}/// The next value after each OID (get-next): `{ oid, name, type, value }` each.pub fn snmp_get_next(host: &str, oid_list: Vec<String>, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; let os = oids(&oid_list)?; let p = request(host, &o, 0xA1, 0, 0, &nulls(&os))?; check_error(&p, &os)?; Ok(result(&p))}/// Get-bulk (v2c and v3): the first `non_repeaters` OIDs once, then `max_repetitions` values after each of the others.pub fn snmp_get_bulk(host: &str, oid_list: Vec<String>, non_repeaters: i64, max_repetitions: i64, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; if o.version == 0 { return Err("get-bulk is SNMP v2c and v3: SNMPv1 agents know only get-next (use snmp_walk)".into()); } let os = oids(&oid_list)?; let p = request(host, &o, 0xA5, non_repeaters.clamp(0, os.len() as i64), max_repetitions.clamp(1, 1000), &nulls(&os))?; check_error(&p, &os)?; Ok(result(&p))}fn walk(host: &str, o: &Opts, root: &[u64], max: usize) -> Result<Vec<(Vec<u64>, u8, Vec<u8>)>, String> { let mut out = vec![]; let mut cur = root.to_vec(); let mut reps: i64 = 25; loop { let p = if o.version == 0 { request(host, o, 0xA1, 0, 0, &nulls(std::slice::from_ref(&cur)))? } else { request(host, o, 0xA5, 0, reps, &nulls(std::slice::from_ref(&cur)))? }; if p.err == 1 && reps > 1 { reps = (reps / 2).max(1); continue; } if o.version == 0 && p.err == 2 { break; // the end of the agent's MIB, as v1 says it } check_error(&p, std::slice::from_ref(&cur))?; if p.vbs.is_empty() { break; } let mut end = false; for (oid, t, b) in p.vbs { if t == 0x82 || !oid.starts_with(root) || oid.len() <= root.len() { end = true; break; } if oid <= cur { return Err(format!("the agent went backwards at {} (its MIB is not in order): the walk stopped", oid_text(&oid))); } cur = oid.clone(); out.push((oid, t, b)); if out.len() >= max { return Ok(out); } } if end { break; } } Ok(out)}/// Every value under an OID (`"system"`, `"ifTable"`, `"1.3.6.1.2.1.43.11"`), with get-bulk on/// v2c and v3 and get-next on v1; options as `snmp_get`, plus `max` values (10000).pub fn snmp_walk(host: &str, oid: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; let root = parse_oid(oid)?; let max = options.get("max").and_then(|v| v.as_u64()).unwrap_or(10000).clamp(1, 1_000_000) as usize; Ok(walk(host, &o, &root, max)?.iter().map(|(o, t, b)| vb_json(o, *t, b)).collect())}/// A table walked into rows (`"ifTable"`, `"ifXTable"`, `"prtMarkerSuppliesTable"`, `"hrStorageTable"`):/// each `{ index, <column name>: value, ... }`, the index being what follows the column's OID.pub fn snmp_table(host: &str, table: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; let mut root = parse_oid(table)?; // a table's columns are under its entry, `.1` if !oid_name(&root).ends_with("Entry") { root.push(1); } let max = options.get("max").and_then(|v| v.as_u64()).unwrap_or(10000).clamp(1, 1_000_000) as usize; let vals = walk(host, &o, &root, max)?; let mut rows: Vec<(String, Map<String, Value>)> = vec![]; let mut at: HashMap<String, usize> = HashMap::new(); for (oid, t, b) in vals { if oid.len() < root.len() + 2 { continue; } let col = &oid[..root.len() + 1]; let idx = oid_text(&oid[root.len() + 1..]); let name = { let n = oid_name(col); if n.contains('.') { format!("column{}", col[root.len()]) } else { n } }; let (_, v) = dec_value(t, &b).unwrap_or(("unknown", Value::Null)); let i = *at.entry(idx.clone()).or_insert_with(|| { let mut m = Map::new(); m.insert("index".into(), json!(idx)); rows.push((idx.clone(), m)); rows.len() - 1 }); rows[i].1.insert(name, v); } Ok(rows.into_iter().map(|(_, m)| Value::Object(m)).collect())}fn vb_list(list: &[Value]) -> Result<Vec<(Vec<u64>, Vec<u8>)>, String> { list.iter().map(|v| { let oid = v.get("oid").and_then(|x| x.as_str()).ok_or_else(|| format!("each varbind is {{ oid, type, value }}: {} has no oid", v))?; let ty = v.get("type").and_then(|x| x.as_str()).unwrap_or("auto"); Ok((parse_oid(oid)?, enc_value(ty, v.get("value").unwrap_or(&Value::Null)).map_err(|e| format!("{}: {}", oid, e))?)) }).collect()}/// Sets values: `[{ oid: "sysLocation.0", type: "string", value: "Hall 2" }]` (types: integer,/// string, hex, oid, ip, counter32, gauge32, timeticks, counter64; left out, from the value):/// the agent's answer, each `{ oid, name, type, value }`. Use the write community (often "private").pub fn snmp_set(host: &str, values: Vec<serde_json::Value>, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let o = opts(&options)?; let vbs = vb_list(&values)?; if vbs.is_empty() { return Err("give at least one { oid, type, value }".into()); } let asked: Vec<Vec<u64>> = vbs.iter().map(|v| v.0.clone()).collect(); let p = request(host, &o, 0xA3, 0, 0, &vbs)?; check_error(&p, &asked)?; Ok(result(&p))}fn local_engine(o: &Value) -> Result<Vec<u8>, String> { match o.get("engine_id").and_then(|v| v.as_str()) { Some(h) => unhex(h), None => { // RFC 3411's format, enterprise 8072 (net-snmp's), from this host's name let host = std::env::var("HOSTNAME").unwrap_or_else(|_| "quillo".into()); let h = Auth::Md5.hash(&[host.as_bytes()]); Ok([&[0x80, 0x00, 0x1F, 0x88, 0x04][..], &h[..8]].concat()) } }}fn uptime_ticks() -> u64 { static START: OnceLock<Instant> = OnceLock::new(); (START.get_or_init(Instant::now).elapsed().as_millis() / 10) as u64}/// Sends a trap (or with `inform: true`, an inform, which waits for the manager's answer) to a/// manager (port 162 unless given): `trap_oid` is the event (`"linkDown"`, `"1.3.6.1.4.1.9999.0.1"`),/// `values` the varbinds after `sysUpTime.0` and `snmpTrapOID.0`. v1 traps (`version: "1"`) take/// `enterprise`, `agent`, `generic` and `specific`. v3 traps are sent from `engine_id` (hex).pub fn snmp_trap(host: &str, trap_oid: &str, values: Vec<serde_json::Value>, options: serde_json::Value) -> Result<serde_json::Value, String> { let mut opt = options.clone(); if opt.get("port").is_none() { if let Some(m) = opt.as_object_mut() { m.insert("port".into(), json!(162)); } else { opt = json!({ "port": 162 }); } } let o = opts(&opt)?; let addr = sock_addr(host, o.port)?; let inform = options.get("inform").and_then(|v| v.as_bool()).unwrap_or(false); let extra = vb_list(&values)?; let up = options.get("uptime").and_then(|v| v.as_u64()).unwrap_or_else(uptime_ticks); if o.version == 0 { if inform { return Err("informs are SNMP v2c and v3".into()); } let ent = parse_oid(options.get("enterprise").and_then(|v| v.as_str()).unwrap_or(trap_oid))?; let agent: std::net::Ipv4Addr = options.get("agent").and_then(|v| v.as_str()).unwrap_or("0.0.0.0").parse().map_err(|_| "agent is an IPv4 address")?; let generic = options.get("generic").and_then(|v| v.as_i64()).unwrap_or(6); let specific = options.get("specific").and_then(|v| v.as_i64()).unwrap_or(0); let mut body = enc_oid(&ent); body.extend(tlv(0x40, &agent.octets())); body.extend(enc_int(generic)); body.extend(enc_int(specific)); body.extend(enc_uint(0x43, up)); body.extend(varbinds(&extra)); let msg = community_msg(&o, &tlv(0xA4, &body)); socket_for(&addr)?.send_to(&msg, addr).map_err(|e| format!("sending to {}: {}", addr, e))?; return Ok(json!({ "sent": true, "to": addr.to_string(), "bytes": msg.len(), "acknowledged": false })); } let mut vbs = vec![(parse_oid("sysUpTime.0")?, enc_uint(0x43, up)), (parse_oid("snmpTrapOID.0")?, enc_oid(&parse_oid(trap_oid)?))]; vbs.extend(extra); let tag = if inform { 0xA6 } else { 0xA7 }; if o.version == 3 { let cred = o.v3.clone().ok_or("SNMPv3 needs a user")?; let flags = if cred.auth.is_some() { 1 } else { 0 } | if cred.privacy != Priv::None { 2 } else { 0 }; if inform { // the manager is authoritative for an inform: discover it like an agent let p = request(host, &o, 0xA6, 0, 0, &vbs)?; check_error(&p, &[])?; return Ok(json!({ "sent": true, "to": addr.to_string(), "acknowledged": true })); } let eid = local_engine(&options)?; let boots = options.get("engine_boots").and_then(|v| v.as_i64()).unwrap_or(1); let time = (uptime_ticks() / 100) as i64; let msg = v3_message(next_id(), flags, &eid, boots, time, &cred, &scoped_pdu(&eid, &cred.context, &pdu(tag, next_id(), 0, 0, &vbs)))?; socket_for(&addr)?.send_to(&msg, addr).map_err(|e| format!("sending to {}: {}", addr, e))?; return Ok(json!({ "sent": true, "to": addr.to_string(), "bytes": msg.len(), "acknowledged": false, "engine_id": hex(&eid) })); } if inform { let p = request(host, &o, 0xA6, 0, 0, &vbs)?; check_error(&p, &[])?; return Ok(json!({ "sent": true, "to": addr.to_string(), "acknowledged": true })); } let msg = community_msg(&o, &pdu(tag, next_id(), 0, 0, &vbs)); socket_for(&addr)?.send_to(&msg, addr).map_err(|e| format!("sending to {}: {}", addr, e))?; Ok(json!({ "sent": true, "to": addr.to_string(), "bytes": msg.len(), "acknowledged": false }))}fn bytes_of(b: &[i64]) -> Result<Vec<u8>, String> { b.iter().map(|x| u8::try_from(*x).map_err(|_| format!("{} is not a byte (0 to 255)", x))).collect()}/// Reads an SNMP message as a `udp.listen` handler gets it (`p.bytes`): `{ version, community,/// type: "trap" | "inform" | "trap-v1" | "get" | ..., request_id, trap_oid, uptime, values }`, a v1/// trap's `enterprise`, `generic` and `specific`. For v3, options give `user`, `auth`,/// `auth_password`, `priv` and `priv_password` to check and decrypt it (else only its header is read).pub fn snmp_parse(bytes: Vec<i64>, options: serde_json::Value) -> Result<serde_json::Value, String> { let b = bytes_of(&bytes)?; let mut r = Rd::new(&b); let body = r.expect(0x30, "an SNMP message")?; let ver = Rd::new(body).int("the version")?; let mut out = Map::new(); let p = if ver == 3 { let cred = if options.get("user").is_some() { let mut o = options.clone(); if let Some(m) = o.as_object_mut() { m.insert("version".into(), json!("3")); } opts(&o)?.v3 } else { None }; let m = v3_read(&b, cred.as_ref())?; out.insert("version".into(), json!("3")); out.insert("user".into(), json!(m.user)); out.insert("engine_id".into(), json!(hex(&m.engine))); out.insert("authenticated".into(), json!(m.flags & 1 != 0 && cred.is_some())); out.insert("encrypted".into(), json!(m.flags & 2 != 0)); if let Some(c) = &cred { if c.user != m.user { return Err(format!("the message is from user {:?}, not {:?}", m.user, c.user)); } } if m.encrypted { out.insert("type".into(), json!("encrypted")); return Ok(Value::Object(out)); } let (_, ctx, p) = read_scoped(&m.scoped)?; out.insert("context".into(), json!(ctx)); p } else { let (v, c, p) = read_community(&b)?; out.insert("version".into(), json!(if v == 0 { "1" } else { "2c" })); out.insert("community".into(), json!(c)); p }; out.insert("type".into(), json!(pdu_kind(p.tag))); out.insert("request_id".into(), json!(p.id)); if p.err != 0 { out.insert("error".into(), json!(error_name(p.err))); out.insert("error_index".into(), json!(p.idx)); } let mut values = vec![]; for (o, t, body) in &p.vbs { let n = oid_name(o); if n == "sysUpTime.0" && matches!(p.tag, 0xA6 | 0xA7) { out.insert("uptime".into(), dec_value(*t, body).map(|x| x.1).unwrap_or(Value::Null)); continue; } if n == "snmpTrapOID.0" && matches!(p.tag, 0xA6 | 0xA7) { let v = dec_value(*t, body).map(|x| x.1).unwrap_or(Value::Null); let name = v.as_str().and_then(|s| parse_oid(s).ok()).map(|x| oid_name(&x)); out.insert("trap_oid".into(), v); out.insert("trap_name".into(), json!(name)); continue; } values.push(vb_json(o, *t, body)); } if let Some(Value::Object(t)) = &p.trap { for (k, v) in t { out.insert(k.clone(), v.clone()); } out.insert("trap_name".into(), t.get("generic_name").cloned().unwrap_or(Value::Null)); } out.insert("values".into(), Value::Array(values)); Ok(Value::Object(out))}/// The answer to a v1 or v2c message as bytes for `udp.reply`: an inform acknowledged (the same/// request ID and values, as RFC 3416 says), or a get answered with `values` (`[{ oid, type, value }]`).pub fn snmp_response_for(bytes: Vec<i64>, values: Vec<serde_json::Value>) -> Result<Vec<u8>, String> { let b = bytes_of(&bytes)?; let (ver, comm, p) = read_community(&b).map_err(|e| if e.contains("v3") { "answering SNMPv3 is not supported: an inform over v3 needs this side's own engine".to_string() } else { e })?; let vbs: Vec<(Vec<u64>, Vec<u8>)> = if values.is_empty() { p.vbs.iter().map(|(o, t, body)| (o.clone(), tlv(*t, body))).collect() } else { vb_list(&values)? }; let o = Opts { version: ver as u8, community: comm, port: 0, timeout: Duration::from_secs(1), retries: 0, v3: None }; Ok(community_msg(&o, &pdu(0xA2, p.id, 0, 0, &vbs)))}/// A MIB name as its OID: `"ifHCInOctets.2"` is `"1.3.6.1.2.1.31.1.1.1.6.2"` (numbers pass through).pub fn snmp_oid(name: &str) -> Result<String, String> { Ok(oid_text(&parse_oid(name)?))}/// An OID as its MIB name where the plugin knows one: `"1.3.6.1.2.1.43.11.1.1.9.1.1"` is `"prtMarkerSuppliesLevel.1.1"`.pub fn snmp_name(oid: &str) -> Result<String, String> { Ok(oid_name(&parse_oid(oid)?))}Related
mdns.qwer · 1.0.0Bonjour on the LAN: printers, web panels and screens found by service type with their host, port, addresses and TXT records, the network's service types listed, .local names resolved, your own services advertised, and a watch that sees them come and go.7 views · 9 downloadsNTP time ntp.qwer · 1.0.0How far this server's clock is off: SNTP queries with offset, round trip, stratum and reference, several servers asked at once and the best kept (Kiss-o'-Death and unsynchronized ones set aside), NTP timestamps read, and a small time server on udp.listen.7 views · 9 downloadsPJL print jobs and status pjl.qwer · 1.0.0PDF, PostScript and PCL files printed on office printers without a driver, wrapped in PJL with copies, duplex, tray, held jobs released by PIN; and the printer asked for its status, model and page count, its codes told in words.6 views · 7 downloadsPrinter status printer_status.qwer · 1.0.0Any printer asked how it is in its own language, Zebra, PJL, TSPL, ESC/POS or Brother, or by SNMP for toner levels, with one clear answer: ready, paper out, ribbon out, head open, paused or jammed.9 views · 7 downloadsRaw printing and LPR raw_print.qwer · 1.0.0Any print job sent as it is to a network printer, on port 9100 or through an LPR/LPD queue, with timeouts, retries and the printer's reply read back, and a check that a printer answers.6 views · 6 downloadsCoAP coap.qwer · 1.0.0CoAP for IoT devices: GET, POST, PUT and DELETE with retransmission and separate responses, block-wise transfers both ways, observed resources whose notifications the program drains, /.well-known/core discovery, and a CoAP server of your own on udp.listen.7 views · 8 downloadsThread 0 posts
On SNMP: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.