mirror of
https://github.com/mjl-/mox.git
synced 2025-07-14 17:34:37 +03:00
add basic rate limiters
limiting is done based on remote ip's, with 3 ip mask variants to limit networks of machines. often with two windows, enabling short bursts of activity, but not sustained high activity. currently only for imap and smtp, not yet http. limits are currently based on: - number of open connections - connection rate - limits after authentication failures. too many failures, and new connections will be dropped. - rate of delivery in total number of messages - rate of delivery in total size of messages the limits on connections and authentication failures are in-memory. the limits on delivery of messages are based on stored messages. the limits themselves are not yet configurable, let's use this first. in the future, we may also want to have stricter limits for senders without any reputation.
This commit is contained in:
@ -48,6 +48,7 @@ import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -70,6 +71,7 @@ import (
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/moxio"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
"github.com/mjl-/mox/ratelimit"
|
||||
"github.com/mjl-/mox/scram"
|
||||
"github.com/mjl-/mox/store"
|
||||
)
|
||||
@ -101,6 +103,33 @@ var (
|
||||
)
|
||||
)
|
||||
|
||||
var limiterConnectionrate, limiterConnections *ratelimit.Limiter
|
||||
|
||||
func init() {
|
||||
// Also called by tests, so they don't trigger the rate limiter.
|
||||
limitersInit()
|
||||
}
|
||||
|
||||
func limitersInit() {
|
||||
mox.LimitersInit()
|
||||
limiterConnectionrate = &ratelimit.Limiter{
|
||||
WindowLimits: []ratelimit.WindowLimit{
|
||||
{
|
||||
Window: time.Minute,
|
||||
Limits: [...]int64{300, 900, 2700},
|
||||
},
|
||||
},
|
||||
}
|
||||
limiterConnections = &ratelimit.Limiter{
|
||||
WindowLimits: []ratelimit.WindowLimit{
|
||||
{
|
||||
Window: time.Duration(math.MaxInt64), // All of time.
|
||||
Limits: [...]int64{30, 90, 270},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
|
||||
// ENABLE: ../rfc/5161
|
||||
// LITERAL+: ../rfc/7888
|
||||
@ -136,6 +165,7 @@ type conn struct {
|
||||
tw *moxio.TraceWriter
|
||||
lastlog time.Time // For printing time since previous log line.
|
||||
tlsConfig *tls.Config // TLS config to use for handshake.
|
||||
remoteIP net.IP
|
||||
noRequireSTARTTLS bool
|
||||
cmd string // Currently executing, for deciding to applyChanges and logging.
|
||||
cmdMetric string // Currently executing, for metrics.
|
||||
@ -507,12 +537,21 @@ func (c *conn) xreadliteral(size int64, sync bool) string {
|
||||
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
|
||||
|
||||
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS bool) {
|
||||
var remoteIP net.IP
|
||||
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
|
||||
remoteIP = a.IP
|
||||
} else {
|
||||
// For net.Pipe, during tests.
|
||||
remoteIP = net.ParseIP("127.0.0.10")
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
cid: cid,
|
||||
conn: nc,
|
||||
tls: xtls,
|
||||
lastlog: time.Now(),
|
||||
tlsConfig: tlsConfig,
|
||||
remoteIP: remoteIP,
|
||||
noRequireSTARTTLS: noRequireSTARTTLS,
|
||||
enabled: map[capability]bool{},
|
||||
cmd: "(greeting)",
|
||||
@ -583,6 +622,25 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
|
||||
default:
|
||||
}
|
||||
|
||||
if !limiterConnectionrate.Add(c.remoteIP, time.Now(), 1) {
|
||||
c.writelinef("* BYE connection rate from your ip or network too high, slow down please")
|
||||
return
|
||||
}
|
||||
|
||||
// If remote IP/network resulted in too many authentication failures, refuse to serve.
|
||||
if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
|
||||
c.log.Debug("refusing connection due to many auth failures", mlog.Field("remoteip", c.remoteIP))
|
||||
c.writelinef("* BYE too many auth failures")
|
||||
return
|
||||
}
|
||||
|
||||
if !limiterConnections.Add(c.remoteIP, time.Now(), 1) {
|
||||
c.log.Debug("refusing connection due to many open connections", mlog.Field("remoteip", c.remoteIP))
|
||||
c.writelinef("* BYE too many open connections from your ip or network")
|
||||
return
|
||||
}
|
||||
defer limiterConnections.Add(c.remoteIP, time.Now(), -1)
|
||||
|
||||
// We register and unregister the original connection, in case it c.conn is
|
||||
// replaced with a TLS connection later on.
|
||||
mox.Connections.Register(nc, "imap", listenerName)
|
||||
@ -1313,6 +1371,12 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||
authResult := "error"
|
||||
defer func() {
|
||||
metrics.AuthenticationInc("imap", authVariant, authResult)
|
||||
switch authResult {
|
||||
case "ok":
|
||||
mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
|
||||
default:
|
||||
mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Request syntax: ../rfc/9051:6341 ../rfc/3501:4561
|
||||
|
@ -300,6 +300,8 @@ func startNoSwitchboard(t *testing.T) *testconn {
|
||||
}
|
||||
|
||||
func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS bool) *testconn {
|
||||
limitersInit() // Reset rate limiters.
|
||||
|
||||
if first {
|
||||
os.RemoveAll("../testdata/imap/data")
|
||||
}
|
||||
|
Reference in New Issue
Block a user