昨天(Day 14)我們完成了由上而下的主動發送路徑,成功將 UDP 封包射出虛擬網卡:
Application ──► UDP ──► IPv4 ──► Ethernet ──► TAP
過去我們測試時,目的 IP 幾乎都寫死在程式碼或指令裡。今天(Day 15),我們要進入第一個應用層協定:Mini DNS Client,學會把 google.com 這種網域名稱轉成真正可連線的 IP 位址。
今天的重點是 DNS Payload 的 RFC 1035 編碼、組包與解析。傳輸部分先使用 Linux 標準 UDP Socket(sendto / recvfrom),不直接接進自製 TAP/UDP 發送路徑。
Application (Mini DNS Client)
│
DNS Payload
│
Linux UDP Socket (sendto / recvfrom)
│
Host Kernel Network Stack
│
Google DNS 8.8.8.8:53
簡單說:DNS 內容我們自己做,UDP/IP/Ethernet 傳輸先交給 Linux Kernel。
dns_encode_name(),將 google.com 轉成 DNS length-prefixed label 格式。dns_build_query(),組出可送往 8.8.8.8:53 的 Type A 查詢。dns_parse_response(),解析 Answer RR、Compression Pointer 與 IPv4 RDATA。google.com,觀察多筆 DNS A Record 回覆。dst_ip 必須是 32-bit 的純數字(如 142.250.204.46)。如果沒有 IP,IPv4 標頭填不出來,路由器根本不知道往哪裡送。google.com。「雞生蛋、蛋生雞」的疑惑:如果我連 Google IP 都不知道,我要怎麼上網查它?
答案是:個人電腦裡本來就沒有 Google 的表!但電腦必須預先知道「查號台的純數字 IP」!

