Foundationপ্রথম নীতি থেকে
LEVEL 7লেসন ১৫/২২অ্যাডভান্সড১ ঘণ্টা

DNS — phonebook যা internet ধরে রাখে

DNS: Resolvers, Records, Caching, DNSSEC

DNS একটা বিতরণকৃত গাছ — root থেকে authoritative পর্যন্ত delegation-এর সিঁড়ি বেয়ে নাম ঠিকানায় বদলায়, cache আর TTL সেই খরচ কয়েকগুণ কমিয়ে রাখে, আর wire-এর নিচে চলে ছোট্ট একটা binary protocol যা আজ আমরা নিজের হাতে parse করব।

এই লেসন শেষে আপনি পারবেন

  • Root, TLD, authoritative — তিন-স্তরের hierarchical namespace আঁকতে ও delegation-এর যুক্তি ব্যাখ্যা করতে পারবেন: কে কার ঠিকানা জানে না, কিন্তু উত্তরটা কার কাছে আছে সেটা জানে
  • Recursive বনাম iterative resolution-এর পার্থক্য বলে, dig output-এর flags আর sections পড়ে একটা সত্যিকারের query-র referral chain হাতে trace করতে পারবেন
  • A, AAAA, CNAME, NS, MX, TXT, SOA — প্রতিটা record type-এর কাজ চিনে কোন পরিস্থিতিতে কোনটা লাগে সিদ্ধান্ত নিতে পারবেন
  • Browser, stub, resolver — তিন স্তরের cache-এর মধ্য দিয়ে উত্তর কীভাবে ফেরে আর TTL-এর সংখ্যা কোন অর্থনৈতিক চাপে বড়-ছোট হয় তা হিসাব করতে পারবেন
  • DNS message-এর 12-byte header, question ও RR section হাতে parse করতে পারবেন — 0xC0 compression pointer-সহ, কোনো library ছাড়া

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে আমরা connect(("127.0.0.1", 9090)) লিখে নিজের echo server-এ ঢুকেছিলাম। বাস্তবে আপনি কখনো IP লিখে browser-এ ঢোকেন না — লেখেন https://example.com; অথচ TCP connection-এর জন্য kernel-এর দরকার numeric address। চোখের আড়ালেই একটা প্রশ্ন হয়ে গেছে — “example.com মানে কোন সংখ্যা?” — উত্তর এসেছে একটা বিশাল, বিশ্বজোড়া, চার দশক পুরনো distributed system থেকে। এটাই DNS — Domain Name System।

উপর থেকে দেখলে DNS একটা phonebook-এর মতোই: নাম দিলে নম্বর দেয়। কিন্তু মিলটা এখানেই শেষ। এই phonebook- কারো হাতে এক খণ্ডে নেই — internet-এর প্রতিটা নাম কয়েক লাখ আলাদা server-এ ছড়ানো; প্রতিটা server জানে মাত্র নিজের ছোট্ট অংশটুকু আর পরের অংশটা কার কাছে আছে; উত্তরগুলো রাস্তার প্রতিটা মোড়ে photocopy করে রেখে দেয় (cache), প্রতিটা photocopy-র মেয়াদ আলাদা ঘড়িতে ফুরোয় (TTL); আর পুরো কথোপকথনটা চলে দুইটা ছোট্ট UDP packet দিয়ে — যার binary layout আজ আমরা byte ধরে পড়ব।

আজকের যাত্রাপথ: root/TLD/authoritative-র delegation-এর সিঁড়ি (কেউ সব ঠিকানা জানে না, তবুও সব উত্তর বের হয়); A/AAAA/CNAME/NS/MX/TXT/SOA record-এর ভোকাবুলারি; recursive বনাম iterative resolution — dig দিয়ে সত্যিকারের chain walk; wire format — 12-byte header, flags আর compression pointer-এর 0xC0 ফাঁদ; শেষে cache-র তিন স্তর, TTL-র অর্থনীতি, DNSSEC কী রক্ষা করে কী করে না, আর কেন সবাই HTTPS-র ভেতর DNS ঢুকিয়ে নিচ্ছে। আজকের build-এ গত লেসনের sockets API-র UDP হাতিয়ারই লাগবে — একটা সৎ query builder + parser, কোনো library ছাড়া।

মূল ধারণা

নাম-জগতটা একটা গাছ — আর প্রতিটা পাতা এক জায়গায় নেই

DNS-র সবচেয়ে গুরুত্বপূর্ণ ১৯৮৩-র সিদ্ধান্তটা নামের structure নিয়ে: সব নাম বসবে একটা উল্টো গাছে — hierarchical namespace। গোড়া হলো ফাঁকা নাম (root, লেখা হয় শুধু বিন্দু .), তার সন্তানরা TLD, তার সন্তানরা আরও নিচে:

                         .            (root zone)
        ┌──────────┬─────┴─────┬──────────┐
       com        org         net         bd
        │                                  │
   example.com                           co.bd
    ┌─────┴─────┐
  www          api

Browser-এ লেখা নামের আসল রূপ www.example.com. — শেষের বিন্দুটাই গোড়ার ইশারা; একে বলে FQDN। কিন্তু structure-টা শুধু নাম সাজানোর নিয়ম না — কাজ ভাগ করার নিয়ম। গাছের প্রতিটা কাটা টুকরো একটা zone, প্রতিটা zone-র মালিক name server-দের একটা দল, যারা শুধু নিজের zone-র উত্তর জানে। এই ভাগটাই delegation:

স্তরকে বসেকী জানেকী জানে না
Root১৩টা logical root server (A–M)সব TLD-র NSexample.com-র IP
TLD.com চালায় registry operatorexample.com-র NSwww.example.com-র IP
Authoritativezone-র নিজের DNS (self-hosted বা provider)নিজের প্রতিটা recordঅন্য কারো zone

