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
|
package scans
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/gosnmp/gosnmp"
|
"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)
|
snmpRes, lldpRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS)
|
||||||
_ = r.store.SaveSNMPResult(job.ID, snmpRes)
|
_ = r.store.SaveSNMPResult(job.ID, snmpRes)
|
||||||
for _, lr := range lldpRes {
|
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}
|
out := SNMPResult{IP: ip, Success: true, CheckedAt: checkedAt}
|
||||||
for _, vb := range pkt.Variables {
|
for _, vb := range pkt.Variables {
|
||||||
switch vb.Name {
|
// Имена OID в ответах gosnmp всегда с ведущей «.», но сравниваем по нормализованной форме.
|
||||||
case ".1.3.6.1.2.1.1.5.0":
|
switch snmpOIDTrim(vb.Name) {
|
||||||
|
case "1.3.6.1.2.1.1.5.0":
|
||||||
out.SysName = snmpValueToString(vb.Value)
|
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)
|
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)
|
out.SysObjectID = snmpValueToString(vb.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,12 +205,168 @@ func probeSNMPAndLLDP(ip, community string, checkedAt time.Time, timeoutMS int)
|
|||||||
|
|
||||||
func snmpValueToString(v any) string {
|
func snmpValueToString(v any) string {
|
||||||
if b, ok := v.([]byte); ok {
|
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 (
|
const (
|
||||||
lldpRemLocalPortNum = ".1.0.8802.1.1.2.1.4.1.1.2"
|
lldpRemLocalPortNum = ".1.0.8802.1.1.2.1.4.1.1.2"
|
||||||
lldpRemChassisID = ".1.0.8802.1.1.2.1.4.1.1.5"
|
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
|
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 {
|
func snmpOIDTrim(s string) string {
|
||||||
return strings.TrimPrefix(strings.TrimSpace(s), ".")
|
return strings.TrimPrefix(strings.TrimSpace(s), ".")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user