Hago este artículo para debugear el problema que he conseguido solucionar con Claude. Esto es un resumen hecho por Claude y revisado por mi.

Resumen del problema: fallo de resolución DNS hacia Pi-hole

Síntoma inicial

En el servidor Trubbish (Debian 13.5, con networking + resolvconf), al intentar resolver otro servidor local contra el servidor Pi-hole se obtenía:

;; communications error to X.X.X.X#53: timed out

Esto indicaba un fallo de comunicación con el servidor DNS, no un fallo de resolución (no era un “NXDOMAIN” ni “REFUSED”, sino ausencia total de respuesta).

Proceso de diagnóstico

  1. Descarte de problema en el propio Pi-hole: se comprobó primero si era un problema general de red o específico del hostname, y si la interfaz de escucha de Pi-hole podía estar rechazando la petición (“Allow only local requests”).

  2. Descarte de problema de enrutamiento: ip route, ip addr y ping a Pi-hole funcionaban correctamente, confirmando que Trubbish y Pi-hole estaban en la misma subred sin problemas de capa 3.

  3. Detección de bloqueo real: los comandos nc -zvu/nc -zv al puerto 53 fallaban, mientras que el ping funcionaba (esto apuntaba a un filtrado específico de puerto/protocolo, no de ruta).

  4. Primera pista falsa: Docker. El servidor tenía nftables.service inactivo pero numerosas redes bridge de Docker activas. Se revisó iptables -L, nft list ruleset, confirmando que las reglas de Docker (DOCKER-USER, DOCKER-FORWARD, etc.) no afectaban al tráfico saliente del host hacia la LAN. Docker quedó descartado como causa.

  5. Causa real encontrada: al final del nft list ruleset apareció una tabla inet nordvpn, perteneciente al cliente de NordVPN instalado en Trubbish. Esta tabla contenía reglas explícitas de bloqueo:

    ip daddr @lan_ranges tcp dport 53 drop comment "block to LAN DNS for TCP"
    ip daddr @lan_ranges udp dport 53 drop comment "block to LAN DNS for UDP"
    

    Estas reglas forman parte de la protección anti-fuga de DNS del kill switch de NordVPN, y bloquean cualquier consulta al puerto 53 hacia rangos de red privados (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), sin excepción.

  6. Primer intento de solución (insuficiente): se añadió 192.168.1.0/24 al allowlist de NordVPN (nordvpn allowlist add subnet). Sin embargo, el problema persistió, porque en nftables las reglas se evalúan en orden y la regla de bloqueo de DNS a lan_ranges estaba antes que la regla de aceptación del allowlist, el paquete se descartaba antes de llegar a la excepción. Es decir: el allowlist de NordVPN no exime al tráfico DNS del bloqueo anti-leak, por diseño del cliente.

Solución intermedia probada

Se desactivaron el Firewall y el Kill Switch de NordVPN, y se configuró el DNS del cliente para apuntar directamente al Pi-hole:

nordvpn set firewall off
nordvpn set killswitch off
nordvpn set dns <ip de Pi-hole>

Con esto, dig @<ip de Pi-hole> <servidor local> resolvió correctamente. Sin embargo, se identificó que desactivar el firewall deja inoperativo también el kill switch (depende técnicamente de las reglas de nftables del firewall), exponiendo el tráfico de Trubbish sin protección en caso de caída de la VPN.

Decisión final

Se decidió mantener el Firewall y el Kill Switch de NordVPN activados, y en su lugar resolver el conflicto reinsertando de forma persistente las reglas de excepción en la tabla inet nordvpn cada vez que NordVPN la reconstruye (lo cual ocurre en cada conexión/reconexión, no solo al arrancar nordvpnd). Para ello se implementó un servicio systemd que usa nft monitor para detectar cambios en la tabla y reaplicar automáticamente las reglas de aceptación del puerto 53 hacia el allowlist, sin depender de un hook fijo de inicio del demonio:

Script de aplicación de reglas (/usr/local/bin/nordvpn-dns-fix.sh):

#!/bin/bash
# /usr/local/bin/nordvpn-dns-fix.sh
 
TABLE="inet nordvpn"
 
apply_rule() {
    local chain="$1"
    local rule="$2"
    if ! sudo nft list chain $TABLE "$chain" 2>/dev/null | grep -q "dport 53.*accept.*allowlist-dns-fix"; then
        sudo nft insert rule $TABLE "$chain" $rule
    fi
}
 
# Esperamos a que la tabla exista tras un (re)connect
for i in {1..10}; do
    sudo nft list table $TABLE &>/dev/null && break
    sleep 1
done
 
sudo nft insert rule inet nordvpn output ip daddr @allowlist_subnets tcp dport 53 accept comment \"allowlist-dns-fix\"
sudo nft insert rule inet nordvpn output ip daddr @allowlist_subnets udp dport 53 accept comment \"allowlist-dns-fix\"
sudo nft insert rule inet nordvpn forward ip daddr @allowlist_subnets tcp dport 53 accept comment \"allowlist-dns-fix\"
sudo nft insert rule inet nordvpn forward ip daddr @allowlist_subnets udp dport 53 accept comment \"allowlist-dns-fix\"

Servicio systemd (/etc/systemd/system/nordvpn-dns-watch.service):

[Unit]
Description=Reaplica excepcion DNS tras cambios en tabla nftables de NordVPN
After=network.target nordvpnd.service
 
[Service]
Type=simple
ExecStart=/bin/bash -c '/usr/local/bin/nordvpn-dns-fix.sh; nft monitor | while read -r line; do echo "$line" | grep -q "table inet nordvpn" && /usr/local/bin/nordvpn-dns-fix.sh; done'
Restart=always
RestartSec=2
 
[Install]
WantedBy=multi-user.target

Activación:

sudo systemctl daemon-reload
sudo systemctl enable --now nordvpn-dns-watch.service

Este servicio aplica el fix al arrancar y permanece escuchando eventos del kernel (nft monitor), reinsertando la excepción cada vez que la tabla inet nordvpn se recrea (por connect, reconexión automática, cambio de servidor, etc.), permitiendo así conservar la protección completa del kill switch de NordVPN sin perder la resolución DNS hacia el Pi-hole local.