Preliminary milter client implementation

Based on github.com/foxcpp/go-milter fork
This commit is contained in:
fox.cpp 2020-05-03 20:20:35 +03:00
parent 1fca7f62c9
commit d0e7df023c
No known key found for this signature in database
GPG key ID: 5B991F6215D2FCC0
5 changed files with 448 additions and 0 deletions

View file

@ -747,3 +747,48 @@ the message pipeline action.
Two codes are defined implicitly, exit code 1 causes the message to be rejected
with a permanent error, exit code 2 causes the message to be quarantined. Both
action can be overriden using the 'code' directive.
## Milter protocol check (milter)
The 'milter' implements subset of Sendmail's milter protocol that can be used
to integrate external software in maddy.
Notable limitations of protocol implementation in maddy include:
1. Changes of envelope sender address are not supported
2. Removal and addition of envelope recipients is not supported
3. Removal and replacement of header fields is not supported
4. Headers fields can be inserted only on top
5. Milter does not receive some "macros" provided by sendmail.
Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be
removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to
incomplete implementation.
```
milter {
endpoint <endpoint>
fail_open false
}
milter <endpoint>
```
## Arguments
When defined inline, the first argument specifies endpoint to access milter
via. See below.
## Configuration directives
**Syntax:** endpoint _scheme://path_ ++
**Default:** not set
Specifies milter protocol endpoint to use.
The endpoit is specified in standard URL-like format:
'tcp://127.0.0.1:6669' or 'unix:///var/lib/milter/filter.sock'
**Syntax:** fail_open _boolean_ ++
**Default:** false
Toggles behavior on milter I/O errors. If false ("fail closed") - message is
rejected with temporary error code. If true ("fail open") - check is skipped.