প্যাটার্নটা খেয়াল করুন: প্রত্যেকে পরের স্তরের ঠিকানা জানে, উত্তরটা নয়। Root জানে না www.example.com-র IP — জানে “.com-র খবর এই তেরোজনের কাছে”; .com জানে “example.com-র খবর এই server-দের কাছে”; শেষ স্তরে গিয়েই প্রথম কেউ বলে “IP হলো 93.184.216.34”। উত্তর-দেওয়া অবস্থাটার নাম authoritative, মাঝের প্রতিটা ধাপ referral — “আমি জানি না, ও-টার কাছে জিজ্ঞেস করো।”

রেকর্ড — phonebook-র entry-গুলোর ধরন

Zone-র ভেতরের প্রতিটা তথ্য একটা resource record; আকৃতি NAME TTL CLASS TYPE RDATA — নাম, মেয়াদ, IN (internet), ধরন, ধরন-ভেদে ডেটা:

Typeকাজউদাহরণ
Aনাম → IPv4example.com. 3600 IN A 93.184.216.34
AAAAনাম → IPv6example.com. 3600 IN AAAA 2606:2800:...:1946
CNAMEalias → canonical নাম (IP নয়!)www.example.com. 300 IN CNAME example.com.
NSএই zone কার কাছে — delegation-র প্রাণexample.com. 172800 IN NS a.iana-servers.net.
MXইমেইল কোথায় যাবে + priorityexample.com. 3600 IN MX 10 mail.example.com.
TXTমুক্ত টেক্সট — SPF, DKIM, verificationexample.com. 3600 IN TXT "v=spf1 -all"
SOAzone-র পরিচয়পত্র — primary NS, serial, timerexample.com. IN SOA ns.icann.org. noc.dns.icann.org. ...

দুইটা সূক্ষ্মতা মুখস্থ রাখুন। এক, CNAME-র নামে অন্য কিছু বসে না: CNAME মানে “এই নামটা আসলে ওই নামেরই অন্য নাম”, তাই একই owner name-এ CNAME-র সাথে A বা MX রাখা illegal; আর zone-র apex-এ (যেমন example.com নিজে) CNAME বসানো যায়ই না — apex-এ SOA আর NS বাধ্যতামূলক। CDN-provider-দের “ALIAS”/“ANAME”-ই এই ফাঁক ভরাটের provider-side hack — DNS-র feature নয়। দুই, SOA-র serial আর timer secondary server-দের sync-র ঘড়ি: serial বদলালে তারা বোঝে update হয়েছে; আর SOA-র শেষ field দেয় negative caching-র TTL — “এমন নাম নেই” জাতীয় উত্তরও কিছুক্ষণ cache হয়।

Query-র যাত্রা — recursive বনাম iterative

আপনার process গাছে হাঁটে না; হাঁটে দুই ধরনের agent। Stub resolver — OS-র ভেতরের ছোট্ট client (getaddrinfo-র পেছনে): একটা প্রশ্ন করে, চূড়ান্ত উত্তরের অপেক্ষা করে, হাঁটে না। Recursive resolver — ISP-র DNS বা 1.1.1.1, 8.8.8.8: stub-র হয়ে পুরো গাছ হেঁটে চূড়ান্ত উত্তর এনে দেয়। হাঁটে কীভাবে? Iteratively — প্রতিটা স্তরে গিয়ে referral নিয়ে নিচে নামে:

www.example.com জানতে চাওয়ার পুরো বংশলতিকা
  1. Browser → stubbrowser cache মিস, getaddrinfo চালু — OS-র stub resolver-ও মিস
  2. Stub → recursive resolverRD flag সেট করে প্রশ্ন: আমার হয়ে পুরো পথ হেঁটে দাও
  3. Resolver → rootroot উত্তর দেয় না, referral দেয়: com-র NS-রা এই তেরোজন
  4. Resolver → TLD (.com).com referral দেয়: example.com-র NS এই server-গুলো
  5. Resolver → authoritativeaa flag-সহ চূড়ান্ত উত্তর: CNAME + A record
  6. ফেরত পথে সবাই কপি রাখেresolver TTL-সহ নিজের cache-এ জমায়; উত্তর উপরে stub-এ, তারপর browser-এ

গুলিয়ে ফেলার সুযোগ আছে, তাই স্পষ্ট করি: recursive বলছে কে দায়িত্ব নিচ্ছে (resolver পুরো কাজটা করবে), iterative বলছে যেভাবে করছে (referral ধরে ধরে, স্তরে স্তরে)। Query-টা recursive, walk-টা iterative।

Cache — তিন স্তরের photocopy, আর TTL-র অর্থনীতি

Walk-টা যদি প্রতিটা hostname-এর জন্য প্রতিবার হতো, root-রা প্রতি সেকেন্ডে কোটি কোটি প্রশ্ন পেত, page-load শুরু হতো ~৩০০ ms দেরিতে। বাস্তবে উত্তর ফেরার পথে তিন জায়গায় photocopy হয়:

স্তরকারসাধারণ আয়ু
Browser cacheChrome/Firefox নিজের process-এমিনিট-পরিমাণ
OS stub cachesystemd-resolved / mDNSResponder / Windows DNS Clientrecord-র TTL পর্যন্ত
Recursive resolver cacheISP / 1.1.1.1 / 8.8.8.8 — সবার sharedrecord-র TTL পর্যন্ত

শেষ স্তরটাই আসল জাদু: এক resolver-র লক্ষ user একই নাম চাইলে গাছের কাছে যায় মাত্র একবার। প্রতিটা photocopy-র গায়ে লেখা মেয়াদ — TTL (Time To Live), সেকেন্ডে; cache-এ ঢোকার মুহূর্ত থেকে countdown শুরু, শূন্য হলে entry মৃত। আর TTL-টা কারিগরি প্যারামিটার না — অর্থনৈতিক সিদ্ধান্ত: ছোট TTL (৩০–৬০ s) মানে ঠিকানা বদলালে বিশ্ব দ্রুত follow করে (failover, traffic-shifting-র প্রাণ) কিন্তু resolver-রা ঘন ঘন আপনার authoritative server-এ ফেরত আসে; বড় TTL (৮৬৪০০ s = ১ দিন) মানে load-latency প্রায় শূন্য কিন্তু ভুল ঠিকানা ঠিক করতে লাগে TTL-পরিমাণ সময় — যেটা আপনার হাতে নেই, বিশ্বের সব cache-র হাতে।

