← Blog

· 19 min de lectura #rust#mqtt#iot#unikernel#código abierto

urMQTT: construimos un broker MQTT que es también un sistema operativo

Notas de desarrollo sobre urMQTT, un unikernel escrito en Rust que es por sí mismo un broker MQTT: un único ELF con su propio TCP/IP, TLS 1.3, broker y SQLite, que arranca desde una sola ISO.

Los dispositivos en vehículos, en aparcamientos y en armarios de electrónica tienen una cosa en común: envían mensajes pequeños, con poca frecuencia, pero son muchos y están siempre conectados. Llevamos años conectándolos a brokers MQTT, y llevamos años con la misma pregunta. ¿Por qué necesitamos un Linux completo, OpenSSL, el runtime de algún lenguaje, SSH, un gestor de paquetes y, solo entonces, el broker, para algo tan sencillo? Cada una de esas capas aporta sus propias vulnerabilidades, consume su propia memoria y añade una pieza más que puede fallar, y una flota de dispositivos solo necesita tres cosas del servidor: que mantenga muchas conexiones casi inactivas, que autentique de forma fiable a cada dispositivo y que transmita los mensajes sin pérdidas.

Así nació urMQTT: un unikernel que es un broker MQTT. Núcleo, TCP/IP, TLS 1.3, MQTT y una base de datos integrada en un único binario de Rust que arranca desde una sola ISO. No hay procesos, ni llamadas al sistema, ni shell. La primera plataforma en la que se construye y se prueba es aarch64, y el mismo código avanza hacia x86_64, tanto por UEFI como por BIOS clásico, y hacia hardware real: el objetivo es una única ISO que arranque en todos ellos. El proyecto es de código abierto, con licencia MIT o Apache 2.0, y en este texto queremos dejar constancia de dónde estamos, de cómo está organizado por dentro y de lo que hemos aprendido por el camino.

Qué significa exactamente "unikernel"

Un broker habitual es un programa que se ejecuta sobre un sistema operativo. urMQTT no tiene ningún sistema operativo debajo: arranca por sí mismo a través del firmware UEFI y del bootloader Limine, configura sus propias tablas de páginas, programa el controlador de interrupciones y el temporizador, encuentra los dispositivos virtio a través de PCIe, obtiene una dirección por DHCP y escucha en un único puerto. Todo lo que normalmente harían el núcleo, los controladores, la pila de red y la aplicación es aquí un único ELF que se ejecuta en el nivel EL1.

La comparación con una instalación típica habla por sí sola:

Broker típico en Linux urMQTT
Servicios expuestos SSH, broker, a menudo también un puerto de métricas o de administración solo TCP 8883 (MQTT sobre TLS 1.3 con mTLS)
Código en la imagen núcleo, userland, libc, OpenSSL, runtime un único ELF de Rust (más SQLite incorporado como dependencia vendorizada)
Gestión remota shell, archivos de configuración, actualizaciones de paquetes ninguna. Una versión nueva es una ISO nueva
Memoria por conexión inactiva kilobytes (búferes de socket, objetos TLS, heap) objetivo ≤ 512 B
Modelo multinúcleo hilos y cerrojos, o un único bucle de eventos un hilo por núcleo, sin estado compartido, procesamiento hasta el final
Integridad del código son posibles mapeos que son a la vez escribibles y ejecutables W^X, tablas de páginas bloqueadas, PAC/BTI

Las prioridades son, por este orden: seguridad, escalabilidad, rendimiento multinúcleo, huella reducida e instalación inmutable. Siempre que dos objetivos entran en conflicto, gana el que está más arriba en esa lista.

Dónde estamos ahora

