fix(snmp): LLDP rem table — BulkWalk first, гибкий индекс, fallback GetNext, лог save LLDP
Made-with: Cursor
This commit is contained in:
+180
-8
@@ -1,6 +1,7 @@
|
||||
package scans
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gosnmp/gosnmp"
|
||||
)
|
||||
@@ -102,7 +104,9 @@ func (r *Runner) run(job ScanJob) {
|
||||
snmpRes, lldpRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS)
|
||||
_ = r.store.SaveSNMPResult(job.ID, snmpRes)
|
||||
for _, lr := range lldpRes {
|
||||
_ = r.store.SaveLLDPResult(job.ID, lr)
|
||||
if err := r.store.SaveLLDPResult(job.ID, lr); err != nil {
|
||||
log.Printf("scan %s: save LLDP для %s: %v", job.ID, lr.IP, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +180,13 @@ func probeSNMPAndLLDP(ip, community string, checkedAt time.Time, timeoutMS int)
|
||||
|
||||
out := SNMPResult{IP: ip, Success: true, CheckedAt: checkedAt}
|
||||
for _, vb := range pkt.Variables {
|
||||
switch vb.Name {
|
||||
case ".1.3.6.1.2.1.1.5.0":
|
||||
// Имена OID в ответах gosnmp всегда с ведущей «.», но сравниваем по нормализованной форме.
|
||||
switch snmpOIDTrim(vb.Name) {
|
||||
case "1.3.6.1.2.1.1.5.0":
|
||||
out.SysName = snmpValueToString(vb.Value)
|
||||
case ".1.3.6.1.2.1.1.1.0":
|
||||
case "1.3.6.1.2.1.1.1.0":
|
||||
out.SysDescr = snmpValueToString(vb.Value)
|
||||
case ".1.3.6.1.2.1.1.2.0":
|
||||
case "1.3.6.1.2.1.1.2.0":
|
||||
out.SysObjectID = snmpValueToString(vb.Value)
|
||||
}
|
||||
}
|
||||
@@ -200,12 +205,168 @@ func probeSNMPAndLLDP(ip, community string, checkedAt time.Time, timeoutMS int)
|
||||
|
||||
func snmpValueToString(v any) string {
|
||||
if b, ok := v.([]byte); ok {
|
||||
return string(b)
|
||||
if len(b) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
if utf8.Valid(b) {
|
||||
s := strings.TrimSpace(string(b))
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
// Chassis ID / port ID в LLDP часто приходят как OCTET STRING (MAC и т.д.) — не UTF-8
|
||||
return "0x" + hex.EncodeToString(b)
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
|
||||
func probeLLDP(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResult {
|
||||
// lldpRemColsPrefix — колонки lldpRemEntry: ...1.1.<col>.<timeMark>.<localPort>.<remIndex>
|
||||
const lldpRemColsPrefix = "1.0.8802.1.1.2.1.4.1.1"
|
||||
|
||||
// lldpRemTableOID — корень lldpRemTable (IEEE 802.1AB LLDP-MIB).
|
||||
const lldpRemTableOID = "1.0.8802.1.1.2.1.4.1"
|
||||
|
||||
type lldpRemAggRow struct {
|
||||
localPort, chassis, portID, sysName string
|
||||
}
|
||||
|
||||
func lldpRemRowsFromWalk(rows map[string]*lldpRemAggRow, pduName string, val any) {
|
||||
n := snmpOIDTrim(pduName)
|
||||
if !strings.HasPrefix(n, lldpRemColsPrefix+".") {
|
||||
return
|
||||
}
|
||||
rest := strings.TrimPrefix(n, lldpRemColsPrefix+".")
|
||||
parts := strings.Split(rest, ".")
|
||||
if len(parts) < 2 {
|
||||
return
|
||||
}
|
||||
col := parts[0]
|
||||
// Индекс строки таблицы — все субидентификаторы после номера колонки (часто 3 компонента, но оставляем произвольную длину).
|
||||
rowKey := strings.Join(parts[1:], ".")
|
||||
v := snmpValueToString(val)
|
||||
r := rows[rowKey]
|
||||
if r == nil {
|
||||
r = &lldpRemAggRow{}
|
||||
rows[rowKey] = r
|
||||
}
|
||||
switch col {
|
||||
case "2":
|
||||
r.localPort = v
|
||||
case "5":
|
||||
r.chassis = v
|
||||
case "7":
|
||||
r.portID = v
|
||||
case "9":
|
||||
r.sysName = v
|
||||
}
|
||||
}
|
||||
|
||||
// probeLLDPViaRemTableWalk один раз обходит всю lldpRemTable (стандарт IEEE 802.1AB).
|
||||
// Сначала GET-BULK (меньше обменов); при пустом результате — Walk (GetNext). Отдельно есть цепочка GetNext
|
||||
// в probeLLDPViaGetNextRemTable на случай сбоев внутреннего Walk у gosnmp на части прошивок.
|
||||
func probeLLDPViaRemTableWalk(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResult {
|
||||
rows := make(map[string]*lldpRemAggRow)
|
||||
|
||||
record := func(pduName string, val any) {
|
||||
lldpRemRowsFromWalk(rows, pduName, val)
|
||||
}
|
||||
|
||||
skipPDU := func(t gosnmp.Asn1BER) bool {
|
||||
return t == gosnmp.NoSuchObject || t == gosnmp.NoSuchInstance || t == gosnmp.EndOfMibView
|
||||
}
|
||||
|
||||
walkOID := "." + lldpRemTableOID
|
||||
pdus, berr := client.BulkWalkAll(walkOID)
|
||||
if berr != nil {
|
||||
log.Printf("lldp remTable BulkWalkAll target=%s: %v", client.Target, berr)
|
||||
}
|
||||
for _, p := range pdus {
|
||||
if skipPDU(p.Type) {
|
||||
continue
|
||||
}
|
||||
record(p.Name, p.Value)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
if err := client.Walk(walkOID, func(pdu gosnmp.SnmpPDU) error {
|
||||
if skipPDU(pdu.Type) {
|
||||
return nil
|
||||
}
|
||||
record(pdu.Name, pdu.Value)
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("lldp remTable Walk target=%s: %v", client.Target, err)
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]LLDPResult, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.localPort == "" && r.chassis == "" && r.portID == "" && r.sysName == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, LLDPResult{
|
||||
IP: ip,
|
||||
LocalPortNum: r.localPort,
|
||||
RemoteChassisID: r.chassis,
|
||||
RemotePortID: r.portID,
|
||||
RemoteSysName: r.sysName,
|
||||
CheckedAt: checkedAt,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// probeLLDPViaGetNextRemTable — пошаговый GetNext по поддереву lldpRemTable без пакетной логики Walk/BulkWalk.
|
||||
// Обходит случаи, когда snmpwalk с хоста работает, а внутренний walk gosnmp обрывается или не вызывает callback.
|
||||
func probeLLDPViaGetNextRemTable(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResult {
|
||||
rows := make(map[string]*lldpRemAggRow)
|
||||
record := func(pduName string, val any) {
|
||||
lldpRemRowsFromWalk(rows, pduName, val)
|
||||
}
|
||||
|
||||
const maxSteps = 80000
|
||||
oid := "." + lldpRemTableOID
|
||||
for step := 0; step < maxSteps; step++ {
|
||||
pkt, err := client.GetNext([]string{oid})
|
||||
if err != nil {
|
||||
log.Printf("lldp remTable GetNext target=%s step=%d: %v", client.Target, step, err)
|
||||
break
|
||||
}
|
||||
if pkt == nil || len(pkt.Variables) == 0 {
|
||||
break
|
||||
}
|
||||
vb := pkt.Variables[0]
|
||||
if vb.Type == gosnmp.NoSuchObject || vb.Type == gosnmp.NoSuchInstance || vb.Type == gosnmp.EndOfMibView {
|
||||
break
|
||||
}
|
||||
n := snmpOIDTrim(vb.Name)
|
||||
if n != lldpRemTableOID && !strings.HasPrefix(n, lldpRemTableOID+".") {
|
||||
break
|
||||
}
|
||||
if n != lldpRemTableOID {
|
||||
record(vb.Name, vb.Value)
|
||||
}
|
||||
oid = vb.Name
|
||||
}
|
||||
|
||||
out := make([]LLDPResult, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.localPort == "" && r.chassis == "" && r.portID == "" && r.sysName == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, LLDPResult{
|
||||
IP: ip,
|
||||
LocalPortNum: r.localPort,
|
||||
RemoteChassisID: r.chassis,
|
||||
RemotePortID: r.portID,
|
||||
RemoteSysName: r.sysName,
|
||||
CheckedAt: checkedAt,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func probeLLDPViaColumnWalks(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResult {
|
||||
const (
|
||||
lldpRemLocalPortNum = ".1.0.8802.1.1.2.1.4.1.1.2"
|
||||
lldpRemChassisID = ".1.0.8802.1.1.2.1.4.1.1.5"
|
||||
@@ -250,6 +411,17 @@ func probeLLDP(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResu
|
||||
return out
|
||||
}
|
||||
|
||||
func probeLLDP(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []LLDPResult {
|
||||
out := probeLLDPViaRemTableWalk(client, ip, checkedAt)
|
||||
if len(out) == 0 {
|
||||
out = probeLLDPViaColumnWalks(client, ip, checkedAt)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
out = probeLLDPViaGetNextRemTable(client, ip, checkedAt)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func snmpOIDTrim(s string) string {
|
||||
return strings.TrimPrefix(strings.TrimSpace(s), ".")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user