El uso de http.ListenAndServe en entornos de producción es una vulnerabilidad de diseño por omisión. Esta función es un “helper” de conveniencia que instancia un *http.Server con valores por defecto, y en Go, el valor por defecto para los timeouts es 0, lo que significa que son infinitos. En un entorno con tráfico real, esto es una invitación al agotamiento de recursos.
Cada vez que un cliente establece una conexión TCP, el runtime de Go levanta una goroutine dedicada para gestionar esa conexión. Si un cliente abre una conexión pero nunca envía datos, o los envía extremadamente lento (un ataque de tipo Slowloris), esa goroutine permanecerá bloqueada indefinidamente, consumiendo memoria y capacidad de procesamiento. Al configurar un *http.Server personalizado, establecemos límites de tiempo estrictos sobre el ciclo de vida de la conexión y la transferencia de datos, garantizando que las goroutines se liberen y los recursos se reutilicen.
Necesitas configurar estos límites siempre que tu servidor esté expuesto a redes no controladas (Internet). Si configuras los timeouts de forma incorrecta, corres el riesgo de que conexiones legítimas pero lentas sean cortadas abruptamente, o que tu servidor colapse ante un flujo de conexiones inactivas que mantienen la tabla de descriptores de archivos llena.
El error frecuente es asumir que un timeout corto es siempre mejor. Si implementas un WriteTimeout muy agresivo en un endpoint que sirve archivos pesados o realiza procesos de backend prolongados, el servidor cerrará la conexión antes de que el cliente reciba la respuesta completa, incluso si la lógica de negocio terminó correctamente.
package main
import (
"fmt"
"io"
"log"
"net/http"
"time"
)
func main() {
// Definimos un mux para manejar las rutas
mux := http.NewServeMux()
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// Simulamos un procesamiento que tarda algo de tiempo
time.Sleep(100 * time.Millisecond)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status": "ok", "message": "datos procesados"}`)
})
mux.HandleFunc("/api/slow", func(w http.ResponseWriter, r *http.Request) {
// Este endpoint es propenso a fallar si WriteTimeout es muy corto
time.Sleep(2 * time.Second)
fmt.Fprint(w, "Respuesta tardía")
})
// Configuración robusta para producción
srv := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
// ReadTimeout: Desde la aceptación de la conexión hasta que se lee todo el cuerpo.
// Protege contra clientes que envían el request extremadamente lento.
ReadTimeout: 5 * time.Second,
// WriteTimeout: Desde que termina la lectura del request hasta que se completa la escritura.
// Ojo: Si el handler tarda más en procesar que este tiempo, la conexión se corta.
WriteTimeout: 15 * time.Second,
// IdleTimeout: Tiempo que se mantiene una conexión Keep-Alive sin actividad.
// Crucial para liberar recursos de conexiones que no se reutilizarán.
IdleTimeout: 120 * time.Second,
// ReadHeaderTimeout es fundamental contra ataques Slowloris.
// Limita el tiempo para leer los headers antes de leer el cuerpo.
ReadHeaderTimeout: 2 * time.Second,
}
log.Printf("Servidor iniciado en %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error crítico en el servidor: %v", err)
}
}
En el ejemplo anterior, la clave no es solo la creación del struct http.Server, sino la elección de los valores. Al usar srv.ListenAndServe(), estamos utilizando nuestra configuración personalizada en lugar de los valores por defecto de http.ListenAndServe.
Fíjate en ReadTimeout. Si un cliente intenta realizar un ataque enviando un byte cada 10 segundos para mantener la conexión abierta, el servidor cortará la conexión al alcanzar los 5 segundos de lectura acumulada, liberando la goroutine. El parámetro ReadHeaderTimeout es un refuerzo preventivo: limita específicamente cuánto tiempo esperamos los encabezados HTTP, lo cual es la primera barrera de defensa contra ataques que intentan mantener el handshake de la petición en un estado incompleto.
La configuración de WriteTimeout en 15 * time.Second es un compromiso. Si el endpoint /api/slow intenta responder después de ese margen (o si el cliente tarda mucho en recibirlo), el servidor cerrará la conexión. Esto es vital para evitar que clientes extremadamente lentos bloqueen la capacidad de escritura del servidor. Finalmente, IdleTimeout gestiona la política de Keep-Alive. Al establecerlo en 120 segundos, permitimos que clientes legítimos (como navegadores o microservicios) reutilicen la conexión TCP para múltiples peticiones, reduciendo la latencia de handshake, pero aseguramos que las conexiones abandonadas no consuman descriptores de archivos para siempre.
El error frecuente ocurre al usar http.ListenAndServe por pura comodidad.
// ERROR: NUNCA HACER ESTO EN PRODUCCIÓN
// Un cliente puede abrir una conexión y no enviar nada.
// La goroutine se queda bloqueada para siempre.
// Un atacante puede agotar la memoria del servidor fácilmente.
http.ListenAndServe(":8080", nil)
En el código erróneo, si un atacante abre 10,000 conexiones y no envía ni un solo byte, tu servidor intentará mantener 10,000 goroutines en espera de datos que nunca llegarán. Esto resultará en un agotamiento de memoria o en que el sistema operativo alcance el límite de archivos abiertos (ulimit), impidiendo que nuevos usuarios se conecten.
N° 192