• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

enbility / zeroconf / 22303507317

23 Feb 2026 11:07AM UTC coverage: 84.545% (+15.0%) from 69.498%
22303507317

push

github

web-flow
Merge pull request #3 from enbility/feature/v3-interfaces-and-testability

v3 refactoring with interface-based testability

150 of 216 new or added lines in 6 files covered. (69.44%)

2 existing lines in 1 file now uncovered.

930 of 1100 relevant lines covered (84.55%)

47.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.57
/server.go
1
package zeroconf
2

3
import (
4
        "fmt"
5
        "log"
6
        "math/rand"
7
        "net"
8
        "os"
9
        "strings"
10
        "sync"
11
        "time"
12

13
        "github.com/enbility/zeroconf/v3/api"
14
        "github.com/miekg/dns"
15
)
16

17
const (
18
        // Number of Multicast responses sent for a query message (default: 1 < x < 9)
19
        multicastRepetitions = 2
20
)
21

22
var defaultTTL uint32 = 3200
23

24
type serverOpts struct {
25
        ttl         uint32
26
        connFactory api.ConnectionFactory
27
}
28

29
func applyServerOpts(options ...ServerOption) serverOpts {
9✔
30
        // Apply default configuration and load supplied options.
9✔
31
        var conf = serverOpts{
9✔
32
                ttl: defaultTTL,
9✔
33
        }
9✔
34
        for _, o := range options {
12✔
35
                if o != nil {
6✔
36
                        o(&conf)
3✔
37
                }
3✔
38
        }
39
        return conf
9✔
40
}
41

42
// ServerOption fills the option struct.
43
type ServerOption func(*serverOpts)
44

45
// TTL sets the TTL for DNS replies.
46
func TTL(ttl uint32) ServerOption {
1✔
47
        return func(o *serverOpts) {
2✔
48
                o.ttl = ttl
1✔
49
        }
1✔
50
}
51

52
// WithServerConnFactory sets a custom connection factory for the server.
53
// This is primarily useful for testing with mock connections.
54
func WithServerConnFactory(factory api.ConnectionFactory) ServerOption {
2✔
55
        return func(o *serverOpts) {
4✔
56
                o.connFactory = factory
2✔
57
        }
2✔
58
}
59

60
// Register a service by given arguments. This call will take the system's hostname
61
// and lookup IP by that hostname.
62
func Register(instance, service, domain string, port int, text []string, ifaces []net.Interface, opts ...ServerOption) (*Server, error) {
5✔
63
        entry := newServiceEntry(instance, service, domain)
5✔
64
        entry.Port = port
5✔
65
        entry.Text = text
5✔
66

5✔
67
        if entry.Instance == "" {
5✔
68
                return nil, fmt.Errorf("missing service instance name")
×
69
        }
×
70
        if entry.Service == "" {
5✔
71
                return nil, fmt.Errorf("missing service name")
×
72
        }
×
73
        if entry.Domain == "" {
5✔
74
                entry.Domain = "local."
×
75
        }
×
76
        if entry.Port == 0 {
5✔
77
                return nil, fmt.Errorf("missing port")
×
78
        }
×
79

80
        var err error
5✔
81
        if entry.HostName == "" {
10✔
82
                entry.HostName, err = os.Hostname()
5✔
83
                if err != nil {
5✔
84
                        return nil, fmt.Errorf("could not determine host")
×
85
                }
×
86
        }
87

88
        if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
10✔
89
                entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
5✔
90
        }
5✔
91

92
        if len(ifaces) == 0 {
10✔
93
                ifaces = NewInterfaceProvider().MulticastInterfaces()
5✔
94
        }
5✔
95

96
        for _, iface := range ifaces {
20✔
97
                v4, v6 := addrsForInterface(&iface)
15✔
98
                entry.AddrIPv4 = append(entry.AddrIPv4, v4...)
15✔
99
                entry.AddrIPv6 = append(entry.AddrIPv6, v6...)
15✔
100
        }
15✔
101

102
        if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil {
5✔
103
                return nil, fmt.Errorf("could not determine host IP addresses")
×
104
        }
×
105

106
        s, err := newServer(ifaces, applyServerOpts(opts...))
5✔
107
        if err != nil {
5✔
108
                return nil, err
×
109
        }