El proyecto está en fase pre-alfa, pero se han cerrado diecisiete hitos de desarrollo, de M0 a M17. Hoy urMQTT atiende MQTT 3.1.1 sobre TLS 1.3 con certificados de cliente obligatorios, y guarda el estado en SQLite sobre un disco en bruto. En resumen, esto es lo que ya funciona:

  • Núcleo: vectores de excepción propios con informes decodificados, tablas de páginas propias con W^X aplicado, páginas de protección, PAC y BTI donde el procesador los tiene, GICv3 y GICv2, temporizador genérico, SMP hasta 64 núcleos.
  • Detección de hardware a partir de DTB o ACPI en una única descripción de plataforma, con un resultado idéntico desde ambas fuentes.
  • Controladores: PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi y virtio-rng, además de un CSPRNG sembrado con entropía de hardware.
  • Red: DHCPv4, ARP, ICMP e IPv4 estricto propios, y un TCP propio solo para el lado servidor: cookies SYN, rueda de temporizadores jerárquica, NewReno, fusión de ACK, sin estado TIME_WAIT.
  • TLS: solo TLS 1.3, mTLS obligatorio, identidad del dispositivo a partir del CN o del URI del SAN; el handshake lo realiza rustls y, después, una capa de registros propia con AES-128-GCM y ChaCha20-Poly1305. Reanudación de sesión con tickets PSK, lista de revocación, control de admisión ya en el handshake.
  • MQTT: códec sin copias, con fuzzing y probado con clientes mosquitto reales; QoS 0 y 1, comodines, mensajes retenidos, testamento (LWT), sesiones persistentes, ACL limitada a devices/{id}/#, límites de tasa por dispositivo.
  • Almacenamiento: SQLite dentro del núcleo mediante un VFS sobre el disco en bruto, en modo WAL, con comprobación de integridad al arrancar; registro de dispositivos, revocaciones, mensajes retenidos, sesiones persistentes con colas de mensajes para los dispositivos que están desconectados; por cada núcleo, un registro de telemetría independiente con CRC por entrada y recuperación tras una caída.
  • Operación y supervisión: temporizador de vigilancia para los manejadores, comprobación de salud, registro limitado, syslog y statsd sobre UDP, una admisión que sobrevive a las tormentas de reconexión.
  • Entrega: reconstrucciones idénticas bit a bit verificadas en CI, ISO e imagen de disco GPT en bruto, firmas minisign, instrucciones para KVM, Proxmox y Hetzner.

Cada paso está marcado con el hito que lo aportó, y el archivo docs/PROGRESS.md del repositorio es la única fuente de verdad sobre lo que realmente funciona.

La arquitectura en tres imágenes

Todo el diseño está descrito en docs/ARCHITECTURE.md, y las razones de cada decisión importante están registradas como ADR en docs/DECISIONS.md. Aquí tiene un resumen.

Las capas

Capas de urMQTT: desde el broker, pasando por el códec MQTT, TLS, TCP y la red, hasta los controladores virtio y la capa de arquitectura, todo en un único ELF en el nivel EL1.
Capas de urMQTT: desde el broker, pasando por el códec MQTT, TLS, TCP y la red, hasta los controladores virtio y la capa de arquitectura, todo en un único ELF en el nivel EL1.

De arriba abajo: el broker (sesiones, árbol de temas, QoS, mensajes retenidos, testamentos, ACL, límites), el códec MQTT, TLS, TCP, la capa de red y, por último, los controladores. Junto a ese camino principal están el almacenamiento (SQLite y el registro de telemetría) y, debajo de todo, la gestión de memoria, la detección de la plataforma y la configuración, que se traduce a constantes en tiempo de compilación, junto con el certificado de CA incorporado, el certificado del servidor y la clave. El hipervisor con los dispositivos virtio es lo único que queda fuera de la imagen.

Un hilo por núcleo, sin estado compartido

Modelo de ejecución: cada núcleo de datos tiene su propio bucle y su parte del broker, y el núcleo más alto ejecuta únicamente SQLite. Los núcleos se comunican exclusivamente mediante anillos SPSC.
Modelo de ejecución: cada núcleo de datos tiene su propio bucle y su parte del broker, y el núcleo más alto ejecuta únicamente SQLite. Los núcleos se comunican exclusivamente mediante anillos SPSC.

Cada núcleo de datos hace girar su propio bucle con su propia pila TCP, sus estados TLS, una parte del broker (sesiones y árbol de temas), una caché del heap y una partición de telemetría. Los núcleos no comparten ningún estado mutable, solo anillos acotados y sin cerrojos, uno por cada par ordenado de núcleos. El núcleo más alto ejecuta SQLite y nada más.

La tarjeta de red tiene hoy un único par de colas, que mantiene el núcleo 0. Este dirige cada flujo MQTT, según el hash Toeplitz de su cuádruple, hacia uno de los núcleos de trabajo y envía lo que los núcleos de trabajo devuelven. La sesión vive en su núcleo de origen, determinado por el hash del identificador de cliente, de modo que la toma de control, los testamentos y la durabilidad no cruzan nunca de un núcleo a otro. Una conexión que aterriza en otro núcleo conserva allí su TCP y su TLS, y reenvía el MQTT en claro al núcleo de origen con un control de flujo basado en créditos. Cada mensaje enrutado se distribuye además a las demás partes del broker, cada una de las cuales mantiene una réplica de los mensajes retenidos.

