C 语言实现 DNS 协议的数据包发送和接收

DNS协议

DNS 协议可以说是计算机网络中必须知道的协议之一了,他最直接的功能就是将域名解析成对应的 IP 地址。

一个简单的 DNS 协议如下图:

DNS 请求过程

客户段查询域名,先查看本地的 DNS 缓存,如果有直接解析,没有就查询本地的 DNS 服务器,然后就是域名的递归查询。

另外一提:很多人讲到 DNS 协议的时候就是会提到 httpDNS 协议,就是一些大厂会自己建立一些域名解析服务,使用 http 协议查询,便于人们查询。

当然部分人对这提出质疑,并不是说技术上不能实现,而是因为 DNS 协议本身是 UDP 传输,而 httpDNS 协议使用了 TCP 协议,需要三次握手,这样解析速度真的能满足要求吗?这里只是简单提一下,如果想要看这部分相关实验,可以看 《 Wireshark网络分析艺术》这本书中 “寻找 httpDNS ”的章节观看。

代码实现

话说回来,如果想要真正实地的发送 DNS 协议首先就是了解数据包的结构。

DNS 数据包中有报文头部和报文内容两部分,报文头部内容如下:

DNS 报文格式

其中前三行是报文头部,后边是报文内容。

所以就有如下数据结构:

代码语言:c
复制
//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; //查询域名
};

老规矩,有了数据结构,就要想办法初始化

初始化代码:

代码语言:c
复制
//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 协议的域名设置要求,我们通常的域名格式如下:

代码语言:c
复制

而我们 DNS 协议是不能这样解析域名的,需要转化成以下格式:

代码语言:c
复制
3www5baidu3com0

前边数字表示后边字符个数,最后以 0 结尾。

如果知道这个应该就知道上述代码中以下部分的是干什么的了。

代码语言:c
复制
 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-&gt;name, question-&gt;length);
offset += question-&gt;length;  //添加域名

memcpy(request+offset, &amp;question-&gt;qtype, sizeof(question-&gt;qtype));
offset += sizeof(question-&gt;qtype); //添加类型

memcpy(request+offset, &amp;question-&gt;qclass, sizeof(question-&gt;qclass));
offset += sizeof(question-&gt;qclass);//添加类
return offset;

}

上述代码比较简单,就是将协议头和协议内容合成一个指针。

最后就是简单的协议的发送和接受了。不过在这之前先进行一个宏定义,定义一下我们的端口和服务器地址。

代码语言:c
复制
#define DNS_SERVER_PORT 53
#define DNS_SERVER_IP "114.114.114.114"

最后上代码:

代码语言:c
复制
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*)&amp;servaddr, sizeof(servaddr));  //连接

struct dns_header header = {0};  //创建协议头
dns_create_header(&amp;header);

struct dns_question question = {0};  //创建协议内容
dns_create_question(&amp;question, domain);

char request[1024] = {0};
int length = dns_build_requestion(&amp;header, &amp;question, request, 1024);  //连接协议头和协议内容

int slen = sendto(sockfd,  request, length, 0, (struct sockaddr*)&amp;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*)&amp;addr, (socklen_t)&amp;addr); //接受内容
printf(&#34;recvfrom : %d, %s\n&#34;, n, response); //打印
return n;

}

上述的代码比较清晰,就是一个简单的协议内容的发送和接受。

wireshark 中的 dns 协议

做网络分析,那么 Wireshark 是必不可少的,这里就用 Wireshark 简单分析一下dns 协议。

图中是一个 dns 的数据包情况,两个发送询问 s19.cnzz.com 另一个返回数据包。

我们先看发送数据包的头部:

数据包是应用层的数据,所以在数据包内容最下方,上述图片是协议头部,跟我的结构体一摸一样,其中 id 是 0x1209,flags 是 0x0100 , questions 是 1 其他都是 0

接下来看协议内容:

主要就是域名 name, 类型和类,其中长度是软件自己算出来的,协议自带内容。

至此,dns 协议内容差不多就是这样,你也可以自己动手实现一下。