×
110

111
        s.service = entry
5✔
112
        s.start()
5✔
113

5✔
114
        return s, nil
5✔
115
}
116

117
// RegisterProxy registers a service proxy. This call will skip the hostname/IP lookup and
118
// will use the provided values.
119
func RegisterProxy(instance, service, domain string, port int, host string, ips []string, text []string, ifaces []net.Interface, opts ...ServerOption) (*Server, error) {
6✔
120
        entry := newServiceEntry(instance, service, domain)
6✔
121
        entry.Port = port
6✔
122
        entry.Text = text
6✔
123
        entry.HostName = host
6✔
124

6✔
125
        if entry.Instance == "" {
7✔
126
                return nil, fmt.Errorf("missing service instance name")
1✔
127
        }
1✔
128
        if entry.Service == "" {
6✔
129
                return nil, fmt.Errorf("missing service name")
1✔
130
        }
1✔
131
        if entry.HostName == "" {
5✔
132
                return nil, fmt.Errorf("missing host name")
1✔
133
        }
1✔
134
        if entry.Domain == "" {
3✔
135
                entry.Domain = "local"
×
136
        }
×
137
        if entry.Port == 0 {
4✔
138
                return nil, fmt.Errorf("missing port")
1✔
139
        }
1✔
140

141
        if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
4✔
142
                entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
2✔
143
        }
2✔
144

145
        for _, ip := range ips {
5✔
146
                ipAddr := net.ParseIP(ip)
3✔
147
                if ipAddr == nil {
4✔
148
                        return nil, fmt.Errorf("failed to parse given IP: %v", ip)
1✔
149
                } else if ipv4 := ipAddr.To4(); ipv4 != nil {
4✔
150
                        entry.AddrIPv4 = append(entry.AddrIPv4, ipAddr)
1✔
151
                } else if ipv6 := ipAddr.To16(); ipv6 != nil {
3✔
152
                        entry.AddrIPv6 = append(entry.AddrIPv6, ipAddr)
1✔
153
                } else {
1✔
154
                        return nil, fmt.Errorf("the IP is neither IPv4 nor IPv6: %#v", ipAddr)
×
155
                }
×
156
        }
157

158
        if len(ifaces) == 0 {
1✔
NEW
159
                ifaces = NewInterfaceProvider().MulticastInterfaces()
×
160
        }
×
161

162
        s, err := newServer(ifaces, applyServerOpts(opts...))
1✔
163
        if err != nil {
1✔
164
                return nil, err
×
165
        }
×
166

167
        s.service = entry
1✔
168
        s.start()
1✔
169

1✔
170
        return s, nil
1✔
171
}
172

173
const (
174
        qClassCacheFlush uint16 = 1 << 15
175
)
176

177
// Server structure encapsulates both IPv4/IPv6 UDP connections
178
type Server struct {
179
        service  *ServiceEntry
180
        ipv4conn api.PacketConn
181
        ipv6conn api.PacketConn
182
        ifaces   []net.Interface
183

184
        shouldShutdown chan struct{}
185
        shutdownLock   sync.Mutex
186
        refCount       sync.WaitGroup
187
        isShutdown     bool
188
        ttl            uint32
189
}
190

191
// Constructs server structure
192
func newServer(ifaces []net.Interface, opts serverOpts) (*Server, error) {
6✔
193
        factory := opts.connFactory
6✔
194
        if factory == nil {
11✔
195
                factory = NewConnectionFactory()
5✔
196
        }
5✔
197

198
        ipv4conn, err4 := factory.CreateIPv4Conn(ifaces)
6✔
199
        if err4 != nil {
6✔
200
                log.Printf("[zeroconf] no suitable IPv4 interface: %s", err4.Error())
×
201
        }
×
202
        ipv6conn, err6 := factory.CreateIPv6Conn(ifaces)
6✔
203
        if err6 != nil {
6✔
204
                log.Printf("[zeroconf] no suitable IPv6 interface: %s", err6.Error())
×
205
        }
×
206
        if err4 != nil && err6 != nil {
6✔
207
                // No supported interface left.
×
208
                return nil, fmt.Errorf("no supported interface")
×
209
        }
×
210

211
        s := &Server{
6✔
212
                ipv4conn:       ipv4conn,
6✔
213
                ipv6conn:       ipv6conn,
6✔
214
                ifaces:         ifaces,
6✔
215
                ttl:            opts.ttl,
6✔
216
                shouldShutdown: make(chan struct{}),
6✔
217
        }
6✔
218

6✔
219
        return s, nil
6✔
220
}
221