Tres reglas mantienen unido este modelo. Procesamiento hasta el final: un paquete recibido se procesa por completo en un único núcleo, desde TCP, pasando por el descifrado de TLS en el sitio, el análisis de MQTT y el enrutamiento, hasta la respuesta cifrada, sin colas de tareas y sin copias a búferes de la aplicación. Sondeo adaptativo: bajo carga las colas de recepción se sondean activamente, y en reposo el núcleo vuelve a las interrupciones y a WFI; los manejadores de interrupciones solo reconocen la interrupción y marcan el trabajo. Temporizador de vigilancia: un pulso de 10 ms por núcleo atrapa a los manejadores que superan su presupuesto.

La vida de un PUBLISH

El recorrido de un mensaje PUBLISH con QoS 1: desde el registro TLS en la tarjeta de red, pasando por el descifrado, la comprobación de ACL y el enrutamiento, hasta el PUBACK fusionado con el ACK de TCP en un solo segmento.
El recorrido de un mensaje PUBLISH con QoS 1: desde el registro TLS en la tarjeta de red, pasando por el descifrado, la comprobación de ACL y el enrutamiento, hasta el PUBACK fusionado con el ACK de TCP en un solo segmento.

El dispositivo envía un registro TLS. La tarjeta de red lo entrega al bucle del núcleo mediante un descriptor, sin copias. El bucle comprueba TCP, descifra TLS en el sitio y analiza MQTT desde el búfer prestado; después, en memoria, comprueba la ACL, el límite de tasa y la ruta por el árbol de temas. Si a otro núcleo le interesa ese tema, recibe un mensaje de enrutamiento con una referencia al contenido y el identificador del tema; la base de datos recibe un registro de telemetría o la actualización de un mensaje retenido, por lotes. La respuesta PUBACK se cifra directamente en el búfer de salida y se fusiona con el ACK de TCP, de modo que al dispositivo le sale un solo segmento. Al otro núcleo, el mensaje le cuesta un cifrado por suscriptor en el momento del envío.

Cuánto cuesta una conexión

El objetivo es como máximo 512 bytes por conexión TLS inactiva, con la aspiración de llegar a 256. Mientras la conexión está en reposo no se guarda nada para ella en búferes: los datos recibidos se procesan directamente desde la trama de red, y los búferes de envío, de registros y de MQTT existen solo mientras haya algo inacabado o sin confirmar. El propio núcleo lo mide: contadores exactos del heap, tanto en bytes solicitados como en los bloques que ocupan, anotados cada diez segundos como heap por encima del nivel base por conexión abierta.

Parte Tamaño Cómo
Bloque de control TCP 96 B banderas empaquetadas, RTT/RTO de 16 bits, ranura en la rueda de temporizadores, sin búferes en reposo
Ranura en la tabla TCP 4–8 B solo el índice de la conexión, tabla llena como máximo a la mitad
Conexión y estado del registro TLS 1.3 152 B secreto y secuencia por sentido (40 B); la clave y el IV se derivan por registro
Conexión MQTT 48 B identidad y testamento en el heap solo mientras existen
Sesión MQTT, forma caliente 64 B una suscripción en línea, identificadores de nodo internados
Suscripción devices/<id>/cmd/# 0 nodos del árbol almacenada de forma relativa al identificador de sesión, común a todos los dispositivos

Medido bajo HVF con cuatro núcleos virtuales, tres de ellos de trabajo, con cada dispositivo manteniendo una conexión mTLS y una suscripción con QoS 1, incluido el redondeo del asignador:

Conexiones Bytes por conexión
960 531 B
9 922 494 B
10 197 474 B

El pool de tramas de red es fijo, de unos 2 MiB, lo que equivale a unos 200 B por conexión con diez mil conexiones y a unos 2 B con un millón. No pudimos medir más allá: la red de usuario de QEMU en macOS dejó de aceptar conexiones en 10 197, aproximadamente en el límite de 10 240 descriptores del proceso de QEMU. La cifra baja a medida que las partes fijas se reparten entre más conexiones, por lo que no crece con su número.

Modelo de seguridad

