DNS Nasıl Çalışır: Resolver, Record Tipleri, TTL ve dig ile Sorgulamak
DNS bir ismi stub resolver, recursive resolver, root, TLD ve authoritative sunucu zinciriyle çözer ve her cevabı TTL süresince cache'ler. dig +trace, A/AAAA/CNAME/MX/TXT/CAA/HTTPS kayıtları, negatif cache, DoH ve HttpClient'ın açık bağlantılarının DNS değişikliğini neden görmediğini gösteren blue-green denemesi.
Sürüm notu: Bu yazıdaki komutlar dig 9.10.6 ve curl 8.7.1 ile, kodlar .NET 10 ile macOS'ta denenmiştir; DNS (Domain Name System) anlatımı RFC (Request for Comments) 1034/1035 ve RFC 9460 esas alır.
Önce Tahmin Et
Bookstore katalog servisini blue-green deployment ile yeni sürüme geçiriyorsun. catalog.bookstore.example adının A kaydı (IPv4 adres kaydı) TTL'i (Time To Live) 60 saniye ve eski "blue" ortamı gösteriyor. Kaydı yeni "green" ortamın adresine çeviriyorsun. Bir dakika sonra yeni gelen istekler green'e gitmeye başlıyor. On dakika sonra blue'yu kapatmak üzereyken loglarına bakıyorsun: sipariş servisi hâlâ blue'ya istek atıyor. DNS kaydı doğru, TTL çoktan doldu. Sipariş servisi neden eski adrese gidiyor?
Bu yazıda DNS'in nasıl çalıştığını sorgudan cache'e kadar izleyip bu sorunun cevabını hem resolver tarafında hem .NET'in bağlantı havuzunda arıyoruz.
İsimden Adrese: Stub, Recursive ve Authoritative
Uygulaman bir ismi çözmek istediğinde işi kendisi yapmaz. İşletim sistemindeki stub resolver soruyu ağdaki bir recursive resolver'a iletir: ev router'ın, şirketinin DNS sunucusu ya da 1.1.1.1 gibi public bir resolver. Recursive resolver cevabı cache'inde bulamazsa hiyerarşiyi tepeden aşağı sorar: root sunucuları .com'u kimin yönettiğini, .com'un TLD (Top-Level Domain) sunucuları example.com'u kimin yönettiğini söyler; son olarak authoritative sunucu asıl cevabı verir.

