forked from libp2p/zeroconf
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathdns-sd.go
438 lines (398 loc) · 12.3 KB
/
dns-sd.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
package zeroconf
import (
"fmt"
"net/netip"
"slices"
"strings"
"time"
"github.com/miekg/dns"
)
// RFC 6763: DNS Service Discovery
//
// service: name of the service (aka instance), any < 63 characters
// type: two-label service type, e.g. `_http._tcp` (last label should be `_tcp` or `_udp`)
// domain: typically `local`, but may in theory be an FQDN, e.g. `example.org`
// subtype: optional service sub-type, e.g. `_printer`
// hostname: hostname of a device, e.g. `Bryans-PC.local`
//
// Names used in DNS records:
//
// service-path: <service> . <type> . <domain>, e.g. `Bryan's Service._http._tcp.local`
// query: <type> . <domain>, e.g. `_http._tcp.local`
// sub-query: <subtype> . `_sub` . <type> . <domain>, e.g. `_printer._sub._http._tcp.local`
// meta-query: `_services._dns-sd._udp.local`
// A responder should resolve the following PTR queries:
//
// PTR <query> -> <service-path> // Service enumeration
// PTR <sub-query> -> <service-path> // Service enumeration by subtype
// PTR <meta-query> -> <type> . <domain> // Meta-service enumeration
//
// The PTR target refers to the SRV and TXT records:
//
// SRV <service-path>:
// Hostname: <hostname>
// Port: <...>
//
// TXT <service-path>: (note this is included as an empty list even if no txt is provided)
// Txt: <txt>
//
// And finally, the SRV refers to the A and AAAA records:
//
// A <hostname>:
// A: <ipv4>
//
// AAAA <hostname>:
// AAAA: <ipv6>
//
// Optional: NSEC records, indicating that an RRSet is exhaustive.
//
// Note that all "referred" records (i.e. the transitive closure) should be included in a response,
// as additional records, in order to avoid successive queries. A responder ignores any queries for
// which it doesn't have an answer.
const (
// RFC 6762 Section 10.2: [...] the host sets the most significant bit of the rrclass
// field of the resource record. This bit, the cache-flush bit, tells neighboring hosts that
// this is not a shared record type.
classCacheFlush = 1 << 15
// RFC 6762 Section 18.12: In the Question Section of a Multicast DNS query, the top bit of the
// qclass field is used to indicate that unicast responses are preferred for this particular
// question.
qClassUnicastResponse = 1 << 15
// RFC6762 Section 10: PTR service records are shared, while others (SRV/TXT/A/AAAA) are unique.
uniqueRecordClass = dns.ClassINET | classCacheFlush
sharedRecordClass = dns.ClassINET
)
// Returns the main service type, e.g. `_http._tcp.local.` and any additional subtypes,
// e.g. `_printer._sub._http._tcp.local.`. Responders only.
//
// # See RFC6763 Section 7.1
//
// Format:
// <type>.<domain>.
// _sub.<subtype>.<type>.<domain>.
func responderNames(ty Type) (types []string) {
types = append(types, fmt.Sprintf("%s.%s.", ty.Name, ty.Domain))
for _, sub := range ty.Subtypes {
types = append(types, fmt.Sprintf("%s._sub.%s.%s.", sub, ty.Name, ty.Domain))
}
return
}
// Returns the query DNS name to use in e.g. a PTR query.
func queryName(ty Type) (str string) {
if len(ty.Subtypes) > 0 {
return fmt.Sprintf("%s._sub.%s.%s.", ty.Subtypes[0], ty.Name, ty.Domain)
} else {
return fmt.Sprintf("%s.%s.", ty.Name, ty.Domain)
}
}
// Returns a complete service path, e.g. `MyDemo\ Service._foobar._tcp.local.`,
// which is composed from service name, its main type and a domain.
//
// RFC 6763 Section 4.3: [...] the <Instance> portion is allowed to contain any characters
// Spaces and backslashes are escaped by "github.com/miekg/dns".
func servicePath(svc *Service) string {
name := strings.ReplaceAll(svc.Name, ".", "\\.")
return fmt.Sprintf("%s.%s.%s.", name, svc.Type.Name, svc.Type.Domain)
}
// Parse a service path into a service type and its name
// E.g. `Jessica._chat._tcp.local.`
func parseServicePath(s string) (svc *Service, err error) {
parts := dns.SplitDomainName(s)
// [service, type-identifier, type-proto, domain...]
if len(parts) < 4 {
return nil, fmt.Errorf("not enough components")
}
// The service name may contain dots.
name := unescapeDns(parts[0])
typeName := fmt.Sprintf("%s.%s", parts[1], parts[2])
domain := strings.Join(parts[3:], ".")
ty := Type{typeName, domain, nil}
if err := ty.Validate(); err != nil {
return nil, err
}
return &Service{Type: ty, Name: name}, nil
}
// Parse a query into a service type and its name
// E.g. `_chat._tcp.local.` or `_emoji._sub._chat._tcp.local.`
func parseQueryName(s string) (ty *Type, err error) {
parts := dns.SplitDomainName(s)
var subtypes []string
// [service, type-identifier, type-proto, domain...]
if len(parts) > 2 && parts[1] == "_sub" {
subtypes = []string{parts[0]}
parts = parts[2:]
}
if len(parts) < 3 {
return nil, fmt.Errorf("not enough components")
}
typeName := fmt.Sprintf("%s.%s", parts[0], parts[1])
domain := strings.Join(parts[2:], ".")
ty = &Type{typeName, domain, subtypes}
if err := ty.Validate(); err != nil {
return nil, err
}
return ty, nil
}
// Returns true if the record is an answer to question
func isAnswerTo(record dns.RR, question dns.Question) bool {
hdr := record.Header()
return (question.Qclass == dns.TypeANY || question.Qclass == hdr.Class) && question.Name == hdr.Name
}
// Returns true if the answer is in the known-answer list, and has more than 1/2 ttl remaining.
//
// RFC6762 7.1. Known-Answer Suppression.
func isKnownAnswer(answer dns.RR, knowns []dns.RR) bool {
answerTtl := answer.Header().Ttl
for _, known := range knowns {
if dns.IsDuplicate(answer, known) && known.Header().Ttl >= answerTtl/2 {
return true
}
}
return false
}
// Returns answers and "extra records" that are considered additional to any answer where:
//
// (1) All SRV and TXT record(s) named in a PTR's rdata and
// (2) All A and AAAA record(s) named in an SRV's rdata.
//
// This is transitive, such that a PTR answer "generates" all other record types.
//
// RFC6762 7.1. DNS Additional Record Generation.
//
// Note that if there is any answer, we return *all other records* as extras.
// This is both allowed, simpler and has minimal overhead in practice.
func answerTo(records, knowns []dns.RR, question dns.Question) (answers, extras []dns.RR) {
// Fast path without allocations, since many questions will be completely unrelated
hasAnswers := false
for _, record := range records {
if isAnswerTo(record, question) {
hasAnswers = true
continue
}
}
if !hasAnswers {
return
}
// Slow path, populate answers and extras
for _, record := range records {
if isAnswerTo(record, question) && !isKnownAnswer(record, knowns) {
answers = append(answers, record)
} else {
extras = append(extras, record)
}
}
if len(answers) == 0 {
extras = nil
}
return
}
// Returns any services from the msg that matches the provided search type.
// This includes unannouncements
func servicesFromRecords(msg *dns.Msg) (services []*Service) {
// TODO: Support meta-queries
var (
answers = append(msg.Answer, msg.Extra...)
m = make(map[string]*Service, 1) // temporary map of service paths to services
addrMap = make(map[string][]netip.Addr, 1)
svc *Service
)
if len(msg.Question) > 0 {
return
}
// SRV, then PTR + TXT, then A and AAAA. The following loop depends on it
// Note that stable sort is necessary to preserve order of A and AAAA records
slices.SortStableFunc(answers, byRecordType)
// Create a svc record if it doesn't exist
ensureSvc := func(name string) *Service {
if svc = m[name]; svc != nil {
return svc
}
svc, err := parseServicePath(name)
if err != nil {
return nil
}
m[name] = svc
return svc
}
for _, answer := range answers {
switch rr := answer.(type) {
// Phase 1: create services
case *dns.SRV:
// pointer to service path, e.g. `My Printer._http._tcp.`
if svc = ensureSvc(rr.Hdr.Name); svc == nil {
continue
}
svc.Hostname = rr.Target
svc.Port = rr.Port
svc.ttl = time.Second * time.Duration(rr.Hdr.Ttl)
// Phase 2: populate subtypes and text
case *dns.PTR:
if svc = ensureSvc(rr.Ptr); svc == nil {
continue
}
// parse type from query, e.g. `_printer._sub._http._tcp.local.`
if ty, _ := parseQueryName(rr.Hdr.Name); ty != nil && ty.Equal(svc.Type) {
svc.Type.Subtypes = append(svc.Type.Subtypes, ty.Subtypes...)
}
case *dns.TXT:
if svc = m[rr.Hdr.Name]; svc == nil {
continue
}
svc.Text = rr.Txt
// Phase 3: add addrs to addrMap
case *dns.A:
if ip, ok := netip.AddrFromSlice(rr.A); ok {
addrMap[rr.Hdr.Name] = append(addrMap[rr.Hdr.Name], ip.Unmap())
}
case *dns.AAAA:
if ip, ok := netip.AddrFromSlice(rr.AAAA); ok {
addrMap[rr.Hdr.Name] = append(addrMap[rr.Hdr.Name], ip)
}
}
}
// Phase 4: add IPs
for _, svc := range m {
svc.Addrs = addrMap[svc.Hostname]
// Unescape afterwards to maintain comparison soundness above
svc.Hostname = unescapeDns(svc.Hostname)
for idx, txt := range svc.Text {
svc.Text[idx] = unescapeDns(txt)
}
svc.Hostname = trimDot(svc.Hostname)
if err := svc.Validate(); err != nil {
continue
}
services = append(services, svc)
}
return
}
// Ptr records for a service
func ptrRecords(svc *Service, unannounce bool) (records []dns.RR) {
var ttl uint32 = 75 * 60
if unannounce {
ttl = 0
}
names := responderNames(svc.Type)
for _, name := range names {
records = append(records, &dns.PTR{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypePTR,
Class: sharedRecordClass,
Ttl: ttl,
},
Ptr: servicePath(svc),
})
}
return
}
func recordsFromService(svc *Service, unannounce bool) (records []dns.RR) {
// RFC6762 Section 10: Records referencing a hostname (SRV/A/AAAA) SHOULD use TTL of 120 s,
// to account for network interface and IP address changes, while others should be 75 min.
var hostRecordTTL, defaultTTL uint32 = 120, 75 * 60
if unannounce {
hostRecordTTL, defaultTTL = 0, 0
}
servicePath := servicePath(svc)
hostname := svc.Hostname + "."
// PTR records
records = ptrRecords(svc, unannounce)
// RFC 6763 Section 9: Service Type Enumeration.
// For this purpose, a special meta-query is defined. A DNS query for
// PTR records with the name "_services._dns-sd._udp.<Domain>" yields a
// set of PTR records, where the rdata of each PTR record is the two-
// label <Service> name, plus the same domain, e.g., "_http._tcp.<Domain>".
records = append(records, &dns.PTR{
Hdr: dns.RR_Header{
Name: fmt.Sprintf("_services._dns-sd._udp.%v.", svc.Type.Domain),
Rrtype: dns.TypePTR,
Class: sharedRecordClass,
Ttl: defaultTTL,
},
Ptr: fmt.Sprintf("%v.%v.", svc.Type.Name, svc.Type.Domain),
})
// SRV record
records = append(records, &dns.SRV{
Hdr: dns.RR_Header{
Name: servicePath,
Rrtype: dns.TypeSRV,
Class: uniqueRecordClass,
Ttl: hostRecordTTL,
},
Port: svc.Port,
Target: hostname,
})
// TXT record
records = append(records, &dns.TXT{
Hdr: dns.RR_Header{
Name: servicePath,
Rrtype: dns.TypeTXT,
Class: uniqueRecordClass,
Ttl: defaultTTL,
},
Txt: svc.Text,
})
// NSEC for SRV, TXT
// See RFC 6762 Section 6.1: Negative Responses
records = append(records, &dns.NSEC{
Hdr: dns.RR_Header{
Name: servicePath,
Rrtype: dns.TypeNSEC,
Class: uniqueRecordClass,
Ttl: defaultTTL,
},
NextDomain: servicePath,
TypeBitMap: []uint16{dns.TypeTXT, dns.TypeSRV},
})
// A and AAAA records
for _, addr := range svc.Addrs {
if addr.Is4() {
records = append(records, &dns.A{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeA,
Class: uniqueRecordClass,
Ttl: hostRecordTTL,
},
A: addr.AsSlice(),
})
} else if addr.Is6() {
records = append(records, &dns.AAAA{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeAAAA,
Class: uniqueRecordClass,
Ttl: hostRecordTTL,
},
AAAA: addr.AsSlice(),
})
}
}
// NSEC for A, AAAA
records = append(records, &dns.NSEC{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeNSEC,
Class: uniqueRecordClass,
Ttl: hostRecordTTL,
},
NextDomain: hostname,
TypeBitMap: []uint16{dns.TypeA, dns.TypeAAAA},
})
return
}
// Compare records to aid in service construction from a record list
func byRecordType(a, b dns.RR) int {
return recordOrder(a) - recordOrder(b)
}
func recordOrder(rr dns.RR) int {
switch rr.Header().Rrtype {
case dns.TypeSRV:
return 0
case dns.TypePTR, dns.TypeTXT:
return 1
case dns.TypeA, dns.TypeAAAA:
return 2
}
return 3
}