Inicio / TLS / Arquitectura

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.

El core es deliberadamente pequeño. Cada línea de código en el core debe justificar su existencia. Si una funcionalidad puede vivir en un module, debe vivir en un module. El core enruta, carga, protege y traza — nada más.

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».

ComponenteFicheroDescripción
Path Routercore_path.cHash table O(1) FNV-1a + fallback con comodines
Module Loadercore_module.cdlopen/dlsym, descarga segura con conteo de referencias
Message Systemcore_message.cAsignación, enrutamiento y liberación con generación de ID atómico
Authenticationcore_auth.cContraseñas SHA-256, API keys, tokens de sesión
Event Loopcore_event.cWrapper de libev (epoll/kqueue/select)
Pub/Subcore_pubsub.cCoincidencia de patrones (exacta, comodín, global)
Event Registrycore_events.cSuscripciones a eventos controladas por ACL
Wire Protocolcore_wire.cSerialización binaria para federation
File Storecore_store.cFicheros INI con escrituras atómicas
Multi-Storagecore_storage.cRegistro de proveedores (file+sqlite+psql)
Configcore_config.cParser INI con secciones por module
Hash Tablecore_hashtable.cFNV-1a open-addressing, auto-redimensionado al 75%
Handlerscore_handlers.cTodos los paths /core, /auth, /users, /groups
Instanceportal_instance.cCableado de instancia + crash isolation
Logcore_log.cLogging 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.

Sin expresiones regulares. La coincidencia de paths usa búsquedas en hash y acortamiento progresivo. Esto es deliberado — las regex en un hot path son una bomba de latencia. El fallback con comodín es predecible, depurable y rápido.

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 pathEtiquetas del usuarioResultado
(ninguna)(cualquiera)PERMITIR — path abierto
adminadmin, devPERMITIR — «admin» coincide
admindev, viewerDENEGAR — sin coincidencia
admin, devdevPERMITIR — «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.

Root omite todo. El usuario root es la única excepción al sistema de etiquetas. Esto es intencionado — root es para administración, no para lógica de aplicación. Los modules deberían autenticarse con sus propias credenciales, nunca como root.

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».

No sustituye a la calidad. El crash isolation es una red de seguridad, no una excusa. Los modules que fallan repetidamente deben corregirse, no tolerarse. El log registra cada fallo con el nombre del module y el número de señal para un diagnóstico inmediato.

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:

MecanismoDestinoCómo funciona
Callbacks internosModules en procesoLlamada directa a función dentro del proceso del core
Notificaciones por fdClientes externoswrite() 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.

Escritura en abanico, primera lectura gana. Este diseño asegura que todos los proveedores se mantengan sincronizados en escrituras, y que el proveedor más rápido sirva las lecturas. Si el servidor PostgreSQL no está disponible, el proveedor file sigue funcionando. El sistema degrada de forma elegante — nunca falla completamente porque un backend esté caído.