dig +trace bu zinciri resolver'a bırakmadan kendisi yürür. Önce public bir resolver'dan root sunucularının listesini alır, sonra her seviyeyi doğrudan sorar:
$ dig +trace +nodnssec @1.1.1.1 example.com A
. 511335 IN NS a.root-servers.net.
. 511335 IN NS b.root-servers.net.
...
;; Received 239 bytes from 1.1.1.1#53(1.1.1.1) in 21 ms
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
...
;; Received 836 bytes from 192.58.128.30#53(j.root-servers.net) in 155 ms
example.com. 172800 IN NS hera.ns.cloudflare.com.
example.com. 172800 IN NS elliott.ns.cloudflare.com.
;; Received 359 bytes from 192.5.6.30#53(a.gtld-servers.net) in 153 ms
example.com. 300 IN A 172.66.147.243
example.com. 300 IN A 104.20.23.154
;; Received 72 bytes from 162.159.44.228#53(elliott.ns.cloudflare.com) in 48 ms
Root sunucusu (j.root-servers.net) cevabı bilmiyor ama .com'un 13 NS (name server) kaydını veriyor; buna referral denir. .com sunucusu da example.com'un Cloudflare'deki iki authoritative sunucusunu gösteriyor. Asıl A kayıtları en sonda, 300 saniyelik TTL ile geliyor. Zincirin tamamı bu ölçümde 350 milisaniyeyi geçti. Recursive resolver'lar bu yüzden her seviyeyi kendi TTL'i boyunca cache'ler: .com'un NS kayıtlarının TTL'i 172.800 saniye, yani iki gün. Pratikte bir resolver root'a nadiren gider.
Normal sorgular UDP (User Datagram Protocol) üzerinden 53 numaralı port'a gider. Cevap tek bir UDP paketine sığmazsa resolver TCP (Transmission Control Protocol) ile yeniden sorar; büyük cevaplar ve DNSSEC (DNS Security Extensions) imzaları bunu tetikler.
Record Tipleri
DNS yalnızca isimden IP (Internet Protocol) adresine çeviri yapmaz; bir alan adı hakkında her türlü yapılandırılmış bilgiyi taşır. example.com ve birkaç başka alan adı üzerinde gerçek sorgular:
$ dig example.com AAAA +noall +answer
example.com. 65 IN AAAA 2606:4700:10::6814:179a
example.com. 65 IN AAAA 2606:4700:10::ac42:93f3
$ dig www.microsoft.com +noall +answer
www.microsoft.com. 867 IN CNAME www.microsoft.com-c-3.edgekey.net.
www.microsoft.com-c-3.edgekey.net. 867 IN CNAME e13678.dscb.akamaiedge.net.
e13678.dscb.akamaiedge.net. 7 IN A 2.22.114.96
$ dig example.com MX +noall +answer
example.com. 300 IN MX 0 .
$ dig example.com TXT +noall +answer
example.com. 300 IN TXT "v=spf1 -all"
example.com. 300 IN TXT "_k2n1y4vw3qtb4skdx9e7dxt97qrmmq9"
$ dig example.com SOA +noall +answer
example.com. 830 IN SOA elliott.ns.cloudflare.com. dns.cloudflare.com. 2418004112 10000 2400 604800 1800
$ dig google.com CAA +noall +answer
google.com. 952 IN CAA 0 issue "pki.goog"
| Tip | Ne taşır | Dikkat edilecek nokta |
|---|---|---|
A / AAAA | IPv4 / IPv6 adresi | Birden çok kayıt dönebilir; sıra her sorguda değişebilir |
CNAME | Bu isim başka bir ismin takma adıdır | Aynı isimde başka kayıtla birlikte duramaz; bu yüzden zone apex'inde (example.com'un kendisi) kullanılamaz |
MX | E-posta sunucuları ve öncelikleri | 0 . "bu alan adı e-posta kabul etmez" demek (null MX (Mail Exchanger)) |
TXT | Serbest metin | SPF (Sender Policy Framework) ve alan adı sahipliği doğrulama token'ları burada |
NS / SOA | Zone'u yöneten sunucular / zone'un yönetim bilgisi | SOA'nın (Start of Authority) son değeri negatif cevapların cache süresini belirler |
CAA | Bu alan adına hangi CA'ların (Certificate Authority) sertifika verebileceği | Yanlış yazılırsa sertifika yenilemesi başarısız olur |
SVCB / HTTPS | Servisin bağlantı parametreleri (ALPN (Application-Layer Protocol Negotiation), IP ipucu, ECH (Encrypted Client Hello) anahtarı) | Tarayıcı, sunucunun HTTP (Hypertext Transfer Protocol) sürümlerinden HTTP/3'ü destekleyip desteklemediğini bağlanmadan önce öğrenebilir |
www.microsoft.com örneği CNAME'in (Canonical Name) pratikteki kullanımını gösteriyor: isim, bir CDN (Content Delivery Network) sağlayıcısının ismine, o da başka bir isme işaret ediyor ve zincirin sonundaki A kaydının TTL'i yalnızca 7 saniye. CDN'ler kullanıcıyı en yakın sunucuya yönlendirebilmek için kısa TTL'ler kullanır. CNAME'in apex kısıtı yüzden birçok DNS sağlayıcısı ALIAS veya "CNAME flattening" gibi standart dışı çözümler sunar; zone yönetimi ilerideki DevOps "Domain, DNS ve CDN" serisinin konusu.
En yeni tip HTTPS (HTTP Secure) kaydı (RFC 9460). dig 9.10 onu henüz tanımadığı için ham hâliyle gösteriyor:
$ dig cloudflare.com TYPE65 +noall +answer
cloudflare.com. 145 IN TYPE65 \# 61 0001000001000602683302683200040008681084E5681085E5000600 20260647000000000000000000681084E52606470000000000000000 00681085E5
Byte'ları çözünce: öncelik 1, hedef aynı isim (00), alpn anahtarı (0001) altında h3 (6833) ve h2 (6832), ardından ipv4hint (0004) altında 104.16.132.229 ve 104.16.133.229, son olarak ipv6hint. Yani tarayıcı daha bağlanmadan bu sunucunun HTTP/3 konuştuğunu öğreniyor. Bunun önemini dokuzuncu yazıda göreceğiz.
TTL ve Cache Katmanları
Her cevap bir TTL taşır ve cevabı alan her cache onu en fazla o süre saklar. Aynı sorguyu beş saniye arayla üç kez sorduğumda:
# 5 saniye arayla üç kez
$ dig @1.1.1.1 example.com A +noall +answer
example.com. 198 IN A 172.66.147.243
example.com. 191 IN A 172.66.147.243
example.com. 185 IN A 172.66.147.243
Resolver cevabı cache'ten veriyor ve TTL'i kalan süreye göre geri sayıyor. Kayıt authoritative tarafta 300 saniye olarak tanımlı; resolver 300'ü dolana kadar authoritative sunucuya tekrar sormayacak. Değişiklik yaptığında yayılmanın "anında" olmamasının sebebi bu: kayıt her resolver'da eski cevabın TTL'i dolunca güncellenir.
Bu cache'lerin birden fazla katmanı var: tarayıcının kendi DNS cache'i, işletim sisteminin cache'i (macOS'ta mDNSResponder, Linux'ta systemd-resolved), recursive resolver'ın cache'i. İşletim sistemi cache'inin etkisini .NET'ten görmek için aynı ismi bir process içinde iki kez çözdüm:
$ dotnet Bookstore.Net.Lab.dll resolve docs.github.com docs.github.com
docs.github.com -> 185.199.109.154, 185.199.110.154, 185.199.111.154, 185.199.108.154, 2606:50c0:8002::154, 2606:50c0:8000::154, 2606:50c0:8003::154, 2606:50c0:8001::154 (66.6 ms)
docs.github.com -> 185.199.109.154, 185.199.110.154, 185.199.111.154, 185.199.108.154, 2606:50c0:8001::154, 2606:50c0:8002::154, 2606:50c0:8000::154, 2606:50c0:8003::154 (1.0 ms)
İlk çözümleme 66,6 milisaniye, ikincisi 1 milisaniye. Dns.GetHostAddressesAsync kendi cache'ini tutmaz; hızlanma işletim sisteminin cache'inden. IPv6 adreslerinin sırasının iki cevapta farklı olduğuna da dikkat et: sıraya güvenen kod kırılgandır.
Negatif cevaplar da cache'lenir. Var olmayan bir isim sorulduğunda cevap NXDOMAIN olur ve yanında zone'un SOA kaydı gelir:
$ dig nonexistent-bookstore-zzz-4711.com +noall +comments +authority
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 19898
com. 824 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1794132981 1800 900 604800 900
RFC 2308'e göre resolver bu "yok" cevabını SOA'nın TTL'i ile son alanından küçük olanı kadar cache'ler; burada 824 saniye. Yeni bir alt alan adı oluşturmadan önce onu sorgulayıp NXDOMAIN almak, kaydı ekledikten sonra bile o resolver'da birkaç dakika "yok" görmen demek.
Araçlar: dig, nslookup, Resolve-DnsName, DoH ve DoT
dig en ayrıntılı araç: kaydın tipini, TTL'ini, hangi sunucunun cevap verdiğini ve süresini gösterir. @1.1.1.1 ile belirli bir resolver'a, +trace ile zinciri kendin yürüyerek, +short ile yalnızca değerleri sorabilirsin. Windows'ta nslookup her yerde vardır; PowerShell'deki Resolve-DnsName example.com -Type AAAA ise dig'e daha yakın, yapılandırılmış bir çıktı verir.
Klasik DNS şifresizdir; yoldaki herkes hangi isimleri sorduğunu görebilir ve cevabı değiştirebilir. DoT (DNS over Transport Layer Security; RFC 7858), sorguları 853 numaralı port'ta TLS (Transport Layer Security) içinde taşır. DoH (DNS over HTTPS, RFC 8484) ise sorguları sıradan HTTPS trafiğinin içine koyar. Cloudflare'in DoH endpoint'inin JSON (JavaScript Object Notation) biçimini curl ile denemek:
$ curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=example.com&type=A'
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"example.com","type":1}],"Answer":[{"name":"example.com","type":1,"TTL":200,"data":"104.20.23.154"},{"name":"example.com","type":1,"TTL":200,"data":"172.66.147.243"}]}
Tarayıcılar DoH'u kendi içlerinde kullanabilir. Bunun bir yan etkisi var: tarayıcının ve işletim sisteminin farklı resolver'lar kullanması, aynı makinede curl ile tarayıcının farklı cevaplar görmesine yol açabilir.
.NET: Bağlantı Havuzu DNS'i Görmez
Tahmin bölümündeki sorunun asıl cevabı DNS'te değil, istemcide. Önceki yazıda gördüğümüz gibi HttpClient DNS'i yalnızca yeni bir bağlantı açarken çözer ve açık bir bağlantı DNS değişikliğinden habersizdir. Bunu göstermek için /etc/hosts'a dokunmadan, SocketsHttpHandler.ConnectCallback ile DNS'i taklit eden küçük bir sınıf yazdım. Callback her yeni bağlantıda çağrılıyor; içinde "DNS'in o an gösterdiği" adrese bağlanıyorum. Blue sunucu 5491'de, green sunucu (ayrı bir container) 5591'de, her ikisi de /whoami'de kendi adını dönüyor:
// /etc/hosts'a dokunmadan bir ismi blue'dan green'e çevirebilmek için process içi küçük bir "DNS"
public sealed class FakeDns
{
private volatile IPEndPoint _current;
public int Lookups;
public FakeDns(IPEndPoint initial) => _current = initial;
public void Point(IPEndPoint target) => _current = target;
public SocketsHttpHandler CreateHandler(TimeSpan pooledLifetime) => new()
{
PooledConnectionLifetime = pooledLifetime,
// Her yeni bağlantıda çağrılır: gerçek handler burada DNS'i çözer
ConnectCallback = async (context, cancellationToken) =>
{
Interlocked.Increment(ref Lookups);
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
await socket.ConnectAsync(_current, cancellationToken);
return new NetworkStream(socket, ownsSocket: true);
},
};
}
İkinci saniyede "DNS"i green'e çeviren bir döngüyü önce varsayılan sonsuz bağlantı ömrüyle, sonra 2 saniyelik PooledConnectionLifetime ile çalıştırdım:
lifetime=infinite
t=0s answered by blue (connections opened so far: 1)
t=1s answered by blue (connections opened so far: 1)
-- DNS now points to green --
t=2s answered by blue (connections opened so far: 1)
t=3s answered by blue (connections opened so far: 1)
t=4s answered by blue (connections opened so far: 1)
t=5s answered by blue (connections opened so far: 1)
lifetime=2s
t=0s answered by blue (connections opened so far: 1)
t=1s answered by blue (connections opened so far: 1)
-- DNS now points to green --
t=2s answered by green (connections opened so far: 2)
t=3s answered by green (connections opened so far: 2)
t=4s answered by green (connections opened so far: 3)
t=5s answered by green (connections opened so far: 3)
Varsayılan ayarla istemci tek bir bağlantı açtı ve DNS değişse de blue'ya konuşmaya devam etti. Trafik olduğu sürece bu bağlantı saatlerce, günlerce yaşayabilir; boşta kaldığı bir dakika (PooledConnectionIdleTimeout) hiç gelmez. Tahmin bölümündeki sipariş servisi tam olarak bu durumda. Ömür sınırı verilince bağlantı süresi dolduğunda emekliye ayrıldı, yeni bağlantı yeni çözümlemeyle green'e gitti.
Üretimde Dikkat
Değişiklikten önce TTL'i düşür. Bir kaydın adresini değiştireceksen, değişiklikten en az eski TTL kadar önce TTL'i 60 saniye gibi kısa bir değere indir; geçiş bitince eski değerine döndür. Aksi hâlde bazı resolver'lar saatlerce eski adresi verir.
DNS değişikliğine güvenen her geçişte istemcinin bağlantı ömrünü de düşün. Blue-green, failover veya bir veritabanının yeni adrese taşınması, uzun yaşayan bağlantılar yüzünden DNS'in söylediğinden çok daha geç tamamlanır. PooledConnectionLifetime'ı birkaç dakika yap ve eski ortamı, en uzun bağlantı ömrü artı TTL kadar açık tut.
DNS'i bir bağımlılık olarak izle. Resolver'ın yavaşlaması veya çökmesi, bütün dış çağrıların bağlantı kurma aşamasında takılması demek. .NET'in dns.lookup.duration metric'i bunu görmenin yolu; serinin son yazısında kullanıyoruz.
Sık Düşülen Tuzaklar
TTL'i yayılma süresi sanmak. TTL bir üst sınır değil, her cache'in kendi saatidir; bazı resolver'lar ve istemciler minimum süre uygular, bazıları TTL'i hiç dikkate almaz. HttpClient'ın açık bağlantıları da bunlardan biri.
Apex'e CNAME koymaya çalışmak. example.com'un kendisinde SOA ve NS kayıtları zaten var; CNAME onlarla birlikte duramaz.
Negatif cache'i unutmak. Kaydı eklemeden önce test amaçlı sorgulamak, birkaç dakika boyunca NXDOMAIN görmene yol açar.
Adres sırasına güvenmek. Aynı sorgunun cevabındaki sıra değişebilir; ilk adresi "birincil sunucu" sanma.
curl ile tarayıcının aynı DNS'i gördüğünü varsaymak. Tarayıcı DoH kullanıyorsa ikisi farklı resolver'lara soruyor olabilir.
Küçük Bir Deneme
★☆☆ Kendi kullandığın bir alan adı için dig +trace çalıştır. Hangi TLD sunucusu cevap verdi, authoritative sunucular hangi sağlayıcıda?
★★☆ dig ile bir alan adının TTL'ini birkaç kez sorgula ve geri sayımı gözlemle. Sonra aynı sorguyu farklı bir resolver'a (@8.8.8.8) sor. TTL değerleri neden farklı?
★★★ FakeDns'i, bir isim için birden çok adres dönecek ve bağlantıları bunlar arasında sırayla dağıtacak şekilde genişlet. MaxConnectionsPerServer = 4 ile eşzamanlı istekler atınca bağlantılar adreslere nasıl dağılıyor?
Adresi bulduk, TCP bağlantısını açtık. Ama bu yazıdaki bütün HTTPS sorgularında bir adım daha vardı: şifreli kanalı kurmak. Bir sonraki yazıda TLS 1.3 handshake'ini, sertifika zincirini, SNI (Server Name Indication) ve ALPN'i ve post-quantum key exchange'i openssl s_client ile adım adım inceliyoruz.