@@ -24,10 +24,15 @@ import (
2424 "time"
2525
2626 "github.com/libdns/libdns"
27+ "github.com/miekg/dns"
2728)
2829
29- // TTL of the temporary DNS record used for DNS-01 validation.
30- const DefaultTTL = 30 * time .Second
30+ const (
31+ // TTL of the temporary DNS record used for DNS-01 validation.
32+ DefaultTTL = 30 * time .Second
33+ // Typical negative response TTL defined in the SOA.
34+ defaultDNSPropagationTimeout = 300 * time .Second
35+ )
3136
3237// DNSProvider defines the operations required for dns-01 challenges.
3338type DNSProvider interface {
@@ -39,6 +44,18 @@ type DNSProvider interface {
3944type DNS01Solver struct {
4045 provider DNSProvider
4146 TTL time.Duration
47+
48+ // How long to wait before starting propagation checks.
49+ // Default: 0 (no wait).
50+ PropagationDelay time.Duration
51+
52+ // Maximum time to wait for temporary DNS record to appear.
53+ // Set to -1 to disable propagation checks.
54+ // Default: 2 minutes.
55+ PropagationTimeout time.Duration
56+
57+ // Preferred DNS resolver(s) to use when doing DNS lookups.
58+ Resolvers []string
4259}
4360
4461func NewDNS01Solver (name string , params map [string ]any , ttl ... time.Duration ) (* DNS01Solver , error ) {
@@ -60,7 +77,7 @@ func (s *DNS01Solver) Present(ctx context.Context, domain, zone, keyAuth string)
6077 rec := makeRecord (domain , keyAuth , s .TTL )
6178
6279 if zone == "" {
63- zone = guessZone (domain )
80+ zone = GuessZone (domain )
6481 } else {
6582 zone = rooted (zone )
6683 }
@@ -76,12 +93,66 @@ func (s *DNS01Solver) Present(ctx context.Context, domain, zone, keyAuth string)
7693 return nil
7794}
7895
96+ // Wait blocks until the TXT record created in Present() appears in
97+ // authoritative lookups, i.e. until it has propagated, or until
98+ // timeout, whichever is first.
99+ func (s * DNS01Solver ) Wait (ctx context.Context , domain , zone , keyAuth string ) error {
100+ // if configured to, pause before doing propagation checks
101+ // (even if they are disabled, the wait might be desirable on its own)
102+ if s .PropagationDelay > 0 {
103+ select {
104+ case <- time .After (s .PropagationDelay ):
105+ case <- ctx .Done ():
106+ return ctx .Err ()
107+ }
108+ }
109+
110+ // skip propagation checks if configured to do so
111+ if s .PropagationTimeout == - 1 {
112+ return nil
113+ }
114+
115+ // timings
116+ timeout := s .PropagationTimeout
117+ if timeout == 0 {
118+ timeout = defaultDNSPropagationTimeout
119+ }
120+ const interval = 5 * time .Second
121+
122+ // how we'll do the checks
123+ checkAuthoritativeServers := len (s .Resolvers ) == 0
124+ resolvers := RecursiveNameservers (s .Resolvers )
125+
126+ absName := strings .Trim (domain , "." )
127+
128+ var err error
129+ start := time .Now ()
130+ for time .Since (start ) < timeout {
131+ select {
132+ case <- time .After (interval ):
133+ case <- ctx .Done ():
134+ return ctx .Err ()
135+ }
136+
137+ var ready bool
138+ ready , err = checkDNSPropagation (ctx , absName , dns .TypeTXT , keyAuth , checkAuthoritativeServers , resolvers )
139+ if err != nil {
140+ return fmt .Errorf ("checking DNS propagation of %q (resolvers=%v): %w" , absName , resolvers , err )
141+ }
142+ if ready {
143+ return nil
144+ }
145+ }
146+
147+ return fmt .Errorf ("DNS propagation timed out. Last error: %v" , err )
148+ }
149+
79150// CleanUp deletes the DNS TXT record created in Present().
80151func (s * DNS01Solver ) CleanUp (ctx context.Context , domain , zone , keyAuth string ) error {
81152 rr := makeRecord (domain , keyAuth , s .TTL )
82153
83154 if zone == "" {
84- zone = guessZone (domain )
155+ zone = GuessZone (domain )
85156 } else {
86157 zone = rooted (zone )
87158 }
@@ -104,13 +175,8 @@ func makeRecord(fqdn, keyAuth string, ttl time.Duration) libdns.RR {
104175 }
105176}
106177
107- // Extract the root zone for a domain in case the user did not provide it.
108- //
109- // This simplistic algorithm will only work for simple cases. The correct
110- // way to do this would be to do an SOA request on the FQDN, but since
111- // dataplaneapi may not use the right resolvers (as configured in haproxy.cfg)
112- // it is better to avoid doing any DNS request.
113- func guessZone (fqdn string ) string {
178+ // Guess the root zone for a domain when we cannot use a better method.
179+ func GuessZone (fqdn string ) string {
114180 fqdn = trimWildcard (fqdn )
115181 parts := make ([]string , 0 , 8 )
116182 strings .SplitSeq (fqdn , "." )(func (part string ) bool {
0 commit comments