ExyonQ Servidor HTTP modular y reverse proxy en Rust

WAF

El WAF de ExyonQ es un motor nativo en Rust, integrado en el proceso exyonq serve. Inspecciona ruta, query, cabeceras y cuerpo con detectores de firma y añade una puerta de abuso por IP. No es un complemento externo ni depende de Pingora.

Modos

waf.mode decide qué se hace con una firma que coincide:

ModoDetectoresRechazo en el host
disabled (o enabled = false)No se compilanNo
monitorSe ejecutan; Block se degrada a LogNo
blockSe ejecutan; Block es efectivoSí, cuando el host aplica la decisión

La puerta de abuso es independiente del modo de firma: con mode = "monitor" y [waf.abuse] mode = "rate_limit" se siguen devolviendo 429. Un reto superado nunca anula un bloqueo de firma.

[waf]

CampoTipoDefectoNotas
enabledbooltruefalse desactiva detectores
modestring"monitor"monitor, block, disabled
enginestring"native"Único valor admitido
max_inspection_body_bytesentero1048576Presupuesto de inspección de cuerpo; debe ser > 0
on_inspection_limitstring"log_and_allow"allow_uninspected, block, log_and_allow
fail_policystring"closed_for_invalid_rules"Único valor admitido

Reglas incluidas

IDFamiliaQué detecta
EXY-SQL-1001SQLiUNION … SELECT
EXY-SQL-1002SQLiTautología numérica (OR 1=1)
EXY-SQL-1003SQLiFunciones de retardo (SLEEP, BENCHMARK, WAITFOR DELAY)
EXY-XSS-1001XSSEtiqueta <script
EXY-XSS-1002XSSManejadores onload, onerror, onclick, onmouseover
EXY-XSS-1003XSSEsquema javascript:
EXY-PATH-1001Path traversal../, ..\, %2e%2e%2f
EXY-PATH-1002Rutas sensibles/etc/passwd, .htaccess, .env, id_rsa, /proc/self
EXY-CMD-1001Ejecución de comandosEncadenado con punto y coma o tubería, backticks, $(…)
EXY-CMD-1002Ejecución de comandosRutas de intérprete (/bin/sh, cmd.exe, powershell)
EXY-BOT-1001Botsqlmap, nikto, nmap, masscan, burpsuite, acunetix
EXY-BOT-1002Botdirbuster, gobuster, wpscan, nessus
EXY-HDR-1001CabecerasUser-Agent ausente o vacío
EXY-HDR-1002CabecerasValor de cabecera mayor de 8 KiB
EXY-IP-1001IPIP de cliente en lista negra
EXY-IP-1002IPIP fuera de la lista blanca, cuando hay lista blanca
EXY-LIMIT-BODY-001Límite de inspecciónPresupuesto de cuerpo agotado en modo block

Los bots legítimos conocidos se perdonan: googlebot, bingbot, slurp, duckduckbot, facebookexternalhit, twitterbot. Antes de comparar se normaliza la URL (incluida la doble codificación) y el cuerpo se lee como UTF-8 con pérdida, de modo que un cuerpo binario no se salta en silencio.

Activación por detector

[waf.ruleset.builtins] ajusta familia por familia. Si ningún ruleset suministra builtins, se usa el paquete por defecto: SQL, XSS, path y comandos en block; cabeceras y bot en log.

ClaveValoresDefecto efectivo
sql_injectionoff, log, blockblock
xssoff, log, blockblock
path_traversaloff, log, blockblock
command_injectionoff, log, blockblock
header_anomalyoff, log, blocklog
bot_uaoff, log, blocklog
ip_filtertabla enabled, whitelist, blacklistdesactivado
[waf]
enabled = true
mode = "block"
engine = "native"
max_inspection_body_bytes = 1048576
on_inspection_limit = "log_and_allow"
fail_policy = "closed_for_invalid_rules"

[[waf.ruleset]]
id = "exyonq-core"
enabled = true

[waf.ruleset.builtins]
sql_injection = "block"
xss = "block"
path_traversal = "block"
command_injection = "block"
header_anomaly = "log"
bot_ua = "block"

Exclusiones

Una exclusión calla reglas concretas, no apaga el WAF. Es la herramienta para casos como el editor de bloques de WordPress, que envía HTML y Markdown en el cuerpo de un POST y dispararía XSS o CMD sobre contenido legítimo.

