PowerDNS ile GeoDNS Yapılandırması

Kullanıcıların fiziksel konumlarına göre farklı DNS yanıtları döndürmek, hem performans hem de içerik yönetimi açısından kritik bir ihtiyaç haline geldi. Büyük CDN sağlayıcıları, yük dengeleyiciler ve global hizmet veren uygulamalar bu tekniğe günlük olarak güveniyor. PowerDNS, esnek backend mimarisi ve Lua scripting desteği sayesinde GeoDNS implementasyonu için son derece güçlü bir platform sunuyor. Bu yazıda sıfırdan bir GeoDNS altyapısı kuracağız, gerçek dünya senaryolarını ele alacağız ve production ortamına hazır bir yapılandırma oluşturacağız.

GeoDNS Nedir ve Ne Zaman Gereklidir

GeoDNS, gelen DNS sorgusunun kaynak IP adresine bakarak coğrafi konuma özgü yanıtlar döndüren bir mekanizmadır. Klasik DNS’ten farkı şudur: standart DNS her istemciye aynı IP adresini döndürürken, GeoDNS ile Türkiye’deki bir kullanıcıya Frankfurt sunucusunun IP’sini, ABD’deki bir kullanıcıya ise Virginia sunucusunun IP’sini verebilirsiniz.

Hangi senaryolarda buna ihtiyaç duyarsınız:

  • Coğrafi içerik kısıtlaması: Belirli bölgelere farklı içerik sunmak
  • Latency optimizasyonu: Kullanıcıyı en yakın sunucuya yönlendirmek
  • Yük dağılımı: Trafiği bölgesel olarak dengelemek
  • Failover: Bir bölgedeki sunucu çöktüğünde trafiği başka bölgeye kaydırmak
  • Uyumluluk: GDPR gibi yasal gerekliliklere göre veri lokasyonu yönetimi

PowerDNS Kurulumu ve Ön Hazırlıklar

Önce temel bileşenleri kuralım. Bu rehberde Ubuntu 22.04 LTS kullanıyorum ama Debian ve RHEL tabanlı sistemlerde de benzer adımlar geçerli.

# PowerDNS Authoritative Server kurulumu
apt-get install -y pdns-server pdns-backend-sqlite3 pdns-backend-pipe

# GeoIP veritabanı için gerekli araçlar
apt-get install -y mmdb-bin libmaxminddb0 libmaxminddb-dev

# MaxMind GeoLite2 veritabanını indirmek için geoipupdate
apt-get install -y geoipupdate

MaxMind GeoLite2 veritabanı için ücretsiz hesap açmanız gerekiyor. Hesap açtıktan sonra lisans anahtarınızı /etc/GeoIP.conf dosyasına ekleyin:

# /etc/GeoIP.conf
AccountID YOUR_ACCOUNT_ID
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-Country GeoLite2-City GeoLite2-ASN
# Veritabanlarını güncelleyin
geoipupdate

# Veritabanlarının indirildiğini doğrulayın
ls -la /var/lib/GeoIP/
# GeoLite2-City.mmdb, GeoLite2-Country.mmdb dosyalarını görmelisiniz

# Test sorgusu
mmdblookup --file /var/lib/GeoIP/GeoLite2-Country.mmdb --ip 88.255.0.1

PowerDNS Temel Yapılandırması

PowerDNS’in ana konfigürasyon dosyasını düzenleyelim:

# /etc/powerdns/pdns.conf

local-address=0.0.0.0
local-port=53
daemon=yes
guardian=yes

# Log ayarları
log-dns-queries=yes
log-dns-details=yes
loglevel=4

# Backend ayarları - Lua backend'i etkinleştiriyoruz
launch=lua
lua-filename=/etc/powerdns/geodns.lua

# Performans ayarları
receiver-threads=4
distributor-threads=3
cache-ttl=20
query-cache-ttl=20
negquery-cache-ttl=60

# EDNS Client Subnet desteği - GeoDNS için kritik
edns-subnet-processing=yes

EDNS Client Subnet (ECS) desteği burada çok önemli. Cloudflare veya Google DNS gibi büyük resolver’lar sorguları kendi IP’lerinden gönderir. ECS olmadan tüm Türkiye trafiği sanki Cloudflare’in Dublin veri merkezinden geliyormuş gibi görünür. ECS ile gerçek istemci IP’si sorguya eklenerek iletilir.

