ExyonQ Servidor HTTP modular y reverse proxy en Rust

Configuración

Referencia de las secciones del IR nativo (TOML), campo a campo, con un ejemplo mínimo por sección. El esquema lo define el propio producto: cualquier campo no listado aquí se rechaza con unknown field. Valida siempre con exyonq validate -c <fichero>.

config_version

Obligatorio. Versiones admitidas: 1 y 2. La 2 es la actual y la única que permite include, una lista de ficheros que se fusionan con el documento principal.

CampoTipoDefectoNotas
config_versionenteroobligatorio1 o 2; otro valor da unsupported config_version
includelista de rutasvacíaSolo con config_version = 2
config_version = 2
include = ["/etc/exyonq/tls.toml", "/etc/exyonq/sites.d/blog.toml"]

[logging]

Niveles y destinos. level acepta la sintaxis de EnvFilter: info, debug, o por objetivo (exyonq=debug,warn).

CampoTipoDefectoNotas
levelstring"info"No vacío
formatstring"text"text o json
queue_capacityentero8192Cola de sinks asíncronos, 1..=1048576
[logging.console]tablaenabled = truestream: stdout, stderr o split
[logging.file]tablaenabled = falsepath obligatorio si se habilita; rotation.enabled, max_bytes (100 MiB), keep (7)
[logging.syslog]tablaenabled = falsetransport: unix, udp o tcp; address obligatorio
[logging.journald]tablaenabled = falseEnvío estructurado nativo a journald
[logging.otel]tablaenabled = falseendpoint http(s) obligatorio; metrics_export está prohibido
[logging.access]tablaenabled = trueUna línea por petición
[logging.audit]tablaenabled = trueEventos de auditoría
[logging]
level = "info"
format = "text"

[logging.console]
enabled = true
stream = "stdout"

[logging.access]
enabled = true

[[server]]

Un bloque por conjunto de nombres que comparten socket. listen es obligatorio; el resto, opcional.

CampoTipoDefectoNotas
listenstringobligatorioIP:puerto o :puerto; cada dirección se abre
server_namestring o listavacíoNombre DNS o *.sufijo; base del SNI y de strict_sni
routeslista de nombresvacíaRutas declaradas para el bloque; un nombre inexistente es error
error_pagestabla estado → rutavacíaClaves string ("404"), valor ruta absoluta; el estado se conserva
proxy_protocoltablaausenteenabled y trusted (CIDR); sin trusted se acepta de cualquier par
http3_listenstringausenteSolo en el primer [[server]]
[server.tls]tablaausenteVer más abajo
[[server]]
listen = "0.0.0.0:8080"
server_name = ["exyonq.dev", "www.exyonq.dev"]
routes = ["site"]
proxy_protocol = { enabled = true, trusted = ["10.0.0.0/8"] }

[server.error_pages]
"404" = "/404.html"
"503" = "/maintenance.html"

[server.tls]

cert y key son obligatorios si la tabla existe. Los campos criptográficos solo se honran en el primer bloque de un listen compartido; declararlos en otro bloque es error, no un ignorado silencioso.

CampoTipoDefectoNotas
certrutaobligatorioPEM con la cadena de certificados
keyrutaobligatorioPEM con la clave privada
strict_sniboolfalseRechaza handshakes cuyo SNI no case con ningún server_name; exige al menos uno
protocolslistavacía (rustls: 1.2 y 1.3)TLSv1.2, TLSv1.3; rustls no soporta versiones anteriores
cipherslistavacía (rustls)Nombres IANA u OpenSSL, o lista separada por :; solo el juego AEAD de rustls
curveslistavacía (rustls)X25519, secp256r1/prime256v1/P-256, secp384r1/P-384
ocsp_staplingrutaausenteRespuesta OCSP en DER; ExyonQ no la descarga del respondedor
must_stapleboolfalseExige ocsp_stapling; sin él, configuración rechazada
[server.tls.acme]tablaausenteVer la sección siguiente
[server.tls]
cert = "/etc/exyonq/tls/exyonq.dev.pem"
key = "/etc/exyonq/tls/exyonq.dev.key"
strict_sni = true
protocols = ["TLSv1.2", "TLSv1.3"]
ciphers = ["TLS13_AES_256_GCM_SHA384", "TLS13_CHACHA20_POLY1305_SHA256"]
curves = ["X25519", "secp256r1"]
ocsp_stapling = "/etc/exyonq/tls/exyonq.dev.ocsp.der"
must_staple = false

[server.tls.acme]

Emisión y renovación automáticas. Solo se honra en el primer [[server]].

CampoTipoDefectoNotas
enabledbooltrue
emailstringobligatorioContacto de la cuenta ACME
domainslistaobligatorioNombres a emitir; el comodín exige dns-01
stagingboolfalseUsa el entorno de pruebas de la CA
challengestring"http-01"http-01 o dns-01
dns_hookstringausenteComando para publicar el TXT; variables EXYONQ_ACME_*
dns_propagation_secsentero15Espera tras publicar el TXT; solo con dns-01
account_pathruta<cert>.acme-account.jsonCredenciales de cuenta, modo 0600
[server.tls.acme]
enabled = true
email = "security@exyonq.dev"
domains = ["exyonq.dev", "www.exyonq.dev"]
challenge = "http-01"

