El DNS split-horizon (también llamado split-brain) consiste en que un mismo nombre, por ejemplo www.example.com, devuelva una IP privada a los equipos de tu red interna y la IP pública a cualquier otro cliente. Así el tráfico interno viaja por la red privada en lugar de salir a Internet y volver (hairpin NAT), y los nombres puramente internos no se publican. En este tutorial configurarás BIND9 en Ubuntu 24.04 con dos vistas, internal y external, y comprobarás desde dentro y desde fuera que cada cliente recibe la respuesta correcta.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS que actuará como servidor DNS, por ejemplo un VPS de CubePath, con una IP pública y una IP en una red privada.
  • Un usuario no root con privilegios sudo.
  • Al menos otro equipo en la red privada y un equipo fuera de ella (tu ordenador, por ejemplo) para las pruebas.
  • Conocimientos básicos de registros DNS (A, NS, SOA).

A lo largo de la guía se usan estos valores de ejemplo. Sustitúyelos por los tuyos:

ElementoValor de ejemplo
Dominioexample.com
Red privada10.10.0.0/24
Servidor DNS (ns1)privada 10.10.0.10, pública 203.0.113.10
Servidor web (www)privada 10.10.0.20, pública 203.0.113.20
Intranet (solo interna)10.10.0.30

Paso 1: Instalar BIND9

Instala el servidor BIND y las utilidades de diagnóstico (dig, named-checkconf, named-checkzone):

sudo apt update
sudo apt install bind9 bind9-utils bind9-dnsutils

Comprueba la versión instalada:

named -v
BIND 9.18.x-0ubuntu0.24.04.x-Ubuntu (Extended Support Version) <id:...>

El paquete arranca el servicio automáticamente. En Ubuntu 24.04 la unidad se llama named (bind9 es un alias):

systemctl status named --no-pager
● named.service - BIND Domain Name Server
     Loaded: loaded (/usr/lib/systemd/system/named.service; enabled; preset: enabled)
     Active: active (running)

Paso 2: Preparar named.conf para usar vistas

En Ubuntu, /etc/bind/named.conf incluye tres archivos: named.conf.options, named.conf.local y named.conf.default-zones. Cuando se usan vistas, BIND exige que todas las zonas estén dentro de alguna vista, y named.conf.default-zones define zonas (la raíz, localhost, etc.) a nivel global. Si lo dejas así, named-checkconf fallará con when using 'view' statements, all zones must be in views.

Abre el archivo principal:

sudo nano /etc/bind/named.conf

Deja solo estas dos líneas. El archivo de zonas por defecto lo incluirás más adelante dentro de la vista interna:

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

Paso 3: Configurar las opciones globales

Las opciones globales se aplican a todas las vistas salvo que una vista las redefina. Desactiva la recursión a nivel global para que el servidor no sea un resolver abierto; la activarás solo para los clientes internos.

sudo nano /etc/bind/named.conf.options

Sustituye el contenido por el siguiente bloque:

options {
    directory "/var/cache/bind";

    listen-on { any; };
    listen-on-v6 { any; };

    dnssec-validation auto;

    // Sin recursión por defecto; la vista interna la activa
    recursion no;

    // Nadie puede pedir transferencias de zona
    allow-transfer { none; };

    // No revelar la versión de BIND
    version "none";
};

Paso 4: Definir la ACL y las vistas

Una ACL agrupa las direcciones que BIND considera internas. Las vistas se evalúan en orden y cada consulta entra en la primera cuya condición match-clients coincida, así que la vista interna va primero y la externa, con any, recoge todo lo demás.

Abre named.conf.local:

sudo nano /etc/bind/named.conf.local

Añade la configuración:

acl "internos" {
    localhost;
    10.10.0.0/24;
};

view "internal" {
    match-clients { internos; };

    recursion yes;
    allow-recursion { internos; };

    zone "example.com" {
        type primary;
        file "/etc/bind/zones/db.example.com.internal";
    };

    // Zonas por defecto (raíz, localhost, etc.) necesarias para resolver
    include "/etc/bind/named.conf.default-zones";
};

view "external" {
    match-clients { any; };

    recursion no;

    zone "example.com" {
        type primary;
        file "/etc/bind/zones/db.example.com.external";
    };
};

La ACL predefinida localhost incluye todas las direcciones del propio servidor, también la pública. Tenlo en cuenta al hacer pruebas en el paso 7.

Paso 5: Crear los archivos de zona

Cada vista necesita su propio archivo de zona, aunque el nombre de la zona sea el mismo. Crea el directorio:

sudo mkdir -p /etc/bind/zones

Crea la zona interna, con las IPs privadas y el registro intranet, que solo existirá aquí:

sudo nano /etc/bind/zones/db.example.com.internal
$TTL 300
@       IN  SOA ns1.example.com. hostmaster.example.com. (
                2026092501 ; serial
                3600       ; refresh
                900        ; retry
                604800     ; expire
                300 )      ; TTL negativo

        IN  NS  ns1.example.com.

