Documentation
¶
Overview ¶
Package ratelimit defines the port authit's service packages use to throttle expensive or guessable operations, plus an in-memory implementation of it.
What this is not ¶
This is not a replacement for rate limiting at your HTTP layer, and it cannot be. A service method sees only the arguments it is given: some have an IP address, some have only an email, and none of them see headers, routes, or the shape of the request. Per-route, per-IP limiting belongs in middleware, where that information exists.
What this port covers is the subset a host *cannot* implement from outside, because the thing being guessed is looked up inside authit: the device flow's user codes, and the login paths where refusing early avoids running a deliberately expensive password KDF for an attacker.
Use both.
Index ¶
Constants ¶
const DefaultMaxKeys = 100_000
DefaultMaxKeys bounds how many distinct keys a Memory tracks.
Variables ¶
var ErrRateLimited = errors.New("authit/ratelimit: rate limit exceeded")
ErrRateLimited is the sentinel every rejection wraps, so a caller can classify one with errors.Is without knowing the implementation.
Functions ¶
func AllowEach ¶
AllowEach consults l for each key in turn and returns the first refusal, or nil if every key was allowed.
Two rules, both of which were learned the hard way and neither of which is obvious from Limiter:
A key ending in ":" is skipped. Callers build keys by joining a namespace to a value -- "login:ip:" + ipAddress -- and a value that is absent leaves the separator dangling. Consulting that key would put every caller with no IP address into one shared bucket, so the first few would spend the budget of all the rest. Skipping is the safe reading: the dimension is unknown for this caller, so it cannot be metered.
Every key is consulted even after one refuses. Returning early would leave the later dimensions uncharged, and an attacker who can make the first key refuse -- by exhausting a bucket they share with nobody -- would then ride the others for free.
A limiter's own failure (a Redis timeout, not a refusal) is returned immediately and unwrapped, so a caller can tell "you are going too fast" from "the control is broken" and fail closed on the second.
Types ¶
type Error ¶
Error is a refusal, carrying a hint for how long to wait. Hosts can surface RetryAfter in a 429 response.
type Limiter ¶
Limiter decides whether an operation identified by key may proceed now, consuming budget when it says yes. Returning a non-nil error refuses it.
Implementations should return an *Error (or something wrapping ErrRateLimited) for a refusal, and reserve other error values for their own failures — a Redis timeout, say. The distinction matters at the call site: the service packages treat a refusal as a denial to report to the caller, and anything else as a fault to propagate.
Allow must be safe for concurrent use.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory is an in-process token-bucket Limiter, suitable for a single instance and for tests.
It is deliberately not suitable for a horizontally scaled deployment: each process keeps its own buckets, so N instances behind a load balancer permit N times the configured rate. Implement Limiter over Redis (or whatever you already run) when that matters — it is one method.
func (*Memory) Allow ¶
Allow consumes one unit of key's budget, refusing with an *Error when there is none left.
If the table is at MaxKeys and no bucket can be evicted -- meaning every tracked key is currently being limited -- a previously unseen key is refused rather than admitted untracked. That is the fail-closed choice: admitting it would be a bypass (flood the table with junk keys, then proceed unmetered), and growing past the cap would make the limiter a memory-exhaustion vector, which is exactly what the cap is for. The cost is that a genuine new caller is turned away while the flood lasts. At the default MaxKeys that state means 100,000 distinct keys are simultaneously over their limit, so the refusal is not the biggest problem you have.
type MemoryConfig ¶
type MemoryConfig struct {
// Burst is how many operations may happen back to back before the
// limit bites. Must be at least 1; defaults to 5.
Burst int
// Interval is how long it takes to earn one unit of budget back.
// Defaults to 1 minute, i.e. a sustained rate of one per minute with
// bursts of Burst.
Interval time.Duration
// MaxKeys is a hard cap on tracked keys, so a caller varying the key on
// every request cannot grow this map without bound. Defaults to
// DefaultMaxKeys.
//
// Reaching it means every tracked key is currently under restriction,
// which is itself a sign of a broad attack. See Allow for what happens
// then; the short version is that it fails closed.
MaxKeys int
}
MemoryConfig configures a Memory limiter.