Lua Backend ile GeoDNS Mantığı

PowerDNS’in Lua backend’i, DNS sorgularını işleyecek özel mantık yazmanıza olanak tanır. Bu bizim GeoDNS motorumuz olacak.

-- /etc/powerdns/geodns.lua

-- MaxMind GeoIP2 kütüphanesini yükle
local mmdb = require("mmdb")

-- Veritabanlarını açık tut (her sorgu için açıp kapatmak performansı öldürür)
local country_db = mmdb.open("/var/lib/GeoIP/GeoLite2-Country.mmdb")
local city_db = mmdb.open("/var/lib/GeoIP/GeoLite2-City.mmdb")

-- Bölgesel sunucu haritası
local geo_map = {
    -- Türkiye ve Orta Doğu
    ["TR"] = "185.220.10.1",
    ["AZ"] = "185.220.10.1",
    ["GE"] = "185.220.10.1",
    
    -- Batı Avrupa
    ["DE"] = "78.46.55.100",
    ["NL"] = "78.46.55.100",
    ["FR"] = "78.46.55.100",
    ["GB"] = "78.46.55.100",
    
    -- Kuzey Amerika
    ["US"] = "104.21.30.200",
    ["CA"] = "104.21.30.200",
    
    -- Asya Pasifik
    ["JP"] = "103.86.20.15",
    ["SG"] = "103.86.20.15",
    ["AU"] = "103.86.20.15",
    
    -- Varsayılan (bilinmeyen bölgeler)
    ["default"] = "78.46.55.100"
}

-- IP adresinden ülke kodu çıkaran fonksiyon
function get_country(ip)
    if not ip or ip == "" then
        return "default"
    end
    
    local result = country_db:lookup(ip)
    if result and result.country and result.country.iso_code then
        return result.country.iso_code
    end
    
    return "default"
end

-- Sunucu IP'sini döndüren fonksiyon
function get_server_for_country(country_code)
    local server = geo_map[country_code]
    if server then
        return server
    end
    return geo_map["default"]
end

Şimdi DNS yanıt fonksiyonunu yazalım. Bu fonksiyon her DNS sorgusunda çağrılır:

-- geodns.lua dosyasına devam

function dns_answer(dname, qtype, ip, edns_subnet)
    local answers = {}
    
    -- Önce EDNS subnet'ten IP al, yoksa sorgu kaynağını kullan
    local lookup_ip = ip
    if edns_subnet and edns_subnet ~= "" then
        lookup_ip = edns_subnet
    end
    
    local country = get_country(lookup_ip)
    local server_ip = get_server_for_country(country)
    
    -- Sadece hedef domainimiz için yanıt üretelim
    if dname == "app.ornek.com." then
        if qtype == QType.A or qtype == QType.ANY then
            answers[#answers + 1] = {
                qtype = QType.A,
                content = server_ip,
                ttl = 60  -- GeoDNS için TTL düşük tutulmalı
            }
        end
    elseif dname == "ornek.com." then
        if qtype == QType.A or qtype == QType.ANY then
            answers[#answers + 1] = {
                qtype = QType.A,
                content = server_ip,
                ttl = 60
            }
        end
        
        -- SOA kaydı
        if qtype == QType.SOA or qtype == QType.ANY then
            answers[#answers + 1] = {
                qtype = QType.SOA,
                content = "ns1.ornek.com. hostmaster.ornek.com. 2024010101 3600 900 604800 300",
                ttl = 3600
            }
        end
    end
    
    return answers
end

Pipe Backend Alternatifi

Lua backend kullanmak istemiyorsanız ya da farklı bir dil tercih ediyorsanız, PowerDNS’in Pipe backend’ini kullanabilirsiniz. Bu yöntemde PowerDNS, sorgular için dış bir sürece yazı yazar ve cevap bekler. Python, Go veya herhangi bir dille yazabilirsiniz.

#!/usr/bin/env python3
# /etc/powerdns/geodns_pipe.py

import sys
import socket
import geoip2.database
import geoip2.errors