ns1       IN  A   10.10.0.10
@         IN  A   10.10.0.20
www       IN  A   10.10.0.20
intranet  IN  A   10.10.0.30

Crea la zona externa, con las IPs públicas y sin los nombres internos:

sudo nano /etc/bind/zones/db.example.com.external
$TTL 3600
@       IN  SOA ns1.example.com. hostmaster.example.com. (
                2026092501 ; serial
                3600       ; refresh
                900        ; retry
                604800     ; expire
                3600 )     ; TTL negativo

        IN  NS  ns1.example.com.

ns1     IN  A   203.0.113.10
@       IN  A   203.0.113.20
www     IN  A   203.0.113.20

Un TTL bajo en la vista interna (300 segundos) permite cambiar IPs internas con rapidez; en la externa un TTL más alto reduce consultas desde Internet.

Paso 6: Validar la configuración y reiniciar BIND

Antes de reiniciar, comprueba la sintaxis. named-checkconf no muestra nada si todo es correcto:

sudo named-checkconf

Valida las dos zonas:

sudo named-checkzone example.com /etc/bind/zones/db.example.com.internal
sudo named-checkzone example.com /etc/bind/zones/db.example.com.external
zone example.com/IN: loaded serial 2026092501
OK
zone example.com/IN: loaded serial 2026092501
OK

Reinicia BIND y confirma que ha cargado la zona en ambas vistas:

sudo systemctl restart named
sudo rndc zonestatus example.com IN internal
sudo rndc zonestatus example.com IN external

Cada comando debe mostrar el nombre de la zona, el archivo correspondiente y el serial 2026092501. Si alguno falla, revisa el registro del servicio:

sudo journalctl -u named -n 30 --no-pager

Abre el puerto 53 en UFW, tanto UDP como TCP (TCP se usa para respuestas grandes):

sudo ufw allow 53/udp
sudo ufw allow 53/tcp

Paso 7: Probar las dos vistas

Prueba primero desde un equipo de la red privada. Debe recibir la IP interna:

dig @10.10.0.10 www.example.com +short
10.10.0.20

El nombre intranet también debe resolver, y la recursión debe funcionar para dominios ajenos:

dig @10.10.0.10 intranet.example.com +short
dig @10.10.0.10 cubepath.com +short

Ahora prueba desde un equipo fuera de la red privada, contra la IP pública del servidor:

dig @203.0.113.10 www.example.com +short
203.0.113.20

El nombre interno no debe existir para clientes externos:

dig @203.0.113.10 intranet.example.com
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 41522

Y el servidor no debe resolver dominios ajenos para clientes externos:

dig @203.0.113.10 cubepath.com
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 18830
;; WARNING: recursion requested but not available

Para que los equipos internos usen este servidor, configura 10.10.0.10 como servidor DNS en ellos (en Ubuntu, en la sección nameservers de Netplan) y comprueba con resolvectl query www.example.com.

Paso 8: Mantener las zonas

Con split-horizon mantienes dos copias de la zona. Cada vez que cambies un registro:

  1. Edita el archivo de la vista correspondiente (o los dos, si el registro existe en ambas).
  2. Incrementa el serial del SOA en cada archivo modificado.
  3. Valida con named-checkzone y recarga solo esa zona y esa vista:
sudo rndc reload example.com IN internal
zone reload queued

Solución de problemas

when using 'view' statements, all zones must be in views: alguna zona está definida fuera de las vistas. Normalmente es el include de named.conf.default-zones en named.conf, o una zona antigua en named.conf.local. Muévela dentro de una vista.

Un cliente recibe la vista equivocada: activa el registro de consultas, que muestra la IP de origen y la vista que se aplicó:

sudo rndc querylog on
sudo journalctl -u named -f
client @0x7f... 198.51.100.7#53211 (www.example.com): view external: query: www.example.com IN A +E(0)K (203.0.113.10)

Si la IP de origen no es la que esperabas (por ejemplo, llega por NAT), ajusta la ACL. Desactiva el registro al terminar con sudo rndc querylog off.

file not found o permission denied al cargar la zona: revisa la ruta en named.conf.local y que los archivos sean legibles por el usuario bind (ls -l /etc/bind/zones).

Los cambios no se ven: casi siempre falta incrementar el serial o la respuesta anterior sigue en la caché del cliente hasta que expire el TTL.

Conclusión

Tienes un servidor BIND9 que responde con direcciones privadas a tu red interna, con direcciones públicas al resto de Internet y que solo hace de resolver recursivo para los clientes internos. Como siguientes pasos, puedes añadir un servidor secundario con transferencias firmadas con TSIG, delegar el dominio en tu registrador apuntando a ns1 para que la vista externa sea la autoritativa pública, o firmar la zona externa con DNSSEC.