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:
| Elemento | Valor de ejemplo |
|---|---|
| Dominio | example.com |
| Red privada | 10.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.
Notasi tu red interna usa otras subredes (varias redes privadas, una VPN), añádelas a la ACL
internos. Si los clientes internos salen a Internet a través de un NAT y llegan al servidor con una IP pública, esa IP también debe estar en la ACL.
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
Importanteno hagas la prueba externa desde el propio servidor DNS. Aunque consultes su IP pública, el origen de la consulta es una dirección local, coincide con la ACL
localhosty recibirás siempre la respuesta interna.
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:
- Edita el archivo de la vista correspondiente (o los dos, si el registro existe en ambas).
- Incrementa el serial del SOA en cada archivo modificado.
- Valida con
named-checkzoney 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.