La seguridad es la prioridad número uno y gana todos los conflictos con los demás objetivos. El modelo tiene varias capas:

  • Un solo puerto. TCP 8883, solo TLS 1.3, certificados de cliente obligatorios firmados por la CA incorporada en la imagen. El identificador de cliente MQTT debe ser igual al identificador de dispositivo del certificado.
  • Sin puntos de entrada. Sin shell, sin entrada por consola, sin SSH, sin API de administración, sin código dinámico, sin SQL accesible por la red y sin sistema de archivos.
  • Seguridad de memoria. Rust, con forbid(unsafe_code) en los paquetes de protocolo; cada bloque unsafe lleva un comentario // SAFETY: que verifica un lint, y ante entradas externas no hay pánicos: clippy prohíbe unwrap, expect, la indexación y panic! en los paquetes de la ruta de datos.
  • Mitigación de la explotación. W^X se aplica mediante la codificación de descriptores y el bit SCTLR_EL1.WXN, y se audita en cada arranque. Tras el arranque, las tablas de páginas son de solo lectura, sin alias escribible. La mitad inferior del espacio de direcciones queda excluida. Cada pila tiene páginas de protección y las excepciones se ejecutan en una pila aparte. PAC y BTI se activan cuando el procesador los admite, y las pruebas de humo lo comprueban.
  • Resistencia al abuso. Cookies SYN; la admisión rechaza nuevas conexiones, antes de cualquier criptografía, cuando el heap está bajo, cuando la base de datos se retrasa, cuando hay demasiados handshakes en curso por núcleo o por dirección de origen, o cuando la tasa de handshakes es demasiado alta. Límites de tasa por dispositivo, colas acotadas, ventana TCP ligada a la contrapresión del broker, registro limitado, temporizador de vigilancia y comprobación de salud.
  • Inmutabilidad. La configuración, la CA y la clave del servidor están incorporadas en la imagen. Cambiar cualquiera de ellas significa una imagen nueva.

El modelo de amenazas está recogido en docs/SECURITY.md.

Almacenamiento sin sistema de archivos

Los datos viven en un disco aparte, virtio-blk o virtio-scsi, en el que no hay sistema de archivos. La disposición es mínima y propia:

LBA 0      [ superbloque: magia · versión de la disposición · uuid · tabla de regiones · CRC-32C ]
LBA 8, 16  [ tabla de tamaños de archivo, ranuras A/B (gana la última válida) ]
LBA 64     [ base principal de SQLite ½ ][ WAL de SQLite ⅛ ][ diario 1 MiB ][ registro de telemetría: 8 particiones … ]
últimos 4 KiB [ espacio para la autocomprobación del dispositivo de bloques al arrancar ]

SQLite (3.54.1 vendorizado, con verificación del resumen en la compilación) se compila dentro del núcleo con clang, sin biblioteca estándar, y utiliza un heap memsys5 fijo. Su VFS asigna tres archivos fijos a regiones del disco. Funciona en modo WAL con locking_mode=EXCLUSIVE, por lo que no necesita un índice en memoria compartida, y con synchronous=FULL; si quick_check encuentra un error al arrancar, el sistema se detiene de forma segura. En la base de datos están el registro de dispositivos, las revocaciones, los mensajes retenidos y las sesiones persistentes con colas de mensajes QoS 1 para los dispositivos que no están conectados.

El núcleo de la base de datos, el que tiene el número más alto, ejecuta SQLite y nada más. El plano de datos le envía los cambios de estado a través de un anillo SPSC sin cerrojos; este los agrupa en una transacción cada 5 ms o cada 512 operaciones y devuelve las confirmaciones por otro anillo. Un disco vacío se formatea en el primer arranque; en cualquier otro caso, nunca se formatea encima.

La telemetría sigue otro camino. Cada PUBLISH aceptado en los temas de storage.telemetry_topics va a la partición del registro de telemetría de su núcleo: un anillo de segmentos con CRC por entrada, salado con la secuencia del segmento, de modo que las pasadas antiguas del anillo no reaparezcan nunca. Los núcleos entregan las entradas a la base de datos mediante su propio anillo, sin copias, y esta las escribe cada 5 ms o cada 256 KiB. Al arrancar, la recuperación conserva cada entrada escrita, recorta la cola incompleta y continúa en un segmento nuevo. La retención es por tamaño, a través del anillo, y por antigüedad, mediante retention_days. Lo verificamos con un millón de entradas y con kill -9 en mitad de la escritura.

