ExyonQ Servidor HTTP modular y reverse proxy en Rust

Operación

Qué comandos usar para validar, arrancar, recargar, leer logs y diagnosticar, más los errores que aparecen una y otra vez. Todo lo de esta página funciona sobre un despliegue real de dos procesos (ver Instalación).

Validar

exyonq validate -c /etc/exyonq/https.toml

Salida correcta:

warning: `exyonq validate` is transitional; prefer `exyonqctl config lint` (or `exyonqctl config test` for RuntimePlan compile)
config OK: /etc/exyonq/https.toml
OK

Con un error, el código de salida es 1 y el diagnóstico señala línea y campo:

error[EXY-CONFIG-0002]: TOML parse error at line 6, column 23
  |
6 | match = { path = "/", upstream = "nope" }
  |                       ^^^^^^^^
unknown field `upstream`, expected one of `path`, `host`, `suffix`, `regex`, `header`, `header_value`

validate comprueba parseo y validación del IR. Para compilar el plan de ejecución completo sin publicar nada, usa exyonqctl config test; para una preflight de recarga, exyonqctl config reload --check.

Arrancar

exyonq serve -c /etc/exyonq/https.toml

En systemd, serve corre bajo el usuario del servicio. La unidad publicada ejecuta la validación como ExecStartPre, así que un TOML roto no llega a arrancar. El proceso no implementa sd_notify; no uses Type=notify.

Variables de entorno relevantes:

VariableEfecto
EXYONQ_CONFIGRuta usada por la unidad del paquete
EXYONQ_CONTROL_SOCKETSocket Unix del plano de control
EXYONQ_ACME_CHALLENGE_DIRDirectorio compartido de retos HTTP-01
EXYONQ_WAF_MODE, EXYONQ_WAF_ENFORCE, EXYONQ_WAF_ABUSE, EXYONQ_WAF_ABUSE_RPS, EXYONQ_WAF_ABUSE_BURSTAnulan el WAF del IR
EXYONQ_CACHE_PURGE_SOCKET, EXYONQ_CACHE_PURGE_TOKENPurga autenticada de caché

Recargar

exyonqctl config reload /etc/exyonq/https.toml --check     # preflight, no publica
exyonqctl config reload /etc/exyonq/https.toml --diff      # clasifica el cambio
exyonqctl reload -s /run/exyonq/https.sock -c /etc/exyonq/https.toml
exyonqctl status -s /run/exyonq/https.sock

--diff compara huellas y enumera rutas:

current_generation = 1
current_fingerprint = 0960bed9…:m0c1r0
candidate_fingerprint = 1f3c2a77…:m0c1r0
routes_added = ["exyonq_dev_assets"]
routes_removed = []
routes_unchanged = 26
IDENTICAL_RELOAD = NO_OP
DYNAMIC_INPLACE = FORBIDDEN
H3_RELOAD_SUPPORT = SAME_LISTENER_CONFIG_ONLY

La línea base sale de --baseline <IR anterior> o de un fichero lateral {config}.exyonq-applied-routes.v1.json que se escribe tras publicar. Si lo borras, la siguiente recarga vuelve a decir «todo añadido» una vez.

Una recarga fallida no toca la generación viva: se descarta el candidato. Si el cambio altera una dirección de escucha o habilita un protocolo nuevo, la recarga no sirve: reinicia (EXY-RELOAD-0005).

Logs y niveles

[logging].level acepta la sintaxis de EnvFilter: info, debug, o por objetivo (exyonq=debug,exyonq_waf=info,warn). format elige text o json; con json cada línea es un objeto con los mismos campos.

Línea de acceso real:

INFO exyonq_core::observability: access event="access" method=GET path=/health status=200 protocol=http duration_ms=0 client_ip="64.181.243.113" request_id=8b9760756feff3b2206a59b5dfea539b outcome="probe"

outcome resume el resultado: ok, probe para /health, forbidden para un 403. Cada petición lleva un request_id, que es la clave para cruzar la línea de acceso con una violación de WAF.

journalctl -u exyonq-sites -f
journalctl -u exyonq-sites --since "-15 min" | grep 'status=5'
journalctl -u exyonq-sites | grep 'waf violation'

Destinos alternativos: [logging.file] (con rotación propia por tamaño y keep), [logging.syslog] y [logging.journald]. Si prefieres logrotate, el fichero publicado recarga el proceso por el socket de control en lugar de mandar SIGHUP.

ACME

HTTP-01 es el modo por defecto y no necesita credenciales DNS: el proceso registra el token y el mundo lo pide por el puerto 80.

[server.tls.acme]
enabled = true
email = "security@exyonq.dev"
domains = ["exyonq.dev", "www.exyonq.dev"]
challenge = "http-01"