# Veritabanını bir kez aç
reader = geoip2.database.Reader('/var/lib/GeoIP/GeoLite2-Country.mmdb')

GEO_MAP = {
    'TR': '185.220.10.1',
    'DE': '78.46.55.100',
    'NL': '78.46.55.100',
    'US': '104.21.30.200',
    'CA': '104.21.30.200',
    'default': '78.46.55.100'
}

def get_country(ip):
    try:
        response = reader.country(ip)
        return response.country.iso_code
    except (geoip2.errors.AddressNotFoundError, ValueError):
        return 'default'

def get_ip_for_country(country):
    return GEO_MAP.get(country, GEO_MAP['default'])

# Pipe protokolü handshake
sys.stdout.write("OKtGeoDNS Backend v1.0n")
sys.stdout.flush()

for line in sys.stdin:
    line = line.strip()
    parts = line.split('t')
    
    if not parts:
        sys.stdout.write("FAILn")
        sys.stdout.flush()
        continue
    
    query_type = parts[0]
    
    if query_type == "Q":
        # Format: Q <qname> <qclass> <qtype> <id> <remoteip> [localip] [ednssubnet]
        if len(parts) < 6:
            sys.stdout.write("ENDn")
            sys.stdout.flush()
            continue
        
        qname = parts[1].lower()
        qclass = parts[2]
        qtype = parts[3]
        qid = parts[4]
        remote_ip = parts[5]
        edns_subnet = parts[7] if len(parts) > 7 else remote_ip
        
        # ECS varsa onu kullan
        lookup_ip = edns_subnet if edns_subnet and edns_subnet != '0.0.0.0' else remote_ip
        
        country = get_country(lookup_ip)
        server_ip = get_ip_for_country(country)
        
        if qname in ('app.ornek.com', 'ornek.com') and qtype in ('A', 'ANY'):
            sys.stdout.write(f"DATAt{qname}t{qclass}tAt60t{qid}t{server_ip}n")
        
        sys.stdout.write("ENDn")
        sys.stdout.flush()
    
    elif query_type == "AXFR":
        sys.stdout.write("ENDn")
        sys.stdout.flush()
    
    else:
        sys.stdout.write("ENDn")
        sys.stdout.flush()

Pipe backend yapılandırması:

# /etc/powerdns/pdns.conf içinde pipe backend için
launch=pipe
pipe-command=/etc/powerdns/geodns_pipe.py
pipe-timeout=2000

Şehir Bazlı Yönlendirme

Ülke seviyesi kaba kalmaya başlarsa, özellikle büyük ülkelerde (ABD, Rusya, Çin gibi) şehir veya koordinat bazlı yönlendirme yapabilirsiniz. Aşağıdaki örnek, ABD’deki kullanıcıları kıyı bölgelerine göre ayırt ediyor:

-- Şehir/bölge bazlı yönlendirme
function get_us_region_server(ip)
    local result = city_db:lookup(ip)
    
    if not result then
        return "104.21.30.200"  -- Varsayılan ABD
    end
    
    local longitude = result.location and result.location.longitude
    
    if not longitude then
        return "104.21.30.200"
    end
    
    -- Boylama göre Doğu/Batı kıyısı ayrımı
    if longitude < -100 then
        -- Batı kıyısı (California, Oregon, Washington)
        return "104.18.20.50"   -- Los Angeles PoP
    else
        -- Doğu kıyısı ve orta ABD
        return "104.21.30.200"  -- Virginia PoP
    end
end

Health Check ile Failover Entegrasyonu

GeoDNS’in en güçlü özelliklerinden biri, sağlık kontrolleriyle birleştirildiğinde otomatik failover sağlamasıdır. Aşağıdaki script, sunucuların sağlık durumunu periyodik olarak kontrol eder ve sonuçları bir dosyaya yazar. Lua backend bu dosyayı okuyarak yanıt üretir:

#!/bin/bash
# /usr/local/bin/geodns_healthcheck.sh

HEALTH_FILE="/var/lib/powerdns/server_health.json"
TIMEOUT=3

declare -A SERVERS=(
    ["tr"]="185.220.10.1"
    ["eu"]="78.46.55.100"
    ["us"]="104.21.30.200"
    ["ap"]="103.86.20.15"
)