[[route]]

CampoTipoDefectoNotas
namestringobligatorioÚnico en toda la configuración
matchtablaobligatorioVer tabla siguiente
rootruta—Raíz estática; actúa como alias del prefijo
indexstring—Fichero por defecto del directorio
upstreamstring—Nombre de [[upstream]]
fastcgistring—Nombre de [[fcgi_pool]]
redirecttabla—status (302 por defecto) y location
rewritestring o tabla—Ruta literal, o pattern/replacement/flag/when
cachestring—Nombre de [[cache_policy]]
cache_tagslistavacíaEtiquetas para purgar por tag
cache_headerstabla—by_mime: MIME → max_age e immutable
response_headerslistavacíaname, value, always; una entrada por nombre
proxy_set_headerslistavacíaname, value; plantillas con variables
location_rewritelistavacíafrom, to: reescribe Location del backend
fallback_rewritestring—Tras un fallo estático, reintenta una vez esa ruta
try_uri / try_uri_dirbool—Sondean URI como fichero / directorio antes del fallback
htaccessstring"off"overlay exige raíz documental y no admite redirect ni proxy

Campos de match:

CampoTipoDefectoNotas
pathstring""Prefijo; XOR con suffix y regex
suffixstringausenteSufijo literal
regexstringausenteRegex de Rust, máximo 256 bytes
hoststringausenteHost exacto o *.sufijo
headerstringausenteFiltro AND por cabecera
header_valuestringausenteValor exacto; sin él, la cabecera debe existir y no estar vacía
[[route]]
name = "assets"
match = { path = "/assets", host = "exyonq.dev" }
root = "/var/www/exyonq/assets"
response_headers = [{ name = "Strict-Transport-Security", value = "max-age=31536000; includeSubDomains" }]

[route.cache_headers.by_mime]
"image/avif" = { max_age = 31536000, immutable = true }

redirect emite location tal cual, sin plantillas: una ruta por host si quieres conservar el destino. rewrite admite flag = "last" | "break" | "redirect" | "permanent" y condiciones when (path, host, query, header, header_value, todas AND). El reemplazo admite $1–$9, $uri y $host.

[[upstream]]

CampoTipoDefectoNotas
namestringobligatorioÚnico; lo referencian las rutas
targetstring—URI http://host:puerto; XOR con endpoints
endpointslista—address, port, weight, priority, max_fails, fail_timeout_ms
timeout_msentero30000Presupuesto total; máximo 86 400 000
max_connect_retriesentero1Solo 0 o 1
selection_policystringweighted_round_robinleast_conn, power_of_two, consistent_hash, cookie_affinity, source_affinity, header_affinity
failover_policystringpriority_bandso none
sticky_cookiestringEXYONQ_ROUTE con afinidad por cookieNombre de cookie, token HTTP válido
sticky_headerstring—Obligatorio con header_affinity
dns_refresh_msentero0 (off)Re-resolución periódica; máximo 24 h
[upstream.health_check]tablaenabled = falseVer más abajo
health_checkTipoDefectoNotas
enabledboolfalse
interval_msentero5000100..=3600000
timeout_msentero10001..=60000 y ≤ interval_ms
pathstring"/health"Ruta sondeada
healthy_threshold / unhealthy_thresholdentero2 / 31..=32
expect_statuslistavacía (cualquier 2xx)Códigos 200..=599
expect_bodystringausenteSubcadena en los primeros 64 KiB
expect_headerstringausente"Nombre" (presencia) o "Nombre: valor"
[[upstream]]
name = "app"
selection_policy = "least_conn"
timeout_ms = 15000
endpoints = [
  { address = "10.0.1.11", port = 8080, weight = 3 },
  { address = "10.0.1.12", port = 8080, weight = 3 },
]

[upstream.health_check]
enabled = true
path = "/health"
expect_status = [200]

[[fcgi_pool]] (FastCGI / PHP-FPM)

CampoTipoDefectoNotas
namestringobligatorioLo referencia fastcgi en la ruta
addressstringobligatorioSocket Unix o host:puerto
transportstring"unix"unix o tcp
document_rootrutaausenteNecesario para htaccess = "overlay"
max_concurrencyentero161..=4096
max_connectionsentero= max_concurrency1..=max_concurrency
idle_timeout_msentero300001..=3600000
total_timeout_msentero300001..=3600000
checkout_timeout_msentero50001..=600000
[[fcgi_pool]]
name = "php"
address = "/run/php/php-fpm.sock"
transport = "unix"
document_root = "/var/www/html"
max_concurrency = 16
total_timeout_ms = 120000

[modules.compression]

CampoTipoDefectoNotas
enabledboolfalse
min_bytesentero256No comprime respuestas menores
levelentero11..=9; 1 es el más rápido
[modules.compression]
enabled = true
min_bytes = 256
level = 5

