restructure nodes to have interfaces with name, mac, and ips

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-18 08:28:57 -08:00
parent e3bed567ab
commit 09a99064c3
5 changed files with 144 additions and 106 deletions

2
arp.go
View File

@@ -48,7 +48,7 @@ func (t *Tendrils) readARPTable() {
log.Printf("[arp] %s: ip=%s mac=%s", entry.iface, entry.ip, entry.mac) log.Printf("[arp] %s: ip=%s mac=%s", entry.iface, entry.ip, entry.mac)
} }
t.nodes.Update([]net.IP{entry.ip}, []net.HardwareAddr{entry.mac}, "arp") t.nodes.Update(entry.mac, []net.IP{entry.ip}, "", "", "arp")
} }
} }

View File

@@ -68,11 +68,7 @@ func (t *Tendrils) handleLLDPPacket(ifaceName string, packet gopacket.Packet) {
log.Printf("[lldp] %s: mac=%s port=%s name=%s", ifaceName, mac, childPort, systemName) log.Printf("[lldp] %s: mac=%s port=%s name=%s", ifaceName, mac, childPort, systemName)
} }
t.nodes.Update(nil, []net.HardwareAddr{mac}, "lldp") t.nodes.Update(mac, nil, childPort, systemName, "lldp")
if systemName != "" {
t.nodes.SetName(mac, systemName)
}
} }
} }
} }

185
nodes.go
View File

