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