222
func (s *Server) start() {
6✔
223
        if s.ipv4conn != nil {
12✔
224
                s.refCount.Add(1)
6✔
225
                go s.recvLoop(s.ipv4conn)
6✔
226
        }
6✔
227
        if s.ipv6conn != nil {
12✔
228
                s.refCount.Add(1)
6✔
229
                go s.recvLoop(s.ipv6conn)
6✔
230
        }
6✔
231
        s.refCount.Add(1)
6✔
232
        go s.probe()
6✔
233
}
234

235
// SetText updates and announces the TXT records
236
func (s *Server) SetText(text []string) {
1✔
237
        s.service.Text = text
1✔
238
        s.announceText()
1✔
239
}
1✔
240

241
// Shutdown closes all udp connections and unregisters the service
242
func (s *Server) Shutdown() {
7✔
243
        s.shutdownLock.Lock()
7✔
244
        defer s.shutdownLock.Unlock()
7✔
245
        if s.isShutdown {
7✔
246
                return
×
247
        }
×
248

249
        if err := s.unregister(); err != nil {
7✔
250
                log.Printf("failed to unregister: %s", err)
×
251
        }
×
252

253
        close(s.shouldShutdown)
7✔
254

7✔
255
        if s.ipv4conn != nil {
14✔
256
                s.ipv4conn.Close()
7✔
257
        }
7✔
258
        if s.ipv6conn != nil {
14✔
259
                s.ipv6conn.Close()
7✔
260
        }
7✔
261

262
        // Wait for connection and routines to be closed
263
        s.refCount.Wait()
7✔
264
        s.isShutdown = true
7✔
265
}
266

267
// recvLoop is a long running routine to receive packets from a connection.
268
// It uses the PacketConn interface, allowing for mock injection in tests.
269
func (s *Server) recvLoop(c api.PacketConn) {
13✔
270
        defer s.refCount.Done()
13✔
271
        if c == nil {
13✔
272
                return
×
273
        }
×
274
        buf := make([]byte, 65536)
13✔
275
        for {
260✔
276
                select {
247✔
277
                case <-s.shouldShutdown:
6✔
278
                        return
6✔
279
                default:
241✔
280
                        n, ifIndex, from, err := c.ReadFrom(buf)
241✔
281
                        if err != nil {
251✔
282
                                // Backoff to prevent CPU spin on persistent errors
10✔
283
                                select {
10✔
284
                                case <-s.shouldShutdown:
7✔
285
                                        return
7✔
286
                                case <-time.After(50 * time.Millisecond):
3✔
287
                                        continue
3✔
288
                                }
289
                        }
290
                        _ = s.parsePacket(buf[:n], ifIndex, from)
231✔
291
                }
292
        }
293
}
294

295
// parsePacket is used to parse an incoming packet
296
func (s *Server) parsePacket(packet []byte, ifIndex int, from net.Addr) error {
231✔
297
        var msg dns.Msg
231✔
298
        if err := msg.Unpack(packet); err != nil {
231✔
299
                // log.Printf("[ERR] zeroconf: Failed to unpack packet: %v", err)
×
300
                return err
×
301
        }
×
302
        return s.handleQuery(&msg, ifIndex, from)
231✔
303
}
304

