Agregando un `rate limit` 🛑 a nuestras API con Redis y Go
9 noviembre, 2021
6 minutos de lectura
¿Ves algún error o quieres modificar algo? Haz una Pull Request
Otro de los casos de uso más frecuentes que solemos afrontar en nuestra actividad de desarrollo es: ¿Cómo implementar un rate limit
a nuestros servicios? (api servers). En este artículo continuaremos haciendo uso de nuestra base de datos Redis y del lenguaje de programación golang e implementaremos un rate limit 🔥.
📖 Rate limiting is a strategy for limiting network traffic. Rate limiting can help stop certain kinds of malicious bot activity. It can also reduce strain on web servers. However, rate limiting is not a complete solution for managing bot activity. cloudflare blog
El uso de rate limit
puede ayudarnos a mitigar posibles ataques a nuestra API por ejemplo:
- Brute force attacks
- DoS and DDoS attacks
- Web scraping
En este tutorial se asume que tenemos instalada la base de datos redis y el lenguaje de programación golang downloads site .
Configuración inicial de nuestro proyecto
En este caso usaremos redis, pero a través de podman . El cual es un motor de contenedores como docker. Para más detalles consultar la documentación oficial .
# Download image
podman pull quay.io/bitnami/redis
# Run the container for our redis instance
podman run --name redis --rm -e ALLOW_EMPTY_PASSWORD=yes -p 6379:6379 quay.io/bitnami/redis:latest
En el artículo Mejorando los tiempos de respuesta de nuestras API con Redis y Go inicializamos un proyecto en github, aquí te dejo las instrucciones por si deseas descargar el código del ejemplo:
git clone https://github.com/kenriortega/app-redis-for-blog.git
Hemos estructurado el código de la siguiente forma.
# Folder structure:
├── examples
│ ├── cacheapp
│ │ └── main.go
│ └── ratelimit
│ ├── http.rest
│ └── main.go
├── go.mod
├── go.sum
├── makefile
├── pkg
│ └── db
│ └── redis.go
└── README.md
Pero bueno no pasa nada si decides tan solo extraer las funcionalidades de nuestro rate limit
.
Aquí te dejo las instrucciones para inicializar un nuevo proyecto.
mkdir app
cd app
# init project using go mod
go mod init app
# install redis package
go get github.com/go-redis/redis/v8
go get -u github.com/gorilla/mux
touch main.go
Pues comencemos con el desarrollo de la API
En el día de hoy construiremos 🏗️ una API, para esto haremos uso del paquete gorrilla/mux
, con fines de hacer el tutorial lo más simple posible solo nos concentraremos en la implementación de nuestro caso de uso rate limit
🛑 de 10 calls/20s
.
Nos dirigimos a nuestro fichero main.go
y comenzamos en la construcción de nuestro servidor web.
...
func main() {
var port int
flag.IntVar(&port, "port", 8081, "Port to serve")
flag.Parse()
// gorrilla/mux router
r := mux.NewRouter()
// handleFunc for endpoint '/'
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Printf(w,"Hello")
})
srv := &http.Server{
Handler: r,
Addr: fmt.Sprintf(":%d", port),
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Println("Server running on http://localhost:",port)
log.Fatal(srv.ListenAndServe())
}
...
Procedemos a ejecutar nuestro servidor a través de la línea de comandos
> go run main.go --port 4000
Luego para chequear que esté correctamente ejecutándose el servicio se pueden usar diversos cliente REST curl
, postman
etc.
Ahora vamos a ir agregando nuestras variables para nuestro rate limit
...
const (
reqLimit = 10
durationLimit = 20
keyIPRequestsPrefix = "requests"
)
...
Como se 👀 definimos nuestros límites para las solicitudes por clientes y el límite de duración de nuestra key
en redis, compuesta por requests:<IP>
.
Para este caso de uso usaremos el IP como identificador principal para saber quién o qué está enviando solicitudes hacia el servidor.
Para ello te comparto este método extractIpAddr
el cual recibe por parámetros el objeto req *http.Request
.
func extractIpAddr(req *http.Request) string {
ipAddress := req.RemoteAddr
fwdAddress := req.Header.Get("X-Forwarded-For") // capitalisation doesn't matter
if fwdAddress != "" {
// Got X-Forwarded-For
ipAddress = fwdAddress // If it's a single IP, then awesome!
// If we got an array... grab the first IP
ips := strings.Split(fwdAddress, ", ")
if len(ips) > 1 {
ipAddress = ips[0]
}
}
remoteAddrToParse := ""
if strings.Contains(ipAddress, "[::1]") {
remoteAddrToParse = strings.Replace(ipAddress, "[::1]", "localhost", -1)
ipAddress = strings.Split(remoteAddrToParse, ":")[0]
} else {
ipAddress = strings.Split(ipAddress, ":")[0]
}
return ipAddress
}
Luego de esta función crearemos un IPController
el cual es una estructura que recibe un cliente de redis *redis.Client
e implementa los siguientes métodos dentro de la misma.
// IPController ...
type IPController struct {
rdb *redis.Client
}
// NewIPController ...
func NewIPController(rdb *redis.Client) *IPController {
return &IPController{
rdb: rdb,
}
}
// createKEY ...
func (c *IPController) createKEY(ip string) string {
return fmt.Sprintf("%s:%s", keyIPRequestsPrefix, ip)
}
La idea para implementar un rate limit
es emplear los siguientes comandos GET
,SETEX
, INCR
que nos provee redis. Seguro conoces GET
para obtener dado una key
su valor,
en el caso de SETEX
es para agregar un valor a una key
en caso de que no exista y de lo contrario la modifica, el último comando INCR
es el encargado de incrementar la cantidad de veces q se utiliza esa key
.
# redis-cli
127.0.0.1> GET requests:<IP>
127.0.0.1> SETEX requests:<IP> 10 0
127.0.0.1> INCR requests:<IP>
Agreguemos AcceptedRequest
como método de nuestro IPController
extrapolando los comandos de redis anteriormente explicados.
// AcceptedRequest ...
func (c *IPController) AcceptedRequest(ctx context.Context, ip string, limit, limitDuration int) (int, bool) {
key := c.createKEY(ip)
// GET requests:<IP>
if _, err := c.rdb.Get(ctx, key).Result(); err == redis.Nil {
// SETEX requests:<IP> limitDuration 0
err := c.rdb.Set(ctx, key, "0", time.Second*time.Duration(limitDuration))
if err != nil {
log.Println(err)
return 0, false
}
}
// INCR requests:<IP>
if _, err := c.rdb.Incr(ctx, key).Result(); err != nil {
log.Println(err)
return 0, false
}
// GET requests:<IP>
requests, err := c.rdb.Get(ctx, key).Result()
if err != nil {
log.Println(err)
return 0, false
}
requestsNum, err := strconv.Atoi(requests)
if err != nil {
log.Println(err)
return 0, false
}
if requestsNum > limit {
return requestsNum, false
}
return requestsNum, true
}
Si nos dirigimos a la función HandleFunc
que maneja la ruta /
, en este punto del tutorial se encuentra así 👇
func main() {
...
// redis instance
rdb := db.GetRedisDbClient(context.TODO())
r := mux.NewRouter()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ip := extractIpAddr(r)
controller := NewIPController(rdb)
requests, accepted := controller.AcceptedRequest(
context.TODO(),
ip,
reqLimit,
durationLimit,
)
if !accepted {
w.WriteHeader(http.StatusTooManyRequests)
}
w.Header().Add("X-RateLimit-Limit", strconv.Itoa(reqLimit))
w.Header().Add("X-RateLimit-Remaining", strconv.Itoa(10-requests))
})
...
}
Como se 👀 se hace uso de las funciones extractIpAddr
para definir el identificador principal y de la función AcceptedRequest
quien maneja el control de nuestras solicitudes entrantes, retornando en caso positivo status 200
y en caso de llegar al límite definido por cantidad de solicitudes o de ⏰ de duración status 429
🛑. Pues ejecutemos de nuevo el servidor y en una terminal aparte con el redis-cli
ejecutemos un nuevo comando MONITOR
, este ultimo nos ayudará a entender el proceso de nuestro rate limit pero desde la vista de la base de datos.
Conclusión
A través de este tutorial, pudimos desarrollar una aplicación en el lenguaje de programación go y Redis. Con la implementación de un rate limit
en nuesta API pudimos agregar una capa de seguridad que nos permita elevar las medidas de seguridad ante posibles ataques de usuarios o bot maliciosos. Pudimos definir como identificador principal el IP
de nuestros usuarios o posibles atacantes. Ampliamos nuestros concimientos en el uso de nuevos comandos de redis como INCR
y MONITOR
.
Recursos
- What is a rate limit
- Basic Redis rate limit with go
- gorrilla/mux
- Rate limiting APIs with Redis + Express.js
- Rate Limiting Algorithms using Redis
Código del proyecto
Aquí tienes el repositorio en GitHub con todo el código utilizado en el artículo. Por si quieres revisarlo.