@@ -8,31 +8,48 @@ import (
"sync" "sync"
) )
type Node struct { type Interface struct {
Name string Name string
MAC net.HardwareAddr
IPs map[string]net.IP IPs map[string]net.IP
MACs map[string]net.HardwareAddr
} }
func (n *Node) String() string { func (i *Interface) String() string {
var macs []string name := i.Name
for _, mac := range n.MACs { if name == "" {
macs = append(macs, mac.String()) name = "??"
} }
sort.Strings(macs)
var ips []string var ips []string
for _, ip := range n.IPs { for _, ip := range i.IPs {
ips = append(ips, ip.String()) ips = append(ips, ip.String())
} }
sort.Strings(ips) sort.Strings(ips)
if len(ips) == 0 {
return fmt.Sprintf("%s/%s", name, i.MAC)
}
return fmt.Sprintf("%s/%s %v", name, i.MAC, ips)
}
type Node struct {
Name string
Interfaces map[string]*Interface
}
func (n *Node) String() string {
name := n.Name name := n.Name
if name == "" { if name == "" {
name = "??" name = "??"
} }
return fmt.Sprintf("%s {macs=%v ips=%v}", name, macs, ips) var ifaces []string
for _, iface := range n.Interfaces {
ifaces = append(ifaces, iface.String())
}
sort.Strings(ifaces)
return fmt.Sprintf("%s {%v}", name, ifaces)
} }
type Nodes struct { type Nodes struct {
@@ -54,84 +71,64 @@ func NewNodes(t *Tendrils) *Nodes {
} }
n.nodes[0] = &Node{ n.nodes[0] = &Node{
IPs: map[string]net.IP{}, Interfaces: map[string]*Interface{},
MACs: map[string]net.HardwareAddr{},
} }
return n return n
} }
func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, source string) { func (n *Nodes) Update(mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName, source string) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
if len(ips) == 0 && len(macs) == 0 { if mac == nil {
return return
} }
existingIDs := map[int]bool{} macKey := mac.String()
for _, ip := range ips {
if id, exists := n.ipIndex[ip.String()]; exists {
existingIDs[id] = true
}
}
for _, mac := range macs {
if id, exists := n.macIndex[mac.String()]; exists {
existingIDs[id] = true
}
}
var targetID int var targetID int
isNew := false isNew := false
if len(existingIDs) == 0 {
if id, exists := n.macIndex[macKey]; exists {
targetID = id
} else {
targetID = n.nextID targetID = n.nextID
n.nextID++ n.nextID++
n.nodes[targetID] = &Node{ n.nodes[targetID] = &Node{
IPs: map[string]net.IP{}, Interfaces: map[string]*Interface{},
MACs: map[string]net.HardwareAddr{},
} }
isNew = true isNew = true
} else if len(existingIDs) == 1 {
for id := range existingIDs {
targetID = id
}
} else {
var ids []int
for id := range existingIDs {
ids = append(ids, id)
}
targetID = ids[0]
var merging []string
for i := 1; i < len(ids); i++ {
merging = append(merging, n.nodes[ids[i]].String())
n.mergeNodes(targetID, ids[i])
}
if n.t.LogEvents {
log.Printf("[merge] %v into %s (via %s)", merging, n.nodes[targetID], source)
}
} }
node := n.nodes[targetID] node := n.nodes[targetID]
var added []string var added []string
iface, exists := node.Interfaces[macKey]
if !exists {
iface = &Interface{
MAC: mac,
IPs: map[string]net.IP{},
}
node.Interfaces[macKey] = iface
n.macIndex[macKey] = targetID
added = append(added, "mac="+macKey)
}
for _, ip := range ips { for _, ip := range ips {
ipKey := ip.String() ipKey := ip.String()
if _, exists := node.IPs[ipKey]; !exists { if _, exists := iface.IPs[ipKey]; !exists {
added = append(added, "ip="+ipKey) added = append(added, "ip="+ipKey)
} }
node.IPs[ipKey] = ip iface.IPs[ipKey] = ip
n.ipIndex[ipKey] = targetID n.ipIndex[ipKey] = targetID
} }
for _, mac := range macs { if ifaceName != "" && iface.Name == "" {
macKey := mac.String() iface.Name = ifaceName
if _, exists := node.MACs[macKey]; !exists { }
added = append(added, "mac="+macKey)
} if nodeName != "" && node.Name == "" {
node.MACs[macKey] = mac node.Name = nodeName
n.macIndex[macKey] = targetID
} }
if len(added) > 0 && n.t.LogEvents { if len(added) > 0 && n.t.LogEvents {
@@ -143,28 +140,74 @@ func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, source string) {
} }
} }
func (n *Nodes) Merge(macs []net.HardwareAddr, source string) {
n.mu.Lock()
defer n.mu.Unlock()
if len(macs) < 2 {
return
}
existingIDs := map[int]bool{}
for _, mac := range macs {
if id, exists := n.macIndex[mac.String()]; exists {
existingIDs[id] = true
}
}
if len(existingIDs) < 2 {
return
}
var ids []int
for id := range existingIDs {
ids = append(ids, id)
}
sort.Ints(ids)
targetID := ids[0]
for i := 1; i < len(ids); i++ {
if n.t.LogEvents {
log.Printf("[merge] %s into %s (via %s)", n.nodes[ids[i]], n.nodes[targetID], source)
}
n.mergeNodes(targetID, ids[i])
}
}
func (n *Nodes) mergeNodes(keepID, mergeID int) { func (n *Nodes) mergeNodes(keepID, mergeID int) {
keep := n.nodes[keepID] keep := n.nodes[keepID]
merge := n.nodes[mergeID] merge := n.nodes[mergeID]
for ipKey, ip := range merge.IPs { if merge.Name != "" && keep.Name == "" {
keep.IPs[ipKey] = ip keep.Name = merge.Name
n.ipIndex[ipKey] = keepID
} }
for macKey, mac := range merge.MACs { for macKey, iface := range merge.Interfaces {
keep.MACs[macKey] = mac if existing, exists := keep.Interfaces[macKey]; exists {
n.macIndex[macKey] = keepID for ipKey, ip := range iface.IPs {
existing.IPs[ipKey] = ip
n.ipIndex[ipKey] = keepID
}
if existing.Name == "" && iface.Name != "" {
existing.Name = iface.Name
}
} else {
keep.Interfaces[macKey] = iface
n.macIndex[macKey] = keepID
for ipKey := range iface.IPs {
n.ipIndex[ipKey] = keepID
}
}
} }
delete(n.nodes, mergeID) delete(n.nodes, mergeID)
} }
func (n *Nodes) GetByIP(ipv4 net.IP) *Node { func (n *Nodes) GetByIP(ip net.IP) *Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
if id, exists := n.ipIndex[ipv4.String()]; exists { if id, exists := n.ipIndex[ip.String()]; exists {
return n.nodes[id] return n.nodes[id]
} }
return nil return nil
@@ -180,18 +223,6 @@ func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node {
return nil return nil
} }
func (n *Nodes) SetName(mac net.HardwareAddr, name string) {
n.mu.Lock()
defer n.mu.Unlock()
if id, exists := n.macIndex[mac.String()]; exists {
node := n.nodes[id]
if node.Name == "" {
node.Name = name
}
}
}
func (n *Nodes) All() []*Node { func (n *Nodes) All() []*Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()

21
snmp.go
View File

@@ -83,12 +83,14 @@ func (t *Tendrils) querySwitches() {
nodes := t.nodes.All() nodes := t.nodes.All()
for _, node := range nodes { for _, node := range nodes {
for _, ip := range node.IPs { for _, iface := range node.Interfaces {
if ip.To4() == nil { for _, ip := range iface.IPs {
continue if ip.To4() == nil {
} continue
}
go t.querySNMPDevice(ip) go t.querySNMPDevice(ip)
}
} }
} }
} }
@@ -166,8 +168,11 @@ func (t *Tendrils) queryInterfaceMACs(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
} }
} }
if len(macs) > 0 { for _, mac := range macs {
t.nodes.Update([]net.IP{deviceIP}, macs, "snmp-ifmac") t.nodes.Update(mac, nil, "", "", "snmp-ifmac")
}
if len(macs) > 1 {
t.nodes.Merge(macs, "snmp-ifmac")
} }
} }
@@ -229,7 +234,7 @@ func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, deviceIP net.IP) {
log.Printf("[snmp] %s: mac=%s port=%s", deviceIP, mac, ifName) log.Printf("[snmp] %s: mac=%s port=%s", deviceIP, mac, ifName)
} }
t.nodes.Update(nil, []net.HardwareAddr{mac}, "snmp") t.nodes.Update(mac, nil, "", "", "snmp")
} }
} }