El resto de módulos de la misma tabla: [modules.metrics] (enabled, path = /metrics, health_path, scrape_bearer_token obligatorio si se escucha en una dirección no local), [modules.ratelimit] (enabled, requests_per_second = 100, burst = 200, zones, routes) y [modules.real_ip] (enabled, trusted, header = x-forwarded-for; con enabled = true exige al menos un CIDR de confianza).

Módulos y campos de 0.9.0

[modules.gunzip]
enabled = true

[modules.auth.routes.site]
realm = "Admin"
user_file = "/etc/exyonq/admin.htpasswd"

[modules.ratelimit]
enabled = true

[modules.ratelimit.zones.site]
key = "ip"
requests_per_second = 20
burst = 40

[modules.ratelimit.routes]
site = "site"

[modules.sub_filter.routes.site]
rules = [{ pattern = "http://old.example", replacement = "https://new.example" }]

[modules.mirror.routes.site]
destinations = ["/_mirror"]

[modules.slice.routes.site]
size = 1048576

El ejemplo junta los bloques en una sola ruta para mostrarlos; en producción se reparten entre las rutas que los necesiten.

[static.preload]

Límites de la precarga del árbol estático. Un cero en cualquiera de las tres claves desactiva la precarga entera; no significa «sin límite».

CampoTipoDefectoNotas
max_file_bytesentero33 554 432 (32 MiB)Máximo por fichero; techo 256 MiB
max_total_bytesentero268 435 456 (256 MiB)Techo 2 GiB
max_entriesentero4096Techo 1 000 000
[static.preload]
max_file_bytes = 33554432
max_total_bytes = 268435456
max_entries = 4096

[static.encoding_cache] es una caché de compresión en disco, apagada por defecto: enabled, cache_dir (/var/cache/exyonq/static-encoding), level (6), min_bytes (300), max_bytes (10 MiB), max_entries (4096), max_total_bytes (1 GiB), gzip (true) y brotli (false).

[full_page_cache]

CampoTipoDefectoNotas
enabledboolfalse
max_entriesentero10000
max_total_bytesentero67 108 864 (64 MiB)
max_object_bytesentero1 048 576 (1 MiB)
default_ttl_secondsentero30
max_ttl_secondsentero3600
namespaceentero4Debe ser distinto de cero si está habilitada
stale_if_error_secsentero0 (off)Sirve la entrada caducada si el backend falla
private_cookieslistavacíaSi la petición trae alguna, no se cachea
[full_page_cache.key]tablavacíaquery, cookies, headers que entran en la clave
[full_page_cache.bypass]tablavacíacookies, headers que fuerzan bypass
[full_page_cache]
enabled = true
default_ttl_seconds = 300
max_ttl_seconds = 3600
namespace = 4
private_cookies = ["wordpress_logged_in", "wp-postpass"]

[full_page_cache.key]
query = ["page", "p"]

[http3]

CampoTipoDefectoNotas
enabledbooltruePor sí solo no abre socket: hace falta http3_listen
providerstring"s2n" en tiempo de ejecucións2n o quiche; builds solo-Quinn lo ignoran
max_ack_delay_msentero1
request_body_drain_cap_bytesentero65 536
qlogboolfalseTrazas qlog
[http3]
enabled = true
provider = "s2n"

[[server]]
listen = "0.0.0.0:443"
http3_listen = "0.0.0.0:443"
server_name = ["exyonq.dev"]
routes = ["site"]

[server.tls]
cert = "/etc/exyonq/tls/exyonq.dev.pem"
key = "/etc/exyonq/tls/exyonq.dev.key"

http3_listen no se puede combinar con strict_sni, protocols, ciphers, curves, ocsp_stapling ni must_staple, ni con varios [[server]] TLS en el mismo listen: los proveedores QUIC solo consumen cert y key, y el producto prefiere rechazar la configuración antes que ignorarla.

[[map]]

Tabla de equivalencias exactas. Tope de 32 mapas y 64 entradas por mapa.

CampoTipoDefectoNotas
namestringobligatorioSe referencia como $nombre
inputstringobligatorioheader, host, remote_addr, request_uri, scheme
headerstring—Obligatorio si input = "header"
defaultstring""Valor cuando no hay coincidencia
valuestablavacíaClaves exactas
[[map]]
name = "connection_upgrade"
input = "header"
header = "Upgrade"
default = "close"
values = { "websocket" = "upgrade" }

[[route]]
name = "ws"
match = { path = "/socket" }
upstream = "app"
proxy_set_headers = [
  { name = "Connection", value = "$connection_upgrade" },
  { name = "Host", value = "$host" },
]

Las variables disponibles en plantillas son exactamente $host, $remote_addr, $request_uri, $scheme y $<map>. Cualquier otro $… es error de configuración.

[[cache_policy]]

CampoTipoDefectoNotas
namestringobligatorioLo referencia cache en la ruta
ttl_secondsentero30
max_object_bytesentero1 048 576
[[cache_policy]]
name = "html"
ttl_seconds = 60

[[route]]
name = "site"
match = { path = "/" }
upstream = "app"
cache = "html"