2026-01-23 23:31:57 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
2026-01-24 11:03:34 -08:00
|
|
|
"sort"
|
2026-01-23 23:31:57 -08:00
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MulticastGroup struct {
|
2026-01-24 11:03:34 -08:00
|
|
|
Name string `json:"name"`
|
|
|
|
|
IP string `json:"ip"`
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
func (g *MulticastGroup) IsDante() bool {
|
|
|
|
|
ip := net.ParseIP(g.IP).To4()
|
2026-01-23 23:31:57 -08:00
|
|
|
if ip == nil {
|
2026-01-24 11:03:34 -08:00
|
|
|
return false
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
2026-01-24 11:03:34 -08:00
|
|
|
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if ip[0] == 239 && ip[1] == 253 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
func multicastGroupName(ip net.IP) string {
|
|
|
|
|
ip4 := ip.To4()
|
|
|
|
|
if ip4 == nil {
|
|
|
|
|
return ip.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch ip.String() {
|
2026-01-23 23:31:57 -08:00
|
|
|
case "224.0.0.251":
|
|
|
|
|
return "mdns"
|
|
|
|
|
case "224.0.1.129":
|
|
|
|
|
return "ptp"
|
|
|
|
|
case "224.0.1.130":
|
|
|
|
|
return "ptp-announce"
|
|
|
|
|
case "224.0.1.131":
|
|
|
|
|
return "ptp-sync"
|
|
|
|
|
case "224.0.1.132":
|
|
|
|
|
return "ptp-delay"
|
|
|
|
|
case "224.2.127.254":
|
|
|
|
|
return "sap"
|
|
|
|
|
case "239.255.254.253":
|
|
|
|
|
return "shure-slp"
|
|
|
|
|
case "239.255.255.250":
|
|
|
|
|
return "ssdp"
|
|
|
|
|
case "239.255.255.253":
|
|
|
|
|
return "slp"
|
|
|
|
|
case "239.255.255.255":
|
|
|
|
|
return "admin-scoped-broadcast"
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
if ip4[0] == 239 && ip4[1] == 255 {
|
|
|
|
|
universe := int(ip4[2])*256 + int(ip4[3])
|
2026-01-23 23:31:57 -08:00
|
|
|
if universe >= 1 && universe <= 63999 {
|
|
|
|
|
return fmt.Sprintf("sacn:%d", universe)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
|
|
|
|
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
2026-01-23 23:31:57 -08:00
|
|
|
return fmt.Sprintf("dante-mcast:%d", flowID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
if ip4[0] == 239 && ip4[1] == 253 {
|
|
|
|
|
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
2026-01-23 23:31:57 -08:00
|
|
|
return fmt.Sprintf("dante-av:%d", flowID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 11:03:34 -08:00
|
|
|
return ip.String()
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
node := n.getNodeByIPLocked(sourceIP)
|
2026-01-28 22:15:54 -08:00
|
|
|
if node == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
groupName := multicastGroupName(groupIP)
|
|
|
|
|
|
|
|
|
|
if node.multicastLastSeen == nil {
|
|
|
|
|
node.multicastLastSeen = map[string]time.Time{}
|
|
|
|
|
}
|
|
|
|
|
node.multicastLastSeen[groupName] = time.Now()
|
|
|
|
|
|
|
|
|
|
if !containsString(node.MulticastGroups, groupName) {
|
|
|
|
|
node.MulticastGroups = append(node.MulticastGroups, groupName)
|
|
|
|
|
sort.Strings(node.MulticastGroups)
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
|
|
|
|
var universe int
|
|
|
|
|
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
|
|
|
|
if !containsInt(node.SACNInputs, universe) {
|
|
|
|
|
node.SACNInputs = append(node.SACNInputs, universe)
|
|
|
|
|
sort.Ints(node.SACNInputs)
|
|
|
|
|
}
|
2026-01-24 11:03:34 -08:00
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
node := n.getNodeByIPLocked(sourceIP)
|
|
|
|
|
if node == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groupName := multicastGroupName(groupIP)
|
|
|
|
|
delete(node.multicastLastSeen, groupName)
|
2026-01-23 23:31:57 -08:00
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
var groups []string
|
|
|
|
|
for _, g := range node.MulticastGroups {
|
|
|
|
|
if g != groupName {
|
|
|
|
|
groups = append(groups, g)
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
node.MulticastGroups = groups
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
|
|
|
|
n.mu.RLock()
|
|
|
|
|
defer n.mu.RUnlock()
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
node := n.getNodeByIPLocked(deviceIP)
|
|
|
|
|
if node == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
var groups []net.IP
|
|
|
|
|
for _, groupName := range node.MulticastGroups {
|
|
|
|
|
g := &MulticastGroup{Name: groupName}
|
|
|
|
|
if g.IsDante() {
|
|
|
|
|
ip := net.ParseIP(groupName)
|
|
|
|
|
if ip != nil {
|
|
|
|
|
groups = append(groups, ip)
|
|
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return groups
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
|
|
|
|
|
n.mu.RLock()
|
|
|
|
|
defer n.mu.RUnlock()
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
groupName := multicastGroupName(groupIP)
|
2026-01-23 23:31:57 -08:00
|
|
|
var members []*Node
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
if containsString(node.MulticastGroups, groupName) {
|
|
|
|
|
members = append(members, node)
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return members
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) expireMulticastMemberships() {
|
|
|
|
|
expireTime := time.Now().Add(-5 * time.Minute)
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
if node.multicastLastSeen == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var keepGroups []string
|
|
|
|
|
var keepSACNInputs []int
|
|
|
|
|
for _, groupName := range node.MulticastGroups {
|
|
|
|
|
if lastSeen, ok := node.multicastLastSeen[groupName]; ok && !lastSeen.Before(expireTime) {
|
|
|
|
|
keepGroups = append(keepGroups, groupName)
|
|
|
|
|
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
|
|
|
|
var universe int
|
|
|
|
|
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
|
|
|
|
keepSACNInputs = append(keepSACNInputs, universe)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
delete(node.multicastLastSeen, groupName)
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
node.MulticastGroups = keepGroups
|
|
|
|
|
sort.Ints(keepSACNInputs)
|
|
|
|
|
node.SACNInputs = keepSACNInputs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) mergeMulticast(keep, merge *Node) {
|
|
|
|
|
if merge.multicastLastSeen == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if keep.multicastLastSeen == nil {
|
|
|
|
|
keep.multicastLastSeen = map[string]time.Time{}
|
|
|
|
|
}
|
|
|
|
|
for groupName, lastSeen := range merge.multicastLastSeen {
|
|
|
|
|
if existing, ok := keep.multicastLastSeen[groupName]; !ok || lastSeen.After(existing) {
|
|
|
|
|
keep.multicastLastSeen[groupName] = lastSeen
|
|
|
|
|
}
|
|
|
|
|
if !containsString(keep.MulticastGroups, groupName) {
|
|
|
|
|
keep.MulticastGroups = append(keep.MulticastGroups, groupName)
|
|
|
|
|
}
|
|
|
|
|
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
|
|
|
|
var universe int
|
|
|
|
|
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
|
|
|
|
if !containsInt(keep.SACNInputs, universe) {
|
|
|
|
|
keep.SACNInputs = append(keep.SACNInputs, universe)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
sort.Strings(keep.MulticastGroups)
|
|
|
|
|
sort.Ints(keep.SACNInputs)
|
2026-01-23 23:31:57 -08:00
|
|
|
}
|