declare -A BACKUPS=(
    ["tr"]="78.46.55.100"
    ["eu"]="104.21.30.200"
    ["us"]="78.46.55.100"
    ["ap"]="104.21.30.200"
)

result="{"

for region in "${!SERVERS[@]}"; do
    primary="${SERVERS[$region]}"
    backup="${BACKUPS[$region]}"
    
    # HTTP health check (443 portuna TCP bağlantı kontrolü)
    if nc -z -w "$TIMEOUT" "$primary" 443 2>/dev/null; then
        active_ip="$primary"
        status="healthy"
    else
        active_ip="$backup"
        status="failover"
        logger -t geodns "UYARI: $region bolgesi primary sunucusu ($primary) DOWN, failover: $backup"
    fi
    
    result+=""${region}":{"ip":"${active_ip}","status":"${status}"},"
done

# Son virgülü kaldır ve kapat
result="${result%,}}"

echo "$result" > "$HEALTH_FILE"
chmod 644 "$HEALTH_FILE"

Bu scripti cron ile dakikada bir çalıştırın:

# crontab -e
* * * * * /usr/local/bin/geodns_healthcheck.sh

Test ve Doğrulama

GeoDNS kurulumunu test etmek için birkaç farklı yöntem kullanın:

# Temel dig testi - local resolver kullanarak
dig @localhost app.ornek.com A

# EDNS subnet ile test - Türkiye IP'si simülasyonu
dig @localhost app.ornek.com A +subnet=88.255.0.1/24

# ABD IP'si ile test
dig @localhost app.ornek.com A +subnet=8.8.8.8/24

# Almanya IP'si ile test
dig @localhost app.ornek.com A +subnet=217.0.0.1/24

# Yanıt sürelerini karşılaştır
for subnet in "88.255.0.1/24" "8.8.8.8/24" "217.0.0.1/24"; do
    echo "--- Subnet: $subnet ---"
    dig @localhost app.ornek.com A +subnet=$subnet +short
done

# PowerDNS log takibi
journalctl -u pdns -f
# veya
tail -f /var/log/pdns.log

Dışarıdan test etmek için farklı coğrafi konumlardaki VPS’lerden ya da online araçlardan yararlanabilirsiniz:

# DNS propagasyonunu dünya genelinde test et
# dnschecker.org veya whatsmydns.net gibi siteler kullanılabilir

# nmap ile DNS scriptleri
nmap -sU -p 53 --script dns-service-discovery ns1.ornek.com

# Curl ile HTTP header kontrolü (sunucuların gerçekten farklı mı cevap verdiğini anlamak için)
curl -H "Host: app.ornek.com" http://185.220.10.1/healthcheck
curl -H "Host: app.ornek.com" http://78.46.55.100/healthcheck

TTL Stratejisi ve Önbellek Yönetimi

GeoDNS’de TTL değeri kritik bir parametre. Çok yüksek TTL koyarsanız, bir sunucu çöktüğünde müşterileriniz uzun süre eski IP’yi kullanmaya devam eder. Çok düşük TTL koyarsanız, DNS altyapınıza gereksiz yük bindirirsiniz.

Önerilen yaklaşım:

  • Normal operasyon TTL: 60-300 saniye arası
  • Planlı bakım öncesi: Bakımdan 2-3 saat önce TTL’i 30 saniyeye düşürün
  • Bakım sonrası: TTL’i tekrar normal değerine çıkarın
  • SOA kaydı TTL: Daha yüksek tutulabilir, genellikle 3600 saniye

Negative caching de önemli. Var olmayan bir kayıt için TTL çok düşük tutulursa, DoS saldırılarına karşı savunmasız kalabilirsiniz:

# pdns.conf içinde negative caching
negquery-cache-ttl=300
cache-ttl=60

Güvenlik Düşünceleri

GeoDNS altyapısı birkaç güvenlik açısından dikkat gerektiriyor:

Rate limiting DNS amplification saldırılarını önlemek için zorunlu:

# pdns.conf
# İstemci başına sorgu limiti
max-tcp-connections=20

# Paket boyutu limiti
udp-truncation-threshold=1220

# Recursive sorguları reddet (authoritative sunucu için)
recursor=no

