add snmpv3 topology discovery with q-bridge support

This commit is contained in:
Ian Gulliver
2025-11-29 21:56:45 -08:00
parent c780fba93c
commit f4972b2b50
3 changed files with 105 additions and 46 deletions

View File

@@ -5,4 +5,5 @@
- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence - Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence
- Always push after commiting - Always push after commiting
- Use git add -A so you don't miss files when committing - Use git add -A so you don't miss files when committing
- Never use go build -- use go run instead - Never use go build -- use go run instead
- Don't commit unless asked to

View File

@@ -58,6 +58,10 @@ func NewNodes() *Nodes {
} }
func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childPort, source string) { func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childPort, source string) {
n.UpdateWithParent(nil, ips, macs, parentPort, childPort, source)
}
func (n *Nodes) UpdateWithParent(parentIP net.IP, ips []net.IP, macs []net.HardwareAddr, parentPort, childPort, source string) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
@@ -65,6 +69,13 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childP
return return
} }
parentID := 0
if parentIP != nil {
if id, exists := n.ipIndex[parentIP.String()]; exists {
parentID = id
}
}
existingIDs := map[int]bool{} existingIDs := map[int]bool{}
for _, ip := range ips { for _, ip := range ips {
@@ -86,7 +97,7 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childP
n.nodes[targetID] = &Node{ n.nodes[targetID] = &Node{
IPs: map[string]net.IP{}, IPs: map[string]net.IP{},
MACs: map[string]net.HardwareAddr{}, MACs: map[string]net.HardwareAddr{},
ParentID: 0, ParentID: parentID,
LocalPort: childPort, LocalPort: childPort,
ParentPort: parentPort, ParentPort: parentPort,
} }
@@ -105,7 +116,7 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childP
merging = append(merging, n.nodes[ids[i]].String()) merging = append(merging, n.nodes[ids[i]].String())
n.mergeNodes(targetID, ids[i]) n.mergeNodes(targetID, ids[i])
} }
log.Printf("[%s] merged nodes %v into %s", source, merging, n.nodes[targetID]) log.Printf("merged nodes %v into %s (via %s)", merging, n.nodes[targetID], source)
} }
node := n.nodes[targetID] node := n.nodes[targetID]

133
snmp.go
View File

