1 package bdns
2
3 import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "net"
9 "strconv"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/jmhodges/clock"
15 "github.com/miekg/dns"
16 "github.com/prometheus/client_golang/prometheus"
17
18 blog "github.com/letsencrypt/boulder/log"
19 "github.com/letsencrypt/boulder/metrics"
20 )
21
22 func parseCidr(network string, comment string) net.IPNet {
23 _, net, err := net.ParseCIDR(network)
24 if err != nil {
25 panic(fmt.Sprintf("error parsing %s (%s): %s", network, comment, err))
26 }
27 return *net
28 }
29
30 var (
31
32 privateNetworks = []net.IPNet{
33
34
35 {
36 IP: []byte{10, 0, 0, 0},
37 Mask: []byte{255, 0, 0, 0},
38 },
39
40 {
41 IP: []byte{172, 16, 0, 0},
42 Mask: []byte{255, 240, 0, 0},
43 },
44
45 {
46 IP: []byte{192, 168, 0, 0},
47 Mask: []byte{255, 255, 0, 0},
48 },
49
50
51 {
52 IP: []byte{127, 0, 0, 0},
53 Mask: []byte{255, 0, 0, 0},
54 },
55
56
57 {
58 IP: []byte{0, 0, 0, 0},
59 Mask: []byte{255, 0, 0, 0},
60 },
61
62
63 {
64 IP: []byte{169, 254, 0, 0},
65 Mask: []byte{255, 255, 0, 0},
66 },
67
68
69 {
70 IP: []byte{192, 0, 0, 0},
71 Mask: []byte{255, 255, 255, 0},
72 },
73
74
75 {
76 IP: []byte{192, 0, 2, 0},
77 Mask: []byte{255, 255, 255, 0},
78 },
79
80 {
81 IP: []byte{198, 51, 100, 0},
82 Mask: []byte{255, 255, 255, 0},
83 },
84
85 {
86 IP: []byte{203, 0, 113, 0},
87 Mask: []byte{255, 255, 255, 0},
88 },
89
90
91 {
92 IP: []byte{192, 88, 99, 0},
93 Mask: []byte{255, 255, 255, 0},
94 },
95
96
97 {
98 IP: []byte{198, 18, 0, 0},
99 Mask: []byte{255, 254, 0, 0},
100 },
101
102
103 {
104 IP: []byte{224, 0, 0, 0},
105 Mask: []byte{240, 0, 0, 0},
106 },
107
108
109 {
110 IP: []byte{240, 0, 0, 0},
111 Mask: []byte{240, 0, 0, 0},
112 },
113
114
115 {
116 IP: []byte{255, 255, 255, 255},
117 Mask: []byte{255, 255, 255, 255},
118 },
119
120
121 {
122 IP: []byte{100, 64, 0, 0},
123 Mask: []byte{255, 192, 0, 0},
124 },
125 }
126
127
128 privateV6Networks = []net.IPNet{
129 parseCidr("::/128", "RFC 4291: Unspecified Address"),
130 parseCidr("::1/128", "RFC 4291: Loopback Address"),
131 parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"),
132 parseCidr("100::/64", "RFC 6666: Discard Address Block"),
133 parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"),
134 parseCidr("2001:2::/48", "RFC 5180: Benchmarking"),
135 parseCidr("2001:db8::/32", "RFC 3849: Documentation"),
136 parseCidr("2001::/32", "RFC 4380: TEREDO"),
137 parseCidr("fc00::/7", "RFC 4193: Unique-Local"),
138 parseCidr("fe80::/10", "RFC 4291: Section 2.5.6 Link-Scoped Unicast"),
139 parseCidr("ff00::/8", "RFC 4291: Section 2.7"),
140
141
142
143
144 parseCidr("2002::/16", "RFC 7526: 6to4 anycast prefix deprecated"),
145 }
146 )
147
148
149 type Client interface {
150 LookupTXT(context.Context, string) (txts []string, err error)
151 LookupHost(context.Context, string) ([]net.IP, error)
152 LookupCAA(context.Context, string) ([]*dns.CAA, string, error)
153 }
154
155
156 type impl struct {
157 dnsClient exchanger
158 servers ServerProvider
159 allowRestrictedAddresses bool
160 maxTries int
161 clk clock.Clock
162 log blog.Logger
163
164 queryTime *prometheus.HistogramVec
165 totalLookupTime *prometheus.HistogramVec
166 timeoutCounter *prometheus.CounterVec
167 idMismatchCounter *prometheus.CounterVec
168 }
169
170 var _ Client = &impl{}
171
172 type exchanger interface {
173 Exchange(m *dns.Msg, a string) (*dns.Msg, time.Duration, error)
174 }
175
176
177
178 func New(
179 readTimeout time.Duration,
180 servers ServerProvider,
181 stats prometheus.Registerer,
182 clk clock.Clock,
183 maxTries int,
184 log blog.Logger,
185 ) Client {
186 dnsClient := new(dns.Client)
187
188
189 dnsClient.ReadTimeout = readTimeout
190 dnsClient.Net = "udp"
191
192 queryTime := prometheus.NewHistogramVec(
193 prometheus.HistogramOpts{
194 Name: "dns_query_time",
195 Help: "Time taken to perform a DNS query",
196 Buckets: metrics.InternetFacingBuckets,
197 },
198 []string{"qtype", "result", "resolver"},
199 )
200 totalLookupTime := prometheus.NewHistogramVec(
201 prometheus.HistogramOpts{
202 Name: "dns_total_lookup_time",
203 Help: "Time taken to perform a DNS lookup, including all retried queries",
204 Buckets: metrics.InternetFacingBuckets,
205 },
206 []string{"qtype", "result", "retries", "resolver"},
207 )
208 timeoutCounter := prometheus.NewCounterVec(
209 prometheus.CounterOpts{
210 Name: "dns_timeout",
211 Help: "Counter of various types of DNS query timeouts",
212 },
213 []string{"qtype", "type", "resolver", "isTLD"},
214 )
215 idMismatchCounter := prometheus.NewCounterVec(
216 prometheus.CounterOpts{
217 Name: "dns_id_mismatch",
218 Help: "Counter of DNS ErrId errors sliced by query type and resolver",
219 },
220 []string{"qtype", "resolver"},
221 )
222 stats.MustRegister(queryTime, totalLookupTime, timeoutCounter, idMismatchCounter)
223
224 return &impl{
225 dnsClient: dnsClient,
226 servers: servers,
227 allowRestrictedAddresses: false,
228 maxTries: maxTries,
229 clk: clk,
230 queryTime: queryTime,
231 totalLookupTime: totalLookupTime,
232 timeoutCounter: timeoutCounter,
233 idMismatchCounter: idMismatchCounter,
234 log: log,
235 }
236 }
237
238
239
240
241 func NewTest(
242 readTimeout time.Duration,
243 servers ServerProvider,
244 stats prometheus.Registerer,
245 clk clock.Clock,
246 maxTries int,
247 log blog.Logger) Client {
248 resolver := New(readTimeout, servers, stats, clk, maxTries, log)
249 resolver.(*impl).allowRestrictedAddresses = true
250 return resolver
251 }
252
253
254
255
256
257 func (dnsClient *impl) exchangeOne(ctx context.Context, hostname string, qtype uint16) (resp *dns.Msg, err error) {
258 m := new(dns.Msg)
259
260 m.SetQuestion(dns.Fqdn(hostname), qtype)
261
262
263
264
265
266
267 m.AuthenticatedData = true
268
269
270
271 m.SetEdns0(4096, false)
272
273 servers, err := dnsClient.servers.Addrs()
274 if err != nil {
275 return nil, fmt.Errorf("failed to list DNS servers: %w", err)
276 }
277 chosenServerIndex := 0
278 chosenServer := servers[chosenServerIndex]
279
280
281
282
283 chosenServerIP, _, err := net.SplitHostPort(chosenServer)
284 if err != nil {
285 return
286 }
287
288 start := dnsClient.clk.Now()
289 client := dnsClient.dnsClient
290 qtypeStr := dns.TypeToString[qtype]
291 tries := 1
292 defer func() {
293 result := "failed"
294 if resp != nil {
295 result = dns.RcodeToString[resp.Rcode]
296 }
297 dnsClient.totalLookupTime.With(prometheus.Labels{
298 "qtype": qtypeStr,
299 "result": result,
300 "retries": strconv.Itoa(tries),
301 "resolver": chosenServerIP,
302 }).Observe(dnsClient.clk.Since(start).Seconds())
303 }()
304 for {
305 ch := make(chan dnsResp, 1)
306
307
308
309
310
311
312 chosenServerIP, _, err = net.SplitHostPort(chosenServer)
313 if err != nil {
314 return
315 }
316
317 go func() {
318 rsp, rtt, err := client.Exchange(m, chosenServer)
319 result := "failed"
320 if rsp != nil {
321 result = dns.RcodeToString[rsp.Rcode]
322 }
323 if err != nil {
324 logDNSError(dnsClient.log, chosenServer, hostname, m, rsp, err)
325 if err == dns.ErrId {
326 dnsClient.idMismatchCounter.With(prometheus.Labels{
327 "qtype": qtypeStr,
328 "resolver": chosenServerIP,
329 }).Inc()
330 }
331 }
332 dnsClient.queryTime.With(prometheus.Labels{
333 "qtype": qtypeStr,
334 "result": result,
335 "resolver": chosenServerIP,
336 }).Observe(rtt.Seconds())
337 ch <- dnsResp{m: rsp, err: err}
338 }()
339 select {
340 case <-ctx.Done():
341 if ctx.Err() == context.DeadlineExceeded {
342 dnsClient.timeoutCounter.With(prometheus.Labels{
343 "qtype": qtypeStr,
344 "type": "deadline exceeded",
345 "resolver": chosenServerIP,
346 "isTLD": isTLD(hostname),
347 }).Inc()
348 } else if ctx.Err() == context.Canceled {
349 dnsClient.timeoutCounter.With(prometheus.Labels{
350 "qtype": qtypeStr,
351 "type": "canceled",
352 "resolver": chosenServerIP,
353 "isTLD": isTLD(hostname),
354 }).Inc()
355 } else {
356 dnsClient.timeoutCounter.With(prometheus.Labels{
357 "qtype": qtypeStr,
358 "type": "unknown",
359 "resolver": chosenServerIP,
360 }).Inc()
361 }
362 err = ctx.Err()
363 return
364 case r := <-ch:
365 if r.err != nil {
366 var operr *net.OpError
367 ok := errors.As(r.err, &operr)
368 isRetryable := ok && operr.Temporary()
369 hasRetriesLeft := tries < dnsClient.maxTries
370 if isRetryable && hasRetriesLeft {
371 tries++
372
373
374
375
376 chosenServerIndex = (chosenServerIndex + 1) % len(servers)
377 chosenServer = servers[chosenServerIndex]
378 continue
379 } else if isRetryable && !hasRetriesLeft {
380 dnsClient.timeoutCounter.With(prometheus.Labels{
381 "qtype": qtypeStr,
382 "type": "out of retries",
383 "resolver": chosenServerIP,
384 "isTLD": isTLD(hostname),
385 }).Inc()
386 }
387 }
388 resp, err = r.m, r.err
389 return
390 }
391 }
392
393 }
394
395
396
397
398 func isTLD(hostname string) string {
399 if strings.Contains(hostname, ".") {
400 return "false"
401 } else {
402 return "true"
403 }
404 }
405
406 type dnsResp struct {
407 m *dns.Msg
408 err error
409 }
410
411
412
413
414 func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string, error) {
415 var txt []string
416 dnsType := dns.TypeTXT
417 r, err := dnsClient.exchangeOne(ctx, hostname, dnsType)
418 errWrap := wrapErr(dnsType, hostname, r, err)
419 if errWrap != nil {
420 return nil, errWrap
421 }
422
423 for _, answer := range r.Answer {
424 if answer.Header().Rrtype == dnsType {
425 if txtRec, ok := answer.(*dns.TXT); ok {
426 txt = append(txt, strings.Join(txtRec.Txt, ""))
427 }
428 }
429 }
430
431 return txt, err
432 }
433
434 func isPrivateV4(ip net.IP) bool {
435 for _, net := range privateNetworks {
436 if net.Contains(ip) {
437 return true
438 }
439 }
440 return false
441 }
442
443 func isPrivateV6(ip net.IP) bool {
444 for _, net := range privateV6Networks {
445 if net.Contains(ip) {
446 return true
447 }
448 }
449 return false
450 }
451
452 func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, error) {
453 resp, err := dnsClient.exchangeOne(ctx, hostname, ipType)
454 errWrap := wrapErr(ipType, hostname, resp, err)
455 if errWrap != nil {
456 return nil, errWrap
457 }
458 return resp.Answer, nil
459 }
460
461
462
463
464
465
466 func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.IP, error) {
467 var recordsA, recordsAAAA []dns.RR
468 var errA, errAAAA error
469 var wg sync.WaitGroup
470
471 wg.Add(1)
472 go func() {
473 defer wg.Done()
474 recordsA, errA = dnsClient.lookupIP(ctx, hostname, dns.TypeA)
475 }()
476 wg.Add(1)
477 go func() {
478 defer wg.Done()
479 recordsAAAA, errAAAA = dnsClient.lookupIP(ctx, hostname, dns.TypeAAAA)
480 }()
481 wg.Wait()
482
483 var addrsA []net.IP
484 if errA == nil {
485 for _, answer := range recordsA {
486 if answer.Header().Rrtype == dns.TypeA {
487 a, ok := answer.(*dns.A)
488 if ok && a.A.To4() != nil && (!isPrivateV4(a.A) || dnsClient.allowRestrictedAddresses) {
489 addrsA = append(addrsA, a.A)
490 }
491 }
492 }
493 if len(addrsA) == 0 {
494 errA = fmt.Errorf("no valid A records found for %s", hostname)
495 }
496 }
497
498 var addrsAAAA []net.IP
499 if errAAAA == nil {
500 for _, answer := range recordsAAAA {
501 if answer.Header().Rrtype == dns.TypeAAAA {
502 aaaa, ok := answer.(*dns.AAAA)
503 if ok && aaaa.AAAA.To16() != nil && (!isPrivateV6(aaaa.AAAA) || dnsClient.allowRestrictedAddresses) {
504 addrsAAAA = append(addrsAAAA, aaaa.AAAA)
505 }
506 }
507 }
508 if len(addrsAAAA) == 0 {
509 errAAAA = fmt.Errorf("no valid AAAA records found for %s", hostname)
510 }
511 }
512
513 if errA != nil && errAAAA != nil {
514
515
516
517
518
519 return nil, fmt.Errorf("%w; %s", errA, errAAAA)
520 }
521
522 return append(addrsA, addrsAAAA...), nil
523 }
524
525
526
527
528
529 func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.CAA, string, error) {
530 dnsType := dns.TypeCAA
531 r, err := dnsClient.exchangeOne(ctx, hostname, dnsType)
532
533
534
535
536
537
538
539 if err == nil && r.Rcode == dns.RcodeNameError && strings.Contains(hostname, ".") {
540 return nil, "", nil
541 }
542
543 errWrap := wrapErr(dnsType, hostname, r, err)
544 if errWrap != nil {
545 return nil, "", errWrap
546 }
547
548 var CAAs []*dns.CAA
549 for _, answer := range r.Answer {
550 if caaR, ok := answer.(*dns.CAA); ok {
551 CAAs = append(CAAs, caaR)
552 }
553 }
554 var response string
555 if len(CAAs) > 0 {
556 response = r.String()
557 }
558 return CAAs, response, nil
559 }
560
561
562
563
564
565
566
567 func logDNSError(
568 logger blog.Logger,
569 chosenServer string,
570 hostname string,
571 msg, resp *dns.Msg,
572 underlying error) {
573
574
575 if msg == nil || len(msg.Question) == 0 || underlying == nil {
576 return
577 }
578 queryType := dns.TypeToString[msg.Question[0].Qtype]
579
580
581
582 if underlying == dns.ErrId {
583 packedMsgBytes, err := msg.Pack()
584 if err != nil {
585 logger.Errf("logDNSError failed to pack msg: %v", err)
586 return
587 }
588 encodedMsg := base64.StdEncoding.EncodeToString(packedMsgBytes)
589
590 var encodedResp string
591 var respQname string
592 if resp != nil {
593 packedRespBytes, err := resp.Pack()
594 if err != nil {
595 logger.Errf("logDNSError failed to pack resp: %v", err)
596 return
597 }
598 encodedResp = base64.StdEncoding.EncodeToString(packedRespBytes)
599 if len(resp.Answer) > 0 && resp.Answer[0].Header() != nil {
600 respQname = resp.Answer[0].Header().Name
601 }
602 }
603
604 logger.Infof(
605 "logDNSError ID mismatch chosenServer=[%s] hostname=[%s] respHostname=[%s] queryType=[%s] msg=[%s] resp=[%s] err=[%s]",
606 chosenServer,
607 hostname,
608 respQname,
609 queryType,
610 encodedMsg,
611 encodedResp,
612 underlying)
613 } else {
614
615 logger.Infof("logDNSError chosenServer=[%s] hostname=[%s] queryType=[%s] err=[%s]",
616 chosenServer,
617 hostname,
618 queryType,
619 underlying)
620 }
621 }
622
View as plain text