ভেতরে কী ঘটছে

UDP-first, TCP-fallback — 512 byte-র পুরনো সীমা

DNS query চলে UDP port 53-এ, কারণ লেনদেনটার আকৃতিই datagram-র: একটা প্রশ্ন, একটা উত্তর — TCP handshake-র ১-RTT বাড়তি খরচ, connection state, teardown কোনোটারই দরকার নেই; হারালে timeout ধরে আবার পাঠানো সস্তা। udp লেসনের thesis-র live প্রমাণ: request-response আকৃতির কাজে UDP-ই স্বাভাবিক ভাষা। কিন্তু UDP datagram-র নিরাপদ সীমা ছিল 512 byte (fragmentation এড়ানোই মূলমন্ত্র)। উত্তর বড় হয়ে গেলে — বড় NS তালিকা, DNSSEC signature — server পুরো উত্তর না পাঠিয়ে TC (Truncated) flag জ্বালিয়ে দেয়; client দেখেই বোঝে একই প্রশ্ন এবার TCP-তে করি — একই port 53-এ TCP-তে। UDP-first, TCP-fallback।

দুইটা exception মনে রাখুন। AXFR (zone transfer — primary থেকে secondary-তে পুরো zone কপি) সবসময়ই TCP — উত্তর গোটা stream। আর EDNS0 extension সীমাটাই বাড়িয়ে দেয়: client প্রথম query-তেই ঘোষণা করে “আমি 1232/4096 byte নিতে পারি” — তাই আজকাল বেশিরভাগ উত্তর TCP-তে না গিয়েই UDP-তে ফিট হয়।

Wire format — 12 byte যে বলে সব

প্রতিটা DNS message — প্রশ্ন হোক বা উত্তর — একই কঙ্কালে: header, question, তারপর তিনটা RR section (পুরো layout-এর মানচিত্র diagram section-এ)। Header-র bit-গুলো একে একে:

FieldBitsঅর্থ
ID16প্রশ্নের টিকিট নম্বর — উত্তর এই ID-ই ফেরত দেয়
QR10 = query, 1 = response
Opcode4সাধারণত 0 (standard query)
AA1Authoritative Answer — উত্তর zone-মালিকের মুখের কথা
TC1Truncated — অসম্পূর্ণ, TCP-তে আবার জিজ্ঞেস করো
RD1Recursion Desired — client চেয়েছে “আমার হয়ে হেঁটে দাও”
RA1Recursion Available — server বলছে “হেঁটে দিতে পারি”
Z3reserved — শূন্য
RCODE40=NOERROR, 2=SERVFAIL, 3=NXDOMAIN (নাম নেই), 5=REFUSED

তারপর ৪টা 16-bit count — প্রতিটা section-এ কয়টা entry — এবং question section: QNAME (labels), QTYPE, QCLASS। RR-র wire আকৃতি সব section-এ হুবহু এক: NAME, TYPE (2), CLASS (2), TTL (4), RDLENGTH (2), RDATA (RDLENGTH byte)। Referral-র ফাঁদটা এখনই চিনুন: root-র উত্তরে ANSWER = 0 আর AUTHORITY = 13 — delegation-ও বৈধ response, শুধু answer section খালি।

Name compression — 0xC0 trick

একই নাম এক message-এ বারবার আসে: question-এ জিজ্ঞেস করলাম www.example.com, প্রতিটা answer record-ও সেই নামেই শুরু। RFC 1035-র সমাধান: নামের প্রথম byte-র top-two-bits যদি 11 হয়, সেটা label-length না — pointer; বাকি 14 bit = message-র শুরু থেকে offset:

offset   bytes                        মানে
-------  ---------------------------  ------------------------
0x0C     03 77 77 77                  "www"       (len=3)
0x10     07 65 78 61 6d 70 6c 65      "example"   (len=7)
0x18     03 63 6f 6d                  "com"       (len=3)
0x1C     00                           root — নাম শেষ

Answer-র NAME field (offset 0x1D থেকে):
0x1D     C0 0C                        pointer → offset 0x0C!

0xC0 = binary 11000000 — top-bits বলছে “pointer”, নিচের 6 bit + পরের full byte = 0x0C = 12 = question-র QNAME ঠিক যেখানে শুরু। 16-byte-র নাম লেখা হলো 2-byte-এ। Label-র দৈর্ঘ্য সর্বোচ্চ 63 বলে top-two-bits সবসময় ফাঁকা — length আর pointer কখনো conflict করে না। Pointer পেছনের যে-কোনো আগের অবস্থানে যেতে পারে, তবে দুষ্টু packet-এ loop বানানো সম্ভব — সৎ parser-এ guard রাখতেই হয় (build-এ দেখব)।

উদাহরণ

একটা পূর্ণ resolution — dig-র চোখে

প্রথমে সাধারণ পথ — আপনার মেশিন → local recursive resolver:

dig www.example.com A
;; ->>HEADER\<<- opcode: QUERY, status: NOERROR, id: 42891
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; QUESTION SECTION:
;www.example.com.           IN  A

;; ANSWER SECTION:
www.example.com.    300     IN  CNAME   example.com.
example.com.        3600    IN  A       93.184.216.34

;; Query time: 42 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
;; MSG SIZE  rcvd: 76

তিনটা জিনিস পড়তে শিখুন। flags: qr rd ra — এটা response (qr), recursion চাওয়া হয়েছিল (rd), server দিতে পারে (ra); aa নেই — উত্তর cache থেকে, zone-মালিকের কাছ থেকে না। দুইটা answer একসাথে — CNAME তারপর A: resolver CNAME follow করে শেষ IP-টাও এনে দিয়েছে, বাড়তি কাজটা সে-ই করে নিয়েছে। Query time: 42 msec — এটা cold পাঠ: cache ফাঁকা ছিল, পুরো hierarchy হাঁটতে হয়েছে; একই প্রশ্ন আবার করলে সংখ্যা প্রায় শূন্যে নামব (experiment-এ প্রমাণ করব)।

এবার resolver বাইপাস করে নিজেরাই iterative walk — +norecurse দিয়ে বলছি “referral-ই দাও”:

