Files
Strix/pkg/probe/mdns.go
T
eduard256 27117900eb Rewrite Strix from scratch as single binary
Complete architecture rewrite following go2rtc patterns:
- pkg/ for pure logic (camdb, tester, probe, generate)
- internal/ for application glue with Init() modules
- Single HTTP server on :4567 with all endpoints
- zerolog with password masking and memory ring buffer
- Environment-based config only (no YAML files)

API endpoints: /api/search, /api/streams, /api/test,
/api/probe, /api/generate, /api/health, /api/log

Dependencies: go2rtc v1.9.14, go-sqlite3, miekg/dns, zerolog
2026-03-25 10:38:46 +00:00

138 lines
2.7 KiB
Go

package probe
import (
"context"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
const (
hapService = "_hap._tcp.local."
txtCategory = "ci"
txtDeviceID = "id"
txtModel = "md"
txtStatusFlags = "sf"
statusPaired = "0"
categoryCamera = "17"
categoryDoorbell = "18"
)
// QueryHAP sends unicast mDNS query to ip:5353 for HomeKit service.
// Returns nil if device is not a HomeKit camera/doorbell.
func QueryHAP(ctx context.Context, ip string) (*MDNSResult, error) {
msg := &dns.Msg{
Question: []dns.Question{
{Name: hapService, Qtype: dns.TypePTR, Qclass: dns.ClassINET},
},
}
query, err := msg.Pack()
if err != nil {
return nil, err
}
conn, err := net.ListenPacket("udp4", ":0")
if err != nil {
return nil, err
}
defer conn.Close()
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(100 * time.Millisecond)
}
_ = conn.SetDeadline(deadline)
addr := &net.UDPAddr{IP: net.ParseIP(ip), Port: 5353}
if _, err = conn.WriteTo(query, addr); err != nil {
return nil, err
}
buf := make([]byte, 1500)
n, _, err := conn.ReadFrom(buf)
if err != nil {
return nil, nil // timeout = not a HomeKit device
}
var resp dns.Msg
if err = resp.Unpack(buf[:n]); err != nil {
return nil, nil
}
return parseHAPResponse(&resp)
}
// internals
func parseHAPResponse(msg *dns.Msg) (*MDNSResult, error) {
records := make([]dns.RR, 0, len(msg.Answer)+len(msg.Extra))
records = append(records, msg.Answer...)
records = append(records, msg.Extra...)
var ptrName string
for _, rr := range records {
if ptr, ok := rr.(*dns.PTR); ok && ptr.Hdr.Name == hapService {
ptrName = ptr.Ptr
break
}
}
if ptrName == "" {
return nil, nil
}
// ex. "My Camera._hap._tcp.local." -> "My Camera"
var name string
if i := strings.Index(ptrName, "."+hapService); i > 0 {
name = strings.ReplaceAll(ptrName[:i], `\ `, " ")
}
info := map[string]string{}
for _, rr := range records {
txt, ok := rr.(*dns.TXT)
if !ok || txt.Hdr.Name != ptrName {
continue
}
for _, s := range txt.Txt {
k, v, _ := strings.Cut(s, "=")
info[k] = v
}
break
}
category := info[txtCategory]
if category != categoryCamera && category != categoryDoorbell {
return nil, nil
}
categoryName := "camera"
if category == categoryDoorbell {
categoryName = "doorbell"
}
var port int
for _, rr := range records {
if srv, ok := rr.(*dns.SRV); ok && srv.Hdr.Name == ptrName {
port = int(srv.Port)
break
}
}
return &MDNSResult{
Name: name,
DeviceID: info[txtDeviceID],
Model: info[txtModel],
Category: categoryName,
Paired: info[txtStatusFlags] == statusPaired,
Port: port,
}, nil
}
func init() {
dns.Id = func() uint16 { return 0 }
}