Las cifras

La herramienta de carga de tools/loadgen hace tres cosas: una tormenta de handshakes TLS 1.3 con mTLS y CONNECT, conexiones de larga duración con PINGREQ que publican telemetría cada segundo, y tormentas de reconexión. make bench en un Apple M4 Pro bajo HVF da:

Núcleos de trabajo handshakes/s en el invitado escalado a través de la red de usuario de QEMU (conexiones/s)
1 862 1,00× 789
2 1 724 2,00× 636
4 3 438 3,99× 536

La cifra en el invitado, donde el cliente y el servidor están dentro de la misma máquina virtual y cada núcleo de trabajo funciona por su cuenta, muestra que el plano de datos escala de forma lineal. La cifra de red está limitada por la red de usuario de un solo hilo de QEMU, no por el invitado; y así consta en el ADR-032.

Probamos las tormentas de reconexión haciendo que todos los dispositivos se conecten a la vez, repitan con espera los intentos rechazados y luego caigan todos sin DISCONNECT. El parámetro heap_limit_mb= en el modo de desarrollo hace que la admisión considere el heap más pequeño de lo que es, de modo que una tormenta mayor que el heap puede probarse con pocos sockets. En una de esas rondas con 3 000 dispositivos, se conectaron 1 368 en 36 segundos y 5 619 intentos fueron rechazados de forma ordenada, con un límite de 8 MiB; la memoria se recuperó tras la tormenta.

Cómo trabajamos

El proyecto se guía por una especificación escrita, PROMPT.md, con un plan de hitos, y cada hito termina con el mismo ritual: make check y make smoke en verde, un commit con la etiqueta M<n>, una entrada en docs/PROGRESS.md y un ADR para cada decisión que no sea obvia. Las reglas son breves y no negociamos sobre ellas: sin pánicos ante entradas externas, unsafe solo en los paquetes de hardware y FFI y siempre con comentario, sin asignaciones en la ruta caliente, un comentario junto a cada orden de operaciones atómicas, cada límite procede de la configuración, y las piezas provisionales deliberadas fallan de forma ruidosa.

El CI hace más de lo que podríamos hacer a mano: fmt, clippy para el host y para aarch64-unknown-none, typos, actionlint y shellcheck; pruebas en Linux y en macOS sobre arm64; compilación del núcleo para la configuración de desarrollo y la de producción con el tamaño del ELF en el resumen; arranque bajo QEMU con todos los escenarios de la prueba de humo y almacenamiento de los registros serie; cargo-deny para avisos, licencias y fuentes; un minuto de fuzzing por cada uno de los once objetivos y, por la noche, diez minutos por objetivo con un corpus en caché. Además, CodeQL, OpenSSF Scorecard, Dependabot y una publicación con procedencia SLSA. Cada acción de terceros está fijada al SHA completo del commit.

Entendemos la reproducibilidad de forma literal: make repro compila HEAD dos veces en árboles de trabajo nuevos y compara el ELF, la ISO y la imagen en bruto byte a byte. Las versiones publicadas están firmadas con minisign, y los artefactos pueden verificarse con gh attestation verify. La imagen de producción contiene la clave privada del servidor, por lo que la ISO misma es un secreto; las imágenes que acompañan a las versiones en GitHub son de desarrollo, con claves temporales, y sirven solo para probar.

Lo que hemos aprendido

Algunas lecciones que cambiaron el plan:

  • Un TCP propio terminó mereciendo la pena. El prototipo funcionó sobre smoltcp y nos sirvió bien hasta M13. Pero un servidor que mantiene cientos de miles de conexiones no necesita un TCP de cliente, necesita cookies SYN, una rueda de temporizadores y un estado de 96 bytes. En M14 sustituimos smoltcp por una pila propia solo para el lado servidor, con pruebas de pérdida simulada y fuzzing de segmentos.
  • Mida la memoria desde dentro. Las estimaciones por conexión eran inútiles hasta que el núcleo empezó a contar el heap en bloques del asignador y no en bytes solicitados. Solo entonces vimos adónde van realmente los 512 bytes y bajamos hasta 494.
  • La herramienta de pruebas forma parte del producto. La mayor limitación no fue el núcleo, sino la red de usuario de QEMU y el número de puertos efímeros del equipo de desarrollo. Para cien mil conexiones o más se necesita vmnet-shared en macOS o Linux con KVM, una interfaz tap y varias direcciones de cliente.
  • SQLite sin sistema operativo es posible, pero solo con condiciones estrictas: un heap fijo, bloqueo exclusivo sin memoria compartida, un VFS que no sabe de archivos y una admisión que no deja que la base de datos se retrase.
  • Las tormentas de reconexión son el estado normal, no la excepción. Cuando la red móvil se cae y vuelve, miles de dispositivos llaman a la puerta al mismo tiempo. Por eso la admisión rechaza la conexión antes de cualquier criptografía, y cada conexión rechazada cuesta casi nada.