ধাপ ১ — root-কে সোজা জিজ্ঞেস (198.41.0.4 = a.root-servers.net):

dig @198.41.0.4 www.example.com A +norecurse
;; ->>HEADER\<<- opcode: QUERY, status: NOERROR, id: 51234
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 13, ADDITIONAL: 17

;; AUTHORITY SECTION:
com.                172800  IN  NS  a.gtld-servers.net.
com.                172800  IN  NS  b.gtld-servers.net.
...                                     (মোট ১৩টা — এখানে ছেঁটা)
;; Query time: 88 msec

hood-র কথা হুবহু মিলল: ANSWER: 0, AUTHORITY: 13 — root উত্তর দেয়নি, delegation দিয়েছে; আর ra নেই — root recursion-ই করে না।

ধাপ ২ — .com TLD-কে (192.5.6.30 = a.gtld-servers.net):

dig @192.5.6.30 www.example.com A +norecurse
;; ->>HEADER\<<- opcode: QUERY, status: NOERROR, id: 52781
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 3

;; AUTHORITY SECTION:
example.com.        172800  IN  NS  a.iana-servers.net.
example.com.        172800  IN  NS  b.iana-servers.net.

আরেকটা referral — authority section-র owner নাম বদলেছে com. থেকে example.com.: গাছে এক ধাপ নিচে।

ধাপ ৩ — authoritative server-কে (199.43.135.53 = a.iana-servers.net):

dig @199.43.135.53 www.example.com A
;; ->>HEADER\<<- opcode: QUERY, status: NOERROR, id: 53309
;; flags: qr rd aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; ANSWER SECTION:
www.example.com.    300     IN  CNAME   example.com.
example.com.        3600    IN  A       93.184.216.34

এবার aa — authoritative answer; এই উত্তর photocopy না, zone-র মূল খাতা থেকে। পুরো walk-র হিসাবখাতা:

HopServerযা পেলামসময়
১root (198.41.0.4)referral: .com-র ১৩ NS88 ms
২.com (192.5.6.30)referral: example.com-র NS~120 ms
৩authoritative (199.43.135.53)CNAME + A, aa flag~95 ms

তিন RTT, ~৩০০ ms — এইজন্যই cache-র তিন স্তর। আর এইজন্যই resolver-রা NS referral-ও cache রাখে: পরেরবার root পর্যন্ত যেতেই হয় না।

ছবিতে

পুরো ব্যবস্থা এক ফ্রেমে

┌──────────────────┐  প্রশ্ন: www.example.com = ?
│ Browser          │─ cache miss ─▼
├──────────────────┤
│ OS stub resolver │─ cache miss ─▼  RD=1
└────────┬─────────┘                │
         ▼                          │
┌───────────────────────────────────────────────────┐
│ RECURSIVE RESOLVER   (ISP / 1.1.1.1 / 8.8.8.8)    │
│                                                   │
│  cache HIT? ──হ্যাঁ──► সঙ্গে সঙ্গে উত্তর (~1 ms)    │
│     │ না                                          │
│     ▼  iterative walk                             │
│  root ─ref─► .com ─ref─► example.com NS ─aa─► A   │
│                                                   │
│  উত্তর + referral সব cache-এ TTL-সহ জমা            │
└────────┬──────────────────────────────────────────┘
         ▼  উত্তর + TTL
   browser ও OS cache-ও নিজেদের কপি রাখে
তিন স্তরের cache-র নিচ দিয়ে iterative walk — browser আর OS শুধু প্রশ্ন করে; হাঁটা আর মনে রাখা দুটোই recursive resolver-র কাজ।

আর wire-র কঙ্কালটা খাড়াভাবে — প্রতিটা DNS message এই ছাঁচেই ঢলে:

┌───────────────────────────────┐
│ ID (16)  │ flags (16)         │ ← QR Opcode AA TC RD RA Z RCODE
├───────────────────────────────┤
│ QD │ AN │ NS │ AR  (৪×16 bit) │ ← প্রতিটা section কত entry
├───────────────────────────────┤
│ QUESTION:  QNAME, TYPE, CLASS │ ← নাম = length-prefixed labels
├───────────────────────────────┤
│ ANSWER:    NAME TYPE CLASS    │
│            TTL RDLEN RDATA    │ ← তিন RR section-ই এই ছাঁচে
├───────────────────────────────┤
│ AUTHORITY: (একই RR ছাঁচ)       │ ← referral-ও এখানেই
├───────────────────────────────┤
│ ADDITIONAL: (একই RR ছাঁচ)      │
└───────────────────────────────┘
RFC 1035 message layout — header সবসময় 12 byte, পরের section-গুলোর সংখ্যা ঠিক করে শেষ ৮ byte-র চারটা count।

নিজে চালিয়ে দেখুন

EXPERIMENT

dig +trace দিয়ে hierarchy হাঁটা, আর TTL countdown চোখে দেখা

macOS বা Linux (dig — bind-utils/dnsutils)· ২০ মিনিট

ধাপ ১ — পুরো hierarchy এক command-এ:

dig +trace www.example.com A

+trace নিজেরাই iterative walk চালায় — প্রতিটা স্তরের কাছ থেকে referral নিয়ে নিচে নামে, প্রতিটা ধাপ ছাপে। Output বড়; সংক্ষেপে কেটে দেখাচ্ছি:

.                       3600000 IN  NS  a.root-servers.net.
...                                     (root-র ১৩ NS — শুরুর hint)
;; Received 1097 bytes from 192.168.1.1#53(192.168.1.1) in 12 ms

com.                    172800  IN  NS  a.gtld-servers.net.
...                                     (.com-র NS — root-র referral)
;; Received 867 bytes from 198.41.0.4#53(a.root-servers.net) in 88 ms

example.com.            172800  IN  NS  a.iana-servers.net.
...                                     (delegation — TLD-র referral)
;; Received 589 bytes from 192.5.6.30#53(a.gtld-servers.net) in 120 ms

www.example.com.        300     IN  CNAME   example.com.
example.com.            3600    IN  A       93.184.216.34
;; Received 106 bytes from 199.43.135.53#53(a.iana-servers.net) in 95 ms

