前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C 语言实现 DNS 协议的数据包发送和接收

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

原创
作者头像
ge3m0r
发布2024-04-28 13:55:20
1190
发布2024-04-28 13:55:20

DNS协议

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

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

DNS 请求过程
DNS 请求过程

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

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

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

代码实现

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

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
复制
www.baidu.com

而我们 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);
        
    }

就是域名格式的转化。

有了头部和数据内容的初始化,我们换需要根据两个内容合成一个数据包内容,就有以下代码:

代码语言:c
复制
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;
    
}

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

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

代码语言: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*)&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 协议内容差不多就是这样,你也可以自己动手实现一下。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • DNS协议
  • 代码实现
  • wireshark 中的 dns 协议
相关产品与服务
移动解析 HTTPDNS
移动解析 HTTPDNS 基于 HTTP 协议向腾讯云的 DNS 服务器发送域名解析请求,替代了基于 DNS 协议向运营商 Local DNS 发起解析请求的传统方式,可以避免 Local DNS 造成的域名劫持和跨网访问问题,解决移动互联网服务中域名解析异常带来的困扰。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档