• C语言实现DNS请求器


    C语言实现DNS请求器

    项目介绍

    本程序完成给定指定域名请求对应ip地址的功能,类似于win和linux下nslookup的功能

    前置知识

    DNS介绍

    域名系统(英文:Domain Name System ,缩写 DNS )是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。 DNS 使用 TCP 和 UDP 端口 53 。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。域名系统(英文:Domain Name System ,缩写 DNS 的作用是将人类可读的域名如, www.example.com) 转换为机器可读的 IP 地址 如, 192.0.2.44) 。

    DNS的分层

    域名系统是分层次的。
    在域名系统的层次结构中,各种域名都隶属于域名系统根域的下级。域名的第一级是顶级域,它包括通用顶级域,例如 .com 、 .net 和 .org ;以及国家和地区顶级域,例如 .us 、 .cn 和 .tk 。顶级域名下一层是二级域名,一级一级地往下。这些域名向人们提供注册服务,人们可以用它创建公开的互联网资源或运行网站。顶级域名的管理服务由对应的 域名注册 管理机构(域名注册局)负责,注册服务通常由域名注册商负责。

    在这里插入图片描述

    域名解析

    主机名到IP 地址的映射有两种方式:
    静态映射 在本机上配置域名和 IP 的映射,旨在本机上使用。 Windows 和 Linux的 hosts 文件中的内容就属于静态映射。

    动态映射 建立一套域名解析系统( DNS ),只在专门的 DNS 服务器上配置主机到IP 地址的映射,网络上需要使用主机名通信的设备,首先需要到 DNS 服务器查询主机所对应的 IP 地址。通过域名去查询域名服务器,得到IP 地址的过程叫做域名解析。在解析域名时,一般先静态域名解析,再动态解析域名。可以将一些常用的域名放入静态域名解析表中,这样可以大大提高域名解析效率。

    递归查询和迭代查询

    • 递归查询:本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域名服务器无法解析,自己会以 DNS 客户机的身份向其它域名服务器查询,直到得到最终的 IP 地址告诉本机。
    • 迭代查询:本地域名服务器向根域名服务器查询,**根域名服务器告诉它下一步到哪里去查询,然后它再去查,**每次它都是以客户机的 身份去各个服务器查询

    DNS协议报文格式

    在这里插入图片描述

    头部(Header)

    在这里插入图片描述

    Queries(查询问题区域)

    在这里插入图片描述

    域名( 2 字节或不定长): 它的格式和 Queries 区域的查询名字字段是一样的。有一点
    不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。比
    如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表
    示,具体格式是最前面的两个高位是 11(0xC0) ,用于识别指针。其余的 14 位从 DNS 报文的开
    始处计数(从 0 开始),指出该报文中的相应字节数。一个典型的例子,
    C00C(11 00 000000001100, 12 (字节)正好是头部的长度,其正好指向 Queries 区域的查询名字字段 。
    查询类型(Type): 表明资源纪录的类型.
    查询类(Class): 对于 Internet 信息,总是 IN
    生存时间( TTL 以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程
    序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳
    定程度,极为稳定的信息会 被分配一个很大的值(比如 86400 ,这是一天的秒数)。
    资源数据: 该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数
    据。**可以是 Address (表明查询报文想要的回应是一个 IP 地址)**或者 CNAME (表明查询
    **报文想要的回应是一个规范主机名)**等。

    wireshark 分析DNS请求和响应报文

    在这里插入图片描述

    在这里插入图片描述

    技术要点

    1. 使用传输层的UDP Socket进行编程
    2. 应用层的DNS报文格式

    程序执行流程

    在这里插入图片描述

    程序设计流程

    1. DNS请求头以及正文部分的结构体定义
    2. DNS header 数据填充
    3. DNS question 数据填充
    4. 合并DNS header和question部分(build_requestion)
    5. 通过UDP Socket 发送请求报文和接受响应报文
    6. 解析出响应报文的相关数据

    代码剖析

    DNS header和 question部分结构体实现

    对照DNS报文格式进行实现即可

    //dns报文Header部分数据结构
    struct dns_header{
        unsigned short id; //2字节(16位)
        unsigned short flags; 
    
        unsigned short questions; //问题数
        unsigned short answer; //回答数
    
        unsigned short authority;
        unsigned short additional;
    };
    
    //dns报文Queries部分的数据结构
    struct dns_question{
        int length; //主机名的长度,自己添加的,便于操作
        unsigned short qtype;
        unsigned short qclass;
        //查询名是长度和域名组合
        //如www.0voice.com ==> 60voice3com0
        //之所以这么做是因为域名查询一般是树结构查询,com顶级域,0voice二级域
        unsigned char *name; // 主机名(长度不确定)
    };
    
    //dns响应报文中数据(域名和ip)的结构体
    struct dns_item{
        char *domain; 
        char *ip;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    DNS header数据填充

    我们只填充3个字段:Transaction IDFlagsQuestions.其中会话标识随机生成,标志和问题数要进行主机字节序到网络字节序的转换:

    //将header部分字段填充数据
    int dns_create_header(struct dns_header *header)
    {
        if(header == NULL)
            return -1;
        memset(header, 0x00, sizeof(struct dns_header));
    
        //id用随机数,种子用time(NULL),表明生成随机数的范围
        srandom(time(NULL)); // 线程不安全
        header->id = random();
        
        //网络字节序(大端):地址低位存数据高位;主机字节序则与之相反
        //主机(host)字节序转网络(net)字节序
        header->flags = htons(0x0100);
        header->questions = htons(1);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    DNS Queries部分数据的填充

    对照报文中Queries的部分进行填充

    int dns_create_question(struct dns_question *question, const char *hostname)
    {
        if(question == NULL || hostname == NULL)
            return -1;
        memset(question, 0x00, sizeof(struct dns_question));
    
        //内存空间长度:hostname长度 + 结尾\0 再多给一个空间
        question->name = (char *)malloc(strlen(hostname) + 2);
        if(question->name == NULL)
        {
            return -2;
        }
    
        question->length = strlen(hostname) + 2;
    
        //查询类型1表示获得IPv4地址
        question->qtype = htons(1);
        //查询类1表示Internet数据
        question->qclass = htons(1);
    
        //【重要步骤】
        //名字存储:www.0voice.com -> 3www60voice3com 
        const char delim[2] = ".";
        char *qname = question->name; //用于填充内容用的指针
    
        //strdup先开辟大小与hostname同的内存,然后将hostname的字符拷贝到开辟的内存上
        char *hostname_dup = strdup(hostname); //复制字符串,调用malloc
        //将按照delim分割出字符串数组,返回第一个字符串
        char *token = strtok(hostname_dup, delim);
    
        while(token != NULL)
        {
            //strlen返回字符串长度不含'\0'
            size_t len = strlen(token);
    
            *qname = len;//长度的ASCII码
            qname++;
    
            //将token所指的字符串复制到qname所指的内存上,最多复制len + 1长度 
            //len+1使得最后一个字符串把\0复制进去
            strncpy(qname, token, len + 1);
            qname += len;
    
            //固定写法,此时内部会获得下一个分割出的字符串返回(strtok会依赖上一次运行的结果)
            token = strtok(NULL, delim); //依赖上一次的结果,线程不安全
        }
    
        free(hostname_dup);       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    合并DNS header和question部分

    将header部分数据和question部分数据合并到字符数组中去。

    //将header和question合并到request中
    //header [in]
    //question [in]
    //request [out]
    //rlen:代表request的大小
    int dns_build_requestion(struct dns_header *header, struct dns_question *question, char *request, int rlen)
    {
        if (header == NULL || question == NULL || request == NULL)
            return -1;
    
        memset(request, 0, rlen);
    
        //header -> request
        memcpy(request, header, sizeof(struct dns_header));
        int offset = sizeof(struct dns_header);
    
        //Queries部分字段写入到request中,question->length是question->name的长度
        memcpy(request + offset, question->name, question->length);
        offset += question->length;
    
        memcpy(request + offset, &question->qclass, sizeof(question->qclass));
        offset += sizeof(question->qclass);
    
        memcpy(request + offset, &question->qtype, sizeof(question->qtype));
        offset += sizeof(question->qtype);
    
        return offset; //返回request数据的实际长度
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    UDP Socket编程发送DNS请求和接收DNS响应

    UDP Socket编程的基本流程和函数调用顺序基本相同,

    代码流程:

    1. 创建UDP Socket
    2. 填充服务器地址结构体的数据struct sockaddr_in
    3. 连接以保证尽可能的可靠(connect
    4. DNS报文数据填充 (dns_create_header、dns_build_requestion)
    5. 通过socket发送DNS请求报文(sendto)
    6. 接受DNS响应报文(recvfrom)
    7. 解析响应报文
    int dns_client_commit(const char *domain)
    {
        //下方流程是基本定死的套路
        //1.创建UDP socket
        //网络层ipv4, 传输层用udp
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd < 0)
        {
            return -1;
        }
    
        //2.结构体填充数据
        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr)); //将结构体数组清空
        servaddr.sin_family = AF_INET; 
        servaddr.sin_port = htons(DNS_SERVER_PORT);
        //点分十进制地址转为网络所用的二进制数 替换inet_pton
        //servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
        inet_pton(AF_INET, DNS_SERVER_IP, &servaddr.sin_addr.s_addr);
    
        //UDP不一定要connect,只是这样提高成功发送请求的可能性
        connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
    
        //3.dns报文的数据填充
        struct dns_header header = {0};
        dns_create_header(&header);
    
        struct dns_question question = {0};
    
        dns_create_question(&question, domain);
    
        char request[1024] = {0};
        int len = dns_build_requestion(&header, &question, request, 1024);
    
        //4.通过sockfd发送DNS请求报文
        int slen = sendto(sockfd, request, len, 0, (struct sockaddr *)&servaddr, sizeof(struct sockaddr));
    
        char response[1024] = {0};
        struct sockaddr_in addr;
        size_t addr_len = sizeof(struct sockaddr_in);
    
        //5.接受DNS服务器的响应报文
        //addr和addr_len是输出参数
        int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
    
        struct dns_item *dns_domain = NULL;
        //6.解析响应
        dns_parse_response(response, &dns_domain);
    
        free(dns_domain);
        
        return n; //返回接受到的响应报文的长度
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    解析出响应报文的相关数据

    解析响应的函数

    //struct dns_item** 是因为当struct dns_item *为NULL时需要改变其值
    static int dns_parse_response(char *buffer, struct dns_item **domains)
    {
        int i = 0;
        unsigned char *ptr = buffer;
    
        ptr += 4; // ptr向前4字节,指向Questions(问题数)字段开头
        int querys = ntohs(*(unsigned short *)ptr);
    
        ptr += 2; //ptr向前2字节,指向Answer RR回答数开头
        int answers = ntohs(*(unsigned short *)ptr); //一个域名可能对应多个ip
    
        ptr += 6; //ptr向前6字节,指向Queries(查询问题区域)Name字段的开头
    
        for(i = 0;i < querys; i++)
        {
            //如查询的网址为www.0voice,则Name = 3www60voice3com0 
            while(1)
            {
                //flag就是随后字符串的长度
                int flag = (int)ptr[0];
                ptr += (flag + 1); 
    
                if(flag == 0)   break;
            }
            ptr += 4; //指向下一个查询名Name字段的开头
            //最后一次循环是跳过Type和Class字段
        }
    
        char cname[128], aname[128], ip[20], netip[4];
        int len, type, ttl, datalen;
    
        int cnt = 0;
        //动态分配内存的数组
        //分配answers个dns_item的内存,并全部置为0,返回指向一个位置的指针
        struct dns_item *list = calloc(answers, sizeof(struct dns_item));
        if(list == NULL)
        {
            return -1;
        }
    
        //解析出Answers(回答区域)的内容
        for(int i = 0;i < answers;++i)
        {
            bzero(aname, sizeof(aname));
            len = 0;
    		
            //从buffer中ptr指向的位置解析出域名到aname中,并将长度写入len中
            dns_parse_name(buffer, ptr, aname, &len);
            ptr += 2; //???
    
            type = htons(*(unsigned short *)ptr);
            ptr += 4;
    
            ttl = htons(*(unsigned short *)ptr);
            ptr += 4;
    
            datalen = ntohs(*(unsigned short *)ptr);
            ptr += 2;
    
            if(type == DNS_CNAME)
            {
                bzero(cname, sizeof(cname));
                len = 0;
                从buffer的ptr位置开始解析出内容到cname中,len用来接受解析出的内容长度
                dns_parse_name(buffer, ptr, cname, &len);
                ptr += datalen;
            }
            else if(type == DNS_HOST)
            {
                bzero(ip, sizeof(ip));
                
                //ipv4为4字节
                if(datalen == 4)
                {
                    memcpy(netip, ptr, datalen);
                    //二进制网络字节序netip转为点分十进制地址存到ip
                    inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
                    
                    printf("%s has address %s\n", aname, ip);
                    printf("\t Time to live : %d minutes, %d seconds\n",ttl / 60, ttl % 60);
    
                    list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
                    memcpy(list[cnt].domain, aname, strlen(aname));
    
                    list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
                    memcpy(list[cnt].ip, ip, strlen(ip));
    
                    cnt++;
                }
    
                ptr += datalen;
            }
        }
        *domains = list;
        ptr += 2; //????
    
        return cnt;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    解析出域名的函数

    域名( 2 字节或不定长): 它的格式和 Queries 区域的查询名字字段是一样的。有一点
    不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。比
    如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表
    示,具体格式是最前面的两个高位是 11(0xC0) ,用于识别指针。其余的 14 位从 DNS 报文的开
    始处计数(从 0 开始),指出该报文中的相应字节数。一个典型的例子,
    C00C(11 00 000000001100, 12 (字节)正好是头部的长度,其正好指向 Queries 区域的查询名字字段 。

    第一次出现某个Name(域名)

    在这里插入图片描述

    第二次出现相同的域名,只占两个字节(C0 0C = 1100 0000 0000 1100),高2位表示是指针,低14位表示这个域名第一次出现的偏移(12字节)

    在这里插入图片描述

    //从chunk的ptr指向的位置开始解析名字,长度写入len
    static void dns_parse_name(unsigned char* chunk, unsigned char *ptr, char *out, int *len)
    {
        int flag = 0, n = 0, alen = 0;
        //pos指向的内存用于存储解析得到的结果
        char *pos = out + (*len); // 传入的 *len = 0
    
        while(1)
        {
            flag = (int)ptr[0]; 
            if(flag == 0) break;
    
            //如果为指针表明该Name重复出现过,这一字段只占2字节
            if(is_pointer(flag))
            {
                n = (int)ptr[1]; //获取第一次Name出现的偏移
                ptr = chunk + n;
                dns_parse_name(chunk, ptr, out, len);
                break;
            }
            else //不是指针,表明是第一次出现Name的地方,此时flag表示随后字符串长度
            {
                ptr++;
                memcpy(pos, ptr, flag);
                pos += flag;
                ptr += flag;
    
                *len += flag;
                if((int)ptr[0] != 0)
                {
                    memcpy(pos, ".", 1);
                    pos += 1;
                    (*len) += 1;
                }
            }
        }
        
    }
    
    static int is_pointer(int in)
    {
        //0xC0 : 1100 0000
        return ((in & 0xC0) == 0xC0);    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    完整源代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <time.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #define DNS_SERVER_PORT     53
    #define DNS_SERVER_IP       "114.114.114.114"
    
    #define DNS_HOST            0x01
    #define DNS_CNAME           0x05
    
    
    //dns报文Header部分数据结构
    struct dns_header{
        unsigned short id; //2字节(16位)
        unsigned short flags; 
    
        unsigned short questions; //问题数
        unsigned short answer; //回答数
    
        unsigned short authority;
        unsigned short additional;
    };
    
    //dns报文Queries部分的数据结构
    struct dns_question{
        int length; //主机名的长度,自己添加的,便于操作
        unsigned short qtype;
        unsigned short qclass;
        //查询名是长度和域名组合
        //如www.0voice.com ==> 60voice3com0
        //之所以这么做是因为域名查询一般是树结构查询,com顶级域,0voice二级域
        unsigned char *name; // 主机名(长度不确定)
    };
    
    //dns响应报文中数据(域名和ip)的结构体
    struct dns_item{
        char *domain; 
        char *ip;
    };
    
    
    
    //将header部分字段填充数据
    int dns_create_header(struct dns_header *header)
    {
        if(header == NULL)
            return -1;
        memset(header, 0x00, sizeof(struct dns_header));
    
        //id用随机数,种子用time(NULL),表明生成随机数的范围
        srandom(time(NULL)); // 线程不安全
        header->id = random();
        
        //网络字节序(大端)地址低位存数据高位
        //主机(host)字节序转网络(net)字节序
        header->flags = htons(0x0100);
        header->questions = htons(1);
        return 0;
    }
    
    
    //将Queries部分的字段填充数据
    int dns_create_question(struct dns_question *question, const char *hostname)
    {
        if(question == NULL || hostname == NULL)
            return -1;
        memset(question, 0x00, sizeof(struct dns_question));
    
        //内存空间长度:hostname长度 + 结尾\0 再多给一个空间
        question->name = (char *)malloc(strlen(hostname) + 2);
        if(question->name == NULL)
        {
            return -2;
        }
    
        question->length = strlen(hostname) + 2;
    
        //查询类型1表示获得IPv4地址
        question->qtype = htons(1);
        //查询类1表示Internet数据
        question->qclass = htons(1);
    
        //【重要步骤】
        //名字存储:www.0voice.com -> 3www60voice3com 
        const char delim[2] = ".";
        char *qname = question->name; //用于填充内容用的指针
    
        //strdup先开辟大小与hostname同的内存,然后将hostname的字符拷贝到开辟的内存上
        char *hostname_dup = strdup(hostname); //复制字符串,调用malloc
        //将按照delim分割出字符串数组,返回第一个字符串
        char *token = strtok(hostname_dup, delim);
    
        while(token != NULL)
        {
            //strlen返回字符串长度不含'\0'
            size_t len = strlen(token);
    
            *qname = len;//长度的ASCII码
            qname++;
    
            //将token所指的字符串复制到qname所指的内存上,最多复制len + 1长度 
            //len+1使得最后一个字符串把\0复制进去
            strncpy(qname, token, len + 1);
            qname += len;
    
            //固定写法,此时内部会获得下一个分割出的字符串返回(strtok会依赖上一次运行的结果)
            token = strtok(NULL, delim); //依赖上一次的结果,线程不安全
        }
    
        free(hostname_dup);       
    }
    
    
    //将header和question合并到request中
    //request是传入传出参数
    int dns_build_requestion(struct dns_header *header, struct dns_question *question, char *request, int rlen)
    {
        if (header == NULL || question == NULL || request == NULL)
            return -1;
    
        memset(request, 0, rlen);
    
        //header -> request
        memcpy(request, header, sizeof(struct dns_header));
        int offset = sizeof(struct dns_header);
    
        //Queries部分字段写入到request中,question->length是question->name的长度
        memcpy(request + offset, question->name, question->length);
        offset += question->length;
    
        memcpy(request + offset, &question->qclass, sizeof(question->qclass));
        offset += sizeof(question->qclass);
    
        memcpy(request + offset, &question->qtype, sizeof(question->qtype));
        offset += sizeof(question->qtype);
    
        return offset; //返回request数据的实际长度
    }
    
    static int is_pointer(int in)
    {
        //0xC0 : 1100 0000
        return ((in & 0xC0) == 0xC0);    
    }
    
    //从chunk的ptr指向的位置开始解析名字,长度写入len
    static void dns_parse_name(unsigned char* chunk, unsigned char *ptr, char *out, int *len)
    {
        int flag = 0, n = 0, alen = 0;
        //pos指向的内存用于存储解析得到的结果
        char *pos = out + (*len); // 传入的 *len = 0
    
        //???
        while(1)
        {
            flag = (int)ptr[0]; // ???
            if(flag == 0) break;
    
            if(is_pointer(flag))
            {
                n = (int)ptr[1];
                ptr = chunk + n;
                dns_parse_name(chunk, ptr, out, len);
                break;
            }
            else //不是指针,表明是第一次出现Name的地方
            {
                ptr++;
                memcpy(pos, ptr, flag);
                pos += flag;
                ptr += flag;
    
                *len += flag;
                if((int)ptr[0] != 0)
                {
                    memcpy(pos, ".", 1);
                    pos += 1;
                    (*len) += 1;
                }
            }
        }
        
    }
    
    //解析响应
    //struct dns_item** 是因为当struct dns_item *为NULL时需要改变其值
    static int dns_parse_response(char *buffer, struct dns_item **domains)
    {
        int i = 0;
        unsigned char *ptr = buffer;
    
        ptr += 4; // ptr向前4字节,指向Questions(问题数)字段开头
        int querys = ntohs(*(unsigned short *)ptr);
    
        ptr += 2; //ptr向前2字节,指向Answer RR回答数开头
        int answers = ntohs(*(unsigned short *)ptr); //一个域名可能对应多个ip
    
        ptr += 6; //ptr向前6字节,指向Queries(查询问题区域)Name字段的开头
    
        for(i = 0;i < querys; i++)
        {
            //如查询的网址为www.0voice,则Name = 3www60voice3com0 
            while(1)
            {
                int flag = (int)ptr[0];
                ptr += (flag + 1); //???
    
                if(flag == 0)   break;
            }
            ptr += 4; //指向下一个查询名Name字段的开头或跳过Type和Class字段
        }
    
        char cname[128], aname[128], ip[20], netip[4];
        int len, type, ttl, datalen;
    
        int cnt = 0;
        //动态分配内存的数组
        //分配answers个dns_item的内存,并全部置为0,返回指向一个位置的指针
        struct dns_item *list = calloc(answers, sizeof(struct dns_item));
        if(list == NULL)
        {
            return -1;
        }
    
        for(int i = 0;i < answers;++i)
        {
            bzero(aname, sizeof(aname));
            len = 0;
    
            //解析出域名
            dns_parse_name(buffer, ptr, aname, &len);
            ptr += 2;
    
            type = htons(*(unsigned short *)ptr);
            ptr += 4;
    
            ttl = htons(*(unsigned short *)ptr);
            ptr += 4;
    
            datalen = ntohs(*(unsigned short *)ptr);
            ptr += 2;
    
            if(type == DNS_CNAME)
            {
                bzero(cname, sizeof(cname));
                len = 0;
                //猜:从buffer的ptr位置开始解析出内容到cname中,len用来接受解析出的内容长度
                dns_parse_name(buffer, ptr, cname, &len);
                ptr += datalen;
            }
            else if(type == DNS_HOST)
            {
                bzero(ip, sizeof(ip));
                
                //ipv4为4字节
                if(datalen == 4)
                {
                    memcpy(netip, ptr, datalen);
                    //二进制网络字节序netip转为点分十进制地址存到ip
                    inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
                    
                    printf("%s has address %s\n", aname, ip);
                    printf("\t Time to live : %d minutes, %d seconds\n",ttl / 60, ttl % 60);
    
                    list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
                    memcpy(list[cnt].domain, aname, strlen(aname));
    
                    list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
                    memcpy(list[cnt].ip, ip, strlen(ip));
    
                    cnt++;
                }
    
                ptr += datalen;
            }
        }
        *domains = list;
        ptr += 2; // 经测试这行不加也行
    
        return cnt;
    }
    
    //客户端向dns服务器发送请求
    int dns_client_commit(const char *domain)
    {
        //下方流程是基本定死的套路
        //1.创建UDP socket
        //网络层ipv4, 传输层用udp
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd < 0)
        {
            return -1;
        }
    
        //2.结构体填充数据
        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr)); //将结构体数组清空
        servaddr.sin_family = AF_INET; 
        servaddr.sin_port = htons(DNS_SERVER_PORT);
        //点分十进制地址转为网络所用的二进制数 替换inet_pton
        //servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
        inet_pton(AF_INET, DNS_SERVER_IP, &servaddr.sin_addr.s_addr);
    
        //UDP不一定要connect,只是这样提高成功发送请求的可能性
        connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
    
        //3.dns报文的数据填充
        struct dns_header header = {0};
        dns_create_header(&header);
    
        struct dns_question question = {0};
    
        dns_create_question(&question, domain);
    
        char request[1024] = {0};
        int len = dns_build_requestion(&header, &question, request, 1024);
    
        //4.通过sockfd发送DNS请求报文
        int slen = sendto(sockfd, request, len, 0, (struct sockaddr *)&servaddr, sizeof(struct sockaddr));
    
        char response[1024] = {0};
        struct sockaddr_in addr;
        size_t addr_len = sizeof(struct sockaddr_in);
    
        //5.接受DNS服务器的响应报文
        //addr和addr_len是输出参数
        int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
    
        struct dns_item *dns_domain = NULL;
        //6.解析响应
        dns_parse_response(response, &dns_domain);
    
        free(dns_domain);
        
        return n; //返回接受到的响应报文的长度
    }
    
    int main(int argc, char *argv[])
    {
        if(argc < 2) return -1;
        dns_client_commit(argv[1]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349

    编译和执行

    在这里插入图片描述

    问题

    1. dns_parse_response函数中下面部分

      for(int i = 0;i < answers;++i)
          {
              bzero(aname, sizeof(aname));
              len = 0;
      
              //解析出域名
              dns_parse_name(buffer, ptr, aname, &len);
              ptr += 2;// ???????为什么ptr指针只要移动2个字节,而不是len,aname难道不是可变的吗,只有跳过aname实际长度才会到Type字段。
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
  • 相关阅读:
    【无标题】
    现场维护:问题处理的策略;要先找到真正原因再实施变通方案
    【工具推荐】KeePass安装与插件推荐
    uniapp 如何发送formData数据请求(全网最佳解决方案)
    【LeetCode】字节面试-行、列递增的二维数组数字查找
    pytest:如何调用 pytest
    使用 SSL 实现 2 路身份验证的最简单方法 - 提供源代码的示例
    在java开发工具IntelliJ IDEA中如何与远程 Git 存储库同步?
    【Docker网络】Docker的容器互联
    微信小程序底层框架实现原理
  • 原文地址:https://blog.csdn.net/qq_42120843/article/details/125416951