View File

@@ -69,27 +69,33 @@ func (t *Tendrils) populateLocalAddresses() {
root.Name = hostname root.Name = hostname
} }
for _, iface := range interfaces { for _, netIface := range interfaces {
if len(iface.HardwareAddr) > 0 { if len(netIface.HardwareAddr) == 0 {
macKey := iface.HardwareAddr.String()
root.MACs[macKey] = iface.HardwareAddr
t.nodes.macIndex[macKey] = 0
}
addrs, err := iface.Addrs()
if err != nil {
continue continue
} }
for _, addr := range addrs { macKey := netIface.HardwareAddr.String()
if ipnet, ok := addr.(*net.IPNet); ok { iface := &Interface{
if ipnet.IP.To4() != nil && !ipnet.IP.IsLoopback() { Name: netIface.Name,
ipKey := ipnet.IP.String() MAC: netIface.HardwareAddr,
root.IPs[ipKey] = ipnet.IP IPs: map[string]net.IP{},
t.nodes.ipIndex[ipKey] = 0 }
addrs, err := netIface.Addrs()
if err == nil {
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok {
if ipnet.IP.To4() != nil && !ipnet.IP.IsLoopback() {
ipKey := ipnet.IP.String()
iface.IPs[ipKey] = ipnet.IP
t.nodes.ipIndex[ipKey] = 0
}
} }
} }
} }
root.Interfaces[macKey] = iface
t.nodes.macIndex[macKey] = 0
} }
} }