/
verifier.go
174 lines (151 loc) · 4.94 KB
/
verifier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dmarc
import (
"context"
"math/rand"
"net"
"runtime/trace"
"strings"
"github.com/emersion/go-message/textproto"
"github.com/emersion/go-msgauth/authres"
"github.com/emersion/go-msgauth/dmarc"
)
type verifyData struct {
policyDomain string
fromDomain string
record *Record
recordErr error
}
// errPanic is used to propagate the panic() from the FetchRecord
// goroutine to the goroutine that called Apply.
type errPanic struct {
err interface{}
}
func (errPanic) Error() string {
return "panic during policy fetch"
}
// Verifier is the structure that wraps all state necessary to verify a
// single message using DMARC checks.
//
// It cannot be reused.
type Verifier struct {
fetchCh chan verifyData
fetchCancel context.CancelFunc
resolver Resolver
// TODO(GH #206): DMARC reporting
// FailureReportFunc is the callback that is called when a failure report
// is generated. If it is nil - failure reports generation is disabled.
// FailureReportFunc func(textproto.Header, io.Reader)
}
func NewVerifier(r Resolver) *Verifier {
return &Verifier{
fetchCh: make(chan verifyData, 1),
resolver: r,
}
}
func (v *Verifier) Close() error {
if v.fetchCancel != nil {
v.fetchCancel()
}
return nil
}
// FetchRecord prepares the Verifier by starting the policy lookup. Lookup is
// performed asynchronously to improve performance.
//
// If panic occurs in the lookup goroutine - call to Apply will panic.
func (v *Verifier) FetchRecord(ctx context.Context, header textproto.Header) {
fromDomain, err := ExtractFromDomain(header)
if err != nil {
v.fetchCh <- verifyData{
recordErr: err,
}
return
}
ctx, v.fetchCancel = context.WithCancel(ctx)
go func() {
defer func() {
if err := recover(); err != nil {
v.fetchCh <- verifyData{
recordErr: errPanic{err: err},
}
}
}()
defer trace.StartRegion(ctx, "DMARC/FetchRecord").End()
policyDomain, record, err := FetchRecord(ctx, v.resolver, fromDomain)
v.fetchCh <- verifyData{
policyDomain: policyDomain,
fromDomain: fromDomain,
record: record,
recordErr: err,
}
}()
}
// Apply actually performs all actions necessary to apply a DMARC policy to the message.
//
// The authRes slice should contain results for DKIM and SPF checks. FetchRecord should be
// caled before calling this function.
//
// It returns the Authentication-Result field to be included in the message (as
// a part of the EvalResult struct) and the appropriate action that should be
// taken by the MTA. In case of PolicyReject, caller should inspect the
// Result.Value to determine whether to use a temporary or permanent error code
// as Apply implements the 'fail closed' strategy for handling of temporary
// errors.
//
// Additionally, it relies on the math/rand default source to be initialized to determine
// whether to apply a policy with the pct key.
func (v *Verifier) Apply(authRes []authres.Result) (EvalResult, Policy) {
data := <-v.fetchCh
if data.recordErr != nil {
result := authres.DMARCResult{
Value: authres.ResultPermError,
Reason: "Policy lookup failed: " + data.recordErr.Error(),
// If may be empty, but it is fine (it will not be included in the field then).
From: data.fromDomain,
}
if dnsErr, ok := data.recordErr.(*net.DNSError); ok && dnsErr.Temporary() {
result.Value = authres.ResultTempError
// 'fail closed' behavior, reject the message if a temporary error
// occurs.
return EvalResult{
Authres: result,
}, dmarc.PolicyReject
}
return EvalResult{
Authres: result,
}, dmarc.PolicyNone
}
if data.record == nil {
return EvalResult{
Authres: authres.DMARCResult{
Value: authres.ResultNone,
From: data.fromDomain,
},
}, dmarc.PolicyNone
}
result := EvaluateAlignment(data.fromDomain, data.record, authRes)
if result.Authres.Value == authres.ResultPass || result.Authres.Value == authres.ResultNone {
return result, dmarc.PolicyNone
}
if data.record.Percent != nil && rand.Int31n(100) > int32(*data.record.Percent) {
return result, dmarc.PolicyNone
}
policy := data.record.Policy
if !strings.EqualFold(data.policyDomain, data.fromDomain) && data.record.SubdomainPolicy != "" {
policy = data.record.SubdomainPolicy
}
return result, policy
}