305
// handleQuery is used to handle an incoming query
306
func (s *Server) handleQuery(query *dns.Msg, ifIndex int, from net.Addr) error {
232✔
307
        // Ignore questions with authoritative section for now
232✔
308
        if len(query.Ns) > 0 {
292✔
309
                return nil
60✔
310
        }
60✔
311

312
        // Handle each question
313
        var err error
172✔
314
        for _, q := range query.Question {
218✔
315
                resp := dns.Msg{}
46✔
316
                resp.SetReply(query)
46✔
317
                resp.Compress = true
46✔
318
                resp.RecursionDesired = false
46✔
319
                resp.Authoritative = true
46✔
320
                resp.Question = nil // RFC6762 section 6 "responses MUST NOT contain any questions"
46✔
321
                resp.Answer = []dns.RR{}
46✔
322
                resp.Extra = []dns.RR{}
46✔
323
                if err = s.handleQuestion(q, &resp, query, ifIndex); err != nil {
46✔
324
                        // log.Printf("[ERR] zeroconf: failed to handle question %v: %v", q, err)
×
325
                        continue
×
326
                }
327
                // Check if there is an answer
328
                if len(resp.Answer) == 0 {
51✔
329
                        continue
5✔
330
                }
331

332
                if isUnicastQuestion(q) {
41✔
333
                        // Send unicast
×
334
                        if e := s.unicastResponse(&resp, ifIndex, from); e != nil {
×
335
                                err = e
×
336
                        }
×
337
                } else {
41✔
338
                        // Send mulicast
41✔
339
                        if e := s.multicastResponse(&resp, ifIndex); e != nil {
41✔
340
                                err = e
×
341
                        }
×
342
                }
343
        }
344

345
        return err
172✔
346
}
347

348
// RFC6762 7.1. Known-Answer Suppression
349
func isKnownAnswer(resp *dns.Msg, query *dns.Msg) bool {
51✔
350
        if len(resp.Answer) == 0 || len(query.Answer) == 0 {
97✔
351
                return false
46✔
352
        }
46✔
353

354
        if resp.Answer[0].Header().Rrtype != dns.TypePTR {
6✔
355
                return false
1✔
356
        }
1✔
357
        answer := resp.Answer[0].(*dns.PTR)
4✔
358

4✔
359
        for _, known := range query.Answer {
8✔
360
                hdr := known.Header()
4✔
361
                if hdr.Rrtype != answer.Hdr.Rrtype {
4✔
362
                        continue
×
363
                }
364
                ptr := known.(*dns.PTR)
4✔
365
                if ptr.Ptr == answer.Ptr && hdr.Ttl >= answer.Hdr.Ttl/2 {
6✔
366
                        // log.Printf("skipping known answer: %v", ptr)
2✔
367
                        return true
2✔
368
                }
2✔
369
        }
370

371
        return false
2✔
372
}
373

374
// handleQuestion is used to handle an incoming question
375
func (s *Server) handleQuestion(q dns.Question, resp *dns.Msg, query *dns.Msg, ifIndex int) error {
53✔
376
        if s.service == nil {
54✔
377
                return nil
1✔
378
        }
1✔
379

380
        switch q.Name {
52✔
381
        case s.service.ServiceTypeName():
1✔
382
                s.serviceTypeName(resp, s.ttl)
1✔
383
                if isKnownAnswer(resp, query) {
1✔
384
                        resp.Answer = nil
×
385
                }
×
386

387
        case s.service.ServiceName():
43✔
388
                s.composeBrowsingAnswers(resp, ifIndex)
43✔
389
                if isKnownAnswer(resp, query) {
44✔
390
                        resp.Answer = nil
1✔
391
                }
1✔
392

393
        case s.service.ServiceInstanceName():
1✔
394
                s.composeLookupAnswers(resp, s.ttl, ifIndex, false)
1✔
395
        default:
7✔
396
                // handle matching subtype query
7✔
397
                for _, subtype := range s.service.Subtypes {
13✔
398
                        subtype = fmt.Sprintf("%s._sub.%s", subtype, s.service.ServiceName())
6✔
399
                        if q.Name == subtype {
7✔
400
                                s.composeBrowsingAnswers(resp, ifIndex)
1✔
401
                                if isKnownAnswer(resp, query) {
1✔
402
                                        resp.Answer = nil
×
403
                                }
×
404
                                break
1✔
405
                        }
406
                }
407
        }
408

409
        return nil
52✔
410
}
411

