DNS 协议可以说是计算机网络中必须知道的协议之一了,他最直接的功能就是将域名解析成对应的 IP 地址。
一个简单的 DNS 协议如下图:
客户段查询域名,先查看本地的 DNS 缓存,如果有直接解析,没有就查询本地的 DNS 服务器,然后就是域名的递归查询。
另外一提:很多人讲到 DNS 协议的时候就是会提到 httpDNS 协议,就是一些大厂会自己建立一些域名解析服务,使用 http 协议查询,便于人们查询。
当然部分人对这提出质疑,并不是说技术上不能实现,而是因为 DNS 协议本身是 UDP 传输,而 httpDNS 协议使用了 TCP 协议,需要三次握手,这样解析速度真的能满足要求吗?这里只是简单提一下,如果想要看这部分相关实验,可以看 《 Wireshark网络分析艺术》这本书中 “寻找 httpDNS ”的章节观看。
话说回来,如果想要真正实地的发送 DNS 协议首先就是了解数据包的结构。
DNS 数据包中有报文头部和报文内容两部分,报文头部内容如下:
其中前三行是报文头部,后边是报文内容。
所以就有如下数据结构:
//dns 头部 六项表示头部六个类型
struct dns_header
{
unsigned short id;
unsigned short flags;
unsigned short questions;
unsigned short answer;
unsigned short authority;
unsigned short additional;
};
//查询问题区域 查询问题有三个标志域名,类型和类
struct dns_question
{
int length; //自己添加的长度
unsigned short qtype; //类型
unsigned short qclass; //类
unsigned char* name; //查询域名
};
老规矩,有了数据结构,就要想办法初始化
初始化代码:
//dns 头初始化 其中前三项是头部必须,因此必须初始化,后边的不太重要
int dns_create_header(struct dns_header* header)
{
if(header == NULL) return -1;
memset(header, 0, sizeof(struct dns_header)); //分配内存
srandom(time(NULL)); // 随机数
header->id = random();
header->flags = htons(0x100); //将16位主机字节序转换为网络字节序
header->questions = 1;
return 0;
}
int dns_create_question(struct dns_question* questions, const char* hostname)
{
if(questions == NULL || hostname == NULL) return -1;
memset(questions, 0, sizeof(struct dns_question)); //分配内存
questions->name = (unsigned char *)malloc(strlen(hostname) + 2); //表示域名长度
if(questions->name == NULL)
{
return -2;
}
questions->length = strlen(hostname) + 2;
questions->qtype = htons(1);
questions->qclass = htons(1);
const char delim[2] = ".";
char *qname = questions->name;
char *hostname_dup = strdup(hostname);
char *token = strtok(hostname_dup, delim);
while(token != NULL)//域名格式转化
{
size_t len = strlen(token);
*qname = len;
qname++;
strncpy(qname, token, token + 1);
qname += len;
token = strtok(NULL, delim);
}
free(hostname_dup);
return 0;
}
此处需要进行两个解释:
1、为什么使用 hton() 这个函数? 因为网络协议我们一般使用大端字节序,而我们大多数电脑内存使用小端字节序,所以在自己传输数据的时候需要进行转换。
2、questions->length = strlen(hostname) + 2; 不知道大家注意到这个没有,为什么要 +2 ,+1 我们能理解,因为字符串有 '\0' 之类的,但是这里为什么 + 2.
这里倒不是什么其他原因,而是 DNS 协议的域名设置要求,我们通常的域名格式如下:
www.baidu.com
而我们 DNS 协议是不能这样解析域名的,需要转化成以下格式:
3www5baidu3com0
前边数字表示后边字符个数,最后以 0 结尾。
如果知道这个应该就知道上述代码中以下部分的是干什么的了。
char *hostname_dup = strdup(hostname);
char *token = strtok(hostname_dup, delim);
while(token != NULL)//域名格式转化
{
size_t len = strlen(token);
*qname = len;
qname++;
strncpy(qname, token, token + 1);
qname += len;
token = strtok(NULL, delim);
}
就是域名格式的转化。
有了头部和数据内容的初始化,我们换需要根据两个内容合成一个数据包内容,就有以下代码:
int dns_build_requestion(const struct dns_header *header, struct dns_question* question, char *request, int rlen )
{
if(header == NULL || request == NULL) return -1;
memset(request, 0, rlen); //分配内存
//int offset = 0;
memcpy(request, header, sizeof(struct dns_header));
int offset = sizeof(struct dns_header); //添加header
memcpy(request+ offset, question->name, question->length);
offset += question->length; //添加域名
memcpy(request+offset, &question->qtype, sizeof(question->qtype));
offset += sizeof(question->qtype); //添加类型
memcpy(request+offset, &question->qclass, sizeof(question->qclass));
offset += sizeof(question->qclass);//添加类
return offset;
}
上述代码比较简单,就是将协议头和协议内容合成一个指针。
最后就是简单的协议的发送和接受了。不过在这之前先进行一个宏定义,定义一下我们的端口和服务器地址。
#define DNS_SERVER_PORT 53
#define DNS_SERVER_IP "114.114.114.114"
最后上代码:
int dns_client_commit(const char* domain)
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建socket 注意是udp 连接
if(sockfd < 0) return -1;
struct sockaddr_in servaddr = {0}; //配置服务器端口地址等
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(DNS_SERVER_PORT);
servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); //连接
struct dns_header header = {0}; //创建协议头
dns_create_header(&header);
struct dns_question question = {0}; //创建协议内容
dns_create_question(&question, domain);
char request[1024] = {0};
int length = dns_build_requestion(&header, &question, request, 1024); //连接协议头和协议内容
int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)); //发送到dns服务器
//recvfrom
char response[1024] = {0}; //接受协议返回
struct sockaddr_in addr;
size_t addr_len = sizeof(struct sockaddr_in);
int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t)&addr); //接受内容
printf("recvfrom : %d, %s\n", n, response); //打印
return n;
}
上述的代码比较清晰,就是一个简单的协议内容的发送和接受。
做网络分析,那么 Wireshark 是必不可少的,这里就用 Wireshark 简单分析一下dns 协议。
图中是一个 dns 的数据包情况,两个发送询问 s19.cnzz.com 另一个返回数据包。
我们先看发送数据包的头部:
数据包是应用层的数据,所以在数据包内容最下方,上述图片是协议头部,跟我的结构体一摸一样,其中 id 是 0x1209,flags 是 0x0100 , questions 是 1 其他都是 0
接下来看协议内容:
主要就是域名 name, 类型和类,其中长度是软件自己算出来的,协议自带内容。
至此,dns 协议内容差不多就是这样,你也可以自己动手实现一下。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。