From 5dbdc0a4084128cefce1c43eae9980c6f0244b9f Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 29 Nov 2025 20:36:45 -0800 Subject: [PATCH] Add neighbor tracking system with LLDP integration --- CLAUDE.md | 3 +- lldp.go | 27 ++++++++ neighbors.go | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++ tendrils.go | 2 + 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 neighbors.go diff --git a/CLAUDE.md b/CLAUDE.md index 248ed5c..8dbec4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,4 +4,5 @@ - Prepend log messages with [ERROR] if applicable - Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence - Always push after commiting -- Use git add -A so you don't miss files when committing \ No newline at end of file +- Use git add -A so you don't miss files when committing +- Never use go build -- use go run instead \ No newline at end of file diff --git a/lldp.go b/lldp.go index f5dfa46..e9acce1 100644 --- a/lldp.go +++ b/lldp.go @@ -53,4 +53,31 @@ func (t *Tendrils) handleLLDPPacket(ifaceName string, packet gopacket.Packet) { log.Printf("[%s] lldp packet received: ChassisID=%x PortID=%s TTL=%d", ifaceName, lldp.ChassisID.ID, lldp.PortID.ID, lldp.TTL) + + if len(lldp.ChassisID.ID) == 6 { + mac := net.HardwareAddr(lldp.ChassisID.ID) + if !isBroadcastOrZero(mac) { + t.neighbors.Update(nil, []net.HardwareAddr{mac}, "lldp:"+ifaceName) + } + } +} + +func isBroadcastOrZero(mac net.HardwareAddr) bool { + if len(mac) != 6 { + return true + } + + allZero := true + allFF := true + + for _, b := range mac { + if b != 0x00 { + allZero = false + } + if b != 0xff { + allFF = false + } + } + + return allZero || allFF } diff --git a/neighbors.go b/neighbors.go new file mode 100644 index 0000000..904f976 --- /dev/null +++ b/neighbors.go @@ -0,0 +1,169 @@ +package tendrils + +import ( + "fmt" + "log" + "net" + "sort" + "sync" +) + +type Neighbor struct { + IPs map[string]net.IP + MACs map[string]net.HardwareAddr +} + +func (n *Neighbor) 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 Neighbors struct { + mu sync.RWMutex + neighbors map[int]*Neighbor + ipIndex map[string]int + macIndex map[string]int + nextID int +} + +func NewNeighbors() *Neighbors { + return &Neighbors{ + neighbors: map[int]*Neighbor{}, + ipIndex: map[string]int{}, + macIndex: map[string]int{}, + nextID: 1, + } +} + +func (n *Neighbors) Update(ips []net.IP, macs []net.HardwareAddr, 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.neighbors[targetID] = &Neighbor{ + IPs: map[string]net.IP{}, + MACs: map[string]net.HardwareAddr{}, + } + } 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.neighbors[ids[i]].String()) + n.mergeNeighbors(targetID, ids[i]) + } + log.Printf("[%s] merged neighbors %v into %s", source, merging, n.neighbors[targetID]) + } + + neighbor := n.neighbors[targetID] + var added []string + + for _, ip := range ips { + ipKey := ip.String() + if _, exists := neighbor.IPs[ipKey]; !exists { + added = append(added, "ip="+ipKey) + } + neighbor.IPs[ipKey] = ip + n.ipIndex[ipKey] = targetID + } + + for _, mac := range macs { + macKey := mac.String() + if _, exists := neighbor.MACs[macKey]; !exists { + added = append(added, "mac="+macKey) + } + neighbor.MACs[macKey] = mac + n.macIndex[macKey] = targetID + } + + if len(added) > 0 { + log.Printf("[%s] updated %s +%v", source, neighbor, added) + } +} + +func (n *Neighbors) mergeNeighbors(keepID, mergeID int) { + keep := n.neighbors[keepID] + merge := n.neighbors[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.neighbors, mergeID) +} + +func (n *Neighbors) GetByIP(ipv4 net.IP) *Neighbor { + n.mu.RLock() + defer n.mu.RUnlock() + + if id, exists := n.ipIndex[ipv4.String()]; exists { + return n.neighbors[id] + } + return nil +} + +func (n *Neighbors) GetByMAC(mac net.HardwareAddr) *Neighbor { + n.mu.RLock() + defer n.mu.RUnlock() + + if id, exists := n.macIndex[mac.String()]; exists { + return n.neighbors[id] + } + return nil +} + +func (n *Neighbors) All() []*Neighbor { + n.mu.RLock() + defer n.mu.RUnlock() + + result := make([]*Neighbor, 0, len(n.neighbors)) + for _, neighbor := range n.neighbors { + result = append(result, neighbor) + } + return result +} diff --git a/tendrils.go b/tendrils.go index c00c9a9..f88fb9b 100644 --- a/tendrils.go +++ b/tendrils.go @@ -9,11 +9,13 @@ import ( type Tendrils struct { activeInterfaces map[string]context.CancelFunc + neighbors *Neighbors } func New() *Tendrils { return &Tendrils{ activeInterfaces: map[string]context.CancelFunc{}, + neighbors: NewNeighbors(), } }