10 "golang.org/x/net/ipv4"
11 "golang.org/x/net/ipv6"
15 "github.com/cenkalti/backoff"
16 "github.com/miekg/dns"
19 // IPType specifies the IP traffic the client listens for.
20 // This does not guarantee that only mDNS entries of this sepcific
21 // type passes. E.g. typical mDNS packets distributed via IPv4, often contain
22 // both DNS A and AAAA entries.
25 // Options for IPType.
29 IPv4AndIPv6 = (IPv4 | IPv6) //< Default option.
32 type clientOpts struct {
34 ifaces []net.Interface
37 // ClientOption fills the option struct to configure intefaces, etc.
38 type ClientOption func(*clientOpts)
40 // SelectIPTraffic selects the type of IP packets (IPv4, IPv6, or both) this
41 // instance listens for.
42 // This does not guarantee that only mDNS entries of this sepcific
43 // type passes. E.g. typical mDNS packets distributed via IPv4, may contain
44 // both DNS A and AAAA entries.
45 func SelectIPTraffic(t IPType) ClientOption {
46 return func(o *clientOpts) {
51 // SelectIfaces selects the interfaces to query for mDNS records
52 func SelectIfaces(ifaces []net.Interface) ClientOption {
53 return func(o *clientOpts) {
58 // Resolver acts as entry point for service lookups and to browse the DNS-SD.
59 type Resolver struct {
63 // NewResolver creates a new resolver and joins the UDP multicast groups to
64 // listen for mDNS messages.
65 func NewResolver(options ...ClientOption) (*Resolver, error) {
66 // Apply default configuration and load supplied options.
67 var conf = clientOpts{
68 listenOn: IPv4AndIPv6,
70 for _, o := range options {
76 c, err := newClient(conf)
85 // Browse for all services of a given type in a given domain.
86 func (r *Resolver) Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry) error {
87 params := defaultParams(service)
89 params.Domain = domain
91 params.Entries = entries
92 ctx, cancel := context.WithCancel(ctx)
93 go r.c.mainloop(ctx, params)
95 err := r.c.query(params)
100 // If previous probe was ok, it should be fine now. In case of an error later on,
101 // the entries' queue is closed.
103 if err := r.c.periodicQuery(ctx, params); err != nil {
111 // Lookup a specific service by its name and type in a given domain.
112 func (r *Resolver) Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry) error {
113 params := defaultParams(service)
114 params.Instance = instance
116 params.Domain = domain
118 params.Entries = entries
119 ctx, cancel := context.WithCancel(ctx)
120 go r.c.mainloop(ctx, params)
121 err := r.c.query(params)
127 // If previous probe was ok, it should be fine now. In case of an error later on,
128 // the entries' queue is closed.
130 if err := r.c.periodicQuery(ctx, params); err != nil {
138 // defaultParams returns a default set of QueryParams.
139 func defaultParams(service string) *LookupParams {
140 return NewLookupParams("", service, "local", make(chan *ServiceEntry))
143 // Client structure encapsulates both IPv4/IPv6 UDP connections.
145 ipv4conn *ipv4.PacketConn
146 ipv6conn *ipv6.PacketConn
147 ifaces []net.Interface
150 // Client structure constructor
151 func newClient(opts clientOpts) (*client, error) {
152 ifaces := opts.ifaces
153 if len(ifaces) == 0 {
154 ifaces = listMulticastInterfaces()
157 var ipv4conn *ipv4.PacketConn
158 if (opts.listenOn & IPv4) > 0 {
160 ipv4conn, err = joinUdp4Multicast(ifaces)
166 var ipv6conn *ipv6.PacketConn
167 if (opts.listenOn & IPv6) > 0 {
169 ipv6conn, err = joinUdp6Multicast(ifaces)
182 // Start listeners and waits for the shutdown signal from exit channel
183 func (c *client) mainloop(ctx context.Context, params *LookupParams) {
184 // start listening for responses
185 msgCh := make(chan *dns.Msg, 32)
186 if c.ipv4conn != nil {
187 go c.recv(ctx, c.ipv4conn, msgCh)
189 if c.ipv6conn != nil {
190 go c.recv(ctx, c.ipv6conn, msgCh)
193 // Iterate through channels from listeners goroutines
194 var entries, sentEntries map[string]*ServiceEntry
195 sentEntries = make(map[string]*ServiceEntry)
199 // Context expired. Notify subscriber that we are done here.
204 entries = make(map[string]*ServiceEntry)
205 sections := append(msg.Answer, msg.Ns...)
206 sections = append(sections, msg.Extra...)
208 for _, answer := range sections {
209 switch rr := answer.(type) {
211 if params.ServiceName() != rr.Hdr.Name {
214 if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Ptr {
217 if _, ok := entries[rr.Ptr]; !ok {
218 entries[rr.Ptr] = NewServiceEntry(
219 trimDot(strings.Replace(rr.Ptr, rr.Hdr.Name, "", -1)),
223 entries[rr.Ptr].TTL = rr.Hdr.Ttl
225 if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
227 } else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
230 if _, ok := entries[rr.Hdr.Name]; !ok {
231 entries[rr.Hdr.Name] = NewServiceEntry(
232 trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
236 entries[rr.Hdr.Name].HostName = rr.Target
237 entries[rr.Hdr.Name].Port = int(rr.Port)
238 entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl
240 if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
242 } else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
245 if _, ok := entries[rr.Hdr.Name]; !ok {
246 entries[rr.Hdr.Name] = NewServiceEntry(
247 trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
251 entries[rr.Hdr.Name].Text = rr.Txt
252 entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl
255 // Associate IPs in a second round as other fields should be filled by now.
256 for _, answer := range sections {
257 switch rr := answer.(type) {
259 for k, e := range entries {
260 if e.HostName == rr.Hdr.Name {
261 entries[k].AddrIPv4 = append(entries[k].AddrIPv4, rr.A)
265 for k, e := range entries {
266 if e.HostName == rr.Hdr.Name {
267 entries[k].AddrIPv6 = append(entries[k].AddrIPv6, rr.AAAA)
274 if len(entries) > 0 {
275 for k, e := range entries {
278 delete(sentEntries, k)
281 if _, ok := sentEntries[k]; ok {
285 // If this is an DNS-SD query do not throw PTR away.
286 // It is expected to have only PTR for enumeration
287 if params.ServiceRecord.ServiceTypeName() != params.ServiceRecord.ServiceName() {
288 // Require at least one resolved IP address for ServiceEntry
289 // TODO: wait some more time as chances are high both will arrive.
290 if len(e.AddrIPv4) == 0 && len(e.AddrIPv6) == 0 {
294 // Submit entry to subscriber and cache it.
295 // This is also a point to possibly stop probing actively for a
299 params.disableProbing()
302 entries = make(map[string]*ServiceEntry)
307 // Shutdown client will close currently open connections and channel implicitly.
308 func (c *client) shutdown() {
309 if c.ipv4conn != nil {
312 if c.ipv6conn != nil {
317 // Data receiving routine reads from connection, unpacks packets into dns.Msg
318 // structures and sends them to a given msgCh channel
319 func (c *client) recv(ctx context.Context, l interface{}, msgCh chan *dns.Msg) {
320 var readFrom func([]byte) (n int, src net.Addr, err error)
322 switch pConn := l.(type) {
323 case *ipv6.PacketConn:
324 readFrom = func(b []byte) (n int, src net.Addr, err error) {
325 n, _, src, err = pConn.ReadFrom(b)
328 case *ipv4.PacketConn:
329 readFrom = func(b []byte) (n int, src net.Addr, err error) {
330 n, _, src, err = pConn.ReadFrom(b)
338 buf := make([]byte, 65536)
341 // Handles the following cases:
342 // - ReadFrom aborts with error due to closed UDP connection -> causes ctx cancel
343 // - ReadFrom aborts otherwise.
344 // TODO: the context check can be removed. Verify!
345 if ctx.Err() != nil || fatalErr != nil {
349 n, _, err := readFrom(buf)
355 if err := msg.Unpack(buf[:n]); err != nil {
356 // log.Printf("[WARN] mdns: Failed to unpack packet: %v", err)
361 // Submit decoded DNS message and continue.
369 // periodicQuery sens multiple probes until a valid response is received by
370 // the main processing loop or some timeout/cancel fires.
371 // TODO: move error reporting to shutdown function as periodicQuery is called from
372 // go routine context.
373 func (c *client) periodicQuery(ctx context.Context, params *LookupParams) error {
374 if params.stopProbing == nil {
378 bo := backoff.NewExponentialBackOff()
379 bo.InitialInterval = 4 * time.Second
380 bo.MaxInterval = 60 * time.Second
384 // Do periodic query.
385 if err := c.query(params); err != nil {
388 // Backoff and cancel logic.
389 wait := bo.NextBackOff()
390 if wait == backoff.Stop {
391 log.Println("periodicQuery: abort due to timeout")
395 case <-time.After(wait):
396 // Wait for next iteration.
397 case <-params.stopProbing:
398 // Chan is closed (or happened in the past).
399 // Done here. Received a matching mDNS entry.
409 // Performs the actual query by service name (browse) or service instance name (lookup),
410 // start response listeners goroutines and loops over the entries channel.
411 func (c *client) query(params *LookupParams) error {
412 var serviceName, serviceInstanceName string
413 serviceName = fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain))
414 if params.Instance != "" {
415 serviceInstanceName = fmt.Sprintf("%s.%s", params.Instance, serviceName)
420 if serviceInstanceName != "" {
421 m.Question = []dns.Question{
422 dns.Question{serviceInstanceName, dns.TypeSRV, dns.ClassINET},
423 dns.Question{serviceInstanceName, dns.TypeTXT, dns.ClassINET},
425 m.RecursionDesired = false
427 m.SetQuestion(serviceName, dns.TypePTR)
428 m.RecursionDesired = false
430 if err := c.sendQuery(m); err != nil {
437 // Pack the dns.Msg and write to available connections (multicast)
438 func (c *client) sendQuery(msg *dns.Msg) error {
439 buf, err := msg.Pack()
443 if c.ipv4conn != nil {
444 var wcm ipv4.ControlMessage
445 for ifi := range c.ifaces {
446 wcm.IfIndex = c.ifaces[ifi].Index
447 c.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
450 if c.ipv6conn != nil {
451 var wcm ipv6.ControlMessage
452 for ifi := range c.ifaces {
453 wcm.IfIndex = c.ifaces[ifi].Index
454 c.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)