11-bo‘lim
DNS
Nom ierarxiyasi va rekursiv qidiruv, DNS paketini qo'lda yasash va o'qish, siqish ko'rsatkichlari, yozuv turlari va keshlash.
Ushbu bo‘lim mundarijasi
softromeda.com - bu odamlar uchun. Kompyuterga esa IP manzil
kerak. Nomni manzilga aylantiradigan tizim - DNS.
U internetdagi eng katta taqsimlangan ma'lumotlar bazasi va deyarli har bir ulanish undan boshlanadi.
Nomlar ierarxiyasi #
DNS nomi o'ngdan chapga o'qiladi:
| Bo'lak | Nomi |
|---|---|
. | Ildiz (root) - odatda yozilmaydi |
com | Yuqori darajali domen (TLD) |
softromeda | Ikkinchi darajali domen |
edu | Quyi domen (subdomain) |
Ya'ni edu.softromeda.com to'liq yozilganda
edu.softromeda.com. bo'ladi - oxiridagi nuqta ildizni
bildiradi.
So'rov paketini qo'lda yasash #
DNS paketi 12 baytlik sarlavha bilan boshlanadi. Nom esa maxsus usulda kodlanadi: har bo'lak oldiga uzunligi yoziladi.
import struct
def nomni_kodla(nom):
"""www.example.com -> b'\\x03www\\x07example\\x03com\\x00'"""
qismlar = b""
for bolak in nom.split("."):
qismlar += bytes([len(bolak)]) + bolak.encode()
return qismlar + b"\x00"
def sorov_yasa(nom, sorov_id=0x1234):
bayroqlar = 0x0100 # rekursiya so'raymiz
sarlavha = struct.pack("!HHHHHH", sorov_id, bayroqlar, 1, 0, 0, 0)
savol = nomni_kodla(nom) + struct.pack("!HH", 1, 1) # A, IN
return sarlavha + savol
paket = sorov_yasa("softromeda.com")
print("so'rov uzunligi:", len(paket), "bayt")
print()
print("sarlavha (12 bayt):", paket[:12].hex(" "))
print(" so'rov ID :", hex(struct.unpack("!H", paket[0:2])[0]))
print(" bayroqlar :", hex(struct.unpack("!H", paket[2:4])[0]))
print(" savollar :", struct.unpack("!H", paket[4:6])[0])
print(" javoblar :", struct.unpack("!H", paket[6:8])[0])
print()
print("savol qismi:", paket[12:].hex(" "))
print("kodlangan nom:", nomni_kodla("softromeda.com"))
so'rov uzunligi: 32 bayt
sarlavha (12 bayt): 12 34 01 00 00 01 00 00 00 00 00 00
so'rov ID : 0x1234
bayroqlar : 0x100
savollar : 1
javoblar : 0
savol qismi: 0a 73 6f 66 74 72 6f 6d 65 64 61 03 63 6f 6d 00 00 01 00 01
kodlangan nom: b'\nsoftromeda\x03com\x00'
0a - bu 10, ya'ni softromeda so'zining uzunligi. Keyin
03 va com. Oxirida 00 - nom tugadi.
DNS UDP ustida ishlaydi, ya'ni javob qaysi so'rovga tegishli ekanini aniqlash kerak.
Shu ish uchun 16 bitli ID ishlatiladi. Javob kelganda mijoz ID mos kelishini tekshiradi.
Misolda 0x1234 deb qo'ydik, lekin haqiqiy dasturda u
tasodifiy bo'lishi shart - sababini quyida ko'ramiz.
Javobni o'qish #
Endi javob paketini tahlil qilamiz. Unda bitta qiziq narsa bor - siqish ko'rsatkichlari:
import struct
import socket
# Haqiqiy javob paketi (bu yerda qo'lda yig'ilgan, lekin
# tuzilishi haqiqiysi bilan bir xil)
JAVOB = bytes.fromhex(
"1234" "8180" "0001" "0002" "0000" "0000"
"0a" "736f6674726f6d656461" "03" "636f6d" "00" "0001" "0001"
"c00c" "0001" "0001" "0000012c" "0004" "5db8d822"
"c00c" "0001" "0001" "0000012c" "0004" "5db8d823"
)
TURLAR = {1: "A", 2: "NS", 5: "CNAME", 15: "MX", 16: "TXT", 28: "AAAA"}
def nom_oqi(paket, joy):
"""Siqish ko'rsatkichlarini ham tushunadi."""
bolaklar = []
sakradik = False
oxiri = joy
while True:
uzunlik = paket[joy]
if uzunlik == 0:
joy += 1
break
if uzunlik & 0xC0 == 0xC0: # ikki yuqori bit - ko'rsatkich
korsatkich = struct.unpack("!H", paket[joy:joy + 2])[0] & 0x3FFF
if not sakradik:
oxiri = joy + 2
sakradik = True
joy = korsatkich
continue
joy += 1
bolaklar.append(paket[joy:joy + uzunlik].decode())
joy += uzunlik
return ".".join(bolaklar), (oxiri if sakradik else joy)
sorov_id, bayroqlar, qd, an, ns, ar = struct.unpack("!HHHHHH", JAVOB[:12])
print("so'rov ID :", hex(sorov_id))
print("javobmi :", bool(bayroqlar & 0x8000))
print("xato kodi :", bayroqlar & 0x000F, "(0 = xato yo'q)")
print("savollar :", qd)
print("javoblar :", an)
joy = 12
nom, joy = nom_oqi(JAVOB, joy)
tur, sinf = struct.unpack("!HH", JAVOB[joy:joy + 4])
joy += 4
print()
print("savol:", nom, TURLAR.get(tur, tur))
print()
print("javob yozuvlari:")
for i in range(an):
nom, joy = nom_oqi(JAVOB, joy)
tur, sinf, ttl, uzunlik = struct.unpack("!HHIH", JAVOB[joy:joy + 10])
joy += 10
malumot = JAVOB[joy:joy + uzunlik]
joy += uzunlik
qiymat = socket.inet_ntoa(malumot) if tur == 1 else malumot.hex()
print(f" {nom:<16} {TURLAR.get(tur, tur):<6} ttl={ttl:<5} {qiymat}")
so'rov ID : 0x1234
javobmi : True
xato kodi : 0 (0 = xato yo'q)
savollar : 1
javoblar : 2
savol: softromeda.com A
javob yozuvlari:
softromeda.com A ttl=300 93.184.216.34
softromeda.com A ttl=300 93.184.216.35
c0 0c - siqish ko'rsatkichiJavobdagi yozuvlarda nom qayta yozilmadi. Uning o'rniga
ikki bayt turibdi: c0 0c.
Ikki yuqori bit (11) "bu ko'rsatkich" degani, qolgan 14 bit
esa paket ichidagi o'rin raqami. 0x00c = 12, ya'ni
savol qismidagi nom.
Foydasi katta: DNS javobida bitta nom o'nlab marta takrorlanishi mumkin. Siqish bo'lmasa paket UDP chegarasidan oshib ketardi.
Shuning uchun DNS ni tahlil qiladigan har bir kod ko'rsatkichlarni albatta tushunishi kerak.
Haqiqiy so'rov - haqiqiy soket orqali #
Endi hamma narsani birlashtiramiz. Quyidagi misolda mahalliy DNS server ishga tushadi, mijoz unga haqiqiy UDP datagramma yuboradi va javobni o'qiydi:
import socket
import struct
import threading
PORT = 9553
# --- soxta DNS server: har so'rovga bitta A yozuvi bilan javob beradi
ZONA = {"softromeda.com": "93.184.216.34"}
def nom_kodla(nom):
natija = b""
for bolak in nom.split("."):
natija += bytes([len(bolak)]) + bolak.encode()
return natija + b"\x00"
# Soket asosiy oqimda bog'lanadi - shunda so'rov yuborilganda
# port allaqachon tayyor bo'ladi
server_soket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_soket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_soket.bind(("127.0.0.1", PORT))
def server():
sorov, kimdan = server_soket.recvfrom(512)
sorov_id = struct.unpack("!H", sorov[:2])[0]
savol = sorov[12:]
sarlavha = struct.pack("!HHHHHH", sorov_id, 0x8180, 1, 1, 0, 0)
javob = struct.pack("!HHIH", 1, 1, 300, 4)
javob += socket.inet_aton(ZONA["softromeda.com"])
server_soket.sendto(sarlavha + savol + b"\xc0\x0c" + javob, kimdan)
oqim = threading.Thread(target=server, daemon=True)
oqim.start()
# --- mijoz: so'rov yasaydi va yuboradi
sorov = struct.pack("!HHHHHH", 0x1234, 0x0100, 1, 0, 0, 0)
sorov += nom_kodla("softromeda.com") + struct.pack("!HH", 1, 1)
mijoz = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
mijoz.settimeout(5)
mijoz.sendto(sorov, ("127.0.0.1", PORT))
javob, _ = mijoz.recvfrom(512)
mijoz.close()
oqim.join()
server_soket.close()
sorov_id, bayroqlar, qd, an, _, _ = struct.unpack("!HHHHHH", javob[:12])
print("so'rov va javob ID lari mos keldimi:", sorov_id == 0x1234)
print("javob bayrog'i o'rnatilganmi:", bool(bayroqlar & 0x8000))
print("javob yozuvlari soni:", an)
# Javob qismi: 12 + savol uzunligi dan keyin boshlanadi
boshi = 12 + len(nom_kodla("softromeda.com")) + 4
tur, sinf, ttl, uzunlik = struct.unpack("!HHIH", javob[boshi + 2:boshi + 12])
manzil = socket.inet_ntoa(javob[boshi + 12:boshi + 12 + uzunlik])
print("yozuv turi (1 = A):", tur)
print("TTL :", ttl)
print("manzil :", manzil)
so'rov va javob ID lari mos keldimi: True
javob bayrog'i o'rnatilganmi: True
javob yozuvlari soni: 1
yozuv turi (1 = A): 1
TTL : 300
manzil : 93.184.216.34
Bu yerda hech qanday DNS kutubxonasi ishlatilmadi - faqat
socket va struct. Aynan shu baytlar haqiqiy DNS serveriga
ham ketadi.
Koddagi ("127.0.0.1", PORT) ni ("8.8.8.8", 53) ga
almashtirsangiz, xuddi shu so'rov ommaviy DNS serveriga ketadi.
Buyruq qatoridan tekshirish uchun:
dig +short softromeda.com A
Natijani bu yerga yozmadik, chunki u vaqt o'tishi bilan o'zgaradi - darslikdagi barcha chiqishlar esa barqaror bo'lishi kerak.
Yozuv turlari #
| Turi | Nima saqlaydi | Misol |
|---|---|---|
A | IPv4 manzil | 93.184.216.34 |
AAAA | IPv6 manzil | 2606:2800::1946 |
CNAME | Boshqa nomga taxallus | www → softromeda.com |
MX | Pochta serveri | mail.softromeda.com |
NS | Zonaning nom serveri | ns1.provayder.com |
TXT | Ixtiyoriy matn | SPF, tasdiqlash kodlari |
PTR | Manzildan nomga (teskari) | 34.216.184.93.in-addr.arpa |
TXT alohida qiziq: u domen egaligini tasdiqlash va pochta
sozlamalari (SPF, DKIM) uchun keng ishlatiladi.
Keshlash va TTL #
Har yozuvda TTL bor - uni necha soniya saqlash mumkinligi. Keshlash tufayli DNS shuncha katta yukni ko'tara oladi:
class Keshlovchi:
def __init__(self):
self.kesh = {} # nom -> (manzil, tugash vaqti)
self.sorovlar = 0
self.soat = 0
def _asl_manba(self, nom):
self.sorovlar += 1
return "93.184.216.34", 300 # manzil va TTL
def top(self, nom):
yozuv = self.kesh.get(nom)
if yozuv and yozuv[1] > self.soat:
qolgan = yozuv[1] - self.soat
print(f" t={self.soat:<5} {nom} -> keshdan ({qolgan} s qoldi)")
return yozuv[0]
sabab = "eskirgan" if yozuv else "keshda yo'q"
manzil, ttl = self._asl_manba(nom)
self.kesh[nom] = (manzil, self.soat + ttl)
print(f" t={self.soat:<5} {nom} -> so'rov yuborildi ({sabab})")
return manzil
k = Keshlovchi()
for vaqt in [0, 10, 120, 299, 300, 301, 400]:
k.soat = vaqt
k.top("softromeda.com")
print()
print("jami murojaat : 7")
print("haqiqiy so'rov :", k.sorovlar)
print("keshdan olingan:", 7 - k.sorovlar)
t=0 softromeda.com -> so'rov yuborildi (keshda yo'q)
t=10 softromeda.com -> keshdan (290 s qoldi)
t=120 softromeda.com -> keshdan (180 s qoldi)
t=299 softromeda.com -> keshdan (1 s qoldi)
t=300 softromeda.com -> so'rov yuborildi (eskirgan)
t=301 softromeda.com -> keshdan (299 s qoldi)
t=400 softromeda.com -> keshdan (200 s qoldi)
jami murojaat : 7
haqiqiy so'rov : 2
keshdan olingan: 5
Saytni yangi serverga ko'chirmoqchisiz. DNS yozuvini o'zgartirdingiz, lekin bir qism foydalanuvchilar hali ham eski serverga tushmoqda.
Sabab: ularning hal qiluvchisida eski yozuv hali eskirmagan.
To'g'ri tartib:
| Qachon | Nima qilinadi |
|---|---|
| Ko'chirishdan 1 kun oldin | TTL ni 300 soniyaga tushiring |
| Ko'chirish kuni | Yozuvni o'zgartiring |
| 5 daqiqadan keyin | Deyarli hamma yangisiga o'tadi |
| Bir necha kundan keyin | TTL ni odatdagi qiymatga qaytaring |
TTL ni ko'chirish kuni tushirish kech: eski, uzun TTL li yozuv allaqachon keshlarda tarqalib ketgan.
DNS an'anaviy ravishda shifrlanmagan UDP ustida ishlaydi va javobni kim yuborganini tekshirmaydi.
Hujumchi to'g'ri so'rov ID sini taxmin qilib, haqiqiy javobdan oldin soxta javob yuborsa - qurbon o'zi ishonadigan domen o'rniga hujumchi serveriga boradi.
Aynan shuning uchun so'rov ID si tasodifiy bo'lishi va mijoz port raqami ham tasodifiy tanlanishi shart. 2008-yilda bu mavzuda jiddiy zaiflik topilgan edi.
Zamonaviy himoya vositalari:
| Vosita | Nima qiladi |
|---|---|
| DNSSEC | Javoblarni imzolaydi - soxtasi rad etiladi |
| DoH / DoT | So'rovni HTTPS yoki TLS ichida shifrlaydi |
Ular bir-birini almashtirmaydi: DNSSEC haqiqiyligini, DoH esa maxfiyligini ta'minlaydi.
- So'rov yasash misolini ishga tushiring.
- Nomni
edu.softromeda.comga o'zgartiring - paket necha bayt bo'ldi? AAAA(28) turi uchun so'rov yasang.- Javobni o'qish misolini ishga tushiring.
c00cni haqiqiy nom bilan almashtirib, natija bir xil ekanini ko'ring.- Mahalliy DNS server misolini ishga tushiring.
ZONAga ikkinchi domen qo'shing va serverni moslashtiring.dig softromeda.comni ishga tushiring va bo'limlarni o'qing.- Kesh misolini ishga tushiring va TTL ni 60 ga o'zgartiring.
- Server ko'chirish rejasini qadamma-qadam yozing.
Xulosa #
- DNS - nomni manzilga aylantiradigan taqsimlangan ma'lumotlar bazasi.
- Nomlar o'ngdan chapga ierarxik: ildiz, TLD, domen, quyi domen.
- Hal qiluvchi har bosqichda javob emas, kimdan so'rashni o'rganadi.
- DNS paketi 12 baytlik sarlavha bilan boshlanadi; nom bo'laklari uzunlik bilan kodlanadi.
- So'rov ID javobni so'rovga bog'laydi va tasodifiy bo'lishi shart.
- Javobdagi nomlar siqish ko'rsatkichi (
c0 xx) bilan qisqartiriladi. - Asosiy yozuv turlari:
A,AAAA,CNAME,MX,NS,TXT,PTR. - TTL yozuvni necha soniya keshlash mumkinligini aytadi.
- Server ko'chirishdan oldin TTL ni tushiring, keyin emas.
- DNS soxtalashtirishga qarshi DNSSEC (haqiqiylik) va DoH/DoT (maxfiylik) ishlatiladi.
Keyingi bo'limda eng ko'p ishlatiladigan ilova protokoliga o'tamiz - HTTP.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.