412
func (s *Server) composeBrowsingAnswers(resp *dns.Msg, ifIndex int) {
44✔
413
        ptr := &dns.PTR{
44✔
414
                Hdr: dns.RR_Header{
44✔
415
                        Name:   s.service.ServiceName(),
44✔
416
                        Rrtype: dns.TypePTR,
44✔
417
                        Class:  dns.ClassINET,
44✔
418
                        Ttl:    s.ttl,
44✔
419
                },
44✔
420
                Ptr: s.service.ServiceInstanceName(),
44✔
421
        }
44✔
422
        resp.Answer = append(resp.Answer, ptr)
44✔
423

44✔
424
        txt := &dns.TXT{
44✔
425
                Hdr: dns.RR_Header{
44✔
426
                        Name:   s.service.ServiceInstanceName(),
44✔
427
                        Rrtype: dns.TypeTXT,
44✔
428
                        Class:  dns.ClassINET,
44✔
429
                        Ttl:    s.ttl,
44✔
430
                },
44✔
431
                Txt: s.service.Text,
44✔
432
        }
44✔
433
        srv := &dns.SRV{
44✔
434
                Hdr: dns.RR_Header{
44✔
435
                        Name:   s.service.ServiceInstanceName(),
44✔
436
                        Rrtype: dns.TypeSRV,
44✔
437
                        Class:  dns.ClassINET,
44✔
438
                        Ttl:    s.ttl,
44✔
439
                },
44✔
440
                Priority: 0,
44✔
441
                Weight:   0,
44✔
442
                Port:     uint16(s.service.Port),
44✔
443
                Target:   s.service.HostName,
44✔
444
        }
44✔
445
        resp.Extra = append(resp.Extra, srv, txt)
44✔
446

44✔
447
        resp.Extra = s.appendAddrs(resp.Extra, s.ttl, ifIndex, false)
44✔
448
}
44✔
449

450
func (s *Server) composeLookupAnswers(resp *dns.Msg, ttl uint32, ifIndex int, flushCache bool) {
32✔
451
        // From RFC6762
32✔
452
        //    The most significant bit of the rrclass for a record in the Answer
32✔
453
        //    Section of a response message is the Multicast DNS cache-flush bit
32✔
454
        //    and is discussed in more detail below in Section 10.2, "Announcements
32✔
455
        //    to Flush Outdated Cache Entries".
32✔
456
        ptr := &dns.PTR{
32✔
457
                Hdr: dns.RR_Header{
32✔
458
                        Name:   s.service.ServiceName(),
32✔
459
                        Rrtype: dns.TypePTR,
32✔
460
                        Class:  dns.ClassINET,
32✔
461
                        Ttl:    ttl,
32✔
462
                },
32✔
463
                Ptr: s.service.ServiceInstanceName(),
32✔
464
        }
32✔
465
        srv := &dns.SRV{
32✔
466
                Hdr: dns.RR_Header{
32✔
467
                        Name:   s.service.ServiceInstanceName(),
32✔
468
                        Rrtype: dns.TypeSRV,
32✔
469
                        Class:  dns.ClassINET | qClassCacheFlush,
32✔
470
                        Ttl:    ttl,
32✔
471
                },
32✔
472
                Priority: 0,
32✔
473
                Weight:   0,
32✔
474
                Port:     uint16(s.service.Port),
32✔
475
                Target:   s.service.HostName,
32✔
476
        }
32✔
477
        txt := &dns.TXT{
32✔
478
                Hdr: dns.RR_Header{
32✔
479
                        Name:   s.service.ServiceInstanceName(),
32✔
480
                        Rrtype: dns.TypeTXT,
32✔
481
                        Class:  dns.ClassINET | qClassCacheFlush,
32✔
482
                        Ttl:    ttl,
32✔
483
                },
32✔
484
                Txt: s.service.Text,
32✔
485
        }
32✔
486
        dnssd := &dns.PTR{
32✔
487
                Hdr: dns.RR_Header{
32✔
488
                        Name:   s.service.ServiceTypeName(),
32✔
489
                        Rrtype: dns.TypePTR,
32✔
490
                        Class:  dns.ClassINET,
32✔
491
                        Ttl:    ttl,
32✔
492
                },
32✔
493
                Ptr: s.service.ServiceName(),
32✔
494
        }
32✔
495
        resp.Answer = append(resp.Answer, srv, txt, ptr, dnssd)
32✔
496

32✔
497
        for _, subtype := range s.service.Subtypes {
53✔
498
                resp.Answer = append(resp.Answer,
21✔
499
                        &dns.PTR{
21✔
500
                                Hdr: dns.RR_Header{
21✔
501
                                        Name:   subtype,
21✔
502
                                        Rrtype: dns.TypePTR,
21✔
503
                                        Class:  dns.ClassINET,
21✔
504
                                        Ttl:    ttl,
21✔
505
                                },
21✔
506
                                Ptr: s.service.ServiceInstanceName(),
21✔
507
                        })
21✔
508
        }
21✔
509

510
        resp.Answer = s.appendAddrs(resp.Answer, ttl, ifIndex, flushCache)
32✔
511
}
512

