diff --git a/README.md b/README.md index e384f9c..01cd23c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/challenge-servers.go b/challenge-servers.go index a118f1f..c069432 100644 --- a/challenge-servers.go +++ b/challenge-servers.go @@ -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 @@ -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), }, } diff --git a/dns.go b/dns.go index 6058b55..d2b71fb 100644 --- a/dns.go +++ b/dns.go @@ -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. @@ -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) @@ -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: diff --git a/mockdns.go b/mockdns.go index 0018e91..e683bbf 100644 --- a/mockdns.go +++ b/mockdns.go @@ -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) {