Tôi viết một con DNS server để hiểu thứ mình đã tin tưởng mù quáng suốt mười năm

Trước khi có DNS: cả internet chạy bằng một file text duy nhất
Để hiểu vì sao DNS tồn tại, phải quay về cái thời nó chưa tồn tại và câu chuyện này, thú thật, làm tôi mê.
Đầu thập niên 1980, ARPANET (tiền thân của internet) chưa có DNS. Vậy máy tính tìm nhau bằng cách nào? Bằng một file text. Đúng vậy một file duy nhất tên HOSTS.TXT, liệt kê tên máy nào ứng với IP nào, được Stanford Research Institute (SRI-NIC) ở California ngồi giữ.
Cơ chế vận hành nghe tới mức buồn cười: ai có máy mới muốn lên mạng thì... gọi điện hoặc gửi mail cho SRI, SRI cập nhật tay vào file, rồi mỗi ngày tất cả các máy trên toàn mạng tải lại cái file đó về. Toàn bộ "danh bạ internet" là một tài liệu được sửa bằng tay và phát đi mỗi ngày một lần.
B��n thấy ngay vấn đề chứ? Khi mạng còn vài trăm máy thì ổn. Nhưng mạng lớn lên theo cấp số nhân. Cái file phình ra, traffic chỉ để tải file đó cũng phình ra, và quan trọng nhất: một thực thể duy nhất phải duyệt tay mọi thay đổi của cả thế giới. Hai máy ở hai đầu địa cầu không thể trùng tên, vì danh bạ là chung. Nó là một nút thắt cổ chai, và nó đang siết dần.
Năm 1983, một người tên Paul Mockapetris, làm tại USC dưới trướng huyền thoại Jon Postel, được giao đúng một việc: nghĩ ra thứ gì đó thay thế. Ông viết hai tài liệu, RFC 882 và RFC 883, và khai sinh ra Domain Name System.
Điều thiên tài không nằm ở chỗ "tra tên ra số" HOSTS.TXT đã làm việc đó rồi. Điều thiên tài nằm ở hai ý tưởng: phân cấp (hierarchy) và ủy quyền (delegation). Thay vì một danh bạ khổng lồ do một người giữ, hãy chẻ nó thành cây: gốc chỉ cần biết ai quản .com, .org; đám quản .com chỉ cần biết ai quản example.com; còn chi tiết bên trong example.com thì... để chính chủ của nó tự lo. Mỗi nhánh tự quản phần của mình, không ai phải duyệt giùm ai.
Cú lật ngược tư duy đó từ tập trung sang phân tán chính là lý do DNS sống sót qua bốn thập kỷ và giờ gánh hàng nghìn tỷ truy vấn mỗi ngày, trong khi HOSTS.TXT thì chết từ lâu. Và khi tự code lại mini-dns, tôi nhận ra mình đang dựng lại đúng cái triết lý 1983 đó: một server biết phần của mình, và biết chỉ đường cho phần không phải của mình.
DNS thực ra hoạt động ra sao: một câu hỏi đi qua bao nhiêu cửa?
Cái triết lý phân cấp ở trên, dịch ra cơ chế chạy thật, là thế này. Khi bạn gõ www.example.com, một chuỗi domino chạy sau lưng trong vài chục mili-giây:
Trình duyệt hỏi máy bạn. Máy bạn (stub resolver) hỏi con recursive resolver thường của nhà mạng, hoặc
8.8.8.8,1.1.1.1.Con recursive nếu chưa biết, nó không hỏi một ai cụ thể nó leo cây từ gốc. Đầu tiên hỏi root server (
.): "ai quản đuôi.com?"Root đáp: "tao không biết IP, nhưng cứ hỏi đám TLD server quản
.comnày nè." → đây là một referral, không phải câu trả lời, mà là chỉ đường.Recursive hỏi tiếp TLD
.com: "ai quảnexample.com?" → TLD lại chỉ đường tới authoritative server của domain.Cuối cùng recursive hỏi authoritative thằng thật sự sở hữu câu trả lời: "
www.example.comIP gì?" → nó đáp:192.0.2.1.
Năm bước, cho một lần truy cập. Sở dĩ bạn không thấy độ trễ đó mỗi lần lướt web là nhờ cache ở mọi tầng linh hồn của cả hệ thống, tôi nói kỹ ở dưới.
Cái làm tôi "ồ" lên: không server nào biết tất cả. Đây chính xác là di sản 1983 mỗi tầng chỉ biết "đứa tiếp theo cần hỏi là ai". Một thiết kế từ chối tập trung hóa ngay từ trong gen.
mini-dns đóng hai vai trong vở kịch này: vừa là authoritative (sở hữu một zone tự định nghĩa), vừa biết làm recursive forwarding (gặp tên lạ thì đẩy lên upstream rồi nhớ lại).
Một câu trả lời DNS trông ra sao bên trong gói tin?
Muốn server trả lời thì phải tự tay dựng từng byte. Một message DNS chia bốn phần:
Header: chứa ID giao dịch (để ghép câu hỏi với câu trả lời) và một mớ cờ (flag) cực quan trọng.
Question: bạn hỏi gì tên miền, loại record, lớp.
Answer / Authority / Additional: ba khu chứa câu trả lời và thông tin phụ.
Mấy cái flag mới dạy tôi nhiều nhất:
AA(Authoritative Answer): bật lên nghĩa là "câu trả lời từ thằng chính chủ, không phải đồ cache". Khimini-dnstrả lời tên trong zone của nó, tôi bật cờ này; khi trả đồ forward về thì không. Phân biệt "tôi biết chắc" với "tôi nghe nói lại" DNS có sẵn cơ chế đó.RCODE: mã kết quả.NOERRORlà ổn.NXDOMAINlà "tên này không tồn tại trên đời". Và đây là cái bẫy tinh tếNXDOMAINkhácNODATA.
Cái bẫy đó, tôi đã code sai lần đầu:
NXDOMAIN= cái tên hoàn toàn không tồn tại. Ví dụkhongcogi.example.com.NODATA= cái tên có tồn tại, nhưng không có loại record bạn hỏi. Ví dụexample.comcó recordA(IPv4) nhưng bạn đi hỏi nóAAAA(IPv6) tên thì có, loại đó thì trống. Lúc này phải trảNOERRORvới answer rỗng, không được trảNXDOMAIN.
Nghe nhỏ, nhưng trả nhầm hai cái này là nguyên nhân thật của bug kiểu "email tự nhiên không gửi được" hay "client retry vô tận". DNS không chỉ trả lời "có hay không", nó trả lời "không, theo kiểu nào". Và cái "kiểu nào" quan trọng chết người.
Zone file: trái tim trần trụi của một authoritative server
Sự thật làm tôi khựng lại: dữ liệu gốc của một tên miền chỉ là một file text. Đúng bốn mươi năm sau HOSTS.TXT, trái tim vẫn là text. Khác biệt nằm ở chỗ giờ mỗi người chỉ giữ phần của mình.
example.com. 3600 SOA ns1.example.com. admin.example.com. 1 7200 3600 1209600 3600
example.com. 3600 NS ns1.example.com.
example.com. 3600 A 192.0.2.1
example.com. 3600 AAAA 2001:db8::1
www.example.com. 3600 CNAME example.com.
example.com. 3600 MX 10 mail.example.com.
example.com. 3600 TXT "v=spf1 -all"
*.example.com. 3600 A 192.0.2.9
Mỗi loại record là một mảnh ghép, và khi tự xử lý từng loại tôi mới thật sự hiểu chúng:
SOA(Start of Authority) dòng "khai sinh" của zone. Đám số phía sau không phải trang trí:serial(phiên bản zone, để server phụ biết có đổi mà đồng bộ), rồirefresh,retry,expire, và quan trọng nhất số cuối là TTL của negative cache, tức "lỗi NXDOMAIN được nhớ bao lâu". Một con số bé xíu quyết định mức chịu tải cả hệ thống.NSchỉ ra ai là nameserver chính chủ. Đây là sợi dây nối zone của bạn vào cây phân cấp toàn cầu — chính cái cây Mockapetris vẽ ra năm 1983.A/AAAAtrỏ tên sang IPv4 / IPv6.CNAMEbí danh. Luật nghiêm: một tên đã có CNAME thì không được có record nào khác cùng cấp. Khi resolve, nếuwwwlà CNAME trỏ tớiexample.com, server tử tế sẽ đi tiếp luôn (CNAME chaining), gói cả đích đến vào cùng một câu trả lời để client khỏi hỏi thêm vòng nữa.MXnơi email tìm tới, kèm số ưu tiên (10). Sai dòng này là mail cả công ty rơi vào hư không.TXTthùng chứa vạn vật: SPF chống giả mạo email, xác minh sở hữu domain, DKIM...*wildcard bắt mọi subdomain chưa khai báo. Tiện nhưng dao hai lưỡi: gõ sai một chỗ, cả ngàn subdomain ma cùng trỏ nhầm.
Khi viết parser, tôi đứng trước một quyết định nhỏ mà nói lên nhiều về nghề: một dòng gõ sai thiếu dấu chấm cuối, sai cú pháp thì làm gì? Cho sập cả file? Hay bỏ qua dòng đó?
Tôi chọn: log đúng số dòng lỗi, bỏ qua nó, để phần còn lại vẫn sống. Đó là ranh giới giữa "một dấu chấm gõ thiếu" và "cả công ty mất website lúc nửa đêm". Phần lớn sự cố production không đến từ lỗi to. Nó đến từ một thứ bé tí được xử lý thiếu tử tế.
(À, để ý dấu chấm cuối example.com. chứ? Đó là FQDN nói "đây là tên tuyệt đối, đã tới gốc rồi, đừng ghép thêm gì". Thiếu nó, nhiều hệ thống tự nối domain phía sau thành example.com.example.com. Một dấu chấm. Cả tối debug.)
TTL và cache linh hồn, và cũng là nỗi đau, của DNS
Số 3600 ở mỗi dòng là TTL (Time To Live), tính bằng giây: "câu trả lời này được phép nhớ trong một tiếng".
Đây là thủ phạm của câu thần chú đau khổ nhất ngành: "đổi DNS rồi mà sao chưa thấy ăn?"
Trước đây tôi nghĩ propagate là internet "lười". Sai. Internet không chậm nó đang tôn trọng lời hứa của chính bạn. Khi bạn đặt TTL 3600, bạn đã bảo cả thế giới "thông tin này nhớ một tiếng cũng được". Và mọi resolver trên đường đi đều nhớ. Giờ đổi ý sau mười phút thì... họ vẫn giữ đúng lời hứa cũ của bạn, tới khi đồng hồ đếm ngược về 0.
Và điều tôi chỉ hiểu khi tự code cache: TTL không phải một, mà đếm ngược độc lập ở từng tầng. Cache trình duyệt, cache hệ điều hành, cache resolver nhà mạng mỗi đứa giữ một bản với đồng hồ riêng. Đó là lý do bạn ở nhà thấy web lên rồi mà ông bạn ở mạng khác vẫn báo chưa. Không ai sai chỉ là đồng hồ của họ chưa về 0.
Điểm nhấn tôi rút ra, áp dụng cả ngoài kỹ thuật: cache là sự đánh đổi giữa tốc độ và sự thật. Nhớ càng lâu càng nhanh, nhưng càng dễ sai khi sự thật đổi. Mọi hệ thống và cả con người đều sống trong đánh đổi đó.
Mẹo thực chiến đổi bằng máu: trước khi định đổi record quan trọng, hạ TTL xuống thật thấp vài ngày trước đã. Để khi đổi thật, cả thế giới quên cái cũ nhanh. Không ai dạy mẹo này; chỉ ai từng ngồi chờ propagate lúc 2 giờ sáng mới khắc cốt.
Bài học sâu nhất: phải biết cache cả những thứ không tồn tại
Đây là khoảnh khắc "à há" tôi nghĩ về mãi.
Cache câu trả lời đúng thì hiển nhiên: hỏi google.com một lần, nhớ lại, lần sau trả tức thì. Nhưng có thứ tinh tế hơn nhiều: negative caching nhớ luôn cả kết quả "tên này không tồn tại" (NXDOMAIN).
Lần đầu nghe tôi thấy phi lý. Nhớ một thứ không có để làm gì?
Rồi tôi hiểu. Ngoài kia có vô số bot dò bừa: aaa.example.com, admin123.example.com, xyz.example.com... Nếu mỗi lần gặp tên rác, server lại cặm cụi leo cây đi hỏi rồi nhận về "không có", nó tự biến mình thành nạn nhân. Nhớ rằng "thằng này không tồn tại" và nhớ bao lâu thì chính dòng SOA ở trên quy định là tấm khiên. (Thấy chưa, mọi thứ nối vào nhau.)
Điều này đẹp một cách kỳ lạ và vượt ngoài kỹ thuật: biết một thứ không tồn tại cũng là tri thức, đôi khi là loại quý nhất. Biết con đường nào không dẫn tới đâu. Biết cách làm nào không hiệu quả. Một hệ thống khôn ngoan không chỉ nhớ câu trả lời đúng nó nhớ cả những ngõ cụt, để khỏi đi lại lần nữa. Con người trưởng thành cũng vậy thôi.
Tôi còn thêm single-flight: một trăm câu hỏi giống hệt ập tới cùng lúc lúc cache trống thì chỉ một câu được đi hỏi, chín mươi chín câu kia đứng chờ ăn chung kết quả. Không có nó, một đợt traffic dồn có thể biến server của bạn thành cái máy bơm, vô tình tấn công chính cái upstream nó đang nhờ vả. Đôi khi điều tốt nhất một hệ thống làm được dưới áp lực là biết kiềm chế, đừng hỏi cùng một câu chín mươi chín lần.
UDP, 512 byte, và lý do DNS có một "khớp nối" kỳ lạ với TCP
Ai cũng thuộc "DNS chạy UDP port 53". Đúng, nhưng thiếu một nửa.
UDP nhanh, nhẹ, không bắt tay lằng nhằng hợp với hỏi đáp chớp nhoáng. Nhưng UDP-DNS truyền thống bị chặn ở 512 byte (một con số chọn từ thời 1983, khi mạng còn mong manh). Câu trả lời dài hơn (nhiều record, hay DNSSEC ký kẽo kẹt) thì sao?
Lúc đó server bật cờ TC (Truncated) kiểu nói "câu trả lời dài quá, cụt rồi, mày hỏi lại qua TCP đi". Client nghe vậy mở hẳn kết nối TCP hỏi lại từ đầu. Tôi phải code đúng nhịp hai bước này, và đây là lúc câu "UDP với fallback TCP" từ trừu tượng biến thành dòng code rất cụ thể.
Rồi EDNS(0) xuất hiện như một bản vá lịch sự cho giới hạn 512 byte cổ lỗ: client gắn kèm một record giả báo trước "tao chịu được gói tới 4096 byte", server nghe thế gửi thẳng gói lớn qua UDP, đỡ vòng qua TCP. Cùng họ là name compression trong một gói, tên miền nào lặp lại thì không ghi nguyên văn mà trỏ ngược về vị trí đã ghi. Mấy chục năm trước người ta đã phải bóp từng byte như vậy, và di sản đó vẫn chạy trong từng gói tin hôm nay.
Cú sốc riêng tư: DNS truyền thống trần như nhộng
Phần này làm tôi thật sự dừng lại.
DNS cổ điển gửi plaintext. Không mã hóa. Đây cũng là di sản của niềm tin ngây thơ thời 1983 khi internet chỉ là vài viện nghiên cứu quen biết nhau, chẳng ai nghĩ tới chuyện nghe lén. Mỗi lần bạn vào một trang, câu hỏi "tên này IP gì?" được hét lên giữa đường truyền cho ai nghe cũng được nhà mạng, ông quản Wi-Fi quán cà phê, bất kỳ ai ngồi đúng chỗ.
Bạn duyệt web qua HTTPS, ổ khóa xanh đẹp đẽ, nội dung mã hóa kín. Nhưng cái danh sách những nơi bạn ghé thì để ngỏ ngay từ bước hỏi DNS. Giống dán kín nội dung lá thư, nhưng địa chỉ người nhận viết to ngoài phong bì cho cả bưu điện đọc.
Khi code thêm DoT (DNS-over-TLS, bọc trong TLS, cổng riêng) và DoH (DNS-over-HTTPS, giấu query DNS luôn vào traffic HTTPS bình thường, gần như không phân biệt được với lướt web), tôi mới hiểu vì sao mấy năm nay trình duyệt âm thầm bật DoH mặc định. Không phải làm màu đó là vá một vết hở nằm im trong nền móng internet suốt bốn mươi năm.
Bài học rộng hơn: thứ rò rỉ thông tin về bạn thường không phải nội dung mà là metadata. Không phải bạn nói gì, mà là nói với ai, lúc nào, bao nhiêu lần.
Thứ phân biệt đồ chơi với đồ thật
Viết được con DNS trả lời đúng chỉ là một nửa. Nửa khó là làm nó không gục khi bị quăng vào đời thực, nơi không ai tử tế với nó.
Rate limiting từng client, vì DNS là món khoái khẩu của tấn công khuếch đại kẻ xấu gửi gói nhỏ với IP giả của nạn nhân, ép server phun gói lớn về phía nạn nhân đó; một server không giới hạn vô tình thành khẩu pháo. Hot reload: sửa zone xong gửi tín hiệu SIGHUP là server nạp lại ngay, không restart, không rớt request đang xử lý dở vì trong production "tắt đi bật lại" là thứ xa xỉ. Và mỗi nhân CPU một socket riêng (SO_REUSEPORT) để kernel tự chia tải, thay vì mọi gói chen qua một cửa hẹp.
Không cái nào khiến con DNS "thông minh hơn". Chúng chỉ khiến nó sống sót. Càng làm lâu tôi càng tin: khoảng cách giữa một thứ chạy được trên máy mình và một thứ trụ được ngoài đời, gần như toàn bộ nằm ở phần không ai khoe trên demo.
Vậy đêm web chết đó, tôi học được gì?
Thành thật: mini-dns không sinh ra để đấu với BIND hay mấy con DNS công nghiệp chinh chiến hàng chục năm. Nó là một bài tập để hiểu. Và nó trả công xứng đáng theo cách tôi không ngờ.
Giờ khi web "không vào được", tôi không đoán mò trong hoảng loạn. Tôi hỏi đúng thứ tự, như bác sĩ khám chứ không phải thầy bói: dig thử ra cái gì? Cache cũ còn sống vì TTL chưa hết? Record A đúng IP không? CNAME trỏ vòng vo? Trả NXDOMAIN hay NODATA? MX sai khiến mail rơi hư không? Đám NS đã propagate và đồng thuận với nhau chưa?
Cái hộp đen đã thành một hệ thống tôi hiểu. Và điều đó áp dụng cho gần như mọi thứ trong nghề: bạn không thật sự sở hữu một công nghệ cho đến khi dám mở nó ra và nhìn vào bên trong. Trước đó, bạn chỉ đang mượn niềm tin của người khác và cầu cho nó đừng hỏng vào ca trực của mình.
Có một điều đẹp tôi nghĩ mãi: cái hệ thống Paul Mockapetris vẽ ra năm 1983, trên một cái máy TOPS-20, để giải quyết nỗi đau của vài trăm cái máy cái thiết kế đó vẫn đang gánh hàng nghìn tỷ truy vấn mỗi ngày cho cả hành tinh, gần như không đổi cốt lõi. Người ta vá thêm bảo mật, thêm mã hóa, thêm IPv6, nhưng trái tim phân cấp - ủy quyền vẫn y nguyên. Một ý tưởng tốt, đủ đơn giản và đủ đúng, có thể sống lâu hơn cả những đế chế.
Nếu bạn cũng thuộc tuýp "học bằng cách đập ra xây lại", thử clone mini-dns về, dựng nó lên:
cargo build --release
./target/release/mini-dns
rồi quăng cho nó câu hỏi đầu tiên:
dig @127.0.0.1 -p 8888 example.com A
Cái khoảnh khắc nó trả về đúng cái IP bạn vừa tự tay gõ vào zone vài giây trước — tự nhiên cuốn danh bạ khổng lồ và phân tán của cả internet bỗng bớt huyền bí đi một chút. Nó không còn là phép màu. Nó là thứ bạn hiểu.
Và với một người làm kỹ thuật, biến một phép màu thành một thứ mình hiểu — đó có lẽ là niềm vui thầm lặng nhưng bền bỉ nhất, cái giữ chân chúng ta ở lại với cái nghề kỳ lạ này, hết đêm deploy này tới đêm deploy khác.
Toàn bộ code: github.com/uy-td-dev/mini-dns
Bài viết liên quan
Thiết kế hệ thốngSáu trạm, sáu cách chết: hành trình sống còn của một HTTP request
Ghi chép kỹ thuật & chiêm nghiệm — về con đường mà mỗi request phải đi, và tại sao nó lại đi như vậy.
Phát triển phần mềmNginx không "dở chứng" — bạn chỉ đang đọc config sai cách
Giới Dev có một nghịch lý vui: chúng ta sẵn sàng tranh cãi cả tuần xem Backend nên viết bằng Rust, Go hay Node.js, nhưng đến lúc đẩy sản phẩm lên production thì 90% lặng lẽ gõ apt install nginx và cắm nó ra phía trước. Nginx là "người gác cổng quốc dân" — và cũng là thứ khiến không ít người ngồi debug tới 2 giờ sáng mà vẫn không hiểu vì sao một request đơn giản lại trả về 404. Điều thú vị là: lỗi gần như không bao giờ nằm ở Nginx. Nó nằm ở chỗ chúng ta đọc file config như đọc một đoạn code chạy tuần tự từ trên xuống — trong khi Nginx hoàn toàn không hoạt động như vậy. Bài này không phải danh sách "những lỗi Nginx thường gặp" để bạn chép về dán vào server. Mục tiêu của nó là đưa cho bạn một mô hình tư duy: hiểu Nginx suy nghĩ ra sao, để những cái bẫy tưởng như thần bí trở nên đoán trước được — thay vì cứ gặp là ngớ người ra.
Phát triển phần mềmGóc Nhìn "Reverse LSP": Khi Bản Năng Sinh Tồn Buộc Bạn Phải Vi Phạm Nguyên Tắc
SOLID không phải là một tôn giáo, và các nguyên tắc thiết kế không phải là những điều răn bất di bất dịch. Đứng ở góc độ một Tech Lead thực chiến, đôi khi quyết định bẻ gãy nguyên tắc Liskov Substitution Principle (LSP) lại là lựa chọn trưởng thành để cứu sống hệ thống. Hãy cùng phân tích 4 kịch bản trade-off kinh điển và nghệ thuật khoanh vùng 'mã độc' an toàn.
Thảo luận
0 bình luận
Hãy là người đầu tiên thảo luận.