@@ -3,22 +3,36 @@ package tendrils
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net" "net"
"regexp"
"strings"
"time" "time"
"github.com/gosnmp/gosnmp" "github.com/gosnmp/gosnmp"
) )
var (
addToParentRules = []*regexp.Regexp{
regexp.MustCompile(`CPU Interface`),
}
portNameRewrites = []struct {
regex *regexp.Regexp
replacement string
}{
{regexp.MustCompile(`Slot: (\d+) Port: (\d+) .+`), "$1/$2"},
}
)
type snmpConfig struct { type snmpConfig struct {
username string username string
authKey string authKey string
privKey string privKey string
authProto gosnmp.SnmpV3AuthProtocol authProto gosnmp.SnmpV3AuthProtocol
privProto gosnmp.SnmpV3PrivProtocol privProto gosnmp.SnmpV3PrivProtocol
secLevel gosnmp.SnmpV3MsgFlags secLevel gosnmp.SnmpV3MsgFlags
timeout time.Duration timeout time.Duration
retries int retries int
} }
func defaultSNMPConfig() *snmpConfig { func defaultSNMPConfig() *snmpConfig {
@@ -63,7 +77,7 @@ func (t *Tendrils) connectSNMP(ip net.IP) (*gosnmp.GoSNMP, error) {
} }
func (t *Tendrils) pollSNMP(ctx context.Context) { func (t *Tendrils) pollSNMP(ctx context.Context) {
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop() defer ticker.Stop()
t.querySwitches() t.querySwitches()
@@ -112,55 +126,90 @@ func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
return return
} }
if len(macResults) == 0 {
macOID = "1.3.6.1.2.1.17.7.1.2.2.1.1"
portOID = "1.3.6.1.2.1.17.7.1.2.2.1.2"
macResults, err = snmp.BulkWalkAll(macOID)
if err != nil {
return
}
}
portResults, err := snmp.BulkWalkAll(portOID) portResults, err := snmp.BulkWalkAll(portOID)
if err != nil { if err != nil {
return return
} }
portMap := make(map[string]int) type macPortEntry struct {
mac net.HardwareAddr
bridgePort int
}
var macPorts []macPortEntry
for _, result := range portResults { for _, result := range portResults {
if result.Type == gosnmp.Integer { if result.Type == gosnmp.Integer {
oidSuffix := result.Name[len(portOID)+1:] oidSuffix := strings.TrimPrefix(result.Name[len(portOID):], ".")
portMap[oidSuffix] = result.Value.(int) 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) bridgePortToIfIndex := t.getBridgePortMapping(snmp)
ifNames := t.getInterfaceNames(snmp) ifNames := t.getInterfaceNames(snmp)
for _, result := range macResults { for _, entry := range macPorts {
if result.Type == gosnmp.OctetString { mac := entry.mac
macBytes := result.Value.([]byte) bridgePort := entry.bridgePort
if len(macBytes) != 6 {
continue
}
mac := net.HardwareAddr(macBytes) if isBroadcastOrZero(mac) {
if isBroadcastOrZero(mac) { continue
continue }
}
oidSuffix := result.Name[len(macOID)+1:] ifIndex, exists := bridgePortToIfIndex[bridgePort]
bridgePort, exists := portMap[oidSuffix] if !exists {
if !exists { ifIndex = bridgePort
continue }
}
ifIndex, exists := bridgePortToIfIndex[bridgePort] ifName := ifNames[ifIndex]
if !exists { if ifName == "" {
ifIndex = bridgePort ifName = "??"
} }
ifName := ifNames[ifIndex] addToParent := false
if ifName == "" { for _, rule := range addToParentRules {
ifName = "??" if rule.MatchString(ifName) {
addToParent = true
break
} }
}
t.nodes.Update(nil, []net.HardwareAddr{mac}, ifName, "", "snmp") for _, rewrite := range portNameRewrites {
if rewrite.regex.MatchString(ifName) {
ifName = rewrite.regex.ReplaceAllString(ifName, rewrite.replacement)
break
}
}
if addToParent {
t.nodes.Update([]net.IP{deviceIP}, []net.HardwareAddr{mac}, "", "", "snmp")
} else {
t.nodes.UpdateWithParent(deviceIP, nil, []net.HardwareAddr{mac}, ifName, "", "snmp")
} }
} }
log.Printf("[snmp] queried bridge mib on %s", deviceIP)
} }
func (t *Tendrils) queryARPTable(snmp *gosnmp.GoSNMP, deviceIP net.IP) { func (t *Tendrils) queryARPTable(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
@@ -231,11 +280,9 @@ func (t *Tendrils) queryARPTable(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
ifName = "??" ifName = "??"
} }
t.nodes.Update(ips, []net.HardwareAddr{mac}, ifName, "", "snmp") t.nodes.UpdateWithParent(deviceIP, ips, []net.HardwareAddr{mac}, ifName, "", "snmp")
} }
} }
log.Printf("[snmp] queried arp table on %s", deviceIP)
} }
func (t *Tendrils) getBridgePortMapping(snmp *gosnmp.GoSNMP) map[int]int { func (t *Tendrils) getBridgePortMapping(snmp *gosnmp.GoSNMP) map[int]int {
@@ -249,7 +296,7 @@ func (t *Tendrils) getBridgePortMapping(snmp *gosnmp.GoSNMP) map[int]int {
mapping := make(map[int]int) mapping := make(map[int]int)
for _, result := range results { for _, result := range results {
if result.Type == gosnmp.Integer { if result.Type == gosnmp.Integer {
oidParts := result.Name[len(oid)+1:] oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
var bridgePort int var bridgePort int
_, err := fmt.Sscanf(oidParts, "%d", &bridgePort) _, err := fmt.Sscanf(oidParts, "%d", &bridgePort)
if err != nil { if err != nil {
@@ -274,7 +321,7 @@ func (t *Tendrils) getInterfaceNames(snmp *gosnmp.GoSNMP) map[int]string {
names := make(map[int]string) names := make(map[int]string)
for _, result := range results { for _, result := range results {
if result.Type == gosnmp.OctetString { if result.Type == gosnmp.OctetString {
oidParts := result.Name[len(oid)+1:] oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
var ifIndex int var ifIndex int
_, err := fmt.Sscanf(oidParts, "%d", &ifIndex) _, err := fmt.Sscanf(oidParts, "%d", &ifIndex)
if err != nil { if err != nil {