El problema fundamental que zeroserve aborda es la complejidad y la fragmentación de la configuración de servidores web modernos, especialmente cuando se requiere lógica de negocio dinámica. Los servidores tradicionales como Nginx o Caddy ofrecen lenguajes de configuración declarativos que, si bien son potentes para casos de uso estándar, se vuelven engorrosos o insuficientes cuando se necesita lógica programática avanzada. Esto lleva a la "bolting-on" de runtimes de scripting (como Lua en Nginx) o sistemas de plugins, lo que fragmenta la lógica de control y dificulta la comprensión del flujo de una solicitud.
zeroserve propone una unificación radical de la configuración y la lógica programática mediante el uso de eBPF como el único mecanismo de configuración. Al tratar el programa eBPF como la configuración completa, se busca una mayor coherencia, auditabilidad y rendimiento. La elección de eBPF, tradicionalmente asociado con el kernel, pero aquí ejecutado en userspace, es clave para ofrecer un entorno de ejecución seguro, rápido y programable que puede manejar desde el enrutamiento básico hasta la autenticación compleja y el rate limiting en una única pieza de código. Este enfoque simplifica la operación y el despliegue, encapsulando todo el sitio y su lógica en un único tarball.
La relevancia de este enfoque radica en la creciente necesidad de servidores de borde (edge servers) que no solo sirvan contenido estático de manera eficiente, sino que también puedan ejecutar lógica de aplicación ligera y personalizada cerca del usuario, sin la sobrecarga de un microservicio completo o la complejidad de una configuración distribuida. La combinación de alto rendimiento, bajo footprint de memoria y flexibilidad programática lo posiciona como una alternativa interesante para arquitecturas de "serverless edge" o para simplificar la capa de proxy en sistemas distribuidos.
Arquitectura del Sistema
zeroserve se construye sobre una arquitectura de "single-threaded event loop" por instancia, aprovechando la eficiencia de io_uring para todas las operaciones de red y disco. Cada instancia es un proceso ligero que puede coexistir en un mismo servidor, escalando horizontalmente mediante múltiples procesos. La base de su rendimiento radica en el uso extensivo de io_uring (a través del runtime monoio), que permite un modelo de I/O asíncrono y sin copias de datos entre kernel y userspace, similar a sendfile() pero aplicable a escenarios con TLS.
El contenido del sitio web se empaqueta en un único archivo .tar, que zeroserve indexa en memoria al cargar, creando un mapa de path -> byte-range. Los archivos se sirven directamente desde el tarball mediante lecturas de byte-range, sin desempaquetar el contenido al disco. Esto facilita despliegues atómicos y hot-reloads (mediante SIGHUP) sin interrupciones de servicio, actualizando el sitio, los scripts e incluso el material TLS de forma transparente.
La característica central es el motor de scripting eBPF en userspace. Los archivos .c ubicados en .zeroserve/scripts/ se compilan a objetos eBPF en tiempo de empaquetado (usando clang y llc). Estos programas eBPF se cargan en un runtime en userspace (async-ebpf, que integra uBPF para la compilación JIT a código nativo x86-64). La seguridad del sandbox se logra mediante un "pointer cage" que restringe los accesos a memoria del script a su propia arena, eliminando la necesidad del verificador del kernel o privilegios CAP_BPF. El runtime es preemptible, utilizando un temporizador para interrumpir scripts de larga duración y evitar que un script lento bloquee todo el event loop. Los scripts se encadenan en orden alfabético, compartiendo un mapa de metadatos por solicitud, y pueden "short-circuit" la cadena llamando a funciones como zs_respond o zs_reverse_proxy. La superficie de API para los scripts incluye inspección/mutación de solicitudes, criptografía (SHA-256, HMAC-SHA256, base64), manipulación de JSON, rate limiting basado en "token buckets" y soporte para OIDC y AWS SigV4. La terminación TLS se realiza con BoringSSL, soportando TLS 1.3, HTTP/2, Encrypted Client Hello (ECH) y JA4 fingerprinting.
Flujo de Solicitud con Middleware eBPF
- 1 Cliente Envía solicitud HTTPS/2 con TLS 1.3
- 2 zeroserve (io_uring) Recibe solicitud, termina TLS (BoringSSL)
- 3 Motor eBPF (Userspace) Ejecuta scripts en cadena, JIT-compilados, preemptibles
- 4 Script eBPF Inspecciona/muta solicitud, aplica auth/rate limit, etc.
- 5 Script eBPF Decide acción: servir archivo, responder JSON, proxy reverso
- 6 zeroserve (io_uring) Si es archivo: lee de tarball (byte-range), aplica plantillas
- 7 zeroserve (io_uring) Si es proxy: reenvía a backend, reutiliza conexiones
- 8 Cliente Recibe respuesta HTTPS/2
| Capa | Tecnología | Justificación |
|---|---|---|
| networking | io_uring | Mecanismo de I/O asíncrono de alto rendimiento para todas las operaciones de red y disco, minimizando la latencia y el overhead del kernel. vs epoll, kqueue, select/poll |
| security | BoringSSL | Implementación de TLS 1.3 para terminación segura de conexiones, incluyendo características avanzadas como Encrypted Client Hello (ECH). vs OpenSSL, rustls |
| compute | eBPF (Userspace) | Motor de scripting para middleware programático (routing, auth, rate limiting) JIT-compilado a código nativo y ejecutado en un sandbox preemptible. vs LuaJIT, Wasm, JavaScript (V8) --preempt-timer-interval-ms (default 2ms, 10ms para mayor throughput) |
| storage | Tarball (in-memory indexing) | Formato de empaquetado y método de servicio de contenido estático, permitiendo despliegues atómicos y lecturas de byte-range sin desempaquetar. vs Sistema de archivos tradicional (document root), Archivos ZIP, Bases de datos de objetos |
| orchestration | SIGHUP | Mecanismo de señalización para hot-reloads atómicos de sitios, scripts y material TLS sin interrupción del servicio. vs API de control, Recarga basada en inotify |
Trade-offs
Ganancias
- ▲ Rendimiento en archivos estáticos pequeños
- ▲ Rendimiento en middleware programático (eBPF vs Lua)
- ▲ Rendimiento en proxy reverso (respuestas pequeñas)
- ▲▲ Simplicidad de configuración y despliegue
- ▲ Consistencia de la lógica de control
- ▲ Footprint de memoria por proceso
Costes
- △ Rendimiento en proxy reverso (respuestas grandes)
- △ Curva de aprendizaje de eBPF para configuración
- △ Flexibilidad de configuración declarativa compleja
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
char peer[64];
if (zs_req_peer(peer, sizeof(peer)) <= 0) zs_strcpy(peer, "unknown");
zs_meta_set(ZS_STR("visitor"), ZS_STR(peer));
zs_meta_set(ZS_STR("zs.response.header.x-served-by"), ZS_STR("zeroserve-ebpf"));
return 0;
}ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
if (zs_strcmp(path, "/health") != 0) return 0;
zs_meta_set(ZS_STR("zs.response.header.content-type"), ZS_STR("application/json"));
zs_respond(200, ZS_STR("{\"status\":\"ok\"}\n"));
return 0;
}Fundamentos Teóricos
La arquitectura de zeroserve, particularmente su enfoque en io_uring y el modelo de "single-threaded event loop", se alinea con principios de diseño de sistemas de alto rendimiento que buscan minimizar el overhead del kernel y las transiciones de contexto. El uso de io_uring es una evolución moderna de modelos de I/O asíncronos como AIO (Asynchronous I/O) y epoll, buscando una mayor eficiencia al permitir la "batching" de operaciones de I/O y la ejecución de operaciones directamente desde el userspace, reduciendo las llamadas al sistema. Esto resuena con los trabajos de "zero-copy" y "kernel bypass" que han sido objeto de investigación en sistemas operativos y redes durante décadas, buscando reducir la latencia y aumentar el throughput en la transferencia de datos.
El uso de eBPF en userspace, aunque no es su aplicación canónica, se basa en los principios de seguridad y eficiencia de los "sandboxed execution environments". La idea de un lenguaje de programación de propósito especial, compilado JIT a código nativo y verificado para seguridad, tiene paralelos con la investigación en "safe languages" y "virtual machines" ligeras. El concepto de "pointer cage" para la seguridad de memoria es una técnica bien establecida en la protección de sistemas, similar a las "memory protection units" (MPU) en hardware o a las técnicas de "software fault isolation" (SFI) exploradas en papers como "Software-based Fault Isolation" de Wahbe et al. (1993). La preemption de scripts para garantizar la equidad y evitar el "starvation" es un concepto fundamental en la planificación de procesos y threads en sistemas operativos, aplicado aquí a la ejecución de middleware programático.