diff --git a/apiary.toml b/apiary.toml index c45375a..b51c501 100644 --- a/apiary.toml +++ b/apiary.toml @@ -63,7 +63,7 @@ RedirectHTTP = true [Ports] # Enable the port listener. # Default: false -Enable = true +Enable = false # Which address to listen on. # Default: "" (listen to all addresses) @@ -76,3 +76,16 @@ TCPPorts = ["25"] # Which UDP ports to listen to. # Default: [] UDPPorts = ["25"] + +[SMTP] +# Enable the port listener. +# Default: false +Enable = true + +# Which address and port to listen on. +# Default: ":25" +Addr = ":25" + +# Enable collecting prometheus metrics +# Default: false +EnableMetrics = true diff --git a/cmd/apiary.go b/cmd/apiary.go index 3a6970f..3504f8f 100644 --- a/cmd/apiary.go +++ b/cmd/apiary.go @@ -15,6 +15,7 @@ import ( "github.uio.no/torjus/apiary" "github.uio.no/torjus/apiary/config" "github.uio.no/torjus/apiary/honeypot/ports" + "github.uio.no/torjus/apiary/honeypot/smtp" "github.uio.no/torjus/apiary/honeypot/ssh" "github.uio.no/torjus/apiary/honeypot/ssh/store" "github.uio.no/torjus/apiary/web" @@ -144,6 +145,39 @@ func ActionServe(c *cli.Context) error { }() } + // Setup smtp honeypot if enabled + if cfg.SMTP.Enable { + honeypot, err := smtp.NewSMTPHoneypot() + if err != nil { + loggers.rootLogger.Warnw("Error seting up SMTP honeypot", "error", err) + } + honeypot.Addr = cfg.SMTP.Addr + honeypot.Logger = loggers.smtpLogger + if cfg.SMTP.EnableMetrics { + honeypot.Store = smtp.NewMetricsStore(&smtp.DiscardStore{}) + } else { + honeypot.Store = &smtp.DiscardStore{} + } + + // Start smtp honeypot + go func() { + loggers.rootLogger.Info("Starting SMTP server") + if err := honeypot.ListenAndServe(); err != nil { + loggers.rootLogger.Warnw("SMTP server returned error", "error", err) + } + }() + + // Wait for smtp shutdown + go func() { + <-serversCtx.Done() + loggers.rootLogger.Info("SMTP server shutdown started") + if err := honeypot.Shutdown(); err != nil { + loggers.rootLogger.Errorw("Error shutting down SMTP server", "error", err) + } + loggers.rootLogger.Info("SMTP server shutdown complete") + }() + } + // Handle interrupt go func() { <-interruptChan @@ -250,6 +284,7 @@ type loggerCollection struct { webAccessLogger *zap.SugaredLogger webServerLogger *zap.SugaredLogger portsLogger *zap.SugaredLogger + smtpLogger *zap.SugaredLogger } func setupLoggers(cfg config.Config) *loggerCollection { @@ -290,6 +325,7 @@ func setupLoggers(cfg config.Config) *loggerCollection { webAccessLogger: rootLogger.Named("ACC").Sugar(), webServerLogger: rootLogger.Named("WEB").Sugar(), portsLogger: rootLogger.Named("PRT").Sugar(), + smtpLogger: rootLogger.Named("SMT").Sugar(), } } diff --git a/config/config.go b/config/config.go index 2b4e23a..3bfa240 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { Honeypot HoneypotConfig `toml:"Honeypot"` Frontend FrontendConfig `toml:"Frontend"` Ports PortsConfig `toml:"Ports"` + SMTP SMTPConfig `toml:"SMTP"` } type StoreConfig struct { Type string `toml:"Type"` @@ -54,6 +55,12 @@ type PortsConfig struct { UDPPorts []string `toml:"UDPPorts"` } +type SMTPConfig struct { + Enable bool `toml:"Enable"` + Addr string `toml:"Addr"` + EnableMetrics bool `toml:"EnableMetrics"` +} + func FromReader(r io.Reader) (Config, error) { var c Config @@ -63,6 +70,7 @@ func FromReader(r io.Reader) (Config, error) { c.Frontend.ListenAddr = ":8080" c.Frontend.LogLevel = "INFO" c.Frontend.AccessLogEnable = true + c.SMTP.Addr = ":25" // Read from config decoder := toml.NewDecoder(r) diff --git a/go.mod b/go.mod index 5a88576..ba3156d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect + github.com/emersion/go-smtp v0.15.0 github.com/fujiwara/shapeio v1.0.0 github.com/gliderlabs/ssh v0.3.3 github.com/go-chi/chi/v5 v5.0.4 diff --git a/go.sum b/go.sum index 204c14f..ecfc48d 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= +github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/honeypot/smtp/mail.go b/honeypot/smtp/mail.go new file mode 100644 index 0000000..f3ec986 --- /dev/null +++ b/honeypot/smtp/mail.go @@ -0,0 +1,12 @@ +package smtp + +import "time" + +type MailData struct { + From string + Recipients []string + Data string + + RemoteAddr string + Timestamp time.Time +} diff --git a/honeypot/smtp/metrics.go b/honeypot/smtp/metrics.go new file mode 100644 index 0000000..eee39d9 --- /dev/null +++ b/honeypot/smtp/metrics.go @@ -0,0 +1,30 @@ +package smtp + +import "github.com/prometheus/client_golang/prometheus" + +type MetricsStore struct { + backend SMTPStore + mailTotal *prometheus.CounterVec +} + +func NewMetricsStore(backend SMTPStore) *MetricsStore { + store := &MetricsStore{backend: backend} + + store.mailTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "apiary_smtp_received_total", + Help: "Total count of received emails.", + ConstLabels: prometheus.Labels{"service": "honeypot_smtp"}, + }, + []string{"from"}, + ) + prometheus.MustRegister(store.mailTotal) + + return store +} + +func (s *MetricsStore) AddMailData(data *MailData) error { + s.mailTotal.WithLabelValues(data.From).Inc() + + return s.backend.AddMailData(data) +} diff --git a/honeypot/smtp/server.go b/honeypot/smtp/server.go new file mode 100644 index 0000000..1c5f6cc --- /dev/null +++ b/honeypot/smtp/server.go @@ -0,0 +1,112 @@ +package smtp + +import ( + "bytes" + "io" + "time" + + smtplib "github.com/emersion/go-smtp" + "go.uber.org/zap" +) + +type backend struct { + honeypot *SMTPHoneypot +} + +func (b *backend) Login(state *smtplib.ConnectionState, username, password string) (smtplib.Session, error) { + sess := &session{ + honeypot: b.honeypot, + loginUsername: username, + loginPassword: password, + remoteAddr: state.RemoteAddr.String(), + } + return sess, nil +} +func (b *backend) AnonymousLogin(state *smtplib.ConnectionState) (smtplib.Session, error) { + sess := &session{ + loginAnonymous: true, + remoteAddr: state.RemoteAddr.String(), + } + return sess, nil +} + +type session struct { + honeypot *SMTPHoneypot + + loginUsername string + loginPassword string + loginAnonymous bool + remoteAddr string + + mail MailData +} + +func (sess *session) Reset() { +} +func (sess *session) Logout() error { + return nil +} +func (sess *session) Mail(from string, opts smtplib.MailOptions) error { + sess.mail.From = from + return nil +} +func (sess *session) Rcpt(to string) error { + sess.mail.Recipients = append(sess.mail.Recipients, to) + return nil +} +func (sess *session) Data(r io.Reader) error { + lr := io.LimitReader(r, 1024*1024) + var buf bytes.Buffer + n, err := io.Copy(&buf, lr) + if err != nil { + sess.honeypot.Logger.Debugw("Error reading email data from client.", "error", err, "remote_addr", sess.remoteAddr) + } + + if n == 1024*1024 { + // There is probably more data, read the rest from the client + // We don't really care what happens to this, or if it errors + _, _ = io.Copy(io.Discard, r) + } + + sess.mail.Data = buf.String() + + if err := sess.honeypot.Store.AddMailData(&sess.mail); err != nil { + sess.honeypot.Logger.Warnw("Error storing maildata.", "error", err, "remote_addr", sess.remoteAddr) + return smtplib.ErrDataTooLarge + } + sess.honeypot.Logger.Infow("Mail received", "from", sess.mail.From, "remote_addr", sess.remoteAddr, "size", len(sess.mail.Data)) + + return nil +} + +type SMTPHoneypot struct { + Addr string + Logger *zap.SugaredLogger + Store SMTPStore + server *smtplib.Server +} + +func NewSMTPHoneypot() (*SMTPHoneypot, error) { + honeypot := &SMTPHoneypot{ + Logger: zap.NewNop().Sugar(), + } + + return honeypot, nil +} + +func (sh *SMTPHoneypot) ListenAndServe() error { + server := smtplib.NewServer(&backend{ + honeypot: sh, + }) + server.Addr = sh.Addr + server.Domain = "apiary.t-juice.club" + server.AllowInsecureAuth = true + server.ReadTimeout = 10 * time.Second + server.WriteTimeout = 10 * time.Second + sh.server = server + return server.ListenAndServe() +} + +func (sh *SMTPHoneypot) Shutdown() error { + return sh.server.Close() +} diff --git a/honeypot/smtp/store.go b/honeypot/smtp/store.go new file mode 100644 index 0000000..32bc52d --- /dev/null +++ b/honeypot/smtp/store.go @@ -0,0 +1,12 @@ +package smtp + +type SMTPStore interface { + AddMailData(mail *MailData) error +} + +type DiscardStore struct { +} + +func (s *DiscardStore) AddMailData(mail *MailData) error { + return nil +}