Arquitectura del core
Un core minimal que enruta mensajes, aplica control de acceso, aísla fallos y deja que los modules hagan todo el trabajo. 15 ficheros fuente. Cero lógica de negocio.
Flujo de mensajes
Toda interacción sigue un único camino a través del core. Un mensaje entra por una interfaz, se enruta por path, se verifica contra la ACL, se despacha al module propietario y se devuelve una respuesta. Los eventos se distribuyen vía pub/sub una vez que el handler completa.
Cliente → Interfaz (CLI/HTTP/TCP) → Core Router → Comprobación ACL → Module Handler → Respuesta
↓
Trace (id, timestamp, hops)
↓
Distribución pub/sub (si método EVENT)
Cada mensaje recibe un ID atómico único en el momento de su creación. El trace lo sigue a través de cada salto — local o federado. Así es como se depura una petición que atraviesa tres nodos y cuatro modules sin adivinar.
Componentes del core
El core completo son 15 ficheros fuente. Cada fichero tiene una única responsabilidad. No hay objetos dios, no hay estado mutable compartido fuera de la hash table, y no hay ningún fichero que «haga un poco de todo».
| Componente | Fichero | Descripción |
|---|---|---|
| Path Router | core_path.c | Hash table O(1) FNV-1a + fallback con comodines |
| Module Loader | core_module.c | dlopen/dlsym, descarga segura con conteo de referencias |
| Message System | core_message.c | Asignación, enrutamiento y liberación con generación de ID atómico |
| Authentication | core_auth.c | Contraseñas SHA-256, API keys, tokens de sesión |
| Event Loop | core_event.c | Wrapper de libev (epoll/kqueue/select) |
| Pub/Sub | core_pubsub.c | Coincidencia de patrones (exacta, comodín, global) |
| Event Registry | core_events.c | Suscripciones a eventos controladas por ACL |
| Wire Protocol | core_wire.c | Serialización binaria para federation |
| File Store | core_store.c | Ficheros INI con escrituras atómicas |
| Multi-Storage | core_storage.c | Registro de proveedores (file+sqlite+psql) |
| Config | core_config.c | Parser INI con secciones por module |
| Hash Table | core_hashtable.c | FNV-1a open-addressing, auto-redimensionado al 75% |
| Handlers | core_handlers.c | Todos los paths /core, /auth, /users, /groups |
| Instance | portal_instance.c | Cableado de instancia + crash isolation |
| Log | core_log.c | Logging con color y marca temporal |
Enrutamiento basado en path
Cuando un module se carga, registra los paths que posee. Estos paths se almacenan en una hash table O(1) usando hashing FNV-1a. Cuando llega un mensaje, el router busca el path completo en la hash table. Si no encuentra coincidencia exacta, recurre progresivamente a paths comodín más cortos hasta encontrar un handler o agotar la búsqueda.
# Path entrante: /iot/resources/devices
/iot/resources/devices → coincidencia exacta (se comprueba primero)
/iot/resources/* → fallback con comodín
/iot/* → fallback con comodín
/* → comodín global (último recurso)
Esto significa que un module puede registrar /iot/* y gestionar todo el tráfico IoT, o registrar sub-paths específicos para un control más granular. La coincidencia exacta siempre gana. Los comodines solo se activan cuando no existe coincidencia exacta. La búsqueda es O(1) para coincidencias exactas y O(profundidad) para el fallback con comodín — en la práctica, los paths rara vez superan los 4 niveles.
ACL basada en etiquetas
El control de acceso en TLS usa etiquetas. Los usuarios pertenecen a grupos, que les otorgan etiquetas. Los paths pueden requerir etiquetas. Cuando llega un mensaje, el router comprueba si el usuario autenticado tiene al menos una etiqueta que coincida con las etiquetas requeridas por el path. Si el path no tiene etiquetas, está abierto a todos.
| Etiquetas del path | Etiquetas del usuario | Resultado |
|---|---|---|
| (ninguna) | (cualquiera) | PERMITIR — path abierto |
| admin | admin, dev | PERMITIR — «admin» coincide |
| admin | dev, viewer | DENEGAR — sin coincidencia |
| admin, dev | dev | PERMITIR — «dev» coincide |
| (cualquiera) | (usuario root) | PERMITIR — root omite todo |
La comprobación es una simple intersección de conjuntos. Sin jerarquías de roles, sin árboles de herencia, sin matrices RBAC. Una etiqueta coincide o no coincide. Esto hace que las decisiones de acceso sean auditables por inspección — puedes leer las etiquetas de un path y las de un usuario y conocer la respuesta sin ejecutar código.
Crash isolation
Un module que falla nunca debe tumbar el core. TLS envuelve cada llamada a un handler de module con sigsetjmp/siglongjmp. Si un module provoca SIGSEGV o SIGBUS, el manejador de señales lo captura, registra el fallo, marca el module como descargado y devuelve una respuesta de error. El core continúa sirviendo al resto de modules sin interrupción.
crash_sig = sigsetjmp(g_crash_jmp, 1);
if (crash_sig == 0) {
rc = module->fn_handle(core, msg, resp); // llamada segura
} else {
LOG_ERROR("MODULE CRASH: '%s' signal %d", mod_name, crash_sig);
resp->status = PORTAL_INTERNAL_ERROR;
module->loaded = 0; // desactivar automáticamente el module caído
}
Tras un fallo, el module se desactiva automáticamente. Puede recargarse con module reload <name> una vez corregido el error. No es necesario reiniciar el core. Ningún otro module se ve afectado. Esta es la diferencia entre «el module IoT ha fallado» y «todo el sistema está caído».
Sistema de eventos
Los modules registran los eventos que pueden emitir. Otros modules (o clientes externos) se suscriben a esos eventos. Cuando se dispara un evento, todos los suscriptores son notificados. El registro de eventos está controlado por ACL — los suscriptores necesitan etiquetas coincidentes para recibir eventos de paths protegidos.
Existen dos mecanismos de entrega:
| Mecanismo | Destino | Cómo funciona |
|---|---|---|
| Callbacks internos | Modules en proceso | Llamada directa a función dentro del proceso del core |
| Notificaciones por fd | Clientes externos | write() al descriptor de fichero del cliente |
Los callbacks internos son síncronos y rápidos — se ejecutan en el contexto del module emisor. Las notificaciones por descriptor de fichero son asíncronas — el evento se serializa y se escribe en la conexión del cliente. Este mecanismo dual permite que un module reaccione a eventos en tiempo real, y que un cliente externo conectado vía TCP o WebSocket reciba los mismos eventos sin necesidad de polling.
Arquitectura de almacenamiento
TLS utiliza un sistema de almacenamiento basado en proveedores. Cuando se modifica un usuario o grupo, el cambio se escribe en todos los proveedores de almacenamiento registrados simultáneamente. Cuando se necesita una lectura, los proveedores se consultan en orden — la primera lectura exitosa gana.
Cambio de usuario → core_storage
→ proveedor file (siempre) → /etc/portal/<name>/users/admin.conf
→ proveedor sqlite (si cargado) → portal.db
→ proveedor psql (si cargado) → PostgreSQL remoto
El proveedor file siempre está presente — está integrado en el core. Los proveedores de SQLite y PostgreSQL son modules que se registran como backends de almacenamiento al cargarse. Esto significa que una instancia autónoma usa ficheros INI por defecto, y añadir una base de datos es un cambio de una línea en la configuración sin ninguna migración.