1// Package spf implements Sender Policy Framework (SPF, RFC 7208) for verifying
2// remote mail server IPs with their published records.
3//
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.
7package spf
8
9import (
10 "context"
11 "errors"
12 "fmt"
13 "net"
14 "net/url"
15 "strconv"
16 "strings"
17 "time"
18
19 "github.com/prometheus/client_golang/prometheus"
20 "github.com/prometheus/client_golang/prometheus/promauto"
21
22 "github.com/mjl-/mox/dns"
23 "github.com/mjl-/mox/mlog"
24 "github.com/mjl-/mox/smtp"
25)
26
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.
30
31var xlog = mlog.New("spf")
32
33var (
34 metricSPFVerify = promauto.NewHistogramVec(
35 prometheus.HistogramOpts{
36 Name: "mox_spf_verify_duration_seconds",
37 Help: "SPF verify, including lookup, duration and result.",
38 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
39 },
40 []string{
41 "status",
42 },
43 )
44)
45
46// cross-link rfc and errata
47// ../rfc/7208-eid5436 ../rfc/7208:2043
48// ../rfc/7208-eid6721 ../rfc/7208:1928
49// ../rfc/7208-eid5227 ../rfc/7208:1297
50// ../rfc/7208-eid6595 ../rfc/7208:984
51
52var (
53 // Lookup errors.
54 ErrName = errors.New("spf: bad domain name")
55 ErrNoRecord = errors.New("spf: no txt record")
56 ErrMultipleRecords = errors.New("spf: multiple spf txt records in dns")
57 ErrDNS = errors.New("spf: lookup of dns record")
58 ErrRecordSyntax = errors.New("spf: malformed spf txt record")
59
60 // Evaluation errors.
61 ErrTooManyDNSRequests = errors.New("spf: too many dns requests")
62 ErrTooManyVoidLookups = errors.New("spf: too many void lookups")
63 ErrMacroSyntax = errors.New("spf: bad macro syntax")
64)
65
66const (
67 // Maximum number of DNS requests to execute. This excludes some requests, such as
68 // lookups of MX host results.
69 dnsRequestsMax = 10
70
71 // Maximum number of DNS lookups that result in no records before a StatusPermerror
72 // is returned. This limit aims to prevent abuse.
73 voidLookupsMax = 2
74)
75
76// Status is the result of an SPF verification.
77type Status string
78
79// ../rfc/7208:517
80// ../rfc/7208:1836
81
82const (
83 StatusNone Status = "none" // E.g. no DNS domain name in session, or no SPF record in DNS.
84 StatusNeutral Status = "neutral" // Explicit statement that nothing is said about the IP, "?" qualifier. None and Neutral must be treated the same.
85 StatusPass Status = "pass" // IP is authorized.
86 StatusFail Status = "fail" // IP is exlicitly not authorized. "-" qualifier.
87 StatusSoftfail Status = "softfail" // Weak statement that IP is probably not authorized, "~" qualifier.
88 StatusTemperror Status = "temperror" // Trying again later may succeed, e.g. for temporary DNS lookup error.
89 StatusPermerror Status = "permerror" // Error requiring some intervention to correct. E.g. invalid DNS record.
90)
91
92// Args are the parameters to the SPF verification algorithm ("check_host" in the RFC).
93//
94// All fields should be set as they can be required for macro expansions.
95type Args struct {
96 // RemoteIP will be checked as sender for email.
97 RemoteIP net.IP
98
99 // Address from SMTP MAIL FROM command. Zero values for a null reverse path (used for DSNs).
100 MailFromLocalpart smtp.Localpart
101 MailFromDomain dns.Domain
102
103 // HelloDomain is from the SMTP EHLO/HELO command.
104 HelloDomain dns.IPDomain
105
106 LocalIP net.IP
107 LocalHostname dns.Domain
108
109 // Explanation string to use for failure. In case of "include", where explanation
110 // from original domain must be used.
111 // May be set for recursive calls.
112 explanation *string
113
114 // Domain to validate.
115 domain dns.Domain
116
117 // Effective sender. Equal to MailFrom if non-zero, otherwise set to "postmaster" at HelloDomain.
118 senderLocalpart smtp.Localpart
119 senderDomain dns.Domain
120
121 // To enforce the limit on lookups. Initialized automatically if nil.
122 dnsRequests *int
123 voidLookups *int
124}
125
126// Mocked for testing expanding "t" macro.
127var timeNow = time.Now
128
129// Lookup looks up and parses an SPF TXT record for domain.
130//
131// authentic indicates if the DNS results were DNSSEC-verified.
132func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rstatus Status, rtxt string, rrecord *Record, authentic bool, rerr error) {
133 log := xlog.WithContext(ctx)
134 start := time.Now()
135 defer func() {
136 log.Debugx("spf lookup result", rerr, mlog.Field("domain", domain), mlog.Field("status", rstatus), mlog.Field("record", rrecord), mlog.Field("duration", time.Since(start)))
137 }()
138
139 // ../rfc/7208:586
140 host := domain.ASCII + "."
141 if err := validateDNS(host); err != nil {
142 return StatusNone, "", nil, false, fmt.Errorf("%w: %s: %s", ErrName, domain, err)
143 }
144
145 // Lookup spf record.
146 txts, result, err := dns.WithPackage(resolver, "spf").LookupTXT(ctx, host)
147 if dns.IsNotFound(err) {
148 return StatusNone, "", nil, result.Authentic, fmt.Errorf("%w for %s", ErrNoRecord, host)
149 } else if err != nil {
150 return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s: %s", ErrDNS, host, err)
151 }
152
153 // Parse the records. We only handle those that look like spf records.
154 var record *Record
155 var text string
156 for _, txt := range txts {
157 var isspf bool
158 r, isspf, err := ParseRecord(txt)
159 if !isspf {
160 // ../rfc/7208:595
161 continue
162 } else if err != nil {
163 // ../rfc/7208:852
164 return StatusPermerror, txt, nil, result.Authentic, fmt.Errorf("%w: %s", ErrRecordSyntax, err)
165 }
166 if record != nil {
167 // ../rfc/7208:576
168 return StatusPermerror, "", nil, result.Authentic, ErrMultipleRecords
169 }
170 text = txt
171 record = r
172 }
173 if record == nil {
174 // ../rfc/7208:837
175 return StatusNone, "", nil, result.Authentic, ErrNoRecord
176 }
177 return StatusNone, text, record, result.Authentic, nil
178}
179
180// Verify checks if a remote IP is allowed to send email for a domain.
181//
182// If the SMTP "MAIL FROM" is set, it is used as identity (domain) to verify.
183// Otherwise, the EHLO domain is verified if it is a valid domain.
184//
185// The returned Received.Result status will always be set, regardless of whether an
186// error is returned.
187// For status Temperror and Permerror, an error is always returned.
188// For Fail, explanation may be set, and should be returned in the SMTP session if
189// it is the reason the message is rejected. The caller should ensure the
190// explanation is valid for use in SMTP, taking line length and ascii-only
191// requirement into account.
192//
193// Verify takes the maximum number of 10 DNS requests into account, and the maximum
194// of 2 lookups resulting in no records ("void lookups").
195//
196// authentic indicates if the DNS results were DNSSEC-verified.
197func Verify(ctx context.Context, resolver dns.Resolver, args Args) (received Received, domain dns.Domain, explanation string, authentic bool, rerr error) {
198 log := xlog.WithContext(ctx)
199 start := time.Now()
200 defer func() {
201 metricSPFVerify.WithLabelValues(string(received.Result)).Observe(float64(time.Since(start)) / float64(time.Second))
202 log.Debugx("spf verify result", rerr, mlog.Field("domain", args.domain), mlog.Field("ip", args.RemoteIP), mlog.Field("status", received.Result), mlog.Field("explanation", explanation), mlog.Field("duration", time.Since(start)))
203 }()
204
205 isHello, ok := prepare(&args)
206 if !ok {
207 received = Received{
208 Result: StatusNone,
209 Comment: "no domain, ehlo is an ip literal and mailfrom is empty",
210 ClientIP: args.RemoteIP,
211 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.HelloDomain.IP.String()),
212 Helo: args.HelloDomain,
213 Receiver: args.LocalHostname.ASCII,
214 }
215 return received, dns.Domain{}, "", false, nil
216 }
217
218 status, mechanism, expl, authentic, err := checkHost(ctx, resolver, args)
219 comment := fmt.Sprintf("domain %s", args.domain.ASCII)
220 if isHello {
221 comment += ", from ehlo because mailfrom is empty"
222 }
223 received = Received{
224 Result: status,
225 Comment: comment,
226 ClientIP: args.RemoteIP,
227 EnvelopeFrom: fmt.Sprintf("%s@%s", args.senderLocalpart, args.senderDomain.ASCII), // ../rfc/7208:2090, explicitly "sender", not "mailfrom".
228 Helo: args.HelloDomain,
229 Receiver: args.LocalHostname.ASCII,
230 Mechanism: mechanism,
231 }
232 if err != nil {
233 received.Problem = err.Error()
234 }
235 if isHello {
236 received.Identity = "helo"
237 } else {
238 received.Identity = "mailfrom"
239 }
240 return received, args.domain, expl, authentic, err
241}
242
243// prepare args, setting fields sender* and domain as required for checkHost.
244func prepare(args *Args) (isHello bool, ok bool) {
245 // If MAIL FROM is set, that identity is used. Otherwise the EHLO identity is used.
246 // MAIL FROM is preferred, because if we accept the message, and we have to send a
247 // DSN, it helps to know it is a verified sender. If we would check an EHLO
248 // identity, and it is different from the MAIL FROM, we may be sending the DSN to
249 // an address with a domain that would not allow sending from the originating IP.
250 // The RFC seems a bit confused, ../rfc/7208:778 implies MAIL FROM is preferred,
251 // but ../rfc/7208:424 mentions that a MAIL FROM check can be avoided by first
252 // doing HELO.
253
254 args.explanation = nil
255 args.dnsRequests = nil
256 args.voidLookups = nil
257 if args.MailFromDomain.IsZero() {
258 // If there is on EHLO, and it is an IP, there is nothing to SPF-validate.
259 if !args.HelloDomain.IsDomain() {
260 return false, false
261 }
262 // If we have a mailfrom, we also have a localpart. But for EHLO we won't. ../rfc/7208:810
263 args.senderLocalpart = "postmaster"
264 args.senderDomain = args.HelloDomain.Domain
265 isHello = true
266 } else {
267 args.senderLocalpart = args.MailFromLocalpart
268 args.senderDomain = args.MailFromDomain
269 }
270 args.domain = args.senderDomain
271 return isHello, true
272}
273
274// lookup spf record, then evaluate args against it.
275func checkHost(ctx context.Context, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
276 status, _, record, rauthentic, err := Lookup(ctx, resolver, args.domain)
277 if err != nil {
278 return status, "", "", rauthentic, err
279 }
280
281 var evalAuthentic bool
282 rstatus, mechanism, rexplanation, evalAuthentic, rerr = evaluate(ctx, record, resolver, args)
283 rauthentic = rauthentic && evalAuthentic
284 return
285}
286
287// Evaluate evaluates the IP and names from args against the SPF DNS record for the domain.
288func Evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
289 _, ok := prepare(&args)
290 if !ok {
291 return StatusNone, "default", "", false, fmt.Errorf("no domain name to validate")
292 }
293 return evaluate(ctx, record, resolver, args)
294}
295
296// evaluate RemoteIP against domain from args, given record.
297func evaluate(ctx context.Context, record *Record, resolver dns.Resolver, args Args) (rstatus Status, mechanism, rexplanation string, rauthentic bool, rerr error) {
298 log := xlog.WithContext(ctx)
299 start := time.Now()
300 defer func() {
301 log.Debugx("spf evaluate result", rerr, mlog.Field("dnsrequests", *args.dnsRequests), mlog.Field("voidlookups", *args.voidLookups), mlog.Field("domain", args.domain), mlog.Field("status", rstatus), mlog.Field("mechanism", mechanism), mlog.Field("explanation", rexplanation), mlog.Field("duration", time.Since(start)))
302 }()
303
304 resolver = dns.WithPackage(resolver, "spf")
305
306 if args.dnsRequests == nil {
307 args.dnsRequests = new(int)
308 args.voidLookups = new(int)
309 }
310
311 // Response is authentic until we find a non-authentic DNS response.
312 rauthentic = true
313
314 // To4 returns nil for an IPv6 address. To16 will return an IPv4-to-IPv6-mapped address.
315 var remote6 net.IP
316 remote4 := args.RemoteIP.To4()
317 if remote4 == nil {
318 remote6 = args.RemoteIP.To16()
319 }
320
321 // Check if ip matches remote ip, taking cidr mask into account.
322 checkIP := func(ip net.IP, d Directive) bool {
323 // ../rfc/7208:1097
324 if remote4 != nil {
325 ip4 := ip.To4()
326 if ip4 == nil {
327 return false
328 }
329 ones := 32
330 if d.IP4CIDRLen != nil {
331 ones = *d.IP4CIDRLen
332 }
333 mask := net.CIDRMask(ones, 32)
334 return ip4.Mask(mask).Equal(remote4.Mask(mask))
335 }
336
337 ip6 := ip.To16()
338 if ip6 == nil {
339 return false
340 }
341 ones := 128
342 if d.IP6CIDRLen != nil {
343 ones = *d.IP6CIDRLen
344 }
345 mask := net.CIDRMask(ones, 128)
346 return ip6.Mask(mask).Equal(remote6.Mask(mask))
347 }
348
349 // Used for "a" and "mx".
350 checkHostIP := func(domain dns.Domain, d Directive, args *Args) (bool, Status, error) {
351 ips, result, err := resolver.LookupIP(ctx, "ip", domain.ASCII+".")
352 rauthentic = rauthentic && result.Authentic
353 trackVoidLookup(err, args)
354 // If "not found", we must ignore the error and treat as zero records in answer. ../rfc/7208:1116
355 if err != nil && !dns.IsNotFound(err) {
356 return false, StatusTemperror, err
357 }
358 for _, ip := range ips {
359 if checkIP(ip, d) {
360 return true, StatusPass, nil
361 }
362 }
363 return false, StatusNone, nil
364 }
365
366 for _, d := range record.Directives {
367 var match bool
368
369 switch d.Mechanism {
370 case "include", "a", "mx", "ptr", "exists":
371 if err := trackLookupLimits(&args); err != nil {
372 return StatusPermerror, d.MechanismString(), "", rauthentic, err
373 }
374 }
375
376 switch d.Mechanism {
377 case "all":
378 // ../rfc/7208:1127
379 match = true
380
381 case "include":
382 // ../rfc/7208:1143
383 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
384 rauthentic = rauthentic && authentic
385 if err != nil {
386 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for include: %w", err)
387 }
388 nargs := args
389 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
390 nargs.explanation = &record.Explanation // ../rfc/7208:1548
391 status, _, _, authentic, err := checkHost(ctx, resolver, nargs)
392 rauthentic = rauthentic && authentic
393 // ../rfc/7208:1202
394 switch status {
395 case StatusPass:
396 match = true
397 case StatusTemperror:
398 return StatusTemperror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q: %w", name, err)
399 case StatusPermerror, StatusNone:
400 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("include %q resulted in status %q: %w", name, status, err)
401 }
402
403 case "a":
404 // ../rfc/7208:1249
405 // note: the syntax for DomainSpec hints that macros should be expanded. But
406 // expansion is explicitly documented, and only for "include", "exists" and
407 // "redirect". This reason for this could be low-effort reuse of the domain-spec
408 // ABNF rule. It could be an oversight. We are not implementing expansion for the
409 // mechanism for which it isn't specified.
410 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
411 if err != nil {
412 return StatusPermerror, d.MechanismString(), "", rauthentic, err
413 }
414 hmatch, status, err := checkHostIP(host, d, &args)
415 if err != nil {
416 return status, d.MechanismString(), "", rauthentic, err
417 }
418 match = hmatch
419
420 case "mx":
421 // ../rfc/7208:1262
422 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
423 if err != nil {
424 return StatusPermerror, d.MechanismString(), "", rauthentic, err
425 }
426 // Note: LookupMX can return an error and still return MX records.
427 mxs, result, err := resolver.LookupMX(ctx, host.ASCII+".")
428 rauthentic = rauthentic && result.Authentic
429 trackVoidLookup(err, &args)
430 // note: we handle "not found" simply as a result of zero mx records.
431 if err != nil && !dns.IsNotFound(err) {
432 return StatusTemperror, d.MechanismString(), "", rauthentic, err
433 }
434 if err == nil && len(mxs) == 1 && mxs[0].Host == "." {
435 // Explicitly no MX.
436 break
437 }
438 for i, mx := range mxs {
439 // ../rfc/7208:947 says that each mx record cannot result in more than 10 DNS
440 // requests. This seems independent of the overall limit of 10 DNS requests. So an
441 // MX request resulting in 11 names is valid, but we must return a permerror if we
442 // found no match before the 11th name.
443 // ../rfc/7208:945
444 if i >= 10 {
445 return StatusPermerror, d.MechanismString(), "", rauthentic, ErrTooManyDNSRequests
446 }
447 // Parsing lax (unless in pedantic mode) for MX targets with underscores as seen in the wild.
448 mxd, err := dns.ParseDomainLax(strings.TrimSuffix(mx.Host, "."))
449 if err != nil {
450 return StatusPermerror, d.MechanismString(), "", rauthentic, err
451 }
452 hmatch, status, err := checkHostIP(mxd, d, &args)
453 if err != nil {
454 return status, d.MechanismString(), "", rauthentic, err
455 }
456 if hmatch {
457 match = hmatch
458 break
459 }
460 }
461
462 case "ptr":
463 // ../rfc/7208:1281
464 host, err := evaluateDomainSpec(d.DomainSpec, args.domain)
465 if err != nil {
466 return StatusPermerror, d.MechanismString(), "", rauthentic, err
467 }
468
469 rnames, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
470 rauthentic = rauthentic && result.Authentic
471 trackVoidLookup(err, &args)
472 if err != nil && !dns.IsNotFound(err) {
473 return StatusTemperror, d.MechanismString(), "", rauthentic, err
474 }
475 lookups := 0
476 ptrnames:
477 for _, rname := range rnames {
478 rd, err := dns.ParseDomain(strings.TrimSuffix(rname, "."))
479 if err != nil {
480 log.Errorx("bad address in ptr record", err, mlog.Field("address", rname))
481 continue
482 }
483 // ../rfc/7208-eid4751 ../rfc/7208:1323
484 if rd.ASCII != host.ASCII && !strings.HasSuffix(rd.ASCII, "."+host.ASCII) {
485 continue
486 }
487
488 // ../rfc/7208:963, we must ignore entries after the first 10.
489 if lookups >= 10 {
490 break
491 }
492 lookups++
493 ips, result, err := resolver.LookupIP(ctx, "ip", rd.ASCII+".")
494 rauthentic = rauthentic && result.Authentic
495 trackVoidLookup(err, &args)
496 for _, ip := range ips {
497 if checkIP(ip, d) {
498 match = true
499 break ptrnames
500 }
501 }
502 }
503
504 // ../rfc/7208:1351
505 case "ip4":
506 if remote4 != nil {
507 match = checkIP(d.IP, d)
508 }
509 case "ip6":
510 if remote6 != nil {
511 match = checkIP(d.IP, d)
512 }
513
514 case "exists":
515 // ../rfc/7208:1382
516 name, authentic, err := expandDomainSpecDNS(ctx, resolver, d.DomainSpec, args)
517 rauthentic = rauthentic && authentic
518 if err != nil {
519 return StatusPermerror, d.MechanismString(), "", rauthentic, fmt.Errorf("expanding domain-spec for exists: %w", err)
520 }
521
522 ips, result, err := resolver.LookupIP(ctx, "ip4", ensureAbsDNS(name))
523 rauthentic = rauthentic && result.Authentic
524 // Note: we do count this for void lookups, as that is an anti-abuse mechanism.
525 // ../rfc/7208:1382 does not say anything special, so ../rfc/7208:984 applies.
526 trackVoidLookup(err, &args)
527 if err != nil && !dns.IsNotFound(err) {
528 return StatusTemperror, d.MechanismString(), "", rauthentic, err
529 }
530 match = len(ips) > 0
531
532 default:
533 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected mechanism %q", d.Mechanism)
534 }
535
536 if !match {
537 continue
538 }
539 switch d.Qualifier {
540 case "", "+":
541 return StatusPass, d.MechanismString(), "", rauthentic, nil
542 case "?":
543 return StatusNeutral, d.MechanismString(), "", rauthentic, nil
544 case "-":
545 nargs := args
546 // ../rfc/7208:1489
547 authentic, expl := explanation(ctx, resolver, record, nargs)
548 rauthentic = rauthentic && authentic
549 return StatusFail, d.MechanismString(), expl, rauthentic, nil
550 case "~":
551 return StatusSoftfail, d.MechanismString(), "", rauthentic, nil
552 }
553 return StatusNone, d.MechanismString(), "", rauthentic, fmt.Errorf("internal error, unexpected qualifier %q", d.Qualifier)
554 }
555
556 if record.Redirect != "" {
557 // We only know "redirect" for evaluating purposes, ignoring any others. ../rfc/7208:1423
558
559 // ../rfc/7208:1440
560 name, authentic, err := expandDomainSpecDNS(ctx, resolver, record.Redirect, args)
561 rauthentic = rauthentic && authentic
562 if err != nil {
563 return StatusPermerror, "", "", rauthentic, fmt.Errorf("expanding domain-spec: %w", err)
564 }
565 nargs := args
566 nargs.domain = dns.Domain{ASCII: strings.TrimSuffix(name, ".")}
567 nargs.explanation = nil // ../rfc/7208:1548
568 status, mechanism, expl, authentic, err := checkHost(ctx, resolver, nargs)
569 rauthentic = rauthentic && authentic
570 if status == StatusNone {
571 return StatusPermerror, mechanism, "", rauthentic, err
572 }
573 return status, mechanism, expl, rauthentic, err
574 }
575
576 // ../rfc/7208:996 ../rfc/7208:2095
577 return StatusNeutral, "default", "", rauthentic, nil
578}
579
580// evaluateDomainSpec returns the parsed dns domain for spec if non-empty, and
581// otherwise returns d, which must be the Domain in checkHost Args.
582func evaluateDomainSpec(spec string, d dns.Domain) (dns.Domain, error) {
583 // ../rfc/7208:1037
584 if spec == "" {
585 return d, nil
586 }
587 d, err := dns.ParseDomain(spec)
588 if err != nil {
589 return d, fmt.Errorf("%w: %s", ErrName, err)
590 }
591 return d, nil
592}
593
594func expandDomainSpecDNS(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
595 return expandDomainSpec(ctx, resolver, domainSpec, args, true)
596}
597
598func expandDomainSpecExp(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args) (string, bool, error) {
599 return expandDomainSpec(ctx, resolver, domainSpec, args, false)
600}
601
602// expandDomainSpec interprets macros in domainSpec.
603// The expansion can fail due to macro syntax errors or DNS errors.
604// Caller should typically treat failures as StatusPermerror. ../rfc/7208:1641
605// ../rfc/7208:1639
606// ../rfc/7208:1047
607func expandDomainSpec(ctx context.Context, resolver dns.Resolver, domainSpec string, args Args, dns bool) (string, bool, error) {
608 exp := !dns
609
610 rauthentic := true // Until non-authentic record is found.
611
612 s := domainSpec
613
614 b := &strings.Builder{}
615 i := 0
616 n := len(s)
617 for i < n {
618 c := s[i]
619 i++
620 if c != '%' {
621 b.WriteByte(c)
622 continue
623 }
624
625 if i >= n {
626 return "", rauthentic, fmt.Errorf("%w: trailing bare %%", ErrMacroSyntax)
627 }
628 c = s[i]
629 i++
630 if c == '%' {
631 b.WriteByte(c)
632 continue
633 } else if c == '_' {
634 b.WriteByte(' ')
635 continue
636 } else if c == '-' {
637 b.WriteString("%20")
638 continue
639 } else if c != '{' {
640 return "", rauthentic, fmt.Errorf("%w: invalid macro opening %%%c", ErrMacroSyntax, c)
641 }
642
643 if i >= n {
644 return "", rauthentic, fmt.Errorf("%w: missing macro ending }", ErrMacroSyntax)
645 }
646 c = s[i]
647 i++
648
649 upper := false
650 if c >= 'A' && c <= 'Z' {
651 upper = true
652 c += 'a' - 'A'
653 }
654
655 var v string
656 switch c {
657 case 's':
658 // 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
659 v = smtp.NewAddress(args.senderLocalpart, args.senderDomain).String()
660 case 'l':
661 // todo: same about utf8 as for 's'.
662 v = string(args.senderLocalpart)
663 case 'o':
664 v = args.senderDomain.ASCII
665 case 'd':
666 v = args.domain.ASCII
667 case 'i':
668 v = expandIP(args.RemoteIP)
669 case 'p':
670 // ../rfc/7208:937
671 if err := trackLookupLimits(&args); err != nil {
672 return "", rauthentic, err
673 }
674 names, result, err := resolver.LookupAddr(ctx, args.RemoteIP.String())
675 rauthentic = rauthentic && result.Authentic
676 trackVoidLookup(err, &args)
677 if len(names) == 0 || err != nil {
678 // ../rfc/7208:1709
679 v = "unknown"
680 break
681 }
682
683 // Verify finds the first dns name that resolves to the remote ip.
684 verify := func(matchfn func(string) bool) (string, error) {
685 for _, name := range names {
686 if !matchfn(name) {
687 continue
688 }
689 ips, result, err := resolver.LookupIP(ctx, "ip", name)
690 rauthentic = rauthentic && result.Authentic
691 trackVoidLookup(err, &args)
692 // ../rfc/7208:1714, we don't have to check other errors.
693 for _, ip := range ips {
694 if ip.Equal(args.RemoteIP) {
695 return name, nil
696 }
697 }
698 }
699 return "", nil
700 }
701
702 // First exact domain name matches, then subdomains, finally other names.
703 domain := args.domain.ASCII + "."
704 dotdomain := "." + domain
705 v, err = verify(func(name string) bool { return name == domain })
706 if err != nil {
707 return "", rauthentic, err
708 }
709 if v == "" {
710 v, err = verify(func(name string) bool { return strings.HasSuffix(name, dotdomain) })
711 if err != nil {
712 return "", rauthentic, err
713 }
714 }
715 if v == "" {
716 v, err = verify(func(name string) bool { return name != domain && !strings.HasSuffix(name, dotdomain) })
717 if err != nil {
718 return "", rauthentic, err
719 }
720 }
721 if v == "" {
722 // ../rfc/7208:1709
723 v = "unknown"
724 }
725
726 case 'v':
727 if args.RemoteIP.To4() != nil {
728 v = "in-addr"
729 } else {
730 v = "ip6"
731 }
732 case 'h':
733 if args.HelloDomain.IsIP() {
734 // ../rfc/7208:1621 explicitly says "domain", not "ip". We'll handle IP, probably does no harm.
735 v = expandIP(args.HelloDomain.IP)
736 } else {
737 v = args.HelloDomain.Domain.ASCII
738 }
739 case 'c', 'r', 't':
740 if !exp {
741 return "", rauthentic, fmt.Errorf("%w: macro letter %c only allowed in exp", ErrMacroSyntax, c)
742 }
743 switch c {
744 case 'c':
745 v = args.LocalIP.String()
746 case 'r':
747 v = args.LocalHostname.ASCII
748 case 't':
749 v = fmt.Sprintf("%d", timeNow().Unix())
750 }
751 default:
752 return "", rauthentic, fmt.Errorf("%w: unknown macro letter %c", ErrMacroSyntax, c)
753 }
754
755 digits := ""
756 for i < n && s[i] >= '0' && s[i] <= '9' {
757 digits += string(s[i])
758 i++
759 }
760 nlabels := -1
761 if digits != "" {
762 v, err := strconv.Atoi(digits)
763 if err != nil {
764 return "", rauthentic, fmt.Errorf("%w: bad macro transformer digits %q: %s", ErrMacroSyntax, digits, err)
765 }
766 nlabels = v
767 if nlabels == 0 {
768 return "", rauthentic, fmt.Errorf("%w: zero labels for digits transformer", ErrMacroSyntax)
769 }
770 }
771
772 // If "r" follows, we must reverse the resulting name, splitting on a dot by default.
773 // ../rfc/7208:1655
774 reverse := false
775 if i < n && (s[i] == 'r' || s[i] == 'R') {
776 reverse = true
777 i++
778 }
779
780 // Delimiters to split on, for subset of labels and/or reversing.
781 delim := ""
782 for i < n {
783 switch s[i] {
784 case '.', '-', '+', ',', '/', '_', '=':
785 delim += string(s[i])
786 i++
787 continue
788 }
789 break
790 }
791
792 if i >= n || s[i] != '}' {
793 return "", rauthentic, fmt.Errorf("%w: missing closing } for macro", ErrMacroSyntax)
794 }
795 i++
796
797 // Only split and subset and/or reverse if necessary.
798 if nlabels >= 0 || reverse || delim != "" {
799 if delim == "" {
800 delim = "."
801 }
802 t := split(v, delim)
803 // ../rfc/7208:1655
804 if reverse {
805 nt := len(t)
806 h := nt / 2
807 for i := 0; i < h; i++ {
808 t[i], t[nt-1-i] = t[nt-1-i], t[i]
809 }
810 }
811 if nlabels > 0 && nlabels < len(t) {
812 t = t[len(t)-nlabels:]
813 }
814 // Always join on dot. ../rfc/7208:1659
815 v = strings.Join(t, ".")
816 }
817
818 // ../rfc/7208:1755
819 if upper {
820 v = url.QueryEscape(v)
821 }
822
823 b.WriteString(v)
824 }
825 r := b.String()
826 if dns {
827 isAbs := strings.HasSuffix(r, ".")
828 r = ensureAbsDNS(r)
829 if err := validateDNS(r); err != nil {
830 return "", rauthentic, fmt.Errorf("invalid dns name: %s", err)
831 }
832 // If resulting name is too large, cut off labels on the left until it fits. ../rfc/7208:1749
833 if len(r) > 253+1 {
834 labels := strings.Split(r, ".")
835 for i := range labels {
836 if i == len(labels)-1 {
837 return "", rauthentic, fmt.Errorf("expanded dns name too long")
838 }
839 s := strings.Join(labels[i+1:], ".")
840 if len(s) <= 254 {
841 r = s
842 break
843 }
844 }
845 }
846 if !isAbs {
847 r = r[:len(r)-1]
848 }
849 }
850 return r, rauthentic, nil
851}
852
853func expandIP(ip net.IP) string {
854 ip4 := ip.To4()
855 if ip4 != nil {
856 return ip4.String()
857 }
858 v := ""
859 for i, b := range ip.To16() {
860 if i > 0 {
861 v += "."
862 }
863 v += fmt.Sprintf("%x.%x", b>>4, b&0xf)
864 }
865 return v
866}
867
868// validateDNS checks if a DNS name is valid. Must not end in dot. This does not
869// check valid host names, e.g. _ is allows in DNS but not in a host name.
870func validateDNS(s string) error {
871 // ../rfc/7208:800
872 // note: we are not checking for max 253 bytes length, because one of the callers may be chopping off labels to "correct" the name.
873 labels := strings.Split(s, ".")
874 if len(labels) > 128 {
875 return fmt.Errorf("more than 128 labels")
876 }
877 for _, label := range labels[:len(labels)-1] {
878 if len(label) > 63 {
879 return fmt.Errorf("label longer than 63 bytes")
880 }
881
882 if label == "" {
883 return fmt.Errorf("empty dns label")
884 }
885 }
886 return nil
887}
888
889func split(v, delim string) (r []string) {
890 isdelim := func(c rune) bool {
891 for _, d := range delim {
892 if d == c {
893 return true
894 }
895 }
896 return false
897 }
898
899 s := 0
900 for i, c := range v {
901 if isdelim(c) {
902 r = append(r, v[s:i])
903 s = i + 1
904 }
905 }
906 r = append(r, v[s:])
907 return r
908}
909
910// explanation does a best-effort attempt to fetch an explanation for a StatusFail response.
911// If no explanation could be composed, an empty string is returned.
912func explanation(ctx context.Context, resolver dns.Resolver, r *Record, args Args) (bool, string) {
913 // ../rfc/7208:1485
914
915 // If this record is the result of an "include", we have to use the explanation
916 // string of the original domain, not of this domain.
917 // ../rfc/7208:1548
918 expl := r.Explanation
919 if args.explanation != nil {
920 expl = *args.explanation
921 }
922
923 // ../rfc/7208:1491
924 if expl == "" {
925 return true, ""
926 }
927
928 // Limits for dns requests and void lookups should not be taken into account.
929 // Starting with zero ensures they aren't triggered.
930 args.dnsRequests = new(int)
931 args.voidLookups = new(int)
932 name, authentic, err := expandDomainSpecDNS(ctx, resolver, r.Explanation, args)
933 if err != nil || name == "" {
934 return authentic, ""
935 }
936 txts, result, err := resolver.LookupTXT(ctx, ensureAbsDNS(name))
937 authentic = authentic && result.Authentic
938 if err != nil || len(txts) == 0 {
939 return authentic, ""
940 }
941 txt := strings.Join(txts, "")
942 s, exauthentic, err := expandDomainSpecExp(ctx, resolver, txt, args)
943 authentic = authentic && exauthentic
944 if err != nil {
945 return authentic, ""
946 }
947 return authentic, s
948}
949
950func ensureAbsDNS(s string) string {
951 if !strings.HasSuffix(s, ".") {
952 return s + "."
953 }
954 return s
955}
956
957func trackLookupLimits(args *Args) error {
958 // ../rfc/7208:937
959 if *args.dnsRequests >= dnsRequestsMax {
960 return ErrTooManyDNSRequests
961 }
962 // ../rfc/7208:988
963 if *args.voidLookups >= voidLookupsMax {
964 return ErrTooManyVoidLookups
965 }
966 *args.dnsRequests++
967 return nil
968}
969
970func trackVoidLookup(err error, args *Args) {
971 if dns.IsNotFound(err) {
972 *args.voidLookups++
973 }
974}
975