2020-07-31 06:07:04 +10:00
|
|
|
package certmagic
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/miekg/dns"
|
2024-04-09 04:24:15 +10:00
|
|
|
"go.uber.org/zap"
|
2020-07-31 06:07:04 +10:00
|
|
|
)
|
|
|
|
|
|
|
|
// Code in this file adapted from go-acme/lego, July 2020:
|
|
|
|
// https://github.com/go-acme/lego
|
|
|
|
// by Ludovic Fernandez and Dominik Menke
|
|
|
|
//
|
|
|
|
// It has been modified.
|
|
|
|
|
|
|
|
// findZoneByFQDN determines the zone apex for the given fqdn by recursing
|
|
|
|
// up the domain labels until the nameserver returns a SOA record in the
|
2024-04-09 04:24:15 +10:00
|
|
|
// answer section. The logger must be non-nil.
|
|
|
|
func findZoneByFQDN(logger *zap.Logger, fqdn string, nameservers []string) (string, error) {
|
2020-07-31 06:07:04 +10:00
|
|
|
if !strings.HasSuffix(fqdn, ".") {
|
|
|
|
fqdn += "."
|
|
|
|
}
|
2024-04-09 04:24:15 +10:00
|
|
|
soa, err := lookupSoaByFqdn(logger, fqdn, nameservers)
|
2020-07-31 06:07:04 +10:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return soa.zone, nil
|
|
|
|
}
|
|
|
|
|
2024-04-09 04:24:15 +10:00
|
|
|
func lookupSoaByFqdn(logger *zap.Logger, fqdn string, nameservers []string) (*soaCacheEntry, error) {
|
|
|
|
logger = logger.Named("soa_lookup")
|
|
|
|
|
2020-07-31 06:07:04 +10:00
|
|
|
if !strings.HasSuffix(fqdn, ".") {
|
|
|
|
fqdn += "."
|
|
|
|
}
|
|
|
|
|
|
|
|
fqdnSOACacheMu.Lock()
|
|
|
|
defer fqdnSOACacheMu.Unlock()
|
|
|
|
|
|
|
|
// prefer cached version if fresh
|
|
|
|
if ent := fqdnSOACache[fqdn]; ent != nil && !ent.isExpired() {
|
2024-04-09 04:24:15 +10:00
|
|
|
logger.Debug("using cached SOA result", zap.String("entry", ent.zone))
|
2020-07-31 06:07:04 +10:00
|
|
|
return ent, nil
|
|
|
|
}
|
|
|
|
|
2024-04-09 04:24:15 +10:00
|
|
|
ent, err := fetchSoaByFqdn(logger, fqdn, nameservers)
|
2020-07-31 06:07:04 +10:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// save result to cache, but don't allow
|
|
|
|
// the cache to grow out of control
|
|
|
|
if len(fqdnSOACache) >= 1000 {
|
|
|
|
for key := range fqdnSOACache {
|
|
|
|
delete(fqdnSOACache, key)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fqdnSOACache[fqdn] = ent
|
|
|
|
|
|
|
|
return ent, nil
|
|
|
|
}
|
|
|
|
|
2024-04-09 04:24:15 +10:00
|
|
|
func fetchSoaByFqdn(logger *zap.Logger, fqdn string, nameservers []string) (*soaCacheEntry, error) {
|
2020-07-31 06:07:04 +10:00
|
|
|
var err error
|
|
|
|
var in *dns.Msg
|
|
|
|
|
|
|
|
labelIndexes := dns.Split(fqdn)
|
|
|
|
for _, index := range labelIndexes {
|
|
|
|
domain := fqdn[index:]
|
|
|
|
|
|
|
|
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if in == nil {
|
|
|
|
continue
|
|
|
|
}
|
2024-04-09 04:24:15 +10:00
|
|
|
logger.Debug("fetched SOA", zap.String("msg", in.String()))
|
2020-07-31 06:07:04 +10:00
|
|
|
|
|
|
|
switch in.Rcode {
|
|
|
|
case dns.RcodeSuccess:
|
|
|
|
// Check if we got a SOA RR in the answer section
|
|
|
|
if len(in.Answer) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// CNAME records cannot/should not exist at the root of a zone.
|
|
|
|
// So we skip a domain when a CNAME is found.
|
|
|
|
if dnsMsgContainsCNAME(in) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ans := range in.Answer {
|
|
|
|
if soa, ok := ans.(*dns.SOA); ok {
|
|
|
|
return newSoaCacheEntry(soa), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case dns.RcodeNameError:
|
|
|
|
// NXDOMAIN
|
|
|
|
default:
|
|
|
|
// Any response code other than NOERROR and NXDOMAIN is treated as error
|
|
|
|
return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
|
|
|
|
}
|
|
|
|
|
|
|
|
// dnsMsgContainsCNAME checks for a CNAME answer in msg
|
|
|
|
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
|
|
|
|
for _, ans := range msg.Answer {
|
|
|
|
if _, ok := ans.(*dns.CNAME); ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
|
|
|
|
m := createDNSMsg(fqdn, rtype, recursive)
|
|
|
|
var in *dns.Msg
|
|
|
|
var err error
|
|
|
|
for _, ns := range nameservers {
|
|
|
|
in, err = sendDNSQuery(m, ns)
|
|
|
|
if err == nil && len(in.Answer) > 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return in, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
|
|
|
|
m := new(dns.Msg)
|
|
|
|
m.SetQuestion(fqdn, rtype)
|
2022-05-30 06:58:11 +10:00
|
|
|
|
|
|
|
// See: https://caddy.community/t/hard-time-getting-a-response-on-a-dns-01-challenge/15721/16
|
|
|
|
m.SetEdns0(1232, false)
|
2020-07-31 06:07:04 +10:00
|
|
|
if !recursive {
|
|
|
|
m.RecursionDesired = false
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
|
|
|
|
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
|
|
|
|
in, _, err := udp.Exchange(m, ns)
|
2020-08-09 00:36:00 +10:00
|
|
|
// two kinds of errors we can handle by retrying with TCP:
|
|
|
|
// truncation and timeout; see https://github.com/caddyserver/caddy/issues/3639
|
|
|
|
truncated := in != nil && in.Truncated
|
|
|
|
timeoutErr := err != nil && strings.Contains(err.Error(), "timeout")
|
|
|
|
if truncated || timeoutErr {
|
2020-07-31 06:07:04 +10:00
|
|
|
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
|
|
|
|
in, _, err = tcp.Exchange(m, ns)
|
|
|
|
}
|
|
|
|
return in, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatDNSError(msg *dns.Msg, err error) string {
|
|
|
|
var parts []string
|
|
|
|
if msg != nil {
|
|
|
|
parts = append(parts, dns.RcodeToString[msg.Rcode])
|
|
|
|
}
|
|
|
|
if err != nil {
|
2020-08-27 04:57:16 +10:00
|
|
|
parts = append(parts, err.Error())
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
if len(parts) > 0 {
|
|
|
|
return ": " + strings.Join(parts, " ")
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// soaCacheEntry holds a cached SOA record (only selected fields)
|
|
|
|
type soaCacheEntry struct {
|
|
|
|
zone string // zone apex (a domain name)
|
|
|
|
primaryNs string // primary nameserver for the zone apex
|
|
|
|
expires time.Time // time when this cache entry should be evicted
|
|
|
|
}
|
|
|
|
|
|
|
|
func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry {
|
|
|
|
return &soaCacheEntry{
|
|
|
|
zone: soa.Hdr.Name,
|
|
|
|
primaryNs: soa.Ns,
|
|
|
|
expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// isExpired checks whether a cache entry should be considered expired.
|
|
|
|
func (cache *soaCacheEntry) isExpired() bool {
|
|
|
|
return time.Now().After(cache.expires)
|
|
|
|
}
|
|
|
|
|
2020-09-10 05:28:57 +10:00
|
|
|
// systemOrDefaultNameservers attempts to get system nameservers from the
|
|
|
|
// resolv.conf file given by path before falling back to hard-coded defaults.
|
|
|
|
func systemOrDefaultNameservers(path string, defaults []string) []string {
|
2020-07-31 06:07:04 +10:00
|
|
|
config, err := dns.ClientConfigFromFile(path)
|
|
|
|
if err != nil || len(config.Servers) == 0 {
|
|
|
|
return defaults
|
|
|
|
}
|
2020-09-10 05:28:57 +10:00
|
|
|
return config.Servers
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
2020-09-10 05:28:57 +10:00
|
|
|
// populateNameserverPorts ensures that all nameservers have a port number.
|
|
|
|
func populateNameserverPorts(servers []string) {
|
|
|
|
for i := range servers {
|
|
|
|
_, port, _ := net.SplitHostPort(servers[i])
|
|
|
|
if port == "" {
|
|
|
|
servers[i] = net.JoinHostPort(servers[i], "53")
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 02:59:55 +10:00
|
|
|
// checkDNSPropagation checks if the expected record has been propagated to all authoritative nameservers.
|
2024-04-09 04:24:15 +10:00
|
|
|
func checkDNSPropagation(logger *zap.Logger, fqdn string, recType uint16, expectedValue string, checkAuthoritativeServers bool, resolvers []string) (bool, error) {
|
|
|
|
logger = logger.Named("propagation")
|
|
|
|
|
2020-07-31 06:07:04 +10:00
|
|
|
if !strings.HasSuffix(fqdn, ".") {
|
|
|
|
fqdn += "."
|
|
|
|
}
|
|
|
|
|
2024-04-09 02:59:55 +10:00
|
|
|
// Initial attempt to resolve at the recursive NS - but do not actually
|
|
|
|
// dereference (follow) a CNAME record if we are targeting a CNAME record
|
|
|
|
// itself
|
|
|
|
if recType != dns.TypeCNAME {
|
|
|
|
r, err := dnsQuery(fqdn, recType, resolvers, true)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("CNAME dns query: %v", err)
|
|
|
|
}
|
|
|
|
if r.Rcode == dns.RcodeSuccess {
|
|
|
|
fqdn = updateDomainWithCName(r, fqdn)
|
|
|
|
}
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
2024-03-15 07:21:07 +10:00
|
|
|
if checkAuthoritativeServers {
|
2024-04-09 04:24:15 +10:00
|
|
|
authoritativeServers, err := lookupNameservers(logger, fqdn, resolvers)
|
2024-03-15 07:21:07 +10:00
|
|
|
if err != nil {
|
2024-04-09 02:59:55 +10:00
|
|
|
return false, fmt.Errorf("looking up authoritative nameservers: %v", err)
|
2024-03-15 07:21:07 +10:00
|
|
|
}
|
|
|
|
populateNameserverPorts(authoritativeServers)
|
|
|
|
resolvers = authoritativeServers
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
2024-04-09 04:24:15 +10:00
|
|
|
logger.Debug("checking authoritative nameservers", zap.Strings("resolvers", resolvers))
|
2020-07-31 06:07:04 +10:00
|
|
|
|
2024-04-09 02:59:55 +10:00
|
|
|
return checkAuthoritativeNss(fqdn, recType, expectedValue, resolvers)
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
2024-04-09 02:59:55 +10:00
|
|
|
// checkAuthoritativeNss queries each of the given nameservers for the expected record.
|
|
|
|
func checkAuthoritativeNss(fqdn string, recType uint16, expectedValue string, nameservers []string) (bool, error) {
|
2020-07-31 06:07:04 +10:00
|
|
|
for _, ns := range nameservers {
|
2024-04-09 02:59:55 +10:00
|
|
|
r, err := dnsQuery(fqdn, recType, []string{ns}, true)
|
2020-07-31 06:07:04 +10:00
|
|
|
if err != nil {
|
2024-04-09 02:59:55 +10:00
|
|
|
return false, fmt.Errorf("querying authoritative nameservers: %v", err)
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
if r.Rcode != dns.RcodeSuccess {
|
2022-09-09 06:11:38 +10:00
|
|
|
if r.Rcode == dns.RcodeNameError || r.Rcode == dns.RcodeServerFailure {
|
2020-07-31 06:07:04 +10:00
|
|
|
// if Present() succeeded, then it must show up eventually, or else
|
|
|
|
// something is really broken in the DNS provider or their API;
|
|
|
|
// no need for error here, simply have the caller try again
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, rr := range r.Answer {
|
2024-04-09 02:59:55 +10:00
|
|
|
switch recType {
|
|
|
|
case dns.TypeTXT:
|
|
|
|
if txt, ok := rr.(*dns.TXT); ok {
|
|
|
|
record := strings.Join(txt.Txt, "")
|
|
|
|
if record == expectedValue {
|
|
|
|
return true, nil
|
|
|
|
}
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
2024-04-09 02:59:55 +10:00
|
|
|
case dns.TypeCNAME:
|
|
|
|
if cname, ok := rr.(*dns.CNAME); ok {
|
|
|
|
// TODO: whether a DNS provider assumes a trailing dot or not varies, and we may have to standardize this in libdns packages
|
|
|
|
if strings.TrimSuffix(cname.Target, ".") == strings.TrimSuffix(expectedValue, ".") {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return false, fmt.Errorf("unsupported record type: %d", recType)
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-15 07:21:07 +10:00
|
|
|
return false, nil
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
// lookupNameservers returns the authoritative nameservers for the given fqdn.
|
2024-04-09 04:24:15 +10:00
|
|
|
func lookupNameservers(logger *zap.Logger, fqdn string, resolvers []string) ([]string, error) {
|
2020-07-31 06:07:04 +10:00
|
|
|
var authoritativeNss []string
|
|
|
|
|
2024-04-09 04:24:15 +10:00
|
|
|
zone, err := findZoneByFQDN(logger, fqdn, resolvers)
|
2020-07-31 06:07:04 +10:00
|
|
|
if err != nil {
|
2024-04-09 02:59:55 +10:00
|
|
|
return nil, fmt.Errorf("could not determine the zone for '%s': %w", fqdn, err)
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
2020-08-22 12:22:42 +10:00
|
|
|
r, err := dnsQuery(zone, dns.TypeNS, resolvers, true)
|
2020-07-31 06:07:04 +10:00
|
|
|
if err != nil {
|
2024-04-09 02:59:55 +10:00
|
|
|
return nil, fmt.Errorf("querying NS resolver for zone '%s' recursively: %v", zone, err)
|
2020-07-31 06:07:04 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, rr := range r.Answer {
|
|
|
|
if ns, ok := rr.(*dns.NS); ok {
|
|
|
|
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(authoritativeNss) > 0 {
|
|
|
|
return authoritativeNss, nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("could not determine authoritative nameservers")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update FQDN with CNAME if any
|
|
|
|
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
|
|
|
|
for _, rr := range r.Answer {
|
|
|
|
if cn, ok := rr.(*dns.CNAME); ok {
|
|
|
|
if cn.Hdr.Name == fqdn {
|
|
|
|
return cn.Target
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fqdn
|
|
|
|
}
|
|
|
|
|
2020-08-22 12:22:42 +10:00
|
|
|
// recursiveNameservers are used to pre-check DNS propagation. It
|
2021-06-09 05:14:45 +10:00
|
|
|
// picks user-configured nameservers (custom) OR the defaults
|
|
|
|
// obtained from resolv.conf and defaultNameservers if none is
|
|
|
|
// configured and ensures that all server addresses have a port value.
|
2020-08-22 12:22:42 +10:00
|
|
|
func recursiveNameservers(custom []string) []string {
|
2021-06-09 05:14:45 +10:00
|
|
|
var servers []string
|
|
|
|
if len(custom) == 0 {
|
|
|
|
servers = systemOrDefaultNameservers(defaultResolvConf, defaultNameservers)
|
|
|
|
} else {
|
|
|
|
servers = make([]string, len(custom))
|
|
|
|
copy(servers, custom)
|
|
|
|
}
|
2020-09-10 05:28:57 +10:00
|
|
|
populateNameserverPorts(servers)
|
|
|
|
return servers
|
2020-08-22 12:22:42 +10:00
|
|
|
}
|
2020-07-31 06:07:04 +10:00
|
|
|
|
|
|
|
var defaultNameservers = []string{
|
|
|
|
"8.8.8.8:53",
|
|
|
|
"8.8.4.4:53",
|
|
|
|
"1.1.1.1:53",
|
|
|
|
"1.0.0.1:53",
|
|
|
|
}
|
|
|
|
|
|
|
|
var dnsTimeout = 10 * time.Second
|
|
|
|
|
|
|
|
var (
|
|
|
|
fqdnSOACache = map[string]*soaCacheEntry{}
|
|
|
|
fqdnSOACacheMu sync.Mutex
|
|
|
|
)
|
|
|
|
|
|
|
|
const defaultResolvConf = "/etc/resolv.conf"
|