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.
19 "golang.org/x/exp/slog"
21 "github.com/mjl-/mox/dns"
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/smtp"
24 "github.com/mjl-/mox/stub"
27// The net package always returns DNS names in absolute, lower-case form. We make
28// sure we make names absolute when looking up. For verifying, we do not want to
29// verify names relative to our local search domain.
32 MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
35// cross-link rfc and errata
43 ErrName = errors.New("spf: bad domain name")
44 ErrNoRecord = errors.New("spf: no txt record")
45 ErrMultipleRecords = errors.New("spf: multiple spf txt records in dns")
46 ErrDNS = errors.New("spf: lookup of dns record")
47 ErrRecordSyntax = errors.New("spf: malformed spf txt record")
50 ErrTooManyDNSRequests = errors.New("spf: too many dns requests")
51 ErrTooManyVoidLookups = errors.New("spf: too many void lookups")
52 ErrMacroSyntax = errors.New("spf: bad macro syntax")
56 // Maximum number of DNS requests to execute. This excludes some requests, such as
57 // lookups of MX host results.
60 // Maximum number of DNS lookups that result in no records before a StatusPermerror
61 // is returned. This limit aims to prevent abuse.
65// Status is the result of an SPF verification.
72 StatusNone Status = "none" // E.g. no DNS domain name in session, or no SPF record in DNS.
73 StatusNeutral Status = "neutral" // Explicit statement that nothing is said about the IP, "?" qualifier. None and Neutral must be treated the same.
74 StatusPass Status = "pass" // IP is authorized.
75 StatusFail Status = "fail" // IP is exlicitly not authorized. "-" qualifier.
76 StatusSoftfail Status = "softfail" // Weak statement that IP is probably not authorized, "~" qualifier.
77 StatusTemperror Status = "temperror" // Trying again later may succeed, e.g. for temporary DNS lookup error.
78 StatusPermerror Status = "permerror" // Error requiring some intervention to correct. E.g. invalid DNS record.
81// Args are the parameters to the SPF verification algorithm ("check_host" in the RFC).
83// All fields should be set as they can be required for macro expansions.
85 // RemoteIP will be checked as sender for email.
88 // Address from SMTP MAIL FROM command. Zero values for a null reverse path (used for DSNs).
89 MailFromLocalpart smtp.Localpart
90 MailFromDomain dns.Domain
92 // HelloDomain is from the SMTP EHLO/HELO command.
93 HelloDomain dns.IPDomain
96 LocalHostname dns.Domain
98 // Explanation string to use for failure. In case of "include", where explanation
99 // from original domain must be used.
100 // May be set for recursive calls.
103 // Domain to validate.
106 // Effective sender. Equal to MailFrom if non-zero, otherwise set to "postmaster" at HelloDomain.
107 senderLocalpart smtp.Localpart
108 senderDomain dns.Domain
110 // To enforce the limit on lookups. Initialized automatically if nil.
115// Mocked for testing expanding "t" macro.
116var timeNow = time.Now
118// Lookup looks up and parses an SPF TXT record for domain.
120// Authentic indicates if the DNS results were DNSSEC-verified.
121func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, authentic bool, rerr error) {
122 log := mlog.New("spf", elog)
125 log.Debugx("spf lookup result", rerr,
126 slog.Any("domain", domain),
127 slog.Any("status", rstatus),
128 slog.Any("record", rrecord),
129 slog.Duration("duration", time.Since(start)))
133 host := domain.ASCII + "."
134 if err := validateDNS(host); err != nil {
135 return StatusNone, "", nil, false, fmt.Errorf("%w: %s: %s", ErrName, domain, err)
138 // Lookup spf record.
139 txts, result, err := dns.WithPackage(resolver, "spf").LookupTXT(ctx, host)
140 if dns.IsNotFound(err) {
141 return StatusNone, "", nil, result.Authentic, fmt.Errorf("%w for %s", ErrNoRecord, host)
142 } else if err != nil {
143 return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s: %s", ErrDNS, host, err)
146 // Parse the records. We only handle those that look like spf records.
149 for _, txt := range txts {
151 r, isspf, err := ParseRecord(txt)
155 } else if err != nil {
157 return StatusPermerror, txt, nil, result.Authentic, fmt.Errorf("%w: %s", ErrRecordSyntax, err)
161 return StatusPermerror, "", nil, result.Authentic, ErrMultipleRecords
168 return StatusNone, "", nil, result.Authentic, ErrNoRecord
170 return StatusNone, text, record, result.Authentic, nil
173// Verify checks if a remote IP is allowed to send email for a domain.
175// If the SMTP "MAIL FROM" is set, it is used as identity (domain) to verify.
176// Otherwise, the EHLO domain is verified if it is a valid domain.
178// The returned Received.Result status will always be set, regardless of whether an
180// For status Temperror and Permerror, an error is always returned.
181// For Fail, explanation may be set, and should be returned in the SMTP session if
182// it is the reason the message is rejected. The caller should ensure the
183// explanation is valid for use in SMTP, taking line length and ascii-only
184// requirement into account.
186// Verify takes the maximum number of 10 DNS requests into account, and the maximum
187// of 2 lookups resulting in no records ("void lookups").
189// Authentic indicates if the DNS results were DNSSEC-verified.
190func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, authentic bool, rerr error) {
191 log := mlog.New("spf", elog)
194 MetricVerify.ObserveLabels(float64(time.Since(start))/float64(time.Second), string(received.Result))
195 log.Debugx("spf verify result", rerr,
196 slog.Any("domain", args.domain),
197 slog.Any("ip", args.RemoteIP),
198 slog.Any("status", received.Result),
199 slog.String("explanation", explanation),
200 slog.Duration("duration", time.Since(start)))
203 isHello, ok := prepare(&args)
207 Comment: "no domain, ehlo is an ip literal and mailfrom is empty",
208 ClientIP: args.RemoteIP,
209 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.HelloDomain.IP.String()),
210 Helo: args.HelloDomain,
211 Receiver: args.LocalHostname.ASCII,
213 return received, dns.Domain{}, "", false, nil
216 status, mechanism, expl, authentic, err := checkHost(ctx, log, resolver, args)
217 comment := fmt.Sprintf("domain %s", args.domain.ASCII)
219 comment += ", from ehlo because mailfrom is empty"
224 ClientIP: args.RemoteIP,
225 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.senderDomain.ASCII), //
../rfc/7208:2090, explicitly "sender", not "mailfrom".
226 Helo: args.HelloDomain,
227 Receiver: args.LocalHostname.ASCII,
228 Mechanism: mechanism,
231 received.Problem = err.Error()
234 received.Identity = "helo"
236 received.Identity = "mailfrom"
238 return received, args.domain, expl, authentic, err
241// prepare args, setting fields sender* and domain as required for checkHost.
242func prepare(args *Args) (isHello bool, ok bool) {
243 // If MAIL FROM is set, that identity is used. Otherwise the EHLO identity is used.
244 // MAIL FROM is preferred, because if we accept the message, and we have to send a
245 // DSN, it helps to know it is a verified sender. If we would check an EHLO
246 // identity, and it is different from the MAIL FROM, we may be sending the DSN to
247 // an address with a domain that would not allow sending from the originating IP.
252 args.explanation = nil
253 args.dnsRequests = nil
254 args.voidLookups = nil
255 if args.MailFromDomain.IsZero() {
256 // If there is on EHLO, and it is an IP, there is nothing to SPF-validate.
257 if !args.HelloDomain.IsDomain() {
260 // If we have a mailfrom, we also have a localpart. But for EHLO we won't.
../rfc/7208:810
261 args.senderLocalpart = "postmaster"
262 args.senderDomain = args.HelloDomain.Domain
265 args.senderLocalpart = args.MailFromLocalpart
266 args.senderDomain = args.MailFromDomain
268 args.domain = args.senderDomain
272// lookup spf record, then evaluate args against it.
273func checkHost(ctx context.Context, log mlog.Log, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
274 status, _, record, rauthentic, err := Lookup(ctx, log.Logger, resolver, args.domain)
276 return status, "", "", rauthentic, err
279 var evalAuthentic bool
280 rstatus, mechanism, rexplanation, evalAuthentic, rerr = evaluate(ctx, log, record, resolver, args)
281 rauthentic = rauthentic && evalAuthentic
285// Evaluate evaluates the IP and names from args against the SPF DNS record for the domain.
286func Evaluate(ctx context.Context, elog *slog.Logger, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
287 log := mlog.New("spf", elog)
288 _, ok := prepare(&args)
290 return StatusNone, "default", "", false, fmt.Errorf("no domain name to validate")
292 return evaluate(ctx, log, record, resolver, args)
295// evaluate RemoteIP against domain from args, given record.
296func evaluate(ctx context.Context, log mlog.Log, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
299 log.Debugx("spf evaluate result", rerr,
300 slog.Int("dnsrequests", *args.dnsRequests),
301 slog.Int("voidlookups", *args.voidLookups),
302 slog.Any("domain", args.domain),
303 slog.Any("status", rstatus),
304 slog.String("mechanism", mechanism),
305 slog.String("explanation", rexplanation),
306 slog.Duration("duration", time.Since(start)))
309 if args.dnsRequests == nil {
310 args.dnsRequests = new(int)
311 args.voidLookups = new(int)
314 // Response is authentic until we find a non-authentic DNS response.
317 // To4 returns nil for an IPv6 address. To16 will return an IPv4-to-IPv6-mapped address.
319 remote4 := args.RemoteIP.To4()
321 remote6 = args.RemoteIP.To16()
324 // Check if ip matches remote ip, taking cidr mask into account.
325 checkIP := func(ip net.IP, d Directive) bool {
333 if d.IP4CIDRLen != nil {
336 mask := net.CIDRMask(ones, 32)
337 return ip4.Mask(mask).Equal(remote4.Mask(mask))
345 if d.IP6CIDRLen != nil {
348 mask := net.CIDRMask(ones, 128)
349 return ip6.Mask(mask).Equal(remote6.Mask(mask))
352 // Used for "a" and "mx".
353 checkHostIP := func(domain dns.Domain, d Directive, args *Args) (bool, Status, error) {
354 ips, result, err := resolver.LookupIP(ctx, "ip", domain.ASCII+".")
355 rauthentic = rauthentic && result.Authentic
356 trackVoidLookup(err, args)
357 // If "not found", we must ignore the error and treat as zero records in answer.
../rfc/7208:1116
358 if err != nil && !dns.IsNotFound(err) {
359 return false, StatusTemperror, err
361 for _, ip := range ips {
363 return true, StatusPass, nil
366 return false, StatusNone, nil
369 for _, d := range record.Directives {
373 case "include", "a", "mx", "ptr", "exists":
374 if err := trackLookupLimits(&args); err != nil {
375 return StatusPermerror, d.MechanismString(), "", rauthentic, err
386 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
387 rauthentic = rauthentic && authentic
389 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for include: %w", err)
392 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
394 status, _, _, authentic, err := checkHost(ctx, log, resolver, nargs)
395 rauthentic = rauthentic && authentic
400 case StatusTemperror:
401 return StatusTemperror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q: %w", name, err)
402 case StatusPermerror, StatusNone:
403 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q resulted in status %q: %w", name, status, err)
408 // note: the syntax for DomainSpec hints that macros should be expanded. But
409 // expansion is explicitly documented, and only for "include", "exists" and
410 // "redirect". This reason for this could be low-effort reuse of the domain-spec
411 // ABNF rule. It could be an oversight. We are not implementing expansion for the
412 // mechanism for which it isn't specified.
413 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
415 return StatusPermerror, d.MechanismString(), "", rauthentic, err
417 hmatch, status, err := checkHostIP(host, d, &args)
419 return status, d.MechanismString(), "", rauthentic, err
425 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
427 return StatusPermerror, d.MechanismString(), "", rauthentic, err
429 // Note: LookupMX can return an error and still return MX records.
430 mxs, result, err := resolver.LookupMX(ctx, host.ASCII+".")
431 rauthentic = rauthentic && result.Authentic
432 trackVoidLookup(err, &args)
433 // note: we handle "not found" simply as a result of zero mx records.
434 if err != nil && !dns.IsNotFound(err) {
435 return StatusTemperror, d.MechanismString(), "", rauthentic, err
437 if err == nil && len(mxs) == 1 && mxs[0].Host == "." {
441 for i, mx := range mxs {
443 // requests. This seems independent of the overall limit of 10 DNS requests. So an
444 // MX request resulting in 11 names is valid, but we must return a permerror if we
445 // found no match before the 11th name.
448 return StatusPermerror, d.MechanismString(), "", rauthentic, ErrTooManyDNSRequests
450 // Parsing lax (unless in pedantic mode) for MX targets with underscores as seen in the wild.
451 mxd, err := dns.ParseDomainLax(strings.TrimSuffix(mx.Host, "."))
453 return StatusPermerror, d.MechanismString(), "", rauthentic, err
455 hmatch, status, err := checkHostIP(mxd, d, &args)
457 return status, d.MechanismString(), "", rauthentic, err
467 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
469 return StatusPermerror, d.MechanismString(), "", rauthentic, err
472 rnames, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
473 rauthentic = rauthentic && result.Authentic
474 trackVoidLookup(err, &args)
475 if err != nil && !dns.IsNotFound(err) {
476 return StatusTemperror, d.MechanismString(), "", rauthentic, err
480 for _, rname := range rnames {
481 rd, err := dns.ParseDomain(strings.TrimSuffix(rname, "."))
483 log.Errorx("bad address in ptr record", err, slog.String("address", rname))
487 if rd.ASCII != host.ASCII && !strings.HasSuffix(rd.ASCII, "."+host.ASCII) {
496 ips, result, err := resolver.LookupIP(ctx, "ip", rd.ASCII+".")
497 rauthentic = rauthentic && result.Authentic
498 trackVoidLookup(err, &args)
499 for _, ip := range ips {
510 match = checkIP(d.IP, d)
514 match = checkIP(d.IP, d)
519 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
520 rauthentic = rauthentic && authentic
522 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for exists: %w", err)
525 ips, result, err := resolver.LookupIP(ctx, "ip4", ensureAbsDNS(name))
526 rauthentic = rauthentic && result.Authentic
527 // Note: we do count this for void lookups, as that is an anti-abuse mechanism.
529 trackVoidLookup(err, &args)
530 if err != nil && !dns.IsNotFound(err) {
531 return StatusTemperror, d.MechanismString(), "", rauthentic, err
536 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected mechanism %q", d.Mechanism)
544 return StatusPass, d.MechanismString(), "", rauthentic, nil
546 return StatusNeutral, d.MechanismString(), "", rauthentic, nil
550 authentic, expl := explanation(ctx, resolver, record, nargs)
551 rauthentic = rauthentic && authentic
552 return StatusFail, d.MechanismString(), expl, rauthentic, nil
554 return StatusSoftfail, d.MechanismString(), "", rauthentic, nil
556 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected qualifier %q", d.Qualifier)
559 if record.Redirect != "" {
563 name, authentic, err := expandDomainSpecDNS(ctx, resolver, record.Redirect, args)
564 rauthentic = rauthentic && authentic
566 return StatusPermerror, "", "", rauthentic, fmt.Errorf("expanding domain-spec: %w", err)
569 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
571 status, mechanism, expl, authentic, err := checkHost(ctx, log, resolver, nargs)
572 rauthentic = rauthentic && authentic
573 if status == StatusNone {
574 return StatusPermerror, mechanism, "", rauthentic, err
576 return status, mechanism, expl, rauthentic, err
580 return StatusNeutral, "default", "", rauthentic, nil
583// evaluateDomainSpec returns the parsed dns domain for spec if non-empty, and
584// otherwise returns d, which must be the Domain in checkHost Args.
585func evaluateDomainSpec(spec string, d dns.Domain) (dns.Domain, error) {
590 d, err := dns.ParseDomain(spec)
592 return d, fmt.Errorf("%w: %s", ErrName, err)
597func expandDomainSpecDNS(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
598 return expandDomainSpec(ctx, resolver, domainSpec, args, true)
601func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
602 return expandDomainSpec(ctx, resolver, domainSpec, args, false)
605// expandDomainSpec interprets macros in domainSpec.
606// The expansion can fail due to macro syntax errors or DNS errors.
610func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args, dns bool) (string, bool, error) {
613 rauthentic := true // Until non-authentic record is found.
617 b := &strings.Builder{}
629 return "", rauthentic, fmt.Errorf("%w: trailing bare %%", ErrMacroSyntax)
643 return "", rauthentic, fmt.Errorf("%w: invalid macro opening %%%c", ErrMacroSyntax, c)
647 return "", rauthentic, fmt.Errorf("%w: missing macro ending }", ErrMacroSyntax)
653 if c >= 'A' && c <= 'Z' {
661 // 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
662 v = smtp.NewAddress(args.senderLocalpart, args.senderDomain).String()
664 // todo: same about utf8 as for 's'.
665 v = string(args.senderLocalpart)
667 v = args.senderDomain.ASCII
669 v = args.domain.ASCII
671 v = expandIP(args.RemoteIP)
674 if err := trackLookupLimits(&args); err != nil {
675 return "", rauthentic, err
677 names, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
678 rauthentic = rauthentic && result.Authentic
679 trackVoidLookup(err, &args)
680 if len(names) == 0 || err != nil {
686 // Verify finds the first dns name that resolves to the remote ip.
687 verify := func(matchfn func(string) bool) (string, error) {
688 for _, name := range names {
692 ips, result, err := resolver.LookupIP(ctx, "ip", name)
693 rauthentic = rauthentic && result.Authentic
694 trackVoidLookup(err, &args)
696 for _, ip := range ips {
697 if ip.Equal(args.RemoteIP) {
705 // First exact domain name matches, then subdomains, finally other names.
706 domain := args.domain.ASCII + "."
707 dotdomain := "." + domain
708 v, err = verify(func(name string) bool { return name == domain })
710 return "", rauthentic, err
713 v, err = verify(func(name string) bool { return strings.HasSuffix(name, dotdomain) })
715 return "", rauthentic, err
719 v, err = verify(func(name string) bool { return name != domain && !strings.HasSuffix(name, dotdomain) })
721 return "", rauthentic, err
730 if args.RemoteIP.To4() != nil {
736 if args.HelloDomain.IsIP() {
737 //
../rfc/7208:1621 explicitly says "domain", not "ip". We'll handle IP, probably does no harm.
738 v = expandIP(args.HelloDomain.IP)
740 v = args.HelloDomain.Domain.ASCII
744 return "", rauthentic, fmt.Errorf("%w: macro letter %c only allowed in exp", ErrMacroSyntax, c)
748 v = args.LocalIP.String()
750 v = args.LocalHostname.ASCII
752 v = fmt.Sprintf("%d", timeNow().Unix())
755 return "", rauthentic, fmt.Errorf("%w: unknown macro letter %c", ErrMacroSyntax, c)
759 for i < n && s[i] >= '0' && s[i] <= '9' {
760 digits += string(s[i])
765 v, err := strconv.Atoi(digits)
767 return "", rauthentic, fmt.Errorf("%w: bad macro transformer digits %q: %s", ErrMacroSyntax, digits, err)
771 return "", rauthentic, fmt.Errorf("%w: zero labels for digits transformer", ErrMacroSyntax)
775 // If "r" follows, we must reverse the resulting name, splitting on a dot by default.
778 if i < n && (s[i] == 'r' || s[i] == 'R') {
783 // Delimiters to split on, for subset of labels and/or reversing.
787 case '.', '-', '+', ',', '/', '_', '=':
788 delim += string(s[i])
795 if i >= n || s[i] != '}' {
796 return "", rauthentic, fmt.Errorf("%w: missing closing } for macro", ErrMacroSyntax)
800 // Only split and subset and/or reverse if necessary.
801 if nlabels >= 0 || reverse || delim != "" {
810 for i := 0; i < h; i++ {
811 t[i], t[nt-1-i] = t[nt-1-i], t[i]
814 if nlabels > 0 && nlabels < len(t) {
815 t = t[len(t)-nlabels:]
818 v = strings.Join(t, ".")
823 v = url.QueryEscape(v)
830 isAbs := strings.HasSuffix(r, ".")
832 if err := validateDNS(r); err != nil {
833 return "", rauthentic, fmt.Errorf("invalid dns name: %s", err)
835 // If resulting name is too large, cut off labels on the left until it fits.
../rfc/7208:1749
837 labels := strings.Split(r, ".")
838 for i := range labels {
839 if i == len(labels)-1 {
840 return "", rauthentic, fmt.Errorf("expanded dns name too long")
842 s := strings.Join(labels[i+1:], ".")
853 return r, rauthentic, nil
856func expandIP(ip net.IP) string {
862 for i, b := range ip.To16() {
866 v += fmt.Sprintf("%x.%x", b>>4, b&0xf)
871// validateDNS checks if a DNS name is valid. Must not end in dot. This does not
872// check valid host names, e.g. _ is allowed in DNS but not in a host name.
873func validateDNS(s string) error {
875 // note: we are not checking for max 253 bytes length, because one of the callers may be chopping off labels to "correct" the name.
876 labels := strings.Split(s, ".")
877 if len(labels) > 128 {
878 return fmt.Errorf("more than 128 labels")
880 for _, label := range labels[:len(labels)-1] {
882 return fmt.Errorf("label longer than 63 bytes")
886 return fmt.Errorf("empty dns label")
892func split(v, delim string) (r []string) {
893 isdelim := func(c rune) bool {
894 for _, d := range delim {
903 for i, c := range v {
905 r = append(r, v[s:i])
913// explanation does a best-effort attempt to fetch an explanation for a StatusFail response.
914// If no explanation could be composed, an empty string is returned.
915func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Args) (bool, string) {
918 // If this record is the result of an "include", we have to use the explanation
919 // string of the original domain, not of this domain.
921 expl := r.Explanation
922 if args.explanation != nil {
923 expl = *args.explanation
931 // Limits for dns requests and void lookups should not be taken into account.
932 // Starting with zero ensures they aren't triggered.
933 args.dnsRequests = new(int)
934 args.voidLookups = new(int)
935 name, authentic, err := expandDomainSpecDNS(ctx, resolver, r.Explanation, args)
936 if err != nil || name == "" {
939 txts, result, err := resolver.LookupTXT(ctx, ensureAbsDNS(name))
940 authentic = authentic && result.Authentic
941 if err != nil || len(txts) == 0 {
944 txt := strings.Join(txts, "")
945 s, exauthentic, err := expandDomainSpecExp(ctx, resolver, txt, args)
946 authentic = authentic && exauthentic
953func ensureAbsDNS(s string) string {
954 if !strings.HasSuffix(s, ".") {
960func trackLookupLimits(args *Args) error {
962 if *args.dnsRequests >= dnsRequestsMax {
963 return ErrTooManyDNSRequests
966 if *args.voidLookups >= voidLookupsMax {
967 return ErrTooManyVoidLookups
973func trackVoidLookup(err error, args *Args) {
974 if dns.IsNotFound(err) {