Skip to content

Commit

Permalink
Merge pull request #2 from d--j/socketmap
Browse files Browse the repository at this point in the history
Socketmap
  • Loading branch information
d--j authored Apr 6, 2023
2 parents e064a1f + eb3c911 commit fcea3f1
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 131 deletions.
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,20 @@ You can use command line parameters to change this default:
```
$ srs-milter -help
Usage of ./srs-milter:
-milterAddr string
Bind to address/port or unix domain socket path (default "127.0.0.1:10382")
-milterProto unix or tcp
Protocol family (unix or tcp) (default "tcp")
-milterAddr address/port
Bind milter server to address/port or unix domain socket path (default "127.0.0.1:10382")
-milterProto family
Protocol family (unix or tcp) of milter server (default "tcp")
-socketmapAddr address/port
Bind socketmap server to address/port or unix domain socket path (default "127.0.0.1:10383")
-socketmapProto family
Protocol family (unix or tcp) of socketmap server (default "tcp")
-forward email
email to do forward SRS lookup for. If specified the milter will not be started.
-reverse email
email to do reverse SRS lookup for. If specified the milter will not be started.
-systemd
enable systemd mode (log without date/time)
```

## MTA configuration
Expand All @@ -107,8 +110,11 @@ Add this to `/etc/postfix/main.cf`
smtpd_milters = inet:127.0.0.1:10382
non_smtpd_milters = inet:127.0.0.1:10382
milter_protocol = 6
# so that SRS bounces can be accepted - otherwise Postfix will reject the bounces
mydestination = $myhostname, srs.example.com
# if you use a dedicated SRS domain (what you should do) then you need to tell Postfix to accept SRS bounces to this domain.
# You could do that with e.g.:
relay_domains = hash:your/relay/domain/list srs.example.com
recipient_canonical_maps = socketmap:inet:localhost:10383:decode
recipient_canonical_classes = envelope_recipient
```
If you already have milters defined (e.g. Rspamd),
Expand Down
44 changes: 37 additions & 7 deletions cmd/srs-milter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package main
import (
"context"
"flag"
"net"
"os"
"sync"

"github.com/d--j/go-milter/mailfilter"
"github.com/d--j/go-socketmap"
"github.com/d--j/srs-milter"
"github.com/fsnotify/fsnotify"
"github.com/inconshreveable/log15"
Expand All @@ -28,23 +30,31 @@ var (
func main() {
// parse commandline arguments
var systemd bool
var milterProtocol, milterAddress, forward, reverse string
var milterProtocol, milterAddress, socketmapProtocol, socketmapAddress, forward, reverse string
flag.StringVar(&milterProtocol,
"milterProto",
"tcp",
"Protocol family (`unix or tcp`)")
"Protocol `family` (unix or tcp) of milter server")
flag.StringVar(&milterAddress,
"milterAddr",
"127.0.0.1:10382",
"Bind to address/port or unix domain socket path")
"Bind milter server to `address/port` or unix domain socket path")
flag.StringVar(&socketmapProtocol,
"socketmapProto",
"tcp",
"Protocol `family` (unix or tcp) of socketmap server")
flag.StringVar(&socketmapAddress,
"socketmapAddr",
"127.0.0.1:10383",
"Bind socketmap server to `address/port` or unix domain socket path")
flag.StringVar(&forward,
"forward",
"",
"`email` to do forward SRS lookup for. If specified the milter will not be started.")
"`email` to do forward SRS lookup for. If specified the daemon will not be started.")
flag.StringVar(&reverse,
"reverse",
"",
"`email` to do reverse SRS lookup for. If specified the milter will not be started.")
"`email` to do reverse SRS lookup for. If specified the daemon will not be started.")
flag.BoolVar(&systemd, "systemd", false, "enable systemd mode (log without date/time)")
flag.Parse()

Expand All @@ -61,7 +71,11 @@ func main() {

// make sure the specified protocol is either unix or tcp
if milterProtocol != "unix" && milterProtocol != "tcp" {
logger.Crit("invalid protocol name", "protocol", milterProtocol)
logger.Crit("invalid miler protocol name", "protocol", milterProtocol)
os.Exit(1)
}
if socketmapProtocol != "unix" && socketmapProtocol != "tcp" {
logger.Crit("invalid socketmap protocol name", "protocol", socketmapProtocol)
os.Exit(1)
}

Expand Down Expand Up @@ -152,7 +166,23 @@ func main() {
logger.Crit("error creating milter", "err", err)
os.Exit(1)
}
logger.Info("milter ready", log15.Ctx{"network": filter.Addr().Network(), "address": filter.Addr().String()})

smListener, err := net.Listen(socketmapProtocol, socketmapAddress)
if err != nil {
logger.Crit("error creating socketmap listener", "err", err)
os.Exit(1)
}

go func() {
socketmap.Serve(smListener, func(_ context.Context, lookup, key string) (string, bool, error) {
RuntimeConfigMutex.RLock()
config := RuntimeConfig
RuntimeConfigMutex.RUnlock()
return srsmilter.Socketmap(config, lookup, key)
})
}()

logger.Info("ready", "milterProto", filter.Addr().Network(), "milterAddr", filter.Addr().String(), "socketmapProto", smListener.Addr().Network(), "socketmapAddr", smListener.Addr().String())

// quit when milter quits
filter.Wait()
Expand Down
2 changes: 1 addition & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func emailWithoutExtension(local string, asciiDomain string) string {
if plus < 1 {
return fmt.Sprintf("%s@%s", local, asciiDomain)
}
return fmt.Sprintf("%s@%s", local[:plus-1], asciiDomain)
return fmt.Sprintf("%s@%s", local[:plus], asciiDomain)
}

func (c *Configuration) ResolveForward(email *addr.RcptTo) (emails []*addr.RcptTo) {
Expand Down
24 changes: 24 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,27 @@ func TestDomain_Unicode(t *testing.T) {
})
}
}

func Test_emailWithoutExtension(t *testing.T) {
type args struct {
local string
asciiDomain string
}
tests := []struct {
name string
args args
want string
}{
{"empty", args{"", ""}, "@"},
{"without", args{"local", "example.com"}, "[email protected]"},
{"with", args{"local+hi", "example.com"}, "[email protected]"},
{"double", args{"local+hi+ho", "example.com"}, "[email protected]"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := emailWithoutExtension(tt.args.local, tt.args.asciiDomain); got != tt.want {
t.Errorf("emailWithoutExtension() = %v, want %v", got, tt.want)
}
})
}
}
82 changes: 53 additions & 29 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,40 @@ package srsmilter

import (
"context"
"fmt"
"strings"
"time"

"github.com/d--j/go-milter/mailfilter"
"github.com/inconshreveable/log15"
"github.com/d--j/go-milter/mailfilter/addr"
"github.com/emersion/go-message/mail"
)

func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache *Cache) (mailfilter.Decision, error) {
startTime := time.Now()
didSomething := false
fromIsSrs := trx.MailFrom().AsciiDomain() == config.SrsDomain.String() && looksLikeSrs(trx.MailFrom().Local())
hasRemoteTo := false
hasDkim := trx.Headers().Value("Dkim-Signature") != ""
toReplacements := make(map[string]string)
actions := []string(nil)

logger := Log.New(log15.Ctx{"qid": trx.QueueId(), "user": trx.MailFrom().AuthenticatedUser()})
logger.Debug("start", log15.Ctx{"ofrom": trx.MailFrom().Addr})
logger := Log.New("sub", "milter", "qid", trx.QueueId(), "user", trx.MailFrom().AuthenticatedUser())
logger.Debug("start", "ofrom", trx.MailFrom().Addr)

// change any rcpt to that is pointing to our SRS domain back to the real address
// (just in case that our socketmap server did not do that already)
for _, to := range trx.RcptTos() {
if to.AsciiDomain() != config.SrsDomain.String() || !looksLikeSrs(to.Local()) {
logger.Debug("to is not one of our SRS addresses", log15.Ctx{"to": to.Addr})
logger.Debug("to is not one of our SRS addresses", "to", to.Addr)
continue
}
didSomething = true
rewrittenTo, err := ReverseSrs(to.Addr, config)
if err != nil {
logger.Error("error while generating reverse SRS address", log15.Ctx{"oto": to.Addr, "to": rewrittenTo, "err": err})
logger.Error("error while generating reverse SRS address", "oto", to.Addr, "to", rewrittenTo, "err", err)
} else {
logger.Info("reverse SRS", log15.Ctx{"oto": to.Addr, "to": rewrittenTo})
toReplacements[to.Addr] = rewrittenTo
logger.Debug("reverse SRS", "oto", to.Addr, "to", rewrittenTo)
trx.AddRcptTo(rewrittenTo, "")
trx.DelRcptTo(to.Addr)
actions = append(actions, fmt.Sprintf("recipient_env:%s:%s", to.Addr, rewrittenTo))
}
}

Expand All @@ -44,28 +46,28 @@ func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache
for _, t := range config.ResolveForward(to) {
if t.Addr != "" && !config.IsLocalDomain(t.AsciiDomain()) {
hasRemoteTo = true
logger.Info("to is remote", "to", t.Addr, "transport", t.Transport())
logger.Debug("to is remote", "to", t.Addr, "transport", t.Transport())
break
}
logger.Debug("to is not remote", "to", t.Addr, "transport", t.Transport())
}
}
if hasRemoteTo && cache.IsLocalNotAllowedToSend(trx.MailFrom().Addr, trx.MailFrom().AsciiDomain()) {
didSomething = true
srsAddress, err := ForwardSrs(trx.MailFrom().Addr, config)
if err != nil {
logger.Error("error while generating SRS address", log15.Ctx{"ofrom": trx.MailFrom().Addr, "from": srsAddress, "err": err})
logger.Error("error while generating SRS address", "ofrom", trx.MailFrom().Addr, "from", srsAddress, "err", err)
} else {
logger.Info("SRS", log15.Ctx{"ofrom": trx.MailFrom().Addr, "from": srsAddress})
logger.Debug("SRS", "ofrom", trx.MailFrom().Addr, "from", srsAddress)
// Sendmail does not like getting ESMTP args, so we always send empty ESMTP args
trx.ChangeMailFrom(srsAddress, "")
actions = append(actions, fmt.Sprintf("sender:%s:%s", trx.MailFrom().Addr, srsAddress))
}
}
}

// fix up To:, Cc: and Bcc: headers -- but only if there are no DKIM-Signatures that we might break
// (we assume that the To header is secured by the DKIM signature – almost all do this)
if len(toReplacements) > 0 && !hasDkim {
// (we assume that the To header is secured by the DKIM signature – almost all DKIM signers do this)
if !hasDkim {
fields := trx.Headers().Fields()
for fields.Next() {
switch fields.CanonicalKey() {
Expand All @@ -75,34 +77,56 @@ func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache
}
addresses, err := fields.AddressList()
if err != nil {
logger.Warn("error parsing address list, skipping", log15.Ctx{"key": fields.Key(), "value": fields.Value(), "err": err})
logger.Warn("error parsing address list, skipping", "key", fields.Key(), "value", fields.Value(), "err", err)
continue
}
changed := false
for _, a := range addresses {
for search, replace := range toReplacements {
if a.Address == search {
a.Address = replace
changed = true
}
to := addr.NewRcptTo(a.Address, "", "")
if to.AsciiDomain() != config.SrsDomain.String() || !looksLikeSrs(to.Local()) {
logger.Debug("to is not one of our SRS addresses", "to", to.Addr, "hdr", fields.Key())
continue
}
rewrittenTo, err := ReverseSrs(to.Addr, config)
if err != nil {
logger.Error("error while generating header reverse SRS address", "oto", to.Addr, "to", rewrittenTo, "err", err)
} else {
logger.Debug("header reverse SRS", "oto", to.Addr, "to", rewrittenTo)
a.Address = rewrittenTo
changed = true
actions = append(actions, fmt.Sprintf("recipient_hdr:%s:%s", to.Addr, rewrittenTo))
}
}
if changed {
fields.SetAddressList(addresses)
logger.Info("fixing MIME header", log15.Ctx{"key": fields.Key(), "ovalue": fields.Value(), "addresses": addresses})
logger.Debug("fixing MIME header", "key", fields.Key(), "ovalue", fields.Value(), "addresses", outputAddresses(addresses))
} else {
logger.Debug("nothing to do", log15.Ctx{"key": fields.Key(), "value": fields.Value(), "addresses": addresses})
logger.Debug("nothing to do", "key", fields.Key(), "value", fields.Value(), "addresses", outputAddresses(addresses))
}
}
} else if len(toReplacements) > 0 && hasDkim {
logger.Info("did not touch MIME headers because of DKIM")
} else {
logger.Debug("did not touch MIME headers because of DKIM")
}

if didSomething {
logger.Info("done", "dur", time.Now().Sub(startTime))
if len(actions) > 0 {
logger.Info("done", "dur", time.Now().Sub(startTime), "actions", strings.Join(actions, ","))
} else {
logger.Debug("done", "dur", time.Now().Sub(startTime))
logger.Debug("end", "dur", time.Now().Sub(startTime))
}

return mailfilter.Accept, nil
}

func outputAddresses(addrs []*mail.Address) string {
b := strings.Builder{}
for i, a := range addrs {
if a == nil {
continue
}
if i > 0 {
b.WriteRune(',')
}
b.WriteString(a.Address)
}
return b.String()
}
Loading

0 comments on commit fcea3f1

Please sign in to comment.