পড়ার নিয়ম: প্রতিটা ব্লকের শেষ লাইন বলে কার কাছ থেকে ব্লকটা এলো — প্রথমে local resolver, তারপর ঠিক উপরের ধাপের server। তিন স্তর, তিন RTT — example section-র হিসাবখাতার live সংস্করণ।

ধাপ ২ — TTL countdown:

for i in 1 2 3 4 5 6; do
  dig +noall +answer www.example.com
  sleep 2
done

প্রত্যাশিত প্যাটার্ন:

www.example.com.    297     IN  CNAME   example.com.
example.com.        3597    IN  A       93.184.216.34
www.example.com.    295     IN  CNAME   example.com.
example.com.        3595    IN  A       93.184.216.34
www.example.com.    293     IN  CNAME   example.com.
example.com.        3593    IN  A       93.184.216.34
...

TTL-র সংখ্যা বয়সের মতো কমছে — প্রতি iteration-এ ~২। মানে উত্তর resolver-র cache থেকে আসছে: fresh হলে প্রতিবার ৩০০/৩৬০০ থেকে শুরু হতো। আপনি cache-র ঘড়ির কাঁটা দেখছেন।

ধাপ ৩ — cold বনাম warm latency:

dig +noall +stats www.example.com | grep 'Query time'
dig +noall +stats www.example.com | grep 'Query time'
dig +noall +stats www.example.com | grep 'Query time'

প্রথমবার (বা TTL ফুরোনোর পরে): ২০–১০০ ms — পুরো walk। এরপর প্রতিবার: ০–৩ ms — cache hit, hierarchy-তে একটা packet-ও যায়নি। ফারাকটাই caching-র অর্থনীতি। বোনাস: dig @1.1.1.1 www.example.com A +noall +answer — অন্য resolver-র কপিতে একই নাম, TTL সংখ্যা ভিন্ন, কারণ তার countdown শুরু হয়েছিল ভিন্ন মুহূর্তে: “TTL = প্রতিটা cache-র নিজস্ব ঘড়ি”-র প্রমাণ।

এটা কী প্রমাণ করে

+trace-র প্রতিটা ধাপ দেখায় root → TLD → authoritative referral chain সত্যিই ঘটে; একই নাম বারবার জিজ্ঞেস করলে TTL সংখ্যা ঠিক ঘড়ির মতো নামে — resolver-র cache-র photocopy live countdown করছে; আর Query time ৪০+ ms থেকে ১-২ ms-এ নামা প্রমাণ করে warm path-এ hierarchy-তে যাওয়াই হয় না।

নিজে বানান

BUILD IT

DNS query builder + response parser — struct আর socket ছাড়া কিছুই নেই

Python · ●●●○○
  1. encode_name — নামকে length-prefixed label sequence-এ ফেলুন, 00 byte-এ শেষ
  2. build_query — 12-byte header pack (ID, RD flag, QDCOUNT=1) + question section
  3. UDP socket দিয়ে resolver-র port 53-এ পাঠান; উত্তরের ID মিলিয়ে নিন
  4. parse_response — header flags আর ৪টা count unpack করে question section পেরোন
  5. RR loop — read_name-এ compression pointer follow করুন, RDATA type-ভেদে decode করে টেবিল ছাপুন

লক্ষ্য: আজকের পুরো তত্ত্ব — header flags, RR ছাঁচ, 0xC0 pointer — একটা ~১১০ লাইনের সৎ parser-এ, কোনো DNS library ছাড়া (dnspython-র নামও নেই)। এটাই গত লেসনের sockets API-র UDP রূপের প্রথম বাস্তব কাজে লাগানো।

# dnsq.py — python3 dnsq.py www.example.com [resolver-ip]
import random
import socket
import struct
import sys


def encode_name(name: str) -> bytes:
    """'www.example.com' → 03www07example03com00"""
    wire = b""
    for label in name.rstrip(".").split("."):
        part = label.encode()
        wire += bytes([len(part)]) + part
    return wire + b"\x00"


def build_query(name: str, qtype: int = 1):
    """A query — RD=1 (recursion চাইছি), QCLASS=IN"""
    qid = random.randint(0, 0xFFFF)
    flags = 0x0100                      # RD bit
    header = struct.pack("!HHHHHH", qid, flags, 1, 0, 0, 0)
    return qid, header + encode_name(name) + struct.pack("!HH", qtype, 1)


def read_name(msg: bytes, off: int):
    """wire-থেকে নাম; 0xC0 pointer follow করে।
    return: (নাম, পরের field কোথা থেকে শুরু)"""
    labels, jumped, next_off = [], False, off
    while True:
        length = msg[off]
        if length & 0xC0 == 0xC0:            # compression pointer
            target = ((length & 0x3F) << 8) | msg[off + 1]
            if not jumped:
                next_off = off + 2           # ফেরার ঠিকানা একবারই
            off = target; jumped = True
        elif length == 0:                    # root label — নাম শেষ
            if not jumped:
                next_off = off + 1
            return ".".join(labels), next_off
        else:                                # সাধারণ label
            labels.append(msg[off + 1 : off + 1 + length].decode())
            off += 1 + length


TYPES = {1: "A", 2: "NS", 5: "CNAME", 6: "SOA", 15: "MX", 28: "AAAA"}
RCODES = {0: "NOERROR", 2: "SERVFAIL", 3: "NXDOMAIN", 5: "REFUSED"}