513
func (s *Server) serviceTypeName(resp *dns.Msg, ttl uint32) {
1✔
514
        // From RFC6762
1✔
515
        // 9.  Service Type Enumeration
1✔
516
        //
1✔
517
        //    For this purpose, a special meta-query is defined.  A DNS query for
1✔
518
        //    PTR records with the name "_services._dns-sd._udp.<Domain>" yields a
1✔
519
        //    set of PTR records, where the rdata of each PTR record is the two-
1✔
520
        //    label <Service> name, plus the same domain, e.g.,
1✔
521
        //    "_http._tcp.<Domain>".
1✔
522
        dnssd := &dns.PTR{
1✔
523
                Hdr: dns.RR_Header{
1✔
524
                        Name:   s.service.ServiceTypeName(),
1✔
525
                        Rrtype: dns.TypePTR,
1✔
526
                        Class:  dns.ClassINET,
1✔
527
                        Ttl:    ttl,
1✔
528
                },
1✔
529
                Ptr: s.service.ServiceName(),
1✔
530
        }
1✔
531
        resp.Answer = append(resp.Answer, dnssd)
1✔
532
}
1✔
533

534
// Perform probing & announcement
535
// TODO: implement a proper probing & conflict resolution
536
func (s *Server) probe() {
6✔
537
        defer s.refCount.Done()
6✔
538

6✔
539
        q := new(dns.Msg)
6✔
540
        q.SetQuestion(s.service.ServiceInstanceName(), dns.TypePTR)
6✔
541
        q.RecursionDesired = false
6✔
542

6✔
543
        srv := &dns.SRV{
6✔
544
                Hdr: dns.RR_Header{
6✔
545
                        Name:   s.service.ServiceInstanceName(),
6✔
546
                        Rrtype: dns.TypeSRV,
6✔
547
                        Class:  dns.ClassINET,
6✔
548
                        Ttl:    s.ttl,
6✔
549
                },
6✔
550
                Priority: 0,
6✔
551
                Weight:   0,
6✔
552
                Port:     uint16(s.service.Port),
6✔
553
                Target:   s.service.HostName,
6✔
554
        }
6✔
555
        txt := &dns.TXT{
6✔
556
                Hdr: dns.RR_Header{
6✔
557
                        Name:   s.service.ServiceInstanceName(),
6✔
558
                        Rrtype: dns.TypeTXT,
6✔
559
                        Class:  dns.ClassINET,
6✔
560
                        Ttl:    s.ttl,
6✔
561
                },
6✔
562
                Txt: s.service.Text,
6✔
563
        }
6✔
564
        q.Ns = []dns.RR{srv, txt}
6✔
565

6✔
566
        // Wait for a random duration uniformly distributed between 0 and 250 ms
6✔
567
        // before sending the first probe packet.
6✔
568
        timer := time.NewTimer(time.Duration(rand.Intn(250)) * time.Millisecond)
6✔
569
        defer timer.Stop()
6✔
570
        select {
6✔
571
        case <-timer.C:
4✔
572
        case <-s.shouldShutdown:
2✔
573
                return
2✔
574
        }
575
        for i := 0; i < 3; i++ {
16✔
576
                if err := s.multicastResponse(q, 0); err != nil {
12✔
577
                        log.Println("[ERR] zeroconf: failed to send probe:", err.Error())
×
578
                }
×
579
                timer.Reset(250 * time.Millisecond)
12✔
580
                select {
12✔
581
                case <-timer.C:
12✔
582
                case <-s.shouldShutdown:
×
583
                        return
×
584
                }
585
        }
586

587
        // From RFC6762
588
        //    The Multicast DNS responder MUST send at least two unsolicited
589
        //    responses, one second apart. To provide increased robustness against
590
        //    packet loss, a responder MAY send up to eight unsolicited responses,
591
        //    provided that the interval between unsolicited responses increases by
592
        //    at least a factor of two with every response sent.
593
        timeout := time.Second
4✔
594
        for i := 0; i < multicastRepetitions; i++ {
12✔
595
                for _, intf := range s.ifaces {
32✔
596
                        resp := new(dns.Msg)
24✔
597
                        resp.MsgHdr.Response = true
24✔
598
                        // TODO: make response authoritative if we are the publisher
24✔
599
                        resp.Compress = true
24✔
600
                        resp.Answer = []dns.RR{}
24✔
601
                        resp.Extra = []dns.RR{}
24✔
602
                        s.composeLookupAnswers(resp, s.ttl, intf.Index, true)
24✔
603
                        if err := s.multicastResponse(resp, intf.Index); err != nil {
24✔
604
                                log.Println("[ERR] zeroconf: failed to send announcement:", err.Error())
×
605
                        }
×
606
                }
607
                timer.Reset(timeout)
8✔
608
                select {
8✔
609
                case <-timer.C:
8✔
610
                case <-s.shouldShutdown:
×
611
                        return
×
612
                }
613
                timeout *= 2
8✔
614
        }
615
}
616

