Problema

En entornos de auto‑hosting es frecuente combinar un contenedor que necesita salir a Internet (por ejemplo, qbittorrent) con una VPN basada en WireGuard. La práctica consiste en montar un archivo wg0.conf dentro del container y delegar la resolución DNS a un resolvedor interno como Unbound. Cuando la configuración parece correcta pero cualquier llamada a curl, ping o la propia aplicación muestra errores de tipo “Could not resolve host”, el contenedor queda sin acceso a la red externa aunque la interfaz wg0 esté UP.

Este patrón aparece en:

  • containers que usan la imagen hotio/qbittorrent (u otras basadas en vpn-client de LinuxServer)
  • despliegues en TrueNAS, Unraid o servidores domésticos con Docker Compose / Dockge
  • configuraciones donde VPN_KEEP_LOCAL_DNS=false y VPN_NAMESERVERS=wg delegan todo el DNS al servidor interno del proveedor VPN

El síntoma principal es la imposibilidad de resolver nombres DNS, mientras que la conectividad de capa 3 (IP) sigue operativa.

Causa

Los fallos de DNS en este tipo de stack suelen deberse a una combinación de factores:

  1. Unbound no escucha en la dirección esperada
    La variable VPN_NAMESERVERS=wg indica al script de inicio que añada el servidor DNS que viene en el bloque [Interface] DNS = … del wg0.conf. Si el proceso unbound se inicia pero no tiene permiso para abrir el puerto 53 en la namespace de red del contenedor, las consultas quedan en timeout.

  2. /etc/resolv.conf no se actualiza
    Algunos scripts sólo sobrescriben resolv.conf cuando VPN_KEEP_LOCAL_DNS=true. Con la opción en false el archivo sigue apuntando a los servidores del host (por ejemplo, 1.1.1.1) que no son alcanzables a través de wg0, provocando la falla.

  3. Rutas predeterminadas mal definidas
    WireGuard añade una ruta 0.0.0.0/0 dev wg0. Si la política de enrutamiento del contenedor mantiene una ruta por defecto a eth0 y la regla de src_valid_mark no está aplicada, el tráfico DNS se envía por la interfaz equivocada y nunca llega al servidor VPN.

  4. MTU incorrecto
    El bloque MTU = 1320 es típico para conexiones PPPoE, pero si la red subyacente tiene un MTU mayor y no se ajusta en el contenedor, los paquetes DNS fragmentados pueden ser descartados sin notificación.

  5. AllowedIPs demasiado amplio sin excepción para la red LAN
    Cuando AllowedIPs = 0.0.0.0/0 se usa sin VPN_LAN_NETWORK o sin rutas estáticas a la subred local, el contenedor intenta resolver DNS a través de la VPN, pero el servidor DNS del proveedor solo responde a peticiones provenientes de la IP del túnel (10.x.x.x). Cualquier petición desde la IP del host (172.x.x.x) es descartada.

Solución

1. Forzar la generación correcta de resolv.conf

Establecer VPN_KEEP_LOCAL_DNS=true obliga al entrypoint a reemplazar el archivo con el DNS del túnel. Si se prefiere mantener los DNS locales, añadir manualmente la línea al Dockerfile o al docker-compose.yml:

environment:
  - VPN_KEEP_LOCAL_DNS=true

2. Verificar que Unbound escucha en 0.0.0.0:53

Dentro del container, ejecutar:

netstat -lnp | grep ':53'

Si no aparece, iniciar Unbound en modo forward con una configuración mínima:

cat > /config/unbound.conf <<'EOF'
server:
  interface: 0.0.0.0
  access-control: 0.0.0.0/0 allow
forward-zone:
  name: "."
  forward-addr: 10.128.0.1
EOF

Reiniciar el servicio (s6-svc -r /var/run/svc.d/unbound o recrear el container).

3. Añadir regla de enrutamiento para tráfico DNS

Crear una regla de marca que obligue al tráfico de origen 10.x.x.x a usar wg0:

ip rule add from 10.0.0.0/8 table 200
ip route add default dev wg0 table 200

En Docker Compose se puede inyectar mediante sysctls o cap_add: - NET_ADMIN.

4. Ajustar MTU si persisten timeouts

Probar con un MTU ligeramente mayor (por ejemplo, 1380) y validar con ping -M do -s 1400 1.1.1.1. Si los paquetes llegan, actualizar el bloque [Interface]:

