Split-Horizon DNS Yapılandırması: İç ve Dış Ağlar İçin Farklı DNS Yanıtları
Bir şirkette hem iç ağ hem de dış ağ kullanıcılarına hizmet veriyorsanız, DNS yapılandırması düşündüğünüzden çok daha kritik bir hal alıyor. Klasik yaklaşımla herkes aynı DNS yanıtını alır, ama bu çoğu zaman sorun çıkarır. İç kullanıcılar dışarıya çıkıp tekrar içeri giren trafikle boğuşur, güvenlik açıkları oluşur ve performans berbat olur. İşte tam bu noktada Split-Horizon DNS devreye giriyor.
Split-Horizon DNS Nedir?
Split-Horizon DNS, aynı alan adı için farklı istemci gruplarına farklı DNS yanıtları döndürme tekniğidir. Temel mantık şu: iç ağdaki bir kullanıcı app.sirket.com adresini sorguladığında 192.168.1.50 gibi bir iç IP alırken, dış ağdan biri aynı sorguyu yaptığında 203.0.113.10 gibi bir dış IP alır.
Bu yapıya bazı kaynaklarda Split-Brain DNS veya Split-View DNS de denir. Farklı isimler, aynı konsept.
Neden buna ihtiyaç duyarsınız?
- Hairpin NAT sorunlarını çözmek: İç kullanıcılar dış IP’ye gidip geri dönmek yerine direkt iç IP’ye bağlanır
- İç kaynakları gizlemek: Dış dünya iç sunucularınızın IP adreslerini görmez
- Performans: İç trafik NAT üzerinden geçmez, gecikme azalır
- Güvenlik: Hassas iç servislerin DNS kayıtları dışarıdan sorgulanamaz
Temel Mimari
Yapıyı kafanızda oturtmak için şöyle düşünün: DNS sunucunuz istemcinin nereden geldiğine bakarak farklı “zone view” kullanır. BIND terminolojisinde buna view deniyor. PowerDNS ve Unbound’da da benzer mekanizmalar mevcut.
Tipik bir senaryo:
- İç görünüm (internal view): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 bloklarından gelen sorgulara iç IP’lerle cevap verir
- Dış görünüm (external view): Geri kalan her şeye dış IP’lerle cevap verir
BIND9 ile Split-Horizon Kurulumu
BIND9, bu iş için en yaygın kullanılan sunucu. Ubuntu/Debian üzerinde kurulum:
apt update && apt install -y bind9 bind9utils bind9-doc
systemctl enable --now named
named.conf Yapılandırması
Ana yapılandırma dosyasını düzenleyelim:
# /etc/bind/named.conf.options
acl "internal_networks" {
10.0.0.0/8;
172.16.0.0/12;
192.168.0.0/16;
127.0.0.1;
::1;
};
options {
directory "/var/cache/bind";
recursion yes;
allow-recursion { internal_networks; };
dnssec-validation auto;
listen-on { any; };
listen-on-v6 { any; };
// Dış sorgular için recursion kapalı
allow-query { any; };
allow-transfer { none; };
};
View Tanımlamaları
Asıl sihir burada gerçekleşiyor. named.conf.local dosyasına view’ları ekliyoruz:
# /etc/bind/named.conf.local
view "internal" {
match-clients { internal_networks; };
recursion yes;
// İç zone tanımı
zone "sirket.com" {
type master;
file "/etc/bind/zones/internal/db.sirket.com";
allow-query { internal_networks; };
};
// Reverse zone
zone "168.192.in-addr.arpa" {
type master;
file "/etc/bind/zones/internal/db.192.168";
allow-query { internal_networks; };
};
// Root hints için
include "/etc/bind/named.conf.default-zones";
};
view "external" {
match-clients { any; };
recursion no;
zone "sirket.com" {
type master;
file "/etc/bind/zones/external/db.sirket.com";
allow-query { any; };
};
};
Önemli not: match-clients direktifi sırayla işlenir. İlk eşleşen view kullanılır. Bu yüzden internal view’ı her zaman önce tanımlayın.
İç Zone Dosyası
# /etc/bind/zones/internal/db.sirket.com
$TTL 300
@ IN SOA ns1.sirket.com. admin.sirket.com. (
2024010101 ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
300 ) ; Minimum TTL
; Name servers
@ IN NS ns1.sirket.com.
ns1 IN A 192.168.1.10
; İç servisler - gerçek iç IP'ler
@ IN A 192.168.1.100
www IN A 192.168.1.100
app IN A 192.168.1.50
mail IN A 192.168.1.25
intranet IN A 192.168.1.30
db IN A 192.168.1.75
vpn IN A 192.168.1.1
; Sadece iç ağdan erişilebilen servisler
monitoring IN A 192.168.1.200
jenkins IN A 192.168.1.150
gitlab IN A 192.168.1.160
Dış Zone Dosyası
# /etc/bind/zones/external/db.sirket.com
$TTL 3600
@ IN SOA ns1.sirket.com. admin.sirket.com. (
2024010101 ; Serial
86400 ; Refresh
7200 ; Retry
604800 ; Expire
3600 ) ; Minimum TTL
; Name servers
@ IN NS ns1.sirket.com.
@ IN NS ns2.sirket.com.
ns1 IN A 203.0.113.10
ns2 IN A 203.0.113.11
; Dışarıya açık servisler - dış IP'ler
@ IN A 203.0.113.20
www IN A 203.0.113.20
app IN A 203.0.113.21
mail IN A 203.0.113.22
vpn IN A 203.0.113.1
; MX kayıtları
@ IN MX 10 mail.sirket.com.
; SPF ve diğer TXT kayıtları
@ IN TXT "v=spf1 ip4:203.0.113.0/24 ~all"
; Dikkat: monitoring, jenkins, gitlab burada YOK
; Bu kayıtlar dış dünyaya görünmez
Yapılandırmayı Test Etme
# Syntax kontrolü
named-checkconf /etc/bind/named.conf
# Zone dosyası kontrolü - internal
named-checkzone sirket.com /etc/bind/zones/internal/db.sirket.com
# Zone dosyası kontrolü - external
named-checkzone sirket.com /etc/bind/zones/external/db.sirket.com
# BIND'ı yeniden başlat
systemctl restart named
# İç ağdan test (192.168.1.x üzerinden)
dig @192.168.1.10 app.sirket.com
# Beklenen çıktı: 192.168.1.50
# Dış ağı simüle etmek için farklı source IP (veya dış bir makineden)
dig @203.0.113.10 app.sirket.com
# Beklenen çıktı: 203.0.113.21
Windows DNS Server ile Split-Horizon
BIND her yerde çalışsa da Windows ortamında Active Directory ile entegre DNS kullanıyorsanız durum biraz farklı. Windows DNS Server, view konseptini BIND gibi desteklemez. Bunun yerine birden fazla DNS sunucusu kullanarak ayrıştırma yapılır.
Önerilen mimari: İki ayrı DNS sunucusu
- İç DNS: Active Directory integrated zone, sadece iç ağdan erişilebilir
- Dış DNS: Sadece dış kayıtları olan, DMZ veya cloud’da çalışan bir sunucu
Firewall kuralları ile bunu zorlayabilirsiniz: iç istemciler sadece iç DNS’e, dış istemciler sadece dış DNS’e ulaşabilsin.
Gerçek Dünya Senaryosu: E-ticaret Şirketi
Diyelim ki orta ölçekli bir e-ticaret şirketinin sysadminliğini yapıyorsunuz. Mimari şöyle:
- Web sunucusu: İç IP 192.168.10.50, dış NAT IP 185.100.200.50
- API sunucusu: İç IP 192.168.10.60, dış NAT IP 185.100.200.60
- Admin paneli: İç IP 192.168.10.70, sadece iç ağdan erişilebilir
- Ödeme servisi: İç IP 192.168.10.80, dış NAT IP 185.100.200.80
Bu senaryoda split-horizon olmadan ne olur? Bir iç kullanıcı www.magazam.com yazdığında router dış IP’ye istek gönderir, NAT bunu içeri çevirir, cevap tekrar dışarı çıkar ve içeri döner. Bu “hairpin NAT” veya “NAT loopback” meselesi ciddi gecikme ve bazı router modellerinde doğrudan bağlantı hatası oluşturabilir.
Split-horizon ile iç kullanıcı direkt 192.168.10.50’ye gider, trafik ağ içinde kalır.
# Senaryo için BIND view konfigürasyonu
view "ecommerce_internal" {
match-clients { 192.168.0.0/16; 10.0.0.0/8; };
recursion yes;
zone "magazam.com" {
type master;
file "/etc/bind/zones/internal/db.magazam.com";
};
include "/etc/bind/named.conf.default-zones";
};
view "ecommerce_external" {
match-clients { any; };
recursion no;
zone "magazam.com" {
type master;
file "/etc/bind/zones/external/db.magazam.com";
};
};
Unbound ile Split-Horizon
Bazı ortamlarda Unbound tercih edilir. Unbound’da bu iş biraz farklı çalışır, local-zone ve local-data direktifleri kullanılır:
# /etc/unbound/unbound.conf
server:
interface: 0.0.0.0
access-control: 192.168.0.0/16 allow
access-control: 10.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
# İç zone tanımları
local-zone: "sirket.com." static
local-data: "www.sirket.com. IN A 192.168.1.100"
local-data: "app.sirket.com. IN A 192.168.1.50"
local-data: "mail.sirket.com. IN A 192.168.1.25"
local-data: "monitoring.sirket.com. IN A 192.168.1.200"
# PTR kayıtları
local-data-ptr: "192.168.1.100 www.sirket.com"
local-data-ptr: "192.168.1.50 app.sirket.com"
Unbound genellikle recursive resolver olarak kullanılır ve iç ağ için mükemmeldir. Dış DNS için ayrı bir authoritative sunucu (Bind, PowerDNS veya bulut sağlayıcısı) kullanırsınız.
Sık Yapılan Hatalar ve Çözümleri
Hata 1: Serial Numarası Senkronizasyonu
İç ve dış zone dosyalarının serial numaralarını senkronize tutmak zorunda değilsiniz, ama unutmak ciddi sorunlara yol açar. Bir script ile bunu otomatize edin:
#!/bin/bash
# /usr/local/bin/update-dns-serial.sh
DATE=$(date +%Y%m%d)
COUNTER=01
# Internal zone
CURRENT_SERIAL=$(grep -oP 'd{10}' /etc/bind/zones/internal/db.sirket.com | head -1)
NEW_SERIAL="${DATE}${COUNTER}"
if [ "$CURRENT_SERIAL" -lt "$NEW_SERIAL" ]; then
sed -i "s/$CURRENT_SERIAL/$NEW_SERIAL/g" /etc/bind/zones/internal/db.sirket.com
sed -i "s/$CURRENT_SERIAL/$NEW_SERIAL/g" /etc/bind/zones/external/db.sirket.com
rndc reload
echo "Serial güncellendi: $NEW_SERIAL"
else
echo "Serial zaten güncel"
fi
Hata 2: include Direktiflerinde View Çakışması
Her view içinde include "/etc/bind/named.conf.default-zones"; kullanıyorsanız, bu root zone ve localhost zone’larını içerir. Birden fazla view’da aynı include kullanmak çakışmaya neden olur. Çözüm:
# Her view'da root hints ayrı tanımlanmalı
view "internal" {
match-clients { internal_networks; };
zone "." {
type hint;
file "/usr/share/dns/root.hints";
};
zone "localhost" {
type master;
file "/etc/bind/db.local";
};
zone "sirket.com" {
type master;
file "/etc/bind/zones/internal/db.sirket.com";
};
};
Hata 3: Önbellek Zehirlenmesi Riski
Dış view’da recursion açık bırakmak DNS amplification saldırılarına davetiye çıkarır. Her zaman:
view "external" {
match-clients { any; };
recursion no; # Kesinlikle kapalı olmalı
additional-from-auth no;
additional-from-cache no;
zone "sirket.com" {
type master;
file "/etc/bind/zones/external/db.sirket.com";
};
};
Monitoring ve Doğrulama
Yapılandırmanın doğru çalıştığını sürekli kontrol etmek gerekir. Basit bir kontrol scripti:
#!/bin/bash
# /usr/local/bin/check-split-dns.sh
INTERNAL_DNS="192.168.1.10"
EXTERNAL_DNS="203.0.113.10"
TEST_DOMAIN="app.sirket.com"
EXPECTED_INTERNAL="192.168.1.50"
EXPECTED_EXTERNAL="203.0.113.21"
echo "=== Split-DNS Kontrol ==="
# İç DNS kontrolü
INTERNAL_RESULT=$(dig +short @$INTERNAL_DNS $TEST_DOMAIN 2>/dev/null)
if [ "$INTERNAL_RESULT" = "$EXPECTED_INTERNAL" ]; then
echo "[OK] İç DNS doğru: $TEST_DOMAIN -> $INTERNAL_RESULT"
else
echo "[HATA] İç DNS yanlış! Beklenen: $EXPECTED_INTERNAL, Alınan: $INTERNAL_RESULT"
exit 1
fi
# Dış DNS kontrolü
EXTERNAL_RESULT=$(dig +short @$EXTERNAL_DNS $TEST_DOMAIN 2>/dev/null)
if [ "$EXTERNAL_RESULT" = "$EXPECTED_EXTERNAL" ]; then
echo "[OK] Dış DNS doğru: $TEST_DOMAIN -> $EXTERNAL_RESULT"
else
echo "[HATA] Dış DNS yanlış! Beklenen: $EXPECTED_EXTERNAL, Alınan: $EXTERNAL_RESULT"
exit 1
fi
echo "=== Tüm kontroller başarılı ==="
Bu scripti cron’a ekleyin:
# /etc/cron.d/check-split-dns
*/5 * * * * root /usr/local/bin/check-split-dns.sh >> /var/log/dns-check.log 2>&1
DNSSEC ile Split-Horizon
DNSSEC kullanıyorsanız dikkatli olun. İç ve dış zone’lar ayrı imzalanmalıdır. İç zone’u imzalamanıza gerek olmayabilir, zira iç ağı zaten güvenli kabul ediyorsunuzdur. Ama dış zone mutlaka imzalanmalı:
# Dış zone için DNSSEC anahtarı oluşturma
dnssec-keygen -a RSASHA256 -b 2048 -n ZONE sirket.com
dnssec-keygen -f KSK -a RSASHA256 -b 4096 -n ZONE sirket.com
# Zone'u imzalama
dnssec-signzone -A -3 $(head -c 1000 /dev/random | sha1sum | cut -b 1-16)
-N INCREMENT -o sirket.com
-t /etc/bind/zones/external/db.sirket.com
Bulut Ortamları ile Entegrasyon
Hybrid cloud kullanıyorsanız, AWS Route 53 Private Hosted Zone veya Azure Private DNS gibi servisleri split-horizon için kullanabilirsiniz. AWS tarafında:
- Public Hosted Zone: Dış kullanıcılar için gerçek public IP’ler
- Private Hosted Zone: VPC içindeki kaynaklar için özel IP’ler
Bu yapıda Route 53 Resolver kuralları ile on-premise DNS’inizi bulutla entegre edebilirsiniz. Şirket içi DNS sunucunuz sirket.com için authoritative kalırken, bulut kaynakları için Route 53 devreye girer.
Sonuç
Split-Horizon DNS ilk bakışta karmaşık görünse de pratikte son derece mantıklı ve yönetilebilir bir yapı. Kurulumun en kritik noktaları şunlar: view sıralamasını doğru yapmak, dış view’da recursion’ı kapatmak, zone serial numaralarını düzenli güncellemek ve otomatik monitoring eklemek.
Bu yapıyı doğru kurduğunuzda hairpin NAT sorunları tarihe karışır, iç servisler dış dünyaya görünmez hale gelir ve ağ trafiğiniz optimize edilmiş olur. Özellikle 50 kişinin üzerinde çalışan ve kendi sunucu altyapısı bulunan her şirketin bu yapıyı uygulaması gerekir.
Yapılandırmayı geliştirme ortamında mutlaka test edin, zone dosyalarını versiyon kontrolüne (git) alın ve değişiklikleri otomatize edin. DNS sorunları genellikle gece yarısı ortaya çıkar, önceden sağlam bir yapı kurmak o geceleri kurtarır.
