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 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 uint: return int(v), true case int64: 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) t.queryInterfaceStats(snmp, node, ifNames) t.queryPoEBudget(snmp, node) t.queryBridgeMIB(snmp, node, ifNames) t.queryDHCPBindings(snmp) } 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) { ifOperStatus := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.8") 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") ifHCOutUcastPkts := t.getInterfaceTable64(snmp, "1.3.6.1.2.1.31.1.1.1.11") 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 { iface.Stats = nil continue } stats := &InterfaceStats{} if speed, ok := ifHighSpeed[ifIndex]; ok { stats.Speed = uint64(speed) * 1000000 } if inErr, ok := ifInErrors[ifIndex]; ok { stats.InErrors = uint64(inErr) } if outErr, ok := ifOutErrors[ifIndex]; ok { stats.OutErrors = uint64(outErr) } inPkts, hasInPkts := ifHCInUcastPkts[ifIndex] outPkts, hasOutPkts := ifHCOutUcastPkts[ifIndex] inBytes, hasInBytes := ifHCInOctets[ifIndex] outBytes, hasOutBytes := ifHCOutOctets[ifIndex] if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes { key := node.ID + ":" + name ifaceTracker.mu.Lock() prev, hasPrev := ifaceTracker.counters[key] if hasPrev { 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 } } } ifaceTracker.counters[key] = &ifaceCounters{ inPkts: inPkts, outPkts: outPkts, inBytes: inBytes, outBytes: outBytes, timestamp: now, } ifaceTracker.mu.Unlock() } if poe, ok := poeStats[name]; ok { stats.PoE = poe } iface.Stats = stats t.errors.CheckPort(node, name, stats) t.errors.CheckUtilization(node, 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") } }