Configuración de DNS Split-Horizon en Linux

DNS split-horizon (también llamado split-brain DNS) es una técnica que permite servir diferentes respuestas DNS para el mismo nombre de dominio dependiendo del origen de la consulta, mostrando IPs internas a los clientes de la red corporativa e IPs públicas a los clientes externos. Esta arquitectura es esencial en entornos empresariales donde los servidores tienen tanto una dirección IP privada (para comunicación interna) como una pública, evitando el hairpin NAT innecesario. Esta guía cubre la implementación con BIND9 usando vistas (views).

Requisitos Previos

  • Servidor Linux (Ubuntu 22.04/Debian 12 o CentOS 9/Rocky 9)
  • BIND9 instalado
  • Al menos dos subredes: red interna y tráfico externo
  • Comprensión básica de zonas DNS y registros DNS
  • Acceso root al servidor

Instalación de BIND9

# Ubuntu/Debian
apt-get update
apt-get install -y bind9 bind9utils bind9-doc

# CentOS/Rocky Linux
dnf install -y bind bind-utils

# Verificar la versión instalada
named -v

# Habilitar el servicio
systemctl enable --now named  # CentOS
# o
systemctl enable --now bind9  # Ubuntu/Debian

# Estructura de archivos de BIND
ls /etc/bind/           # Ubuntu/Debian
ls /etc/named.conf*     # CentOS/Rocky

Arquitectura de Vistas DNS

Con split-horizon, BIND evalúa la dirección IP origen de cada consulta y aplica la vista correspondiente:

Red Interna (10.0.0.0/8)
  → Vista "internal"
    → empresa.com → 10.0.1.10 (IP privada del servidor web)
    
Internet (cualquier otra IP)
  → Vista "external"
    → empresa.com → 93.184.216.34 (IP pública del servidor web)

Cada vista tiene su propio conjunto de zonas y puede configurarse con distintos servidores upstream y opciones de resolución.

Configuración de ACLs

Define las ACLs para identificar clientes internos y externos:

# En Ubuntu/Debian: /etc/bind/named.conf
# En CentOS/Rocky: /etc/named.conf

cat > /etc/bind/named.conf.acls << 'EOF'
// Definición de ACLs para split-horizon DNS

// Clientes internos: redes privadas de la empresa
acl "clientes-internos" {
    localhost;            // El propio servidor DNS
    10.0.0.0/8;          // Red corporativa privada clase A
    172.16.0.0/12;       // Redes privadas adicionales
    192.168.0.0/16;      // Redes de oficinas remotas
};

// Clientes externos: todo lo que no es interno
acl "clientes-externos" {
    !10.0.0.0/8;         // Excluir red interna
    !172.16.0.0/12;
    !192.168.0.0/16;
    !localhost;
    any;                  // Todos los demás
};
EOF

Incluye las ACLs en la configuración principal:

# /etc/bind/named.conf (Ubuntu) o /etc/named.conf (CentOS)
# Añadir al principio del archivo:
include "/etc/bind/named.conf.acls";

Configuración de Vistas Interna y Externa

cat > /etc/bind/named.conf.views << 'EOF'
// Vista interna: para clientes de la red corporativa
view "internal" {
    match-clients { clientes-internos; };
    
    // Recursión habilitada para clientes internos
    recursion yes;
    
    // Servidores de reenvío para dominios externos (usando DNS interno de la empresa)
    forwarders {
        10.0.0.1;   // DNS primario corporativo
        10.0.0.2;   // DNS secundario corporativo
    };
    
    // Incluir las zonas internas
    include "/etc/bind/named.conf.zones.internal";
    
    // Incluir zonas de resolución inversa privadas
    zone "0.0.10.in-addr.arpa" {
        type master;
        file "/etc/bind/zones/db.10.0.0.reverse";
    };
};

// Vista externa: para clientes de Internet
view "external" {
    match-clients { clientes-externos; };
    
    // Sin recursión para clientes externos (evitar ser resolver abierto)
    recursion no;
    
    // Solo servir nuestras zonas autorizadas a clientes externos
    include "/etc/bind/named.conf.zones.external";
    
    // Denegar transferencias de zona desde Internet
    allow-transfer { none; };
};
EOF

# Incluir las vistas en la configuración principal
# Añadir a /etc/bind/named.conf:
# include "/etc/bind/named.conf.views";

Definición de Zonas por Vista

Crea los archivos de zona para cada vista:

# Archivo de zonas para la vista INTERNA
cat > /etc/bind/named.conf.zones.internal << 'EOF'
// Zona interna de empresa.com - devuelve IPs privadas
zone "empresa.com" {
    type master;
    file "/etc/bind/zones/db.empresa.com.internal";
    allow-transfer { 10.0.0.2; };  // Solo al servidor DNS secundario
};
EOF

# Archivo de zona interna con IPs privadas
mkdir -p /etc/bind/zones
cat > /etc/bind/zones/db.empresa.com.internal << 'EOF'
$ORIGIN empresa.com.
$TTL 300

@   IN  SOA ns1.empresa.com. admin.empresa.com. (
            2024011501 ; Serial
            3600       ; Refresh
            900        ; Retry
            604800     ; Expire
            300 )      ; TTL mínimo

; Servidores de nombres
@   IN  NS  ns1.empresa.com.

; APUNTAR A IPS INTERNAS PRIVADAS
ns1     IN  A   10.0.0.10   ; El propio servidor DNS
www     IN  A   10.0.1.10   ; IP interna del servidor web
api     IN  A   10.0.2.10   ; IP interna del servidor API
mail    IN  A   10.0.4.10   ; IP interna del servidor de correo
vpn     IN  A   10.0.5.10   ; Servidor VPN interno

