Files
tendrils/snmp.go
2026-01-31 13:01:07 -08:00

678 lines
16 KiB
Go

package tendrils
import (
"fmt"
"log"
"net"
"regexp"
"strings"
"sync"
"time"
"github.com/gosnmp/gosnmp"
)
type ifaceCounters struct {
inPkts uint64
outPkts uint64
inBytes uint64
outBytes uint64
uptime uint64
timestamp time.Time
}
type counterTracker struct {
mu sync.Mutex
counters map[string]*ifaceCounters
}
var ifaceTracker = &counterTracker{
counters: map[string]*ifaceCounters{},
}
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
}
type snmpConfig struct {
username string
authKey string
privKey string
authProto gosnmp.SnmpV3AuthProtocol
privProto gosnmp.SnmpV3PrivProtocol
secLevel gosnmp.SnmpV3MsgFlags
timeout time.Duration
retries int
}
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,
}
}
func snmpToInt(val interface{}) (int, bool) {
switch v := val.(type) {
case int:
return v, true
case int32:
return int(v), true
case int64:
return int(v), true
case uint:
return int(v), true
case uint32:
return int(v), true
case uint64:
return int(v), true
default:
return 0, false
}
}
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
}
func (t *Tendrils) querySNMPDevice(node *Node, ip net.IP) {
snmp, err := t.connectSNMP(ip)
if err != nil {
if t.DebugSNMP {
log.Printf("[snmp] %s: connect failed: %v", ip, err)
}
return
}
defer snmp.Conn.Close()
ifNames := t.getInterfaceNames(snmp)
t.querySysName(snmp, node)
t.queryInterfaceMACs(snmp, node, ifNames)
sysUpTime := t.getSysUpTime(snmp)
t.queryInterfaceStats(snmp, node, ifNames, sysUpTime)
t.queryPoEBudget(snmp, node)
t.queryBridgeMIB(snmp, node, ifNames)
t.queryDHCPBindings(snmp)
}
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)
}
func (t *Tendrils) querySysName(snmp *gosnmp.GoSNMP, node *Node) {
oid := "1.3.6.1.2.1.1.5.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.OctetString {
return
}
sysName := string(variable.Value.([]byte))
if sysName == "" {
return
}
t.nodes.Update(node, nil, nil, "", sysName, "snmp-sysname")
}
func (t *Tendrils) queryInterfaceMACs(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string) {
oid := "1.3.6.1.2.1.2.2.1.6"
results, err := snmp.BulkWalkAll(oid)
if err != nil {
return
}
for _, result := range results {
if result.Type != gosnmp.OctetString {
continue
}
macBytes := result.Value.([]byte)
if len(macBytes) != 6 {
continue
}
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 {
log.Printf("[snmp] %s: interface %d mac=%s name=%s", snmp.Target, ifIndex, mac, name)
}
t.nodes.Update(node, mac, nil, name, "", "snmp-ifmac")
}
}
func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string, sysUpTime uint64) {
ifOperStatus := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.8")
ifLastChange := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.9")
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")
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")
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")
ifHCOutUcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.11")
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")
poeStats := t.getPoEStats(snmp, ifNames)
now := time.Now()
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 {
if iface.Up {
log.Printf("[ERROR] port down on %s %s", node.DisplayName(), name)
t.errors.AddPortDown(node, name)
}
iface.Up = false
iface.Stats = nil
continue
}
iface.Up = true
stats := &InterfaceStats{}
if speed, ok := ifHighSpeed[ifIndex]; ok {
stats.Speed = uint64(speed) * 1000000
}
if lastChange, ok := ifLastChange[ifIndex]; ok && sysUpTime > 0 {
if uint64(lastChange) <= sysUpTime {
stats.Uptime = (sysUpTime - uint64(lastChange)) / 100
}
}
if inErr, ok := ifInErrors[ifIndex]; ok {
stats.InErrors = uint64(inErr)
}
if outErr, ok := ifOutErrors[ifIndex]; ok {
stats.OutErrors = uint64(outErr)
}
inBytes, hasInBytes := ifHCInOctets[ifIndex]
outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
inPkts := ifHCInUcastPkts[ifIndex] + ifHCInMcastPkts[ifIndex] + ifHCInBcastPkts[ifIndex]
outPkts := ifHCOutUcastPkts[ifIndex] + ifHCOutMcastPkts[ifIndex] + ifHCOutBcastPkts[ifIndex]
key := node.ID + ":" + name
ifaceTracker.mu.Lock()
prev, hasPrev := ifaceTracker.counters[key]
if hasPrev {
if prev.uptime > 0 && stats.Uptime > 0 && stats.Uptime < prev.uptime {
log.Printf("[ERROR] port flap on %s %s: uptime dropped from %d to %d seconds", node.DisplayName(), name, prev.uptime, stats.Uptime)
t.errors.AddPortFlap(node, name)
}
if hasInBytes && hasOutBytes {
elapsed := now.Sub(prev.timestamp).Seconds()
if elapsed > 0 {
stats.InPktsRate = float64(inPkts-prev.inPkts) / elapsed
stats.OutPktsRate = float64(outPkts-prev.outPkts) / elapsed
stats.InBytesRate = float64(inBytes-prev.inBytes) / elapsed
stats.OutBytesRate = float64(outBytes-prev.outBytes) / elapsed
if stats.InPktsRate < 0 {
stats.InPktsRate = 0
}
if stats.OutPktsRate < 0 {
stats.OutPktsRate = 0
}
if stats.InBytesRate < 0 {
stats.InBytesRate = 0
}
if stats.OutBytesRate < 0 {
stats.OutBytesRate = 0
}
}
}
}
storedUptime := stats.Uptime
if storedUptime == 0 && hasPrev {
storedUptime = prev.uptime
}
ifaceTracker.counters[key] = &ifaceCounters{
inPkts: inPkts,
outPkts: outPkts,
inBytes: inBytes,
outBytes: outBytes,
uptime: storedUptime,
timestamp: now,
}
ifaceTracker.mu.Unlock()
if poe, ok := poeStats[name]; ok {
stats.PoE = poe
}
node.SetInterfaceStats(name, stats)
}
}
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 {
val, ok := snmpToInt(v.Value)
if !ok {
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}
t.nodes.mu.Unlock()
}
}
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
}
value, ok := snmpToInt(result.Value)
if !ok {
continue
}
table[ifIndex] = value
}
return table
}
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
}
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
}
status, ok := snmpToInt(result.Value)
if !ok {
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
}
value, ok := snmpToInt(result.Value)
if !ok {
continue
}
table[portIndex] = value
}
return table
}
func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, node *Node, ifNames map[int]string) {
portOID := "1.3.6.1.2.1.17.7.1.2.2.1.2"
portResults, err := snmp.BulkWalkAll(portOID)
if err != nil {
return
}
type macPortEntry struct {
mac net.HardwareAddr
bridgePort int
}
var macPorts []macPortEntry
for _, result := range portResults {
if result.Type == gosnmp.Integer {
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})
}
}
}
}
bridgePortToIfIndex := t.getBridgePortMapping(snmp)
for _, entry := range macPorts {
mac := entry.mac
if isBroadcastOrZero(mac) {
continue
}
ifIndex, exists := bridgePortToIfIndex[entry.bridgePort]
if !exists {
ifIndex = entry.bridgePort
}
ifName := rewritePortName(ifNames[ifIndex])
if t.DebugSNMP {
log.Printf("[snmp] %s: mac=%s port=%s", snmp.Target, mac, ifName)
}
t.nodes.Update(nil, mac, nil, "", "", "snmp")
t.nodes.UpdateMACTable(node, mac, ifName)
}
}
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 {
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
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 {
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
var ifIndex int
_, err := fmt.Sscanf(oidParts, "%d", &ifIndex)
if err != nil {
continue
}
name := string(result.Value.([]byte))
names[ifIndex] = name
}
}
return names
}
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")
}
}