La detección de DNS tunneling (a partir del tráfico DNS) es un caso clásico de evasión, porque el “ruido” se disfraza como consultas DNS aparentemente legítimas. Este post adapta el enfoque a vuestro entorno real: AdGuard Home exporta las consultas a JSON y Wazuh-lite aplica reglas heurísticas sobre el FQDN para estimar si hay payload codificado y/o patrones de subdominios profundos, sin necesidad de tcpdump en el host.
Resumen
Este post adapta la idea del detector “DNS Tunneling” (basado en tráfico DNS) a vuestro entorno real: en vez de capturar paquetes con tcpdump, usamos el pipeline que ya tenéis con AdGuard Home:
- AdGuard registra las consultas DNS en
querylog adguard-to-wazuh.shconvierte esos eventos a JSON y los escribe enadguard-dns.json- Wazuh-lite lee ese JSON con
localfile - Se disparan reglas nuevas de heurísticas de DNS tunneling basadas en patrones del FQDN (
domain)
Con esto, el detector es viable y “plug-and-play” dentro de Wazuh-lite (4.9.2), y además evita hacer sniffing a nivel de host.
Referencia del enfoque original: https://wazuh.com/blog/detecting-dns-tunneling-attacks-with-wazuh/
¿Qué detecta?
No replicamos el enfoque basado en qtype (TXT/NULL) porque el pipeline de AdGuard que exporta a Wazuh-lite no expone ese campo; en su lugar, trasladamos la detección al FQDN, manteniendo la misma lógica de análisis de “payload” que suele aparecer codificado en los nombres.
A cambio, detectamos patrones típicos en el nombre de dominio:
- Etiqueta larga alfanumérica (probable payload codificado)
- Profundidad anómala de subdominios (muchos labels)
- Variante con keywords de túnel (
tunnel,dns-tunnel,googlezip)
Arquitectura
Niños/clients -> (DNS) -> AdGuard Home
-> querylog.json
-> adguard-to-wazuh.sh (JSON)
-> Wazuh-lite (localfile: adguard-dns.json)
-> reglas de dns_tunneling
-> Telegram (solo niñ@s, level >= 7)
-> Grafana (alertas recientes y categorías)
Esquema mínimo del JSON (replicable)
Para que el localfile del pipeline funcione “tal cual”, el log que lee debe ser JSON line-delimited: una línea por evento, donde las claves se convierten en campos accesibles para las reglas.
Las reglas se anclan en:
domain(FQDN sobre el que aplican las heurísticas)is_kid(para decidir si el evento es accionable vía Telegram)
Ejemplo de evento mínimo (anonimizado):
{
"timestamp": "2026-03-19T10:15:23Z",
"source": "adguard",
"client_ip": "X.X.X.X",
"client_name": "KidName",
"is_kid": true,
"domain": "ab12cd34ef56gh78.a.b.c.example.com",
"is_blocked": true,
"reason": "dns_tunnel_test"
}
Reglas añadidas en Wazuh-lite
Se han creado reglas en: zz-adguard-dns-tunneling-rules.xml (directorio de reglas de Wazuh).
Las reglas usan como base el if_sid del “evento AdGuard DNS” existente en adguard-rules.xml (rule id=111001).
Heurísticas (para todos, sin Telegram)
111070(level 4): etiqueta larga alfanumérica endomain(min 16 chars)111071(level 4): FQDN con profundidad >= 4 labels (>= 3 puntos)111072(level 6): encoded + deep (disparo conjunto)111073(level 5): keyword de túnel + deep
Estas reglas no disparan Telegram porque el integration de Telegram (custom-telegram.py) exige level >= 7.
Alertas (solo niñ@s, con Telegram)
111074(level 10): encoded + deep yis_kid=true111075(level 9): keyword + deep yis_kid=true
Se disparan para is_kid=true, y por tanto aparecen en:
- Telegram (por el
custom-telegram.py, MIN_LEVEL=7) - Grafana en “Alertas Recientes (nivel >= 7)” (panel ya existente)
Regex reales que estáis usando ahora
En estas heurísticas se aplican regex (tipo PCRE2) sobre el campo domain que llega desde adguard-dns.json.
El core de las señales que componen las reglas (y que luego se combinan para 111072/111073 y se elevan para 111074/111075) queda así:
<rule id="111070" level="4">
<if_sid>111001</if_sid>
<field name="domain" type="pcre2">(?:^|\.)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{16,}(?:$|\.)</field>
</rule>
<rule id="111071" level="4">
<if_sid>111001</if_sid>
<field name="domain" type="pcre2">^(?:[^.]+\.){3,}[^.]+$</field>
<field name="domain" type="pcre2">(?:^|\.)[A-Za-z0-9]{12,}(?:$|\.)</field>
</rule>
<rule id="111072" level="6">
<if_sid>111001</if_sid>
<field name="domain" type="pcre2">(?:^|\.)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{16,}(?:$|\.)</field>
<field name="domain" type="pcre2">^(?:[^.]+\.){3,}[^.]+$</field>
</rule>
<rule id="111073" level="5">
<if_sid>111001</if_sid>
<field name="domain" type="pcre2">^(?:[^.]+\.){3,}[^.]+$</field>
<field name="domain" type="pcre2">(?i)(tunnel|dns-tunnel|googlezip)</field>
</rule>
Qué tenéis que mirar (detalle que evita confusiones):
- En
111070(etiqueta “larga/encoded”), el match exige una etiqueta con 16+ caracteres alfanuméricos contiguos y además presencia de al menos una letra y al menos un dígito. - En
111071(deep), se exige primero “profundidad” (>= 4 labels / >= 3 puntos) y luego que exista al menos una etiqueta >= 12 caracteres. - En
111072(encoded + deep) combinamos el patrón de111070con la profundidad de111071, pero sin exigir el filtro extra de “>=12 chars” (solo el “>=16” de la etiqueta encoded). - En
111073(deep + keyword) la profundidad va acompañada por keywords típicas del tunneling. - Si las etiquetas que os llegan contienen guiones (
-) dentro, esas etiquetas pueden dejar de matchear porque los patrones usanA-Za-z0-9(no contempla-).
Cómo se ve en Grafana (qué paneles usar)
En wazuh-agentes-updated.json ya tenéis:
“Alertas Recientes (nivel >= 7)”
- Aquí veréis las reglas para niñ@s (
111074,111075) al tenerlevel >= 7.
- Aquí veréis las reglas para niñ@s (
“Top Categorías”
- Este panel usa agregado por
groupsy cuenta también niveles bajos. - Por eso, aunque “heurísticas para todos” (level 4/5/6) no disparen Telegram, sí se reflejarán en las categorías incluyendo
dns_tunneling.
- Este panel usa agregado por
Ajuste de falsos positivos
Ajustar falsos positivos no es “si queda tiempo”: es la parte que convierte heurísticas en un detector útil.
Ejemplo práctico de “ignore” por dominios conocidos
Por ejemplo, CDNs o dominios grandes pueden generar labels largas y parecerse a payload codificado:
<rule id="111080" level="0">
<if_sid>111070,111071,111072,111073</if_sid>
<match>google.com|gstatic.com|microsoft.com</match>
<description>Ignore known legit domains</description>
</rule>
Qué tenéis que asumir:
- Los CDNs generan patrones que pueden activar “deep” y “labels largas” sin ser tunneling.
- Los tracking domains a veces se parecen a “payload” porque usan IDs en el subdominio.
- SIEMPRE hay ruido: hay que filtrar con “ignore” y con correlación, no solo con thresholds por evento.
Correlación opcional por cliente (replicable)
Para evitar falsos positivos, es habitual correlacionar cuando el mismo cliente repite patrones en un intervalo corto en lugar de alertar por “una única query”.
En vuestro pipeline, estas heurísticas se anclan en domain, pero la correlación suele hacerse con un campo estable del evento. El campo estable recomendado es client_ip (si en tu stack usas otro, sustituye el nombre en el ejemplo).
Ejemplo (pseudocriterio) de correlación por client_ip:
<rule id="111090" level="12" frequency="30" timeframe="60">
<if_sid>111072</if_sid>
<same_field>client_ip</same_field>
<description>Possible DNS tunneling (client behavior)</description>
</rule>
Esta regla requiere que los eventos compartan exactamente el mismo valor de
client_ip; si el campo cambia (por ejemplo, hostname vs IP) la correlación no funcionará.
Verificación end-to-end (replicable, sin depender de Grafana)
La forma “pro” de validar es comprobar el flujo completo con una señal sintética, sin asumir que el dashboard de Grafana refleja lo correcto.
wazuh-logtest: pasa un evento sintético que contengadomainyis_kid=truey confirma que el match esrule id='111074'.localfile: inyecta el mismo evento (JSON line-delimited) en el fichero que tulocalfileestá leyendo y espera a que Wazuh lo procese.- UI de Wazuh: filtra por
rule.id(111074/111075) o por el grupodns_tunneling.
Si además quieres validar Telegram (opcional):
- revisa que
custom-telegram.pyesté donde Wazuh lo monta (normalmente/var/ossec/integrations/) y que exista dentro del contenedor. - asegúrate de que la dependencia
requestsestá instalada para el contenedor/integración. - evita
parse_mode="Markdown"si el texto tiene caracteres que puedan romper el parseo (típico error 400 por entidades).
Checklist de validación
- Confirmar que el pipeline ya funciona:
- AdGuard exporta al log
adguard-dns.jsonque ya consume Wazuh-lite - Wazuh-lite carga reglas custom desde
etc/rules/
- AdGuard exporta al log
- Generar consultas DNS que cumplan al menos:
- etiqueta alfanumérica >= 16 chars
- y/o profundidad >= 4 labels
- Ejecutar pruebas desde los equipos marcados como
is_kid=true(para ver Telegram) - Esperar el refresco del pipeline (normalmente hasta 5-10 min por el cron de extracción)
- Ver:
- Grafana “Alertas Recientes (nivel >= 7)”
- Telegram (solo si se cumple
is_kid=true)
Pruebas recomendadas (ejemplos)
Ejecutar desde un cliente detrás de AdGuard (sin forzar resolver a 8.8.8.8, para que el query pase por AdGuard).
Instala asn (para forzar resoluciones DNS)
asn es un tool de lookup/OSINT que, cuando le pasas un hostname, lo resuelve y así genera consultas DNS que vuelven a pasar por tu resolver (y por tanto por AdGuard si el cliente está configurado para usarlo).
Requisitos y dependencias (para Debian/Ubuntu; adapta si usas otra distro):
sudo apt -y install curl whois bind9-host mtr-tiny jq ipcalc grepcidr nmap ncat aha
Instalación rápida del script (método manual):
curl "https://raw.githubusercontent.com/nitefood/asn/master/asn" > /usr/bin/asn && chmod 0755 /usr/bin/asn
encoded + deep
- Usa un label largo alfanumérico y 3+ subdominios (ASN resuelve el hostname):
asn -n a1b2c3d4e5f6g7h8.a.b.c.example.com
keyword + deep
- Incluye la keyword
tunnel(ASN resuelve el hostname):asn -n tunnelabc1234567890.a.b.c.example.com
Nota para pruebas: el detector se dispara por el domain (FQDN) que llega al pipeline. asn puede obtener NXDOMAIN/NOERROR según la red/recursión, pero lo importante es que la resolución pase por AdGuard y el evento llegue al querylog con domain/client_ip/client_name correctos (no el status).
Si la petición viene de un cliente con is_kid=true, deberíais ver:
111074o111075
Limitaciones
- No se detecta
qtypeTXT/NULL porque el JSON exportado desde AdGuard no incluye esa información. - La detección se basa en heurísticas del FQDN, lo que puede causar falsos positivos si el tráfico usa patrones similares (por ejemplo, labels largas de
tracking). Por eso:- se usan niveles bajos para “todos” (visibles en Grafana/categorías)
- y se eleva a level >= 7 solo para niñ@s (acción vía Telegram)
Qué se gana (y por qué merece la pena)
- Cubre un caso realista en vuestro setup (AdGuard -> Wazuh-lite).
- Reduce complejidad: no hace falta montar
tcpdump/capturas en el host. - Opera con los datos ya existentes y se integra en vuestro panel/Telegram.
Conclusión
Con estos cambios, el post deja de ser un resumen “bonito” y pasa a ser implementación real:
- Dato real (ejemplo de
adguard-dns.json) - Precisión (regex explícitos y decisiones técnicas)
- Operación (falsos positivos + correlación por cliente)
Eso es lo que normalmente convierte un “hobby” en algo que puedes usar como base de charla interna o incluso para tu portfolio.
