From 536c2d3dc9ac6509c8dbebfc73c1caa8ffe6a351 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Thu, 22 Jan 2026 23:59:32 -0800 Subject: [PATCH] add mdns hostname discovery and artnet universe tracking Co-Authored-By: Claude Opus 4.5 --- artnet.go | 366 +++++++++++++++++++++++++++++++++++++++++++ cmd/tendrils/main.go | 8 + go.mod | 9 +- go.sum | 18 ++- mdns.go | 149 ++++++++++++++++++ nodes.go | 2 + tendrils.go | 34 ++-- 7 files changed, 570 insertions(+), 16 deletions(-) create mode 100644 artnet.go create mode 100644 mdns.go diff --git a/artnet.go b/artnet.go new file mode 100644 index 0000000..690881c --- /dev/null +++ b/artnet.go @@ -0,0 +1,366 @@ +package tendrils + +import ( + "context" + "encoding/binary" + "fmt" + "log" + "net" + "sort" + "strings" + "sync" + "time" + + "github.com/fvbommel/sortorder" +) + +const ( + artNetPort = 6454 + artNetID = "Art-Net\x00" + opPoll = 0x2000 + opPollReply = 0x2100 + protocolVersion = 14 +) + +type ArtNetNode struct { + IP net.IP + MAC net.HardwareAddr + ShortName string + LongName string + Inputs []int + Outputs []int + LastSeen time.Time +} + +func (t *Tendrils) listenArtNet(ctx context.Context, iface net.Interface) { + addrs, err := iface.Addrs() + if err != nil { + return + } + + var srcIP net.IP + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { + srcIP = ipnet.IP.To4() + break + } + } + if srcIP == nil { + return + } + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: artNetPort}) + if err != nil { + log.Printf("[ERROR] failed to listen artnet on %s: %v", iface.Name, err) + return + } + defer conn.Close() + + go t.runArtNetPoller(ctx, iface, conn) + + buf := make([]byte, 65536) + for { + select { + case <-ctx.Done(): + return + default: + } + + conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + n, src, err := conn.ReadFromUDP(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + continue + } + + t.handleArtNetPacket(iface.Name, src.IP, buf[:n]) + } +} + +func (t *Tendrils) handleArtNetPacket(ifaceName string, srcIP net.IP, data []byte) { + if len(data) < 12 { + return + } + + if string(data[:8]) != artNetID { + return + } + + opcode := binary.LittleEndian.Uint16(data[8:10]) + + switch opcode { + case opPollReply: + t.handleArtPollReply(ifaceName, srcIP, data) + } +} + +func (t *Tendrils) handleArtPollReply(ifaceName string, srcIP net.IP, data []byte) { + if len(data) < 207 { + return + } + + ip := net.IPv4(data[10], data[11], data[12], data[13]) + + var mac net.HardwareAddr + if len(data) >= 207 { + mac = net.HardwareAddr(data[201:207]) + } + + shortName := strings.TrimRight(string(data[26:44]), "\x00") + longName := strings.TrimRight(string(data[44:108]), "\x00") + + netSwitch := int(data[18]) + subSwitch := int(data[19]) + + numPorts := int(data[173]) + if numPorts > 4 { + numPorts = 4 + } + + var inputs, outputs []int + for i := 0; i < numPorts; i++ { + portType := data[174+i] + swIn := int(data[186+i]) + swOut := int(data[190+i]) + + universe := netSwitch<<8 | subSwitch<<4 + + if portType&0x40 != 0 { + inputs = append(inputs, universe|swIn) + } + if portType&0x80 != 0 { + outputs = append(outputs, universe|swOut) + } + } + + if t.DebugArtNet { + log.Printf("[artnet] %s: %s %s short=%q long=%q numPorts=%d portTypes=%v in=%v out=%v", ifaceName, ip, mac, shortName, longName, numPorts, data[174:178], inputs, outputs) + } + + node := &ArtNetNode{ + IP: ip, + MAC: mac, + ShortName: shortName, + LongName: longName, + Inputs: inputs, + Outputs: outputs, + LastSeen: time.Now(), + } + + t.nodes.UpdateArtNet(node) + + name := longName + if name == "" { + name = shortName + } + if name != "" { + t.nodes.Update(nil, mac, []net.IP{ip}, "", name, "artnet") + } +} + +func (t *Tendrils) runArtNetPoller(ctx context.Context, iface net.Interface, conn *net.UDPConn) { + addrs, err := iface.Addrs() + if err != nil { + return + } + + var broadcast net.IP + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { + ip := ipnet.IP.To4() + mask := ipnet.Mask + broadcast = make(net.IP, 4) + for i := 0; i < 4; i++ { + broadcast[i] = ip[i] | ^mask[i] + } + break + } + } + if broadcast == nil { + return + } + + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + t.sendArtPoll(conn, broadcast, iface.Name) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.sendArtPoll(conn, broadcast, iface.Name) + } + } +} + +func (t *Tendrils) sendArtPoll(conn *net.UDPConn, broadcast net.IP, ifaceName string) { + packet := make([]byte, 14) + copy(packet[0:8], artNetID) + binary.LittleEndian.PutUint16(packet[8:10], opPoll) + binary.LittleEndian.PutUint16(packet[10:12], protocolVersion) + packet[12] = 0x00 + packet[13] = 0x00 + + _, err := conn.WriteToUDP(packet, &net.UDPAddr{IP: broadcast, Port: artNetPort}) + if err != nil { + if t.DebugArtNet { + log.Printf("[artnet] %s: failed to send poll: %v", ifaceName, err) + } + return + } + + if t.DebugArtNet { + log.Printf("[artnet] %s: sent poll to %s", ifaceName, broadcast) + } +} + +type ArtNetNodes struct { + mu sync.RWMutex + nodes map[string]*ArtNetNode +} + +func NewArtNetNodes() *ArtNetNodes { + return &ArtNetNodes{ + nodes: map[string]*ArtNetNode{}, + } +} + +func (a *ArtNetNodes) Update(node *ArtNetNode) { + a.mu.Lock() + defer a.mu.Unlock() + key := node.IP.String() + + existing, exists := a.nodes[key] + if exists { + for _, u := range node.Inputs { + if !containsInt(existing.Inputs, u) { + existing.Inputs = append(existing.Inputs, u) + } + } + for _, u := range node.Outputs { + if !containsInt(existing.Outputs, u) { + existing.Outputs = append(existing.Outputs, u) + } + } + existing.LastSeen = node.LastSeen + if node.ShortName != "" { + existing.ShortName = node.ShortName + } + if node.LongName != "" { + existing.LongName = node.LongName + } + } else { + a.nodes[key] = node + } +} + +func containsInt(slice []int, val int) bool { + for _, v := range slice { + if v == val { + return true + } + } + return false +} + +func (a *ArtNetNodes) Expire() { + a.mu.Lock() + defer a.mu.Unlock() + expireTime := time.Now().Add(-60 * time.Second) + for key, node := range a.nodes { + if node.LastSeen.Before(expireTime) { + delete(a.nodes, key) + } + } +} + +func (a *ArtNetNodes) GetAll() []*ArtNetNode { + a.mu.RLock() + defer a.mu.RUnlock() + result := make([]*ArtNetNode, 0, len(a.nodes)) + for _, node := range a.nodes { + result = append(result, node) + } + return result +} + +func (a *ArtNetNodes) LogAll() { + a.Expire() + + a.mu.RLock() + defer a.mu.RUnlock() + + if len(a.nodes) == 0 { + return + } + + var nodes []*ArtNetNode + for _, node := range a.nodes { + nodes = append(nodes, node) + } + sort.Slice(nodes, func(i, j int) bool { + return sortorder.NaturalLess(nodes[i].LongName, nodes[j].LongName) + }) + + inputUniverses := map[int][]string{} + outputUniverses := map[int][]string{} + + for _, node := range nodes { + name := node.LongName + if name == "" { + name = node.ShortName + } + if name == "" { + name = node.IP.String() + } + for _, u := range node.Inputs { + inputUniverses[u] = append(inputUniverses[u], name) + } + for _, u := range node.Outputs { + outputUniverses[u] = append(outputUniverses[u], name) + } + } + + var allUniverses []int + seen := map[int]bool{} + for u := range inputUniverses { + if !seen[u] { + allUniverses = append(allUniverses, u) + seen[u] = true + } + } + for u := range outputUniverses { + if !seen[u] { + allUniverses = append(allUniverses, u) + seen[u] = true + } + } + sort.Ints(allUniverses) + + log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses)) + for _, u := range allUniverses { + ins := inputUniverses[u] + outs := outputUniverses[u] + var parts []string + if len(ins) > 0 { + sort.Slice(ins, func(i, j int) bool { return sortorder.NaturalLess(ins[i], ins[j]) }) + parts = append(parts, fmt.Sprintf("in:%v", ins)) + } + if len(outs) > 0 { + sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) }) + parts = append(parts, fmt.Sprintf("out:%v", outs)) + } + net := (u >> 8) & 0x7f + subnet := (u >> 4) & 0x0f + universe := u & 0x0f + log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, net, subnet, universe, strings.Join(parts, " ")) + } +} + +func (n *Nodes) UpdateArtNet(artNode *ArtNetNode) { + n.t.artnet.Update(artNode) +} diff --git a/cmd/tendrils/main.go b/cmd/tendrils/main.go index 9fda0d1..8c2c1d9 100644 --- a/cmd/tendrils/main.go +++ b/cmd/tendrils/main.go @@ -12,12 +12,16 @@ func main() { noLLDP := flag.Bool("no-lldp", false, "disable LLDP discovery") noSNMP := flag.Bool("no-snmp", false, "disable SNMP discovery") noIGMP := flag.Bool("no-igmp", false, "disable IGMP querier") + noMDNS := flag.Bool("no-mdns", false, "disable mDNS discovery") + noArtNet := flag.Bool("no-artnet", false, "disable Art-Net discovery") logEvents := flag.Bool("log-events", false, "log node events") logNodes := flag.Bool("log-nodes", false, "log full node details on changes") debugARP := flag.Bool("debug-arp", false, "debug ARP discovery") debugLLDP := flag.Bool("debug-lldp", false, "debug LLDP discovery") debugSNMP := flag.Bool("debug-snmp", false, "debug SNMP discovery") debugIGMP := flag.Bool("debug-igmp", false, "debug IGMP querier") + debugMDNS := flag.Bool("debug-mdns", false, "debug mDNS discovery") + debugArtNet := flag.Bool("debug-artnet", false, "debug Art-Net discovery") flag.Parse() t := tendrils.New() @@ -26,11 +30,15 @@ func main() { t.DisableLLDP = *noLLDP t.DisableSNMP = *noSNMP t.DisableIGMP = *noIGMP + t.DisableMDNS = *noMDNS + t.DisableArtNet = *noArtNet t.LogEvents = *logEvents t.LogNodes = *logNodes t.DebugARP = *debugARP t.DebugLLDP = *debugLLDP t.DebugSNMP = *debugSNMP t.DebugIGMP = *debugIGMP + t.DebugMDNS = *debugMDNS + t.DebugArtNet = *debugArtNet t.Run() } diff --git a/go.mod b/go.mod index 52b1917..dcd2df1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,13 @@ require ( github.com/fvbommel/sortorder v1.1.0 github.com/google/gopacket v1.1.19 github.com/gosnmp/gosnmp v1.42.1 + github.com/miekg/dns v1.1.72 ) -require golang.org/x/sys v0.13.0 // indirect +require ( + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect +) diff --git a/go.sum b/go.sum index 9a7401c..f987b9d 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/gosnmp/gosnmp v1.42.1 h1:MEJxhpC5v1coL3tFRix08PYmky9nyb1TLRRgJAmXm8A= github.com/gosnmp/gosnmp v1.42.1/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -14,17 +18,23 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mdns.go b/mdns.go new file mode 100644 index 0000000..4135964 --- /dev/null +++ b/mdns.go @@ -0,0 +1,149 @@ +package tendrils + +import ( + "context" + "log" + "net" + "strings" + "time" + + "github.com/miekg/dns" +) + +const ( + mdnsAddr = "224.0.0.251:5353" +) + +func (t *Tendrils) listenMDNS(ctx context.Context, iface net.Interface) { + addr, err := net.ResolveUDPAddr("udp4", mdnsAddr) + if err != nil { + log.Printf("[ERROR] failed to resolve mdns address: %v", err) + return + } + + conn, err := net.ListenMulticastUDP("udp4", &iface, addr) + if err != nil { + log.Printf("[ERROR] failed to listen mdns on %s: %v", iface.Name, err) + return + } + defer conn.Close() + + go t.runMDNSQuerier(ctx, iface) + + buf := make([]byte, 65536) + for { + select { + case <-ctx.Done(): + return + default: + } + + conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + n, src, err := conn.ReadFromUDP(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + continue + } + + t.handleMDNSPacket(iface.Name, src.IP, buf[:n]) + } +} + +func (t *Tendrils) handleMDNSPacket(ifaceName string, srcIP net.IP, data []byte) { + msg := new(dns.Msg) + if err := msg.Unpack(data); err != nil { + return + } + + if msg.Response { + t.processMDNSResponse(ifaceName, srcIP, msg) + } +} + +func (t *Tendrils) processMDNSResponse(ifaceName string, srcIP net.IP, msg *dns.Msg) { + var hostname string + + allRecords := append(msg.Answer, msg.Extra...) + for _, rr := range allRecords { + switch r := rr.(type) { + case *dns.A: + name := strings.TrimSuffix(r.Hdr.Name, ".local.") + name = strings.TrimSuffix(name, ".") + if name != "" && r.A.Equal(srcIP) { + hostname = name + } + case *dns.AAAA: + continue + case *dns.PTR: + name := strings.TrimSuffix(r.Ptr, ".local.") + name = strings.TrimSuffix(name, ".") + if hostname == "" && name != "" && !strings.HasPrefix(name, "_") { + hostname = name + } + } + } + + if hostname != "" { + if t.DebugMDNS { + log.Printf("[mdns] %s: %s -> %s", ifaceName, srcIP, hostname) + } + t.nodes.Update(nil, nil, []net.IP{srcIP}, "", hostname, "mdns") + } +} + +func (t *Tendrils) runMDNSQuerier(ctx context.Context, iface net.Interface) { + addrs, err := iface.Addrs() + if err != nil { + return + } + + var srcIP net.IP + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { + srcIP = ipnet.IP.To4() + break + } + } + if srcIP == nil { + return + } + + ticker := time.NewTicker(60 * time.Second) + defer ticker.Stop() + + t.sendMDNSQuery(iface.Name, srcIP) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.sendMDNSQuery(iface.Name, srcIP) + } + } +} + +func (t *Tendrils) sendMDNSQuery(ifaceName string, srcIP net.IP) { + conn, err := net.DialUDP("udp4", &net.UDPAddr{IP: srcIP}, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}) + if err != nil { + return + } + defer conn.Close() + + msg := new(dns.Msg) + msg.SetQuestion("_services._dns-sd._udp.local.", dns.TypePTR) + msg.RecursionDesired = false + + data, err := msg.Pack() + if err != nil { + return + } + + conn.Write(data) + + if t.DebugMDNS { + log.Printf("[mdns] %s: sent query", ifaceName) + } +} diff --git a/nodes.go b/nodes.go index a4e81d9..c9cffa7 100644 --- a/nodes.go +++ b/nodes.go @@ -617,6 +617,8 @@ func (n *Nodes) LogAll() { log.Printf("[sigusr1] %s: %v", gm.Group.Name(), memberNames) } } + + n.t.artnet.LogAll() } func (n *Nodes) expireMulticastMemberships() { diff --git a/tendrils.go b/tendrils.go index 04f5a9e..bb71e3d 100644 --- a/tendrils.go +++ b/tendrils.go @@ -13,23 +13,29 @@ import ( type Tendrils struct { activeInterfaces map[string]context.CancelFunc nodes *Nodes + artnet *ArtNetNodes - Interface string - DisableARP bool - DisableLLDP bool - DisableSNMP bool - DisableIGMP bool - LogEvents bool - LogNodes bool - DebugARP bool - DebugLLDP bool - DebugSNMP bool - DebugIGMP bool + Interface string + DisableARP bool + DisableLLDP bool + DisableSNMP bool + DisableIGMP bool + DisableMDNS bool + DisableArtNet bool + LogEvents bool + LogNodes bool + DebugARP bool + DebugLLDP bool + DebugSNMP bool + DebugIGMP bool + DebugMDNS bool + DebugArtNet bool } func New() *Tendrils { t := &Tendrils{ activeInterfaces: map[string]context.CancelFunc{}, + artnet: NewArtNetNodes(), } t.nodes = NewNodes(t) return t @@ -183,4 +189,10 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) { if !t.DisableIGMP { go t.listenIGMP(ctx, iface) } + if !t.DisableMDNS { + go t.listenMDNS(ctx, iface) + } + if !t.DisableArtNet { + go t.listenArtNet(ctx, iface) + } }