在上一篇博客 Linux: C语言发起 DNS 查询报文 中,自己构造 DNS 查询报文,发出去,接收响应,以二进制形式把响应的数据写入文件并进行分析。文章的最后留下一个悬念,就是写代码解析 DNS answer section 部分。本文来完成解析应答报文的代码。
当我们使用浏览器访问某个网站的时候,浏览器拿到 URL 后,会解析 URL,拿到网站的域名,然后再进行 DNS 解析,拿到这个网站域名对应服务器的 IP 地址。然后使用网站服务器的 IP 地址和服务器建立一个 TCP 连接。再往后还有 SSL/TLS 握手等等操作,然后是交换数据。
不止是浏览器访问网站,很多情景下都会用到 DNS 解析。
DNS 协议多数情况下使用 UDP 协议进行通信,有的时候也会使用 TCP 进行通信(传输大量数据)。
DNS 协议使用 53 号端口。
Talk is cheap, show code:
//dnr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <ctype.h> // for isprint()
#define VERSION "1.0.0"
#define DNS_SERVER "8.8.8.8" // Google's public DNS server
#define DNS_PORT 53 // DNS uses port 53
// 生成随机的 16 位事务 ID
unsigned short generate_random_id() {
srand(time(NULL)); // 设置随机数种子(基于当前时间)
return (unsigned short)(rand() % 65536); // 生成 0 到 65535 的随机数
}
// DNS 头部结构体
struct DNSHeader {
unsigned short id; // Transaction ID
unsigned short flags; // DNS flags
unsigned short qdcount; // Number of questions
unsigned short ancount; // Number of answers
unsigned short nscount; // Number of authority records
unsigned short arcount; // Number of additional records
};
// DNS 查询部分
struct DNSQuestion {
unsigned short qtype; // Query type (A, MX, etc.)
unsigned short qclass; // Query class (IN, etc.)
};
// DNS Resource Record structure
struct DNSRecord {
unsigned short type; // Record Type (A, CNAME, etc.)
unsigned short class_; // Class (IN)
unsigned int ttl; // TTL (Time to Live)
unsigned short rdlength; // Length of the record data
unsigned char rdata[]; // Record data (IP address for A record)
// unsigned char *rdata; // *(DNSRecord->rdata + 1) ===> Segmentation fault (core dumped)
};
// 构建 DNS 查询报文
int build_dns_query(char *query, const char *hostname, int pos) {
char *label;
for (label = strtok(strdup(hostname), "."); label != NULL; label = strtok(NULL, ".")) {
query[pos++] = strlen(label);
strcpy(query + pos, label);
pos += *(query + pos - 1);
}
query[pos++] = 0;
struct DNSQuestion question = { htons(1), htons(1) }; // For CNAME use "{ htons(5), htons(1)}"";
memcpy(query + pos, &question, sizeof(question));
return (pos + sizeof(question));
}
// 打印十六进制数据,每行显示 16 个字节
void print_hex(const unsigned char *data, size_t length) {
for (size_t i = 0; i < length; i++) {
// 每行打印 16 个字节
if (i % 16 == 0) {
// 打印行号偏移 (16进制格式)
printf("%08zx: ", i); // z 长度修饰符表示接下来要输出的是一个size_t类型的值。size_t是一个无符号整数类型
}
// 打印当前字节的十六进制表示
printf("%02x ", data[i]);
// 每行结束时,打印字符表示(可打印字符显示,其他显示点 '.')
if (i % 16 == 15) {
// 如果是当前行最后一个字节
printf(" ");
for (size_t j = i - (i % 16); j <= i; j++) {
if (isprint(data[j])) {
printf("%c", data[j]);
} else {
printf(".");
}
}
printf("\n"); // 换行
} else if (i == length - 1) {
//或者是最后一行
for (size_t k = 0; k < (16 - (length % 16)); k++)
printf(" ");
printf(" ");
for (size_t j = i - (i % 16); j <= i; j++) {
if (isprint(data[j])) {
printf("%c", data[j]);
} else {
printf(".");
}
}
printf("\n"); // 换行
}
}
}
// Parse DNS Answer Section
void parse_answer(const unsigned char *data, size_t len) {
struct DNSRecord *answer = (struct DNSRecord *)(data);
const unsigned short TYPE = ntohs(answer->type);
const unsigned short CLASS = ntohs(answer->class_);
const unsigned int TTL = ntohl(answer->ttl);
const unsigned short RDLENGTH = ntohs(answer->rdlength);
// Print TYPE, CLASS, TTL, and RDLENGTH
printf("TYPE: ");
switch (TYPE) {
case 1: // A Record
printf("A\n");
break;
case 5: // CNAME Record
printf("CNAME\n");
break;
default:
printf("%d\n", TYPE); // For other record types, just print the number
break;
}
printf("CLASS: ");
switch (CLASS) {
case 1: // IN (Internet)
printf("IN\n");
break;
default:
printf("%d\n", CLASS); // For other classes, just print the number
break;
}
printf("TTL: %d\n", TTL);
printf("RDLENGTH: %d\n", RDLENGTH);
// Handle RDATA based on the record type
if (TYPE == 1) { // A record (IPv4 Address)
if (RDLENGTH == 4) { // RDATA for A record should always be 4 bytes (IPv4 address)
unsigned char *ip = answer->rdata;
printf("RDATA (IP Address): %u.%u.%u.%u\n",
(unsigned char) *(answer->rdata),
(unsigned char) *(answer->rdata + 1),
(unsigned char) *(answer->rdata + 2),
(unsigned char) *(answer->rdata + 3));
} else {
printf("Invalid RDLENGTH for A record\n");
}
} else if (TYPE == 5) { // CNAME record
// CNAME is a domain name, it is stored as a series of labels
// rdata points to the domain name, so print it
printf("RDATA (CNAME): ");
unsigned char *cname = answer->rdata;
// DNS names are in "label" format, so we need to handle them accordingly
while (*cname != 0) {
int label_length = *cname; // Length of the label
cname++;
for (int i = 0; i < label_length; i++) {
printf("%c", cname[i]);
}
cname += label_length;
if (*cname != 0) {
printf(".");
}
}
printf("\n");
} else {
printf("RDATA: Raw Data\n");
// For other record types, just print the raw data (for debugging purposes)
print_hex(answer->rdata, RDLENGTH);
}
putchar('\n');
}
// DNS request
char* dns_request(char *hostname, const char *DNS_Server) {
printf("\ndnr version %s\n\n", VERSION);
char query[512] = { 0 };
// 设置 DNS 请求头
unsigned short id = generate_random_id();
struct DNSHeader header = { htons(id), htons(0x0100), htons(1), htons(0), htons(0), htons(0) };
memcpy(query, &header, sizeof(header));
int pos;
pos = build_dns_query(query, hostname, sizeof(header));
printf("Query:\n");
print_hex(query, pos);
int sockfd;
struct sockaddr_in server_addr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP
if (sockfd < 0) {
return "Socket creation failed";
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(DNS_PORT);
if (DNS_Server == NULL)
server_addr.sin_addr.s_addr = inet_addr(DNS_SERVER);
else
server_addr.sin_addr.s_addr = inet_addr(DNS_Server);
if (sendto(sockfd, query, sizeof(query), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
close(sockfd);
return "Sendto failed";
}
char buffer[512] = { 0 };
socklen_t len = sizeof(server_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &len);
if (n < 0) {
close(sockfd);
return "Recvfrom failed";
}
printf("\n\nReceived %d bytes from DNS server.\n\n", n);
printf("Response:\n");
print_hex(buffer, n);
printf("\n\nAnswer Section:\n");
for (int i = pos; i < n; i++)
printf("%02x ", (unsigned char) *(buffer + i));
printf("\n\nAnalysis:\n");
struct DNSHeader *resHeader = NULL;
resHeader = (struct DNSHeader *)buffer;
//memcpy(resHeader, buffer, sizeof(struct DNSHeader));
printf("Number of questions: %d\n", ntohs(resHeader->qdcount));
printf("Number of answers: %d\n", ntohs(resHeader->ancount));
printf("Number of authority records: %d\n", ntohs(resHeader->nscount));
printf("Number of additional records: %d\n", ntohs(resHeader->arcount));
if (0 != ntohs(resHeader->ancount)) { // Number of answers is not zero.
while (pos < n) {
unsigned short RDlen;
memcpy(&RDlen, (buffer + pos + 10), 2);
RDlen = ntohs(RDlen);
parse_answer(buffer + pos + 2, 10 + RDlen);
pos += (12 + RDlen);
}
}
close(sockfd);
}
int main(int argc, char* argv[]) {
if (argc < 2 || !(strcmp("-h", argv[1])) || !(strcmp("-help", argv[1]))) {
fprintf(stderr, "\n%s version %s\n\n\tAuthor: Jackey Song\n\n\tDescription: Get the IP addresses corresponding to the domain names.\n\n\tUsage:\n\t %s <hostname_1> <hostname_2> <hostname_3> ...\n\t %s -s <DNS_Server_IP_Address> <hostname_1> <hostname_2> <hostname_3> ...\n\n",argv[0], VERSION, argv[0], argv[0]);
fprintf(stderr, "-----------------------------------------------------------------------------------------------\n\n%s 版本 %s\n\n\t作者: Jackey Song\n\n\t描述: 获取与域名对应的IP地址。\n\n\t用法:\n\t %s <hostname_1> <hostname_2> <hostname_3> ...\n\t %s -s <DNS_Server_IP_Address> <hostname_1> <hostname_2> <hostname_3> ...\n\n",argv[0], VERSION, argv[0], argv[0]);
return 1; // 如果没有提供主机名,打印帮助信息并退出
}
else if (!(strcmp("-s", argv[1]))) {
for (int i = 3; i < argc; i++)
dns_request(argv[i], argv[2]);
}
else {
for (int i = 1; i < argc; i++) {
dns_request(argv[i], NULL);
}
}
return 0;
}
编译器仍然是 gcc
,gcc -o dnr dnr.c
编译后的二进制文件为 dnr
运行:./dnr -h
& ./dnr -help
显示帮助信息。
解析 baidu.com 和 jackey-song.com :
./dnr baidu.com jackey-song.com
查询 www.baidu.com CNAME
记录:
要修改代码 struct DNSQuestion question = { htons(1), htons(1) }; // For CNAME use "{ htons(5), htons(1)}"";
./dnr -s 192.168.3.1 baidu.com
,这里的 -s
可以指定 DNS 服务器的 IP 地址,192.168.3.1
是我本地 WIFI 路由器的 IP 地址,路由器配置 DNS 后,相当于是一个本地 DNS 服务器。如果不使用 -s
来指定 DNS 服务器,代码中会使用默认的 DNS 服务器,8.8.8.8
Google 公共 DNS 服务器。
从打印的结果可以看到 www.baidu.com
别名为 www.a.shifen.
,其实我的代码中还没有完善,Response 的最后两个字节是 c0 16
,这是一个指针,十六进制数 16
转换成十进制数就是 22,也就是说 www.a.shifen.
后面还有一部分,在整个 Response 的偏移量 22 位置处,偏移量下标从 0 开始,第 22 位置就是 03 63 6f 6d
(com
),所以 www.baidu.com
完整的别名就是 www.a.shifen.com
。
代码中需要注意的地方:
在 struct DNSRecord
的定义这里,一开始我使用的是 unsigned char *rdata
,当我使用指针 *(DNSRecord->rdata + 1)
操作的时候会出现错误 Segmentation fault (core dumped)
。 这是因为 rdata
被定义为指向 unsigned char
的指针(unsigned char *rdata
)。这样的话,rdata
只是一个指针,并没有分配内存来存储 DNS 记录的数据。使用 unsigned char rdata[];
柔性数组(变长数组类型)就可以解决使用指针操作结构体成员变量内存泄露的问题。
unsigned char rdata[]; // Record data (IP address for A record)
// unsigned char *rdata; // *(DNSRecord->rdata + 1) ===> Segmentation fault (core dumped)
在 parse_answer()
函数中,我一开始使用的是 memcpy(answer, data, len)
来进行内存操作,仍然会出现内存泄露的问题。
// Parse DNS Answer Section
void parse_answer(const unsigned char *data, size_t len) {
struct DNSRecord *answer = (struct DNSRecord *)(data);
//struct DNSRecord *answer;
//memcpy(answer, data, len); // printf("%s", answer->rdata); ===> Segmentation fault (core dumped)
(struct DNSRecord*)(data)
只是将 data
指针转换为 struct DNSRecord*
类型,告诉编译器 data
实际上是一个指向 struct DNSRecord
类型数据的指针。这种操作不会更改内存内容,只是改变了指针的解释方式。这是安全的,前提是 data
本身确实指向 struct DNSRecord
类型的数据(即它指向的数据布局与 struct DNSRecord
一致)。
memcpy(answer, data, len)
将 data
中的内容复制到 answer
中,假设 answer
已经是一个有效的指针,指向了足够的内存空间,能够容纳 len
字节数据。如果 answer
指向了非法的或未初始化的内存,或者 len
超出了 answer
可以承受的内存空间,就会发生访问违规,导致 segmentation fault。
注意网络字节序 使用 大端字节序 Big Endian,而有的主机使用小端字节序 Little Endian,htons()
主机字节序转换成网络字节序。ntohs()
网络字节序转换成主机字节序。
- 在大端字节序中,高位字节存储在低地址处,低位字节存储在高地址处。简单来说,数据的高字节放在内存的起始位置。
- 在小端字节序中,低位字节存储在低地址处,高位字节存储在高地址处。也就是说,数据的低字节放在内存的起始位置。