3
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-message v0.11.2
github.com/emersion/go-milter v0.1.0
github.com/emersion/go-msgauth v0.4.1-0.20200429175443-e4c87369d72f
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-smtp v0.12.2-0.20200219094142-f9be832b5554
@ -36,3 +37,5 @@ require (
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
replace github.com/emersion/go-milter => github.com/foxcpp/go-milter v0.1.1-0.20200502214548-312001df0b51

5
go.sum
View file

@ -38,6 +38,8 @@ github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0
github.com/emersion/go-message v0.11.2 h1:oxO9SQ+3wgBAQRdk07eqfkCJ26Tl8ZHF7CcpGVoE00o=
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-milter v0.0.0-20190311184326-c3095a41a6fe/go.mod h1:aEaq7U51ARlk+2UeXTtdrDYeYWAUn/QjEwWzs7lD8OU=
github.com/emersion/go-milter v0.1.0 h1:61u1N/QaHCcQpryDrH5icX2pO1hmU/1g3YID209JKc4=
github.com/emersion/go-milter v0.1.0/go.mod h1:Ys4s0SSEt4H2jEl3kDbupNQq61wH4ztM9VHZMBfYKqw=
github.com/emersion/go-msgauth v0.4.0 h1:LM9vNyaecGbhWiwKCAnzql7sVfgL072/QL5ZsIaHSL8=
github.com/emersion/go-msgauth v0.4.0/go.mod h1:7r9HUSXL1dq+KK7Xqg0JlyBxNFGf5+JouRvSz4wBZCQ=
github.com/emersion/go-msgauth v0.4.1-0.20200429121350-af2e57960c76 h1:lH/JVE7BLxJweku8z15q/w9N4Fk/GYy8NqJy/dCmOfU=
@ -60,6 +62,9 @@ github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005 h1:pfoFtk
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005/go.mod h1:34FwxnjC2N+EFs2wMtsHevrZLWRKRuVU8wEcHWKq/nE=
github.com/foxcpp/go-imap-sql v0.4.1-0.20200426175844-c3172a53940a h1:7IHua8lv9Qouf/TnjzB/8eYtjzws5zcn8Zxvf49ZJOo=
github.com/foxcpp/go-imap-sql v0.4.1-0.20200426175844-c3172a53940a/go.mod h1:DISGTnTW4OX3qgG8xP5lI+qYRb97b8NvEHV7TSlUy5g=
github.com/foxcpp/go-milter v0.1.1-0.20200430224718-2b7763a588dc/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
github.com/foxcpp/go-milter v0.1.1-0.20200502214548-312001df0b51 h1:njk1z7faETtQRTr+l9kSgXtUoAS6FghmAC7N8SSA3Ac=
github.com/foxcpp/go-milter v0.1.1-0.20200502214548-312001df0b51/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
github.com/foxcpp/go-mockdns v0.0.0-20191216195825-5eabd8dbfe1f h1:b/CFmrdqIGU6eV774xeaQwd1VfgiLuR/8jiY3LyLiMc=
github.com/foxcpp/go-mockdns v0.0.0-20191216195825-5eabd8dbfe1f/go.mod h1:tPg4cp4nseejPd+UKxtCVQ2hUxNTZ7qQZJa7CLriIeo=
github.com/foxcpp/go-mockdns v0.0.0-20200218175156-e3faeb733052 h1:a9cMhXBc4p7TUqjHvqUWbhpLmRx+mgpRRPZB7L648GY=

View file

@ -0,0 +1,394 @@
package milter
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"github.com/emersion/go-message/textproto"
"github.com/emersion/go-milter"
"github.com/foxcpp/maddy/internal/buffer"
"github.com/foxcpp/maddy/internal/config"
"github.com/foxcpp/maddy/internal/exterrors"
"github.com/foxcpp/maddy/internal/log"
"github.com/foxcpp/maddy/internal/module"
"github.com/foxcpp/maddy/internal/target"
)
const modName = "milter"
type Check struct {
cl *milter.Client
milterUrl string
failOpen bool
instName string
log log.Logger
}
func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
c := &Check{
instName: instName,
log: log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},
}
switch len(inlineArgs) {
case 1:
c.milterUrl = inlineArgs[0]
case 0:
default:
return nil, fmt.Errorf("%s: unexpected amount of arguments, want 1 or 0", modName)
}
return c, nil
}
func (c *Check) Name() string {
return modName
}
func (c *Check) InstanceName() string {
return c.instName
}
func (c *Check) Init(cfg *config.Map) error {
cfg.String("endpoint", false, false, c.milterUrl, &c.milterUrl)
cfg.Bool("fail_open", false, false, &c.failOpen)
if _, err := cfg.Process(); err != nil {
return err
}
if c.milterUrl == "" {
return fmt.Errorf("%s: milter endpoint is not set", modName)
}
endp, err := config.ParseEndpoint(c.milterUrl)
if err != nil {
return fmt.Errorf("%s: %v", modName, err)
}
switch endp.Scheme {
case "tcp", "unix":
default:
return fmt.Errorf("%s: scheme unsupported: %v", modName, endp.Scheme)
}
if endp.Path != "" {
return fmt.Errorf("%s: stray path in endpoint: %v", modName, endp)
}
c.cl = milter.NewClient(endp.Scheme, endp.Host)
return nil
}
type state struct {
c *Check
session *milter.ClientSession
msgMeta *module.MsgMetadata
skipChecks bool
discarded bool
log log.Logger
}
func (c *Check) CheckStateForMsg(msgMeta *module.MsgMetadata) (module.CheckState, error) {
const supportedActions = milter.OptAddHeader | milter.OptQuarantine
protocolOpts := milter.OptProtocol(0)
if msgMeta.Conn == nil {
protocolOpts = milter.OptNoConnect | milter.OptNoHelo
}
session, err := c.cl.Session(supportedActions, protocolOpts)
if err != nil {
return nil, err
}
return &state{
c: c,
session: session,
msgMeta: msgMeta,
log: target.DeliveryLogger(c.log, msgMeta),
}, nil
}
func (s *state) handleAction(act *milter.Action) module.CheckResult {
switch act.Code {
case milter.ActAccept:
s.skipChecks = true
return module.CheckResult{}
case milter.ActContinue:
return module.CheckResult{}
case milter.ActReplyCode:
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: act.SMTPCode,
EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
Message: "Message rejected due to local policy",
Reason: "reply code action",
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
case milter.ActDiscard:
s.log.Msg("silent discard is not supported, rejecting message")
fallthrough
case milter.ActReject:
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
Message: "Message rejected due to local policy",
Reason: "reject action",
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
default:
s.log.Msg("unknown action code ignored", "code", act.Code, "milter", s.c.milterUrl)
return module.CheckResult{}
}
}
// apply applies the modification actions returned by milter to the check results object.
func (s *state) apply(modifyActs []milter.ModifyAction, res module.CheckResult) module.CheckResult {
out := res
for _, act := range modifyActs {
switch act.Code {
case milter.ActAddRcpt, milter.ActDelRcpt:
s.log.Msg("envelope changes are not supported", "rcpt", act.Rcpt, "code", act.Code, "milter", s.c.milterUrl)
case milter.ActChangeFrom:
s.log.Msg("envelope changes are not supported", "from", act.From, "code", act.Code, "milter", s.c.milterUrl)
case milter.ActChangeHeader:
s.log.Msg("header field changes are not supported", "field", act.HdrName, "milter", s.c.milterUrl)
case milter.ActInsertHeader:
if act.HdrIndex != 1 {
s.log.Msg("header inserting not on top is not supported, prepending instead", "field", act.HdrName, "milter", s.c.milterUrl)
}
fallthrough
case milter.ActAddHeader:
// Header field might be arbitarly folded by the caller and we want
// to preserve that exact format in case it is important (DKIM
// signature is added by milter).
field := make([]byte, 0, len(act.HdrName)+2+len(act.HdrValue)+2)
field = append(field, act.HdrName...)
field = append(field, ':', ' ')
field = append(field, act.HdrValue...)
field = append(field, '\r', '\n')
out.Header.AddRaw(field)
case milter.ActQuarantine:
out.Quarantine = true
out.Reason = exterrors.WithFields(errors.New("milter quarantine action"), map[string]interface{}{
"check": "milter",
"milter": s.c.milterUrl,
"reason": act.Reason,
})
}
}
return out
}
func (s *state) CheckConnection(ctx context.Context) module.CheckResult {
if s.msgMeta.Conn == nil {
return module.CheckResult{}
}
if !s.session.ProtocolOption(milter.OptNoConnect) {
if err := s.session.Macros(milter.CodeConn,
"daemon_name", "maddy",
"if_name", "unknown",
"if_addr", "0.0.0.0",
// TODO: $j
// TODO: $_
); err != nil {
return s.ioError(err)
}
var (
protoFamily milter.ProtoFamily
port uint16
addr string
)
switch rAddr := s.msgMeta.Conn.RemoteAddr.(type) {
case *net.TCPAddr:
port = uint16(rAddr.Port)
if v4 := rAddr.IP.To4(); v4 != nil {
// Make sure to not accidentally send IPv6-mapped IPv4 address.
protoFamily = milter.FamilyInet
addr = v4.String()
} else {
protoFamily = milter.FamilyInet6
addr = rAddr.IP.String()
}
case *net.UnixAddr:
protoFamily = milter.FamilyUnix
addr = rAddr.Name
default:
protoFamily = milter.FamilyUnknown
}
act, err := s.session.Conn(s.msgMeta.Conn.Hostname, protoFamily, port, addr)
if err != nil {
return s.ioError(err)
}
if act.Code != milter.ActContinue {
return s.handleAction(act)
}
}
if !s.session.ProtocolOption(milter.OptNoHelo) {
if s.msgMeta.Conn.TLS.HandshakeComplete {
fields := make([]string, 0, 4*2)
tlsState := s.msgMeta.Conn.TLS
switch tlsState.Version {
case tls.VersionTLS10:
fields = append(fields, "tls_version", "TLSv1")
case tls.VersionTLS11:
fields = append(fields, "tls_version", "TLSv1.1")
case tls.VersionTLS12:
fields = append(fields, "tls_version", "TLSv1.2")
case tls.VersionTLS13:
fields = append(fields, "tls_version", "TLSv1.3")
}
fields = append(fields, "cipher", tls.CipherSuiteName(tlsState.CipherSuite))
if len(tlsState.PeerCertificates) != 0 {
fields = append(fields, "cert_subject",
tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Subject.String())
fields = append(fields, "cert_issuer",
tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Issuer.String())
}
if err := s.session.Macros(milter.CodeHelo, fields...); err != nil {
return s.ioError(err)
}
}
act, err := s.session.Helo(s.msgMeta.Conn.Hostname)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
return module.CheckResult{}
}
func (s *state) ioError(err error) module.CheckResult {
if s.c.failOpen {
s.skipChecks = true // silently permit processing to continue
s.c.log.Error("I/O error", err)
return module.CheckResult{}
}
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 451,
EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
Message: "I/O error during policy check",
Err: err,
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
}
func (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {
if s.skipChecks || s.session.ProtocolOption(milter.OptNoMailFrom) {
return module.CheckResult{}
}
fields := make([]string, 0, 2)
fields = append(fields, "i", s.msgMeta.ID)
// TODO: fields = append(fields, "auth_type", s.msgMeta.???)
if s.msgMeta.Conn.AuthUser != "" {
fields = append(fields, "auth_authen", s.msgMeta.Conn.AuthUser)
}
s.session.Macros(milter.CodeMail, fields...)
esmtpArgs := make([]string, 0, 2)
if s.msgMeta.SMTPOpts.UTF8 {
esmtpArgs = append(esmtpArgs, "SMTPUTF8")
}
act, err := s.session.Mail(mailFrom, esmtpArgs)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
func (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {
if s.skipChecks {
return module.CheckResult{}
}
act, err := s.session.Rcpt(rcptTo, nil)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
func (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {
if s.skipChecks {
return module.CheckResult{}
}
act, err := s.session.Header(header)
if err != nil {
return s.ioError(err)
}
if act.Code != milter.ActContinue {
return s.handleAction(act)
}
var modifyAct []milter.ModifyAction
if !s.session.ProtocolOption(milter.OptNoBody) {
// body.Open can be expensive for on-disk buffering.
r, err := body.Open()
if err != nil {
// Not ioError(err) because fail_open directive is applied only for external I/O.
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 451,
EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
Message: "Internal error during policy check",
Err: err,
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
}
modifyAct, act, err = s.session.Body(r)
if err != nil {
return s.ioError(err)
}
} else {
modifyAct, act, err = s.session.End()
if err != nil {
return s.ioError(err)
}
}
result := s.handleAction(act)
return s.apply(modifyAct, result)
}
func (s *state) Close() error {
return s.session.Close()
}
func init() {
module.Register(modName, New)
}

View file

@ -28,6 +28,7 @@ import (
_ "github.com/foxcpp/maddy/internal/check/dkim"
_ "github.com/foxcpp/maddy/internal/check/dns"
_ "github.com/foxcpp/maddy/internal/check/dnsbl"
_ "github.com/foxcpp/maddy/internal/check/milter"
_ "github.com/foxcpp/maddy/internal/check/requiretls"
_ "github.com/foxcpp/maddy/internal/check/spf"
_ "github.com/foxcpp/maddy/internal/endpoint/imap"