2025-11-29 20:53:29 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-22 23:09:54 -08:00
|
|
|
"context"
|
2025-11-29 20:53:29 -08:00
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"sort"
|
|
|
|
|
"sync"
|
2026-01-22 23:09:54 -08:00
|
|
|
"time"
|
2026-01-18 14:05:33 -08:00
|
|
|
|
|
|
|
|
"github.com/fvbommel/sortorder"
|
2025-11-29 20:53:29 -08:00
|
|
|
)
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
type Interface struct {
|
2026-01-22 22:47:30 -08:00
|
|
|
Name string
|
|
|
|
|
MAC net.HardwareAddr
|
|
|
|
|
IPs map[string]net.IP
|
|
|
|
|
Stats *InterfaceStats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type InterfaceStats struct {
|
|
|
|
|
Speed uint64 // bits per second
|
|
|
|
|
InErrors uint64
|
|
|
|
|
OutErrors uint64
|
|
|
|
|
PoE *PoEStats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PoEStats struct {
|
|
|
|
|
Power float64 // watts in use
|
|
|
|
|
MaxPower float64 // watts allocated/negotiated
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
func (i *Interface) String() string {
|
2025-11-29 20:53:29 -08:00
|
|
|
var ips []string
|
2026-01-18 08:28:57 -08:00
|
|
|
for _, ip := range i.IPs {
|
2025-11-29 20:53:29 -08:00
|
|
|
ips = append(ips, ip.String())
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(ips)
|
|
|
|
|
|
2026-01-18 08:37:13 -08:00
|
|
|
var parts []string
|
|
|
|
|
parts = append(parts, i.MAC.String())
|
|
|
|
|
if i.Name != "" {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
|
|
|
|
|
}
|
|
|
|
|
if len(ips) > 0 {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%v", ips))
|
|
|
|
|
}
|
2026-01-22 22:47:30 -08:00
|
|
|
if i.Stats != nil {
|
|
|
|
|
parts = append(parts, i.Stats.String())
|
|
|
|
|
}
|
2026-01-18 08:37:13 -08:00
|
|
|
|
|
|
|
|
result := parts[0]
|
|
|
|
|
for _, p := range parts[1:] {
|
|
|
|
|
result += " " + p
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
2026-01-18 08:37:13 -08:00
|
|
|
return result
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:47:30 -08:00
|
|
|
func (s *InterfaceStats) String() string {
|
|
|
|
|
var parts []string
|
|
|
|
|
|
|
|
|
|
if s.Speed > 0 {
|
|
|
|
|
if s.Speed >= 1000000000 {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%dG", s.Speed/1000000000))
|
|
|
|
|
} else if s.Speed >= 1000000 {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%dM", s.Speed/1000000))
|
|
|
|
|
} else {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%d", s.Speed))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.InErrors > 0 || s.OutErrors > 0 {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("err:%d/%d", s.InErrors, s.OutErrors))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.PoE != nil {
|
|
|
|
|
if s.PoE.MaxPower > 0 {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("poe:%.1f/%.1fW", s.PoE.Power, s.PoE.MaxPower))
|
|
|
|
|
} else {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("poe:%.1fW", s.PoE.Power))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "[" + fmt.Sprintf("%s", joinParts(parts)) + "]"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func joinParts(parts []string) string {
|
|
|
|
|
result := ""
|
|
|
|
|
for i, p := range parts {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
result += " "
|
|
|
|
|
}
|
|
|
|
|
result += p
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:56:47 -08:00
|
|
|
type PoEBudget struct {
|
|
|
|
|
Power float64 // watts in use
|
|
|
|
|
MaxPower float64 // watts total budget
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
type Node struct {
|
2026-01-23 00:32:07 -08:00
|
|
|
Name string
|
|
|
|
|
Interfaces map[string]*Interface
|
|
|
|
|
MACTable map[string]string // peer MAC -> local interface name
|
|
|
|
|
PoEBudget *PoEBudget
|
2026-01-23 00:24:36 -08:00
|
|
|
IsDanteClockMaster bool
|
2026-01-23 00:32:07 -08:00
|
|
|
pollTrigger chan struct{}
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) String() string {
|
2025-11-29 22:27:31 -08:00
|
|
|
name := n.Name
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = "??"
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:56:47 -08:00
|
|
|
var parts []string
|
|
|
|
|
parts = append(parts, name)
|
|
|
|
|
|
|
|
|
|
if n.PoEBudget != nil {
|
|
|
|
|
parts = append(parts, fmt.Sprintf("[poe:%.0f/%.0fW]", n.PoEBudget.Power, n.PoEBudget.MaxPower))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
var ifaces []string
|
|
|
|
|
for _, iface := range n.Interfaces {
|
|
|
|
|
ifaces = append(ifaces, iface.String())
|
|
|
|
|
}
|
2026-01-18 14:05:33 -08:00
|
|
|
sort.Slice(ifaces, func(i, j int) bool { return sortorder.NaturalLess(ifaces[i], ifaces[j]) })
|
2026-01-18 08:28:57 -08:00
|
|
|
|
2026-01-22 22:56:47 -08:00
|
|
|
parts = append(parts, fmt.Sprintf("{%v}", ifaces))
|
|
|
|
|
|
|
|
|
|
return joinParts(parts)
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:46:56 -08:00
|
|
|
type MulticastGroup struct {
|
|
|
|
|
IP net.IP
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *MulticastGroup) Name() string {
|
|
|
|
|
ip := g.IP.To4()
|
|
|
|
|
if ip == nil {
|
|
|
|
|
return g.IP.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 07:02:47 -08:00
|
|
|
// Well-known multicast addresses
|
|
|
|
|
switch g.IP.String() {
|
|
|
|
|
case "224.0.0.251":
|
|
|
|
|
return "mdns"
|
|
|
|
|
case "224.0.1.129":
|
|
|
|
|
return "ptp"
|
|
|
|
|
case "224.2.127.254":
|
|
|
|
|
return "sap"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sACN (239.255.x.x, universes 1-63999)
|
2026-01-22 23:46:56 -08:00
|
|
|
if ip[0] == 239 && ip[1] == 255 {
|
|
|
|
|
universe := int(ip[2])*256 + int(ip[3])
|
2026-01-23 07:02:47 -08:00
|
|
|
if universe >= 1 && universe <= 63999 {
|
|
|
|
|
return fmt.Sprintf("sacn:%d", universe)
|
|
|
|
|
}
|
2026-01-22 23:46:56 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 07:02:47 -08:00
|
|
|
// Dante audio multicast (239.69-71.x.x)
|
2026-01-23 00:24:36 -08:00
|
|
|
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
|
|
|
|
flowID := (int(ip[1]-69) << 16) | (int(ip[2]) << 8) | int(ip[3])
|
|
|
|
|
return fmt.Sprintf("dante-mcast:%d", flowID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:46:56 -08:00
|
|
|
return g.IP.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 00:24:36 -08:00
|
|
|
func (g *MulticastGroup) IsDante() bool {
|
|
|
|
|
ip := g.IP.To4()
|
|
|
|
|
if ip == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if ip[0] == 239 && ip[1] == 255 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:46:56 -08:00
|
|
|
type MulticastMembership struct {
|
|
|
|
|
Node *Node
|
|
|
|
|
LastSeen time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MulticastGroupMembers struct {
|
|
|
|
|
Group *MulticastGroup
|
|
|
|
|
Members map[string]*MulticastMembership // source IP -> membership
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 20:53:29 -08:00
|
|
|
type Nodes struct {
|
2026-01-22 23:46:56 -08:00
|
|
|
mu sync.RWMutex
|
|
|
|
|
nodes map[int]*Node
|
|
|
|
|
ipIndex map[string]int
|
|
|
|
|
macIndex map[string]int
|
|
|
|
|
nodeCancel map[int]context.CancelFunc
|
|
|
|
|
multicastGroups map[string]*MulticastGroupMembers // group IP string -> group with members
|
|
|
|
|
nextID int
|
|
|
|
|
t *Tendrils
|
|
|
|
|
ctx context.Context
|
|
|
|
|
cancelAll context.CancelFunc
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 21:02:30 -08:00
|
|
|
func NewNodes(t *Tendrils) *Nodes {
|
2026-01-22 23:09:54 -08:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
2026-01-18 14:44:15 -08:00
|
|
|
return &Nodes{
|
2026-01-22 23:46:56 -08:00
|
|
|
nodes: map[int]*Node{},
|
|
|
|
|
ipIndex: map[string]int{},
|
|
|
|
|
macIndex: map[string]int{},
|
|
|
|
|
nodeCancel: map[int]context.CancelFunc{},
|
|
|
|
|
multicastGroups: map[string]*MulticastGroupMembers{},
|
|
|
|
|
nextID: 1,
|
|
|
|
|
t: t,
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
cancelAll: cancel,
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:09:54 -08:00
|
|
|
func (n *Nodes) Shutdown() {
|
|
|
|
|
n.cancelAll()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName, source string) {
|
2025-11-29 20:53:29 -08:00
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
2026-01-23 00:24:36 -08:00
|
|
|
if mac == nil && target == nil && len(ips) == 0 {
|
2025-11-29 20:53:29 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:00:51 -08:00
|
|
|
targetID := -1
|
2026-01-18 08:17:45 -08:00
|
|
|
isNew := false
|
2026-01-18 08:28:57 -08:00
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
if target != nil {
|
|
|
|
|
for id, node := range n.nodes {
|
|
|
|
|
if node == target {
|
|
|
|
|
targetID = id
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:47:46 -08:00
|
|
|
if mac != nil {
|
2026-01-18 14:44:15 -08:00
|
|
|
macKey := mac.String()
|
|
|
|
|
if id, exists := n.macIndex[macKey]; exists {
|
|
|
|
|
if _, nodeExists := n.nodes[id]; nodeExists {
|
2026-01-18 14:47:46 -08:00
|
|
|
if targetID == -1 {
|
|
|
|
|
targetID = id
|
|
|
|
|
} else if id != targetID {
|
|
|
|
|
n.mergeNodes(targetID, id)
|
|
|
|
|
}
|
2026-01-18 14:44:15 -08:00
|
|
|
} else {
|
|
|
|
|
delete(n.macIndex, macKey)
|
|
|
|
|
}
|
2026-01-18 14:00:51 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 00:24:36 -08:00
|
|
|
if targetID == -1 {
|
|
|
|
|
for _, ip := range ips {
|
|
|
|
|
if id, exists := n.ipIndex[ip.String()]; exists {
|
|
|
|
|
if _, nodeExists := n.nodes[id]; nodeExists {
|
|
|
|
|
targetID = id
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:09:54 -08:00
|
|
|
var node *Node
|
2026-01-18 14:00:51 -08:00
|
|
|
if targetID == -1 {
|
2025-11-29 20:53:29 -08:00
|
|
|
targetID = n.nextID
|
|
|
|
|
n.nextID++
|
2026-01-22 23:09:54 -08:00
|
|
|
node = &Node{
|
|
|
|
|
Interfaces: map[string]*Interface{},
|
|
|
|
|
MACTable: map[string]string{},
|
|
|
|
|
pollTrigger: make(chan struct{}, 1),
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
2026-01-22 23:09:54 -08:00
|
|
|
n.nodes[targetID] = node
|
2026-01-18 08:17:45 -08:00
|
|
|
isNew = true
|
2026-01-22 23:09:54 -08:00
|
|
|
n.startNodePoller(targetID, node)
|
|
|
|
|
} else {
|
|
|
|
|
node = n.nodes[targetID]
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:44:15 -08:00
|
|
|
var added []string
|
|
|
|
|
if mac != nil {
|
|
|
|
|
added = n.updateNodeInterface(node, targetID, mac, ips, ifaceName)
|
2026-01-23 00:24:36 -08:00
|
|
|
} else {
|
|
|
|
|
for _, ip := range ips {
|
|
|
|
|
ipKey := ip.String()
|
|
|
|
|
if _, exists := n.ipIndex[ipKey]; !exists {
|
|
|
|
|
n.ipIndex[ipKey] = targetID
|
2026-01-23 00:32:07 -08:00
|
|
|
iface, exists := node.Interfaces[ipKey]
|
|
|
|
|
if !exists {
|
|
|
|
|
iface = &Interface{
|
|
|
|
|
IPs: map[string]net.IP{},
|
|
|
|
|
}
|
|
|
|
|
node.Interfaces[ipKey] = iface
|
|
|
|
|
}
|
|
|
|
|
iface.IPs[ipKey] = ip
|
2026-01-23 00:24:36 -08:00
|
|
|
added = append(added, "ip="+ipKey)
|
|
|
|
|
go n.t.requestARP(ip)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 14:44:15 -08:00
|
|
|
}
|
2026-01-18 14:12:14 -08:00
|
|
|
|
|
|
|
|
if nodeName != "" && node.Name == "" {
|
|
|
|
|
node.Name = nodeName
|
2026-01-18 14:44:15 -08:00
|
|
|
added = append(added, "name="+nodeName)
|
2026-01-18 14:12:14 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:09:54 -08:00
|
|
|
hasNewIP := false
|
|
|
|
|
for _, a := range added {
|
|
|
|
|
if len(a) > 3 && a[:3] == "ip=" {
|
|
|
|
|
hasNewIP = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:12:14 -08:00
|
|
|
if len(added) > 0 {
|
|
|
|
|
if n.t.LogEvents {
|
|
|
|
|
if isNew {
|
|
|
|
|
log.Printf("[add] %s %v (via %s)", node, added, source)
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("[update] %s +%v (via %s)", node, added, source)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if n.t.LogNodes {
|
|
|
|
|
n.logNode(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 23:09:54 -08:00
|
|
|
|
|
|
|
|
if hasNewIP {
|
|
|
|
|
n.triggerPoll(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) startNodePoller(nodeID int, node *Node) {
|
|
|
|
|
ctx, cancel := context.WithCancel(n.ctx)
|
|
|
|
|
n.nodeCancel[nodeID] = cancel
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-node.pollTrigger:
|
|
|
|
|
n.t.pollNode(node)
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
n.t.pollNode(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) triggerPoll(node *Node) {
|
|
|
|
|
select {
|
|
|
|
|
case node.pollTrigger <- struct{}{}:
|
|
|
|
|
default:
|
|
|
|
|
}
|
2026-01-18 14:12:14 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
|
|
|
|
|
macKey := mac.String()
|
2025-11-29 20:53:29 -08:00
|
|
|
var added []string
|
|
|
|
|
|
2026-01-18 14:00:51 -08:00
|
|
|
ifaceKey := macKey
|
|
|
|
|
if ifaceName != "" {
|
|
|
|
|
ifaceKey = ifaceName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iface, exists := node.Interfaces[ifaceKey]
|
2026-01-18 14:12:14 -08:00
|
|
|
if !exists {
|
|
|
|
|
if ifaceName != "" {
|
|
|
|
|
if oldIface, oldExists := node.Interfaces[macKey]; oldExists && oldIface.MAC.String() == macKey {
|
|
|
|
|
iface = oldIface
|
|
|
|
|
iface.Name = ifaceName
|
|
|
|
|
delete(node.Interfaces, macKey)
|
|
|
|
|
node.Interfaces[ifaceKey] = iface
|
|
|
|
|
added = append(added, "iface="+ifaceKey)
|
|
|
|
|
exists = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for _, existing := range node.Interfaces {
|
|
|
|
|
if existing.MAC.String() == macKey {
|
|
|
|
|
iface = existing
|
|
|
|
|
exists = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 08:28:57 -08:00
|
|
|
if !exists {
|
|
|
|
|
iface = &Interface{
|
2026-01-18 14:00:51 -08:00
|
|
|
Name: ifaceName,
|
|
|
|
|
MAC: mac,
|
|
|
|
|
IPs: map[string]net.IP{},
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
2026-01-18 14:00:51 -08:00
|
|
|
node.Interfaces[ifaceKey] = iface
|
|
|
|
|
added = append(added, "iface="+ifaceKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, exists := n.macIndex[macKey]; !exists {
|
2026-01-18 14:12:14 -08:00
|
|
|
n.macIndex[macKey] = nodeID
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 20:53:29 -08:00
|
|
|
for _, ip := range ips {
|
|
|
|
|
ipKey := ip.String()
|
2026-01-18 08:28:57 -08:00
|
|
|
if _, exists := iface.IPs[ipKey]; !exists {
|
2025-11-29 20:53:29 -08:00
|
|
|
added = append(added, "ip="+ipKey)
|
|
|
|
|
}
|
2026-01-18 08:28:57 -08:00
|
|
|
iface.IPs[ipKey] = ip
|
2026-01-18 14:12:14 -08:00
|
|
|
n.ipIndex[ipKey] = nodeID
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:12:14 -08:00
|
|
|
return added
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
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])
|
|
|
|
|
}
|
2026-01-18 08:37:13 -08:00
|
|
|
|
|
|
|
|
if n.t.LogNodes {
|
|
|
|
|
n.logNode(n.nodes[targetID])
|
|
|
|
|
}
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 20:53:29 -08:00
|
|
|
func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
|
|
|
|
keep := n.nodes[keepID]
|
|
|
|
|
merge := n.nodes[mergeID]
|
|
|
|
|
|
2026-01-18 14:23:21 -08:00
|
|
|
if keep == nil || merge == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
if merge.Name != "" && keep.Name == "" {
|
|
|
|
|
keep.Name = merge.Name
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:12:14 -08:00
|
|
|
for _, iface := range merge.Interfaces {
|
|
|
|
|
var ips []net.IP
|
|
|
|
|
for _, ip := range iface.IPs {
|
|
|
|
|
ips = append(ips, ip)
|
2026-01-18 08:28:57 -08:00
|
|
|
}
|
2026-01-18 14:12:14 -08:00
|
|
|
n.updateNodeInterface(keep, keepID, iface.MAC, ips, iface.Name)
|
2026-01-18 14:23:21 -08:00
|
|
|
n.macIndex[iface.MAC.String()] = keepID
|
2025-11-29 20:53:29 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:54:06 -08:00
|
|
|
for peerMAC, ifaceName := range merge.MACTable {
|
|
|
|
|
if keep.MACTable == nil {
|
|
|
|
|
keep.MACTable = map[string]string{}
|
|
|
|
|
}
|
|
|
|
|
keep.MACTable[peerMAC] = ifaceName
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 20:53:29 -08:00
|
|
|
delete(n.nodes, mergeID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
func (n *Nodes) GetByIP(ip net.IP) *Node {
|
2025-11-29 20:53:29 -08:00
|
|
|
n.mu.RLock()
|
|
|
|
|
defer n.mu.RUnlock()
|
|
|
|
|
|
2026-01-18 08:28:57 -08:00
|
|
|
if id, exists := n.ipIndex[ip.String()]; exists {
|
2025-11-29 20:53:29 -08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 14:54:06 -08:00
|
|
|
func (n *Nodes) UpdateMACTable(node *Node, peerMAC net.HardwareAddr, ifaceName string) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if node.MACTable == nil {
|
|
|
|
|
node.MACTable = map[string]string{}
|
|
|
|
|
}
|
|
|
|
|
node.MACTable[peerMAC.String()] = ifaceName
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 00:24:36 -08:00
|
|
|
func (n *Nodes) SetDanteClockMaster(ip net.IP) {
|
|
|
|
|
n.mu.RLock()
|
|
|
|
|
currentMaster := ""
|
|
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
if node.IsDanteClockMaster {
|
|
|
|
|
currentMaster = ip.String()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
n.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
if currentMaster != ip.String() {
|
|
|
|
|
n.Update(nil, nil, []net.IP{ip}, "", "", "ptp")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
node.IsDanteClockMaster = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if id, exists := n.ipIndex[ip.String()]; exists {
|
|
|
|
|
n.nodes[id].IsDanteClockMaster = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:46:56 -08:00
|
|
|
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
node := n.getNodeByIPLocked(sourceIP)
|
|
|
|
|
|
|
|
|
|
groupKey := groupIP.String()
|
|
|
|
|
sourceKey := sourceIP.String()
|
|
|
|
|
|
|
|
|
|
gm := n.multicastGroups[groupKey]
|
|
|
|
|
if gm == nil {
|
|
|
|
|
gm = &MulticastGroupMembers{
|
|
|
|
|
Group: &MulticastGroup{IP: groupIP},
|
|
|
|
|
Members: map[string]*MulticastMembership{},
|
|
|
|
|
}
|
|
|
|
|
n.multicastGroups[groupKey] = gm
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gm.Members[sourceKey] = &MulticastMembership{
|
|
|
|
|
Node: node,
|
|
|
|
|
LastSeen: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
groupKey := groupIP.String()
|
|
|
|
|
sourceKey := sourceIP.String()
|
|
|
|
|
|
|
|
|
|
if gm := n.multicastGroups[groupKey]; gm != nil {
|
|
|
|
|
delete(gm.Members, sourceKey)
|
|
|
|
|
if len(gm.Members) == 0 {
|
|
|
|
|
delete(n.multicastGroups, groupKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node {
|
|
|
|
|
if id, exists := n.ipIndex[ip.String()]; exists {
|
|
|
|
|
return n.nodes[id]
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 08:37:13 -08:00
|
|
|
func (n *Nodes) logNode(node *Node) {
|
|
|
|
|
name := node.Name
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = "??"
|
|
|
|
|
}
|
2026-01-23 00:24:36 -08:00
|
|
|
var tags []string
|
2026-01-22 22:56:47 -08:00
|
|
|
if node.PoEBudget != nil {
|
2026-01-23 00:24:36 -08:00
|
|
|
tags = append(tags, fmt.Sprintf("poe:%.0f/%.0fW", node.PoEBudget.Power, node.PoEBudget.MaxPower))
|
|
|
|
|
}
|
|
|
|
|
if node.IsDanteClockMaster {
|
|
|
|
|
tags = append(tags, "dante-clock-master")
|
|
|
|
|
}
|
|
|
|
|
if len(tags) > 0 {
|
|
|
|
|
log.Printf("[node] %s [%s]", name, joinParts(tags))
|
2026-01-22 22:56:47 -08:00
|
|
|
} else {
|
|
|
|
|
log.Printf("[node] %s", name)
|
|
|
|
|
}
|
2026-01-18 08:37:13 -08:00
|
|
|
|
2026-01-18 14:05:33 -08:00
|
|
|
var ifaceKeys []string
|
|
|
|
|
for ifaceKey := range node.Interfaces {
|
|
|
|
|
ifaceKeys = append(ifaceKeys, ifaceKey)
|
2026-01-18 08:37:13 -08:00
|
|
|
}
|
2026-01-18 14:05:33 -08:00
|
|
|
sort.Slice(ifaceKeys, func(i, j int) bool { return sortorder.NaturalLess(ifaceKeys[i], ifaceKeys[j]) })
|
2026-01-18 08:37:13 -08:00
|
|
|
|
2026-01-18 14:05:33 -08:00
|
|
|
for _, ifaceKey := range ifaceKeys {
|
|
|
|
|
iface := node.Interfaces[ifaceKey]
|
2026-01-18 08:37:13 -08:00
|
|
|
log.Printf("[node] %s", iface)
|
|
|
|
|
}
|
2026-01-18 14:54:06 -08:00
|
|
|
|
|
|
|
|
if len(node.MACTable) > 0 {
|
|
|
|
|
log.Printf("[node] mac table: %d entries", len(node.MACTable))
|
|
|
|
|
}
|
2026-01-18 08:37:13 -08:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 20:53:29 -08:00
|
|
|
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
|
|
|
|
|
}
|
2026-01-18 14:44:15 -08:00
|
|
|
|
|
|
|
|
func (n *Nodes) LogAll() {
|
|
|
|
|
n.mu.RLock()
|
|
|
|
|
defer n.mu.RUnlock()
|
|
|
|
|
|
2026-01-22 23:29:22 -08:00
|
|
|
nodes := make([]*Node, 0, len(n.nodes))
|
2026-01-18 14:44:15 -08:00
|
|
|
for _, node := range n.nodes {
|
2026-01-22 23:29:22 -08:00
|
|
|
nodes = append(nodes, node)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
|
|
|
return sortorder.NaturalLess(nodes[i].Name, nodes[j].Name)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
log.Printf("[sigusr1] ================ %d nodes ================", len(nodes))
|
|
|
|
|
for _, node := range nodes {
|
2026-01-18 14:44:15 -08:00
|
|
|
n.logNode(node)
|
|
|
|
|
}
|
2026-01-22 23:29:22 -08:00
|
|
|
|
|
|
|
|
links := n.getDirectLinks()
|
|
|
|
|
sort.Slice(links, func(i, j int) bool {
|
|
|
|
|
if links[i].NodeA.Name != links[j].NodeA.Name {
|
|
|
|
|
return sortorder.NaturalLess(links[i].NodeA.Name, links[j].NodeA.Name)
|
|
|
|
|
}
|
|
|
|
|
if links[i].InterfaceA != links[j].InterfaceA {
|
|
|
|
|
return sortorder.NaturalLess(links[i].InterfaceA, links[j].InterfaceA)
|
|
|
|
|
}
|
|
|
|
|
if links[i].NodeB.Name != links[j].NodeB.Name {
|
|
|
|
|
return sortorder.NaturalLess(links[i].NodeB.Name, links[j].NodeB.Name)
|
|
|
|
|
}
|
|
|
|
|
return sortorder.NaturalLess(links[i].InterfaceB, links[j].InterfaceB)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if len(links) > 0 {
|
|
|
|
|
log.Printf("[sigusr1] ================ %d links ================", len(links))
|
|
|
|
|
for _, link := range links {
|
|
|
|
|
log.Printf("[sigusr1] %s", link)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 23:46:56 -08:00
|
|
|
|
|
|
|
|
n.expireMulticastMemberships()
|
|
|
|
|
|
|
|
|
|
if len(n.multicastGroups) > 0 {
|
|
|
|
|
var groups []*MulticastGroupMembers
|
|
|
|
|
for _, gm := range n.multicastGroups {
|
|
|
|
|
groups = append(groups, gm)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(groups, func(i, j int) bool {
|
|
|
|
|
return sortorder.NaturalLess(groups[i].Group.Name(), groups[j].Group.Name())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groups))
|
|
|
|
|
for _, gm := range groups {
|
|
|
|
|
var memberNames []string
|
|
|
|
|
for sourceIP, membership := range gm.Members {
|
|
|
|
|
var name string
|
|
|
|
|
if membership.Node != nil {
|
|
|
|
|
name = membership.Node.Name
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = sourceIP
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
name = sourceIP
|
|
|
|
|
}
|
|
|
|
|
memberNames = append(memberNames, name)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(memberNames, func(i, j int) bool {
|
|
|
|
|
return sortorder.NaturalLess(memberNames[i], memberNames[j])
|
|
|
|
|
})
|
|
|
|
|
log.Printf("[sigusr1] %s: %v", gm.Group.Name(), memberNames)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
n.t.artnet.LogAll()
|
2026-01-22 23:46:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) expireMulticastMemberships() {
|
|
|
|
|
expireTime := time.Now().Add(-5 * time.Minute)
|
|
|
|
|
for groupKey, gm := range n.multicastGroups {
|
|
|
|
|
for sourceKey, membership := range gm.Members {
|
|
|
|
|
if membership.LastSeen.Before(expireTime) {
|
|
|
|
|
delete(gm.Members, sourceKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(gm.Members) == 0 {
|
|
|
|
|
delete(n.multicastGroups, groupKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 23:29:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Link struct {
|
|
|
|
|
NodeA *Node
|
|
|
|
|
InterfaceA string
|
|
|
|
|
NodeB *Node
|
|
|
|
|
InterfaceB string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Link) String() string {
|
|
|
|
|
nameA := l.NodeA.Name
|
|
|
|
|
if nameA == "" {
|
|
|
|
|
nameA = "??"
|
|
|
|
|
}
|
|
|
|
|
nameB := l.NodeB.Name
|
|
|
|
|
if nameB == "" {
|
|
|
|
|
nameB = "??"
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%s:%s <-> %s:%s", nameA, l.InterfaceA, nameB, l.InterfaceB)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) getDirectLinks() []*Link {
|
|
|
|
|
macToNode := map[string]*Node{}
|
|
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
for _, iface := range node.Interfaces {
|
|
|
|
|
macToNode[iface.MAC.String()] = node
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
var links []*Link
|
|
|
|
|
|
|
|
|
|
for _, target := range n.nodes {
|
|
|
|
|
seenMACs := map[string]bool{}
|
|
|
|
|
for _, iface := range target.Interfaces {
|
|
|
|
|
mac := iface.MAC.String()
|
|
|
|
|
if seenMACs[mac] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seenMACs[mac] = true
|
|
|
|
|
|
|
|
|
|
var lastHop *Node
|
|
|
|
|
var lastPort string
|
|
|
|
|
|
|
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
port, sees := node.MACTable[mac]
|
|
|
|
|
if !sees || node == target {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasCloserNode := false
|
|
|
|
|
for otherMAC, otherPort := range node.MACTable {
|
|
|
|
|
if otherPort != port {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
otherNode := macToNode[otherMAC]
|
|
|
|
|
if otherNode == nil || otherNode == node || otherNode == target {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, alsoSees := otherNode.MACTable[mac]; alsoSees {
|
|
|
|
|
hasCloserNode = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasCloserNode {
|
|
|
|
|
lastHop = node
|
|
|
|
|
lastPort = port
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if lastHop != nil {
|
|
|
|
|
targetIface := mac
|
|
|
|
|
for lastHopMAC, targetPort := range target.MACTable {
|
|
|
|
|
if macToNode[lastHopMAC] == lastHop {
|
|
|
|
|
targetIface = targetPort
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := makeLinkKey(lastHop, lastPort, target, targetIface)
|
|
|
|
|
if !seen[key] {
|
|
|
|
|
seen[key] = true
|
|
|
|
|
links = append(links, &Link{
|
|
|
|
|
NodeA: lastHop,
|
|
|
|
|
InterfaceA: lastPort,
|
|
|
|
|
NodeB: target,
|
|
|
|
|
InterfaceB: targetIface,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return links
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeLinkKey(nodeA *Node, ifaceA string, nodeB *Node, ifaceB string) string {
|
|
|
|
|
ptrA := fmt.Sprintf("%p", nodeA)
|
|
|
|
|
ptrB := fmt.Sprintf("%p", nodeB)
|
|
|
|
|
if ptrA < ptrB {
|
|
|
|
|
return ptrA + ":" + ifaceA + "-" + ptrB + ":" + ifaceB
|
|
|
|
|
}
|
|
|
|
|
return ptrB + ":" + ifaceB + "-" + ptrA + ":" + ifaceA
|
2026-01-18 14:44:15 -08:00
|
|
|
}
|