2026-01-22 23:59:32 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
mdnsAddr = "224.0.0.251:5353"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-23 09:12:09 -08:00
|
|
|
func isSkaarhojService(s string) bool {
|
|
|
|
|
return strings.Contains(s, "_skaarhoj._tcp")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractSkaarhojName(s string) string {
|
|
|
|
|
idx := strings.Index(s, "._skaarhoj._tcp")
|
|
|
|
|
if idx <= 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.ReplaceAll(s[:idx], "\\", "")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 11:28:56 -08:00
|
|
|
func isNetaudioService(s string) bool {
|
2026-01-25 11:52:28 -08:00
|
|
|
return strings.Contains(s, "_netaudio-cmc._udp") || strings.Contains(s, "_netaudio-arc._udp") || strings.Contains(s, "_netaudio-chan._udp")
|
2026-01-25 11:28:56 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractNetaudioName(s string) string {
|
|
|
|
|
for _, suffix := range []string{"._netaudio-cmc._udp", "._netaudio-arc._udp"} {
|
|
|
|
|
idx := strings.Index(s, suffix)
|
|
|
|
|
if idx > 0 {
|
|
|
|
|
return strings.ReplaceAll(s[:idx], "\\", "")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 11:52:28 -08:00
|
|
|
func extractNetaudioChanDevice(s string) string {
|
|
|
|
|
idx := strings.Index(s, "._netaudio-chan._udp")
|
|
|
|
|
if idx <= 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
name := strings.ReplaceAll(s[:idx], "\\", "")
|
|
|
|
|
if atIdx := strings.Index(name, "@"); atIdx >= 0 {
|
|
|
|
|
return name[atIdx+1:]
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:59:32 -08:00
|
|
|
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()
|
|
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
go t.runMDNSQuerier(ctx, iface, conn)
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
allRecords := append(msg.Answer, msg.Extra...)
|
2026-01-23 00:24:36 -08:00
|
|
|
|
|
|
|
|
if t.DebugMDNS {
|
|
|
|
|
for _, rr := range allRecords {
|
|
|
|
|
log.Printf("[mdns] %s: record %s", ifaceName, rr.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aRecords := map[string]net.IP{}
|
|
|
|
|
srvTargets := map[string]string{}
|
2026-01-23 09:12:09 -08:00
|
|
|
skaarhojNames := map[string]bool{}
|
2026-01-25 11:28:56 -08:00
|
|
|
netaudioNames := map[string]bool{}
|
2026-01-23 00:24:36 -08:00
|
|
|
|
2026-01-22 23:59:32 -08:00
|
|
|
for _, rr := range allRecords {
|
|
|
|
|
switch r := rr.(type) {
|
|
|
|
|
case *dns.A:
|
2026-01-23 00:24:36 -08:00
|
|
|
name := strings.TrimSuffix(r.Hdr.Name, ".")
|
|
|
|
|
aRecords[name] = r.A
|
2026-01-22 23:59:32 -08:00
|
|
|
case *dns.AAAA:
|
|
|
|
|
continue
|
|
|
|
|
case *dns.PTR:
|
2026-01-23 09:12:09 -08:00
|
|
|
if isSkaarhojService(r.Ptr) {
|
|
|
|
|
name := extractSkaarhojName(r.Ptr)
|
|
|
|
|
if name != "" {
|
|
|
|
|
skaarhojNames[name] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 00:24:36 -08:00
|
|
|
case *dns.SRV:
|
2026-01-23 09:12:09 -08:00
|
|
|
target := strings.TrimSuffix(r.Target, ".")
|
|
|
|
|
if isSkaarhojService(r.Hdr.Name) {
|
|
|
|
|
name := extractSkaarhojName(r.Hdr.Name)
|
|
|
|
|
if name != "" {
|
|
|
|
|
skaarhojNames[name] = true
|
|
|
|
|
if target != "" {
|
|
|
|
|
srvTargets[name] = target
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-25 11:28:56 -08:00
|
|
|
if isNetaudioService(r.Hdr.Name) {
|
|
|
|
|
name := extractNetaudioName(r.Hdr.Name)
|
2026-01-25 11:52:28 -08:00
|
|
|
if name == "" {
|
|
|
|
|
name = extractNetaudioChanDevice(r.Hdr.Name)
|
|
|
|
|
}
|
2026-01-25 11:28:56 -08:00
|
|
|
if name != "" {
|
|
|
|
|
netaudioNames[name] = true
|
|
|
|
|
if target != "" {
|
|
|
|
|
srvTargets[name] = target
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 00:24:36 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:12:09 -08:00
|
|
|
for name := range skaarhojNames {
|
|
|
|
|
var ip net.IP
|
|
|
|
|
if target, ok := srvTargets[name]; ok {
|
|
|
|
|
ip = aRecords[target]
|
|
|
|
|
}
|
|
|
|
|
if ip == nil {
|
|
|
|
|
ip = srcIP
|
|
|
|
|
}
|
|
|
|
|
if t.DebugMDNS {
|
|
|
|
|
log.Printf("[mdns] %s: skaarhoj %s -> %s", ifaceName, name, ip)
|
|
|
|
|
}
|
|
|
|
|
t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "skaarhoj")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 11:28:56 -08:00
|
|
|
for name := range netaudioNames {
|
|
|
|
|
var ip net.IP
|
|
|
|
|
var targetHostname string
|
|
|
|
|
if target, ok := srvTargets[name]; ok {
|
|
|
|
|
ip = aRecords[target]
|
|
|
|
|
targetHostname = strings.TrimSuffix(target, ".local")
|
|
|
|
|
}
|
|
|
|
|
if ip == nil {
|
|
|
|
|
ip = srcIP
|
|
|
|
|
}
|
|
|
|
|
if t.DebugMDNS {
|
|
|
|
|
log.Printf("[mdns] %s: netaudio %s -> %s (target %s)", ifaceName, name, ip, targetHostname)
|
|
|
|
|
}
|
|
|
|
|
t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "mdns-srv")
|
|
|
|
|
if targetHostname != "" && targetHostname != name {
|
|
|
|
|
t.nodes.Update(nil, nil, []net.IP{ip}, "", targetHostname, "mdns-srv")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(skaarhojNames) == 0 && len(netaudioNames) == 0 {
|
2026-01-23 00:32:07 -08:00
|
|
|
for aName, ip := range aRecords {
|
|
|
|
|
hostname := strings.TrimSuffix(aName, ".local")
|
2026-01-25 09:30:55 -08:00
|
|
|
if hostname != "" && hostname != aName && !strings.Contains(hostname, "in-addr") && !strings.Contains(hostname, "ip6.arpa") {
|
2026-01-23 00:32:07 -08:00
|
|
|
if t.DebugMDNS {
|
|
|
|
|
log.Printf("[mdns] %s: %s -> %s", ifaceName, ip, hostname)
|
|
|
|
|
}
|
|
|
|
|
t.nodes.Update(nil, nil, []net.IP{ip}, "", hostname, "mdns")
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
var mdnsServices = []string{
|
|
|
|
|
"_services._dns-sd._udp.local.",
|
2026-01-23 23:05:04 -08:00
|
|
|
"_skaarhoj._tcp.local.",
|
2026-01-24 22:43:20 -08:00
|
|
|
"_smb._tcp.local.",
|
|
|
|
|
"_airplay._tcp.local.",
|
|
|
|
|
"_qlab._tcp.local.",
|
|
|
|
|
"_blackmagic._tcp.local.",
|
|
|
|
|
"_hyperdeck_ctrl._tcp.local.",
|
2026-01-25 11:28:56 -08:00
|
|
|
"_netaudio-cmc._udp.local.",
|
|
|
|
|
"_netaudio-arc._udp.local.",
|
2026-01-25 11:52:28 -08:00
|
|
|
"_netaudio-chan._udp.local.",
|
2026-01-23 09:27:45 -08:00
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
func (t *Tendrils) runMDNSQuerier(ctx context.Context, iface net.Interface, conn *net.UDPConn) {
|
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
2026-01-22 23:59:32 -08:00
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
t.sendMDNSQueries(iface.Name, conn)
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
2026-01-23 09:27:45 -08:00
|
|
|
t.sendMDNSQueries(iface.Name, conn)
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
func (t *Tendrils) sendMDNSQueries(ifaceName string, conn *net.UDPConn) {
|
|
|
|
|
dest := &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
for _, service := range mdnsServices {
|
|
|
|
|
msg := new(dns.Msg)
|
|
|
|
|
msg.SetQuestion(service, dns.TypePTR)
|
|
|
|
|
msg.RecursionDesired = false
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
data, err := msg.Pack()
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-23 09:27:45 -08:00
|
|
|
conn.WriteToUDP(data, dest)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
if t.DebugMDNS {
|
2026-01-23 09:27:45 -08:00
|
|
|
log.Printf("[mdns] %s: sent queries for %d services", ifaceName, len(mdnsServices))
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|