jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Network Programming · Part 4 — DNS & Addressing: From Names to IPs

How DNS turns hostnames into IP addresses, the resolution chain from your app to authoritative servers, Node.js dns.lookup vs dns.resolve, record types, TTL caching, and dual-stack IPv4/IPv6 — bilingual, with runnable TypeScript examples.

Đây là Phần 4 của series 10 bài về lập trình mạng với Node.js + TypeScript. Phần 1–3 đã cho bạn mô hình phân tầng, TCP và UDP. Giờ ta xử lý khoảng cách giữa tên dễ nhớ cho con ngườithứ socket thực sự cần: địa chỉ IP.

Mỗi lần bạn connect('api.example.com', 443) hay fetch('https://example.com'), phải có thứ gì đó dịch hostname thành địa chỉ có thể định tuyến trước khi một packet rời máy bạn. Đó là DNS — Hệ thống Tên Miền.


Vì sao cần DNS

Con người nhớ tên; Internet định tuyến theo số. 93.184.216.34 khó gõ và không thể “di chuyển” khi server đổi máy. example.com ổn định — bạn chỉ cập nhật bản ghi DNS phía sau tên khi hạ tầng thay đổi.

DNS là cơ sở dữ liệu phân tán ánh xạ tên sang địa chỉ (và metadata khác). Ứng dụng gần như không nói chuyện trực tiếp ngay — bạn truyền hostname cho net.connect, http.request, hay fetch, và runtime thực hiện bước phân giải tên trước khi mở socket.

Ý chính: socket kết nối tới IP; DNS là cầu nối từ hostname → IP.


Chuỗi phân giải

Khi process Node hỏi “IP của example.com là gì?”, truy vấn thường đi qua nhiều bước — mỗi tầng có thể cache câu trả lời.

Your app example.com? Resolver recursive cache Root + TLD .com servers Authoritative 93.184.216.34 name → IP, so the socket knows where to connect (cached at each hop)
Your app asks a recursive resolver; the resolver walks root → TLD → authoritative until it gets an IP (cached at each hop)
  1. Ứng dụng: gọi dns.lookup, dns.resolve4, hoặc để net/http tự phân giải.
  2. Resolver đệ quy: thường là server OS cấu hình — nó làm phần việc nặng và cache kết quả.
  3. Server gốc + TLD: . gốc . chỉ tới nameserver .com, rồi chỉ tới authority của domain.
  4. Nameserver chủ quyền: nguồn sự thật cho example.com — trả về bản ghi A / AAAA thực tế.

Câu trả lời kèm TTL — cache được giữ bản ghi bao lâu trước khi hỏi lại.


Các loại bản ghi DNS phổ biến

RecordMục đíchVí dụ
Ađịa chỉ IPv4example.com → 93.184.216.34
AAAAđịa chỉ IPv6example.com → 2606:2800:220:1:248:1893:25c8:1946
CNAMEbí danh trỏ tới tên khácwww.example.com → example.com
MXserver mail + độ ưu tiênexample.com → 10 mail.example.com
TXTvăn bản tùy ýexample.com → "v=spf1 include:..."
NSnameserver của vùngexample.com → ns1.example.com
SRVvị trí dịch vụ_xmpp._tcp → 5269 xmpp.example.com

Với lập trình socket, bạn quan tâm nhất AAAAA — chúng cho IP để connect(). CNAME nghĩa là “phân giải tên khác trước”; MX / TXT / SRV dùng cho mail, xác thực, discovery — không phải client TCP thường.


So sánh lookup và resolve

Module node:dns có hai họ API mà người mới hay nhầm.

APILàm gìBên trong
dns.lookupHostname → IP để kết nốiresolver OS qua getaddrinfo — cùng đường net / http dùng
dns.resolve / dns.promises.resolve4Truy vấn DNS cho loại bản ghi cụ thểNói trực tiếp với server DNS

