limiter

package module
v3.11.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 24, 2023 License: MIT Imports: 7 Imported by: 216

README

Limiter

Documentation License Build Status Go Report Card

Dead simple rate limit middleware for Go.

  • Simple API
  • "Store" approach for backend
  • Redis support (but not tied too)
  • Middlewares: HTTP, FastHTTP and Gin

Installation

Using Go Modules

$ go get github.com/ulule/limiter/v3@v3.11.2

Usage

In five steps:

  • Create a limiter.Rate instance (the number of requests per period)
  • Create a limiter.Store instance (see Redis or In-Memory)
  • Create a limiter.Limiter instance that takes store and rate instances as arguments
  • Create a middleware instance using the middleware of your choice
  • Give the limiter instance to your middleware initializer

Example:

// Create a rate with the given limit (number of requests) for the given
// period (a time.Duration of your choice).
import "github.com/ulule/limiter/v3"

rate := limiter.Rate{
    Period: 1 * time.Hour,
    Limit:  1000,
}

// You can also use the simplified format "<limit>-<period>"", with the given
// periods:
//
// * "S": second
// * "M": minute
// * "H": hour
// * "D": day
//
// Examples:
//
// * 5 reqs/second: "5-S"
// * 10 reqs/minute: "10-M"
// * 1000 reqs/hour: "1000-H"
// * 2000 reqs/day: "2000-D"
//
rate, err := limiter.NewRateFromFormatted("1000-H")
if err != nil {
    panic(err)
}

// Then, create a store. Here, we use the bundled Redis store. Any store
// compliant to limiter.Store interface will do the job. The defaults are
// "limiter" as Redis key prefix and a maximum of 3 retries for the key under
// race condition.
import "github.com/ulule/limiter/v3/drivers/store/redis"

store, err := redis.NewStore(client)
if err != nil {
    panic(err)
}

// Alternatively, you can pass options to the store with the "WithOptions"
// function. For example, for Redis store:
import "github.com/ulule/limiter/v3/drivers/store/redis"

store, err := redis.NewStoreWithOptions(pool, limiter.StoreOptions{
    Prefix:   "your_own_prefix",
})
if err != nil {
    panic(err)
}

// Or use a in-memory store with a goroutine which clears expired keys.
import "github.com/ulule/limiter/v3/drivers/store/memory"

store := memory.NewStore()

// Then, create the limiter instance which takes the store and the rate as arguments.
// Now, you can give this instance to any supported middleware.
instance := limiter.New(store, rate)

// Alternatively, you can pass options to the limiter instance with several options.
instance := limiter.New(store, rate, limiter.WithClientIPHeader("True-Client-IP"), limiter.WithIPv6Mask(mask))

// Finally, give the limiter instance to your middleware initializer.
import "github.com/ulule/limiter/v3/drivers/middleware/stdlib"

middleware := stdlib.NewMiddleware(instance)

See middleware examples:

How it works

The ip address of the request is used as a key in the store.

If the key does not exist in the store we set a default value with an expiration period.

You will find two stores:

  • Redis: rely on TTL and incrementing the rate limit on each request.
  • In-Memory: rely on a fork of go-cache with a goroutine to clear expired keys using a default interval.

When the limit is reached, a 429 HTTP status code is sent.

Limiter behind a reverse proxy

Introduction

If your limiter is behind a reverse proxy, it could be difficult to obtain the "real" client IP.

Some reverse proxies, like AWS ALB, lets all header values through that it doesn't set itself. Like for example, True-Client-IP and X-Real-IP. Similarly, X-Forwarded-For is a list of comma-separated IPs that gets appended to by each traversed proxy. The idea is that the first IP (added by the first proxy) is the true client IP. Each subsequent IP is another proxy along the path.

An attacker can spoof either of those headers, which could be reported as a client IP.

By default, limiter doesn't trust any of those headers: you have to explicitly enable them in order to use them. If you enable them, you must always be aware that any header added by any (reverse) proxy not controlled by you are completely unreliable.

X-Forwarded-For

For example, if you make this request to your load balancer:

curl -X POST https://example.com/login -H "X-Forwarded-For: 1.2.3.4, 11.22.33.44"

And your server behind the load balancer obtain this:

X-Forwarded-For: 1.2.3.4, 11.22.33.44, <actual client IP>

That's mean you can't use X-Forwarded-For header, because it's unreliable and untrustworthy. So keep TrustForwardHeader disabled in your limiter option.

However, if you have configured your reverse proxy to always remove/overwrite X-Forwarded-For and/or X-Real-IP headers so that if you execute this (same) request:

curl -X POST https://example.com/login -H "X-Forwarded-For: 1.2.3.4, 11.22.33.44"

And your server behind the load balancer obtain this:

X-Forwarded-For: <actual client IP>

Then, you can enable TrustForwardHeader in your limiter option.

Custom header

Many CDN and Cloud providers add a custom header to define the client IP. Like for example, this non exhaustive list:

  • Fastly-Client-IP from Fastly
  • CF-Connecting-IP from Cloudflare
  • X-Azure-ClientIP from Azure

You can use these headers using ClientIPHeader in your limiter option.

None of the above

If none of the above solution are working, please use a custom KeyGetter in your middleware.

You can use this excellent article to help you define the best strategy depending on your network topology and your security need: https://adam-p.ca/blog/2022/03/x-forwarded-for/

If you have any idea/suggestions on how we could simplify this steps, don't hesitate to raise an issue. We would like some feedback on how we could implement this steps in the Limiter API.

Thank you.

Why Yet Another Package

You could ask us: why yet another rate limit package?

Because existing packages did not suit our needs.

We tried a lot of alternatives:

  1. Throttled. This package uses the generic cell-rate algorithm. To cite the documentation: "The algorithm has been slightly modified from its usual form to support limiting with an additional quantity parameter, such as for limiting the number of bytes uploaded". It is brillant in term of algorithm but documentation is quite unclear at the moment, we don't need burst feature for now, impossible to get a correct After-Retry (when limit exceeds, we can still make a few requests, because of the max burst) and it only supports http.Handler middleware (we use Gin). Currently, we only need to return 429 and X-Ratelimit-* headers for n reqs/duration.

  2. Speedbump. Good package but maybe too lightweight. No Reset support, only one middleware for Gin framework and too Redis-coupled. We rather prefer to use a "store" approach.

  3. Tollbooth. Good one too but does both too much and too little. It limits by remote IP, path, methods, custom headers and basic auth usernames... but does not provide any Redis support (only in-memory) and a ready-to-go middleware that sets X-Ratelimit-* headers. tollbooth.LimitByRequest(limiter, r) only returns an HTTP code.

  4. ratelimit. Probably the closer to our needs but, once again, too lightweight, no middleware available and not active (last commit was in August 2014). Some parts of code (Redis) comes from this project. It should deserve much more love.

There are other many packages on GitHub but most are either too lightweight, too old (only support old Go versions) or unmaintained. So that's why we decided to create yet another one.

Contributing

Don't hesitate ;)

Documentation

Index

Constants

View Source
const (
	// DefaultPrefix is the default prefix to use for the key in the store.
	DefaultPrefix = "limiter"

	// DefaultMaxRetry is the default maximum number of key retries under
	// race condition (mainly used with database-based stores).
	DefaultMaxRetry = 3

	// DefaultCleanUpInterval is the default time duration for cleanup.
	DefaultCleanUpInterval = 30 * time.Second
)

Variables

View Source
var (
	// DefaultIPv4Mask defines the default IPv4 mask used to obtain user IP.
	DefaultIPv4Mask = net.CIDRMask(32, 32)
	// DefaultIPv6Mask defines the default IPv6 mask used to obtain user IP.
	DefaultIPv6Mask = net.CIDRMask(128, 128)
)

Functions

func GetIP added in v3.1.0

func GetIP(r *http.Request, options ...Options) net.IP

GetIP returns IP address from request. If options is defined and either TrustForwardHeader is true or ClientIPHeader is defined, it will lookup IP in HTTP headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

func GetIPWithMask added in v3.1.0

func GetIPWithMask(r *http.Request, options ...Options) net.IP

GetIPWithMask returns IP address from request by applying a mask. If options is defined and either TrustForwardHeader is true or ClientIPHeader is defined, it will lookup IP in HTTP headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

Types

type Context

type Context struct {
	Limit     int64
	Remaining int64
	Reset     int64
	Reached   bool
}

Context is the limit context.

type Limiter

type Limiter struct {
	Store   Store
	Rate    Rate
	Options Options
}

Limiter is the limiter instance.

func New

func New(store Store, rate Rate, options ...Option) *Limiter

New returns an instance of Limiter.

func (*Limiter) Get

func (limiter *Limiter) Get(ctx context.Context, key string) (Context, error)

Get returns the limit for given identifier.

func (*Limiter) GetIP

func (limiter *Limiter) GetIP(r *http.Request) net.IP

GetIP returns IP address from request. If options is defined and either TrustForwardHeader is true or ClientIPHeader is defined, it will lookup IP in HTTP headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

func (*Limiter) GetIPKey

func (limiter *Limiter) GetIPKey(r *http.Request) string