MTU = 1380

5. Definir explícitamente la red LAN en la variable VPN_LAN_NETWORK

Esto evita que el tráfico a la subred local (por ejemplo, 192.168.0.0/24) sea forzado a la VPN:

environment:
  - VPN_LAN_NETWORK=192.168.0.0/24

Con esta excepción, el contenedor puede seguir resolviendo nombres internos del host si fuera necesario.

6. Reemplazar el script de inicio por una versión que respete VPN_NAMESERVERS=wg

Algunas imágenes usan wireguard-go y un wrapper que solo escribe nameserver 127.0.0.1 cuando UNBOUND_ENABLED=true. Si se desactiva Unbound, el DNS queda sin backend. La solución práctica es:

  • Dejar UNBOUND_ENABLED=true y usar la configuración mínima mostrada en el punto 2, o
  • Cambiar VPN_NAMESERVERS a una lista de servidores públicos (1.1.1.1,8.8.8.8) y desactivar la inserción automática del DNS del túnel.

Cuándo aplicar esta solución

Utilice este conjunto de pasos cuando:

  • El contenedor muestra “Could not resolve host” pese a que wg0 está UP.
  • Los logs del contenedor indican [VPN] [IPV4] IP lookup failed!.
  • La variable UNBOUND_ENABLED está en false pero VPN_NAMESERVERS=wg está presente.
  • Se necesita que todo el tráfico (incluido DNS) salga por la VPN sin perder acceso a la LAN.

No es necesario aplicar todo el checklist si:

  • El contenedor ya resuelve DNS correctamente usando servidores externos.
  • La VPN se configura en modo split‑tunnel y el DNS local no está dentro del túnel.
  • Se usa una imagen que gestiona DNS de forma diferente (por ejemplo, gluetun).

Código

# docker-compose.yml (fragmento esencial)
services:
  qbittorrent:
    image: ghcr.io/hotio/qbittorrent
    container_name: qbittorrent
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - PUID=568
      - PGID=568
      - TZ=America/New_York
      - VPN_ENABLED=true
      - VPN_CONF=wg0
      - VPN_PROVIDER=generic
      - VPN_LAN_NETWORK=192.168.0.0/24
      - VPN_KEEP_LOCAL_DNS=true
      - VPN_NAMESERVERS=wg
      - UNBOUND_ENABLED=true          # activar Unbound
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    volumes:
      - ./configs/:/config
      - /mnt/tank/library:/media
      - /mnt/tank/downloads/hotio-qbit:/downloads
# /config/unbound.conf (mínimo)
server:
  interface: 0.0.0.0
  access-control: 0.0.0.0/0 allow
forward-zone:
  name: "."
  forward-addr: 10.128.0.1

Verificación

  1. Comprobar resolvers

    cat /etc/resolv.conf
    # debe contener 10.128.0.1 o 127.0.0.1 si Unbound está activo
    
  2. Test de DNS interno

    dig @127.0.0.1 ip.me +short
    # debe devolver una IP pública
    
  3. Ping externo

    ping -c 3 1.1.1.1
    
  4. Comprobar ruta predeterminada

    ip route show default
    # la tabla debe indicar dev wg0
    
  5. Revisar logs

    docker logs qbittorrent 2>&1 | grep -i 'unbound\|vpn'
    # ausencia de “IP lookup failed” confirma la solución
    

Notas adicionales

  • En TrueNAS SCALE, la red del host a veces impone su propio dnsmasq. Asegúrese de que el contenedor no herede /etc/resolv.conf del host montándolo como tmpfs o usando read_only: true en la sección de volúmenes.
  • Si el proveedor VPN usa DNS sobre TLS, Unbound necesita la opción forward-tls-upstream: yes y los certificados del servidor.
  • Cuando se usa docker compose up --detach, los cambios en unbound.conf requieren recrear el container (docker compose up -d --force-recreate).
  • Mantenga el archivo wg0.conf actualizado; cualquier cambio en la IP del servidor DNS del proveedor obliga a reiniciar el contenedor para que el script vuelva a escribir resolv.conf.

Con estos ajustes, la mayoría de los despliegues que combinan Docker, WireGuard y Unbound recuperan la capacidad de resolución DNS y vuelven a descargar torrents o a ejecutar cualquier cliente que dependa de nombres de dominio.