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:
Mechiel Lukkien
2023-02-07 22:56:03 +01:00
parent 1617b7c0d6
commit 2154392bd8
7 changed files with 584 additions and 6 deletions

View File

@ -0,0 +1,72 @@
package ratelimit
import (
"net"
"testing"
"time"
)
func TestLimiter(t *testing.T) {
l := &Limiter{
WindowLimits: []WindowLimit{
{
Window: time.Minute,
Limits: [...]int64{2, 4, 6},
},
},
}
now := time.Now()
check := func(exp bool, ip net.IP, tm time.Time, n int64) {
t.Helper()
ok := l.CanAdd(ip, tm, n)
if ok != exp {
t.Fatalf("canadd, got %v, expected %v", ok, exp)
}
ok = l.Add(ip, tm, n)
if ok != exp {
t.Fatalf("add, got %v, expected %v", ok, exp)
}
}
check(false, net.ParseIP("10.0.0.1"), now, 3) // past limit
check(true, net.ParseIP("10.0.0.1"), now, 1)
check(false, net.ParseIP("10.0.0.1"), now, 2) // now past limit
check(true, net.ParseIP("10.0.0.1"), now, 1)
check(false, net.ParseIP("10.0.0.1"), now, 1) // now past limit
next := now.Add(time.Minute)
check(true, net.ParseIP("10.0.0.1"), next, 2) // next minute, should have reset
check(true, net.ParseIP("10.0.0.2"), next, 2) // other ip
check(false, net.ParseIP("10.0.0.3"), next, 2) // yet another ip, ipmasked2 was consumed
check(true, net.ParseIP("10.0.1.4"), next, 2) // using ipmasked3
check(false, net.ParseIP("10.0.2.4"), next, 2) // ipmasked3 consumed
l.Reset(net.ParseIP("10.0.1.4"), next)
if !l.CanAdd(net.ParseIP("10.0.1.4"), next, 2) {
t.Fatalf("reset did not free up count for ip")
}
check(true, net.ParseIP("10.0.2.4"), next, 2) // ipmasked3 available again
l = &Limiter{
WindowLimits: []WindowLimit{
{
Window: time.Minute,
Limits: [...]int64{1, 2, 3},
},
{
Window: time.Hour,
Limits: [...]int64{2, 3, 4},
},
},
}
min1 := time.UnixMilli((time.Now().UnixNano() / int64(time.Hour)) * int64(time.Hour) / int64(time.Millisecond))
min2 := min1.Add(time.Minute)
min3 := min1.Add(2 * time.Minute)
check(true, net.ParseIP("10.0.0.1"), min1, 1)
check(true, net.ParseIP("10.0.0.1"), min2, 1)
check(false, net.ParseIP("10.0.0.1"), min3, 1)
check(true, net.ParseIP("10.0.0.255"), min3, 1) // ipmasked2 still ok
check(false, net.ParseIP("10.0.0.255"), min3, 1) // ipmasked2 also full
check(true, net.ParseIP("10.0.1.1"), min3, 1) // ipmasked3 still ok
check(false, net.ParseIP("10.0.1.255"), min3, 1) // ipmasked3 also full
}