Add LLDP neighbor collection and endpoint.

Collect LLDP-MIB neighbor fields during SNMP scans, persist records, and expose LLDP results via REST.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 21:58:30 +10:00
parent cca4477792
commit ec7abe384d
6 changed files with 212 additions and 5 deletions
+69 -5
View File
@@ -7,6 +7,7 @@ import (
"net/netip"
"os/exec"
"runtime"
"strings"
"strconv"
"sync"
"time"
@@ -97,7 +98,11 @@ func (r *Runner) run(job ScanJob) {
}
}
if isUp && r.cfg.SNMPEnabled {
_ = r.store.SaveSNMPResult(job.ID, probeSNMP(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)
for _, lr := range lldpRes {
_ = r.store.SaveLLDPResult(job.ID, lr)
}
}
mu.Lock()
@@ -137,7 +142,7 @@ func (r *Runner) run(job ScanJob) {
})
}
func probeSNMP(ip, community string, checkedAt time.Time, timeoutMS int) SNMPResult {
func probeSNMPAndLLDP(ip, community string, checkedAt time.Time, timeoutMS int) (SNMPResult, []LLDPResult) {
if timeoutMS <= 0 {
timeoutMS = 700
}
@@ -154,7 +159,7 @@ func probeSNMP(ip, community string, checkedAt time.Time, timeoutMS int) SNMPRes
Retries: 1,
}
if err := client.Connect(); err != nil {
return SNMPResult{IP: ip, Success: false, Error: err.Error(), CheckedAt: checkedAt}
return SNMPResult{IP: ip, Success: false, Error: err.Error(), CheckedAt: checkedAt}, nil
}
defer client.Conn.Close()
@@ -165,7 +170,7 @@ func probeSNMP(ip, community string, checkedAt time.Time, timeoutMS int) SNMPRes
}
pkt, err := client.Get(oids)
if err != nil {
return SNMPResult{IP: ip, Success: false, Error: err.Error(), CheckedAt: checkedAt}
return SNMPResult{IP: ip, Success: false, Error: err.Error(), CheckedAt: checkedAt}, nil
}
out := SNMPResult{IP: ip, Success: true, CheckedAt: checkedAt}
@@ -179,7 +184,7 @@ func probeSNMP(ip, community string, checkedAt time.Time, timeoutMS int) SNMPRes
out.SysObjectID = snmpValueToString(vb.Value)
}
}
return out
return out, probeLLDP(client, ip, checkedAt)
}
func snmpValueToString(v any) string {
@@ -189,6 +194,65 @@ func snmpValueToString(v any) string {
return fmt.Sprint(v)
}
func probeLLDP(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"
lldpRemPortID = ".1.0.8802.1.1.2.1.4.1.1.7"
lldpRemSysName = ".1.0.8802.1.1.2.1.4.1.1.9"
)
ports := walkAsMap(client, lldpRemLocalPortNum)
chassis := walkAsMap(client, lldpRemChassisID)
remotePorts := walkAsMap(client, lldpRemPortID)
sysNames := walkAsMap(client, lldpRemSysName)
keys := make(map[string]struct{})
for k := range ports {
keys[k] = struct{}{}
}
for k := range chassis {
keys[k] = struct{}{}
}
for k := range remotePorts {
keys[k] = struct{}{}
}
for k := range sysNames {
keys[k] = struct{}{}
}
out := make([]LLDPResult, 0, len(keys))
for k := range keys {
item := LLDPResult{
IP: ip,
LocalPortNum: ports[k],
RemoteChassisID: chassis[k],
RemotePortID: remotePorts[k],
RemoteSysName: sysNames[k],
CheckedAt: checkedAt,
}
if item.LocalPortNum == "" && item.RemoteChassisID == "" && item.RemotePortID == "" && item.RemoteSysName == "" {
continue
}
out = append(out, item)
}
return out
}
func walkAsMap(client *gosnmp.GoSNMP, oid string) map[string]string {
out := map[string]string{}
pdus, err := client.BulkWalkAll(oid)
if err != nil {
return out
}
prefix := oid + "."
for _, p := range pdus {
key := strings.TrimPrefix(p.Name, prefix)
out[key] = snmpValueToString(p.Value)
}
return out
}
func expandTargets(cidrs []string, excludeIPs []string) ([]string, error) {
excluded := make(map[string]struct{}, len(excludeIPs))
for _, ip := range excludeIPs {