From 064edfd098609b74f2f082de8b29832d0d7713db Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 19 Aug 2026 03:08:34 +0000 Subject: [PATCH] fix(snmp): probe with GETBULK, which is what every device answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit snmp-verify has always reported FAIL for neo while snmp-exporter scraped it successfully. The difference was the PDU type: the exporter bulk-walks, this script sent a GET. The MokerLink switch answers GETBULK and silently drops both GET and GETNEXT, so no GET-based probe could ever succeed against it. That is not a cosmetic FAIL. --old infers "rejected" from a timeout, so a probe the device never answers reports every community as refused — including one that still works. Against neo the old-community check was capable only of agreeing with the operator, which is the single thing this script exists not to do. It was skipping neo for an unrelated reason, which is the only thing that kept the wrong answer from being printed. One GETBULK with non-repeaters 0 and max-repetitions 1 is a single round trip returning one varbind — the same cost as the GET it replaces, which matters on a switch that has been bricked by a full walk before. Verified against all four devices. Aimed at the sysDescr node rather than sysDescr.0, since GETBULK is GETNEXT-shaped. The returned OID is asserted, because a GETBULK against a device with no sysDescr returns the next varbind rather than an error, and that would otherwise read as a pass carrying the wrong value. All four now PASS. Refs #9 Co-Authored-By: Claude Opus 5 --- scripts/snmp-verify.sh | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/scripts/snmp-verify.sh b/scripts/snmp-verify.sh index c34b8c6..02920a8 100755 --- a/scripts/snmp-verify.sh +++ b/scripts/snmp-verify.sh @@ -43,6 +43,10 @@ fail() { printf '\033[0;31m FAIL\033[0m %s\n' "$*"; FAILED=1; } skip() { printf '\033[0;33m SKIP\033[0m %s\n' "$*"; } head_() { printf '\n\033[1m%s\033[0m\n' "$*"; } +# sysDescr, as a node and as the instance GETBULK returns for it. +SYSDESCR_NODE='1.3.6.1.2.1.1.1' +SYSDESCR_OID='.1.3.6.1.2.1.1.1.0' + FAILED=0 CHECK_OLD=0 DRY_RUN=0 @@ -140,10 +144,23 @@ Regenerate it with scripts/gen-secret.sh and set it on the device." # Sets PROBE_STATUS to ok | noresponse | nosuchobject | error, and PROBE_DETAIL # to something safe to print. # -# snmpget rather than snmpwalk: one GET of exactly sysDescr.0, one round trip. -# The old runbook's `snmpwalk ... 1.3.6.1.2.1.1.1.0` did a GETNEXT *from* -# sysDescr.0 and so returned sysObjectID — its claim that each device "must -# return a sysDescr string" was never quite right. +# GETBULK, not GET. The MokerLink switch answers GETBULK and silently drops both +# GET and GETNEXT, so a GET-based probe times out against it no matter which +# community is used. That is not merely a cosmetic FAIL: --old infers "rejected" +# from a timeout, so a probe the device never answers would report every +# community as refused, including one that still works. The check would agree +# with the operator instead of testing them, which is the one thing this script +# exists not to do. +# +# One GETBULK with non-repeaters 0 and max-repetitions 1 is a single round trip +# returning a single varbind — the same cost as the GET it replaces. All four +# devices are SNMPv2c and all four answer it. +# +# It is aimed at the sysDescr *node*, not sysDescr.0, because GETBULK is +# GETNEXT-shaped: asking for 1.3.6.1.2.1.1.1 returns 1.3.6.1.2.1.1.1.0. The +# returned OID is then asserted, because unlike GET, a GETBULK against a device +# with no sysDescr does not raise an error — it just returns whatever comes +# next, which would otherwise be reported as a pass carrying the wrong value. # # MIBS= disables MIB loading: the OID is numeric, and a partial MIB directory # on the operator's host otherwise prints twenty lines of parse warnings that @@ -158,18 +175,20 @@ probe() { PROBE_STATUS=""; PROBE_DETAIL="" out="$(timeout 15 env MIBS= SNMPCONFPATH="${conf_dir}" \ - snmpget -v2c -t 2 -r "${retries}" -Oqv "${ip}${SNMP_VERIFY_PORT:+:${SNMP_VERIFY_PORT}}" \ - 1.3.6.1.2.1.1.1.0 2>&1)" || rc=$? + snmpbulkget -v2c -t 2 -r "${retries}" -Cn0 -Cr1 -Oqn \ + "${ip}${SNMP_VERIFY_PORT:+:${SNMP_VERIFY_PORT}}" \ + "${SYSDESCR_NODE}" 2>&1)" || rc=$? # net-snmp can echo an offending config line on a parse error, and that line # is the defCommunity line. So `out` is classified and then discarded — it is # never printed, and never passed to sed for redaction either, because that # would put the community into sed's own argument vector. - if ((rc == 0)) && [[ "${out}" == *"No Such Object"* || "${out}" == *"No Such Instance"* ]]; then - PROBE_STATUS="nosuchobject" - elif ((rc == 0)) && [[ -n "${out}" ]]; then + if ((rc == 0)) && [[ "${out}" == "${SYSDESCR_OID} "* ]]; then PROBE_STATUS="ok" - PROBE_DETAIL="$(printf '%s' "${out}" | tr -cd '[:print:]' | cut -c1-60)" + PROBE_DETAIL="$(printf '%s' "${out#"${SYSDESCR_OID} "}" | tr -cd '[:print:]' | cut -c1-60)" + elif ((rc == 0)) && [[ -n "${out}" ]]; then + # Answered, but the varbind after the sysDescr node is something else. + PROBE_STATUS="nosuchobject" elif [[ "${out}" == *"Timeout"* ]]; then PROBE_STATUS="noresponse" else