Linux: C语言解析域名
在上一篇博客 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 IDunsigned short flags; // DNS flagsunsigned short qdcount; // Number of questionsunsigned short ancount; // Number of answersunsigned short nscount; // Number of authority recordsunsigned 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 dataunsigned 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 RDLENGTHprintf("TYPE: ");switch (TYPE) {case 1: // A Recordprintf("A\n");break;case 5: // CNAME Recordprintf("CNAME\n");break;default:printf("%d\n", TYPE); // For other record types, just print the numberbreak;}printf("CLASS: ");switch (CLASS) {case 1: // IN (Internet)printf("IN\n");break;default:printf("%d\n", CLASS); // For other classes, just print the numberbreak;}printf("TTL: %d\n", TTL);printf("RDLENGTH: %d\n", RDLENGTH);// Handle RDATA based on the record typeif (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 itprintf("RDATA (CNAME): ");unsigned char *cname = answer->rdata;// DNS names are in "label" format, so we need to handle them accordinglywhile (*cname != 0) {int label_length = *cname; // Length of the labelcname++;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); // UDPif (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);elseserver_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()
网络字节序转换成主机字节序。
- 在大端字节序中,高位字节存储在低地址处,低位字节存储在高地址处。简单来说,数据的高字节放在内存的起始位置。
- 在小端字节序中,低位字节存储在低地址处,高位字节存储在高地址处。也就是说,数据的低字节放在内存的起始位置。