GetIPKey extracts IP from request and returns hashed IP to use as store key. If options is defined and either TrustForwardHeader is true or ClientIPHeader is defined, it will lookup IP in HTTP headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

func (*Limiter) GetIPWithMask

func (limiter *Limiter) GetIPWithMask(r *http.Request) net.IP

GetIPWithMask returns IP address from request by applying a mask. If options is defined and either TrustForwardHeader is true or ClientIPHeader is defined, it will lookup IP in HTTP headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

func (*Limiter) Increment added in v3.9.0

func (limiter *Limiter) Increment(ctx context.Context, key string, count int64) (Context, error)

Increment increments the limit by given count & gives back the new limit for given identifier

func (*Limiter) Peek

func (limiter *Limiter) Peek(ctx context.Context, key string) (Context, error)

Peek returns the limit for given identifier, without modification on current values.

func (*Limiter) Reset added in v3.3.0

func (limiter *Limiter) Reset(ctx context.Context, key string) (Context, error)

Reset sets the limit for given identifier to zero.

type Option

type Option func(*Options)

Option is a functional option.

func WithClientIPHeader added in v3.10.0

func WithClientIPHeader(header string) Option

WithClientIPHeader will configure the limiter to use a custom header to obtain user IP. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

func WithIPv4Mask

func WithIPv4Mask(mask net.IPMask) Option

WithIPv4Mask will configure the limiter to use given mask for IPv4 address.

func WithIPv6Mask

func WithIPv6Mask(mask net.IPMask) Option

WithIPv6Mask will configure the limiter to use given mask for IPv6 address.

func WithTrustForwardHeader

func WithTrustForwardHeader(enable bool) Option

WithTrustForwardHeader will configure the limiter to trust X-Real-IP and X-Forwarded-For headers. Please be advised that using this option could be insecure (ie: spoofed) if your reverse proxy is not configured properly to forward a trustworthy client IP. Please read the section "Limiter behind a reverse proxy" in the README for further information.

type Options

type Options struct {
	// IPv4Mask defines the mask used to obtain a IPv4 address.
	IPv4Mask net.IPMask
	// IPv6Mask defines the mask used to obtain a IPv6 address.
	IPv6Mask net.IPMask
	// TrustForwardHeader enable parsing of X-Real-IP and X-Forwarded-For headers to obtain user IP.
	// Please be advised that using this option could be insecure (ie: spoofed) if your reverse
	// proxy is not configured properly to forward a trustworthy client IP.
	// Please read the section "Limiter behind a reverse proxy" in the README for further information.
	TrustForwardHeader bool
	// ClientIPHeader defines a custom header (likely defined by your CDN or Cloud provider) to obtain user IP.
	// If configured, this option will override "TrustForwardHeader" option.
	// Please be advised that using this option could be insecure (ie: spoofed) if your reverse
	// proxy is not configured properly to forward a trustworthy client IP.
	// Please read the section "Limiter behind a reverse proxy" in the README for further information.
	ClientIPHeader string
}

Options are limiter options.

type Rate

type Rate struct {
	Formatted string
	Period    time.Duration
	Limit     int64
}

Rate is the rate.

func NewRateFromFormatted

func NewRateFromFormatted(formatted string) (Rate, error)

NewRateFromFormatted returns the rate from the formatted version.

type Store

type Store interface {
	// Get returns the limit for given identifier.
	Get(ctx context.Context, key string, rate Rate) (Context, error)
	// Peek returns the limit for given identifier, without modification on current values.
	Peek(ctx context.Context, key string, rate Rate) (Context, error)
	// Reset resets the limit to zero for given identifier.
	Reset(ctx context.Context, key string, rate Rate) (Context, error)
	// Increment increments the limit by given count & gives back the new limit for given identifier
	Increment(ctx context.Context, key string, count int64, rate Rate) (Context, error)
}

Store is the common interface for limiter stores.

type StoreOptions

type StoreOptions struct {
	// Prefix is the prefix to use for the key.
	Prefix string

	// MaxRetry is the maximum number of retry under race conditions on redis store.
	// Deprecated: this option is no longer required since all operations are atomic now.
	MaxRetry int

	// CleanUpInterval is the interval for cleanup (run garbage collection) on stale entries on memory store.
	// Setting this to a low value will optimize memory consumption, but will likely
	// reduce performance and increase lock contention.
	// Setting this to a high value will maximum throughput, but will increase the memory footprint.
	CleanUpInterval time.Duration
}

StoreOptions are options for store.

Directories

Path Synopsis
drivers
internal
fasttime
Package fasttime gets wallclock time, but super fast.
Package fasttime gets wallclock time, but super fast.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL