DNS — phonebook যা internet ধরে রাখে
DNS: Resolvers, Records, Caching, DNSSEC
DNS একটা বিতরণকৃত গাছ — root থেকে authoritative পর্যন্ত delegation-এর সিঁড়ি বেয়ে নাম ঠিকানায় বদলায়, cache আর TTL সেই খরচ কয়েকগুণ কমিয়ে রাখে, আর wire-এর নিচে চলে ছোট্ট একটা binary protocol যা আজ আমরা নিজের হাতে parse করব।
আগে এটা বুঝি
গত লেসনে আমরা 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 apiBrowser-এ লেখা নামের আসল রূপ www.example.com. — শেষের বিন্দুটাই গোড়ার ইশারা; একে বলে FQDN। কিন্তু structure-টা শুধু নাম সাজানোর নিয়ম না — কাজ ভাগ করার নিয়ম। গাছের প্রতিটা কাটা টুকরো একটা zone, প্রতিটা zone-র মালিক name server-দের একটা দল, যারা শুধু নিজের zone-র উত্তর জানে। এই ভাগটাই delegation:
| স্তর | কে বসে | কী জানে | কী জানে না |
|---|---|---|---|
| Root | ১৩টা logical root server (A–M) | সব TLD-র NS | example.com-র IP |
| TLD | .com চালায় registry operator | example.com-র NS | www.example.com-র IP |
| Authoritative | zone-র নিজের 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 | নাম → IPv4 | example.com. 3600 IN A 93.184.216.34 |
| AAAA | নাম → IPv6 | example.com. 3600 IN AAAA 2606:2800:...:1946 |
| CNAME | alias → canonical নাম (IP নয়!) | www.example.com. 300 IN CNAME example.com. |
| NS | এই zone কার কাছে — delegation-র প্রাণ | example.com. 172800 IN NS a.iana-servers.net. |
| MX | ইমেইল কোথায় যাবে + priority | example.com. 3600 IN MX 10 mail.example.com. |
| TXT | মুক্ত টেক্সট — SPF, DKIM, verification | example.com. 3600 IN TXT "v=spf1 -all" |
| SOA | zone-র পরিচয়পত্র — primary NS, serial, timer | example.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 নিয়ে নিচে নামে:
- Browser → stubbrowser cache মিস, getaddrinfo চালু — OS-র stub resolver-ও মিস
- Stub → recursive resolverRD flag সেট করে প্রশ্ন: আমার হয়ে পুরো পথ হেঁটে দাও
- Resolver → rootroot উত্তর দেয় না, referral দেয়: com-র NS-রা এই তেরোজন
- Resolver → TLD (.com).com referral দেয়: example.com-র NS এই server-গুলো
- Resolver → authoritativeaa flag-সহ চূড়ান্ত উত্তর: CNAME + A record
- ফেরত পথে সবাই কপি রাখেresolver TTL-সহ নিজের cache-এ জমায়; উত্তর উপরে stub-এ, তারপর browser-এ
গুলিয়ে ফেলার সুযোগ আছে, তাই স্পষ্ট করি: recursive বলছে কে দায়িত্ব নিচ্ছে (resolver পুরো কাজটা করবে), iterative বলছে যেভাবে করছে (referral ধরে ধরে, স্তরে স্তরে)। Query-টা recursive, walk-টা iterative।
Cache — তিন স্তরের photocopy, আর TTL-র অর্থনীতি
Walk-টা যদি প্রতিটা hostname-এর জন্য প্রতিবার হতো, root-রা প্রতি সেকেন্ডে কোটি কোটি প্রশ্ন পেত, page-load শুরু হতো ~৩০০ ms দেরিতে। বাস্তবে উত্তর ফেরার পথে তিন জায়গায় photocopy হয়:
| স্তর | কার | সাধারণ আয়ু |
|---|---|---|
| Browser cache | Chrome/Firefox নিজের process-এ | মিনিট-পরিমাণ |
| OS stub cache | systemd-resolved / mDNSResponder / Windows DNS Client | record-র TTL পর্যন্ত |
| Recursive resolver cache | ISP / 1.1.1.1 / 8.8.8.8 — সবার shared | record-র 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-গুলো একে একে:
| Field | Bits | অর্থ |
|---|---|---|
| ID | 16 | প্রশ্নের টিকিট নম্বর — উত্তর এই ID-ই ফেরত দেয় |
| QR | 1 | 0 = query, 1 = response |
| Opcode | 4 | সাধারণত 0 (standard query) |
| AA | 1 | Authoritative Answer — উত্তর zone-মালিকের মুখের কথা |
| TC | 1 | Truncated — অসম্পূর্ণ, TCP-তে আবার জিজ্ঞেস করো |
| RD | 1 | Recursion Desired — client চেয়েছে “আমার হয়ে হেঁটে দাও” |
| RA | 1 | Recursion Available — server বলছে “হেঁটে দিতে পারি” |
| Z | 3 | reserved — শূন্য |
| RCODE | 4 | 0=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 msechood-র কথা হুবহু মিলল: 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-র হিসাবখাতা:
| Hop | Server | যা পেলাম | সময় |
|---|---|---|---|
| ১ | root (198.41.0.4) | referral: .com-র ১৩ NS | 88 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-ও নিজেদের কপি রাখেআর 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 ছাঁচ) │
└───────────────────────────────┘নিজে চালিয়ে দেখুন
dig +trace দিয়ে hierarchy হাঁটা, আর TTL countdown চোখে দেখা
ধাপ ১ — পুরো 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-তে যাওয়াই হয় না।
নিজে বানান
DNS query builder + response parser — struct আর socket ছাড়া কিছুই নেই
- encode_name — নামকে length-prefixed label sequence-এ ফেলুন, 00 byte-এ শেষ
- build_query — 12-byte header pack (ID, RD flag, QDCOUNT=1) + question section
- UDP socket দিয়ে resolver-র port 53-এ পাঠান; উত্তরের ID মিলিয়ে নিন
- parse_response — header flags আর ৪টা count unpack করে question section পেরোন
- 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-ই রেখেছে।
নিজে বাড়ান:
TC=1দেখলে একই query TCP-তে আবার পাঠানোর fallback লিখুন — বড় NS তালিকাওলা নাম দিয়ে পরীক্ষা করুনread_name-এ visited-offset set যোগ করে pointer-loop guard দিন, তারপর হাতেC0 00 C0 00-জাতীয় দুষ্টু packet বানিয়ে guard যাচাই করুন- 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-তালিকা ছোট, গল্পগুলো আলাদা।
বুঝেছেন কি না দেখুন
1Recursive 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।
2dig @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 না?
প্রয়োগ
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-র উত্তর দেয়নি, পথ দেখিয়েছে মাত্র।
3Record-র 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 কমাও, তারপর বদলাও।
4DNS কেন 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 কী?
প্রয়োগ
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।
6Global 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 ব্যাখ্যা