rfc2136.go raw

   1  // Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update.
   2  package rfc2136
   3  
   4  import (
   5  	"errors"
   6  	"fmt"
   7  	"net"
   8  	"strings"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/challenge"
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/platform/config/env"
  14  	"github.com/go-acme/lego/v4/providers/dns/rfc2136/internal"
  15  	"github.com/miekg/dns"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "RFC2136_"
  21  
  22  	EnvTSIGFile = envNamespace + "TSIG_FILE"
  23  
  24  	EnvTSIGKey       = envNamespace + "TSIG_KEY"
  25  	EnvTSIGSecret    = envNamespace + "TSIG_SECRET"
  26  	EnvTSIGAlgorithm = envNamespace + "TSIG_ALGORITHM"
  27  
  28  	EnvNameserver = envNamespace + "NAMESERVER"
  29  	EnvDNSTimeout = envNamespace + "DNS_TIMEOUT"
  30  
  31  	EnvTTL                = envNamespace + "TTL"
  32  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  33  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  34  	EnvSequenceInterval   = envNamespace + "SEQUENCE_INTERVAL"
  35  )
  36  
  37  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  38  
  39  // Config is used to configure the creation of the DNSProvider.
  40  type Config struct {
  41  	Nameserver string
  42  
  43  	TSIGFile string
  44  
  45  	TSIGAlgorithm string
  46  	TSIGKey       string
  47  	TSIGSecret    string
  48  
  49  	PropagationTimeout time.Duration
  50  	PollingInterval    time.Duration
  51  	TTL                int
  52  	SequenceInterval   time.Duration
  53  	DNSTimeout         time.Duration
  54  }
  55  
  56  // NewDefaultConfig returns a default configuration for the DNSProvider.
  57  func NewDefaultConfig() *Config {
  58  	return &Config{
  59  		TSIGAlgorithm:      env.GetOrDefaultString(EnvTSIGAlgorithm, dns.HmacSHA1),
  60  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  61  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, env.GetOrDefaultSecond("RFC2136_TIMEOUT", dns01.DefaultPropagationTimeout)),
  62  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  63  		SequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
  64  		DNSTimeout:         env.GetOrDefaultSecond(EnvDNSTimeout, 10*time.Second),
  65  	}
  66  }
  67  
  68  // DNSProvider implements the challenge.Provider interface.
  69  type DNSProvider struct {
  70  	config *Config
  71  }
  72  
  73  // NewDNSProvider returns a DNSProvider instance configured for rfc2136
  74  // dynamic update. Configured with environment variables:
  75  // RFC2136_NAMESERVER: Network address in the form "host" or "host:port".
  76  // RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5).
  77  // See https://github.com/miekg/dns/blob/master/tsig.go for supported values.
  78  // RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration.
  79  // RFC2136_TSIG_SECRET: Secret key payload.
  80  // RFC2136_PROPAGATION_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)
  81  // To disable TSIG authentication, leave the RFC2136_TSIG* variables unset.
  82  func NewDNSProvider() (*DNSProvider, error) {
  83  	values, err := env.Get(EnvNameserver)
  84  	if err != nil {
  85  		return nil, fmt.Errorf("rfc2136: %w", err)
  86  	}
  87  
  88  	config := NewDefaultConfig()
  89  	config.Nameserver = values[EnvNameserver]
  90  
  91  	config.TSIGFile = env.GetOrDefaultString(EnvTSIGFile, "")
  92  
  93  	config.TSIGKey = env.GetOrFile(EnvTSIGKey)
  94  	config.TSIGSecret = env.GetOrFile(EnvTSIGSecret)
  95  
  96  	return NewDNSProviderConfig(config)
  97  }
  98  
  99  // NewDNSProviderConfig return a DNSProvider instance configured for rfc2136.
 100  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 101  	if config == nil {
 102  		return nil, errors.New("rfc2136: the configuration of the DNS provider is nil")
 103  	}
 104  
 105  	if config.Nameserver == "" {
 106  		return nil, errors.New("rfc2136: nameserver missing")
 107  	}
 108  
 109  	if config.TSIGFile != "" {
 110  		key, err := internal.ReadTSIGFile(config.TSIGFile)
 111  		if err != nil {
 112  			return nil, fmt.Errorf("rfc2136: read TSIG file %s: %w", config.TSIGFile, err)
 113  		}
 114  
 115  		config.TSIGAlgorithm = key.Algorithm
 116  		config.TSIGKey = key.Name
 117  		config.TSIGSecret = key.Secret
 118  	}
 119  
 120  	// Append the default DNS port if none is specified.
 121  	if _, _, err := net.SplitHostPort(config.Nameserver); err != nil {
 122  		if strings.Contains(err.Error(), "missing port") {
 123  			config.Nameserver = net.JoinHostPort(config.Nameserver, "53")
 124  		} else {
 125  			return nil, fmt.Errorf("rfc2136: %w", err)
 126  		}
 127  	}
 128  
 129  	if config.TSIGKey == "" || config.TSIGSecret == "" {
 130  		config.TSIGKey = ""
 131  		config.TSIGSecret = ""
 132  	} else {
 133  		// zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
 134  		config.TSIGKey = dns.CanonicalName(config.TSIGKey)
 135  	}
 136  
 137  	if config.TSIGAlgorithm == "" {
 138  		config.TSIGAlgorithm = dns.HmacSHA1
 139  	} else {
 140  		// To be compatible with https://github.com/miekg/dns/blob/master/tsig.go
 141  		config.TSIGAlgorithm = dns.Fqdn(config.TSIGAlgorithm)
 142  	}
 143  
 144  	switch config.TSIGAlgorithm {
 145  	case dns.HmacSHA1, dns.HmacSHA224, dns.HmacSHA256, dns.HmacSHA384, dns.HmacSHA512:
 146  		// valid algorithm
 147  	default:
 148  		return nil, fmt.Errorf("rfc2136: unsupported TSIG algorithm: %s", config.TSIGAlgorithm)
 149  	}
 150  
 151  	return &DNSProvider{config: config}, nil
 152  }
 153  
 154  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 155  // Adjusting here to cope with spikes in propagation times.
 156  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 157  	return d.config.PropagationTimeout, d.config.PollingInterval
 158  }
 159  
 160  // Sequential All DNS challenges for this provider will be resolved sequentially.
 161  // Returns the interval between each iteration.
 162  func (d *DNSProvider) Sequential() time.Duration {
 163  	return d.config.SequenceInterval
 164  }
 165  
 166  // Present creates a TXT record using the specified parameters.
 167  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 168  	info := dns01.GetChallengeInfo(domain, keyAuth)
 169  
 170  	err := d.changeRecord("INSERT", info.EffectiveFQDN, info.Value, d.config.TTL)
 171  	if err != nil {
 172  		return fmt.Errorf("rfc2136: failed to insert: %w", err)
 173  	}
 174  
 175  	return nil
 176  }
 177  
 178  // CleanUp removes the TXT record matching the specified parameters.
 179  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 180  	info := dns01.GetChallengeInfo(domain, keyAuth)
 181  
 182  	err := d.changeRecord("REMOVE", info.EffectiveFQDN, info.Value, d.config.TTL)
 183  	if err != nil {
 184  		return fmt.Errorf("rfc2136: failed to remove: %w", err)
 185  	}
 186  
 187  	return nil
 188  }
 189  
 190  func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
 191  	// Find the zone for the given fqdn
 192  	zone, err := dns01.FindZoneByFqdnCustom(fqdn, []string{d.config.Nameserver})
 193  	if err != nil {
 194  		return err
 195  	}
 196  
 197  	// Create RR
 198  	rrs := []dns.RR{&dns.TXT{
 199  		Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)},
 200  		Txt: []string{value},
 201  	}}
 202  
 203  	// Create dynamic update packet
 204  	m := new(dns.Msg).SetUpdate(zone)
 205  
 206  	switch action {
 207  	case "INSERT":
 208  		// Always remove old challenge left over from who knows what.
 209  		m.RemoveRRset(rrs)
 210  		m.Insert(rrs)
 211  	case "REMOVE":
 212  		m.Remove(rrs)
 213  	default:
 214  		return fmt.Errorf("unexpected action: %s", action)
 215  	}
 216  
 217  	// Setup client
 218  	c := &dns.Client{Timeout: d.config.DNSTimeout}
 219  
 220  	// TSIG authentication / msg signing
 221  	if d.config.TSIGKey != "" && d.config.TSIGSecret != "" {
 222  		m.SetTsig(d.config.TSIGKey, d.config.TSIGAlgorithm, 300, time.Now().Unix())
 223  
 224  		// Secret(s) for TSIG map[<zonename>]<base64 secret>.
 225  		c.TsigSecret = map[string]string{d.config.TSIGKey: d.config.TSIGSecret}
 226  	}
 227  
 228  	// Send the query
 229  	reply, _, err := c.Exchange(m, d.config.Nameserver)
 230  	if err != nil {
 231  		return fmt.Errorf("DNS update failed: %w", err)
 232  	}
 233  
 234  	if reply != nil && reply.Rcode != dns.RcodeSuccess {
 235  		return fmt.Errorf("DNS update failed: server replied: %s", dns.RcodeToString[reply.Rcode])
 236  	}
 237  
 238  	return nil
 239  }
 240