diff --git a/README.md b/README.md index 9d1807aa..b03d776c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ GLOBAL OPTIONS: digitalocean: DO_AUTH_TOKEN dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION - rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE + rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER manual: none --help, -h show help --version, -v print the version diff --git a/acme/dns_challenge_rfc2136.go b/acme/dns_challenge_rfc2136.go index e2954d25..cab76817 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/acme/dns_challenge_rfc2136.go @@ -12,26 +12,23 @@ import ( // uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver. type DNSProviderRFC2136 struct { nameserver string - zone string tsigAlgorithm string tsigKey string tsigSecret string - records map[string]string + domain2zone map[string]string } // NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. // To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string. -// 'nameserver' must be a network address in the the form "host" or "host:port". 'zone' must be the fully -// qualified name of the zone. -func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { +// 'nameserver' must be a network address in the the form "host" or "host:port". +func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { // Append the default DNS port if none is specified. if !strings.Contains(nameserver, ":") { nameserver += ":53" } d := &DNSProviderRFC2136{ - nameserver: nameserver, - zone: zone, - records: make(map[string]string), + nameserver: nameserver, + domain2zone: make(map[string]string), } if tsigAlgorithm == "" { tsigAlgorithm = dns.HmacMD5 @@ -48,18 +45,21 @@ func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret // Present creates a TXT record using the specified parameters func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error { fqdn, value, ttl := DNS01Record(domain, keyAuth) - r.records[fqdn] = value return r.changeRecord("INSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { - fqdn, _, ttl := DNS01Record(domain, keyAuth) - value := r.records[fqdn] + fqdn, value, ttl := DNS01Record(domain, keyAuth) return r.changeRecord("REMOVE", fqdn, value, ttl) } func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error { + zone, err := r.findZone(fqdn) + if err != nil { + return err + } + // Create RR rr := new(dns.TXT) rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} @@ -69,9 +69,11 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e // Create dynamic update packet m := new(dns.Msg) - m.SetUpdate(dns.Fqdn(r.zone)) + m.SetUpdate(zone) switch action { case "INSERT": + // Always remove old challenge left over from who knows what. + m.RemoveRRset(rrs) m.Insert(rrs) case "REMOVE": m.Remove(rrs) @@ -99,3 +101,55 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e return nil } + +// findZone determines the zone of a qualifying cert DNSname +func (r *DNSProviderRFC2136) findZone(fqdn string) (string, error) { + // Do we have it cached? + if val, ok := r.domain2zone[fqdn]; ok { + return val, nil + } + + // Query the authorative nameserver for a hopefully non-existing SOA record, + // in the authority section of the reply it will have the SOA of the + // containing zone. rfc2308 has this to say on the subject: + // Name servers authoritative for a zone MUST include the SOA record of + // the zone in the authority section of the response when reporting an + // NXDOMAIN or indicating that no data (NODATA) of the requested type exists + m := new(dns.Msg) + m.SetQuestion(fqdn, dns.TypeSOA) + m.SetEdns0(4096, false) + m.RecursionDesired = true + m.Authoritative = true + + in, err := dns.Exchange(m, r.nameserver) + if err == dns.ErrTruncated { + tcp := &dns.Client{Net: "tcp"} + in, _, err = tcp.Exchange(m, r.nameserver) + } + if err != nil { + return "", err + } + if in.Rcode != dns.RcodeNameError { + if in.Rcode != dns.RcodeSuccess { + return "", fmt.Errorf("DNS Query for zone %q failed", fqdn) + } + // We have a success, so one of the answers has to be a SOA RR + for _, ans := range in.Answer { + if ans.Header().Rrtype == dns.TypeSOA { + zone := ans.Header().Name + r.domain2zone[fqdn] = zone + return zone, nil + } + } + // Or it is NODATA, fall through to NXDOMAIN + } + // Search the authority section for our precious SOA RR + for _, ns := range in.Ns { + if ns.Header().Rrtype == dns.TypeSOA { + zone := ns.Header().Name + r.domain2zone[fqdn] = zone + return zone, nil + } + } + return "", fmt.Errorf("Expected a SOA record in the authority section") +} diff --git a/cli.go b/cli.go index 893d1cda..91bef0cd 100644 --- a/cli.go +++ b/cli.go @@ -124,7 +124,7 @@ func main() { "\n\tdigitalocean: DO_AUTH_TOKEN" + "\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" + "\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" + - "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE" + + "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER" + "\n\tmanual: none", }, } diff --git a/cli_handlers.go b/cli_handlers.go index cdf4dca8..3a8d3d6b 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -79,12 +79,11 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { provider, err = acme.NewDNSProviderRoute53("", "", awsRegion) case "rfc2136": nameserver := os.Getenv("RFC2136_NAMESERVER") - zone := os.Getenv("RFC2136_ZONE") tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") tsigKey := os.Getenv("RFC2136_TSIG_KEY") tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") - provider, err = acme.NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret) + provider, err = acme.NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret) case "manual": provider, err = acme.NewDNSProviderManual() }