1// Package spf implements Sender Policy Framework (SPF, RFC 7208) for verifying
2// remote mail server IPs with their published records.
4// With SPF a domain can publish a policy as a DNS TXT record describing which IPs
5// are allowed to send email with SMTP with the domain in the MAIL FROM command,
6// and how to treat SMTP transactions coming from other IPs.
20 "github.com/mjl-/mox/dns"
21 "github.com/mjl-/mox/mlog"
22 "github.com/mjl-/mox/smtp"
23 "github.com/mjl-/mox/stub"
26// The net package always returns DNS names in absolute, lower-case form. We make
27// sure we make names absolute when looking up. For verifying, we do not want to
28// verify names relative to our local search domain.
31 MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
34// cross-link rfc and errata
42 ErrName = errors.New("spf: bad domain name")
43 ErrNoRecord = errors.New("spf: no txt record")
44 ErrMultipleRecords = errors.New("spf: multiple spf txt records in dns")
45 ErrDNS = errors.New("spf: lookup of dns record")
46 ErrRecordSyntax = errors.New("spf: malformed spf txt record")
49 ErrTooManyDNSRequests = errors.New("spf: too many dns requests")
50 ErrTooManyVoidLookups = errors.New("spf: too many void lookups")
51 ErrMacroSyntax = errors.New("spf: bad macro syntax")
55 // Maximum number of DNS requests to execute. This excludes some requests, such as
56 // lookups of MX host results.
59 // Maximum number of DNS lookups that result in no records before a StatusPermerror
60 // is returned. This limit aims to prevent abuse.
64// Status is the result of an SPF verification.
71 StatusNone Status = "none" // E.g. no DNS domain name in session, or no SPF record in DNS.
72 StatusNeutral Status = "neutral" // Explicit statement that nothing is said about the IP, "?" qualifier. None and Neutral must be treated the same.
73 StatusPass Status = "pass" // IP is authorized.
74 StatusFail Status = "fail" // IP is exlicitly not authorized. "-" qualifier.
75 StatusSoftfail Status = "softfail" // Weak statement that IP is probably not authorized, "~" qualifier.
76 StatusTemperror Status = "temperror" // Trying again later may succeed, e.g. for temporary DNS lookup error.
77 StatusPermerror Status = "permerror" // Error requiring some intervention to correct. E.g. invalid DNS record.
80// Args are the parameters to the SPF verification algorithm ("check_host" in the RFC).
82// All fields should be set as they can be required for macro expansions.
84 // RemoteIP will be checked as sender for email.
87 // Address from SMTP MAIL FROM command. Zero values for a null reverse path (used for DSNs).
88 MailFromLocalpart smtp.Localpart
89 MailFromDomain dns.Domain
91 // HelloDomain is from the SMTP EHLO/HELO command.
92 HelloDomain dns.IPDomain
95 LocalHostname dns.Domain
97 // Explanation string to use for failure. In case of "include", where explanation
98 // from original domain must be used.
99 // May be set for recursive calls.
102 // Domain to validate.
105 // Effective sender. Equal to MailFrom if non-zero, otherwise set to "postmaster" at HelloDomain.
106 senderLocalpart smtp.Localpart
107 senderDomain dns.Domain
109 // To enforce the limit on lookups. Initialized automatically if nil.
114// Mocked for testing expanding "t" macro.
115var timeNow = time.Now
117// Lookup looks up and parses an SPF TXT record for domain.
119// Authentic indicates if the DNS results were DNSSEC-verified.
120func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, authentic bool, rerr error) {
121 log := mlog.New("spf", elog)
124 log.Debugx("spf lookup result", rerr,
125 slog.Any("domain", domain),
126 slog.Any("status", rstatus),
127 slog.Any("record", rrecord),
128 slog.Duration("duration", time.Since(start)))
132 host := domain.ASCII + "."
133 if err := validateDNS(host); err != nil {
134 return StatusNone, "", nil, false, fmt.Errorf("%w: %s: %s", ErrName, domain, err)
137 // Lookup spf record.
138 txts, result, err := dns.WithPackage(resolver, "spf").LookupTXT(ctx, host)
139 if dns.IsNotFound(err) {
140 return StatusNone, "", nil, result.Authentic, fmt.Errorf("%w for %s", ErrNoRecord, host)
141 } else if err != nil {
142 return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s: %s", ErrDNS, host, err)
145 // Parse the records. We only handle those that look like spf records.
148 for _, txt := range txts {
150 r, isspf, err := ParseRecord(txt)
154 } else if err != nil {
156 return StatusPermerror, txt, nil, result.Authentic, fmt.Errorf("%w: %s", ErrRecordSyntax, err)
160 return StatusPermerror, "", nil, result.Authentic, ErrMultipleRecords
167 return StatusNone, "", nil, result.Authentic, ErrNoRecord
169 return StatusNone, text, record, result.Authentic, nil
172// Verify checks if a remote IP is allowed to send email for a domain.
174// If the SMTP "MAIL FROM" is set, it is used as identity (domain) to verify.
175// Otherwise, the EHLO domain is verified if it is a valid domain.
177// The returned Received.Result status will always be set, regardless of whether an
179// For status Temperror and Permerror, an error is always returned.
180// For Fail, explanation may be set, and should be returned in the SMTP session if
181// it is the reason the message is rejected. The caller should ensure the
182// explanation is valid for use in SMTP, taking line length and ascii-only
183// requirement into account.
185// Verify takes the maximum number of 10 DNS requests into account, and the maximum
186// of 2 lookups resulting in no records ("void lookups").
188// Authentic indicates if the DNS results were DNSSEC-verified.
189func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, authentic bool, rerr error) {
190 log := mlog.New("spf", elog)
193 MetricVerify.ObserveLabels(float64(time.Since(start))/float64(time.Second), string(received.Result))
194 log.Debugx("spf verify result", rerr,
195 slog.Any("domain", args.domain),
196 slog.Any("ip", args.RemoteIP),
197 slog.Any("status", received.Result),
198 slog.String("explanation", explanation),
199 slog.Duration("duration", time.Since(start)))
202 isHello, ok := prepare(&args)
206 Comment: "no domain, ehlo is an ip literal and mailfrom is empty",
207 ClientIP: args.RemoteIP,
208 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.HelloDomain.IP.String()),
209 Helo: args.HelloDomain,
210 Receiver: args.LocalHostname.ASCII,
212 return received, dns.Domain{}, "", false, nil
215 status, mechanism, expl, authentic, err := checkHost(ctx, log, resolver, args)
216 comment := fmt.Sprintf("domain %s", args.domain.ASCII)
218 comment += ", from ehlo because mailfrom is empty"
223 ClientIP: args.RemoteIP,
224 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.senderDomain.ASCII), //
../rfc/7208:2090, explicitly "sender", not "mailfrom".
225 Helo: args.HelloDomain,
226 Receiver: args.LocalHostname.ASCII,
227 Mechanism: mechanism,
230 received.Problem = err.Error()
233 received.Identity = "helo"
235 received.Identity = "mailfrom"
237 return received, args.domain, expl, authentic, err
240// prepare args, setting fields sender* and domain as required for checkHost.
241func prepare(args *Args) (isHello bool, ok bool) {
242 // If MAIL FROM is set, that identity is used. Otherwise the EHLO identity is used.
243 // MAIL FROM is preferred, because if we accept the message, and we have to send a
244 // DSN, it helps to know it is a verified sender. If we would check an EHLO
245 // identity, and it is different from the MAIL FROM, we may be sending the DSN to
246 // an address with a domain that would not allow sending from the originating IP.
251 args.explanation = nil
252 args.dnsRequests = nil
253 args.voidLookups = nil
254 if args.MailFromDomain.IsZero() {
255 // If there is on EHLO, and it is an IP, there is nothing to SPF-validate.
256 if !args.HelloDomain.IsDomain() {
259 // If we have a mailfrom, we also have a localpart. But for EHLO we won't.
../rfc/7208:810
260 args.senderLocalpart = "postmaster"
261 args.senderDomain = args.HelloDomain.Domain
264 args.senderLocalpart = args.MailFromLocalpart
265 args.senderDomain = args.MailFromDomain
267 args.domain = args.senderDomain
271// lookup spf record, then evaluate args against it.
272func checkHost(ctx context.Context, log mlog.Log, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
273 status, _, record, rauthentic, err := Lookup(ctx, log.Logger, resolver, args.domain)
275 return status, "", "", rauthentic, err
278 var evalAuthentic bool
279 rstatus, mechanism, rexplanation, evalAuthentic, rerr = evaluate(ctx, log, record, resolver, args)
280 rauthentic = rauthentic && evalAuthentic
284// Evaluate evaluates the IP and names from args against the SPF DNS record for the domain.
285func Evaluate(ctx context.Context, elog *slog.Logger, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
286 log := mlog.New("spf", elog)
287 _, ok := prepare(&args)
289 return StatusNone, "default", "", false, fmt.Errorf("no domain name to validate")
291 return evaluate(ctx, log, record, resolver, args)
294// evaluate RemoteIP against domain from args, given record.
295func evaluate(ctx context.Context, log mlog.Log, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
298 log.Debugx("spf evaluate result", rerr,
299 slog.Int("dnsrequests", *args.dnsRequests),
300 slog.Int("voidlookups", *args.voidLookups),
301 slog.Any("domain", args.domain),
302 slog.Any("status", rstatus),
303 slog.String("mechanism", mechanism),
304 slog.String("explanation", rexplanation),
305 slog.Duration("duration", time.Since(start)))
308 if args.dnsRequests == nil {
309 args.dnsRequests = new(int)
310 args.voidLookups = new(int)
313 // Response is authentic until we find a non-authentic DNS response.
316 // To4 returns nil for an IPv6 address. To16 will return an IPv4-to-IPv6-mapped address.
318 remote4 := args.RemoteIP.To4()
320 remote6 = args.RemoteIP.To16()
323 // Check if ip matches remote ip, taking cidr mask into account.
324 checkIP := func(ip net.IP, d Directive) bool {
332 if d.IP4CIDRLen != nil {
335 mask := net.CIDRMask(ones, 32)
336 return ip4.Mask(mask).Equal(remote4.Mask(mask))
344 if d.IP6CIDRLen != nil {
347 mask := net.CIDRMask(ones, 128)
348 return ip6.Mask(mask).Equal(remote6.Mask(mask))
351 // Used for "a" and "mx".
352 checkHostIP := func(domain dns.Domain, d Directive, args *Args) (bool, Status, error) {
353 ips, result, err := resolver.LookupIP(ctx, "ip", domain.ASCII+".")
354 rauthentic = rauthentic && result.Authentic
355 trackVoidLookup(err, args)
356 // If "not found", we must ignore the error and treat as zero records in answer.
../rfc/7208:1116
357 if err != nil && !dns.IsNotFound(err) {
358 return false, StatusTemperror, err
360 for _, ip := range ips {
362 return true, StatusPass, nil
365 return false, StatusNone, nil
368 for _, d := range record.Directives {
372 case "include", "a", "mx", "ptr", "exists":
373 if err := trackLookupLimits(&args); err != nil {
374 return StatusPermerror, d.MechanismString(), "", rauthentic, err
385 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
386 rauthentic = rauthentic && authentic
388 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for include: %w", err)
391 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
393 status, _, _, authentic, err := checkHost(ctx, log, resolver, nargs)
394 rauthentic = rauthentic && authentic
399 case StatusTemperror:
400 return StatusTemperror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q: %w", name, err)
401 case StatusPermerror, StatusNone:
402 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q resulted in status %q: %w", name, status, err)
407 // note: the syntax for DomainSpec hints that macros should be expanded. But
408 // expansion is explicitly documented, and only for "include", "exists" and
409 // "redirect". This reason for this could be low-effort reuse of the domain-spec
410 // ABNF rule. It could be an oversight. We are not implementing expansion for the
411 // mechanism for which it isn't specified.
412 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
414 return StatusPermerror, d.MechanismString(), "", rauthentic, err
416 hmatch, status, err := checkHostIP(host, d, &args)
418 return status, d.MechanismString(), "", rauthentic, err
424 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
426 return StatusPermerror, d.MechanismString(), "", rauthentic, err
428 // Note: LookupMX can return an error and still return MX records.
429 mxs, result, err := resolver.LookupMX(ctx, host.ASCII+".")
430 rauthentic = rauthentic && result.Authentic
431 trackVoidLookup(err, &args)
432 // note: we handle "not found" simply as a result of zero mx records.
433 if err != nil && !dns.IsNotFound(err) {
434 return StatusTemperror, d.MechanismString(), "", rauthentic, err
436 if err == nil && len(mxs) == 1 && mxs[0].Host == "." {
440 for i, mx := range mxs {
442 // requests. This seems independent of the overall limit of 10 DNS requests. So an
443 // MX request resulting in 11 names is valid, but we must return a permerror if we
444 // found no match before the 11th name.
447 return StatusPermerror, d.MechanismString(), "", rauthentic, ErrTooManyDNSRequests
449 // Parsing lax (unless in pedantic mode) for MX targets with underscores as seen in the wild.
450 mxd, err := dns.ParseDomainLax(strings.TrimSuffix(mx.Host, "."))
452 return StatusPermerror, d.MechanismString(), "", rauthentic, err
454 hmatch, status, err := checkHostIP(mxd, d, &args)
456 return status, d.MechanismString(), "", rauthentic, err
466 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
468 return StatusPermerror, d.MechanismString(), "", rauthentic, err
471 rnames, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
472 rauthentic = rauthentic && result.Authentic
473 trackVoidLookup(err, &args)
474 if err != nil && !dns.IsNotFound(err) {
475 return StatusTemperror, d.MechanismString(), "", rauthentic, err
479 for _, rname := range rnames {
480 rd, err := dns.ParseDomain(strings.TrimSuffix(rname, "."))
482 log.Errorx("bad address in ptr record", err, slog.String("address", rname))
486 if rd.ASCII != host.ASCII && !strings.HasSuffix(rd.ASCII, "."+host.ASCII) {
495 ips, result, err := resolver.LookupIP(ctx, "ip", rd.ASCII+".")
496 rauthentic = rauthentic && result.Authentic
497 trackVoidLookup(err, &args)
498 for _, ip := range ips {
509 match = checkIP(d.IP, d)
513 match = checkIP(d.IP, d)
518 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
519 rauthentic = rauthentic && authentic
521 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for exists: %w", err)
524 ips, result, err := resolver.LookupIP(ctx, "ip4", ensureAbsDNS(name))
525 rauthentic = rauthentic && result.Authentic
526 // Note: we do count this for void lookups, as that is an anti-abuse mechanism.
528 trackVoidLookup(err, &args)
529 if err != nil && !dns.IsNotFound(err) {
530 return StatusTemperror, d.MechanismString(), "", rauthentic, err
535 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected mechanism %q", d.Mechanism)
543 return StatusPass, d.MechanismString(), "", rauthentic, nil
545 return StatusNeutral, d.MechanismString(), "", rauthentic, nil
549 authentic, expl := explanation(ctx, resolver, record, nargs)
550 rauthentic = rauthentic && authentic
551 return StatusFail, d.MechanismString(), expl, rauthentic, nil
553 return StatusSoftfail, d.MechanismString(), "", rauthentic, nil
555 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected qualifier %q", d.Qualifier)
558 if record.Redirect != "" {
562 name, authentic, err := expandDomainSpecDNS(ctx, resolver, record.Redirect, args)
563 rauthentic = rauthentic && authentic
565 return StatusPermerror, "", "", rauthentic, fmt.Errorf("expanding domain-spec: %w", err)
568 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
570 status, mechanism, expl, authentic, err := checkHost(ctx, log, resolver, nargs)
571 rauthentic = rauthentic && authentic
572 if status == StatusNone {
573 return StatusPermerror, mechanism, "", rauthentic, err
575 return status, mechanism, expl, rauthentic, err
579 return StatusNeutral, "default", "", rauthentic, nil
582// evaluateDomainSpec returns the parsed dns domain for spec if non-empty, and
583// otherwise returns d, which must be the Domain in checkHost Args.
584func evaluateDomainSpec(spec string, d dns.Domain) (dns.Domain, error) {
589 d, err := dns.ParseDomain(spec)
591 return d, fmt.Errorf("%w: %s", ErrName, err)
596func expandDomainSpecDNS(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
597 return expandDomainSpec(ctx, resolver, domainSpec, args, true)
600func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
601 return expandDomainSpec(ctx, resolver, domainSpec, args, false)
604// expandDomainSpec interprets macros in domainSpec.
605// The expansion can fail due to macro syntax errors or DNS errors.
609func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args, dns bool) (string, bool, error) {
612 rauthentic := true // Until non-authentic record is found.
616 b := &strings.Builder{}
628 return "", rauthentic, fmt.Errorf("%w: trailing bare %%", ErrMacroSyntax)
642 return "", rauthentic, fmt.Errorf("%w: invalid macro opening %%%c", ErrMacroSyntax, c)
646 return "", rauthentic, fmt.Errorf("%w: missing macro ending }", ErrMacroSyntax)
652 if c >= 'A' && c <= 'Z' {
660 // todo: should we check for utf8 in localpart, and fail? we may now generate utf8 strings to places that may not be able to parse them. it will probably lead to relatively harmless error somewhere else. perhaps we can just transform the localpart to IDN? because it may be used in a dns lookup.
../rfc/7208:1507
661 v = smtp.NewAddress(args.senderLocalpart, args.senderDomain).String()
663 // todo: same about utf8 as for 's'.
664 v = string(args.senderLocalpart)
666 v = args.senderDomain.ASCII
668 v = args.domain.ASCII
670 v = expandIP(args.RemoteIP)
673 if err := trackLookupLimits(&args); err != nil {
674 return "", rauthentic, err
676 names, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
677 rauthentic = rauthentic && result.Authentic
678 trackVoidLookup(err, &args)
679 if len(names) == 0 || err != nil {
685 // Verify finds the first dns name that resolves to the remote ip.
686 verify := func(matchfn func(string) bool) (string, error) {
687 for _, name := range names {
691 ips, result, err := resolver.LookupIP(ctx, "ip", name)
692 rauthentic = rauthentic && result.Authentic
693 trackVoidLookup(err, &args)
695 for _, ip := range ips {
696 if ip.Equal(args.RemoteIP) {
704 // First exact domain name matches, then subdomains, finally other names.
705 domain := args.domain.ASCII + "."
706 dotdomain := "." + domain
707 v, err = verify(func(name string) bool { return name == domain })
709 return "", rauthentic, err
712 v, err = verify(func(name string) bool { return strings.HasSuffix(name, dotdomain) })
714 return "", rauthentic, err
718 v, err = verify(func(name string) bool { return name != domain && !strings.HasSuffix(name, dotdomain) })
720 return "", rauthentic, err
729 if args.RemoteIP.To4() != nil {
735 if args.HelloDomain.IsIP() {
736 //
../rfc/7208:1621 explicitly says "domain", not "ip". We'll handle IP, probably does no harm.
737 v = expandIP(args.HelloDomain.IP)
739 v = args.HelloDomain.Domain.ASCII
743 return "", rauthentic, fmt.Errorf("%w: macro letter %c only allowed in exp", ErrMacroSyntax, c)
747 v = args.LocalIP.String()
749 v = args.LocalHostname.ASCII
751 v = fmt.Sprintf("%d", timeNow().Unix())
754 return "", rauthentic, fmt.Errorf("%w: unknown macro letter %c", ErrMacroSyntax, c)
758 for i < n && s[i] >= '0' && s[i] <= '9' {
759 digits += string(s[i])
764 v, err := strconv.Atoi(digits)
766 return "", rauthentic, fmt.Errorf("%w: bad macro transformer digits %q: %s", ErrMacroSyntax, digits, err)
770 return "", rauthentic, fmt.Errorf("%w: zero labels for digits transformer", ErrMacroSyntax)
774 // If "r" follows, we must reverse the resulting name, splitting on a dot by default.
777 if i < n && (s[i] == 'r' || s[i] == 'R') {
782 // Delimiters to split on, for subset of labels and/or reversing.
786 case '.', '-', '+', ',', '/', '_', '=':
787 delim += string(s[i])
794 if i >= n || s[i] != '}' {
795 return "", rauthentic, fmt.Errorf("%w: missing closing } for macro", ErrMacroSyntax)
799 // Only split and subset and/or reverse if necessary.
800 if nlabels >= 0 || reverse || delim != "" {
809 for i := 0; i < h; i++ {
810 t[i], t[nt-1-i] = t[nt-1-i], t[i]
813 if nlabels > 0 && nlabels < len(t) {
814 t = t[len(t)-nlabels:]
817 v = strings.Join(t, ".")
822 v = url.QueryEscape(v)
829 isAbs := strings.HasSuffix(r, ".")
831 if err := validateDNS(r); err != nil {
832 return "", rauthentic, fmt.Errorf("invalid dns name: %s", err)
834 // If resulting name is too large, cut off labels on the left until it fits.
../rfc/7208:1749
836 labels := strings.Split(r, ".")
837 for i := range labels {
838 if i == len(labels)-1 {
839 return "", rauthentic, fmt.Errorf("expanded dns name too long")
841 s := strings.Join(labels[i+1:], ".")
852 return r, rauthentic, nil
855func expandIP(ip net.IP) string {
861 for i, b := range ip.To16() {
865 v += fmt.Sprintf("%x.%x", b>>4, b&0xf)
870// validateDNS checks if a DNS name is valid. Must not end in dot. This does not
871// check valid host names, e.g. _ is allowed in DNS but not in a host name.
872func validateDNS(s string) error {
874 // note: we are not checking for max 253 bytes length, because one of the callers may be chopping off labels to "correct" the name.
875 labels := strings.Split(s, ".")
876 if len(labels) > 128 {
877 return fmt.Errorf("more than 128 labels")
879 for _, label := range labels[:len(labels)-1] {
881 return fmt.Errorf("label longer than 63 bytes")
885 return fmt.Errorf("empty dns label")
891func split(v, delim string) (r []string) {
892 isdelim := func(c rune) bool {
893 for _, d := range delim {
902 for i, c := range v {
904 r = append(r, v[s:i])
912// explanation does a best-effort attempt to fetch an explanation for a StatusFail response.
913// If no explanation could be composed, an empty string is returned.
914func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Args) (bool, string) {
917 // If this record is the result of an "include", we have to use the explanation
918 // string of the original domain, not of this domain.
920 expl := r.Explanation
921 if args.explanation != nil {
922 expl = *args.explanation
930 // Limits for dns requests and void lookups should not be taken into account.
931 // Starting with zero ensures they aren't triggered.
932 args.dnsRequests = new(int)
933 args.voidLookups = new(int)
934 name, authentic, err := expandDomainSpecDNS(ctx, resolver, r.Explanation, args)
935 if err != nil || name == "" {
938 txts, result, err := resolver.LookupTXT(ctx, ensureAbsDNS(name))
939 authentic = authentic && result.Authentic
940 if err != nil || len(txts) == 0 {
943 txt := strings.Join(txts, "")
944 s, exauthentic, err := expandDomainSpecExp(ctx, resolver, txt, args)
945 authentic = authentic && exauthentic
952func ensureAbsDNS(s string) string {
953 if !strings.HasSuffix(s, ".") {
959func trackLookupLimits(args *Args) error {
961 if *args.dnsRequests >= dnsRequestsMax {
962 return ErrTooManyDNSRequests
965 if *args.voidLookups >= voidLookupsMax {
966 return ErrTooManyVoidLookups
972func trackVoidLookup(err error, args *Args) {
973 if dns.IsNotFound(err) {