Resiliencia y autoreparación
Una red descentralizada solo es tan duradera como su capacidad para recuperarse de fallos parciales. Los servidores se desconectan. Servidores nuevos se unen y deben hacer bootstrap de sus datos. Las particiones de red causan divergencia. Hashiverse aborda esto mediante protocolos de autoreparación que operan sin ningún coordinador central — impulsados por clientes y servidores que detectan inconsistencias y las reparan.
Sharding y distribución de carga
Las redes sociales siguen leyes de potencia. Un pequeño número de usuarios y hashtags concentra la mayoría de los seguidores y genera la mayoría de las publicaciones. En una DHT ingenua, eso significaría que un pequeño número de nodos cargaría con la mayor parte — convirtiéndose en cuellos de botella de rendimiento y objetivos de DDoS de alto valor por el contenido que alojan. Hashiverse aborda esto a nivel de modelo de datos mediante sharding basado en tiempo y frecuencia.
Claves de bucket por época
El contenido de un usuario o hashtag no se almacena bajo una única clave DHT fija. La clave es función del identificador público del usuario (o del texto del hashtag) y de una época temporal. Esto significa que la ubicación DHT de las publicaciones de un usuario se desplaza con el tiempo, repartiendo la responsabilidad de alojar su contenido entre distintos nodos en distintas épocas.
Para usuarios y hashtags de gran volumen, el espacio de claves se subdivide aún
más: cuanto más activa es una identidad en volumen de publicaciones, más
granular se vuelve la subdivisión en buckets. El contenido de un usuario
prolífico se reparte entre más buckets, más pequeños — cada uno alojado por una
región distinta del anillo DHT. Esto se captura en el tipo
BucketLocation, que codifica juntas la identidad, la época y la
granularidad del bucket.
El resultado es que ningún nodo se vuelve permanentemente responsable del tráfico de un usuario muy seguido, y ningún nodo se convierte en un objetivo estable para un DDoS dirigido a silenciar una cuenta o hashtag concretos.
Fuente: buckets.rs —
BucketLocation
Caché impulsado por la demanda
Más allá de la reparación (que arregla datos faltantes a posteriori), Hashiverse implementa un caché impulsado por la demanda para reducir la carga sobre los servidores más cercanos en la DHT para los buckets populares. Servidores intermedios cachean bundles de publicaciones y bundles de retroalimentación en nombre de los nodos más próximos al contenido, sirviendo a los clientes directamente y terminando antes el recorrido por Kademlia. El caché se expande hacia fuera bajo demanda sostenida y se contrae automáticamente cuando el interés cae — autorregulado por completo, sin coordinación necesaria.
Mecanismo de token con umbral de hits
Cada servidor mantiene un caché de bundles y un caché
de retroalimentación (un caché Moka ponderado por tipo). En cada
petición entrante GetPostBundleV1 o
GetPostBundleFeedbackV1, el servidor incrementa un contador de
hits para esa ubicación de bucket. Una vez que el contador alcanza el umbral
(actualmente 10 hits dentro de la ventana de inactividad), el servidor emite un
CacheRequestTokenV1 — un token de corta vida firmado por el
servidor y delimitado a esa ubicación de bucket.
El cliente recoge cualquier token emitido durante su recorrido por Kademlia.
Tras recuperar y verificar con éxito el bundle desde un servidor responsable,
sube de forma asíncrona el bundle a cada servidor emisor de token mediante
CachePostBundleV1 / CachePostBundleFeedbackV1. El
servidor valida el token (firma, expiración, coincidencia de ubicación), parsea
el bundle y lo almacena en el caché en memoria. Los clientes posteriores que
pasen por ese servidor reciben directamente el bundle cacheado, sin continuar
el recorrido hacia dentro.
Un caché de deduplicación en curso (con clave por ID de ubicación, TTL acorde a la vida útil del token) evita que el servidor emita tokens duplicados a varios recorridos concurrentes para el mismo bucket.
Fuente: post_bundle_caching.rs,
post_bundle_feedback_caching.rs
Buckets vivos vs sellados
El caché distingue entre datos vivos y sellados:
- Buckets vivos (época actual, sigue aceptando publicaciones) — cada bundle cacheado lleva una expiración absoluta de tiempo del servidor + 5 minutos. Los bundles solo se sirven mientras estén frescos; las entradas obsoletas se saltan al leer sin desalojarlas, preservando el historial de hits de la ubicación para que el umbral pueda volver a alcanzarse rápido cuando llegue contenido nuevo.
- Buckets sellados (época cerrada, sin nuevas publicaciones) — los bundles cacheados no llevan expiración individual. Solo se desalojan por el time-to-idle a nivel de ubicación (60 segundos sin peticiones) o por el límite de capacidad en bytes de Moka. Mientras un bucket sellado se mantenga caliente, su copia cacheada persiste indefinidamente.
Radio de descubrimiento del lado del cliente
Para aprovechar el caché en expansión, cada cliente mantiene un radio
de descubrimiento por bucket: la distancia XOR al servidor más lejano
que devolvió un bundle correctamente en el recorrido anterior. En el siguiente
recorrido, el PeerIterator salta los servidores más cercanos que
ese radio y empieza a consultar desde la frontera hacia fuera — yendo
directamente adonde se halló el caché por última vez, evitando por completo a
los servidores responsables (los más interiores).
El radio se actualiza tras cada recorrido a la distancia XOR del respondedor positivo más lejano. Si las capas externas del caché ya han caducado y ningún servidor de la frontera responde, el recorrido cae al siguiente par más cercano y el radio se contrae naturalmente para reflejar la profundidad actual del caché. El radio se guarda en un caché con time-to-idle, así que los buckets que no se han visitado desde hace un rato vuelven a empezar desde los servidores responsables en el siguiente acceso.
Desalojo y capacidad del caché
Ambos cachés se ponderan por el tamaño real del bundle en bytes, respaldados por la política de admisión W-TinyLFU de Moka. Las entradas «placeholder» (ubicaciones consultadas pero aún no pobladas con un bundle) usan un peso fijo pequeño para participar en el sketch de frecuencia sin consumir capacidad real. Cuando se alcanza el límite alto de bytes, las ubicaciones menos frecuentemente usadas se desalojan primero. Cada ubicación tiene además un time-to-idle de 60 segundos: una ubicación que deja de recibir peticiones se desaloja en silencio, reseteando su contador de hits y contrayendo el radio efectivo del caché.
El caché de bundles almacena hasta un número configurable de originadores por ubicación (actualmente 5). Si llega un nuevo originador con el cupo lleno, se sustituye el bundle que expira antes. Los bundles sellados (sin expiración) se tratan como si expiraran al final y solo los desplazan otros bundles sellados.
Fuente: post_bundle_caching_shared.rs,
config.rs — SERVER_POST_BUNDLE_CACHE_MAX_BYTES entre otros.
Reparación de bundles de publicaciones
Cuando un cliente recupera publicaciones para un bucket, consulta a varios servidores y recopila sus respuestas. Luego compara: ¿qué servidor tiene publicaciones que a otro le faltan? Por cada par donante-objetivo en el que el donante tiene publicaciones que al objetivo le faltan, el cliente organiza una reparación en dos fases:
- Fase de claim: el cliente pregunta al servidor objetivo
cuáles de las publicaciones del donante necesita, mediante una petición
HealPostBundleClaimV1. El servidor declara qué IDs de publicación le faltan. - Fase de commit: el cliente (o el donante, actuando como
proxy) envía esos bytes específicos al objetivo mediante
HealPostBundleCommitV1.
Esto se ejecuta en tareas en segundo plano lanzadas tras completar la recuperación principal del cliente. El usuario ve su contenido sin esperar a la reparación; la red se repara en paralelo.
Fuente: post_bundle_healing.rs,
test_healing_post_bundles.rs
Reparación de retroalimentación
Las señales de retroalimentación — me gusta, denuncias, no me gusta — también
divergen entre servidores a medida que se propagan por la red. Una denuncia
puede llegar al servidor A pero no al servidor B. La reparación de
retroalimentación se aborda por separado de la reparación de publicaciones
porque la retroalimentación tiene una forma de datos distinta (el registro
EncodedPostFeedbackV1 de 50 bytes) y una regla de fusión distinta.
La regla de fusión para la retroalimentación es: para cada par (post_id,
tipo_de_retroalimentación), conservar la señal con la PoW más alta entre todos
los servidores. El cliente, tras recopilar retroalimentación de varios
servidores, calcula el máximo global e identifica los servidores con señales
más débiles que ese máximo. Luego envía las señales más fuertes a esos
servidores mediante HealPostBundleFeedbackV1.
Es una reparación de una sola fase (sin paso de claim): el registro de retroalimentación es lo bastante pequeño como para que enviarlo incondicionalmente sea más barato que el ida y vuelta de preguntar antes.
Fuente: post_bundle_feedback_healing.rs,
test_healing_post_bundle_feedbacks.rs
Resistencia a DDoS
Hashiverse tiene cuatro capas de protección DDoS que interceptan ataques en puntos progresivamente más tempranos del ciclo de vida de la conexión, desde el filtrado de paquetes a nivel de kernel hasta la limitación de tasa a nivel de aplicación.
Capa de kernel: lista negra ipset
Para ataques sostenidos o severos, el servidor escala al descarte de paquetes
a nivel de kernel mediante el ipset de Linux. Las IPs en lista
negra se añaden a un conjunto de hash con un timeout de cinco minutos;
iptables (o nftables) descarta los paquetes coincidentes antes de que lleguen
a la capa de aplicación, eliminando incluso el coste de aceptar y rechazar la
conexión TCP.
ipset create hashiverse_ddos_blacklist hash:ip timeout 300
El servidor en Rust añade IPs a este conjunto mediante llamadas no
bloqueantes Command::new("ipset") — la operación de kernel no
bloquea el manejo de peticiones. Esto requiere CAP_NET_ADMIN o
root en producción, gestionado mediante Linux Ambient Capabilities.
Pre-TLS: tope de conexiones y límite de slots por IP
Cada nueva conexión TCP se comprueba antes de empezar el handshake TLS. Dos guardas se ejecutan en este punto:
- Tope global de conexiones — un semáforo limita el número total de conexiones TCP simultáneas en vuelo. Si se alcanza el tope, el socket se cierra de inmediato; no se consumen recursos del handshake.
- Tope de conexiones por IP — un contador en memoria separado limita cuántas conexiones concurrentes puede mantener una sola IP. Esto evita que una sola dirección monopolice todos los slots disponibles. Las IPs ya en la lista de baneo de la capa de aplicación también se rechazan aquí.
Ambas comprobaciones se disparan antes de llamar a
TlsAcceptor::accept, así que un torrente de nuevas conexiones
desde una sola IP no consume CPU en la negociación TLS. El servidor es
deliberadamente solo IPv4 por ahora; el soporte IPv6 requiere seguimiento a
nivel de prefijo (/64) para ser efectivo, lo que aún no está implementado.
Timeout del handshake TLS
Una vez que una conexión pasa los guardas pre-TLS, el handshake TLS se envuelve en un timeout estricto. Un cliente que envía un ClientHello pero nunca completa el handshake — una conexión lenta clásica a nivel TLS — se cae tras un número fijo de segundos y su puntuación de mala petición se incrementa, acercándolo al baneo de la capa de aplicación.
Defensa Slow Loris: timeouts de lectura de cabeceras y cuerpo
Tras un handshake TLS exitoso, dos timeouts adicionales protegen contra ataques de retención de conexión a nivel HTTP:
- Timeout de lectura de cabeceras — si un cliente HTTP/1.1 no ha terminado de enviar sus cabeceras de petición dentro de la ventana permitida, la conexión se corta. Es la defensa principal contra ataques Slow Loris clásicos, en los que un atacante abre muchas conexiones y envía cabeceras byte a byte para mantener slots abiertos indefinidamente.
- Timeout de lectura del cuerpo — un timeout separado cubre el cuerpo de la petición. Defiende frente a variantes a nivel de cuerpo donde las cabeceras llegan rápido pero el cuerpo se transmite a cuentagotas. Combinado con un tamaño máximo de cuerpo explícito, esto acota tanto el tiempo como la memoria consumidos por petición.
Capa de aplicación: caché de reputación de IP
Las peticiones que pasan todas las anteriores se rastrean por dirección IP usando un caché en memoria Moka. Las IPs que superan el umbral de tasa de peticiones dentro de una ventana deslizante reciben una respuesta de error RPC de Hashiverse y acumulan una puntuación de mala petición. Una vez que la puntuación cruza un umbral, la IP se promociona a la lista negra ipset, cayendo a la capa de kernel para futuras conexiones.
Fuente: hashiverse-server-lib/src/network/transport/,
config.rs
Abstracción TimeProvider
El trait TimeProvider abstrae sobre el tiempo real, habilitando
ScaledTimeProvider — un reloj de simulación con tiempo
acelerado usado en pruebas. Las pruebas de integración que verifican el
comportamiento de reparación sobre ventanas de tiempo que normalmente
llevarían horas pueden ejecutarse en segundos escalando el tiempo. Así es
cómo la suite de pruebas de integración puede verificar el ciclo completo
de reparación — incluyendo la expiración de contenido antiguo — sin
retrasos de reloj de pared.