/
plugin.go
207 lines (188 loc) · 5.92 KB
/
plugin.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package dns
import (
"context"
"fmt"
"strings"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"
"github.com/gophish/healthcheck/config"
"github.com/gophish/healthcheck/db"
"github.com/miekg/dns"
)
// HealthCheckPluginName is the name of the health check plugin needed
// to implement the Handler interface
const HealthCheckPluginName = "healthcheck"
// HealthCheckPlugin is a CoreDNS plugin that emulate various email
// authentication states.
type HealthCheckPlugin struct {
Next plugin.Handler
}
// Name implements the Handler interface.
func (hc HealthCheckPlugin) Name() string {
return HealthCheckPluginName
}
func (hc HealthCheckPlugin) generateSPFTemplate(message *db.Message) string {
response := "v=spf1 "
switch message.MessageConfiguration.SPF {
case db.Pass:
response += fmt.Sprintf("%s -all", config.Config.EmailHostname)
// Return a valid SPF record
case db.SoftFail:
// Return an invalid SPF record with the softfail directive set
response += "~all"
case db.HardFail:
// Return an invalid SPF record with the hardfail directive set
response += "-all"
case db.Neutral:
response += "?all"
}
return response
}
func (hc HealthCheckPlugin) generateDKIMTemplate(message *db.Message) string {
// TODO
switch message.MessageConfiguration.DKIM {
case db.Pass:
// Return a valid DKIM public key
case db.HardFail:
// Return an invalid DKIM public key
}
return "DKIM"
}
func (hc HealthCheckPlugin) generateDMARCTemplate(message *db.Message) string {
// The DMARC policy pass/fail is determined by the SPF/DKIM configuration.
// However, we can still set the DMARC result to none, quarantine (softfail)
// or reject (hardfail).
response := "v=DMARC1;"
switch message.MessageConfiguration.DMARC {
case db.Neutral:
// Return the DMARC policy set to none
response += " p=none; sp=none; adkim=r; aspf=r;"
case db.Quarantine:
// Return the DMARC policy set to quarantine
response += " p=quarantine; sp=quarantine; adkim=r; aspf=r;"
case db.Reject:
// Return the DMARC policy set to reject
response += " p=reject; sp=reject; adkim=r; aspf=r;"
}
response += " pct=100;"
return response
}
func (hc HealthCheckPlugin) processDMARCRecord(state request.Request, messageID string) ([]dns.RR, error) {
rrs := []dns.RR{}
message, err := db.GetMessage(messageID)
if err != nil {
return rrs, err
}
rr := new(dns.TXT)
rr.Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeTXT, Class: state.QClass()}
rr.Txt = []string{hc.generateDMARCTemplate(message)}
rrs = append(rrs, rr)
return rrs, nil
}
func (hc HealthCheckPlugin) processDKIMRecord(state request.Request, messageID string) ([]dns.RR, error) {
rrs := []dns.RR{}
message, err := db.GetMessage(messageID)
if err != nil {
return rrs, err
}
rr := new(dns.TXT)
rr.Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeTXT, Class: state.QClass()}
rr.Txt = []string{hc.generateDKIMTemplate(message)}
rrs = append(rrs, rr)
return rrs, nil
}
func (hc HealthCheckPlugin) processSPFRecord(state request.Request) ([]dns.RR, error) {
rrs := []dns.RR{}
messageID := strings.Split(state.QName(), ".")[0]
message, err := db.GetMessage(messageID)
if err != nil {
return rrs, err
}
rr := new(dns.SPF)
rr.Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeSPF, Class: state.QClass()}
rr.Txt = []string{hc.generateSPFTemplate(message)}
rrs = append(rrs, rr)
return rrs, nil
}
func (hc HealthCheckPlugin) processTXTRecord(state request.Request) ([]dns.RR, error) {
rrs := []dns.RR{}
var messageID string
parts := strings.Split(state.QName(), ".")
switch parts[0] {
case config.DMARCPrefix:
messageID = parts[1]
return hc.processDMARCRecord(state, messageID)
case config.DKIMPrefix:
messageID = parts[1]
return hc.processDKIMRecord(state, messageID)
}
messageID = parts[0]
message, err := db.GetMessage(messageID)
if err != nil {
return rrs, err
}
// Process the SPF (as a TXT record) response
rr := new(dns.TXT)
rr.Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeTXT, Class: state.QClass()}
rr.Txt = []string{hc.generateSPFTemplate(message)}
rrs = append(rrs, rr)
return rrs, nil
}
func (hc HealthCheckPlugin) processMXRecord(state request.Request) ([]dns.RR, error) {
rrs := []dns.RR{}
messageID := strings.Split(state.QName(), ".")[0]
message, err := db.GetMessage(messageID)
if err != nil {
return rrs, err
}
rr := new(dns.MX)
rr.Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeMX, Class: state.QClass()}
rr.Preference = 10
fmt.Println(message.MessageConfiguration)
switch message.MessageConfiguration.MX {
case db.None:
return rrs, nil
case db.HardFail:
rr.Mx = dns.Fqdn(fmt.Sprintf("invalid.%s", config.Config.EmailHostname))
case db.Pass:
rr.Mx = dns.Fqdn(config.Config.EmailHostname)
}
fmt.Println(rr.Mx)
rrs = append(rrs, rr)
return rrs, nil
}
// ServeDNS retrieves the health check configuration for the requested message
// and returns an appropriate response.
func (hc HealthCheckPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if state.QType() != dns.TypeTXT && state.QType() != dns.TypeSPF && state.QType() != dns.TypeMX {
return plugin.NextOrFailure(hc.Name(), hc.Next, ctx, w, r)
}
a := new(dns.Msg)
a.SetReply(r)
a.Authoritative = true
a.Compress = true
var err error
switch state.QType() {
case dns.TypeTXT:
a.Answer, err = hc.processTXTRecord(state)
if err != nil {
return plugin.NextOrFailure(hc.Name(), hc.Next, ctx, w, r)
}
case dns.TypeMX:
a.Answer, err = hc.processMXRecord(state)
if err != nil {
return plugin.NextOrFailure(hc.Name(), hc.Next, ctx, w, r)
}
// This is really only supported for odd legacy issues. Per RFC 7208, SPF
// records must be TXT records
case dns.TypeSPF:
a.Answer, err = hc.processSPFRecord(state)
if err != nil {
return plugin.NextOrFailure(hc.Name(), hc.Next, ctx, w, r)
}
}
state.SizeAndDo(a)
w.WriteMsg(a)
return dns.RcodeSuccess, nil
}