11-bo‘lim

DNS

Nom ierarxiyasi va rekursiv qidiruv, DNS paketini qo'lda yasash va o'qish, siqish ko'rsatkichlari, yozuv turlari va keshlash.

🕑 10 daqiqa o‘qish 📄 873 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Nomlar ierarxiyasi
  2. So'rov paketini qo'lda yasash
  3. Javobni o'qish
  4. Haqiqiy so'rov - haqiqiy soket orqali
  5. Yozuv turlari
  6. Keshlash va TTL
  7. Xulosa

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'lakNomi
.Ildiz (root) - odatda yozilmaydi
comYuqori darajali domen (TLD)
softromedaIkkinchi darajali domen
eduQuyi domen (subdomain)

Ya'ni edu.softromeda.com to'liq yozilganda edu.softromeda.com. bo'ladi - oxiridagi nuqta ildizni bildiradi.

edu.softromeda.com qanday topiladi Sizning Rekursiv hal qiluvchi Ildiz serverlari (.) com serverlari softromeda.com serveri 1. kim biladi? "com dan so'ra" 2. kim biladi? "softromeda dan so'ra" 3. kim biladi? "mana manzil" javob Siz bitta so'rov yuborasiz, hal qiluvchi esa uchtasini Har bosqichda javob emas, "kimdan so'rash kerakligi" aytiladi - bunga yo'llanma deyiladi Keshlash tufayli amalda bu uch qadam kamdan-kam to'liq bajariladi
Dunyoda 13 ta ildiz server manzili bor - lekin ular yuzlab nusxada ishlaydi

So'rov paketini qo'lda yasash #

DNS paketi 12 baytlik sarlavha bilan boshlanadi. Nom esa maxsus usulda kodlanadi: har bo'lak oldiga uzunligi yoziladi.

Python
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"))
Natija
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.

So'rov ID nima uchun kerak?

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:

Python
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}")
Natija
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'rsatkichi

Javobdagi 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:

Python
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)
Natija
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.

Haqiqiy serverga so'rov yuborish

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:

Terminal
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 #

TuriNima saqlaydiMisol
AIPv4 manzil93.184.216.34
AAAAIPv6 manzil2606:2800::1946
CNAMEBoshqa nomga taxalluswwwsoftromeda.com
MXPochta serverimail.softromeda.com
NSZonaning nom serverins1.provayder.com
TXTIxtiyoriy matnSPF, tasdiqlash kodlari
PTRManzildan 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:

Python
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)
Natija
  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
TTL - server ko'chirishdagi asosiy tuzoq

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:

QachonNima qilinadi
Ko'chirishdan 1 kun oldinTTL ni 300 soniyaga tushiring
Ko'chirish kuniYozuvni o'zgartiring
5 daqiqadan keyinDeyarli hamma yangisiga o'tadi
Bir necha kundan keyinTTL ni odatdagi qiymatga qaytaring

TTL ni ko'chirish kuni tushirish kech: eski, uzun TTL li yozuv allaqachon keshlarda tarqalib ketgan.

DNS ni soxtalashtirish

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:

VositaNima qiladi
DNSSECJavoblarni imzolaydi - soxtasi rad etiladi
DoH / DoTSo'rovni HTTPS yoki TLS ichida shifrlaydi

Ular bir-birini almashtirmaydi: DNSSEC haqiqiyligini, DoH esa maxfiyligini ta'minlaydi.

Amaliy topshiriq
  1. So'rov yasash misolini ishga tushiring.
  2. Nomni edu.softromeda.com ga o'zgartiring - paket necha bayt bo'ldi?
  3. AAAA (28) turi uchun so'rov yasang.
  4. Javobni o'qish misolini ishga tushiring.
  5. c00c ni haqiqiy nom bilan almashtirib, natija bir xil ekanini ko'ring.
  6. Mahalliy DNS server misolini ishga tushiring.
  7. ZONA ga ikkinchi domen qo'shing va serverni moslashtiring.
  8. dig softromeda.com ni ishga tushiring va bo'limlarni o'qing.
  9. Kesh misolini ishga tushiring va TTL ni 60 ga o'zgartiring.
  10. 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.

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.