diff --git a/pkg/mdns/client.go b/pkg/mdns/client.go index a9ea0f1a..6816f919 100644 --- a/pkg/mdns/client.go +++ b/pkg/mdns/client.go @@ -13,7 +13,10 @@ import ( "github.com/miekg/dns" // awesome library for parsing mDNS records ) -const ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol +const ( + ServiceDNSSD = "_services._dns-sd._udp.local." + ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol +) type ServiceEntry struct { Name string `json:"name,omitempty"` @@ -153,6 +156,7 @@ type Browser struct { Service string Addr net.Addr + Nets []*net.IPNet Recv net.PacketConn Sends []net.PacketConn @@ -165,7 +169,7 @@ type Browser struct { // Receiver will get multicast responses on senders requests. func (b *Browser) ListenMulticastUDP() error { // 1. Collect IPv4 interfaces - ip4s, err := InterfacesIP4() + nets, err := IPNets() if err != nil { return err } @@ -182,11 +186,12 @@ func (b *Browser) ListenMulticastUDP() error { ctx := context.Background() - for _, ip4 := range ip4s { - conn, err := lc1.ListenPacket(ctx, "udp4", ip4.String()+":5353") // same port important + for _, ipn := range nets { + conn, err := lc1.ListenPacket(ctx, "udp4", ipn.IP.String()+":5353") // same port important if err != nil { continue } + b.Nets = append(b.Nets, ipn) b.Sends = append(b.Sends, conn) } @@ -365,35 +370,39 @@ func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) { return } -func InterfacesIP4() ([]net.IP, error) { +// Common docker addresses (class B): +// https://en.wikipedia.org/wiki/Private_network +// - docker0 172.17.0.1/16 +// - br-xxxx 172.18.0.1/16 +// - hassio 172.30.32.1/23 +var docker = net.IPNet{ + IP: []byte{172, 16, 0, 0}, + Mask: []byte{255, 240, 0, 0}, +} + +func IPNets() ([]*net.IPNet, error) { intfs, err := net.Interfaces() if err != nil { return nil, err } - var ips []net.IP + var nets []*net.IPNet -loop: for _, intf := range intfs { if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { continue } - addrs, err := intf.Addrs() - if err != nil { - continue - } - + addrs, _ := intf.Addrs() for _, addr := range addrs { switch v := addr.(type) { case *net.IPNet: - if ip := v.IP.To4(); ip != nil { - ips = append(ips, ip) - continue loop + if ip := v.IP.To4(); ip != nil && !docker.Contains(ip) { + nets = append(nets, v) } } } } - return ips, nil + return nets, nil } diff --git a/pkg/mdns/server.go b/pkg/mdns/server.go index ec31886e..802c07cd 100644 --- a/pkg/mdns/server.go +++ b/pkg/mdns/server.go @@ -20,7 +20,11 @@ func Serve(service string, entries []*ServiceEntry) error { } func (b *Browser) Serve(entries []*ServiceEntry) error { - var msg dns.Msg + names := make(map[string]*ServiceEntry, len(entries)) + for _, entry := range entries { + name := entry.name() + "." + b.Service + names[name] = entry + } buf := make([]byte, 1500) for { @@ -29,129 +33,130 @@ func (b *Browser) Serve(entries []*ServiceEntry) error { break } - if err = msg.Unpack(buf[:n]); err != nil { + var req dns.Msg // request + if err = req.Unpack(buf[:n]); err != nil { continue } - if !HasQuestionPTP(&msg, b.Service) { + // skip messages without Questions + if req.Question == nil { continue } remoteIP := addr.(*net.UDPAddr).IP - localIP := MatchLocalIP(remoteIP) + localIP := b.MatchLocalIP(remoteIP) + + // skip messages from unknown networks (can be docker network) if localIP == nil { continue } - answer, err := NewDNSAnswer(entries, b.Service, localIP).Pack() + var res dns.Msg // response + for _, q := range req.Question { + if q.Qtype != dns.TypePTR || q.Qclass != dns.ClassINET { + continue + } + + if q.Name == ServiceDNSSD { + AppendDNSSD(&res, b.Service) + } else if q.Name == b.Service { + for _, entry := range entries { + AppendEntry(&res, entry, b.Service, localIP) + } + } else if entry, ok := names[q.Name]; ok { + AppendEntry(&res, entry, b.Service, localIP) + } + } + + if res.Answer == nil { + continue + } + + res.MsgHdr.Response = true + res.MsgHdr.Authoritative = true + + data, err := res.Pack() if err != nil { continue } for _, send := range b.Sends { - _, _ = send.WriteTo(answer, MulticastAddr) + _, _ = send.WriteTo(data, MulticastAddr) } } return nil } -func HasQuestionPTP(msg *dns.Msg, name string) bool { - for _, q := range msg.Question { - if q.Qtype == dns.TypePTR && q.Name == name { - return true +func (b *Browser) MatchLocalIP(remote net.IP) net.IP { + for _, ipn := range b.Nets { + if ipn.Contains(remote) { + return ipn.IP } } - return false + return nil } -func NewDNSAnswer(entries []*ServiceEntry, service string, ip net.IP) *dns.Msg { - msg := dns.Msg{ - MsgHdr: dns.MsgHdr{ - Response: true, - Authoritative: true, +func AppendDNSSD(msg *dns.Msg, service string) { + msg.Answer = append( + msg.Answer, + &dns.PTR{ + Hdr: dns.RR_Header{ + Name: ServiceDNSSD, // _services._dns-sd._udp.local. + Rrtype: dns.TypePTR, // 12 + Class: dns.ClassINET, // 1 + Ttl: 4500, + }, + Ptr: service, // _home-assistant._tcp.local. }, - } - - for _, entry := range entries { - ptrName := entry.name() + "." + service - srvName := entry.name() + ".local." - - msg.Answer = append( - msg.Answer, - &dns.PTR{ - Hdr: dns.RR_Header{ - Name: service, - Rrtype: dns.TypePTR, - Class: dns.ClassINET, - Ttl: 4500, - }, - Ptr: ptrName, - }, - ) - msg.Extra = append( - msg.Extra, - &dns.TXT{ - Hdr: dns.RR_Header{ - Name: ptrName, - Rrtype: dns.TypeTXT, - Class: ClassCacheFlush, - Ttl: 4500, - }, - Txt: entry.TXT(), - }, - &dns.SRV{ - Hdr: dns.RR_Header{ - Name: ptrName, - Rrtype: dns.TypeSRV, - Class: ClassCacheFlush, - Ttl: 120, - Rdlength: 0, - }, - Port: entry.Port, - Target: srvName, - }, - &dns.A{ - Hdr: dns.RR_Header{ - Name: srvName, - Rrtype: dns.TypeA, - Class: ClassCacheFlush, - Ttl: 120, - Rdlength: 0, - }, - A: ip, - }, - ) - } - - return &msg + ) } -func MatchLocalIP(remote net.IP) net.IP { - intfs, err := net.Interfaces() - if err != nil { - return nil - } +func AppendEntry(msg *dns.Msg, entry *ServiceEntry, service string, ip net.IP) { + ptrName := entry.name() + "." + service + srvName := entry.name() + ".local." - for _, intf := range intfs { - if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { - continue - } - - addrs, err := intf.Addrs() - if err != nil { - continue - } - - for _, addr := range addrs { - switch v := addr.(type) { - case *net.IPNet: - if local := v.IP.To4(); local != nil && v.Contains(remote) { - return local - } - } - } - } - - return nil + msg.Answer = append( + msg.Answer, + &dns.PTR{ + Hdr: dns.RR_Header{ + Name: service, // _home-assistant._tcp.local. + Rrtype: dns.TypePTR, // 12 + Class: dns.ClassINET, // 1 + Ttl: 4500, + }, + Ptr: ptrName, // Home\ Assistant._home-assistant._tcp.local. + }, + ) + msg.Extra = append( + msg.Extra, + &dns.TXT{ + Hdr: dns.RR_Header{ + Name: ptrName, // Home\ Assistant._home-assistant._tcp.local. + Rrtype: dns.TypeTXT, // 16 + Class: ClassCacheFlush, // 32769 + Ttl: 4500, + }, + Txt: entry.TXT(), + }, + &dns.SRV{ + Hdr: dns.RR_Header{ + Name: ptrName, // Home\ Assistant._home-assistant._tcp.local. + Rrtype: dns.TypeSRV, // 33 + Class: ClassCacheFlush, // 32769 + Ttl: 120, + }, + Port: entry.Port, // 8123 + Target: srvName, // 963f1fa82b7142809711cebe7c826322.local. + }, + &dns.A{ + Hdr: dns.RR_Header{ + Name: srvName, // 963f1fa82b7142809711cebe7c826322.local. + Rrtype: dns.TypeA, // 1 + Class: ClassCacheFlush, // 32769 + Ttl: 120, + }, + A: ip, + }, + ) }