Khác biệt quan trọng:

  • dns.lookup tuân cấu hình OS (/etc/hosts, mDNS, VPN) và có thể trả IPv4 hoặc IPv6 tùy chính sách hệ thống.
  • dns.resolve4 luôn hỏi mạng lấy bản ghi A — bạn nhận đúng DNS nói, không phải OS “ưu tiên” khi kết nối.
  • dns.lookup dùng libuv thread pool cho getaddrinfo blocking — nhiều lookup đồng thời có thể đói việc khác trong pool (file I/O, crypto).
  • dns.promises.resolve* async hoàn toàn từ góc nhìn Node — tốt hơn khi tra DNS hàng loạt.

Dùng lookup khi muốn “địa chỉ tôi sẽ kết nối”. Dùng resolve4 / resolve6 / resolveMx khi cần dữ liệu DNS thô hoặc nhiều câu trả lời.


Phân giải tên bằng TypeScript

Lưu thành resolve-demo.ts và chạy bằng npx tsx resolve-demo.ts.

Đường resolver OS

import { lookup } from 'node:dns/promises';

// What net.connect() effectively uses: one "best" address for connecting.
const { address, family } = await lookup('example.com');
console.log(`lookup → ${address} (IPv${family})`);
// e.g. lookup → 93.184.216.34 (IPv4)

family4 hoặc 6 — cho biết OS chọn stack nào. Truyền \{ all: true \} để lấy mọi địa chỉ OS sẽ thử (hữu ích debug dual-stack):

import { lookup } from 'node:dns/promises';

const results = await lookup('example.com', { all: true });
for (const r of results) {
  console.log(`  ${r.address}  IPv${r.family}`);
}

Truy vấn bản ghi A trực tiếp

import { resolve4 } from 'node:dns/promises';

// Talks to DNS servers; returns ALL A records (may be multiple!).
const ipv4s = await resolve4('example.com');
console.log('A records:', ipv4s);
// A records: [ '93.184.216.34' ]

Site lớn thường trả nhiều bản ghi A để cân bằng tải (round-robin DNS). OS / client có thể thử theo thứ tự hoặc chọn một — đừng giả định một tên = một IP.

Định tuyến mail

import { resolveMx } from 'node:dns/promises';

const mx = await resolveMx('gmail.com');
// Sorted by priority (lower = preferred).
for (const record of mx) {
  console.log(`priority ${record.priority} → ${record.exchange}`);
}

Tra ngược IP → hostname

import { reverse } from 'node:dns/promises';

const hostnames = await reverse('93.184.216.34');
console.log('PTR:', hostnames);
// PTR: [ 'example.com', 'www.example.com', ... ]  (depends on PTR record)

Reverse DNS dùng bản ghi PTR; thiếu PTR là bình thường, không ảnh hưởng kết nối thuận.


IPv4, IPv6 và dual-stack

Internet vẫn chủ yếu IPv4 (bốn byte). IPv6 giải quyết cạn kiệt địa chỉ và đơn giản hóa định tuyến.

Máy dual-stack có cả hai:

  • A record → IPv4
  • AAAA record → IPv6

Khi lookup('example.com') không gợi ý, Node/OS áp dụng chính sách kiểu Happy Eyeballs: thử địa chỉ có độ trễ thấp, thường ưu tiên IPv6 nếu có.

import { lookup, resolve4, resolve6 } from 'node:dns/promises';

const v4only = await lookup('example.com', { family: 4 });
const v6only = await lookup('example.com', { family: 6 }).catch(() => null);

const allA = await resolve4('example.com');
const allAAAA = await resolve6('example.com').catch(() => [] as string[]);

console.log({ v4only, v6only, allA, allAAAA });

resolve6 có thể ném ENODATA nếu không có AAAA — luôn xử lý trong code production.


TTL, cache và câu trả lời cũ

Mỗi phản hồi DNS có TTL tính bằng giây. Resolver và OS cache ánh xạ đến khi TTL hết hạn.

LayerCache gì
OS resolverKết quả lookup gần đây
Recursive resolver (8.8.8.8, etc.)Câu trả lời theo TTL từ upstream
Your appChỉ khi bạn cache — Node không cache resolve4 giữa các lần gọi

