e2e24c7578
Unicast mDNS queries (direct to IP:5353) are ignored by some HomeKit devices. Switch to multicast (224.0.0.251:5353) and filter responses by sender IP. Also consider mDNS response as reachability signal. Split probe timeouts: 100ms for ports/DNS/HTTP, 120ms total to give mDNS extra time. HomeKit responds in ~0.2ms via multicast.
148 lines
2.8 KiB
Go
148 lines
2.8 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"
|
|
)
|
|
|
|
var multicastAddr = &net.UDPAddr{IP: net.IP{224, 0, 0, 251}, Port: 5353}
|
|
|
|
// QueryHAP sends multicast mDNS query for HomeKit service and waits
|
|
// for a response from the specified ip. 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.ListenMulticastUDP("udp4", nil, multicastAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
deadline = time.Now().Add(time.Second)
|
|
}
|
|
_ = conn.SetDeadline(deadline)
|
|
|
|
if _, err = conn.WriteTo(query, multicastAddr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targetIP := net.ParseIP(ip)
|
|
buf := make([]byte, 1500)
|
|
|
|
for {
|
|
n, from, err := conn.ReadFrom(buf)
|
|
if err != nil {
|
|
return nil, nil // timeout
|
|
}
|
|
|
|
if !from.(*net.UDPAddr).IP.Equal(targetIP) {
|
|
continue
|
|
}
|
|
|
|
var resp dns.Msg
|
|
if err = resp.Unpack(buf[:n]); err != nil {
|
|
continue
|
|
}
|
|
|
|
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 }
|
|
}
|