add mdns hostname discovery and artnet universe tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
366
artnet.go
Normal file
366
artnet.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
9
go.mod
9
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
|
||||
)
|
||||
|
||||
18
go.sum
18
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=
|
||||
|
||||
149
mdns.go
Normal file
149
mdns.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
2
nodes.go
2
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() {
|
||||
|
||||
12
tendrils.go
12
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user