CampoTipoDefectoNotas
hoststringausenteSi se define, debe coincidir
path_prefixstringausenteCasa la ruta exacta o prefijo + "/" + resto; /api no casa /api2
methodstringausenteSi se define, debe coincidir
rule_idslistaobligatoriaLista de IDs a suprimir; vacía no casa nada
[[waf.exclusion]]
host = "blog.exyonq.org"
path_prefix = "/wp-admin"
method = "POST"
rule_ids = ["EXY-SQL-1001", "EXY-XSS-1001", "EXY-CMD-1001"]

rule_ids vacío no significa «todas las reglas». En configuración se rechaza; en tiempo de ejecución una lista vacía no coincide con nada. Una exclusión nunca es un bypass genérico.

Límite de inspección

El cuerpo se inspecciona hasta max_inspection_body_bytes (1 MiB por defecto). Al agotarse el presupuesto, on_inspection_limit decide:

ValorEfecto
allow_uninspectedSigue sin tratar el truncamiento como bloqueo
blockEmite EXY-LIMIT-BODY-001 como bloqueo cuando la política lo permite
log_and_allowDeja evidencia y permite la petición

La truncación se registra: nunca se reporta como «inspeccionado completo».

fail_policy

Solo existe closed_for_invalid_rules. Si el WAF está habilitado y la compilación del snapshot falla (regla inválida, regex roto, configuración incoherente), el arranque o la recarga fallan cerrados y no se publica una generación sin protección.

Abuso y rate limit

[waf.abuse] es una tabla única (singleton), no una lista: una sola política por proceso, indexada por IP.

CampoTipoDefectoNotas
modestring"off"off, rate_limit, challenge
requests_per_secondentero10≥ 1 cuando el modo no es off
burstentero20≥ 1 cuando el modo no es off
[waf.abuse]
mode = "rate_limit"
requests_per_second = 20
burst = 40

Con rate_limit, una petición denegada devuelve 429 con Retry-After en segundos (mínimo 1). Con challenge, el host devuelve 403 con una prueba de trabajo HMAC-SHA256 y cookie de concesión (exyonq_waf_grant, TTL 300 s); la verificación es un POST /.exyonq/waf-challenge, que sigue pasando por las firmas.

Snapshot fijo por generación

Las reglas se compilan a un snapshot inmutable. Al arrancar se compila el snapshot de la generación 1; cada recarga compila un candidato en la fase de preparación y solo lo intercambia si la publicación sale bien. Un fallo de compilación deja vivo el snapshot anterior, y las peticiones en vuelo conservan el suyo.

Las variables de entorno mandan sobre el IR cuando están definidas: EXYONQ_WAF_MODE, EXYONQ_WAF_ENFORCE (solo con mode = "block"), EXYONQ_WAF_ABUSE, EXYONQ_WAF_ABUSE_RPS, EXYONQ_WAF_ABUSE_BURST y EXYONQ_WAF_CHALLENGE_KEY_FILE (32 bytes exactos). Un valor desconocido en EXYONQ_WAF_ABUSE falla cerrado.

Verificar con curl

Con mode = "block", estas peticiones se rechazan con 403 (comprobado en un despliegue real con la configuración de ejemplo):

curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/?q=1%20UNION%20SELECT%20password%20FROM%20users'
curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/?q=%3Cscript%3Ealert(1)%3C/script%3E'
curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/../../etc/passwd'
curl -s -o /dev/null -w '%{http_code}\n' -A 'sqlmap/1.7' 'https://exyonq.dev/'

Las cuatro devuelven 403 y el cuerpo blocked by waf. Una petición limpia devuelve 200. Si obtienes un 200 en una carga que debería bloquear, revisa el mode, el host de la petición y si hay una exclusión que cubra esa ruta y ese método.

Verificar en el journal

Cada coincidencia deja una línea warn con el objetivo exyonq_waf:

WARN exyonq_waf: waf violation phase=RequestHeaders rule_id=EXY-SQL-1001 action=Block truncated=false field=Some("query") enforce=true sql injection pattern

Campos útiles: phase (RequestHeaders, RequestBodyComplete…), rule_id, action, truncated, field (path, query, header:user-agent), enforce. La línea de acceso asociada lleva status=403 y outcome="forbidden".

journalctl -u exyonq-sites | grep 'waf violation' | tail -20
journalctl -u exyonq-sites | grep -c 'rule_id=EXY-BOT-1001'

Con [modules.metrics] habilitado, el WAF publica exyonq_waf_inspections_total, exyonq_waf_decisions_total y exyonq_waf_violations_total (por rule_id).

Límites conocidos