2025-11-29 21:16:58 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-01-17 21:12:21 -08:00
|
|
|
"log"
|
2025-11-29 21:16:58 -08:00
|
|
|
"net"
|
2026-01-18 14:00:51 -08:00
|
|
|
"regexp"
|
2025-11-29 21:56:45 -08:00
|
|
|
"strings"
|
2026-02-02 20:33:42 -08:00
|
|
|
"sync"
|
2025-11-29 21:16:58 -08:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gosnmp/gosnmp"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-18 14:00:51 -08:00
|
|
|
var portNameRewrites = []struct {
|
|
|
|
|
re *regexp.Regexp
|
|
|
|
|
repl string
|
|
|
|
|
}{
|
|
|
|
|
{regexp.MustCompile(`.*Slot: (\d+) Port: (\d+).*`), "$1/$2"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func rewritePortName(name string) string {
|
|
|
|
|
for _, rw := range portNameRewrites {
|
|
|
|
|
if rw.re.MatchString(name) {
|
|
|
|
|
return rw.re.ReplaceAllString(name, rw.repl)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 21:16:58 -08:00
|
|
|
type snmpConfig struct {
|
2025-11-29 21:56:45 -08:00
|
|
|
username string
|
|
|
|
|
authKey string
|
|
|
|
|
privKey string
|
|
|
|
|
authProto gosnmp.SnmpV3AuthProtocol
|
|
|
|
|
privProto gosnmp.SnmpV3PrivProtocol
|
|
|
|
|
secLevel gosnmp.SnmpV3MsgFlags
|
|
|
|
|
timeout time.Duration
|
|
|
|
|
retries int
|
2025-11-29 21:16:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func defaultSNMPConfig() *snmpConfig {
|
|
|
|
|
return &snmpConfig{
|
|
|
|
|
username: "tendrils",
|
|
|
|
|
authKey: "tendrils",
|
|
|
|
|
privKey: "tendrils",
|
|
|
|
|
authProto: gosnmp.SHA512,
|
|
|
|
|
privProto: gosnmp.AES,
|
|
|
|
|
secLevel: gosnmp.AuthPriv,
|
|
|
|
|
timeout: 5 * time.Second,
|
|
|
|
|
retries: 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
func snmpToInt(val interface{}) (int, bool) {
|
|
|
|
|
switch v := val.(type) {
|
|
|
|
|
case int:
|
|
|
|
|
return v, true
|
2026-01-31 12:06:59 -08:00
|
|
|
case int32:
|
2026-01-23 23:28:58 -08:00
|
|
|
return int(v), true
|
|
|
|
|
case int64:
|
|
|
|
|
return int(v), true
|
2026-01-31 12:06:59 -08:00
|
|
|
case uint:
|
|
|
|
|
return int(v), true
|
|
|
|
|
case uint32:
|
|
|
|
|
return int(v), true
|
2026-01-23 23:28:58 -08:00
|
|
|
case uint64:
|
|
|
|
|
return int(v), true
|
|
|
|
|
default:
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 21:16:58 -08:00
|
|
|
func (t *Tendrils) connectSNMP(ip net.IP) (*gosnmp.GoSNMP, error) {
|
|
|
|
|
cfg := defaultSNMPConfig()
|
|
|
|
|
|
|
|
|
|
snmp := &gosnmp.GoSNMP{
|
|
|
|
|
Target: ip.String(),
|
|
|
|
|
Port: 161,
|
|
|
|
|
Version: gosnmp.Version3,
|
|
|
|
|
Timeout: cfg.timeout,
|
|
|
|
|
Retries: cfg.retries,
|
|
|
|
|
SecurityModel: gosnmp.UserSecurityModel,
|
|
|
|
|
MsgFlags: cfg.secLevel,
|
|
|
|
|
SecurityParameters: &gosnmp.UsmSecurityParameters{
|
|
|
|
|
UserName: cfg.username,
|
|
|
|
|
AuthenticationProtocol: cfg.authProto,
|
|
|
|
|
AuthenticationPassphrase: cfg.authKey,
|
|
|
|
|
PrivacyProtocol: cfg.privProto,
|
|
|
|
|
PrivacyPassphrase: cfg.privKey,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := snmp.Connect()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return snmp, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 20:33:42 -08:00
|
|
|
func (t *Tendrils) pollSNMP(node *Node, ip net.IP) {
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(2)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
t.queryWirelessAP(node, ip)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
t.querySNMPDevice(node, ip)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
func (t *Tendrils) querySNMPDevice(node *Node, ip net.IP) {
|
2025-11-29 21:16:58 -08:00
|
|
|
snmp, err := t.connectSNMP(ip)
|
|
|
|
|
if err != nil {
|
2026-01-17 21:12:21 -08:00
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: connect failed: %v", ip, err)
|
|
|
|
|
}
|
2025-11-29 21:16:58 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer snmp.Conn.Close()
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
ifNames := t.getInterfaceNames(snmp)
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
t.querySysName(snmp, node)
|
2026-01-23 23:28:58 -08:00
|
|
|
t.queryInterfaceMACs(snmp, node, ifNames)
|
2026-01-31 12:06:59 -08:00
|
|
|
sysUpTime := t.getSysUpTime(snmp)
|
|
|
|
|
t.queryInterfaceStats(snmp, node, ifNames, sysUpTime)
|
2026-01-22 22:56:47 -08:00
|
|
|
t.queryPoEBudget(snmp, node)
|
2026-01-23 23:28:58 -08:00
|
|
|
t.queryBridgeMIB(snmp, node, ifNames)
|
2026-01-24 23:11:55 -08:00
|
|
|
t.queryDHCPBindings(snmp)
|
2025-11-29 21:16:58 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 12:06:59 -08:00
|
|
|
func (t *Tendrils) getSysUpTime(snmp *gosnmp.GoSNMP) uint64 {
|
|
|
|
|
oid := "1.3.6.1.2.1.1.3.0"
|
|
|
|
|
|
|
|
|
|
result, err := snmp.Get([]string{oid})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(result.Variables) == 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v, ok := snmpToInt(result.Variables[0].Value)
|
|
|
|
|
if !ok {
|
|
|
|
|
log.Printf("[ERROR] failed to parse sysUpTime: type=%T value=%v", result.Variables[0].Value, result.Variables[0].Value)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return uint64(v)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
func (t *Tendrils) querySysName(snmp *gosnmp.GoSNMP, node *Node) {
|
2025-11-29 22:27:31 -08:00
|
|
|
oid := "1.3.6.1.2.1.1.5.0"
|
|
|
|
|
|
|
|
|
|
result, err := snmp.Get([]string{oid})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
if len(result.Variables) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
variable := result.Variables[0]
|
|
|
|
|
if variable.Type != gosnmp.OctetString {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sysName := string(variable.Value.([]byte))
|
|
|
|
|
if sysName == "" {
|
|
|
|
|
return
|
2025-11-29 22:27:31 -08:00
|
|
|
}
|
2026-01-18 14:44:15 -08:00
|
|
|
|
|
|
|
|
t.nodes.Update(node, nil, nil, "", sysName, "snmp-sysname")
|
2025-11-29 22:27:31 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
func (t *Tendrils) queryInterfaceMACs(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string) {
|
2026-01-17 21:21:57 -08:00
|
|
|
oid := "1.3.6.1.2.1.2.2.1.6"
|
|
|
|
|
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, result := range results {
|
2026-01-18 14:00:51 -08:00
|
|
|
if result.Type != gosnmp.OctetString {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macBytes := result.Value.([]byte)
|
|
|
|
|
if len(macBytes) != 6 {
|
|
|
|
|
continue
|
2026-01-17 21:21:57 -08:00
|
|
|
}
|
2026-01-18 14:00:51 -08:00
|
|
|
|
|
|
|
|
mac := net.HardwareAddr(macBytes)
|
|
|
|
|
if isBroadcastOrZero(mac) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
|
|
|
|
var ifIndex int
|
|
|
|
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &ifIndex); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := rewritePortName(ifNames[ifIndex])
|
|
|
|
|
if t.DebugSNMP {
|
2026-01-18 14:44:15 -08:00
|
|
|
log.Printf("[snmp] %s: interface %d mac=%s name=%s", snmp.Target, ifIndex, mac, name)
|
2026-01-18 14:23:21 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
t.nodes.Update(node, mac, nil, name, "", "snmp-ifmac")
|
2026-01-17 21:21:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 12:06:59 -08:00
|
|
|
func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string, sysUpTime uint64) {
|
2026-01-22 22:47:30 -08:00
|
|
|
ifOperStatus := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.8")
|
2026-01-31 12:06:59 -08:00
|
|
|
ifLastChange := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.9")
|
2026-01-22 22:47:30 -08:00
|
|
|
ifHighSpeed := t.getInterfaceTable(snmp, "1.3.6.1.2.1.31.1.1.1.15")
|
|
|
|
|
ifInErrors := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.14")
|
|
|
|
|
ifOutErrors := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.20")
|
|
|
|
|
|
2026-01-26 11:16:32 -08:00
|
|
|
ifHCInOctets := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.6")
|
|
|
|
|
ifHCOutOctets := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.10")
|
|
|
|
|
ifHCInUcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.7")
|
2026-01-31 07:35:49 -08:00
|
|
|
ifHCInMcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.8")
|
|
|
|
|
ifHCInBcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.9")
|
2026-01-26 11:16:32 -08:00
|
|
|
ifHCOutUcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.11")
|
2026-01-31 07:35:49 -08:00
|
|
|
ifHCOutMcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.12")
|
|
|
|
|
ifHCOutBcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.13")
|
2026-01-26 11:16:32 -08:00
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
poeStats := t.getPoEStats(snmp, ifNames)
|
2026-01-26 11:16:32 -08:00
|
|
|
now := time.Now()
|
2026-01-22 22:47:30 -08:00
|
|
|
|
|
|
|
|
t.nodes.mu.Lock()
|
|
|
|
|
defer t.nodes.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
for ifIndex, name := range ifNames {
|
|
|
|
|
name = rewritePortName(name)
|
|
|
|
|
iface, exists := node.Interfaces[name]
|
|
|
|
|
if !exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
status, hasStatus := ifOperStatus[ifIndex]
|
|
|
|
|
isUp := hasStatus && status == 1
|
|
|
|
|
if !isUp {
|
2026-01-31 13:01:07 -08:00
|
|
|
if iface.Up {
|
|
|
|
|
log.Printf("[ERROR] port down on %s %s", node.DisplayName(), name)
|
|
|
|
|
t.errors.AddPortDown(node, name)
|
|
|
|
|
}
|
|
|
|
|
iface.Up = false
|
2026-01-22 22:47:30 -08:00
|
|
|
iface.Stats = nil
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-01-31 13:01:07 -08:00
|
|
|
iface.Up = true
|
2026-01-22 22:47:30 -08:00
|
|
|
|
|
|
|
|
stats := &InterfaceStats{}
|
|
|
|
|
|
|
|
|
|
if speed, ok := ifHighSpeed[ifIndex]; ok {
|
|
|
|
|
stats.Speed = uint64(speed) * 1000000
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 12:06:59 -08:00
|
|
|
if lastChange, ok := ifLastChange[ifIndex]; ok && sysUpTime > 0 {
|
|
|
|
|
if uint64(lastChange) <= sysUpTime {
|
|
|
|
|
stats.Uptime = (sysUpTime - uint64(lastChange)) / 100
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
if inErr, ok := ifInErrors[ifIndex]; ok {
|
|
|
|
|
stats.InErrors = uint64(inErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if outErr, ok := ifOutErrors[ifIndex]; ok {
|
|
|
|
|
stats.OutErrors = uint64(outErr)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 11:16:32 -08:00
|
|
|
inBytes, hasInBytes := ifHCInOctets[ifIndex]
|
|
|
|
|
outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
|
|
|
|
|
|
2026-01-31 07:35:49 -08:00
|
|
|
inPkts := ifHCInUcastPkts[ifIndex] + ifHCInMcastPkts[ifIndex] + ifHCInBcastPkts[ifIndex]
|
|
|
|
|
outPkts := ifHCOutUcastPkts[ifIndex] + ifHCOutMcastPkts[ifIndex] + ifHCOutBcastPkts[ifIndex]
|
|
|
|
|
|
2026-02-02 10:16:37 -08:00
|
|
|
hasPrev := !iface.prevTimestamp.IsZero()
|
2026-01-31 13:01:07 -08:00
|
|
|
if hasPrev {
|
2026-02-02 10:16:37 -08:00
|
|
|
if iface.prevUptime > 0 && stats.Uptime > 0 && stats.Uptime < iface.prevUptime {
|
|
|
|
|
log.Printf("[ERROR] port flap on %s %s: uptime dropped from %d to %d seconds", node.DisplayName(), name, iface.prevUptime, stats.Uptime)
|
2026-01-31 13:01:07 -08:00
|
|
|
t.errors.AddPortFlap(node, name)
|
|
|
|
|
}
|
|
|
|
|
if hasInBytes && hasOutBytes {
|
2026-02-02 10:16:37 -08:00
|
|
|
elapsed := now.Sub(iface.prevTimestamp).Seconds()
|
2026-01-26 11:16:32 -08:00
|
|
|
if elapsed > 0 {
|
2026-02-02 10:16:37 -08:00
|
|
|
stats.InPktsRate = float64(inPkts-iface.prevInPkts) / elapsed
|
|
|
|
|
stats.OutPktsRate = float64(outPkts-iface.prevOutPkts) / elapsed
|
|
|
|
|
stats.InBytesRate = float64(inBytes-iface.prevInBytes) / elapsed
|
|
|
|
|
stats.OutBytesRate = float64(outBytes-iface.prevOutBytes) / elapsed
|
2026-02-01 17:05:06 -08:00
|
|
|
|
|
|
|
|
maxBytesRate := float64(stats.Speed) / 8 * 2
|
|
|
|
|
if stats.InBytesRate < 0 || stats.InBytesRate > maxBytesRate {
|
2026-01-26 11:16:32 -08:00
|
|
|
stats.InPktsRate = 0
|
|
|
|
|
stats.InBytesRate = 0
|
|
|
|
|
}
|
2026-02-01 17:05:06 -08:00
|
|
|
if stats.OutBytesRate < 0 || stats.OutBytesRate > maxBytesRate {
|
|
|
|
|
stats.OutPktsRate = 0
|
2026-01-26 11:16:32 -08:00
|
|
|
stats.OutBytesRate = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 10:16:37 -08:00
|
|
|
|
|
|
|
|
iface.prevInPkts = inPkts
|
|
|
|
|
iface.prevOutPkts = outPkts
|
|
|
|
|
iface.prevInBytes = inBytes
|
|
|
|
|
iface.prevOutBytes = outBytes
|
|
|
|
|
if stats.Uptime > 0 {
|
|
|
|
|
iface.prevUptime = stats.Uptime
|
2026-01-31 13:01:07 -08:00
|
|
|
}
|
2026-02-02 10:16:37 -08:00
|
|
|
iface.prevTimestamp = now
|
2026-01-26 11:16:32 -08:00
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
if poe, ok := poeStats[name]; ok {
|
|
|
|
|
stats.PoE = poe
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:06:26 -08:00
|
|
|
node.SetInterfaceStats(name, stats)
|
2026-01-22 22:47:30 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:56:47 -08:00
|
|
|
func (t *Tendrils) queryPoEBudget(snmp *gosnmp.GoSNMP, node *Node) {
|
|
|
|
|
maxPowerOID := "1.3.6.1.2.1.105.1.3.1.1.2.1"
|
|
|
|
|
powerOID := "1.3.6.1.2.1.105.1.3.1.1.4.1"
|
|
|
|
|
|
|
|
|
|
result, err := snmp.Get([]string{maxPowerOID, powerOID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var power, maxPower float64
|
|
|
|
|
for _, v := range result.Variables {
|
2026-01-23 23:28:58 -08:00
|
|
|
val, ok := snmpToInt(v.Value)
|
|
|
|
|
if !ok {
|
2026-01-22 22:56:47 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if v.Name == "."+powerOID {
|
|
|
|
|
power = float64(val)
|
|
|
|
|
} else if v.Name == "."+maxPowerOID {
|
|
|
|
|
maxPower = float64(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if maxPower > 0 {
|
|
|
|
|
t.nodes.mu.Lock()
|
|
|
|
|
node.PoEBudget = &PoEBudget{Power: power, MaxPower: maxPower}
|
2026-02-02 20:33:42 -08:00
|
|
|
node.Type = NodeTypeSwitch
|
2026-01-22 22:56:47 -08:00
|
|
|
t.nodes.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
func (t *Tendrils) getInterfaceTable(snmp *gosnmp.GoSNMP, oid string) map[int]int {
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table := map[int]int{}
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
|
|
|
|
var ifIndex int
|
|
|
|
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &ifIndex); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
value, ok := snmpToInt(result.Value)
|
|
|
|
|
if !ok {
|
2026-01-22 22:47:30 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
table[ifIndex] = value
|
|
|
|
|
}
|
|
|
|
|
return table
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 11:16:32 -08:00
|
|
|
func (t *Tendrils) getInterfaceTable64(snmp *gosnmp.GoSNMP, oid string) map[int]uint64 {
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table := map[int]uint64{}
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
|
|
|
|
var ifIndex int
|
|
|
|
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &ifIndex); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch v := result.Value.(type) {
|
|
|
|
|
case uint64:
|
|
|
|
|
table[ifIndex] = v
|
|
|
|
|
case uint:
|
|
|
|
|
table[ifIndex] = uint64(v)
|
|
|
|
|
case int:
|
|
|
|
|
table[ifIndex] = uint64(v)
|
|
|
|
|
case int64:
|
|
|
|
|
table[ifIndex] = uint64(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return table
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
func (t *Tendrils) getPoEStats(snmp *gosnmp.GoSNMP, ifNames map[int]string) map[string]*PoEStats {
|
|
|
|
|
statusOID := "1.3.6.1.2.1.105.1.1.1.6"
|
|
|
|
|
|
|
|
|
|
statusResults, err := snmp.BulkWalkAll(statusOID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
powerTable := t.getPoETable(snmp, "1.3.6.1.4.1.4526.10.15.1.1.1.2")
|
|
|
|
|
maxPowerTable := t.getPoETable(snmp, "1.3.6.1.4.1.4526.10.15.1.1.1.1")
|
|
|
|
|
|
|
|
|
|
stats := map[string]*PoEStats{}
|
|
|
|
|
for _, result := range statusResults {
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+statusOID), ".")
|
|
|
|
|
parts := strings.Split(oidSuffix, ".")
|
|
|
|
|
if len(parts) < 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var portIndex int
|
|
|
|
|
if _, err := fmt.Sscanf(parts[1], "%d", &portIndex); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
status, ok := snmpToInt(result.Value)
|
|
|
|
|
if !ok {
|
2026-01-22 22:47:30 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ifName := rewritePortName(ifNames[portIndex])
|
|
|
|
|
if ifName == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if status != 3 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
poe := &PoEStats{}
|
|
|
|
|
if power, ok := powerTable[portIndex]; ok {
|
|
|
|
|
poe.Power = float64(power) / 1000.0
|
|
|
|
|
}
|
|
|
|
|
if maxPower, ok := maxPowerTable[portIndex]; ok {
|
|
|
|
|
poe.MaxPower = float64(maxPower) / 1000.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: poe port %d (%s) power=%v maxpower=%v", snmp.Target, portIndex, ifName, powerTable[portIndex], maxPowerTable[portIndex])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if poe.Power > 0 || poe.MaxPower > 0 {
|
|
|
|
|
stats[ifName] = poe
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return stats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) getPoETable(snmp *gosnmp.GoSNMP, oid string) map[int]int {
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table := map[int]int{}
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
|
|
|
|
parts := strings.Split(oidSuffix, ".")
|
|
|
|
|
if len(parts) < 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var portIndex int
|
|
|
|
|
if _, err := fmt.Sscanf(parts[1], "%d", &portIndex); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
value, ok := snmpToInt(result.Value)
|
|
|
|
|
if !ok {
|
2026-01-22 22:47:30 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
table[portIndex] = value
|
|
|
|
|
}
|
|
|
|
|
return table
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:28:58 -08:00
|
|
|
func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string) {
|
2025-11-29 22:19:05 -08:00
|
|
|
portOID := "1.3.6.1.2.1.17.7.1.2.2.1.2"
|
2025-11-29 21:56:45 -08:00
|
|
|
|
2025-11-29 21:16:58 -08:00
|
|
|
portResults, err := snmp.BulkWalkAll(portOID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 21:56:45 -08:00
|
|
|
type macPortEntry struct {
|
|
|
|
|
mac net.HardwareAddr
|
|
|
|
|
bridgePort int
|
|
|
|
|
}
|
|
|
|
|
var macPorts []macPortEntry
|
|
|
|
|
|
2025-11-29 21:16:58 -08:00
|
|
|
for _, result := range portResults {
|
|
|
|
|
if result.Type == gosnmp.Integer {
|
2025-11-29 21:56:45 -08:00
|
|
|
oidSuffix := strings.TrimPrefix(result.Name[len(portOID):], ".")
|
|
|
|
|
parts := strings.Split(oidSuffix, ".")
|
|
|
|
|
|
|
|
|
|
if len(parts) >= 8 {
|
|
|
|
|
var macBytes []byte
|
|
|
|
|
for j := 2; j <= 7; j++ {
|
|
|
|
|
var b int
|
|
|
|
|
fmt.Sscanf(parts[j], "%d", &b)
|
|
|
|
|
macBytes = append(macBytes, byte(b))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(macBytes) == 6 {
|
|
|
|
|
mac := net.HardwareAddr(macBytes)
|
|
|
|
|
bridgePort := result.Value.(int)
|
|
|
|
|
macPorts = append(macPorts, macPortEntry{mac: mac, bridgePort: bridgePort})
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-29 21:16:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bridgePortToIfIndex := t.getBridgePortMapping(snmp)
|
|
|
|
|
|
2026-01-31 13:20:20 -08:00
|
|
|
t.nodes.ClearMACTable(node)
|
|
|
|
|
|
2025-11-29 21:56:45 -08:00
|
|
|
for _, entry := range macPorts {
|
|
|
|
|
mac := entry.mac
|
2025-11-29 21:16:58 -08:00
|
|
|
|
2025-11-29 21:56:45 -08:00
|
|
|
if isBroadcastOrZero(mac) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-29 21:16:58 -08:00
|
|
|
|
2026-01-18 14:54:06 -08:00
|
|
|
ifIndex, exists := bridgePortToIfIndex[entry.bridgePort]
|
|
|
|
|
if !exists {
|
|
|
|
|
ifIndex = entry.bridgePort
|
|
|
|
|
}
|
|
|
|
|
ifName := rewritePortName(ifNames[ifIndex])
|
|
|
|
|
|
2026-01-18 08:15:27 -08:00
|
|
|
if t.DebugSNMP {
|
2026-01-18 14:54:06 -08:00
|
|
|
log.Printf("[snmp] %s: mac=%s port=%s", snmp.Target, mac, ifName)
|
2026-01-17 21:12:21 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
t.nodes.Update(nil, mac, nil, "", "", "snmp")
|
2026-01-18 14:54:06 -08:00
|
|
|
t.nodes.UpdateMACTable(node, mac, ifName)
|
2025-11-29 21:16:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) getBridgePortMapping(snmp *gosnmp.GoSNMP) map[int]int {
|
|
|
|
|
oid := "1.3.6.1.2.1.17.1.4.1.2"
|
|
|
|
|
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mapping := make(map[int]int)
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
if result.Type == gosnmp.Integer {
|
2025-11-29 21:56:45 -08:00
|
|
|
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
2025-11-29 21:16:58 -08:00
|
|
|
var bridgePort int
|
|
|
|
|
_, err := fmt.Sscanf(oidParts, "%d", &bridgePort)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ifIndex := result.Value.(int)
|
|
|
|
|
mapping[bridgePort] = ifIndex
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mapping
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) getInterfaceNames(snmp *gosnmp.GoSNMP) map[int]string {
|
|
|
|
|
oid := "1.3.6.1.2.1.2.2.1.2"
|
|
|
|
|
|
|
|
|
|
results, err := snmp.BulkWalkAll(oid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
names := make(map[int]string)
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
if result.Type == gosnmp.OctetString {
|
2025-11-29 21:56:45 -08:00
|
|
|
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
2025-11-29 21:16:58 -08:00
|
|
|
var ifIndex int
|
|
|
|
|
_, err := fmt.Sscanf(oidParts, "%d", &ifIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name := string(result.Value.([]byte))
|
|
|
|
|
names[ifIndex] = name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return names
|
|
|
|
|
}
|
2026-01-24 23:11:55 -08:00
|
|
|
|
|
|
|
|
func (t *Tendrils) queryDHCPBindings(snmp *gosnmp.GoSNMP) {
|
|
|
|
|
baseOID := "1.3.6.1.4.1.4526.10.12.3.2.1"
|
|
|
|
|
ipOID := baseOID + ".1"
|
|
|
|
|
macOID := baseOID + ".3"
|
|
|
|
|
|
|
|
|
|
ipResults, err := snmp.BulkWalkAll(ipOID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macResults, err := snmp.BulkWalkAll(macOID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ips := map[int]net.IP{}
|
|
|
|
|
for _, result := range ipResults {
|
|
|
|
|
if result.Type != gosnmp.IPAddress {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+ipOID), ".")
|
|
|
|
|
var idx int
|
|
|
|
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &idx); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ips[idx] = net.ParseIP(result.Value.(string))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macs := map[int]net.HardwareAddr{}
|
|
|
|
|
for _, result := range macResults {
|
|
|
|
|
if result.Type != gosnmp.OctetString {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
macBytes := result.Value.([]byte)
|
|
|
|
|
if len(macBytes) != 6 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+macOID), ".")
|
|
|
|
|
var idx int
|
|
|
|
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &idx); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
macs[idx] = net.HardwareAddr(macBytes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for idx, ip := range ips {
|
|
|
|
|
mac, ok := macs[idx]
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if isBroadcastOrZero(mac) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: dhcp binding mac=%s ip=%s", snmp.Target, mac, ip)
|
|
|
|
|
}
|
|
|
|
|
t.nodes.Update(nil, mac, []net.IP{ip}, "", "", "dhcp")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 20:33:42 -08:00
|
|
|
|
|
|
|
|
func (t *Tendrils) connectSNMPv2c(ip net.IP, community string) (*gosnmp.GoSNMP, error) {
|
|
|
|
|
snmp := &gosnmp.GoSNMP{
|
|
|
|
|
Target: ip.String(),
|
|
|
|
|
Port: 161,
|
|
|
|
|
Version: gosnmp.Version2c,
|
|
|
|
|
Community: community,
|
|
|
|
|
Timeout: 5 * time.Second,
|
|
|
|
|
Retries: 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := snmp.Connect()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return snmp, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) queryWirelessAP(node *Node, ip net.IP) bool {
|
|
|
|
|
snmp, err := t.connectSNMPv2c(ip, "tendrils12!")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
defer snmp.Conn.Close()
|
|
|
|
|
|
|
|
|
|
sysObjID := t.getSysObjectID(snmp)
|
|
|
|
|
if !strings.HasPrefix(sysObjID, "1.3.6.1.4.1.11863.") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.nodes.mu.Lock()
|
|
|
|
|
node.Type = NodeTypeAP
|
|
|
|
|
t.nodes.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
ifNames := t.getInterfaceNames(snmp)
|
|
|
|
|
t.querySysName(snmp, node)
|
|
|
|
|
t.queryInterfaceMACs(snmp, node, ifNames)
|
|
|
|
|
t.queryTPLinkWirelessClients(snmp, node)
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) getSysObjectID(snmp *gosnmp.GoSNMP) string {
|
|
|
|
|
oid := "1.3.6.1.2.1.1.2.0"
|
|
|
|
|
|
|
|
|
|
result, err := snmp.Get([]string{oid})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(result.Variables) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
variable := result.Variables[0]
|
|
|
|
|
if variable.Type != gosnmp.ObjectIdentifier {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return strings.TrimPrefix(variable.Value.(string), ".")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) queryTPLinkWirelessClients(snmp *gosnmp.GoSNMP, node *Node) {
|
|
|
|
|
clientMACOID := "1.3.6.1.4.1.11863.10.1.1.2.1.2"
|
|
|
|
|
|
|
|
|
|
results, err := snmp.BulkWalkAll(clientMACOID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: tp-link client walk failed: %v", snmp.Target, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.nodes.ClearMACTable(node)
|
|
|
|
|
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
if result.Type != gosnmp.OctetString {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macText := string(result.Value.([]byte))
|
|
|
|
|
macText = strings.TrimRight(macText, "\x00")
|
|
|
|
|
macText = strings.ReplaceAll(macText, "-", ":")
|
|
|
|
|
mac, err := net.ParseMAC(macText)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: invalid wireless client mac %q: %v", snmp.Target, macText, err)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.nodes.UpdateMACTable(node, mac, "wifi")
|
|
|
|
|
|
|
|
|
|
if t.DebugSNMP {
|
|
|
|
|
log.Printf("[snmp] %s: wireless client mac=%s", snmp.Target, mac.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.NotifyUpdate()
|
|
|
|
|
}
|