def parse_response(msg: bytes):
    rid, flags, qd, an, ns, ar = struct.unpack("!HHHHHH", msg[:12])
    fl = {"QR": (flags >> 15) & 1, "AA": (flags >> 10) & 1,
          "TC": (flags >> 9) & 1, "RD": (flags >> 8) & 1, "RA": (flags >> 7) & 1}
    print("id={} {}".format(rid, " ".join(f"{k}={v}" for k, v in fl.items())))
    print("RCODE={}  QD={} AN={} NS={} AR={}".format(
        RCODES.get(flags & 0xF), qd, an, ns, ar))

    off = 12
    for _ in range(qd):                      # question section পেরোনো
        _, off = read_name(msg, off)
        off += 4                             # QTYPE + QCLASS

    print("{:<26} {:>7} {:<7} RDATA".format("NAME", "TTL", "TYPE"))
    for count in (an, ns, ar):               # তিন RR section-ই এক ছাঁচে
        for _ in range(count):
            name, off = read_name(msg, off)
            rtype, _cls, ttl, rdlen = struct.unpack("!HHIH", msg[off:off + 10])
            off += 10
            end = off + rdlen
            if rtype == 1:                   # A — ৪ byte IPv4
                rdata = ".".join(str(b) for b in msg[off:end])
            elif rtype in (2, 5):            # NS / CNAME — RDATA নামই
                rdata, _ = read_name(msg, off)
            elif rtype == 15:                # MX — 2-byte pref + নাম
                pref = struct.unpack("!H", msg[off:off + 2])[0]
                host, _ = read_name(msg, off + 2)
                rdata = "{} {}".format(pref, host)
            elif rtype == 28:                # AAAA — 16 byte IPv6
                rdata = ":".join(msg[i:i + 2].hex() for i in range(off, end, 2))
            else:                            # SOA/TXT/OPT — আপাতত raw
                rdata = msg[off:end].hex()
            print("{:<26} {:>7} {:<7} {}".format(
                name, ttl, TYPES.get(rtype, rtype), rdata))
            off = end


def main():
    name = sys.argv[1] if len(sys.argv) > 1 else "www.example.com"
    server = (sys.argv[2] if len(sys.argv) > 2 else "1.1.1.1", 53)
    qid, query = build_query(name)
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP-first!
    s.settimeout(3)
    s.sendto(query, server)
    reply, _ = s.recvfrom(4096)
    assert struct.unpack("!H", reply[:2])[0] == qid, "ID mismatch!"
    parse_response(reply)


main()

চালিয়ে দেখুন:

$ python3 dnsq.py www.example.com
id=39172 QR=1 AA=0 TC=0 RD=1 RA=1
RCODE=NOERROR  QD=1 AN=2 NS=0 AR=1
NAME                        TTL    TYPE    RDATA
www.example.com.             297   CNAME   example.com.
example.com.                 359   A       93.184.216.34
00fb01c00102030405...

তিনটা জিনিস লক্ষ্য করুন। এক — উত্তরের NAME কলামে পুরো নাম, অথচ wire-এ সেখানে মাত্র C0 0C ছিল: read_name pointer follow করে নাম পুনর্গঠন করেছে। দুই — TTL ৩৬০ না, ৩৫৯-ঘেঁঁটে: কপিটা cache-এ কিছুক্ষণ আগেই এসেছে (experiment-র countdown এখানেও)। তিন — শেষ row hex-এ, কারণ সেটা OPT (rtype 41) — EDNS0-র pseudo-record, hood-র 1232-byte কথার স্বাক্ষর; আসল RR না বলে decoder raw-ই রেখেছে।

নিজে বাড়ান:

  1. TC=1 দেখলে একই query TCP-তে আবার পাঠানোর fallback লিখুন — বড় NS তালিকাওলা নাম দিয়ে পরীক্ষা করুন
  2. read_name-এ visited-offset set যোগ করে pointer-loop guard দিন, তারপর হাতে C0 00 C0 00-জাতীয় দুষ্টু packet বানিয়ে guard যাচাই করুন
  3. SOA আর TXT-র decode যোগ করুন — python3 dnsq.py example.com NS @a.gtld-servers.net চালিয়ে delegation-র NS তালিকা নিজের parser দিয়েই দেখুন

বাস্তব সিস্টেমে

বাস্তব জগতে — আর কেন পুরো internet এক বিকেলে গা ঢেকে যায়

৪ অক্টোবর ২০২১ — Facebook “internet থেকে হারিয়ে গেল।” Facebook নিজের BGP route withdraw করে বসেছিল — ফলে শুধু facebook.com না, তার authoritative name server-রাও দুনিয়ার resolvers-র নাগালের বাইরে। Cache-রা TTL ফুরোতেই খালি হতে থাকল, আর পুরো empire — app, website, ভেতরের badge/door-access system পর্যন্ত — এক মুহূর্তে অস্তিত্বহীন। শিক্ষা দুই স্তরে: DNS মরলে ব্যর্থতা দেখায় সবকিছুর মৃত্যু, কারণ সবকিছু তার ওপর দাঁড়ানো; আর বিপর্যয়ের মধ্যেও root আর TLD স্তর অটল ছিল — hierarchy-র ওপরের তলা কোনো এক zone-র সাথে পড়ে না, এটাই ডিজাইনের resilience।

২১ অক্টোবর ২০১৬ — Dyn DDoS। Mirai botnet (হ্যাকড CCTV আর router-র বাহিনী) ঢেউয়ের মতো ট্রাফিক ঢেলেছিল managed-DNS provider Dyn-র ওপর। ফল: Twitter, Netflix, PayPal, Reddit “down” — অথচ তাদের কোনো application server-ই মরেনি; মরেছিল phonebook। বাড়িগুলো অক্ষত, ঠিকানা-বই পোড়া। Mitigation-ও DNS-native: বিকল্প provider-এ failover, cache warm রাখা, multi-provider redundancy।

TTL হলো CDN-র steering wheel। CDN provider-রা নিজেদের উত্তরে ৩০–৬০ সেকেন্ডের ছোট TTL দেয় — ওরা DNS-কে ব্যবহার করে traffic-control plane হিসেবে: server অসুস্থ বা region ভারাক্রান্ত হলে কয়েক সেকেন্ডেই লক্ষ user-র প্রশ্ন অন্য edge-এ steer হয়। আপনার service-র নীতি উল্টো হতে পারে (stable infrastructure = বড় TTL) — সিদ্ধান্তটা conscious রাখুন, default-এ ছেড়ে দেবেন না।