DNS 請求(Query)與回覆(Reply)共用同一個固定 12 Bytes 的 Header(RFC 1035):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (bits)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID | -> 16 bits (Transaction ID)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE | -> 16 bits (Flags 標誌)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT | -> 16 bits (問題數量)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT | -> 16 bits (回答數量)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT | -> 16 bits (授權伺服器數量)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT | -> 16 bits (額外記錄數量)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
id(交易識別碼):因為 UDP 是無狀態的(Stateless),Client 發送時自訂一個隨機 ID(存根)。Server 回覆時必須原樣抄回,Client 藉此比對這是哪一筆查詢的回信。flags(控制旗標):指示是 Query 還是 Response、是否要求遞迴查詢、是否有錯誤。qdcount(問題數):Client 發問時設為 1。ancount(答案數):發送時填 0;接收時由 Server 告知有幾筆 IP 答案。nscount / arcount:授權與額外記錄數量,發送時為 0。DNS 的 flags 是整個協定的核心指揮官:
| 欄位 | 位元數 | 功能說明 | 發送 Query (0x0100) |
接收 Response (0x8180) |
|---|---|---|---|---|
| QR | 1 bit | 0 = Query(問題),1 = Response(回答) | 0 |
1 |
| Opcode | 4 bits | 0 = 標準查詢(Standard Query) | 0000 |
0000 |
| AA | 1 bit | 授權回答(Authoritative Answer) | 0 |
0 |
| TC | 1 bit | 截斷標誌(超過 UDP 512 bytes 需改用 TCP) | 0 |
0 |
| RD | 1 bit | 遞迴查詢期望(Recursion Desired) | 1 |
1 |
| RA | 1 bit | 伺服器支援遞迴(Recursion Available) | 0 |
1 |
| Z | 3 bits | 保留位元,必須為 0 | 000 |
000 |
| RCODE | 4 bits | 回應狀態碼:0 = NoError,3 = NXDOMAIN |
0000 |
0000 (成功) |
0x0100?0000 0001 0000 0000,代表:QR=0(發問)且 RD=1。告訴 8.8.8.8:「拜託幫我跑腿查到底,直接回我最終 IP,不要叫我再跑去問其他伺服器!」0x8180?1000 0001 1000 0000,代表:QR=1(回信)、RD=1、RA=1(已幫你遞迴查完)、RCODE=0(查詢成功無誤)!DNS 封包不採用句點(.)與結尾 \0,而是採用長度標籤編碼格式:
0x00(長度為 0 的根標籤)。以 google.com 為例:
人類閱讀: g o o g l e . c o m
長度切分: [6] g o o g l e [3] c o m [0]
Hex 位元組: 06 67 6f 6f 67 6c 65 03 63 6f 6d 00
總長度 : 1 + 6 + 1 + 3 + 1 = 12 Bytes
qtrailer_ptr)因為 QNAME 的長度隨網址而異(google.com 是 12 bytes,其他網址可能 30 bytes),無法用固定結構描述,必須使用動態指標計算:
記憶體起始: buf
│
▼
┌──────────────────┬───────────────────────────────┬──────────┬──────────┐
│ dns_header │ QNAME │ QTYPE │ QCLASS │
│ (固定 12 B) │ (動態變長!) │ (2 B) │ (2 B) │
└──────────────────┴───────────────────────────────┴──────────┴──────────┘
▲ ▲ ▲
│ │ │
buf qname_ptr qtrailer_ptr
(buf + 12) (qname_ptr + name_len)
qtrailer_ptr = qname_ptr + name_len; 填入 QTYPE = htons(1)(Type A 查詢 IPv4)。qtrailer_ptr + 2 填入 QCLASS = htons(1)(Class IN 網際網路)。0x1234:便於 Wireshark 抓包除錯。rand() & 0xFFFF):不過要特別注意:rand() 不是密碼學安全亂數,不能稱為真正的工業級 DNS 防護。真實 DNS Resolver 面對 DNS 快取毒害(DNS Cache Poisoning / Kaminsky 攻擊) 時,通常還會搭配高品質亂數、隨機 Client UDP Source Port、查詢名稱隨機化、bailiwick checking,甚至 DNSSEC 等機制。今天的版本重點是理解 Transaction ID 的角色,而不是完成完整資安防護。
0xc0)DNS 回應封包中,網域名稱通常不會重複拼寫,而是採用 壓縮指標(Compression Pointer):
11(即十六進位 0xc0),代表這是一個 2-Byte 指標,指向封包前面出現過的網址名稱。(*p & 0xc0) == 0xc0 時,直接將指標前進 2 bytes(p += 2)即可略過名稱!Answer Resource Record 記憶體排列:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ NAME │ TYPE │ CLASS │ TTL │ RDLENGTH │ RDATA │
│ (指標2B) │ (2 B) │ (2 B) │ (4 B) │ (2 B) │ (IP, 4B) │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
▲
│
指標 p 循序解析並前進...
p += 2; // skip class 絕不能漏?):CLASS 欄位實實在在佔用了封包中的 2 個 bytes。若少跳 2 個 bytes,後面的 TTL、RDLENGTH 與 RDATA(真實 IP)記憶體位移將全面錯位,讀出的 IP 將淪為亂碼!教學版簡化提醒:本文程式碼為了聚焦 DNS 格式,部分欄位讀取採用直接指標轉型,例如
*(uint16_t *)p。更嚴謹的 parser 應使用memcpy讀取多位元組欄位,避免未對齊存取問題,並在每次p前進與讀取RDLENGTH後確認p + rdlength <= buffer + len,避免 malformed packet 造成越界讀取。
include/dns.h:DNS 標頭與常數定義#ifndef DNS_H
#define DNS_H
#include <stdint.h>
#include <stddef.h>
#define DNS_PORT 53
/* DNS 查詢類型 (QTYPE) */
#define DNS_TYPE_A 1 // 查詢 IPv4 位址
#define DNS_TYPE_CNAME 5 // 別名
/* DNS 查詢類別 (QCLASS) */
#define DNS_CLASS_IN 1 // 網際網路 (Internet)
/* DNS Header (RFC 1035) - 固定 12 Bytes */
struct dns_header
{
uint16_t id; // Transaction ID
uint16_t flags; // Flags
uint16_t qdcount; // Questions Count
uint16_t ancount; // Answer RRs Count
uint16_t nscount; // Authority RRs Count
uint16_t arcount; // Additional RRs Count
} __attribute__((packed));
/* 核心函式介面 */
int dns_encode_name(const char *domain, uint8_t *buffer);
int dns_build_query(const char *domain, uint8_t *buf, size_t buf_size);
void dns_parse_response(const uint8_t *buffer, size_t len);
#endif /* DNS_H */
src/dns.c:編碼、組裝與回覆解析實作#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h> // 提供 rand() 與 srand()
#include <time.h> // 提供 time() 作為隨機種子
#include "dns.h"
/* 將一般網址格式(如 google.com)轉換為 DNS 標籤格式(06 google 03 com 00) */
int dns_encode_name(const char *domain, uint8_t *buffer)
{
const char *start = domain;
uint8_t *p = buffer;
while (*start) {
const char *dot = strchr(start, '.');
int len;
if (dot) {
len = dot - start;
} else {
len = strlen(start);
}
if (len > 63) {
return -1;
}
*p++ = (uint8_t)len;
memcpy(p, start, len);
p += len;
if (!dot) {
break;
}
start = dot + 1;
}
*p++ = 0; // 結尾 0x00
return p - buffer;
}
/* 組裝完整的 DNS 查詢封包 (Header + Question) */
int dns_build_query(const char *domain, uint8_t *buf, size_t buf_size)
{
if (buf_size < sizeof(struct dns_header) + 4) {
return -1;
}
memset(buf, 0, buf_size);
// 1. 組裝 DNS Header (12 bytes)
struct dns_header *dns = (struct dns_header *)buf;
uint16_t tx_id = (uint16_t)(rand() & 0xFFFF); // 隨機 Transaction ID
dns->id = htons(tx_id);
dns->flags = htons(0x0100); // 0x0100: 標準查詢 + Recursion Desired
dns->qdcount = htons(1); // 1 個問題
dns->ancount = 0;
dns->nscount = 0;
dns->arcount = 0;
// 2. 組裝 Question: QNAME
uint8_t *qname_ptr = buf + sizeof(struct dns_header);
int name_len = dns_encode_name(domain, qname_ptr);
if (name_len < 0) {
return -1;
}
// 3. 組裝 Question: QTYPE (2 bytes) 與 QCLASS (2 bytes)
uint8_t *qtrailer_ptr = qname_ptr + name_len;
if ((size_t)(sizeof(struct dns_header) + name_len + 4) > buf_size) {
return -1;
}
uint16_t qtype = htons(DNS_TYPE_A);
memcpy(qtrailer_ptr, &qtype, sizeof(uint16_t));
uint16_t qclass = htons(DNS_CLASS_IN);
memcpy(qtrailer_ptr + sizeof(uint16_t), &qclass, sizeof(uint16_t));
return sizeof(struct dns_header) + name_len + 4;
}
/* 解析 DNS 回覆封包,提取並印出 IPv4 位址 */
void dns_parse_response(const uint8_t *buffer, size_t len)
{
if (len < sizeof(struct dns_header)) {
printf("[DNS] Response too short!\n");
return;
}
const struct dns_header *dns = (const struct dns_header *)buffer;
uint16_t ancount = ntohs(dns->ancount);
uint16_t qdcount = ntohs(dns->qdcount);
printf("\n=== DNS Response ===\n");
printf("Transaction ID : 0x%04x\n", ntohs(dns->id));
printf("Flags : 0x%04x\n", ntohs(dns->flags));
printf("Questions : %u\n", qdcount);
printf("Answer RRs : %u\n", ancount);
if (ancount == 0) {
printf("[DNS] No answers found.\n");
return;
}
// 跳過 Question 區段
const uint8_t *p = buffer + sizeof(struct dns_header);
for (int i = 0; i < qdcount; i++) {
while (p < buffer + len && *p != 0) {
if ((*p & 0xc0) == 0xc0) {
p += 2;
break;
}
p += (*p + 1);
}
if (*p == 0) {
p++;
}
p += 4; // 跳過 QTYPE (2B) + QCLASS (2B)
}
// 解析 Answer 區段
printf("\n--- Resolved IP Addresses ---\n");
for (int i = 0; i < ancount; i++) {
if (p >= buffer + len) break;
// 處理 Answer 中的 NAME (壓縮指標 0xc0)
if ((*p & 0xc0) == 0xc0) {
p += 2;
} else {
while (p < buffer + len && *p != 0) {
p += (*p + 1);
}
if (*p == 0) p++;
}
if (p + 10 > buffer + len) break;
uint16_t type = ntohs(*(uint16_t *)p);
p += 2; // skip type
uint16_t class = ntohs(*(uint16_t *)p);
p += 2; // skip class
uint32_t ttl = ntohl(*(uint32_t *)p);
p += 4; // skip ttl
uint16_t rdlength = ntohs(*(uint16_t *)p);
p += 2; // skip rdlength
if (type == DNS_TYPE_A && rdlength == 4) {
const uint8_t *ip = p;
printf("IPv4 Address : %u.%u.%u.%u (Class: %u [IN], TTL: %us)\n",
ip[0], ip[1], ip[2], ip[3], class, ttl);
} else if (type == DNS_TYPE_CNAME) {
printf("CNAME Record (Alias) [Class: %u, TTL: %us]\n", class, ttl);
}
p += rdlength; // 精準位移至下一筆 Answer
}
printf("=============================\n\n");
}
test/dns_client.c:客戶端發送與接收驗收工具#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "dns.h"
#define DNS_SERVER_IP "8.8.8.8"
int main(int argc, char *argv[])
{
const char *domain = "google.com";
if (argc > 1) {
domain = argv[1];
}
srand(time(NULL));
printf("Querying DNS Server (%s:53) for: %s ...\n", DNS_SERVER_IP, domain);
uint8_t query_buf[512];
int query_len = dns_build_query(domain, query_buf, sizeof(query_buf));
if (query_len < 0) {
fprintf(stderr, "Failed to build DNS query packet.\n");
return 1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return 1;
}
struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(DNS_PORT);
dest.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
ssize_t sent = sendto(sockfd, query_buf, query_len, 0,
(struct sockaddr *)&dest, sizeof(dest));
if (sent < 0) {
perror("sendto");
close(sockfd);
return 1;
}
printf("Sent %zd bytes DNS query to %s\n", sent, DNS_SERVER_IP);
uint8_t resp_buf[1024];
ssize_t resp_len = recvfrom(sockfd, resp_buf, sizeof(resp_buf), 0, NULL, NULL);
if (resp_len < 0) {
perror("recvfrom (Timeout or Server unreachable)");
close(sockfd);
return 1;
}
printf("Received %zd bytes DNS reply!\n", resp_len);
dns_parse_response(resp_buf, resp_len);
close(sockfd);
return 0;
}
user@MSI:/mnt/c/Users/zenboen/Tutorial/Networking_Fundamentals$ make dns_client
cc -Wall -Wextra -Iinclude test/dns_client.c src/dns.c -o dns_client
user@MSI:/mnt/c/Users/zenboen/Tutorial/Networking_Fundamentals$ ./dns_client google.com
Querying DNS Server (8.8.8.8:53) for: google.com ...
Sent 28 bytes DNS query to 8.8.8.8
Received 124 bytes DNS reply!
=== DNS Response ===
Transaction ID : 0x73d0
Flags : 0x8180
Questions : 1
Answer RRs : 6
--- Resolved IP Addresses ---
IPv4 Address : 142.250.157.100 (Class: 1 [IN], TTL: 121s)
IPv4 Address : 142.250.157.102 (Class: 1 [IN], TTL: 121s)
IPv4 Address : 142.250.157.113 (Class: 1 [IN], TTL: 121s)
IPv4 Address : 142.250.157.138 (Class: 1 [IN], TTL: 121s)
IPv4 Address : 142.250.157.101 (Class: 1 [IN], TTL: 121s)
IPv4 Address : 142.250.157.139 (Class: 1 [IN], TTL: 121s)
=============================
| 區段 | 欄位內容 | 長度 (Bytes) | 累計長度 | 說明 |
|---|---|---|---|---|
| DNS Header | ID, Flags, Counts | 12 | 12 | Transaction ID(2) + Flags(2) + QDCOUNT(2) + 其餘Counts(6) |
| QNAME | 06 google 03 com 00 |
12 | 24 | [6]google(7) + [3]com(4) + \0(1) |
| Question Trailer | QTYPE + QCLASS | 4 | 28 | QTYPE=1(2) + QCLASS=1(2) |
$$\text{總長度} = 12 \text{ (Header)} + 12 \text{ (QNAME)} + 4 \text{ (Trailer)} = 28 \text{ Bytes (100% 吻合)}$$
| 區段 | 欄位內容 | 長度 (Bytes) | 累計長度 | 說明 |
|---|---|---|---|---|
| DNS Header | ID, Flags(0x8180), Counts | 12 | 12 | 包含 ANCOUNT = 6 |
| Question Section | 原樣抄回的問題 | 16 | 28 | QNAME(12) + QTYPE(2) + QCLASS(2) |
| Answer RRs (共 6 筆) | 6 筆 IPv4 A 記錄 | 96 ($16 \times 6$) | 124 | 每一筆 A 記錄佔 16 Bytes:• Name 指標 0xc00c (2B)• Type=1 (2B)• Class=1 (2B)• TTL (4B)• RDLength=4 (2B)• RDATA IPv4 (4B) |
$$\text{總長度} = 12 \text{ (Header)} + 16 \text{ (Question)} + (16 \times 6) \text{ (6 筆 Answer)} = 124 \text{ Bytes (100% 吻合)}$$
google.com 回覆 6 組 IP?從實測輸出可以看到:ANCOUNT = 6,代表這次 DNS 回覆包含 6 筆 Answer RR,也就是 6 組 IPv4 位址。
這通常是因為大型服務會透過 DNS 回傳多個 A Record:
dns_parse_response() 能依照 ANCOUNT 逐筆前進,成功解析多筆 Answer,而不是只處理第一筆。Application Layer
│
├── DNS (Mini DNS Client - Day 15) [Port 53]
│
Transport Layer
│
├── UDP (Port Multiplexing & Dispatcher - Day 12~14)
│
Network Layer
│
├── ICMP (Echo Request / Reply / Time Exceeded - Day 07~10)
│
├── IPv4 (Header, Checksum, Decrement TTL, Routing - Day 05~11)
│
└── ARP (ARP Request / Reply & ARP Table Cache - Day 03~06)
Link Layer
│
└── Ethernet (EtherType Dispatcher, MAC Filtering - Day 01~02)
Hardware / Virtual Interface
│
└── Linux TAP Device (/dev/net/tun)
完成第一個 L7 應用服務後,明天我們即將邁入電腦網路中最深奧、也是全世界流量承載量最大的傳輸層霸主:
TCP (Transmission Control Protocol)
我們不會一開始就做複雜的 Handshake,而是先打穩基本功:
Sequence Number 與 Acknowledgement Number 如何實現可靠傳輸與重傳。SYN(同步連線)ACK(確認收訖)FIN(結束連線)RST(強制重置)PSH(立即推送)URG(緊急指標)你將第一次親眼看到網際網路每天運作幾千億次的連線心臟!