-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathsinksmtp.go
1718 lines (1585 loc) · 50.4 KB
/
sinksmtp.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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// For sinksmtp usage and other documentation, see doc.go
// godoc . will show it.
package main
import (
"bufio"
"bytes"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"expvar"
"flag"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof"
"net/mail"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/siebenmann/smtpd"
)
var stats = expvar.NewMap("sinksmtp")
var times expvar.Map
var iptimes expvar.Map
var manyIps bool
var events struct {
connections, tlserrs, yakkers, ruleserr expvar.Int
ehlo, mailfrom, rcptto, data, messages, quits expvar.Int
starttls expvar.Int
ehloAccept, mailfromAccept expvar.Int
rcpttoAccept, dataAccept expvar.Int
aborts, rsets, tlson, rsetdrops expvar.Int
yakads, yakforces expvar.Int
notlscnt expvar.Int
abandons, refuseds expvar.Int
}
// TimeNZ is our message/logging time format; it's time without the timezone.
const TimeNZ = "2006-01-02 15:04:05"
func warnf(format string, elems ...interface{}) {
fmt.Fprintf(os.Stderr, "sinksmtp: "+format, elems...)
}
func die(format string, elems ...interface{}) {
warnf(format, elems...)
os.Exit(1)
}
// Suppress duplicate warning messages by running them all through
// a channel to a master, which can simply keep track of what the
// last message was.
var uniquer = make(chan string)
func warnonce(format string, elems ...interface{}) {
s := fmt.Sprintf(format, elems...)
uniquer <- s
}
// TODO: work out a way to clear this so that a sequence of
// parse error -> everything is clear -> same parse error again
// results in the error being printed again.
func warnbackend() {
var lastmsg string
for {
nmsg := <-uniquer
if nmsg != lastmsg {
if nmsg != "" {
fmt.Fprintf(os.Stderr, "sinksmtp: %s", nmsg)
}
lastmsg = nmsg
}
}
}
// ----
// Read address lists in. This is done here because we call warnf()
// under some circumstances.
// TODO: fix that.
func readList(rdr *bufio.Reader) ([]string, error) {
var a []string
for {
line, err := rdr.ReadString('\n')
if err != nil {
if err == io.EOF {
return a, nil
}
return a, err
}
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
line = strings.ToLower(line)
a = append(a, line)
}
// Cannot be reached; for loop has no breaks.
}
func loadList(fname string) []string {
if fname == "" {
return nil
}
fp, err := os.Open(fname)
if err != nil {
// An address list that is missing entirely is not an
// error that we bother reporting.
if !os.IsNotExist(err) {
warnonce("error opening %s: %v\n", fname, err)
}
return nil
}
defer fp.Close()
alist, err := readList(bufio.NewReader(fp))
if err != nil {
// We deliberately return a nil addrList on error instead
// of a partial one.
warnonce("Problem loading addr list %s: %v\n", fname, err)
return nil
}
return alist
}
// ----
// Load a|the rule file. We assume filename is non-empty.
func loadRules(fname string) ([]*Rule, error) {
fp, err := os.Open(fname)
if err != nil {
return nil, err
}
defer fp.Close()
b, err := io.ReadAll(fp)
if err != nil {
return nil, err
}
rl, err := Parse(string(b))
if err != nil {
return nil, fmt.Errorf("rules parsing error %v", err)
}
return rl, nil
}
// our contract is that we always return either real rules or an error.
func accumRules(baserules []*Rule, fname string) ([]*Rule, error) {
if fname == "" {
return baserules, nil
}
rules, err := loadRules(fname)
return append(baserules, rules...), err
}
// Accumulate the set of rules for this connection from our base rules
// (already pre-parsed) and rules loaded from our rules files, if any.
// If there are any errors in loading or parsing the rules files, we
// use the rules system itself to return a single rule that will defer
// everything.
func setupRules(baserules []*Rule) ([]*Rule, bool) {
var rules []*Rule
var rfile string
var err error
rules = baserules
for _, rfile = range rulefiles {
rules, err = accumRules(rules, rfile)
if err != nil {
break
}
}
if err == nil {
return rules, true
}
// Our rule is that if we're going to stall all activity, we're
// going to write a warning message about it.
// If the rules fail to load, we panic and stall everything via
// the simple mechanism of generating a 'stall all' set of rules.
warnonce("problem loading rules %s: %s\n", rfile, err)
events.ruleserr.Add(1)
return stallall, false
}
// ----
// Support for IP blacklists. We have two.
//
// notls is a blacklist of IPs that have TLS problems when talking to
// us. If an IP is present and is more recent than tlsTimeout (3
// days), we don't advertise TLS to them even if we could.
//
// yakkers is a blacklist of people who have made too many connections
// to us that they didn't do anything meaningful with within a certain
// period of time. Implicitly this is yakTimeout. Yakkers get a 'stall
// all' timeout.
const tlsTimeout = time.Hour * 72
// Theoretically redundant in the face of flag settings.
var yakTimeout = time.Hour * 8
var yakCount = 5
type ipEnt struct {
when time.Time
count int
}
type ipMap struct {
sync.Mutex
ips map[string]*ipEnt
stats struct {
// Size is not valid on the fly. Life is like that!
Size, Adds, AddsNew, AddsExpired, Dels, Sets int
// We count hits instead of misses because misses
// are the normal case.
Lookup, LookupHit, LookupExpired int
}
}
var notls = &ipMap{ips: make(map[string]*ipEnt)}
var yakkers = &ipMap{ips: make(map[string]*ipEnt)}
// We must take a TTL because we want to annul the count of existing
// but stale entries. Right now this only matters for yakkers, which
// is the only thing that cares about counts. Add() returns the count
// because that turns out to be convenient.
func (i *ipMap) Add(ip string, ttl time.Duration) int {
if ip == "" {
return 0
}
i.Lock()
i.stats.Adds++
t := i.ips[ip]
switch {
case t == nil:
t = &ipEnt{}
i.ips[ip] = t
i.stats.AddsNew++
case time.Since(t.when) >= ttl:
t.count = 0
i.stats.AddsExpired++
}
t.count++
t.when = time.Now()
cnt := t.count
i.Unlock()
return cnt
}
// Set forces the count for a specific IP to a specific value and
// returns the *old* count, not the current one.
func (i *ipMap) Set(ip string, ttl time.Duration, cnt int) int {
if ip == "" {
return 0
}
i.Lock()
i.stats.Sets++
t := i.ips[ip]
// TODO: should this be a new SetsNew/SetsExpired stats?
switch {
case t == nil:
t = &ipEnt{}
i.ips[ip] = t
i.stats.AddsNew++
case time.Since(t.when) >= ttl:
t.count = 0
i.stats.AddsExpired++
}
t.when = time.Now()
ocnt := t.count
t.count = cnt
i.Unlock()
return ocnt
}
func (i *ipMap) Del(ip string) {
if ip == "" {
return
}
i.Lock()
// we only count deletes if the entry actually existed,
// because I am crazy that way.
if _, ok := i.ips[ip]; ok {
i.stats.Dels++
delete(i.ips, ip)
}
i.Unlock()
}
func (i *ipMap) Lookup(ip string, ttl time.Duration) (bool, int) {
i.Lock()
// we defer the unlock because we now increment stats later.
defer i.Unlock()
t := i.ips[ip]
i.stats.Lookup++
if t == nil {
return false, 0
}
if time.Since(t.when) < ttl {
i.stats.LookupHit++
return true, t.count
}
i.stats.LookupExpired++
// NOTE: we cannot call i.Del() here because that would attempt
// to lock again. So we must delete directly. This has the side
// effect of not increasing stats.Dels; an expired lookup implies
// a delete.
delete(i.ips, ip)
return false, 0
}
// This is a hack. We feed this to expvar.Func().
func (i *ipMap) Stats() interface{} {
i.Lock()
defer i.Unlock()
i.stats.Size = len(i.ips)
return i.stats
}
// Count DNSBL hits
type dnsblCounts struct {
sync.Mutex
dbls map[string]uint64
}
var dblcounts = &dnsblCounts{dbls: make(map[string]uint64)}
var sblcounts = &dnsblCounts{dbls: make(map[string]uint64)}
var loccounts = &dnsblCounts{dbls: make(map[string]uint64)}
func (dc *dnsblCounts) Add(dbls []string) {
if len(dbls) == 0 {
return
}
dc.Lock()
for i := range dbls {
t := dc.dbls[dbls[i]]
t++
dc.dbls[dbls[i]] = t
}
dc.Unlock()
}
func (dc *dnsblCounts) Stats() interface{} {
dc.Lock()
defer dc.Unlock()
nm := make(map[string]uint64)
for k, v := range dc.dbls {
nm[k] = v
}
return nm
}
// This is used to log the SMTP commands et al for a given SMTP session.
// It encapsulates the prefix. Perhaps we could do this some other way,
// for example with a function closure, but PUNT for now.
// TODO: I'm convinced this is the wrong interface. See
//
// https://utcc.utoronto.ca/~cks/space/blog/programming/GoLoggingWrongIdiom
type smtpLogger struct {
prefix []byte
writer *bufio.Writer
}
func (log *smtpLogger) Write(b []byte) (n int, err error) {
// MY HEAD HURTS. WHY DOES THIS HAPPEN.
// ... long story involving implicit casts to interfaces.
// This safety code is disabled because I want a crash if I screw
// this up at a higher level. This may be a mistake.
//if log == nil {
// return
//}
// we might as well create the buffer at the right size.
buf := make([]byte, 0, len(b)+len(log.prefix))
buf = append(buf, log.prefix...)
buf = append(buf, b...)
n, err = log.writer.Write(buf)
if err == nil {
err = log.writer.Flush()
}
return n, err
}
// ----
//
// SMTP transaction data accumulated for a single message. If multiple
// messages were delivered over the same Conn, some parts of this will
// be reused.
type smtpTransaction struct {
raddr, laddr net.Addr
rip string
lip string
rdns *rDNSResults
// these tracking fields are valid only after the relevant
// phase/command has been accepted, ie they have the *accepted*
// EHLO name, MAIL FROM, etc.
heloname string
from string
rcptto []string
data string
hash string // canonical hash of the data, currently SHA1
bodyhash string // canonical hash of the message body (no headers)
when time.Time // when the email message data was received.
savedir string // directory to save message to
// Reflects the current state, so tlson false can convert to
// tlson true over time. cipher is valid only if tlson is true.
// servername is the SNI we were given, while peername is the
// name from any peer certificate; tlsverified is true if it
// is a verified name.
tlson bool
cipher uint16
tlsversion uint16
servername string
peername string
tlsverified bool
// Make our logger accessible in decider() as a hack.
log *smtpLogger
lastmsg string
lastamsg string
lastresgood bool // last result from decider()
}
// returns overall hash and body-of-message hash. The latter may not
// exist if the message is mangled, eg no actual body.
func genHash(b []byte) string {
h := sha1.New()
h.Write(b)
return fmt.Sprintf("%x", h.Sum(nil))
}
func getHashes(trans *smtpTransaction) (string, string) {
var hash, bodyhash string
hash = genHash([]byte(trans.data))
msg, err := mail.ReadMessage(strings.NewReader(trans.data))
if err != nil {
return hash, "<cannot-parse-message>"
}
body, err := io.ReadAll(msg.Body)
if err != nil {
return hash, "<cannot-read-body?>"
}
bodyhash = genHash(body)
return hash, bodyhash
}
func writeDNSList(writer io.Writer, pref string, dlist []string) {
if len(dlist) == 0 {
return
}
fmt.Fprint(writer, pref)
for _, e := range dlist {
fmt.Fprintf(writer, " %s", e)
}
fmt.Fprintf(writer, "\n")
}
// tlsProtoVersion returns a useful string describing a TLS version.
// It's annoying that crypto/tls doesn't already provide things like
// this, because of build issues that result across Go versions.
//
// This doesn't use the constants from crypto/tls so that it can build
// under Go versions that don't have the constants defined (either
// because they weren't added yet, for TLS 1.3, or because they've been
// deprecated, for SSLv3.
func tlsProtoVersion(ver uint16) string {
switch ver {
case 0x0300: // tls.VersionSSL30
return "SSLv3"
case 0x0301: // tls.VersionTLS10
return "TLSv1.0"
case 0x0302: // tls.VersionTLS11
return "TLSv1.1"
case 0x0303: // tls.VersionTLS12
return "TLSv1.2"
case 0x0304: // tls.VersionTLS13
return "TLSv1.3"
default:
return fmt.Sprintf("tls-0x%04x", ver)
}
}
// tlsName returns the (host) name for the peer certificate,
// either the Subject CommonName if it has one or the first
// DNS name otherwise (or a complaint).
func tlsName(cert *x509.Certificate) string {
if cert.Subject.CommonName != "" {
return cert.Subject.CommonName
}
if len(cert.DNSNames) > 0 {
return cert.DNSNames[0]
}
return "<no CN or DNS Names>"
}
func tlsTryVerifyConnection(state tls.ConnectionState) error {
if len(state.VerifiedChains) > 0 {
return nil
}
opts := x509.VerifyOptions{
// Since we're verifying a client certificate, we don't
// set DNSName. The only thing we can set it to is the
// name the client gave us, and that obviously verifies.
// Otherwise, this errors out if the client does SNI along
// with sending a client certificate (as GMail does).
//DNSName: state.ServerName,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
for _, cert := range state.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(opts)
return err
}
// tlsPeerLog logs a report about the verification status of a TLS
// client certificate.
func tlsClientLog(state tls.ConnectionState, logger *smtpLogger) {
if len(state.PeerCertificates) == 0 {
return
}
err := tlsTryVerifyConnection(state)
peername := tlsName(state.PeerCertificates[0])
if err == nil {
writeLog(logger, "! TLS client certificate for '%s' verifies.\n", peername)
} else {
writeLog(logger, "! TLS client certificate for '%s' doesn't verify: %s\n", peername, err)
}
}
// return a block of bytes that records the message details,
// including the actual message itself. We also return a hash of what
// we consider the constant data about this message, which included
// envelope metadata and the source IP and its DNS information.
func msgDetails(prefix string, trans *smtpTransaction) ([]byte, string) {
var outbuf, outbuf2 bytes.Buffer
fwrite := bufio.NewWriter(&outbuf)
fmt.Fprintf(fwrite, "id %s %v %s\n", prefix, trans.raddr,
trans.when.Format(TimeNZ))
writer := bufio.NewWriter(&outbuf2)
rmsg := trans.rip
if rmsg == "" {
rmsg = trans.raddr.String()
}
fmt.Fprintf(writer, "remote %s to %v with helo '%s'\n", rmsg,
trans.laddr, trans.heloname)
writeDNSList(writer, "remote-dns", trans.rdns.verified)
writeDNSList(writer, "remote-dns-nofwd", trans.rdns.nofwd)
writeDNSList(writer, "remote-dns-inconsist", trans.rdns.inconsist)
if trans.tlson {
fmt.Fprintf(writer, "tls on cipher 0x%04x", trans.cipher)
if cn := cipherNames[trans.cipher]; cn != "" {
fmt.Fprintf(writer, " name %s", cn)
}
fmt.Fprintf(writer, " proto %s", tlsProtoVersion(trans.tlsversion))
if trans.servername != "" {
fmt.Fprintf(writer, " server-name '%s'", trans.servername)
}
if trans.peername != "" {
fmt.Fprintf(writer, " client-name '%s'", trans.peername)
}
if trans.tlsverified {
fmt.Fprintf(writer, " client-name-verified")
}
fmt.Fprintf(writer, "\n")
}
fmt.Fprintf(writer, "from <%s>\n", trans.from)
for _, a := range trans.rcptto {
fmt.Fprintf(writer, "to <%s>\n", a)
}
fmt.Fprintf(writer, "hash %s bytes %d\n", trans.hash, len(trans.data))
fmt.Fprintf(writer, "bodyhash %s\n", trans.bodyhash)
fmt.Fprintf(writer, "body\n%s", trans.data)
writer.Flush()
metahash := genHash(outbuf2.Bytes())
fwrite.Write(outbuf2.Bytes())
fwrite.Flush()
return outbuf.Bytes(), metahash
}
// Log details about the message to the logfile.
// Not all details covered by msgDetails() are reflected in the logfile,
// which is intended to be more terse.
func logMessage(prefix string, trans *smtpTransaction, logf io.Writer) {
if logf == nil {
return
}
var outbuf bytes.Buffer
writer := bufio.NewWriter(&outbuf)
fmt.Fprintf(writer, "%s [%s] from %v / ",
trans.when.Format(TimeNZ), prefix,
trans.raddr)
fmt.Fprintf(writer, "<%s> to", trans.from)
for _, a := range trans.rcptto {
fmt.Fprintf(writer, " <%s>", a)
}
fmt.Fprintf(writer, ": message %d bytes hash %s body %s | local %v helo '%s'",
len(trans.data), trans.hash, trans.bodyhash, trans.laddr,
trans.heloname)
if trans.tlson {
fmt.Fprintf(writer, " tls:cipher 0x%04x tls:proto 0x%04x", trans.cipher, trans.tlsversion)
}
fmt.Fprintf(writer, "\n")
writer.Flush()
logf.Write(outbuf.Bytes())
}
// Having received a message, do everything to it that we want to.
// Here we log the message reception and possibly save it.
func handleMessage(prefix string, trans *smtpTransaction, logf io.Writer) (string, error) {
var hash string
logMessage(prefix, trans, logf)
if trans.savedir == "" {
return trans.hash, nil
}
m, mhash := msgDetails(prefix, trans)
// There are three possible hashes for message naming:
//
// 'msg' uses only the DATA (actual email) and counts on the
// transaction log to recover metadata.
//
// 'full' adds all metadata except the ID line and the sender
// port; this should squelch duplicates that emerge from
// things that resend after a rejected DATA transaction.
//
// 'all' adds even the ID line and the sender port, which is
// very likely to be completely unique for every message (a
// sender would have to reuse the same source port for a
// message received within a second).
//
// There is no option to save based on the body hash alone,
// because that would lose data unless we saved the message
// headers separately and no let's not get that complicated.
switch hashtype {
case "msg":
hash = trans.hash
case "full":
hash = mhash
case "all":
hash = genHash(m)
default:
panic(fmt.Sprintf("unhandled hashtype '%s'", hashtype))
}
tgt := trans.savedir + "/" + hash
// O_CREATE|O_EXCL will fail if the file already exists, which
// is okay with us.
fp, err := os.OpenFile(tgt, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err == nil {
_, err = fp.Write(m)
if err != nil {
warnf("error writing message file: %s\n", err)
}
_ = fp.Sync()
err = fp.Close()
if err != nil {
warnf("error closing message file: %s\n", err)
}
} else if !os.IsExist(err) {
warnf("error writing message file: %v\n", err)
} else {
err = nil
}
return hash, err
}
// Given a SBL hit on a remote IP, try to give us what SBL records were hit.
func getSBLHits(t *smtpTransaction) []string {
var sbls []string
// since we know this hit, we can omit a lot of checks.
s := strings.Split(t.rip, ".")
ln := fmt.Sprintf("%s.%s.%s.%s.sbl.spamhaus.org.", s[3], s[2], s[1], s[0])
txts, err := net.LookupTXT(ln)
if err != nil {
return sbls
}
for _, txt := range txts {
idx := strings.LastIndex(txt, "/")
if idx == -1 || idx == len(txt)-1 {
continue
}
sbls = append(sbls, txt[idx+1:])
}
sort.Strings(sbls)
return sbls
}
// As a hack, log DNSBL hits if any. These hits may not have
// determined the results but there you go.
// This is a hack but I want to capture this information somehow.
func logDnsbls(c *Context) {
if len(c.dnsblhit) == 0 || c.trans.log == nil {
return
}
sort.Strings(c.dnsblhit)
lmsg := fmt.Sprintf("! dnsbl hit: %s\n",
strings.Join(c.dnsblhit, " "))
if lmsg == c.trans.lastmsg {
return
}
c.trans.log.Write([]byte(lmsg))
c.trans.lastmsg = lmsg
// Count them:
dblcounts.Add(c.dnsblhit)
// Special bonus feature: log actual SBL entries.
for i := range c.dnsblhit {
if c.dnsblhit[i] == "sbl.spamhaus.org." {
sbls := getSBLHits(c.trans)
if len(sbls) > 0 {
lmsg = fmt.Sprintf("! SBL records: %s\n",
strings.Join(sbls, " "))
c.trans.log.Write([]byte(lmsg))
sblcounts.Add(sbls)
}
}
}
}
// trivial but I care about these messages, neurotic though it may be.
func pluralRecips(c *Context) string {
if len(c.trans.rcptto) > 1 {
return "those addresses"
}
return "that address"
}
func doAccept(convo *smtpd.Conn, c *Context, transid string) {
msg := c.withprops["message"]
switch {
case msg != "":
if transid != "" {
msg += "\nAccepted with ID " + transid
}
convo.AcceptMsg("%s", msg)
case transid != "":
convo.AcceptData(transid)
default:
convo.Accept()
}
}
// Decide what to do and then do it if it is a rejection or a tempfail.
// If given an id (and it is in the message handling phase) we call
// RejectData(). This is our convenience driver for the rules engine,
// Decide().
//
// Returns false if the message was accepted, true if decider() handled
// a rejection or tempfail.
func decider(ph Phase, evt smtpd.EventInfo, c *Context, convo *smtpd.Conn, id string, trans *smtpTransaction) bool {
res := Decide(ph, evt, c)
logDnsbls(c)
// Terrible hack to log DNS lookup failure specifics.
if c.domerr != nil && c.trans.log != nil {
lmsg := fmt.Sprintf("! %s\n", c.domerr)
if lmsg != c.trans.lastamsg {
c.trans.log.Write([]byte(lmsg))
c.trans.lastamsg = lmsg
}
}
// The moment a rule sets a savedir, it becomes sticky.
// This lets you select a savedir based on eg from matching
// instead of having to do games later.
if sd := c.withprops["savedir"]; sd != "" {
c.trans.savedir = sd
}
// rule notes are deliberately logged every time they hit.
// this may be a mistake given EHLO retrying as HELO, but
// I'll see.
if note := c.withprops["note"]; note != "" {
c.trans.log.Write([]byte(fmt.Sprintf("! rule note: %s\n", note)))
}
// Disable TLS if desired, or just disable asking for client certs.
switch c.withprops["tls-opt"] {
case "off":
convo.Config.TLSConfig = nil
case "no-client":
// 'tls-opt no-client' without certificates should not
// crash.
if convo.Config.TLSConfig != nil {
convo.Config.TLSConfig.ClientAuth = tls.NoClientCert
}
}
if res == aNoresult || res == aAccept {
trans.lastresgood = true
return false
}
trans.lastresgood = false
if ph == pConnect {
// TODO: have some way to stall or reject connections
// in smtpd. Or should that be handled outside of it?
// Right now a reject result means 'drop', stall will
// implicitly cause us to go on.
return res == aReject
}
msg := c.withprops["message"]
switch res {
case aReject:
// This is kind of a hack.
// We assume that 'id' is only set when we should report it,
// which is kind of safe.
if msg != "" {
if id != "" {
msg += "\nRejected with ID " + id
}
convo.RejectMsg("%s", msg)
return true
}
// Default messages are kind of intricate.
switch {
case id != "" && ph == pMessage:
convo.RejectMsg("We do not consent to you emailing %s\nRejected with ID %s", pluralRecips(c), id)
case ph == pMessage || ph == pData:
convo.RejectMsg("We do not consent to you emailing %s", pluralRecips(c))
case ph == pRto:
convo.RejectMsg("We do not consent to you emailing that address")
default:
convo.Reject()
}
case aStall:
if msg != "" {
convo.TempfailMsg("%s", msg)
} else {
convo.Tempfail()
}
default:
panic("impossible res")
}
return true
}
func writeLog(logger *smtpLogger, format string, elems ...interface{}) {
if logger == nil {
return
}
s := fmt.Sprintf(format, elems...)
logger.Write([]byte(s))
}
func yakLog(dnlog io.Writer, trans *smtpTransaction, prefix, what string) {
if dnlog == nil {
return
}
fmt.Fprintf(dnlog, "%s [%s] %s %s -> %s\n", time.Now().Format(TimeNZ),
prefix, what, trans.rip, trans.laddr)
}
// Process a single connection.
func process(cid int, nc net.Conn, certs []tls.Certificate, logf io.Writer, smtplog io.Writer, dnlog io.Writer, baserules []*Rule) {
var evt smtpd.EventInfo
var convo *smtpd.Conn
var logger *smtpLogger
var l2 io.Writer
var gotsomewhere, stall, sesscounts bool
var cfg smtpd.Config
defer nc.Close()
trans := &smtpTransaction{}
trans.savedir = savedir
trans.raddr = nc.RemoteAddr()
trans.laddr = nc.LocalAddr()
laddrstr := trans.laddr.String()
prefix := fmt.Sprintf("%d/%d", os.Getpid(), cid)
trans.rip, _, _ = net.SplitHostPort(trans.raddr.String())
trans.lip, _, _ = net.SplitHostPort(laddrstr)
loccounts.Add([]string{laddrstr})
var c *Context
// nit: in the presence of yakkers, we must know whether or not
// the rules are good because bad rules turn *everyone* into
// yakkers (since they prevent clients from successfully EHLO'ing).
rules, rulesgood := setupRules(baserules)
// A yakker is a client that is repeatedly connecting to us
// without doing anything successfully. After a certain number
// of attempts we turn them off. We only do this if we're logging
// SMTP commands; if we're not logging, we don't care.
// This is kind of a hack, but this code is for Chris and this is
// what Chris cares about.
// sesscounts is true if this session should count for being a
// 'bad' session if we don't get far enough. Sessions with TLS
// errors don't count, as do sessions with bad rules or sessions
// where yakCount == 0.
sesscounts = rulesgood && yakCount > 0
hit, cnt := yakkers.Lookup(trans.rip, yakTimeout)
if yakCount > 0 && hit && cnt >= yakCount && smtplog != nil {
// nit: if the rules are bad and we're stalling anyways,
// yakkers still have their SMTP transactions not logged.
c = newContext(trans, stallall)
stall = true
sesscounts = false
events.yakkers.Add(1)
updateTimeOf("yakker", laddrstr)
// Log one line of information about this yakker.
// It would be potentially interesting to find out how old
// this yakker entry is, but we can't get that right now.
yakLog(dnlog, trans, prefix, "connection")
} else {
c = newContext(trans, rules)
updateTimeOf("regular", laddrstr)
}
//fmt.Printf("rules are:\n%+v\n", c.ruleset)
if smtplog != nil && !stall {
logger = &smtpLogger{}
logger.prefix = []byte(prefix)
logger.writer = bufio.NewWriterSize(smtplog, 8*1024)
trans.log = logger
l2 = logger
}
sname := laddrstr
if srvname != "" {
sname = srvname
} else {
lip, _, _ := net.SplitHostPort(sname)
// we don't do a verified lookup of the local IP address
// because it's theoretically under your control, so if
// you want to forge stuff that's up to you.
nlst, err := net.LookupAddr(lip)
if err == nil && len(nlst) > 0 {
sname = nlst[0]
if sname[len(sname)-1] == '.' {
sname = sname[:len(sname)-1]
}
}
}
if connfile != "" {
dm, err := loadConnFile(connfile)
if err != nil {
warnf("error loading per-connection rules '%s': %s\n", connfile, err)
}
// dm.find() explicitly works even on nil dm, so we don't
// need to guard it.
if pd := dm.find(nc); pd != nil {
if pd.myname != "" {
sname = pd.myname
}
certs = pd.certs
}
}
cfg.LocalName = sname
cfg.SayTime = true
cfg.SftName = "sinksmtp"
cfg.Announce = "This server does not deliver email."
// stalled conversations are always slow, even if -S is not set.
// TODO: make them even slower than this? I probably don't care.
if goslow || stall {
cfg.Delay = time.Second / 10
}
// Don't offer TLS to hosts that have too many TLS failures.
// We give hosts *two* tries at setting up TLS because some
// hosts start by offering SSLv2, which is an instant-fail,
// even if they support stuff that we do. We hope that their
// SSLv2 failure will cause them to try again in another
// connection with TLS only.
// See https://code.google.com/p/go/issues/detail?id=3930
blocktls, blcount := notls.Lookup(trans.rip, tlsTimeout)
if len(certs) > 0 && !(blocktls && blcount >= 2) {
var tlsc tls.Config
tlsc.Certificates = certs
// if there is already one TLS failure for this host,
// it might be because of a bad client certificate.
// so on the second time around we don't ask for one.
// (More precisely we only ask for a client cert if
// there are no failures so far.)
// Another reason for failure here is a SSLv3 only
// host without a client certificate. This produces
// the error:
// tls: received unexpected handshake message of type *tls.clientKeyExchangeMsg when waiting for *tls.certificateMsg
//if blcount == 0 {
// tlsc.ClientAuth = tls.VerifyClientCertIfGiven