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

@@ -6,3 +6,4 @@
- Always push after commiting
- Use git add -A so you don't miss files when committing
- 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) {
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()
defer n.mu.Unlock()
@@ -65,6 +69,13 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childP
return
}
parentID := 0
if parentIP != nil {
if id, exists := n.ipIndex[parentIP.String()]; exists {
parentID = id
}
}
existingIDs := map[int]bool{}
for _, ip := range ips {
@@ -86,7 +97,7 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childP
n.nodes[targetID] = &Node{
IPs: map[string]net.IP{},
MACs: map[string]net.HardwareAddr{},
ParentID: 0,
ParentID: parentID,
LocalPort: childPort,
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())
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]

97
snmp.go
View File

@@ -3,13 +3,27 @@ package tendrils
import (
"context"
"fmt"
"log"
"net"
"regexp"
"strings"
"time"
"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 {
username string
authKey string
@@ -63,7 +77,7 @@ func (t *Tendrils) connectSNMP(ip net.IP) (*gosnmp.GoSNMP, error) {
}
func (t *Tendrils) pollSNMP(ctx context.Context) {
ticker := time.NewTicker(5 * time.Minute)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
t.querySwitches()
@@ -112,40 +126,59 @@ func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
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)
if err != nil {
return
}
portMap := make(map[string]int)
type macPortEntry struct {
mac net.HardwareAddr
bridgePort int
}
var macPorts []macPortEntry
for _, result := range portResults {
if result.Type == gosnmp.Integer {
oidSuffix := result.Name[len(portOID)+1:]
portMap[oidSuffix] = result.Value.(int)
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)
ifNames := t.getInterfaceNames(snmp)
for _, result := range macResults {
if result.Type == gosnmp.OctetString {
macBytes := result.Value.([]byte)
if len(macBytes) != 6 {
continue
}
for _, entry := range macPorts {
mac := entry.mac
bridgePort := entry.bridgePort
mac := net.HardwareAddr(macBytes)
if isBroadcastOrZero(mac) {
continue
}
oidSuffix := result.Name[len(macOID)+1:]
bridgePort, exists := portMap[oidSuffix]
if !exists {
continue
}
ifIndex, exists := bridgePortToIfIndex[bridgePort]
if !exists {
ifIndex = bridgePort
@@ -156,11 +189,27 @@ func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
ifName = "??"
}
t.nodes.Update(nil, []net.HardwareAddr{mac}, ifName, "", "snmp")
addToParent := false
for _, rule := range addToParentRules {
if rule.MatchString(ifName) {
addToParent = true
break
}
}
log.Printf("[snmp] queried bridge mib on %s", deviceIP)
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")
}
}
}
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 = "??"
}
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 {
@@ -249,7 +296,7 @@ func (t *Tendrils) getBridgePortMapping(snmp *gosnmp.GoSNMP) map[int]int {
mapping := make(map[int]int)
for _, result := range results {
if result.Type == gosnmp.Integer {
oidParts := result.Name[len(oid)+1:]
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
var bridgePort int
_, err := fmt.Sscanf(oidParts, "%d", &bridgePort)
if err != nil {
@@ -274,7 +321,7 @@ func (t *Tendrils) getInterfaceNames(snmp *gosnmp.GoSNMP) map[int]string {
names := make(map[int]string)
for _, result := range results {
if result.Type == gosnmp.OctetString {
oidParts := result.Name[len(oid)+1:]
oidParts := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
var ifIndex int
_, err := fmt.Sscanf(oidParts, "%d", &ifIndex)
if err != nil {