617
// announceText sends a Text announcement with cache flush enabled
618
func (s *Server) announceText() {
1✔
619
        resp := new(dns.Msg)
1✔
620
        resp.MsgHdr.Response = true
1✔
621

1✔
622
        txt := &dns.TXT{
1✔
623
                Hdr: dns.RR_Header{
1✔
624
                        Name:   s.service.ServiceInstanceName(),
1✔
625
                        Rrtype: dns.TypeTXT,
1✔
626
                        Class:  dns.ClassINET | qClassCacheFlush,
1✔
627
                        Ttl:    s.ttl,
1✔
628
                },
1✔
629
                Txt: s.service.Text,
1✔
630
        }
1✔
631

1✔
632
        resp.Answer = []dns.RR{txt}
1✔
633
        _ = s.multicastResponse(resp, 0)
1✔
634
}
1✔
635

636
func (s *Server) unregister() error {
7✔
637
        resp := new(dns.Msg)
7✔
638
        resp.MsgHdr.Response = true
7✔
639
        resp.Answer = []dns.RR{}
7✔
640
        resp.Extra = []dns.RR{}
7✔
641
        s.composeLookupAnswers(resp, 0, 0, true)
7✔
642
        return s.multicastResponse(resp, 0)
7✔
643
}
7✔
644

645
func (s *Server) appendAddrs(list []dns.RR, ttl uint32, ifIndex int, flushCache bool) []dns.RR {
76✔
646
        v4 := s.service.AddrIPv4
76✔
647
        v6 := s.service.AddrIPv6
76✔
648
        if len(v4) == 0 && len(v6) == 0 {
81✔
649
                iface, _ := net.InterfaceByIndex(ifIndex)
5✔
650
                if iface != nil {
9✔
651
                        a4, a6 := addrsForInterface(iface)
4✔
652
                        v4 = append(v4, a4...)
4✔
653
                        v6 = append(v6, a6...)
4✔
654
                }
4✔
655
        }
656
        if ttl > 0 {
145✔
657
                // RFC6762 Section 10 says A/AAAA records SHOULD
69✔
658
                // use TTL of 120s, to account for network interface
69✔
659
                // and IP address changes.
69✔
660
                ttl = 120
69✔
661
        }
69✔
662
        var cacheFlushBit uint16
76✔
663
        if flushCache {
107✔
664
                cacheFlushBit = qClassCacheFlush
31✔
665
        }
31✔
666
        for _, ipv4 := range v4 {
216✔
667
                a := &dns.A{
140✔
668
                        Hdr: dns.RR_Header{
140✔
669
                                Name:   s.service.HostName,
140✔
670
                                Rrtype: dns.TypeA,
140✔
671
                                Class:  dns.ClassINET | cacheFlushBit,
140✔
672
                                Ttl:    ttl,
140✔
673
                        },
140✔
674
                        A: ipv4,
140✔
675
                }
140✔
676
                list = append(list, a)
140✔
677
        }
140✔
678
        for _, ipv6 := range v6 {
215✔
679
                aaaa := &dns.AAAA{
139✔
680
                        Hdr: dns.RR_Header{
139✔
681
                                Name:   s.service.HostName,
139✔
682
                                Rrtype: dns.TypeAAAA,
139✔
683
                                Class:  dns.ClassINET | cacheFlushBit,
139✔
684
                                Ttl:    ttl,
139✔
685
                        },
139✔
686
                        AAAA: ipv6,
139✔
687
                }
139✔
688
                list = append(list, aaaa)
139✔
689
        }
139✔
690
        return list
76✔
691
}
692

