Configuración
Toda la configuración de EQProxy vive en un único fichero TOML con schema_version = 1. Esta página recoge las claves que existen —tipo, defecto y nota— por sección, con un ejemplo mínimo por bloque.
El esquema real es eqproxy.schema.json, regenerable con eqproxy config schema; eqproxy.toml.example es el fichero comentado que repasa las mismas claves.
Estructura del fichero
El esquema es cerrado (additionalProperties: false en todas las tablas): una clave con errata se rechaza, no se ignora. schema_version es obligatoria y solo vale 1. Las duraciones son cadenas con sufijo ("500ms", "5s", "1h"). Las rutas relativas —certificados, mapas, páginas de error, caché en disco— se resuelven desde el directorio del TOML, no desde el directorio de trabajo.
Los nombres de listener y backend son claves de tabla ([listener.web], [backend.api]). Un listener exige listen y backend; un backend exige server o servers, nunca los dos.
Global y métricas
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
schema_version | entero | — (obligatorio) | Único valor aceptado: 1 |
shutdown_grace | duración | "10s" | Drenaje de conexiones vivas al apagar con SIGINT |
[metrics] detailed | booleano | false | Contadores por servidor y por ruta; recargable |
[metrics] max_series | entero | 128 | Presupuesto de series, 1..=4096; fijo al arrancar |
schema_version = 1
shutdown_grace = "10s"
[metrics]
detailed = false
max_series = 128
Listeners
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
listen | cadena | — (obligatorio) | IP:PORT; el puerto 0 pide uno efímero |
backend | cadena | — (obligatorio) | Backend de respaldo cuando ninguna regla coincide |
mode | cadena | "tcp" | "tcp" (túnel de bytes) o "http" (HTTP/1.1 estricto) |
maxconn | entero | 1024 | Conexiones concurrentes; al llegar al tope se rechaza al recién llegado |
connect_timeout | duración | "5s" | Conexión al backend |
idle_timeout | duración | "60s" | Inactividad por dirección |
client_header_timeout | duración | "5s" | Solo http: recibir la cabecera de la petición |
server_header_timeout | duración | "30s" | Solo http: recibir la cabecera de la respuesta |
max_stream_duration | duración | "0s" | Tope de duración total del intercambio; "0s" lo desactiva |
forwarded_headers | cadena | "off" | "off", "append" o "replace" |
proxy_receive | booleano | false | Aceptar PROXY protocol v1; exige proxy_allow con IPs o CIDR |
quic | booleano | false | HTTP/3 sobre QUIC en la misma IP:PORT por UDP; exige TLS y mode = "http" |
rules | lista de tablas | [] | Reglas ordenadas; gana la primera que coincide |
error_pages | lista de tablas | [] | Páginas de error propias, por código |
local_response | tabla | ausente | Respuesta propia cuando no hay servidor disponible |
[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "api"
maxconn = 2048
forwarded_headers = "append"
Timeouts
Cada fase tiene su reloj y su respuesta observable; config explain imprime las dos cosas antes de que nada ocurra.
| Timeout | Qué mide | Al dispararse |
|---|---|---|
connect_timeout | Conectar con el backend | 502 |
client_header_timeout | Recibir la cabecera de la petición | 408 en el primer intercambio; cierre después |
server_header_timeout | Recibir la cabecera de la respuesta | 504 |
idle_timeout | Inactividad en una dirección | Cierre, también en túneles WebSocket, CONNECT y tcp |
max_stream_duration | Duración total del intercambio | 504 antes de la cabecera; flujo truncado después |
timeout_queue | Espera en la cola del backend | 503 explícito |
health_check.timeout | Un intento de comprobación | Cuenta como fallo |
Inactividad y duración son cosas distintas: idle_timeout corta un intercambio que no se mueve; max_stream_duration, uno que se mueve pero dura demasiado.
Conexiones y límites
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
listener.maxconn | entero | 1024 | Tope de conexiones concurrentes del listener |
servers[].maxconn | entero | 0 | 0..=1000000; 0 = solo lo limita el listener; al llegar al tope el balanceador deja de elegirlo |
limits.max_head_bytes | entero | 16384 | 1024..=1048576; aplica a la cabecera de petición y de respuesta |
limits.max_headers | entero | 100 | 1..=1000 cabeceras por mensaje |
limits.max_body_bytes | entero | 0 | 0 = sin límite; se responde 413 antes de conectar si la longitud viene declarada |
limits.stream_buffer_bytes | entero | ausente | Tubería de cada flujo h2/h3/WebSocket: 1024..=1048576; ausente = 64 KiB (16 KiB en h3) |
[listener.web.limits]
max_head_bytes = 16384
max_headers = 100
max_body_bytes = 1048576
Cuando todos los servidores de un backend están en su maxconn, la petición falla de forma explícita con no_available_server y, si hay cola, espera en ella.
TLS en el listener
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
cert | ruta | — (obligatorio) | Cadena PEM, hoja primero |
key | ruta | — (obligatorio) | Clave PEM: PKCS#8, PKCS#1 o SEC1 |
alpn | lista | [] | Por ejemplo ["h2", "http/1.1"]; vacío = sin ALPN; "h3" se rechaza aquí |
min_version | cadena | "1.2" | TLS 1.0 y 1.1 nunca se aceptan |
max_version | cadena | ausente (= 1.3) | "1.2" limita a TLS 1.2; no puede quedar por debajo de min_version |
client_ca | ruta | ausente | Presente = mTLS: el certificado de cliente debe encadenar con esta CA |
certs | lista de tablas | [] | Certificados adicionales por SNI: cert, key y names |
[listener.web.tls]
cert = "certs/fullchain.pem"
key = "certs/privkey.pem"
alpn = ["h2", "http/1.1"]
[[listener.web.tls.certs]]
cert = "certs/api.pem"
key = "certs/api.key"
names = ["api.example.com", "*.api.example.com"]
El certificado por defecto responde a los clientes sin SNI y a los nombres que ninguna entrada reclama: un nombre desconocido no hace fallar el handshake. La selección es nombre exacto y después el comodín más largo.
Reglas de enrutamiento
Cada regla de [listener.*.rules] suma condiciones (AND) y lleva exactamente una acción; el orden del array es el orden de evaluación.
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
name | cadena | "rule-N" | Nombre para explain y logs |
host / host_prefix / host_regex | cadena | — | Host sin puerto, sin distinguir mayúsculas |
path / path_prefix / path_regex | cadena | — | Sin normalizar: /v1 también casa con /v10 |
methods, headers, cookies, ip | lista | [] | Cabeceras y cookies con exact, regex o present = true |
map | tabla | ausente | { name, key o header, equals }: consulta un [maps.*] |
use_backend | cadena | — | Acción: enviar al backend |
split | lista | — | Acción: reparto ponderado [{ backend, weight }] |
redirect | tabla | — | Acción: 3xx local con status y location |
direct_response | tabla | — | Acción: cuerpo local con status, body y content_type |
rewrite | tabla | — | Solo con use_backend: path_prefix_rewrite, host_rewrite |
set_headers / add_headers / del_headers | lista | [] | Solo con use_backend; valores con $remote_addr, $host, $scheme |
response_headers | lista | [] | Cabeceras que ve el cliente, con las mismas plantillas |
rate_limit | tabla | ausente | { rate, per, burst }; cubo por IP de cliente y por regla |
cors | tabla | ausente | allow_origin obligatorio; el preflight se contesta localmente |
auth | tabla | ausente | url (subpetición http://) y/o type = "basic" con users htpasswd |
[[listener.web.rules]]
name = "api-v1"
host = "api.example.com"
path_prefix = "/api/v1/"
use_backend = "api"
rate_limit = { rate = 10, per = "1s", burst = 20 }
[listener.web.rules.rewrite]
path_prefix_rewrite = "/api/"
Un 429 por rate limit es siempre explícito y suma rate_limited. El cubo es local al proceso: con varias instancias, cada una lleva el suyo.
Upstreams y balanceo
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
server | cadena | — | Un solo servidor (IP:PORT, HOSTNAME:PORT o unix:/ruta); excluyente con servers |
servers | lista de tablas | — | addr, weight, backup, priority, maxconn, source |
balance | cadena | "roundrobin" | "roundrobin" (ponderado suave) o "leastconn" |
slow_start | duración | "0s" | Rampa del peso efectivo tras volver a operativo; máximo "1h" |
all_backups | booleano | false | Reparte entre todos los respaldos en vez de un escalón cada vez |
resolve_interval | duración | "30s" | Re-resolución de servidores con nombre; "0s" = solo al arrancar |
proxy_protocol | cadena | "none" | "v1" o "v2" hacia el servidor |
maxqueue | entero | 0 | Cola acotada cuando no hay servidor disponible; 0 = rechazar ya |
timeout_queue | duración | "5s" | Espera máxima en esa cola |
retries | entero | 0 | 0..=5 redes de conexión |
retry_non_idempotent | booleano | false | Permite reintentar POST y compañía |
affinity | tabla | ausente | mode = "cookie" (cookie eqproxy_affinity por defecto) o "source" |
[backend.*.tls] | tabla | ausente | verify (true), ca_file, server_name, alpn, min_version |
En servers, los pesos van de 0 a 256 y weight = 0 deja el servidor drenado sin tocar el resto. priority (0..=255) solo vale con backup = true y ordena los escalones de failover; all_backups y priority juntos son un error de configuración. Un source fija la IP local de salida y solo vale para destinos TCP.
[backend.api]
balance = "leastconn"
servers = [
{ addr = "10.0.0.11:9000", weight = 3 },
{ addr = "10.0.0.12:9000", weight = 1 },
{ addr = "10.0.0.13:9000", weight = 1, backup = true },
]
maxqueue = 256
timeout_queue = "500ms"
Health checks
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
type | cadena | "tcp" | "tcp" (solo conectar) o "http" (un intercambio real) |
interval | duración | "2s" | Entre ciclos |
timeout | duración | "1s" | Por intento: conexión, petición y respuesta |
fall / rise | entero | 3 / 2 | Fallos consecutivos para caer; éxitos para volver |
passive | booleano | false | El tráfico vivo (5xx, fallos de conexión) alimenta la misma máquina |
http_path / http_method | cadena | "/" / "GET" | Solo type = "http"; el método, GET o HEAD |
http_host | cadena | la dirección del servidor | Cabecera Host de la sonda |
expect_status | cadena | "200-299" | Códigos o rangos aceptados |
expect_rstring | cadena | ausente | Regex que debe aparecer en el cuerpo (se lee hasta 64 KiB) |
[backend.api.health_check]
type = "http"
interval = "2s"
fall = 3
rise = 2
http_path = "/healthz"
expect_status = "200-299"
Sin sección health_check, todos los servidores se consideran operativos. Una comprobación correcta no demuestra que la aplicación funcione: prueba lo que le pidas y nada más.
Caché
Solo en mode = "http" y solo si la sección existe.
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
default_ttl | duración | "60s" | 1s..=24h; s-maxage/max-age solo acortan |
max_memory_bytes | entero | 67108864 | 1 MiB..=1 GiB; desalojo LRU al superarlo |
max_entry_bytes | entero | 1048576 | 64 KiB..=64 MiB y nunca mayor que la memoria |
disk_path | ruta | ausente | Persistencia en disco; sin ella, solo memoria |
[listener.web.cache]
default_ttl = "60s"
max_memory_bytes = 67108864
disk_path = "cache/web"
Solo se almacenan GET con estados 200, 301, 308, 404 y 410 y sin Set-Cookie. Las respuestas servidas llevan X-Cache: HIT, MISS o REVALIDATED.
Para comprimir respuestas hay una sección aparte, [listener.*.compression]: gzip y level (1..9, 6 por defecto), brotli y brotli_quality (0..11, 5 por defecto), min_size (1024) y types.
Filtros WASM
Filtro de petición, uno por listener, solo en mode = "http". El módulo se compila al cargar la configuración: un fichero ausente, un import denegado o una exportación que falte rechazan el snapshot, no fallan por petición.
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
path | ruta | — (obligatorio) | Módulo .wasm, relativo al fichero de configuración |
fuel | entero | 10000000 | 1..=100000000 instrucciones por petición |
timeout | duración | "50ms" | 1ms..=5s, con resolución de 1 ms |
memory_bytes | entero | 16777216 | 1 MiB..=256 MiB de memoria lineal por instancia |
max_header_ops | entero | 16 | 1..=64 mutaciones de cabecera por petición |
on_error | cadena | "closed" | "closed" responde 503; "open" reenvía sin cambios y cuenta el error |
[listener.web.plugin]
path = "plugins/filter.wasm"
timeout = "50ms"
on_error = "closed"
El plugin puede añadir, sustituir o quitar cabeceras de petición y rechazar con 400..=599; las de framing (content-length, transfer-encoding, host) están reservadas.
Mapas de datos
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
file | ruta | ausente | Un par clave valor por línea; # solo al inicio; claves duplicadas se rechazan |
entries | tabla | ausente | Entradas en línea, estáticas; excluyente con file |
default | cadena | ausente | Valor para claves sin entrada |
reload_interval | duración | "5s" | Relectura del fichero; "0s" = cargar una vez |
[maps.tiers]
file = "tiers.map"
default = "free"
[[listener.web.rules]]
name = "vip-tenants"
map = { name = "tiers", header = "x-tenant", equals = "vip" }
use_backend = "vip"
Un mapa se relee en caliente sin recargar la configuración: los datos nuevos entran de forma atómica y el resto del estado —contadores, cubos, pools, conexiones— sobrevive. Escribe con escritura + renombrado para que el vigilante nunca lea un fichero a medias; una actualización mal formada se rechaza, suma map_reload_failures y sigue sirviendo la última buena.
Secretos y recarga en caliente
| Secreto | Dónde | Recargable con SIGHUP |
|---|---|---|
| Certificados y claves TLS | [listener.*.tls] y certs[].cert/key | Sí, y con POST /api/v1/certificates/reload |
| CA de cliente (mTLS) | client_ca | Sí; en un listener QUIC vivo, no |
| CA hacia el backend | [backend.*.tls] ca_file | Sí |
| Token de lectura y de escritura | token_file, write_token_file | No: se fijan al arrancar |
| Modo, grupo, ACL y allowlist del socket Unix | [admin] unix_socket_* | Sí, sobre el socket ya enlazado |
[admin]
unix_socket = "/run/eqproxy/admin.sock"
unix_socket_mode = "0660"
unix_socket_gid = 20
token_file = "/etc/eqproxy/admin.token"
write_token_file = "/etc/eqproxy/admin-write.token"
Prefiere token_file a token: un token escrito en el TOML viaja allí donde se copia el fichero. El token de escritura debe ser distinto del de lectura; sin él, el plano entero es de solo lectura.
Plano de administración
| Nombre | Tipo | Defecto | Notas |
|---|---|---|---|
listen | cadena | ausente | IP:PORT; loopback salvo allow_remote. Excluyente con unix_socket |
unix_socket | ruta | ausente | Socket Unix; modo 0600 por defecto |
unix_socket_mode | cadena | "0600" | Solo "0600" o "0660"; con 0660 hace falta unix_socket_gid |
unix_socket_allow_uids | lista | [] | Uids de par permitidos, comprobados con SO_PEERCRED/getpeereid |
unix_socket_acl_uids | lista | [] | Usuarios con rw en la ACL POSIX del fichero; solo Linux, exige 0660 y gid |
token / token_file | cadena / ruta | ausente | Token de lectura; al menos uno |
write_token / write_token_file | cadena / ruta | ausente | Token de escritura; sin él no hay escrituras |
allow_remote | booleano | false | Permite un bind no loopback; el token viaja en claro (el plano no tiene TLS) |
panel | booleano | false | Sirve el panel web en GET /panel; apagado, todo /panel es 404 |
[admin]
listen = "127.0.0.1:9001"
token_file = "/etc/eqproxy/admin.token"
write_token_file = "/etc/eqproxy/admin-write.token"
panel = true
Sin sección [admin] no hay plano de administración. Los endpoints, los roles y los códigos de respuesta están en API de administración.