package tendrils import ( "fmt" "log" "net" "sort" "sync" ) type Node struct { IPs map[string]net.IP MACs map[string]net.HardwareAddr ParentID int LocalPort string ParentPort string } func (n *Node) String() string { var macs []string for _, mac := range n.MACs { macs = append(macs, mac.String()) } sort.Strings(macs) var ips []string for _, ip := range n.IPs { ips = append(ips, ip.String()) } sort.Strings(ips) return fmt.Sprintf("{macs=%v ips=%v}", macs, ips) } type Nodes struct { mu sync.RWMutex nodes map[int]*Node ipIndex map[string]int macIndex map[string]int nextID int } func NewNodes() *Nodes { n := &Nodes{ nodes: map[int]*Node{}, ipIndex: map[string]int{}, macIndex: map[string]int{}, nextID: 1, } n.nodes[0] = &Node{ IPs: map[string]net.IP{}, MACs: map[string]net.HardwareAddr{}, ParentID: 0, } return n } func (n *Nodes) Update(ips []net.IP, macs []net.HardwareAddr, parentPort, childPort, source string) { n.mu.Lock() defer n.mu.Unlock() if len(ips) == 0 && len(macs) == 0 { return } existingIDs := map[int]bool{} 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 if len(existingIDs) == 0 { targetID = n.nextID n.nextID++ n.nodes[targetID] = &Node{ IPs: map[string]net.IP{}, MACs: map[string]net.HardwareAddr{}, ParentID: 0, LocalPort: childPort, ParentPort: parentPort, } } 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]) } log.Printf("[%s] merged nodes %v into %s", source, merging, n.nodes[targetID]) } node := n.nodes[targetID] var added []string for _, ip := range ips { ipKey := ip.String() if _, exists := node.IPs[ipKey]; !exists { added = append(added, "ip="+ipKey) } node.IPs[ipKey] = ip n.ipIndex[ipKey] = targetID } for _, mac := range macs { macKey := mac.String() if _, exists := node.MACs[macKey]; !exists { added = append(added, "mac="+macKey) } node.MACs[macKey] = mac n.macIndex[macKey] = targetID } if len(added) > 0 { log.Printf("updated %s +%v (via %s)", node, added, source) n.mu.Unlock() n.LogTree() n.mu.Lock() } } func (n *Nodes) mergeNodes(keepID, mergeID int) { keep := n.nodes[keepID] merge := n.nodes[mergeID] for ipKey, ip := range merge.IPs { keep.IPs[ipKey] = ip n.ipIndex[ipKey] = keepID } for macKey, mac := range merge.MACs { keep.MACs[macKey] = mac n.macIndex[macKey] = keepID } delete(n.nodes, mergeID) } func (n *Nodes) GetByIP(ipv4 net.IP) *Node { n.mu.RLock() defer n.mu.RUnlock() if id, exists := n.ipIndex[ipv4.String()]; exists { return n.nodes[id] } return nil } func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node { n.mu.RLock() defer n.mu.RUnlock() if id, exists := n.macIndex[mac.String()]; exists { return n.nodes[id] } return nil } func (n *Nodes) All() []*Node { n.mu.RLock() defer n.mu.RUnlock() result := make([]*Node, 0, len(n.nodes)) for _, node := range n.nodes { result = append(result, node) } return result } func (n *Nodes) LogTree() { n.mu.RLock() defer n.mu.RUnlock() n.logNode(0, "", true) } func (n *Nodes) logNode(id int, prefix string, isLast bool) { node := n.nodes[id] if id == 0 { log.Printf("[root] %s", node) } else { connector := "├──" if isLast { connector = "└──" } if node.ParentPort != "" && node.LocalPort != "" { log.Printf("%s%s %s -> %s on %s", prefix, connector, node.ParentPort, node.LocalPort, node) } else { log.Printf("%s%s %s", prefix, connector, node) } } children := n.getChildren(id) for i, childID := range children { childIsLast := i == len(children)-1 childPrefix := prefix if id != 0 { if isLast { childPrefix += " " } else { childPrefix += "│ " } } n.logNode(childID, childPrefix, childIsLast) } } func (n *Nodes) getChildren(parentID int) []int { var children []int for id, node := range n.nodes { if node.ParentID == parentID && id != 0 { children = append(children, id) } } sort.Ints(children) return children }