本文目录
- 使用 getaddrinfo()
- 手动构造 DNS 查询报文
- DNS 查询部分(Question Section)
- QNAME (查询的域名)
- QTYPE (查询类型)
- QCLASS (查询类)
- Answer Section (答案部分)
- C语言代码发起 DNS 查询报文
使用 getaddrinfo()
getaddrinfo()
是一个高层的接口,它可以用来处理 DNS 查询、地址解析等任务。使用这个接口非常简单,不需要手动构建 DNS 查询报文。
// dnsReq.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>void dnsRequests(char *hostname) {struct addrinfo hints, *res;int status;// 设置 hintsmemset(&hints, 0, sizeof(hints));hints.ai_family = AF_INET; // IPv4 地址hints.ai_socktype = SOCK_STREAM; // TCP 连接// 获取解析结果if ((status = getaddrinfo(hostname, NULL, &hints, &res)) != 0) {fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));return; // 出现错误时直接返回}// 打印解析到的 IP 地址char ipstr[INET_ADDRSTRLEN];struct sockaddr_in *ipv4 = (struct sockaddr_in *)res->ai_addr;inet_ntop(res->ai_family, &ipv4->sin_addr, ipstr, sizeof(ipstr));printf("IP address of %s: %s\n", hostname, ipstr);// 释放地址信息freeaddrinfo(res);
}int main(int argc, char* argv[]) {if (argc < 2) {fprintf(stderr, "Usage: %s <hostname>...\n", argv[0]);return 1; // 如果没有提供主机名,打印帮助信息并退出}for (int i = 1; i < argc; i++) { // 从 argv[1] 开始dnsRequests(argv[i]);}return 0;
}
编译:gcc -o dnsReq dnsReq.c
运行:./dnsReq hostname_1 hostname_2 hostname_3 ... ...
手动构造 DNS 查询报文
DNS 查询和回答报文具有相同的格式。
DNS 报文首部占 12 个字节。
- 前 16 比特标识,事务 ID,用于标识请求和响应之间的匹配。客户端和服务器在发送和接收查询时会使用该 ID。
- Flags :标志字段,指示查询类型、响应状态等。
- QR:1 位,表示查询(0)还是响应(1)。
- Opcode:4 位,表示查询的类型(标准查询、反向查询等)。
- 0:标准查询(Standard query)。这是最常见的查询类型,用于获取指定域名的IP地址或其他DNS记录。
- 1:反向查询(Inverse query)。这种查询类型通常用于根据IP地址获取对应的域名,与标准查询相反。
- 2:服务器状态请求(Status request)。这种查询类型用于请求DNS服务器的状态信息,通常用于诊断或监控目的。
- AA:1 位,表示是否为权威答案(1 表示是)。
- TC:1 位,表示响应是否被截断(通常用于 TCP 响应)。
- RD:1 位,表示是否要求递归查询(1 表示要求)。
- RA:1 位,表示是否支持递归查询(1 表示支持)。
- zero:3 位,保留,通常为 0。
- rCode:4 位,响应码,表示查询的状态(成功、无数据等)。
- 0:无差错。表示DNS查询成功完成,没有遇到任何问题。
- 1:格式差错(Format error)。这表示服务器不能理解请求的报文格式,可能是由于报文格式不正确或存在无法识别的字段。
- 2:域名服务器失败(Server failure)。这表示由于服务器的原因导致无法处理请求,可能是由于服务器内部错误或资源不足等原因。
- 3:名字错误(Name Error)。这表示解析的域名不存在,只有对授权域名解析服务器有意义。它指出客户端请求的域名在DNS系统中不存在或无法找到。
- 4:查询类型不支持(Not Implemented)。这表示域名服务器不支持客户端请求的查询类型。
- 5:拒绝(Refused)。这表示服务器由于设置的策略拒绝给出应答,例如服务器不希望对某些请求者给出应答。
- Number of questions:表示查询中的问题数目,通常为 1,表示只查询一个域名的记录。
- Number of anwser RRs:表示响应中答案的数量。在查询报文中,这个字段一般为 0。
- Number of anthority RRs:表示响应中权威记录的数量。
- Number of additional RRs:表示响应中附加记录的数量。
DNS 查询部分(Question Section)
查询部分包含一个或多个问题,通常情况下一个 DNS 查询只包含一个问题。该部分包括域名和查询的类型。每个问题由以下字段组成:
字段 | 描述 | 长度(字节) |
---|---|---|
QNAME | 查询的域名,格式为标签(label)加点(‘.’)。 | 可变 |
QTYPE | 查询类型,表示想要查询的记录类型(如 A、MX)。 | 2 |
QCLASS | 查询类,表示查询的类,通常为 IN(Internet)。 | 2 |
QNAME (查询的域名)
QNAME
是一个以点(.
)分隔的标签(label)格式的域名。例如,如果查询 www.example.com
,QNAME
就是由标签 www
、example
、com
组成的。
每个标签之前有一个字节表示该标签的长度(例如 “www” 对应的标签长度为 3,“example” 长度为 7)。最终,域名以一个 0x00
字节结束,表示域名的结尾。
QTYPE (查询类型)
QTYPE
是一个 16 位的字段,表示查询的记录类型。常见的查询类型有:
类型 | 描述 | 代码 |
---|---|---|
A | IPv4 地址 | 1 |
AAAA | IPv6 地址 | 28 |
MX | 邮件交换记录 | 15 |
CNAME | 别名记录 | 5 |
NS | 域名服务器记录 | 2 |
PTR | 反向查询记录 | 12 |
QCLASS (查询类)
QCLASS
是一个 16 位的字段,表示查询的类。通常查询类为 IN(互联网),其值为 1。
类别 | 描述 | 代码 |
---|---|---|
IN | Internet(互联网) | 1 |
CH | Chaos(用于 Chaosnet) | 3 |
HS | Hesiod(Hesiod 系统) | 4 |
Answer Section (答案部分)
答案部分包含 DNS 响应的资源记录。每个资源记录由以下字段组成:
字段 | 描述 | 长度(字节) |
---|---|---|
NAME | 域名(通常是指向查询的域名或子域名)。 | 可变 |
TYPE | 记录类型(如 A 记录、MX 记录)。 | 2 |
CLASS | 类别(通常是 IN)。 | 2 |
TTL | 生存时间(Time to Live),表示记录在缓存中的有效时间(单位是秒)。 | 4 |
RDLENGTH | 数据的长度。 | 2 |
RDATA | 记录数据,存储具体的信息(如 IP 地址)。 | 可变 |
DNS answer section:TYPE 类型字段有以下常用值:
-
A记录(Address Record)
- TYPE字段值为1。
- 表示将域名映射到IPv4地址。
-
NS记录(Name Server Record)
- TYPE字段值为2。
- 指定域名服务器负责特定区域的权威信息。
-
CNAME记录(Canonical Name Record)
- TYPE字段值为5。
- 提供域名的规范别名,通常用于将一个域名重定向到另一个域名。
-
PTR记录(Pointer Record)
- TYPE字段值为12。
- 用于反向DNS查询,将IP地址映射到域名。
-
MX记录(Mail Exchange Record)
- TYPE字段值为15。
- 指定邮件服务器的优先级和域名,用于邮件路由。
-
AAAA记录
- TYPE字段值为28。
- 类似于A记录,但用于将域名映射到IPv6地址。
-
SRV记录
- TYPE字段值为33。
- 指定提供特定服务的服务器的位置,包括主机名、端口号和优先级等信息。
-
TXT记录
- TYPE字段值为16。
- 提供任意文本信息,通常用于存储元数据或说明性信息。
-
NAPTR记录(Naming Authority Pointer Record)
- TYPE字段值为35。
- 用于基于正则表达式的域名到URI的映射。
-
TLSA记录
- TYPE字段值为863。
- 用于存储DNSSEC(域名系统安全扩展)的TLS证书关联信息。
C语言代码发起 DNS 查询报文
// dnsReq.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>#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 查询报文
void 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) };memcpy(query + pos, &question, sizeof(question));
}int main(int argc, char* argv[]) {char query[512] = { 0 };// 设置 DNS 请求头unsigned short id = generate_random_id();printf("%x\n", id);struct DNSHeader header = { htons(id), htons(0x0180), htons(1), htons(0), htons(0), htons(0) };memcpy(query, &header, sizeof(header));build_dns_query(query, "baidu.com", sizeof(header));int sockfd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDPif (sockfd < 0) {perror("Socket creation failed");return 1;}memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(DNS_PORT);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) {perror("Sendto failed");close(sockfd);return 1;}char buffer[512];socklen_t len = sizeof(server_addr);int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &len);if (n < 0) {perror("Recvfrom failed");close(sockfd);return 1;}printf("Received %d bytes from DNS server\n", n);FILE *file = fopen("output.bin", "wb");if (file == NULL) {perror("Failed to open file");return EXIT_FAILURE;}size_t written = fwrite(buffer, sizeof(unsigned char), n, file);if (written != n) {perror("Failed to write complete data to file");fclose(file);return EXIT_FAILURE;}// 关闭文件fclose(file);close(sockfd);return 0;
}
环境在 Linux 发送版 Ubuntu 下,编译器使用 gcc,
编译命令: gcc -o dnsReq dnsReq.c
运行:命令行下 ./dnsReq
,./
表示当前目录,dnsReq
编译后的可执行文件。
运行以上代码:构造了一个 DNS 查询报文(查询 baidu.com),并使用 socket 套接字编程连接谷歌公共 DNS 服务器 8.8.8.8
的 53
端口,发送了该 DNS 查询报文,将得到的响应以二进制的形式写入文件 output.bin
中。
一下是对该二进制文件的分析:
2136 8180 0001 0002 0000 0000 0562 6169 !6...........bai
6475 0363 6f6d 0000 0100 01c0 0c00 0100 du.com..........
0100 0000 9000 046e f244 42c0 0c00 0100 .......n.DB.....
0100 0000 9000 0427 9c42 0a .......'.B.
2136
ID8180
Flags,意思是: 响应报文、递归查询、支持递归查询、响应成功。0001
Number of questions :1个查询。0002
Number of answers:2个应答。0000
Number of anthority RRs :00000
Number of additional RRs :00562 6169 6475 0363 6f6d 0000 0100 01
代码中构造的 DNS 查询,请见代码。c0 0c00 0100 0100 0000 9000 046e f244 42
第一条应答:- NAME:
c0 0c
表示域名的指针,前两个 bit 为11
,后面14个 bit 的值为 12,表示域名在整个应答报文中的偏移量,偏移量的下标从 0 开始,也就是第 12 个字节处,正好指向 baidu.com - TYPE:
00 01
表示 A 记录。 - CLASS:
00 01
表示 IN (Internet 互联网) - TTL:
00 0000 90
生存时间,16*9 秒。 - RDLENGTH:
00 04
数据长度 4 个字节。 - RDDATA:
6e f244 42
4个字节的数据,写成点分十进制就是110.242.68.66
- NAME:
c0 0c00 0100 0100 0000 9000 0427 9c42 0a
第二条应答:数据部分写成点分十进制就是39.156.66.10
验证:
DNS 应答报文如果最后多出了一个 0a 字节,则表示换行,表示应答报文的结束。
拿到二进制数据,自己手动去翻译应答报文实属麻烦,写代码解析 DNS 应答报文的任务,就交给屏幕前正在阅读此文的你。