Privacy-র মোড় — DoH আর DoT। Classic DNS চলে plaintext UDP-তে: রাস্তার যে-কেউ (Wi-Fi operator, ISP) প্রতিটা নাম-প্রশ্ন দেখতে পারে — আর নামের তালিকা মানেই browse-history-র খসড়া। সমাধানের ঢেউ: DoT (DNS over TLS — port 853) আর DoH (DNS over HTTPS — সেই 443, বাকি traffic-র ভিড়ে লুকিয়ে); browser আর OS-রা default করে চলেছে। বিতর্কটাও সত্যি: privacy লাভলেও প্রশ্নগুলো কয়েকটা বিশাল public resolver-এ কেন্দ্রীভূত হচ্ছে — বিকেন্দ্রীকরণ-জয়ের ওপর কেন্দ্রীকরণের ছায়া। আর মনে রাখুন: এসব শুধু গোপনীয়তা আনে — উত্তর সঠিক কি না তার নিশ্চয়তা দেয় আলাদা mechanism, DNSSEC।

যে ভুলগুলো সবাই করে

“DNSSEC চালু মানে আমার DNS query গোপন।”

ঠিক উল্টো প্রান্তের কথা। DNSSEC authenticate করে — signature-র chain-of-trust দিয়ে প্রমাণ করে উত্তরটা আসল zone-মালিকেরই আর পথে এক byte-ও বদলায়নি। কিন্তু encrypt করে না: signed postcard-র মতো — সই আছে, খাম নেই; observer নামটা আগের মতোই পড়ে। Chain শুরু হয় root-র স্বাক্ষরে — root-র public key validating resolver-দের built-in — তারপর root signs TLD, TLD signs zone। গোপনীয়তার হাতিয়ার আলাদা: DoT বা DoH। দুটো সমান্তরাল সমস্যার দুই সমাধান — integrity আর confidentiality; কেউ কারো বিকল্প না।

“CNAME তো সুবিধার জিনিস — apex-এও (example.com নিজে) বসিয়ে দিই।”

বসানো যাবে না — দুই কারণে। এক: একই owner name-এ CNAME থাকলে আর কোনো record থাকতে পারে না — CNAME মানে “এই নামের সব প্রশ্নের উত্তর ওই অন্য নামে যাও”। দুই: apex-এ SOA আর NS বাধ্যতামূলক — zone-কে অস্তিত্ব ঘোষণাই করতে হয়; CNAME + SOA একসাথে illegal। CDN-দের ALIAS/ANAME এই ফাঁক ভরাট করে — তাদের নিজের resolver resolution করে শেষ IP দেয়; স্ট্যান্ডার্ড DNS-র feature ভাববেন না।

“TTL ৩৬০০ দিলে ঠিক ১ ঘণ্টা পরে সব resolver-এ নতুন মান চলে আসবে।”

TTL হলো সর্বোচ্চ মেয়াদ — সবার একসাথে expire-র schedule না। প্রতিটা cache-র countdown শুরু হয় সে যখন fetch করেছে তখন, আর সেই মুহূর্তগুলো সারা বিশ্বে ছড়ানো: কোনো resolver-র কপি ৫৯ মিনিট বয়সী, কোনোটার ১ মিনিট। তাই বদলানোর পরেও পুরনো মান দেখার জানালা = পুরনো TTL + fresh-fetch-র spread — probabilistic window, schedule না। Playbook: আগে TTL কমান, অপেক্ষা করুন, তারপর বদলান।

“Firewall-এ 53/udp খুললেই DNS-র সব কাজ চলবে।”

পথগুলো একাদশে না। UDP/53 — প্রধান রাস্তা। TCP/53 — TC-flag fallback, AXFR, বড় DNSSEC উত্তর: বন্ধ থাকলে কিছু নামের উত্তরই আসবে না। আধুনিক দুইটা: DoT = 853/tcp, DoH = 443/tcp (HTTPS-র ভেতরে গুটিয়ে) — admin-দের জন্য সবচেয়ে বিভ্রান্তিকর, কারণ DoH ব্লক মানে সাধারণ HTTPS ব্লক। উল্টোদিকে captive portal পেরোতে মানুষ ইচ্ছা করেই 53/udp-তে tunnel করে — তাই অনেক network DNS-port নিজেদের resolver ছাড়া বন্ধ রাখে। Port-তালিকা ছোট, গল্পগুলো আলাদা।

বুঝেছেন কি না দেখুন

1

Recursive resolution আর iterative resolution-র পার্থক্য কী? কোন পক্ষ কোন flag সেট করে কী চায়, আর hierarchy-র হাঁটাটা আসলে কে করে?

স্মরণ

Recursive = দায়িত্বের চুক্তি: stub resolver recursive resolver-কে RD flag সেট করে বলে “চূড়ান্ত উত্তর এনে দাও”। Iterative = হাঁটার শৈলী: resolver প্রতিটা স্তরে গিয়ে referral নেয় — root দেয় .com-র NS, .com দেয় zone-র NS, authoritative দেয় aa-flagসহ চূড়ান্ত answer। হাঁটে একমাত্র recursive resolver; stub আর browser শুধু প্রশ্ন করে। Query-টা recursive, walk-টা iterative।

2

dig @198.41.0.4 www.example.com A +norecurse-র উত্তরে দেখলেন: flags: qr rd; — ANSWER: 0, AUTHORITY: 13, authority section ভরা com. IN NS ...gtld-servers.net.। Response-টা কী বলছে, কেন এটা error না?

প্রয়োগ

এটা delegation/referral — সম্পূর্ণ বৈধ response। Root www.example.com-র IP জানে না, জানার কথাই না; বলছে “উত্তরের দায়িত্ব .com TLD-র — এই ১৩জন NS”: ANSWER খালি, AUTHORITY-তে ১৩ NS। Flags পড়ুন: qr = response; rd শুধু echo (client চেয়েছিল, +norecurse-এ কাজে লাগেনি); ra নেই — root recursion offer করেই না; aa নেই — কোনো zone-র উত্তর দেয়নি, পথ দেখিয়েছে মাত্র।

3

Record-র TTL ৩০০। একটা recursive resolver রেকর্ডটা fetch করেছে 12:00:00-এ। আপনি 12:04:00-এ নতুন IP-তে update করলেন — ওই resolver-র user-রা সর্বশেষ কখন পুরনো IP দেখবে? সারা বিশ্বের সব resolver-র worst case?