Hàm ý cho lập trình viên mạng:

  • Sau deploy hay failover, một số client vẫn tới IP cũ đến khi TTL hết.
  • TTL thấp (vd 60s) lan nhanh nhưng tăng tải truy vấn DNS.
  • Gọi resolve4 mỗi request HTTP lãng phí — lookup + tái sử dụng kết nối (Phần 5+) là đường thường gặp.

Xem TTL bằng tùy chọn resolve4 (Node 18+) hoặc dig:

dig example.com A +noall +answer
# example.com.  86400  IN  A  93.184.216.34
#                 ^^^^^ TTL in seconds

Lookup ngầm khi connect

Bạn không bắt buộc gọi dns trước. Truyền hostname cho net.connect sẽ tự phân giải:

import { connect } from 'node:net';

// Implicit dns.lookup inside — blocks threadpool briefly, then connects.
const socket = connect({ host: 'example.com', port: 80 }, () => {
  console.log('connected to', socket.remoteAddress, 'port', socket.remotePort);
  socket.end();
});

socket.on('error', (err) => console.error('connect failed:', err.message));

Trình tự: phân giải hostname → chọn địa chỉ → bắt tay TCP (Phần 2). DNS lỗi → ENOTFOUND trước khi packet tới server. Đã có IP thì truyền thẳng, bỏ qua DNS.


Lỗi người mới hay mắc

  • Dùng resolve4 khi ý là lookup — nguồn dữ liệu và ngữ nghĩa khác nhau.
  • Bỏ qua TTL và cache — thắc mắc vì sao client vẫn tới IP chết sau khi đổi DNS.
  • Giả định một hostname = một IP — round-robin trả nhiều A; kết nối có thể tới máy khác nhau.
  • Gọi hàng nghìn dns.lookup song song — cạn libuv thread pool và làm chậm việc async khác.

Bài tập

Thử từng bài trước khi mở lời giải.

  1. Chạy cả hai và so sánh kết quả, giải thích khác biệt.
  2. Gọi resolve4('google.com') hai lần — đếm số IP; giải thích vì sao connect vẫn hoạt động.
  3. Viết hàm trả về cả IPv4 và IPv6, mảng rỗng khi thiếu.
Lời giải
import { lookup, resolve4, resolve6 } from 'node:dns/promises';

// 1. localhost — OS resolver vs direct DNS
const os = await lookup('localhost');
const direct = await resolve4('localhost');
console.log('lookup:', os);       // often ::1 (IPv6) on modern macOS/Linux
console.log('resolve4:', direct); // often ['127.0.0.1'] from DNS/hosts

// lookup follows getaddrinfo policy; resolve4 asks DNS for A records only.

// 2. google.com — many A records, connect picks one
const ips = await resolve4('google.com');
console.log(`${ips.length} A records:`, ips.slice(0, 3), '...');
// connect() uses lookup + Happy Eyeballs; one successful TCP handshake is enough.

// 3. resolveAll
async function resolveAll(host: string): Promise<{ ipv4: string[]; ipv6: string[] }> {
  const [ipv4, ipv6] = await Promise.all([
    resolve4(host).catch(() => [] as string[]),
    resolve6(host).catch(() => [] as string[]),
  ]);
  return { ipv4, ipv6 };
}

console.log(await resolveAll('example.com'));

lookup có thể trả ::1 còn resolve4 trả 127.0.0.1 vì OS ưu tiên IPv6 loopback nhưng bản ghi A vẫn là IPv4. Google có nhiều A để cân tải; connect chỉ cần một địa chỉ hoạt động.


Điều cốt lõi

DNS nối tên con người với địa chỉ máy qua chuỗi phân giải phân cấp có cache. Trong Node, dns.lookup đi theo đường OS mà nethttp dùng; dns.resolve* hỏi DNS trực tiếp theo loại bản ghi. Tôn trọng TTL, chấp nhận nhiều IP, tránh làm ngập lookup trên thread pool. Đã phân giải tên, Phần 5 xây HTTP thật trên socket TCP bạn đã biết.