; Alias internos
app     IN  CNAME   api.empresa.com.
intranet IN A       10.0.1.20  ; Solo accesible desde la red interna
gitlab  IN  A       10.0.1.30  ; GitLab privado interno
EOF

# Archivo de zonas para la vista EXTERNA
cat > /etc/bind/named.conf.zones.external << 'EOF'
// Zona externa de empresa.com - devuelve IPs públicas
zone "empresa.com" {
    type master;
    file "/etc/bind/zones/db.empresa.com.external";
    allow-transfer { none; };
};
EOF

# Archivo de zona externa con IPs públicas
cat > /etc/bind/zones/db.empresa.com.external << 'EOF'
$ORIGIN empresa.com.
$TTL 3600

@   IN  SOA ns1.empresa.com. admin.empresa.com. (
            2024011501 ; Serial
            3600       ; Refresh
            900        ; Retry
            604800     ; Expire
            3600 )     ; TTL mínimo (mayor para externos)

; Servidores de nombres públicos
@   IN  NS  ns1.empresa.com.
@   IN  NS  ns2.empresa.com.

; APUNTAR A IPS PÚBLICAS
ns1     IN  A   93.184.216.10   ; IP pública del servidor DNS
ns2     IN  A   93.184.216.11   ; IP pública del DNS secundario
@       IN  A   93.184.216.34   ; IP pública principal de la empresa
www     IN  A   93.184.216.34   ; Servidor web público
api     IN  A   93.184.216.35   ; API pública
mail    IN  A   93.184.216.36   ; Servidor de correo público
@       IN  MX  10 mail.empresa.com.
@       IN  TXT "v=spf1 mx -all"

; Servicios internos NO visibles externamente (sin registro)
; intranet - NO aparece aquí intencionalmente
; gitlab   - NO aparece aquí intencionalmente
EOF

Configura la configuración principal de BIND:

cat > /etc/bind/named.conf << 'EOF'
// Configuración principal de BIND9 con split-horizon

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.acls";
include "/etc/bind/named.conf.views";
// No incluir named.conf.local aquí (las zonas van en las vistas)
EOF

# Configuración de opciones globales
cat > /etc/bind/named.conf.options << 'EOF'
options {
    directory "/var/cache/bind";
    
    // Escuchar en todas las interfaces
    listen-on { any; };
    listen-on-v6 { any; };
    
    // No permitir recursión por defecto (se define por vista)
    recursion no;
    
    // DNSSEC
    dnssec-validation auto;
    
    // Estadísticas
    statistics-file "/var/log/named/named_stats.txt";
};
EOF

# Verificar la configuración antes de reiniciar
named-checkconf /etc/bind/named.conf
named-checkzone empresa.com /etc/bind/zones/db.empresa.com.internal
named-checkzone empresa.com /etc/bind/zones/db.empresa.com.external

# Reiniciar BIND con la nueva configuración
systemctl restart bind9  # Ubuntu
# systemctl restart named  # CentOS

Estrategias de Prueba

# Verificar la vista interna (simular consulta desde la red interna)
# Desde un cliente en la red 10.0.0.0/8:
dig @IP-servidor-DNS www.empresa.com
# Debe devolver: 10.0.1.10

# Verificar la vista externa (simular consulta desde Internet)
# Usando la IP externa del servidor:
dig @IP-publica-servidor-DNS www.empresa.com
# Debe devolver: 93.184.216.34

# Forzar una vista específica con bind9 (debug desde el servidor)
# Ver qué vista se aplica para una IP origen
rndc status

# Ver estadísticas del servidor
rndc stats
cat /var/log/named/named_stats.txt | grep -A5 "View"

# Prueba completa desde el propio servidor DNS
# (localhost siempre usa la vista interna)
dig @127.0.0.1 www.empresa.com     # Debe ser IP interna
dig @IP-pública www.empresa.com    # Debe ser IP externa

# Verificar que la intranet NO es visible desde fuera
# Desde una IP externa:
dig @IP-publica-servidor-DNS intranet.empresa.com
# Debe devolver NXDOMAIN

Solución de Problemas

# Ver logs de BIND en tiempo real
journalctl -u bind9 -f     # Ubuntu
journalctl -u named -f     # CentOS

# Aumentar el nivel de log para debug (temporalmente)
rndc querylog on    # Activar log de consultas
tail -f /var/log/syslog | grep named

# Verificar que las zonas se cargaron correctamente
rndc reload
rndc zonestatus empresa.com in internal    # Estado de zona en vista interna
rndc zonestatus empresa.com in external    # Estado de zona en vista externa

# Reload suave (sin reiniciar el servicio)
rndc reload

# Error común: "no matching view found"
# Verificar que las ACLs cubren todos los clientes y no hay solapamiento
# La directiva match-clients se evalúa en orden

# Error: "zone already defined"
# Cada zona debe definirse dentro de su vista, no fuera

# Comprobar sintaxis de ACL
named-checkconf -p | grep acl   # Mostrar configuración parseada

rndc querylog off   # Desactivar log de consultas al terminar el debug

Conclusión

El DNS split-horizon con BIND9 es una solución elegante para gestionar la dualidad de direccionamiento interno/externo en infraestructuras empresariales, eliminando el hairpin NAT y mejorando el rendimiento de las comunicaciones internas. Una configuración cuidadosa de las ACLs y el mantenimiento sincronizado de ambas vistas garantizan que los servicios sean accesibles correctamente tanto desde la red corporativa como desde Internet.