Skip to content

Commit

Permalink
Add simple support for CNAMEs as DNS aliases (#7)
Browse files Browse the repository at this point in the history
- supports create/read/destroy and DNS lookup of CNAME record for hosts
- supports one level of "aliasing" by resolving CNAME in mock DNS server
  • Loading branch information
ryansouza authored and cpu committed Mar 29, 2019
1 parent 26580cb commit 285efd6
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
The `challtestsrv` package offers a library/command that can be used by test
code to respond to HTTP-01, DNS-01, and TLS-ALPN-01 ACME challenges. The
`challtestsrv` package can also be used as a mock DNS server letting
developers mock `A`, `AAAA`, and `CAA` DNS data for specific hostnames.
developers mock `A`, `AAAA`, `CNAME`, and `CAA` DNS data for specific hostnames.
The mock server will resolve up to one level of `CNAME` aliasing for accepted
DNS request types.

**Important note: The `challtestsrv` command and library are for TEST USAGE
ONLY. It is trivially insecure, offering no authentication. Only use
Expand Down
13 changes: 8 additions & 5 deletions challenge-servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type mockDNSData struct {
aaaaRecords map[string][]string
// A map of host to CAA policies for CAA responses.
caaRecords map[string][]MockCAAPolicy
// A map of host to CNAME records.
cnameRecords map[string]string
}

// MockCAAPolicy holds a tag and a value for a CAA record. See
Expand Down Expand Up @@ -131,11 +133,12 @@ func New(config Config) (*ChallSrv, error) {
tlsALPNOne: make(map[string]string),
redirects: make(map[string]string),
dnsMocks: mockDNSData{
defaultIPv4: defaultIPv4,
defaultIPv6: defaultIPv6,
aRecords: make(map[string][]string),
aaaaRecords: make(map[string][]string),
caaRecords: make(map[string][]MockCAAPolicy),
defaultIPv4: defaultIPv4,
defaultIPv6: defaultIPv6,
aRecords: make(map[string][]string),
aaaaRecords: make(map[string][]string),
caaRecords: make(map[string][]MockCAAPolicy),
cnameRecords: make(map[string]string),
},
}

Expand Down
39 changes: 37 additions & 2 deletions dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ func mockSOA() *dns.SOA {
// more RRs for the response.
type dnsAnswerFunc func(question dns.Question) []dns.RR

// cnameAnswers is a dnsAnswerFunc that creates CNAME RR's for the given question
// using the ChallSrv's dns mock data. If there is no mock CNAME data for the
// given hostname in the question no RR's will be returned.
func (s *ChallSrv) cnameAnswers(q dns.Question) []dns.RR {
var records []dns.RR

if value := s.GetDNSCNAMERecord(q.Name); value != "" {
record := &dns.CNAME{
Hdr: dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
},
Target: value,
}

records = append(records, record)
}

return records
}

// txtAnswers is a dnsAnswerFunc that creates TXT RR's for the given question
// using the ChallSrv's dns mock data. If there is no mock TXT data for the
// given hostname in the question no RR's will be returned.
Expand Down Expand Up @@ -133,8 +155,10 @@ func (s *ChallSrv) caaAnswers(q dns.Question) []dns.RR {
}

// dnsHandler is a miekg/dns handler that can process a dns.Msg request and
// write a response to the provided dns.ResponseWriter. TXT, A, AAAA, and CAA
// queries types are supported and answered using the ChallSrv's mock DNS data.
// write a response to the provided dns.ResponseWriter. TXT, A, AAAA, CNAME,
// and CAA queries types are supported and answered using the ChallSrv's mock
// DNS data. A host that is aliased by a CNAME record will follow that alias
// one level and return the requested record types for that alias' target
func (s *ChallSrv) dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
Expand All @@ -146,8 +170,19 @@ func (s *ChallSrv) dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
Question: q,
})

// If a CNAME exists for the question include the CNAME record and modify
// the question to instead lookup based on that CNAME's target
if cname := s.GetDNSCNAMERecord(q.Name); cname != "" {
cnameRecords := s.cnameAnswers(q)
m.Answer = append(m.Answer, cnameRecords...)

q = dns.Question{Name: cname, Qtype: q.Qtype}
}

var answerFunc dnsAnswerFunc
switch q.Qtype {
case dns.TypeCNAME:
answerFunc = s.cnameAnswers
case dns.TypeTXT:
answerFunc = s.txtAnswers
case dns.TypeA:
Expand Down
27 changes: 27 additions & 0 deletions mockdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ func (s *ChallSrv) GetDefaultDNSIPv6() string {
return s.dnsMocks.defaultIPv6
}

// AddDNSCNAMERecord sets a CNAME record that will be used like an alias when
// querying for other DNS records for the given host.
func (s *ChallSrv) AddDNSCNAMERecord(host string, value string) {
s.challMu.Lock()
defer s.challMu.Unlock()
host = dns.Fqdn(host)
value = dns.Fqdn(value)
s.dnsMocks.cnameRecords[host] = value
}

// GetDNSCNAMERecord returns a target host if a CNAME is set for the querying
// host and an empty string otherwise.
func (s *ChallSrv) GetDNSCNAMERecord(host string) string {
s.challMu.RLock()
host = dns.Fqdn(host)
defer s.challMu.RUnlock()
return s.dnsMocks.cnameRecords[host]
}

// DeleteDNSCAMERecord deletes any CNAME alias set for the given host.
func (s *ChallSrv) DeleteDNSCNAMERecord(host string) {
s.challMu.Lock()
defer s.challMu.Unlock()
host = dns.Fqdn(host)
delete(s.dnsMocks.cnameRecords, host)
}

// AddDNSARecord adds IPv4 addresses that will be returned when querying for
// A records for the given host.
func (s *ChallSrv) AddDNSARecord(host string, addresses []string) {
Expand Down

0 comments on commit 285efd6

Please sign in to comment.