DNSSEC desteği eklemek, yanıtlarınızın sahte olmamasını garanti eder. PowerDNS ile DNSSEC yapılandırması için alan adı başına anahtar oluşturmanız gerekiyor:

# DNSSEC anahtarı oluştur
pdnsutil add-zone-key ornek.com ksk active ecdsa256

# Zone'u imzala
pdnsutil rectify-zone ornek.com

# DS kaydını göster (registrar'a ekleyeceksiniz)
pdnsutil show-zone ornek.com | grep DS

İzleme ve Metriks

Production ortamında GeoDNS davranışını izlemek için Prometheus ve Grafana kombinasyonu işe yarar. PowerDNS, yerleşik web API’si ile metrikleri dışa aktarır:

# pdns.conf içinde API ve metrikleri etkinleştir
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-password=guclu_sifre_buraya
api=yes
api-key=api_anahtari_buraya

# Prometheus exporter ile metrik toplama
# pdns_exporter kurulumu
wget https://github.com/janeczku/powerdns-prometheus-exporter/releases/latest/download/pdns-exporter_linux_amd64.tar.gz
tar xzf pdns-exporter_linux_amd64.tar.gz
mv pdns-exporter /usr/local/bin/

# Servis olarak çalıştır
cat > /etc/systemd/system/pdns-exporter.service << EOF
[Unit]
Description=PowerDNS Prometheus Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/pdns-exporter -api-url http://127.0.0.1:8081/api/v1 -api-key api_anahtari_buraya
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl enable --now pdns-exporter

İzlemeniz gereken kritik metrikler:

  • query-count: Toplam sorgu sayısı, ani düşüşler probleme işaret eder
  • servfail-count: SERVFAIL yanıt sayısı, backend hatalarını gösterir
  • latency: Yanıt gecikme süresi, 5ms üzeri araştırılmalı
  • cache-hit-ratio: Önbellek hit oranı, %80 altındaysa TTL ayarlarını gözden geçirin

Gerçek Dünya Örneği: E-Ticaret Platformu

Bir e-ticaret müşterisi için kurduğumuz yapıyı paylaşayım. Türkiye, Almanya ve ABD’de sunucuları vardı. Önceden tek bir IP kullanıyorlardı ve Türk kullanıcılar ABD sunucusuna, oradan S3’ten resim çekiyordu. Ortalama sayfa yüklenme süresi 4.2 saniyeydi.

GeoDNS sonrasında:

  • Türk kullanıcılar İstanbul CDN’ine yönlendirildi (0.8 saniye)
  • Avrupa kullanıcıları Frankfurt’a yönlendirildi (1.1 saniye)
  • ABD kullanıcıları Virginia’ya bağlandı (0.9 saniye)

Toplam altyapı maliyeti neredeyse aynı kaldı, çünkü zaten bu sunucular mevcuttu. Sadece trafik yönlendirme değişti. Müşteri dönüşüm oranı ise ortalama %12 arttı, ki bu rakam sayfa yüklenme süresiyle doğrudan ilişkili.

Sonuç

PowerDNS ile GeoDNS yapılandırması, ilk bakışta karmaşık görünse de adım adım ele alındığında yönetilebilir bir süreç. Temel noktalara dikkat etmeniz gerekiyor:

  • Düşük TTL kullanın, GeoDNS’in hızlı failover yapabilmesi için
  • ECS desteğini mutlaka etkinleştirin, aksi halde büyük resolver’lardan gelen trafiği doğru yönlendiremezsiniz
  • Health check mekanizması olmadan GeoDNS yarım kalır
  • GeoIP veritabanını düzenli güncelleyin, eski veritabanları yanlış yönlendirmeye neden olur
  • Test sürecini ciddiye alın, farklı ülkelerden simülasyon yaparak her senaryoyu kontrol edin

Pipe backend ile Python yazmak tercih ettiğim yöntem, çünkü debug etmesi çok daha kolay. Lua backend ise daha performanslı ama hatayı bulmak bazen can sıkıcı oluyor. Production’a almadan önce hangi yöntemi seçerseniz seçin, kapsamlı bir test süreci geçirin. DNS hatalarının kullanıcıya etkisi anlıktır ve önbellek süresi boyunca devam eder, geri almak genellikle o kadar kolay olmaz.

Yorum yapın