boulder/core/dns.go

192 lines
5.4 KiB
Go

// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package core
import (
"errors"
"fmt"
"math/rand"
"net"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
// DNSResolver represents a resolver system
type DNSResolver struct {
DNSClient *dns.Client
Servers []string
}
// NewDNSResolverImpl constructs a new DNS resolver object that utilizes the
// provided list of DNS servers for resolution.
func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolverImpl {
dnsClient := new(dns.Client)
// Set timeout for underlying net.Conn
dnsClient.DialTimeout = dialTimeout
return &DNSResolverImpl{DNSClient: dnsClient, Servers: servers}
}
// ExchangeOne performs a single DNS exchange with a randomly chosen server
// out of the server list, returning the response, time, and error (if any)
func (dnsResolver *DNSResolver) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
// Set DNSSEC OK bit for resolver
m.SetEdns0(4096, true)
if len(dnsResolver.Servers) < 1 {
err = fmt.Errorf("Not configured with at least one DNS Server")
return
}
// Randomly pick a server
chosenServer := dnsResolver.Servers[rand.Intn(len(dnsResolver.Servers))]
return dnsResolver.DNSClient.Exchange(m, chosenServer)
}
// LookupTXT sends a DNS query to find all TXT records associated with
// the provided hostname.
func (dnsResolver *DNSResolver) LookupTXT(hostname string) ([]string, time.Duration, error) {
var txt []string
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeTXT)
r, rtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return nil, 0, err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
err = fmt.Errorf("Failure at resolver: %d-%s for TXT query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, 0, err
}
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeTXT {
txtRec := answer.(*dns.TXT)
for _, field := range txtRec.Txt {
txt = append(txt, field)
}
}
}
return txt, rtt, err
}
// LookupHost sends a DNS query to find all A/AAAA records associated with
// the provided hostname.
func (dnsResolver *DNSResolver) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
var addrs []net.IP
var answers []dns.RR
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
r, aRtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return addrs, aRtt, err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
err = fmt.Errorf("Failure at resolver: %d-%s for A query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, 0, err
}
answers = append(answers, r.Answer...)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA)
r, aaaaRtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return addrs, aRtt + aaaaRtt, err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
err = fmt.Errorf("Failure at resolver: %d-%s for AAAA query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt + aaaaRtt, err
}
answers = append(answers, r.Answer...)
for _, answer := range answers {
if answer.Header().Rrtype == dns.TypeA {
a := answer.(*dns.A)
addrs = append(addrs, a.A)
} else if answer.Header().Rrtype == dns.TypeAAAA {
aaaa := answer.(*dns.AAAA)
addrs = append(addrs, aaaa.AAAA)
}
}
return addrs, aRtt + aaaaRtt, nil
}
// LookupCNAME sends a DNS query to find a CNAME record associated domain and returns the
// record target.
func (dnsResolver *DNSResolver) LookupCNAME(domain string) (string, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME)
r, _, err := dnsResolver.ExchangeOne(m)
if err != nil {
return "", err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
err = fmt.Errorf("Failure at resolver: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
return "", err
}
for _, answer := range r.Answer {
if cname, ok := answer.(*dns.CNAME); ok {
return cname.Target, nil
}
}
return "", nil
}
// LookupCAA sends a DNS query to find all CAA records associated with
// the provided hostname. If the response code from the resolver is SERVFAIL
// an empty slice of CAA records is returned.
func (dnsResolver *DNSResolver) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
if alias {
// Check if there is a CNAME record for domain
canonName, err := dnsResolver.LookupCNAME(domain)
if err != nil {
return nil, err
}
if canonName == "" || canonName == domain {
return []*dns.CAA{}, nil
}
domain = canonName
}
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCAA)
r, _, err := dnsResolver.ExchangeOne(m)
if err != nil {
return nil, err
}
var CAAs []*dns.CAA
// XXX: On resolver validation failure, or other server failures, return empty
// set and no error.
if r.Rcode != dns.RcodeServerFailure {
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeCAA {
caaR, ok := answer.(*dns.CAA)
if !ok {
err = errors.New("Badly formatted record")
return nil, err
}
CAAs = append(CAAs, caaR)
}
}
}
return CAAs, nil
}