Skip to content

Commit ba67a26

Browse files
authored
feat: improve errors and logs related to DNS call (#2109)
1 parent 7fe1796 commit ba67a26

File tree

87 files changed

+314
-182
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+314
-182
lines changed

challenge/dns01/dns_challenge.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"strconv"
9+
"strings"
910
"time"
1011

1112
"github.com/go-acme/lego/v4/acme"
@@ -124,7 +125,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
124125
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
125126
}
126127

127-
log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
128+
log.Infof("[%s] acme: Checking DNS record propagation. [nameservers=%s]", domain, strings.Join(recursiveNameservers, ","))
128129

129130
time.Sleep(interval)
130131

challenge/dns01/dns_challenge_manual.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
2525

2626
authZone, err := FindZoneByFqdn(info.EffectiveFQDN)
2727
if err != nil {
28-
return err
28+
return fmt.Errorf("manual: could not find zone: %w", err)
2929
}
3030

3131
fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone)
3232
fmt.Printf(dnsTemplate+"\n", info.EffectiveFQDN, DefaultTTL, info.Value)
3333
fmt.Printf("lego: Press 'Enter' when you are done\n")
3434

3535
_, err = bufio.NewReader(os.Stdin).ReadBytes('\n')
36+
if err != nil {
37+
return fmt.Errorf("manual: %w", err)
38+
}
3639

37-
return err
40+
return nil
3841
}
3942

4043
// CleanUp prints instructions for manually removing the TXT record.
@@ -43,7 +46,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
4346

4447
authZone, err := FindZoneByFqdn(info.EffectiveFQDN)
4548
if err != nil {
46-
return err
49+
return fmt.Errorf("manual: could not find zone: %w", err)
4750
}
4851

4952
fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone)

challenge/dns01/nameserver.go

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ func lookupNameservers(fqdn string) ([]string, error) {
9999

100100
zone, err := FindZoneByFqdn(fqdn)
101101
if err != nil {
102-
return nil, fmt.Errorf("could not determine the zone: %w", err)
102+
return nil, fmt.Errorf("could not find zone: %w", err)
103103
}
104104

105105
r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true)
106106
if err != nil {
107-
return nil, err
107+
return nil, fmt.Errorf("NS call failed: %w", err)
108108
}
109109

