ExyonQ Servidor HTTP modular y reverse proxy en Rust

Instalación

ExyonQ se instala como un binario único más un servicio systemd. Para publicar en los puertos 80 y 443 se ejecutan dos procesos hermanos con configuraciones distintas. Esta página cubre la compilación, el despliegue, esas dos unidades, los permisos y el primer arranque.

Compilar desde fuentes

El repositorio fija el canal de Rust en rust-toolchain.toml (1.99.1). Construir el producto y el CLI de control:

cargo build -p exyonq -p exyonqctl --release --locked

Comprobar el árbol completo antes de empaquetar:

cargo check --workspace --locked
cargo test --workspace --locked

Las compilaciones con el proveedor HTTP/3 quiche necesitan paquetes del host: cmake, clang y libclang-dev. El proveedor por defecto es s2n; quiche es opcional y existe un rollback quinn-legacy. El asignador por defecto es el del sistema, con una feature opcional allocator-jemalloc en el binario exyonq. Mimalloc no es una característica del producto.

Artefactos publicados

ArtefactoContenido
exyonq-0.9.0-linux-{amd64,arm64}.tar.gzBinarios exyonq y exyonqctl para esa arquitectura
SHA256SUMS.txtSumas SHA-256 de los tarballs
sbom.cdx.jsonSBOM en CycloneDX
THIRD_PARTY_NOTICES.mdLicencias y avisos de terceros
ghcr.io/exyonq/exyonq:0.9.0, ghcr.io/exyonq/exyonq-wordpress:0.9.0Imágenes OCI (sin :latest)

El release 0.9.0 publica estos artefactos: no incluye paquetes .deb/.rpm ni builds para macOS o Windows, así que el despliegue se hace con el tarball y una unidad systemd propia (más abajo). Las imágenes OCI están en GHCR (ghcr.io/exyonq/exyonq:0.9.0 y ghcr.io/exyonq/exyonq-wordpress:0.9.0).

Instalar el binario a mano

sudo install -m 0755 exyonq exyonqctl /usr/bin/
sudo install -d -m 0750 -o exyonq -g exyonq /etc/exyonq /var/lib/exyonq /var/log/exyonq /run/exyonq

Tras el primer arranque, systemd-tmpfiles --create recrea /run/exyonq, /var/lib/exyonq y /var/log/exyonq con propietario exyonq y modo 0750.

El patrón de dos procesos

El despliegue estándar no ejecuta un proceso que escuche en :80 y :443 a la vez. Ejecuta dos instancias del mismo binario, cada una con su TOML y su socket de control:

# /etc/systemd/system/exyonq-http.service — :80, solo redirecciones y retos ACME
[Unit]
Description=ExyonQ HTTP to HTTPS redirect
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ExecStart=/usr/local/bin/exyonq serve -c /etc/exyonq/http.toml
Restart=on-failure
RestartSec=2
Environment=EXYONQ_ACME_CHALLENGE_DIR=/var/lib/exyonq-http01
Environment=EXYONQ_CONTROL_SOCKET=/run/exyonq/http.sock
RuntimeDirectory=exyonq

[Install]
WantedBy=multi-user.target

La unidad del 443 es idéntica salvo ExecStart=/usr/local/bin/exyonq serve -c /etc/exyonq/https.toml y EXYONQ_CONTROL_SOCKET=/run/exyonq/https.sock.

Por qué no se fusionan

La identidad de una ruta es (host, tipo de match, patrón) y es global en toda la configuración, no por listener. Si el mismo fichero declarase la ruta de redirección / para exyonq.org en :80 y la ruta de contenido / para exyonq.org en :443, ambas tendrían la misma identidad: la de redirección taparía a la de contenido y el resultado observado es un bucle (https://host respondiendo Location: https://host). Fusionar los dos TOML obligaría a diseñar un enrutado por listener que el producto no tiene.

Con dos procesos el reparto es limpio: el del :80 solo redirige, el del :443 es el único listener público con contenido. Cada uno mantiene su socket de control, así que se recargan por separado (exyonqctl reload -s /run/exyonq/http.sock, .../https.sock).

El único dato que ambos comparten es el directorio de retos ACME (EXYONQ_ACME_CHALLENGE_DIR). El proceso del :443 registra el token, el del :80 lo sirve porque lo lee del mismo directorio. Ese directorio debe quedar fuera de RuntimeDirectory: systemd borra un RuntimeDirectory al parar cualquiera de las unidades que lo declaran, y se llevarían por delante el socket del hermano. Usa una ruta estable, por ejemplo /var/lib/exyonq-http01 con modo 0750 y propietario del usuario del servicio.

Permisos

Primer arranque

  1. Valida la configuración (siempre antes de instalar o recargar):
exyonq validate -c /etc/exyonq/https.toml
  1. Arranca el servicio y sigue el journal:
systemctl start exyonq-sites && journalctl -u exyonq-sites -f
  1. Comprueba que responde y que el estado del proceso es correcto:
curl -sS -o /dev/null -w '%{http_code}\n' https://exyonq.org/health
exyonqctl status --socket /run/exyonq/https.sock

La salida de status incluye process_state, generation, fingerprint, uptime_s, active_connections, draining y version.

Arranque sin certificado

Desde 0.9.0 el proceso del :443 puede arrancar sin certificado en disco: responde los retos HTTP-01 en el listener en claro (el del :80, con el directorio de retos compartido), instala el certificado emitido y reconstruye el aceptador TLS sin reiniciar. Mientras el material no está disponible, los listeners TLS fallan cerrados: no sirven tráfico con un certificado inválido ni con el de otro host.

Validar antes de instalar

La unidad del paquete ejecuta la validación como ExecStartPre, de modo que un error de configuración impide el arranque en lugar de dejar el servicio en bucle:

ExecStartPre=/usr/bin/exyonq validate -c ${EXYONQ_CONFIG}
ExecStart=/usr/bin/exyonq serve -c ${EXYONQ_CONFIG}
ExecReload=/usr/bin/exyonqctl reload --config ${EXYONQ_CONFIG} --socket ${EXYONQ_CONTROL_SOCKET}

validate sale con código 0 e imprime config OK: <ruta> seguido de OK; con un error sale con código 1 e imprime un diagnóstico error[EXY-CONFIG-…] con la línea y el campo culpables. El comando avisa de que es transitorio y recomienda exyonqctl config lint o exyonqctl config test.

La unidad del paquete no usa sd_notify: el demonio no lo implementa. No configures Type=notify ni NotifyAccess. La parada ordenada es SIGTERM, con TimeoutStopSec holgado (90 s en la unidad publicada).