package tendrils import ( "context" "encoding/json" "fmt" "net" "sort" "strings" "time" "github.com/fvbommel/sortorder" "go.jetify.com/typeid" ) func newID(prefix string) string { tid, _ := typeid.WithPrefix(prefix) return tid.String() } type ArtNetUniverse int func (u ArtNetUniverse) Net() int { return (int(u) >> 8) & 0x7f } func (u ArtNetUniverse) Subnet() int { return (int(u) >> 4) & 0x0f } func (u ArtNetUniverse) Universe() int { return int(u) & 0x0f } func (u ArtNetUniverse) String() string { return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe()) } type ArtNetUniverseSet map[ArtNetUniverse]time.Time func (s ArtNetUniverseSet) Add(u ArtNetUniverse) { s[u] = time.Now() } func (s ArtNetUniverseSet) Universes() []ArtNetUniverse { result := make([]ArtNetUniverse, 0, len(s)) for u := range s { result = append(result, u) } sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) return result } func (s ArtNetUniverseSet) Expire(maxAge time.Duration) { expireTime := time.Now().Add(-maxAge) for u, lastSeen := range s { if lastSeen.Before(expireTime) { delete(s, u) } } } func (s ArtNetUniverseSet) MarshalJSON() ([]byte, error) { return json.Marshal(s.Universes()) } type SACNUniverse int func (u SACNUniverse) String() string { return fmt.Sprintf("%d", u) } type SACNUniverseSet map[SACNUniverse]time.Time func (s SACNUniverseSet) Add(u SACNUniverse) { s[u] = time.Now() } func (s SACNUniverseSet) Universes() []SACNUniverse { result := make([]SACNUniverse, 0, len(s)) for u := range s { result = append(result, u) } sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) return result } func (s SACNUniverseSet) Expire(maxAge time.Duration) { expireTime := time.Now().Add(-maxAge) for u, lastSeen := range s { if lastSeen.Before(expireTime) { delete(s, u) } } } func (s SACNUniverseSet) MarshalJSON() ([]byte, error) { return json.Marshal(s.Universes()) } type MulticastGroupID int const ( MulticastUnknown MulticastGroupID = iota MulticastMDNS MulticastPTP MulticastPTPAnnounce MulticastPTPSync MulticastPTPDelay MulticastSAP MulticastShureSLP MulticastSSDP MulticastSLP MulticastAdminScopedBroadcast ) type MulticastGroup struct { ID MulticastGroupID SACNUniverse SACNUniverse DanteFlow int DanteAV int RawIP string } func (g MulticastGroup) String() string { switch g.ID { case MulticastMDNS: return "mdns" case MulticastPTP: return "ptp" case MulticastPTPAnnounce: return "ptp-announce" case MulticastPTPSync: return "ptp-sync" case MulticastPTPDelay: return "ptp-delay" case MulticastSAP: return "sap" case MulticastShureSLP: return "shure-slp" case MulticastSSDP: return "ssdp" case MulticastSLP: return "slp" case MulticastAdminScopedBroadcast: return "admin-scoped-broadcast" } if g.SACNUniverse > 0 { return fmt.Sprintf("sacn:%d", g.SACNUniverse) } if g.DanteFlow > 0 { return fmt.Sprintf("dante-mcast:%d", g.DanteFlow) } if g.DanteAV > 0 { return fmt.Sprintf("dante-av:%d", g.DanteAV) } return g.RawIP } func (g MulticastGroup) MarshalJSON() ([]byte, error) { return json.Marshal(g.String()) } func (g MulticastGroup) IsDante() bool { return g.DanteFlow > 0 || g.DanteAV > 0 } func (g MulticastGroup) IsSACN() bool { return g.SACNUniverse > 0 } func ParseMulticastGroup(ip net.IP) MulticastGroup { ip4 := ip.To4() if ip4 == nil { return MulticastGroup{RawIP: ip.String()} } switch ip.String() { case "224.0.0.251": return MulticastGroup{ID: MulticastMDNS} case "224.0.1.129": return MulticastGroup{ID: MulticastPTP} case "224.0.1.130": return MulticastGroup{ID: MulticastPTPAnnounce} case "224.0.1.131": return MulticastGroup{ID: MulticastPTPSync} case "224.0.1.132": return MulticastGroup{ID: MulticastPTPDelay} case "224.2.127.254": return MulticastGroup{ID: MulticastSAP} case "239.255.254.253": return MulticastGroup{ID: MulticastShureSLP} case "239.255.255.250": return MulticastGroup{ID: MulticastSSDP} case "239.255.255.253": return MulticastGroup{ID: MulticastSLP} case "239.255.255.255": return MulticastGroup{ID: MulticastAdminScopedBroadcast} } if ip4[0] == 239 && ip4[1] == 255 { universe := int(ip4[2])*256 + int(ip4[3]) if universe >= 1 && universe <= 63999 { return MulticastGroup{SACNUniverse: SACNUniverse(universe)} } } if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 { flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3]) return MulticastGroup{DanteFlow: flowID} } if ip4[0] == 239 && ip4[1] == 253 { flowID := (int(ip4[2]) << 8) | int(ip4[3]) return MulticastGroup{DanteAV: flowID} } return MulticastGroup{RawIP: ip.String()} } type MulticastMembership struct { Group MulticastGroup LastSeen time.Time } type MulticastMembershipSet map[string]*MulticastMembership func (s MulticastMembershipSet) Add(group MulticastGroup) { key := group.String() if m, exists := s[key]; exists { m.LastSeen = time.Now() } else { s[key] = &MulticastMembership{Group: group, LastSeen: time.Now()} } } func (s MulticastMembershipSet) Remove(group MulticastGroup) { delete(s, group.String()) } func (s MulticastMembershipSet) Groups() []MulticastGroup { result := make([]MulticastGroup, 0, len(s)) for _, m := range s { result = append(result, m.Group) } sort.Slice(result, func(i, j int) bool { return result[i].String() < result[j].String() }) return result } func (s MulticastMembershipSet) SACNInputs() []SACNUniverse { var result []SACNUniverse for _, m := range s { if m.Group.IsSACN() { result = append(result, m.Group.SACNUniverse) } } sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) return result } func (s MulticastMembershipSet) Expire(maxAge time.Duration) { expireTime := time.Now().Add(-maxAge) for key, m := range s { if m.LastSeen.Before(expireTime) { delete(s, key) } } } func (s MulticastMembershipSet) MarshalJSON() ([]byte, error) { return json.Marshal(s.Groups()) } type MAC string func (m MAC) Parse() net.HardwareAddr { mac, _ := net.ParseMAC(string(m)) return mac } func MACFrom(mac net.HardwareAddr) MAC { if mac == nil { return "" } return MAC(mac.String()) } type IPSet map[string]bool func (s IPSet) MarshalJSON() ([]byte, error) { ips := make([]string, 0, len(s)) for ip := range s { ips = append(ips, ip) } sort.Strings(ips) return json.Marshal(ips) } func (s IPSet) Add(ip net.IP) { s[ip.String()] = true } func (s IPSet) Has(ip string) bool { return s[ip] } func (s IPSet) Slice() []string { ips := make([]string, 0, len(s)) for ip := range s { ips = append(ips, ip) } sort.Strings(ips) return ips } type NameSet map[string]bool func sortNamesByLength(names []string) { sort.Slice(names, func(i, j int) bool { if len(names[i]) != len(names[j]) { return len(names[i]) < len(names[j]) } return names[i] < names[j] }) } func (s NameSet) MarshalJSON() ([]byte, error) { names := make([]string, 0, len(s)) for name := range s { names = append(names, name) } sortNamesByLength(names) return json.Marshal(names) } func (s NameSet) Add(name string) { s[name] = true } func (s NameSet) Has(name string) bool { return s[name] } type InterfaceMap map[string]*Interface func (m InterfaceMap) MarshalJSON() ([]byte, error) { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return sortorder.NaturalLess(keys[i], keys[j]) }) ifaces := make([]*Interface, 0, len(m)) for _, k := range keys { ifaces = append(ifaces, m[k]) } return json.Marshal(ifaces) } type Interface struct { Name string `json:"name,omitempty"` MAC MAC `json:"mac"` IPs IPSet `json:"ips"` Stats *InterfaceStats `json:"stats,omitempty"` } type InterfaceStats struct { Speed uint64 `json:"speed,omitempty"` InErrors uint64 `json:"in_errors,omitempty"` OutErrors uint64 `json:"out_errors,omitempty"` InPktsRate float64 `json:"in_pkts_rate,omitempty"` OutPktsRate float64 `json:"out_pkts_rate,omitempty"` InBytesRate float64 `json:"in_bytes_rate,omitempty"` OutBytesRate float64 `json:"out_bytes_rate,omitempty"` PoE *PoEStats `json:"poe,omitempty"` } type PoEStats struct { Power float64 `json:"power"` MaxPower float64 `json:"max_power"` } type PoEBudget struct { Power float64 `json:"power"` MaxPower float64 `json:"max_power"` } type Node struct { ID string `json:"id"` Names NameSet `json:"names"` Interfaces InterfaceMap `json:"interfaces"` MACTable map[string]string `json:"-"` PoEBudget *PoEBudget `json:"poe_budget,omitempty"` IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"` DanteTxChannels string `json:"dante_tx_channels,omitempty"` MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"` ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"` ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"` SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"` DanteTx []*DantePeer `json:"dante_tx,omitempty"` DanteRx []*DantePeer `json:"dante_rx,omitempty"` Unreachable bool `json:"unreachable,omitempty"` pollTrigger chan struct{} cancelFunc context.CancelFunc danteLastSeen time.Time } func (n *Node) MACTableSize() int { return len(n.MACTable) } func (n *Node) SACNInputs() []SACNUniverse { if n.MulticastGroups == nil { return nil } return n.MulticastGroups.SACNInputs() } func (n *Node) MarshalJSON() ([]byte, error) { type nodeJSON struct { ID string `json:"id"` Names NameSet `json:"names"` Interfaces InterfaceMap `json:"interfaces"` MACTableSize int `json:"mac_table_size,omitempty"` PoEBudget *PoEBudget `json:"poe_budget,omitempty"` IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"` DanteTxChannels string `json:"dante_tx_channels,omitempty"` MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"` ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"` ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"` SACNInputs []SACNUniverse `json:"sacn_inputs,omitempty"` SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"` DanteTx []*DantePeer `json:"dante_tx,omitempty"` DanteRx []*DantePeer `json:"dante_rx,omitempty"` Unreachable bool `json:"unreachable,omitempty"` } return json.Marshal(nodeJSON{ ID: n.ID, Names: n.Names, Interfaces: n.Interfaces, MACTableSize: n.MACTableSize(), PoEBudget: n.PoEBudget, IsDanteClockMaster: n.IsDanteClockMaster, DanteTxChannels: n.DanteTxChannels, MulticastGroups: n.MulticastGroups, ArtNetInputs: n.ArtNetInputs, ArtNetOutputs: n.ArtNetOutputs, SACNInputs: n.SACNInputs(), SACNOutputs: n.SACNOutputs, DanteTx: n.DanteTx, DanteRx: n.DanteRx, Unreachable: n.Unreachable, }) } type DantePeer struct { Node *Node `json:"node"` Channels []string `json:"channels,omitempty"` Status map[string]string `json:"status,omitempty"` } func (p *DantePeer) MarshalJSON() ([]byte, error) { type peerJSON struct { Node *Node `json:"node"` Channels []string `json:"channels,omitempty"` Status map[string]string `json:"status,omitempty"` } nodeRef := &Node{ ID: p.Node.ID, Names: p.Node.Names, Interfaces: p.Node.Interfaces, } return json.Marshal(peerJSON{ Node: nodeRef, Channels: p.Channels, Status: p.Status, }) } func (n *Node) WithInterface(ifaceKey string) *Node { if ifaceKey == "" { return n } iface, exists := n.Interfaces[ifaceKey] if !exists { return n } return &Node{ ID: n.ID, Names: n.Names, Interfaces: InterfaceMap{ifaceKey: iface}, MACTable: n.MACTable, PoEBudget: n.PoEBudget, IsDanteClockMaster: n.IsDanteClockMaster, DanteTxChannels: n.DanteTxChannels, } } func (i *Interface) String() string { var parts []string parts = append(parts, string(i.MAC)) if i.Name != "" { parts = append(parts, fmt.Sprintf("(%s)", i.Name)) } if len(i.IPs) > 0 { parts = append(parts, fmt.Sprintf("%v", i.IPs.Slice())) } if i.Stats != nil { parts = append(parts, i.Stats.String()) } result := parts[0] for _, p := range parts[1:] { result += " " + p } return result } 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.InBytesRate > 0 || s.OutBytesRate > 0 { parts = append(parts, fmt.Sprintf("%.0f/%.0fB/s", s.InBytesRate, s.OutBytesRate)) } 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 "[" + strings.Join(parts, " ") + "]" } func (n *Node) String() string { name := n.DisplayName() if name == "" { name = "??" } 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)) } var ifaces []string for _, iface := range n.Interfaces { ifaces = append(ifaces, iface.String()) } sort.Slice(ifaces, func(i, j int) bool { return sortorder.NaturalLess(ifaces[i], ifaces[j]) }) parts = append(parts, fmt.Sprintf("{%v}", ifaces)) return strings.Join(parts, " ") } func (n *Node) DisplayName() string { if len(n.Names) > 0 { var names []string for name := range n.Names { names = append(names, name) } sortNamesByLength(names) return strings.Join(names, "/") } for _, iface := range n.Interfaces { for ip := range iface.IPs { return ip } } for _, iface := range n.Interfaces { if iface.MAC != "" { return string(iface.MAC) } } return "" } func (n *Node) FirstMAC() string { for _, iface := range n.Interfaces { if iface.MAC != "" { return string(iface.MAC) } } return "" } func (n *Node) FirstIP() string { for _, iface := range n.Interfaces { for ip := range iface.IPs { return ip } } return "" }