প্রয়োগ

Countdown শুরু 12:00:00, TTL ৩০০ s → entry মরে 12:05:00-এ; user-রা পুরনো IP দেখবে 12:05 পর্যন্ত — update-র পরেও ~১ মিনিট। বিশ্ব-jুড়ে worst case = পুরনো TTL + fetch-র spread: যে cache update-র ঠিক আগে নিয়েছিল সেটাও পুরো TTL পুরনো মান serve করবে, আর প্রতিটা cache ভিন্ন মুহূর্তে fetch করেছে। এইজন্যই migration playbook: আগে TTL কমাও, তারপর বদলাও।

4

DNS কেন UDP-first? TCP দিয়ে শুরু করলে ঠিক কী খরচ বাড়ত? আর কোন তিনটা অবস্থায় TCP-তে যাওয়া ছাড়া উপায় নেই?

যুক্তি

Query-response এক-datagram-র আকৃতির: TCP নিলে প্রতিটা প্রশ্নে handshake-র ১-RTT বাড়তি latency + দুই প্রান্তে connection state + teardown খরচ — অথচ DNS-র transaction ছোট আর সংখ্যায় কোটি কোটি; stateless UDP-তে timeout+retransmit-ই যথেষ্ট। TCP অনিবার্য তিন ক্ষেত্র: (১) উত্তর 512 byte ছাড়িয়ে TC flag এলে (EDNS0 ছাড়া), (২) EDNS0-র size-ও ছাড়িয়ে গেলে, (৩) AXFR zone transfer — উত্তর গোটা stream। DoT/DoH-তে অবশ্য সব TCP(TLS)-র ভেতরেই — খরচটা privacy-র বিনিময়ে conscious।

5

উত্তরের কোনো RR-র NAME field-র শুরুতে পড়লেন C0 0C। এটা কী, কোথায় point করছে, label-length-র সাথে গুলিয়ে ফেলা কেন যায় না? দুষ্টু packet-এ বিপদ আর guard কী?

প্রয়োগ

0xC0 = 11000000 — top-two-bits 11 মানে compression pointer; বাকি 14 bit (0x0C) = message-র শুরু থেকে offset 12 = header-র ঠিক পরে — question-র QNAME, যেখানে পুরো নাম once লেখা। Parser সেখান থেকে নাম পড়ে pointer-পরের অবস্থানে (offset+2) ফেরে। গুলিয়ে ফেলা যায় না কারণ label-দৈর্ঘ্য সর্বোচ্চ 63 — top-two-bits সবসময় 00। বিপদ: pointer-কে pointer-এ loop করানো যায় — naive parser infinite loop-এ CPU খাবে (DoS)। Guard: jump-count limit বা visited-offset set।

6

Global service-কে regional failover ৩০ সেকেন্ডের ভেতরে করতে হবে — region মরলে user-রা দ্রুত অন্য region-এ যাবে। TTL কী হবে, কেন, কোন চাপ মাথায় রাখবেন? NXDOMAIN-র ফাঁদটাও ব্যাখ্যা করুন।

ডিজাইন

TTL ৩০–৬০ s — failover-র upper bound ≈ TTL। খরচ মাপুন: TTL ৩৬০০ থেকে ৬০-এ নামালে authoritative-র query load বহু গুণ বাড়ে — managed/anycast-authoritative scale দরকার। Health-check-ভিত্তিক dynamic answer যোগ করুন — DNS নিজেই মৃত endpoint বাদ দেয়। ফাঁদ: outage-এ region NXDOMAIN ফেরত দিলে সেই “নাম নেই” উত্তরও negative caching-এ জমে — region ফিরলেও কিছু user কয়েক মিনিট ব্যর্থ দেখবে; তাই outage-এও অন্য region-র IP সহ সত্যি answer-ই দিন।

এরপর কী

এরপর কী

আজ দেখলাম নাম ঠিকানায় বদলায় কোথায়। কিন্তু একটা প্রশ্ন পুরো লেসন ধরে পেছন থেকে চেঁচিয়েছে: আপনার মেশিন নিজের IP পেল কোথা থেকে? Wi-Fi-তে ঢোকার কয়েক সেকেন্ডেই ঠিকানা, gateway, subnet mask জায়গামতো বসে যায় — কেউ টাইপ করেনি। আর ভাবুন সেই গিঁটটাও: DNS-কে প্রশ্ন করতে resolver-র address লাগে — সেটাও কেউ দিয়েছে।

পরের লেসন — DHCP:

  • Discover, Offer, Request, Ack — চার-ধাপের নামতালি, আর শুরুটা broadcast দিয়ে কেন
  • Lease — ঠিকানাও ভাড়াটে, মেয়াদ ফুরোলে renewal
  • DHCP snooping কেন hotel Wi-Fi-র security checklist-এ থাকে
  • Boot-এর ক্রম: DNS-র আগে DHCP — কোন system কোনটার ওপর দাঁড়িয়ে আছে

আজকের শেষ প্রশ্নের (“resolver-র address এলো কোথা থেকে?”) উত্তরই পরের লেসনের প্রথম বাক্য।

আরও পড়ুন

  • RFC 1034 — Domain Names: Concepts and Facilities — P. Mockapetris · DNS-এর জন্মসনদ — namespace, delegation আর resolver-এর conceptual model এখানেই প্রথম সংজ্ঞায়িত
  • RFC 1035 — Domain Names: Implementation and Specification — P. Mockapetris · Wire format, resource record-এর binary encoding আর name compression — build অংশের মূল রেফারেন্স
  • Computer Networking: A Top-Down Approach, Chapter 2 — DNS section — James F. Kurose, Keith W. Ross · Recursive বনাম iterative resolution-এর সবচেয়ে পরিষ্কার পাঠ্যপুস্তক আলোচনা, latency-র হিসাবসহ
  • UNIX Network Programming, Volume 1 — Chapter 11 (Name and Address Conversions) — W. Richard Stevens, Bill Fenner, Andrew Rudoff · getaddrinfo-র নিচের DNS layer আর resolver behavior-এর practical ব্যাখ্যা