Пристрій сам каже, що він таке, і шаблон чіпляється без натискань. Системна група знімається тією ж SNMP-сесією, що й обхід топології: три зайві PDU дешевші за окремий чек із власним розкладом. Збіг за префіксом на межі компонента: моделей у виробника тисячі, і повний збіг означав би рядок на кожну коробку. Довший префікс перемагає. Дванадцять вбудованих правил на основних виробників. Шаблони тільки додаються, ніколи не знімаються: автоматика знає модель пристрою, але не знає, чому цьому хосту дали ще один шаблон руками. DiscoveredDevice отримав device_id: зіставляти за адресою не можна — за одним NAT кілька хостів мають ту саму адресу опитування. Заразом дубль перевірки перестав давати «внутрішню помилку». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
719 lines
22 KiB
Go
719 lines
22 KiB
Go
// Package topology — автовиявлення сусідів і інвентаризація портів.
|
||
//
|
||
// Модуль НЕ вирішує, хто з ким з'єднаний. Він доповідає сире:
|
||
// «на порту X бачу chassis Y, port Z». Зведення в лінки — на сервері
|
||
// (topo.neighbors → topo.links), і саме тому правила зіставлення можна
|
||
// міняти без оновлення зондів у полі.
|
||
package topology
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/gosnmp/gosnmp"
|
||
"github.com/netpulse/netpulse/agent/internal/module"
|
||
"github.com/netpulse/netpulse/agent/internal/snmpx"
|
||
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
|
||
"google.golang.org/protobuf/types/known/timestamppb"
|
||
)
|
||
|
||
// LLDP-MIB (1.0.8802.1.1.2)
|
||
const (
|
||
oidLldpLocPortIDSubtype = ".1.0.8802.1.1.2.1.3.7.1.2"
|
||
oidLldpLocPortID = ".1.0.8802.1.1.2.1.3.7.1.3"
|
||
|
||
oidLldpRemChassisIDSubtype = ".1.0.8802.1.1.2.1.4.1.1.4"
|
||
oidLldpRemChassisID = ".1.0.8802.1.1.2.1.4.1.1.5"
|
||
oidLldpRemPortIDSubtype = ".1.0.8802.1.1.2.1.4.1.1.6"
|
||
oidLldpRemPortID = ".1.0.8802.1.1.2.1.4.1.1.7"
|
||
oidLldpRemPortDesc = ".1.0.8802.1.1.2.1.4.1.1.8"
|
||
oidLldpRemSysName = ".1.0.8802.1.1.2.1.4.1.1.9"
|
||
oidLldpRemSysCapEnabled = ".1.0.8802.1.1.2.1.4.1.1.12"
|
||
)
|
||
|
||
// CISCO-CDP-MIB (cdpCacheTable). Індекс: ifIndex.deviceIndex —
|
||
// перший компонент одразу дає локальний порт, на відміну від LLDP.
|
||
const (
|
||
oidCdpCacheAddress = ".1.3.6.1.4.1.9.9.23.1.2.1.1.4"
|
||
oidCdpCacheVersion = ".1.3.6.1.4.1.9.9.23.1.2.1.1.5"
|
||
oidCdpCacheDeviceID = ".1.3.6.1.4.1.9.9.23.1.2.1.1.6"
|
||
oidCdpCacheDevicePort = ".1.3.6.1.4.1.9.9.23.1.2.1.1.7"
|
||
oidCdpCachePlatform = ".1.3.6.1.4.1.9.9.23.1.2.1.1.8"
|
||
oidCdpCacheCapabilities = ".1.3.6.1.4.1.9.9.23.1.2.1.1.9"
|
||
)
|
||
|
||
// IP-MIB / BRIDGE-MIB
|
||
const (
|
||
oidIPNetToMediaPhysAddress = ".1.3.6.1.2.1.4.22.1.2"
|
||
oidDot1dTpFdbPort = ".1.3.6.1.2.1.17.4.3.1.2"
|
||
oidDot1dBasePortIfIndex = ".1.3.6.1.2.1.17.1.4.1.2"
|
||
)
|
||
|
||
// IF-MIB
|
||
const (
|
||
oidIfDescr = ".1.3.6.1.2.1.2.2.1.2"
|
||
oidIfType = ".1.3.6.1.2.1.2.2.1.3"
|
||
oidIfMtu = ".1.3.6.1.2.1.2.2.1.4"
|
||
oidIfSpeed = ".1.3.6.1.2.1.2.2.1.5"
|
||
oidIfPhysAddress = ".1.3.6.1.2.1.2.2.1.6"
|
||
oidIfAdminStatus = ".1.3.6.1.2.1.2.2.1.7"
|
||
oidIfOperStatus = ".1.3.6.1.2.1.2.2.1.8"
|
||
oidIfName = ".1.3.6.1.2.1.31.1.1.1.1"
|
||
oidIfHighSpeed = ".1.3.6.1.2.1.31.1.1.1.15"
|
||
oidIfAlias = ".1.3.6.1.2.1.31.1.1.1.18"
|
||
)
|
||
|
||
// Params — вміст params_json для topology.discover.
|
||
type Params struct {
|
||
// lldp | cdp | arp | fdb. Порожньо — lldp + cdp.
|
||
Protos []string `json:"protos"`
|
||
// Збирати інвентар портів разом із сусідами. Обидва беруться з
|
||
// одного SNMP-обходу, тому окремий чек був би зайвим трафіком.
|
||
CollectInterfaces *bool `json:"collect_interfaces"`
|
||
}
|
||
|
||
type Module struct{}
|
||
|
||
func New() *Module { return &Module{} }
|
||
|
||
func (m *Module) Key() string { return "topology" }
|
||
func (m *Module) CheckTypes() []string { return []string{"topology.discover"} }
|
||
func (m *Module) Close() error { return nil }
|
||
|
||
func (m *Module) Run(ctx context.Context, task module.Task) (module.Result, error) {
|
||
var p Params
|
||
if len(task.Params) > 0 {
|
||
if err := json.Unmarshal(task.Params, &p); err != nil {
|
||
return module.Result{}, fmt.Errorf("невалідні params для topology.discover: %w", err)
|
||
}
|
||
}
|
||
if len(p.Protos) == 0 {
|
||
p.Protos = []string{"lldp", "cdp"}
|
||
}
|
||
collectIfaces := p.CollectInterfaces == nil || *p.CollectInterfaces
|
||
|
||
client, err := snmpx.Dial(ctx, task.Target.Address, task.Credentials, task.Timeout)
|
||
if err != nil {
|
||
return module.Result{}, err
|
||
}
|
||
defer client.Conn.Close()
|
||
|
||
var (
|
||
res module.Result
|
||
errs []string
|
||
)
|
||
|
||
// Системна група — найдешевше, що можна взяти з цієї сесії, і
|
||
// єдине джерело для автопризначення шаблонів за sysObjectID.
|
||
if d := collectSystem(ctx, client, task.DeviceID, task.Target.Address); d != nil {
|
||
res.Devices = append(res.Devices, d)
|
||
}
|
||
|
||
// Інвентар портів потрібен першим: LLDP оперує власною нумерацією
|
||
// портів, і без ifName/ifDescr її нема на що відобразити.
|
||
ifaces, err := collectInterfaces(ctx, client, task.DeviceID)
|
||
if err != nil {
|
||
errs = append(errs, "інтерфейси: "+err.Error())
|
||
} else if collectIfaces {
|
||
res.InterfaceRecords = ifaces
|
||
}
|
||
|
||
byIndex := make(map[int64]*npv1.InterfaceRecord, len(ifaces))
|
||
for _, r := range ifaces {
|
||
byIndex[r.IfIndex] = r
|
||
}
|
||
|
||
for _, proto := range p.Protos {
|
||
var (
|
||
found []*npv1.NeighborRecord
|
||
perr error
|
||
)
|
||
switch strings.ToLower(proto) {
|
||
case "lldp":
|
||
found, perr = collectLLDP(ctx, client, task.DeviceID, byIndex)
|
||
case "cdp":
|
||
found, perr = collectCDP(ctx, client, task.DeviceID, byIndex)
|
||
case "arp":
|
||
found, perr = collectARP(ctx, client, task.DeviceID, byIndex)
|
||
case "fdb":
|
||
found, perr = collectFDB(ctx, client, task.DeviceID, byIndex)
|
||
default:
|
||
continue
|
||
}
|
||
if perr != nil {
|
||
// Відсутність LLDP-MIB — норма для половини обладнання,
|
||
// а не привід завалити весь чек: CDP може працювати.
|
||
errs = append(errs, proto+": "+perr.Error())
|
||
continue
|
||
}
|
||
res.Neighbors = append(res.Neighbors, found...)
|
||
}
|
||
|
||
if len(res.Neighbors) == 0 && len(res.InterfaceRecords) == 0 && len(errs) > 0 {
|
||
return module.Result{}, fmt.Errorf("автовиявлення не дало результату: %s", strings.Join(errs, "; "))
|
||
}
|
||
|
||
if len(errs) > 0 {
|
||
res.Payload, _ = json.Marshal(map[string]any{"warnings": errs})
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Інтерфейси
|
||
// ---------------------------------------------------------------------
|
||
|
||
func collectInterfaces(ctx context.Context, c *gosnmp.GoSNMP, deviceID string) ([]*npv1.InterfaceRecord, error) {
|
||
recs := make(map[uint64]*npv1.InterfaceRecord)
|
||
|
||
get := func(idx uint64) *npv1.InterfaceRecord {
|
||
r, ok := recs[idx]
|
||
if !ok {
|
||
r = &npv1.InterfaceRecord{DeviceId: deviceID, IfIndex: int64(idx)}
|
||
recs[idx] = r
|
||
}
|
||
return r
|
||
}
|
||
|
||
columns := []struct {
|
||
oid string
|
||
apply func(r *npv1.InterfaceRecord, pdu gosnmp.SnmpPDU)
|
||
}{
|
||
{oidIfDescr, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if r.Name == "" {
|
||
r.Name = snmpx.AsString(p)
|
||
}
|
||
}},
|
||
// ifName точніший за ifDescr і має пріоритет: саме його
|
||
// віддає LLDP як port-id, тому саме за ним зійдеться лінк.
|
||
{oidIfName, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if s := snmpx.AsString(p); s != "" {
|
||
r.Name = s
|
||
}
|
||
}},
|
||
{oidIfAlias, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
r.Alias = snmpx.AsString(p)
|
||
}},
|
||
{oidIfType, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok {
|
||
r.Type = ifTypeName(v)
|
||
}
|
||
}},
|
||
{oidIfMtu, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok {
|
||
r.Mtu = uint32(v)
|
||
}
|
||
}},
|
||
{oidIfSpeed, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok && r.SpeedBps == 0 {
|
||
r.SpeedBps = v
|
||
}
|
||
}},
|
||
// ifSpeed 32-бітний і на 10G+ упирається в 4294967295.
|
||
// ifHighSpeed (у Мбіт/с) — єдине джерело правди для швидких портів.
|
||
{oidIfHighSpeed, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok && v > 0 {
|
||
r.SpeedBps = v * 1_000_000
|
||
}
|
||
}},
|
||
{oidIfPhysAddress, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if b, ok := snmpx.AsBytes(p); ok {
|
||
r.Mac = snmpx.FormatMAC(b)
|
||
}
|
||
}},
|
||
{oidIfAdminStatus, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok {
|
||
r.AdminStatus = adminStatusName(v)
|
||
}
|
||
}},
|
||
{oidIfOperStatus, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(p); ok {
|
||
r.OperStatus = operStatusName(v)
|
||
}
|
||
}},
|
||
}
|
||
|
||
for _, col := range columns {
|
||
oid := col.oid
|
||
apply := col.apply
|
||
err := snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
|
||
if !ok || len(idx) != 1 {
|
||
return nil
|
||
}
|
||
apply(get(idx[0]), pdu)
|
||
return nil
|
||
})
|
||
if err != nil && oid == oidIfDescr {
|
||
// Без ifDescr таблиці інтерфейсів фактично немає.
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
out := make([]*npv1.InterfaceRecord, 0, len(recs))
|
||
for _, r := range recs {
|
||
if r.Name == "" {
|
||
r.Name = fmt.Sprintf("if%d", r.IfIndex)
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// LLDP
|
||
// ---------------------------------------------------------------------
|
||
|
||
// collectLLDP читає lldpRemTable.
|
||
//
|
||
// Індекс рядка: timeMark.lldpRemLocalPortNum.lldpRemIndex. Другий
|
||
// компонент — НЕ ifIndex, а власна нумерація LLDP. Більшість
|
||
// реалізацій робить її рівною ifIndex, але покладатись на це не можна,
|
||
// тому спершу пробуємо відобразити через lldpLocPortId (ім'я порту).
|
||
func collectLLDP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
|
||
localPorts, err := lldpLocalPortMap(ctx, c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
byRow := make(map[string]*npv1.NeighborRecord)
|
||
subtypes := make(map[string]uint64)
|
||
portSubtypes := make(map[string]uint64)
|
||
|
||
row := func(idx snmpx.Index) *npv1.NeighborRecord {
|
||
key := idx.Key()
|
||
r, ok := byRow[key]
|
||
if !ok {
|
||
r = &npv1.NeighborRecord{
|
||
DeviceId: deviceID,
|
||
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_LLDP,
|
||
SeenAt: timestamppb.Now(),
|
||
}
|
||
if portNum, ok := idx.At(1); ok {
|
||
resolveLocalPort(r, portNum, localPorts, byIndex)
|
||
}
|
||
byRow[key] = r
|
||
}
|
||
return r
|
||
}
|
||
|
||
walkCol := func(oid string, fn func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU)) error {
|
||
return snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
|
||
if !ok || len(idx) < 3 {
|
||
return nil
|
||
}
|
||
fn(row(idx), idx, pdu)
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// Підтипи читаємо першими: від них залежить, як тлумачити
|
||
// chassis-id і port-id — як MAC чи як текст.
|
||
if err := walkCol(oidLldpRemChassisIDSubtype, func(_ *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(pdu); ok {
|
||
subtypes[idx.Key()] = v
|
||
}
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
_ = walkCol(oidLldpRemPortIDSubtype, func(_ *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
if v, ok := snmpx.AsUint(pdu); ok {
|
||
portSubtypes[idx.Key()] = v
|
||
}
|
||
})
|
||
|
||
if err := walkCol(oidLldpRemChassisID, func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
raw, _ := snmpx.AsBytes(pdu)
|
||
// subtype 4 = macAddress
|
||
if subtypes[idx.Key()] == 4 {
|
||
if mac := snmpx.FormatMAC(raw); mac != "" {
|
||
r.RemoteChassisId = mac
|
||
r.RemoteMac = mac
|
||
return
|
||
}
|
||
}
|
||
r.RemoteChassisId = snmpx.AsString(pdu)
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
_ = walkCol(oidLldpRemPortID, func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
raw, _ := snmpx.AsBytes(pdu)
|
||
// subtype 3 = macAddress
|
||
if portSubtypes[idx.Key()] == 3 {
|
||
if mac := snmpx.FormatMAC(raw); mac != "" {
|
||
r.RemotePortId = mac
|
||
return
|
||
}
|
||
}
|
||
r.RemotePortId = snmpx.AsString(pdu)
|
||
})
|
||
_ = walkCol(oidLldpRemPortDesc, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
r.RemotePortDescr = snmpx.AsString(pdu)
|
||
})
|
||
_ = walkCol(oidLldpRemSysName, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
r.RemoteSystemName = snmpx.AsString(pdu)
|
||
})
|
||
_ = walkCol(oidLldpRemSysCapEnabled, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
|
||
if raw, ok := snmpx.AsBytes(pdu); ok {
|
||
r.RemoteCapabilities = decodeLldpCaps(raw)
|
||
}
|
||
})
|
||
|
||
out := make([]*npv1.NeighborRecord, 0, len(byRow))
|
||
for _, r := range byRow {
|
||
// Сусід без жодного ідентифікатора не зіставиться ні з чим —
|
||
// краще не засмічувати topo.neighbors.
|
||
if r.RemoteChassisId == "" && r.RemoteSystemName == "" && r.RemoteMac == "" {
|
||
continue
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// lldpLocalPortMap: lldpRemLocalPortNum → ім'я локального порту.
|
||
func lldpLocalPortMap(ctx context.Context, c *gosnmp.GoSNMP) (map[uint64]string, error) {
|
||
subtypes := make(map[uint64]uint64)
|
||
_ = snmpx.Walk(ctx, c, oidLldpLocPortIDSubtype, func(pdu gosnmp.SnmpPDU) error {
|
||
if idx, ok := snmpx.SplitIndex(oidLldpLocPortIDSubtype, pdu.Name); ok && len(idx) == 1 {
|
||
if v, ok := snmpx.AsUint(pdu); ok {
|
||
subtypes[idx[0]] = v
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
|
||
names := make(map[uint64]string)
|
||
err := snmpx.Walk(ctx, c, oidLldpLocPortID, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oidLldpLocPortID, pdu.Name)
|
||
if !ok || len(idx) != 1 {
|
||
return nil
|
||
}
|
||
// subtype 5 = interfaceName, 7 = local — обидва текстові.
|
||
names[idx[0]] = snmpx.AsString(pdu)
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return names, err
|
||
}
|
||
return names, nil
|
||
}
|
||
|
||
// resolveLocalPort прив'язує запис до локального інтерфейсу.
|
||
func resolveLocalPort(r *npv1.NeighborRecord, portNum uint64, localPorts map[uint64]string, byIndex map[int64]*npv1.InterfaceRecord) {
|
||
name := localPorts[portNum]
|
||
|
||
if name != "" {
|
||
r.LocalPortName = name
|
||
for _, ifc := range byIndex {
|
||
if strings.EqualFold(ifc.Name, name) || strings.EqualFold(ifc.Alias, name) {
|
||
r.LocalIfIndex = ifc.IfIndex
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// Запасний варіант: більшість реалізацій робить lldpLocPortNum
|
||
// рівним ifIndex. Приймаємо лише якщо такий інтерфейс справді є —
|
||
// інакше сервер отримає посилання в нікуди.
|
||
if ifc, ok := byIndex[int64(portNum)]; ok {
|
||
r.LocalIfIndex = ifc.IfIndex
|
||
if r.LocalPortName == "" {
|
||
r.LocalPortName = ifc.Name
|
||
}
|
||
}
|
||
}
|
||
|
||
// decodeLldpCaps розбирає бітову маску можливостей (LLDP-MIB).
|
||
func decodeLldpCaps(raw []byte) []string {
|
||
if len(raw) == 0 {
|
||
return nil
|
||
}
|
||
names := []string{"other", "repeater", "bridge", "wlan-ap", "router",
|
||
"telephone", "docsis", "station"}
|
||
|
||
var out []string
|
||
bits := raw[0]
|
||
for i, name := range names {
|
||
if i >= 8 {
|
||
break
|
||
}
|
||
if bits&(1<<(7-uint(i))) != 0 {
|
||
out = append(out, name)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// CDP
|
||
// ---------------------------------------------------------------------
|
||
|
||
func collectCDP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
|
||
byRow := make(map[string]*npv1.NeighborRecord)
|
||
|
||
row := func(idx snmpx.Index) *npv1.NeighborRecord {
|
||
key := idx.Key()
|
||
r, ok := byRow[key]
|
||
if !ok {
|
||
r = &npv1.NeighborRecord{
|
||
DeviceId: deviceID,
|
||
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_CDP,
|
||
SeenAt: timestamppb.Now(),
|
||
}
|
||
// У CDP перший компонент індексу — одразу ifIndex.
|
||
if ifIdx, ok := idx.At(0); ok {
|
||
if ifc, exists := byIndex[int64(ifIdx)]; exists {
|
||
r.LocalIfIndex = ifc.IfIndex
|
||
r.LocalPortName = ifc.Name
|
||
} else {
|
||
r.LocalIfIndex = int64(ifIdx)
|
||
}
|
||
}
|
||
byRow[key] = r
|
||
}
|
||
return r
|
||
}
|
||
|
||
walkCol := func(oid string, fn func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU)) error {
|
||
return snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
|
||
if !ok || len(idx) < 2 {
|
||
return nil
|
||
}
|
||
fn(row(idx), pdu)
|
||
return nil
|
||
})
|
||
}
|
||
|
||
if err := walkCol(oidCdpCacheDeviceID, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
s := snmpx.AsString(pdu)
|
||
r.RemoteSystemName = s
|
||
// CDP device-id часто є серійником або MAC, а не іменем.
|
||
// Якщо це схоже на MAC — кладемо і в chassis_id, бо саме за
|
||
// ним сервер зіставляє надійніше, ніж за іменем.
|
||
if raw, ok := snmpx.AsBytes(pdu); ok {
|
||
if mac := snmpx.FormatMAC(raw); mac != "" {
|
||
r.RemoteChassisId = mac
|
||
r.RemoteMac = mac
|
||
}
|
||
}
|
||
if r.RemoteChassisId == "" {
|
||
r.RemoteChassisId = s
|
||
}
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
_ = walkCol(oidCdpCacheDevicePort, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
r.RemotePortId = snmpx.AsString(pdu)
|
||
})
|
||
_ = walkCol(oidCdpCachePlatform, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
r.RemotePlatform = snmpx.AsString(pdu)
|
||
})
|
||
_ = walkCol(oidCdpCacheVersion, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
if r.RemotePortDescr == "" {
|
||
r.RemotePortDescr = truncate(snmpx.AsString(pdu), 120)
|
||
}
|
||
})
|
||
_ = walkCol(oidCdpCacheAddress, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
if raw, ok := snmpx.AsBytes(pdu); ok {
|
||
r.RemoteMgmtIp = snmpx.FormatIP(raw)
|
||
}
|
||
})
|
||
_ = walkCol(oidCdpCacheCapabilities, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
|
||
if raw, ok := snmpx.AsBytes(pdu); ok {
|
||
r.RemoteCapabilities = decodeCdpCaps(raw)
|
||
}
|
||
})
|
||
|
||
out := make([]*npv1.NeighborRecord, 0, len(byRow))
|
||
for _, r := range byRow {
|
||
if r.RemoteChassisId == "" && r.RemoteSystemName == "" {
|
||
continue
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func decodeCdpCaps(raw []byte) []string {
|
||
if len(raw) < 4 {
|
||
return nil
|
||
}
|
||
bits := uint32(raw[0])<<24 | uint32(raw[1])<<16 | uint32(raw[2])<<8 | uint32(raw[3])
|
||
names := map[uint32]string{
|
||
0x01: "router", 0x02: "transparent-bridge", 0x04: "source-route-bridge",
|
||
0x08: "switch", 0x10: "host", 0x20: "igmp", 0x40: "repeater",
|
||
}
|
||
|
||
var out []string
|
||
for bit, name := range names {
|
||
if bits&bit != 0 {
|
||
out = append(out, name)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// ARP і FDB
|
||
//
|
||
// Це не «сусіди» в сенсі LLDP: ARP каже лише, що якийсь MAC живе за
|
||
// цим портом. Але для пристроїв без LLDP/CDP (принтери, IP-камери,
|
||
// дешеві комутатори) це єдиний шанс потрапити на мапу, тому сервер
|
||
// приймає їх з низькою впевненістю.
|
||
// ---------------------------------------------------------------------
|
||
|
||
func collectARP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
|
||
var out []*npv1.NeighborRecord
|
||
|
||
err := snmpx.Walk(ctx, c, oidIPNetToMediaPhysAddress, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oidIPNetToMediaPhysAddress, pdu.Name)
|
||
if !ok || len(idx) < 5 {
|
||
return nil
|
||
}
|
||
ip, ok := idx.IPv4At(1)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
raw, _ := snmpx.AsBytes(pdu)
|
||
mac := snmpx.FormatMAC(raw)
|
||
if mac == "" {
|
||
return nil
|
||
}
|
||
|
||
r := &npv1.NeighborRecord{
|
||
DeviceId: deviceID,
|
||
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_ARP,
|
||
RemoteMac: mac,
|
||
RemoteMgmtIp: ip,
|
||
SeenAt: timestamppb.Now(),
|
||
}
|
||
if ifc, exists := byIndex[int64(idx[0])]; exists {
|
||
r.LocalIfIndex = ifc.IfIndex
|
||
r.LocalPortName = ifc.Name
|
||
} else {
|
||
r.LocalIfIndex = int64(idx[0])
|
||
}
|
||
out = append(out, r)
|
||
return nil
|
||
})
|
||
|
||
return out, err
|
||
}
|
||
|
||
func collectFDB(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
|
||
// dot1dTpFdbPort віддає НЕ ifIndex, а номер мосту (bridge port).
|
||
// Без dot1dBasePortIfIndex ці числа ні з чим не зіставити.
|
||
bridgeToIf := make(map[uint64]int64)
|
||
_ = snmpx.Walk(ctx, c, oidDot1dBasePortIfIndex, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oidDot1dBasePortIfIndex, pdu.Name)
|
||
if !ok || len(idx) != 1 {
|
||
return nil
|
||
}
|
||
if v, ok := snmpx.AsUint(pdu); ok {
|
||
bridgeToIf[idx[0]] = int64(v)
|
||
}
|
||
return nil
|
||
})
|
||
|
||
var out []*npv1.NeighborRecord
|
||
err := snmpx.Walk(ctx, c, oidDot1dTpFdbPort, func(pdu gosnmp.SnmpPDU) error {
|
||
idx, ok := snmpx.SplitIndex(oidDot1dTpFdbPort, pdu.Name)
|
||
if !ok || len(idx) < 6 {
|
||
return nil
|
||
}
|
||
mac, ok := idx.MACAt(len(idx) - 6)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
port, ok := snmpx.AsUint(pdu)
|
||
if !ok || port == 0 {
|
||
return nil
|
||
}
|
||
|
||
r := &npv1.NeighborRecord{
|
||
DeviceId: deviceID,
|
||
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_FDB,
|
||
RemoteMac: mac,
|
||
SeenAt: timestamppb.Now(),
|
||
}
|
||
if ifIdx, exists := bridgeToIf[port]; exists {
|
||
r.LocalIfIndex = ifIdx
|
||
if ifc, ok := byIndex[ifIdx]; ok {
|
||
r.LocalPortName = ifc.Name
|
||
}
|
||
}
|
||
out = append(out, r)
|
||
return nil
|
||
})
|
||
|
||
return out, err
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
|
||
func adminStatusName(v uint64) string {
|
||
switch v {
|
||
case 1:
|
||
return "up"
|
||
case 2:
|
||
return "down"
|
||
case 3:
|
||
return "testing"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
func operStatusName(v uint64) string {
|
||
switch v {
|
||
case 1:
|
||
return "up"
|
||
case 2:
|
||
return "down"
|
||
case 3:
|
||
return "testing"
|
||
case 5:
|
||
return "dormant"
|
||
case 6:
|
||
return "notPresent"
|
||
case 7:
|
||
return "lowerLayerDown"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
func ifTypeName(v uint64) string {
|
||
switch v {
|
||
case 6:
|
||
return "ethernetCsmacd"
|
||
case 24:
|
||
return "softwareLoopback"
|
||
case 53:
|
||
return "propVirtual"
|
||
case 131:
|
||
return "tunnel"
|
||
case 135:
|
||
return "l2vlan"
|
||
case 136:
|
||
return "l3ipvlan"
|
||
case 161:
|
||
return "ieee8023adLag"
|
||
default:
|
||
return fmt.Sprintf("type%d", v)
|
||
}
|
||
}
|
||
|
||
func truncate(s string, n int) string {
|
||
if len(s) <= n {
|
||
return s
|
||
}
|
||
return s[:n]
|
||
}
|