El problema fundamental que aborda esta solución es la limitación de recursos en sistemas embebidos de bajo costo, específicamente la memoria RAM, al implementar funcionalidades que requieren grandes conjuntos de datos, como las listas de bloqueo de dominios DNS. Tradicionalmente, los bloqueadores de DNS cargan listas de dominios como cadenas de texto en la RAM, lo que en microcontroladores como el ESP32 requiere módulos PSRAM adicionales, aumentando el costo y la complejidad.
La tesis central es que se puede lograr una funcionalidad similar o superior, con un costo significativamente menor y un menor consumo de recursos, al repensar cómo se almacenan y acceden estos datos. En lugar de almacenar las cadenas de texto completas en RAM, se propone almacenar representaciones compactas (hashes) de los dominios en la memoria flash, que es más abundante y económica, y utilizar algoritmos de búsqueda eficientes adaptados a este medio de almacenamiento.
Este enfoque no es una solución específica para el ESP32-C3, sino una aplicación de principios de optimización de memoria y estructuras de datos que son relevantes en cualquier sistema con restricciones de recursos, desde sistemas embebidos hasta arquitecturas de bases de datos distribuidas que buscan minimizar el footprint de memoria para índices o filtros.
Arquitectura del Sistema
El sistema se compone de un microcontrolador ESP32-C3 que actúa como un servidor DNS. Cuando recibe una consulta DNS, el dominio solicitado se extrae y se somete a una función de hashing FNV-1a. Este hash de 40 bits (5 bytes) se utiliza para realizar una búsqueda binaria eficiente en una tabla de hashes preconstruida y almacenada en la memoria flash del dispositivo. La tabla de hashes está ordenada, lo que permite la búsqueda binaria.
Si el hash del dominio se encuentra en la tabla de flash (hit), el sistema responde con 0.0.0.0, 'sinkholeando' la solicitud. Si el hash no se encuentra (miss), la consulta se reenvía a un resolvedor DNS upstream configurado, y la respuesta se retransmite al cliente original. La lista de bloqueo se construye previamente en un host externo y se carga en la flash del ESP32-C3 como un archivo blocklist.bin.
La persistencia de la configuración y la lista de bloqueo se gestiona a través del sistema de archivos de la flash (SPIFFS o LittleFS). El dispositivo también incluye una interfaz web para gestión, actualizaciones OTA (Over-The-Air) de firmware y de la lista de bloqueo, y un portal cautivo para la configuración inicial de la red WiFi. La seguridad se gestiona con HTTP Basic Auth para endpoints que modifican el estado y un encabezado X-Requested-With para mitigar ataques CSRF.
Flujo de Consulta DNS
- 1 Cliente DNS Envía consulta DNS al ESP32-C3
- 2 ESP32-C3 Extrae el dominio de la consulta
- 3 ESP32-C3 Calcula hash FNV-1a de 40 bits del dominio
- 4 Flash Búsqueda binaria del hash en tabla ordenada
- 5 ESP32-C3 Si hay 'hit': responde 0.0.0.0
- 6 ESP32-C3 Si hay 'miss': reenvía a DNS upstream
- 7 DNS Upstream Resuelve el dominio
- 8 ESP32-C3 Retransmite la respuesta al cliente
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | ESP32-C3 | Microcontrolador principal, ejecuta el servidor DNS y la lógica de bloqueo. vs ESP32 (con PSRAM), ESP32-S3 (con PSRAM) 4 MB de flash, sin PSRAM. |
| storage | Memoria Flash (interna del ESP32) | Almacena la tabla de hashes de dominios bloqueados y el firmware. Utilizada para persistencia de datos. vs PSRAM (para almacenar cadenas de dominios) 0.67 MB para 141k dominios (hashes de 40 bits). |
| networking | DNS | Protocolo principal para la interceptación y resolución de nombres de dominio. Respuesta 0.0.0.0 para dominios bloqueados; reenvío a upstream para no bloqueados. |
| security | HTTP Basic Auth | Autenticación para endpoints de administración que modifican el estado del dispositivo. Credenciales base64, no cifradas (limitación de recursos para TLS). |
| security | X-Requested-With Header | Mitigación de ataques CSRF en la interfaz web. Requiere 'c3-adblock' para endpoints mutables. |
Trade-offs
Ganancias
- ▲ Costo de hardware
- ▲▲ Consumo de RAM
- ▲ Capacidad de lista de bloqueo (dominios)
Costes
- △ Latencia de lookup (frente a RAM)
- △ Riesgo de colisiones de hash
- ▲ Flexibilidad de reglas de bloqueo
python3 tools/build_blocklist.py data/blocklist.bin [SOURCE ...]cp src/secrets.example.h src/secrets.h
// then edit src/secrets.hFundamentos Teóricos
La técnica de almacenar representaciones compactas de datos en lugar de los datos completos para búsquedas eficientes tiene raíces profundas en la informática teórica. El uso de funciones hash para mapear elementos de un conjunto grande a un espacio más pequeño es un concepto fundamental, explorado en estructuras de datos como las tablas hash y los filtros de Bloom. Aunque este proyecto no usa un filtro de Bloom directamente, la idea de usar un hash para una búsqueda rápida con una probabilidad controlada de colisiones (falsos positivos) es análoga.
El 'problema del cumpleaños' (birthday problem) es directamente relevante para la elección del tamaño del hash (40 bits). Este principio estadístico predice la probabilidad de colisiones de hash, informando la decisión de que 40 bits son un 'sweet spot' para 141,000 dominios, ofreciendo una probabilidad de colisión cercana a cero. La búsqueda binaria en un array ordenado es un algoritmo clásico con una complejidad de tiempo logarítmica (O(log n)), lo que lo hace eficiente incluso con un número considerable de entradas, especialmente cuando el acceso a los datos (en este caso, la flash) es relativamente lento pero predecible.