Con dos procesos, el del :443 emite y el del :80 responde el reto. Ambos deben apuntar EXYONQ_ACME_CHALLENGE_DIR al mismo directorio estable, fuera de RuntimeDirectory (por ejemplo /var/lib/exyonq-http01, 0750, propietario del servicio). El proceso escribe <dir>/<token> de forma atómica con modo 0600 y lo borra al terminar; solo se sirven nombres de token seguros.

Renovación: se usa ARI (ACME Renewal Information) cuando la CA lo soporta y, si no, se renueva 30 días antes del vencimiento. Los certificados se escriben de forma atómica y el aceptador TLS se reconstruye con la configuración vigente, sin reiniciar.

INFO exyonq_acme::manager: ACME HTTP-01 challenge registered token=N-jBkXUc3ssTrcN83YiKUxcI-n-Plav3JkhO3UmAcnM domain=www.exyonq.dev
INFO exyonq_acme::manager: ACME certificate issued cert=/etc/exyonq/tls/exyonq.pem key=/etc/exyonq/tls/exyonq.key
INFO exyonq_acme::manager: ACME ARI renewal window renewal_window_start=1798673700

Si el pedido falla, el fallo queda en el journal y el certificado vigente sigue sirviéndose; si no hay material válido en disco, el listener TLS falla cerrado: prefiere no negociar antes que servir un certificado caducado o de otro host. Un certificado emitido a mano en cert/key sigue funcionando aunque ACME esté deshabilitado.

La cuenta se guarda en account_path o en <cert>.acme-account.json, con modo 0600, y se reutiliza entre ejecuciones.

Diagnóstico

SíntomaQué mirar
El servicio no arrancajournalctl -u <unidad> -n 50; exyonq validate -c …
502 en un proxyQue el backend escuche en el target; timeout_ms; journal del backend
403 inesperado en una rutagrep 'waf violation'; rule_id y field dicen qué regla y qué parte de la petición
404 de un estáticoroot de la ruta, el prefijo del match y si el fichero existe
El certificado no se renuevaEXYONQ_ACME_CHALLENGE_DIR en ambas unidades; que el puerto 80 sea alcanzable; logs exyonq_acme
Cambios que no se aplicanexyonqctl status (huella y generación), después config reload --diff

Comandos de apoyo:

exyonqctl status -s /run/exyonq/https.sock --format json
exyonqctl cache stats --url http://127.0.0.1:9090/metrics
exyonqctl cache explain --config /etc/exyonq/https.toml https://exyonq.dev/
exyonqctl purge url -s /run/exyonq/purge.sock --token "$TOKEN" exyonq https exyonq.dev /
exyonqctl drain -s /run/exyonq/https.sock
exyonqctl shutdown -s /run/exyonq/https.sock

status devuelve una línea como status ok process_state=serving generation=1 fingerprint=…:m0c1r0 uptime_s=418 active_connections=6 draining=false reload_in_progress=false version=0.9.0.

Errores típicos

MensajeCausa
config must define at least one [[server]] blockFalta el listener
unknown field …Campo que no existe en el IR, o mal escrito
route match must set exactly one of path, suffix, or regexDos patrones a la vez en match
duplicate route match identity host=… pattern=…Dos rutas con el mismo host, tipo y patrón
server references unknown route: Xroutes lista un nombre que no existe
route X references unknown upstream: Yupstream mal escrito o no declarado
route X must define upstream, root, redirect, rewrite, or fastcgiRuta sin acción
route X defines conflicting actionsDos acciones en la misma ruta
http3_listen is only honored on the first [[server]] blockhttp3_listen en un bloque secundario
http3_listen cannot be combined with tls.strict_sni/…Opciones que QUIC no honra
modules.metrics.scrape_bearer_token is required when metrics is enabled and any server.listen or server.http3_listen is non-loopbackMétricas expuestas sin token
health_check.timeout_ms must be <= interval_msSondeo mal dimensionado
upstream X: max_connect_retries=2 exceeds Cap021 v1 max 1Reintentos de conexión por encima del máximo

Checklist antes de reiniciar

  1. exyonq validate -c <config> devuelve OK para cada fichero del despliegue (http.toml y https.toml).
  2. Si el cambio es de rutas, upstreams o material TLS, prueba primero exyonqctl config reload <config> --check; si pasa, recarga en lugar de reiniciar.
  3. Si el cambio toca listen, http3_listen o habilita un protocolo, el reinicio es obligatorio: planifica la ventana.
  4. Copia el TOML vigente y anota la huella de exyonqctl status; es tu vuelta atrás.
  5. Comprueba que el usuario del servicio puede leer cert/key y escribir en el directorio de certificados y en el de retos.
  6. Tras reiniciar: status con process_state=serving, curl -sS -o /dev/null -w '%{http_code}' https://<host>/health y journalctl sin errores nuevos.