Lo que viene

El trabajo posterior a M17 está planificado en tres direcciones, y el orden entre ellas está recogido en docs/PLAN.md. Nada de esto está construido todavía.

  • Una ISO para varias plataformas. El mismo archivo urmqtt-<env>-<versión>.iso y una sola imagen GPT en bruto que arrancan en UEFI aarch64, UEFI x86_64 y BIOS x86_64, en máquinas virtuales y en hardware real, empezando por un PC Intel. Siempre un appliance con todo dentro, reproducible y firmado.
  • Disco en bruto, sin sistema de archivos. Una especificación del formato a nivel de byte, la herramienta de host urmqtt-disk que comparte los analizadores con el núcleo, alineación de las regiones a 1 MiB, crecimiento del disco con los datos, discard para discos con aprovisionamiento ligero, instantáneas A/B de la base de datos, verificación de bits y, opcionalmente, cifrado AES-256-XTS con AES por hardware.
  • Seguridad demostrable. Revocación y lista de dispositivos permitidos en tiempo de ejecución mediante un canal de control firmado, sin imagen nueva; pruebas diferenciales y de conformidad de nuestro propio TLS y TCP; KASLR, protección de pila, kCFI, MTE y un asignador reforzado; SQLite aislado en el nivel EL0; Secure Boot; SBOM y cargo-vet; y, por último, un paquete para una auditoría externa.

Las plataformas que ofrecen solo dispositivos que no son virtio, como AWS ENA o Azure MANA, por ahora no son compatibles. Raspberry Pi 4 y 5 como anfitrión KVM, Hetzner CAX y Oracle Ampere A1 están documentados y soportados en el código, pero aún no se han probado en un servicio real.

Pruébelo

El repositorio está en github.com/zeljan-alduk/urMQTT. Con Rust con la cadena de herramientas nightly fijada y QEMU, make certs, make run y un cliente mosquitto bastan para el primer PUBLISH sobre mTLS en su propio equipo. La advertencia del principio vale también al final: esto es pre-alfa, y todo lo que funciona figura en docs/PROGRESS.md. Los informes de errores, las preguntas sobre el diseño y los avisos de seguridad, según las instrucciones de SECURITY.md, son bienvenidos. Le informaremos aquí de cada nuevo hito.

Comentarios

Normas de los comentarios

Normas de los comentarios

Los comentarios sirven para compartir experiencias y preguntas sobre los temas del blog. Estas normas mantienen el debate útil para todos.

  1. Cortesía Sin insultos, amenazas, acoso, discurso de odio ni discriminación por ningún motivo. Critique el argumento, no a la persona.
  2. Sobre el tema Un comentario debe referirse a la entrada o al debate que la sigue.
  3. Sin publicidad ni spam Sin anuncios, cadenas de mensajes ni enlaces sin contexto. Los enlaces e imágenes son bienvenidos cuando aportan al debate.
  4. Privacidad de los demás No publique datos personales de otras personas: nombres completos de particulares, direcciones, teléfonos, correos, fotos sin consentimiento.
  5. Derechos de autor Publique solo contenido que pueda compartir. Al citar, indique la fuente.
  6. Una sola identidad Use un solo apodo y no se haga pasar por otra persona.
  7. Confirmación por correo Un comentario se publica solo tras confirmar el enlace enviado por correo. La dirección no se muestra ni se usa para nada más; los comentarios sin confirmar se eliminan a las 48 horas.
  8. Moderación Los comentarios que incumplan las normas se ocultan o eliminan sin aviso, y un debate puede bloquearse. Las decisiones de los moderadores son definitivas.
  9. Responsabilidad El autor es responsable del contenido de su comentario. Los comentarios no reflejan la opinión de ALDO TECH.
  10. Denuncias Denuncie el contenido inapropiado en info@aldo.tech.