110110
for _, rr := range r.Answer {
@@ -116,7 +116,8 @@ func lookupNameservers(fqdn string) ([]string, error) {
116116
if len(authoritativeNss) > 0 {
117117
return authoritativeNss, nil
118118
}
119-
return nil, errors.New("could not determine authoritative nameservers")
119+
120+
return nil, fmt.Errorf("[zone=%s] could not determine authoritative nameservers", zone)
120121
}
121122

122123
// FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn
@@ -130,7 +131,7 @@ func FindPrimaryNsByFqdn(fqdn string) (string, error) {
130131
func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) {
131132
soa, err := lookupSoaByFqdn(fqdn, nameservers)
132133
if err != nil {
133-
return "", err
134+
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
134135
}
135136
return soa.primaryNs, nil
136137
}
@@ -146,7 +147,7 @@ func FindZoneByFqdn(fqdn string) (string, error) {
146147
func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
147148
soa, err := lookupSoaByFqdn(fqdn, nameservers)
148149
if err != nil {
149-
return "", err
150+
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
150151
}
151152
return soa.zone, nil
152153
}
@@ -171,35 +172,35 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
171172

172173
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
173174
var err error
174-
var in *dns.Msg
175+
var r *dns.Msg
175176

176177
labelIndexes := dns.Split(fqdn)
177178
for _, index := range labelIndexes {
178179
domain := fqdn[index:]
179180

180-
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
181+
r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
181182
if err != nil {
182183
continue
183184
}
184185

185-
if in == nil {
186+
if r == nil {
186187
continue
187188
}
188189

189-
switch in.Rcode {
190+
switch r.Rcode {
190191
case dns.RcodeSuccess:
191192
// Check if we got a SOA RR in the answer section
192-
if len(in.Answer) == 0 {
193+
if len(r.Answer) == 0 {
193194
continue
194195
}
195196

196197
// CNAME records cannot/should not exist at the root of a zone.
197198
// So we skip a domain when a CNAME is found.
198-
if dnsMsgContainsCNAME(in) {
199+
if dnsMsgContainsCNAME(r) {
199200
continue
200201
}
201202

202-
for _, ans := range in.Answer {
203+
for _, ans := range r.Answer {
203204
if soa, ok := ans.(*dns.SOA); ok {
204205
return newSoaCacheEntry(soa), nil
205206
}
@@ -208,11 +209,11 @@ func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
208209
// NXDOMAIN
209210
default:
210211
// Any response code other than NOERROR and NXDOMAIN is treated as error
211-
return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
212+
return nil, &DNSError{Message: fmt.Sprintf("unexpected response for '%s'", domain), MsgOut: r}
212213
}
213214
}
214215

215-
return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
216+
return nil, &DNSError{Message: fmt.Sprintf("could not find the start of authority for '%s'", fqdn), MsgOut: r, Err: err}
216217
}
217218

218219
// dnsMsgContainsCNAME checks for a CNAME answer in msg.
@@ -226,16 +227,28 @@ func dnsMsgContainsCNAME(msg *dns.Msg) bool {
226227
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
227228
m := createDNSMsg(fqdn, rtype, recursive)
228229

229-
var in *dns.Msg
230+
if len(nameservers) == 0 {
231+
return nil, &DNSError{Message: "empty list of nameservers"}
232+
}
233+
234+
var r *dns.Msg
230235
var err error
236+
var errAll error
231237

232238
for _, ns := range nameservers {
233-
in, err = sendDNSQuery(m, ns)
234-
if err == nil && len(in.Answer) > 0 {
239+
r, err = sendDNSQuery(m, ns)
240+
if err == nil && len(r.Answer) > 0 {
235241
break
236242
}
243+
244+
errAll = errors.Join(errAll, err)
245+
}
246+
247+
if err != nil {
248+
return r, errAll
237249
}
238-
return in, err
250+
251+
return r, nil
239252
}
240253

241254
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
@@ -253,37 +266,82 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
253266
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
254267
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok {
255268
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
256-
in, _, err := tcp.Exchange(m, ns)
269+
r, _, err := tcp.Exchange(m, ns)
270+
if err != nil {
271+
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err}
272+
}
257273

258-
return in, err
274+
return r, nil
259275
}
260276

261277
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
262-
in, _, err := udp.Exchange(m, ns)
278+
r, _, err := udp.Exchange(m, ns)
263279

264-
if in != nil && in.Truncated {
280+
if r != nil && r.Truncated {
265281
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
266282
// If the TCP request succeeds, the "err" will reset to nil
267-
in, _, err = tcp.Exchange(m, ns)
283+
r, _, err = tcp.Exchange(m, ns)
284+
}
285+
286+
if err != nil {
287+
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err}
268288
}
269289

270-
return in, err
290+
return r, nil
271291
}
272292

273-
func formatDNSError(msg *dns.Msg, err error) string {
274-
var parts []string
293+
// DNSError error related to DNS calls.
294+
type DNSError struct {
295+
Message string
296+
NS string
297+
MsgIn *dns.Msg
298+
MsgOut *dns.Msg
299+
Err error
300+
}
275301

276-
if msg != nil {
277-
parts = append(parts, dns.RcodeToString[msg.Rcode])
302+
func (d *DNSError) Error() string {
303+
var details []string
304+
if d.NS != "" {
305+
details = append(details, "ns="+d.NS)
278306
}
279307

280-
if err != nil {
281-
parts = append(parts, err.Error())
308+
if d.MsgIn != nil && len(d.MsgIn.Question) > 0 {
309+
details = append(details, fmt.Sprintf("question='%s'", formatQuestions(d.MsgIn.Question)))
310+
}
311+
312+
if d.MsgOut != nil {
313+
if d.MsgIn == nil || len(d.MsgIn.Question) == 0 {
314+
details = append(details, fmt.Sprintf("question='%s'", formatQuestions(d.MsgOut.Question)))
315+
}
316+
317+
details = append(details, "code="+dns.RcodeToString[d.MsgOut.Rcode])
318+
}
319+
320+
msg := "DNS error"
321+
if d.Message != "" {
322+
msg = d.Message
323+
}
324+
325+
if d.Err != nil {
326+
msg += ": " + d.Err.Error()
327+
}
328+
329+
if len(details) > 0 {
330+
msg += " [" + strings.Join(details, ", ") + "]"
282331
}
283332

284-
if len(parts) > 0 {
285-
return ": " + strings.Join(parts, " ")
333+
return msg
334+
}
335+
336+
func (d *DNSError) Unwrap() error {
337+
return d.Err
338+
}
339+
340+
func formatQuestions(questions []dns.Question) string {
341+
var parts []string
342+
for _, question := range questions {
343+
parts = append(parts, strings.ReplaceAll(strings.TrimPrefix(question.String(), ";"), "\t", " "))
286344
}
287345

288-
return ""
346+
return strings.Join(parts, ";")
289347
}

0 commit comments

Comments
 (0)