Protocols

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

#monitoring#network#devices#printing#alerts

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 error

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.

snmp_values(host: str, oid_list: list, options: any) → anyor an error

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.

snmp_get_next(host: str, oid_list: list, options: any) → listor an error

The 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 error

Get-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 error

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).

snmp_table(host: str, table: str, options: any) → listor an error

A 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 error

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").

snmp_trap(host: str, trap_oid: str, values: list, options: any) → anyor an error

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).

snmp_parse(bytes: list, options: any) → anyor an error

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).

snmp_response_for(bytes: list, values: list) → listor an error

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 }]).

snmp_oid(name: str) → stror an error

A 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 error

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".

Get it

$ quillo install snmp               # snmp.qwer into this folder$ quillo install snmp --example     # and its example, snmp.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/snmp.qwer$ curl -O https://quillolang.org/plugins/files/snmp.qwe

Then, in your program, all of it or only what you call:

main.qwe
use snmp.qweruse snmp_get, snmp_values from snmp      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
md-5 = "0.10"sha1 = "0.10"sha2 = "0.10"hmac = "0.12"aes = "0.8"des = "0.8"

Example

snmp.qwe
// 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

snmp.qwer
//! 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(&reg);        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), &copy) != 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)?))}
Bonjour and mDNS 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.networkdevicesprintingiot7 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.timemonitoringnetworkdevices7 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.printingpdfstatusdevicesmonitoring6 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.printingstatusmonitoringdevices9 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.printingnetworkretriesdevices6 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.iotsensorsdevicesnetwork7 views · 8 downloads
Thread
89Useful? Like it, or add to its thread below.

Thread 0 posts

On SNMP: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.