693
func addrsForInterface(iface *net.Interface) ([]net.IP, []net.IP) {
19✔
694
        var v4, v6, v6local []net.IP
19✔
695
        addrs, _ := iface.Addrs()
19✔
696
        for _, address := range addrs {
47✔
697
                if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
48✔
698
                        if ipnet.IP.To4() != nil {
30✔
699
                                v4 = append(v4, ipnet.IP)
10✔
700
                        } else {
20✔
701
                                switch ip := ipnet.IP.To16(); ip != nil {
10✔
702
                                case ip.IsGlobalUnicast():
×
703
                                        v6 = append(v6, ipnet.IP)
×
704
                                case ip.IsLinkLocalUnicast():
10✔
705
                                        v6local = append(v6local, ipnet.IP)
10✔
706
                                }
707
                        }
708
                }
709
        }
710
        if len(v6) == 0 {
38✔
711
                v6 = v6local
19✔
712
        }
19✔
713
        return v4, v6
19✔
714
}
715

716
// unicastResponse is used to send a unicast response packet
717
func (s *Server) unicastResponse(resp *dns.Msg, ifIndex int, from net.Addr) error {
1✔
718
        buf, err := resp.Pack()
1✔
719
        if err != nil {
1✔
720
                return err
×
721
        }
×
722
        addr := from.(*net.UDPAddr)
1✔
723
        if addr.IP.To4() != nil {
2✔
724
                _, err = s.ipv4conn.WriteTo(buf, ifIndex, addr)
1✔
725
                return err
1✔
726
        }
1✔
NEW
727
        _, err = s.ipv6conn.WriteTo(buf, ifIndex, addr)
×
NEW
728
        return err
×
729
}
730

731
// multicastResponse is used to send a multicast response packet
732
func (s *Server) multicastResponse(msg *dns.Msg, ifIndex int) error {
87✔
733
        buf, err := msg.Pack()
87✔
734
        if err != nil {
87✔
735
                return fmt.Errorf("failed to pack msg %v: %w", msg, err)
×
736
        }
×
737

738
        // Determine which interfaces to send to
739
        var ifaces []int
87✔
740
        if ifIndex != 0 {
153✔
741
                ifaces = []int{ifIndex}
66✔
742
        } else {
87✔
743
                for _, intf := range s.ifaces {
76✔
744
                        ifaces = append(ifaces, intf.Index)
55✔
745
                }
55✔
746
        }
747

748
        // Send to IPv4 multicast group
749
        if s.ipv4conn != nil {
174✔
750
                for _, idx := range ifaces {
208✔
751
                        _, _ = s.ipv4conn.WriteTo(buf, idx, ipv4Addr)
121✔
752
                }
121✔
753
        }
754

755
        // Send to IPv6 multicast group
756
        if s.ipv6conn != nil {
174✔
757
                for _, idx := range ifaces {
208✔
758
                        _, _ = s.ipv6conn.WriteTo(buf, idx, ipv6Addr)
121✔
759
                }
121✔
760
        }
761

762
        return nil
87✔
763
}
764

765
func isUnicastQuestion(q dns.Question) bool {
41✔
766
        // From RFC6762
41✔
767
        // 18.12.  Repurposing of Top Bit of qclass in Question Section
41✔
768
        //
41✔
769
        //    In the Question Section of a Multicast DNS query, the top bit of the
41✔
770
        //    qclass field is used to indicate that unicast responses are preferred
41✔
771
        //    for this particular question.  (See Section 5.4.)
41✔
772
        return q.Qclass&qClassCacheFlush != 0
41✔
773
}
41✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc