From c780fba93c59278152b440b17f27a8fbb78ec55c Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 29 Nov 2025 21:16:58 -0800 Subject: [PATCH] add snmpv3 support --- go.mod | 5 +- go.sum | 5 + snmp.go | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++ tendrils.go | 1 + 4 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 snmp.go diff --git a/go.mod b/go.mod index 2198ae6..89838ae 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,7 @@ go 1.24.4 require github.com/google/gopacket v1.1.19 -require golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect +require ( + github.com/gosnmp/gosnmp v1.42.1 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/go.sum b/go.sum index a915606..e2171f6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/gosnmp/gosnmp v1.42.1 h1:MEJxhpC5v1coL3tFRix08PYmky9nyb1TLRRgJAmXm8A= +github.com/gosnmp/gosnmp v1.42.1/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -7,10 +9,13 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/snmp.go b/snmp.go new file mode 100644 index 0000000..e4bafa1 --- /dev/null +++ b/snmp.go @@ -0,0 +1,289 @@ +package tendrils + +import ( + "context" + "fmt" + "log" + "net" + "time" + + "github.com/gosnmp/gosnmp" +) + +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 (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) pollSNMP(ctx context.Context) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + t.querySwitches() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.querySwitches() + } + } +} + +func (t *Tendrils) querySwitches() { + nodes := t.nodes.All() + + for _, node := range nodes { + for _, ip := range node.IPs { + if ip.To4() == nil { + continue + } + + go t.querySNMPDevice(ip) + } + } +} + +func (t *Tendrils) querySNMPDevice(ip net.IP) { + snmp, err := t.connectSNMP(ip) + if err != nil { + return + } + defer snmp.Conn.Close() + + t.queryBridgeMIB(snmp, ip) + t.queryARPTable(snmp, ip) +} + +func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, deviceIP net.IP) { + macOID := "1.3.6.1.2.1.17.4.3.1.1" + portOID := "1.3.6.1.2.1.17.4.3.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) + for _, result := range portResults { + if result.Type == gosnmp.Integer { + oidSuffix := result.Name[len(portOID)+1:] + portMap[oidSuffix] = result.Value.(int) + } + } + + 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 + } + + 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 + } + + ifName := ifNames[ifIndex] + if ifName == "" { + ifName = "??" + } + + t.nodes.Update(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) { + macOID := "1.3.6.1.2.1.4.22.1.2" + ipOID := "1.3.6.1.2.1.4.22.1.3" + ifIndexOID := "1.3.6.1.2.1.4.22.1.1" + + macResults, err := snmp.BulkWalkAll(macOID) + if err != nil { + return + } + + ipResults, err := snmp.BulkWalkAll(ipOID) + if err != nil { + return + } + + ifIndexResults, err := snmp.BulkWalkAll(ifIndexOID) + if err != nil { + return + } + + ipMap := make(map[string]net.IP) + for _, result := range ipResults { + if result.Type == gosnmp.IPAddress { + oidSuffix := result.Name[len(ipOID)+1:] + ipBytes := result.Value.([]byte) + ipMap[oidSuffix] = net.IP(ipBytes) + } + } + + ifIndexMap := make(map[string]int) + for _, result := range ifIndexResults { + if result.Type == gosnmp.Integer { + oidSuffix := result.Name[len(ifIndexOID)+1:] + ifIndexMap[oidSuffix] = result.Value.(int) + } + } + + ifNames := t.getInterfaceNames(snmp) + + for _, result := range macResults { + if result.Type == gosnmp.OctetString { + macBytes := result.Value.([]byte) + if len(macBytes) != 6 { + continue + } + + mac := net.HardwareAddr(macBytes) + if isBroadcastOrZero(mac) { + continue + } + + oidSuffix := result.Name[len(macOID)+1:] + ip, hasIP := ipMap[oidSuffix] + ifIndex, hasIfIndex := ifIndexMap[oidSuffix] + + var ips []net.IP + if hasIP { + ips = []net.IP{ip} + } + + ifName := "" + if hasIfIndex { + ifName = ifNames[ifIndex] + } + if ifName == "" { + ifName = "??" + } + + t.nodes.Update(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 { + 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 := result.Name[len(oid)+1:] + 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 := result.Name[len(oid)+1:] + var ifIndex int + _, err := fmt.Sscanf(oidParts, "%d", &ifIndex) + if err != nil { + continue + } + name := string(result.Value.([]byte)) + names[ifIndex] = name + } + } + + return names +} diff --git a/tendrils.go b/tendrils.go index cc198ce..4b40529 100644 --- a/tendrils.go +++ b/tendrils.go @@ -24,6 +24,7 @@ func (t *Tendrils) Run() { defer cancel() go t.pollARP(ctx) + go t.pollSNMP(ctx) ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()