DNS协议
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 协议的域名设置要求,我们通常的域名格式如下:
而我们 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); }</code></pre></div></div><p>就是域名格式的转化。</p><p>有了头部和数据内容的初始化,我们换需要根据两个内容合成一个数据包内容,就有以下代码:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>c</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-c"><code class="language-c" style="margin-left:0">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 中的 dns 协议
做网络分析,那么 Wireshark 是必不可少的,这里就用 Wireshark 简单分析一下dns 协议。
图中是一个 dns 的数据包情况,两个发送询问 s19.cnzz.com 另一个返回数据包。
我们先看发送数据包的头部:
数据包是应用层的数据,所以在数据包内容最下方,上述图片是协议头部,跟我的结构体一摸一样,其中 id 是 0x1209,flags 是 0x0100 , questions 是 1 其他都是 0
接下来看协议内容:
主要就是域名 name, 类型和类,其中长度是软件自己算出来的,协议自带内容。
至此,dns 协议内容差不多就是这样,你也可以自己动手实现一下。