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