Skip to content

Commit 51eaa5a

Browse files
oliwermjuraga
authored andcommitted
MEDIUM: Implement DNS propagation checks for ACME
For now, the propagation check timeout defaults to 1 hour, but can be customized using the environment variables `DPAPI_ACME_PROPAGTIMEOUT_SEC` and `DPAPI_ACME_PROPAGDELAY_SEC`, with a positive number of seconds. Setting `DPAPI_ACME_PROPAGTIMEOUT_SEC` to -1 will disable propagation checks. The propagation check will use the system's DNS servers as configured in /etc/resolv.conf, or fallback to a hard-coded list of public DNS resolvers. In a future version of HAProxy, it will be possible to configure those settings directly from haproxy.cfg.
1 parent e382733 commit 51eaa5a

File tree

8 files changed

+463
-18
lines changed

8 files changed

+463
-18
lines changed

.aspell.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ allowed:
4444
- txt
4545
- testname
4646
- uid
47+
- DPAPI
48+
- PROPAGDELAY
49+
- PROPAGTIMEOUT
50+
- cfg
51+
- resolv
52+
- conf
53+
- resolvers

acme/dns01.go

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3338
type DNSProvider interface {
@@ -39,6 +44,18 @@ type DNSProvider interface {
3944
type 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

4461
func 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().
80151
func (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 {

acme/dns01_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func Test_guessZone(t *testing.T) {
9494
}
9595
for _, tt := range tests {
9696
t.Run(tt.fqdn, func(t *testing.T) {
97-
got := guessZone(tt.fqdn)
97+
got := GuessZone(tt.fqdn)
9898
if got != tt.want {
9999
t.Errorf("guessZone() = %v, want %v", got, tt.want)
100100
}

0 commit comments

Comments
 (0)