• Linux C/C++ 学习笔记(七):DNS协议与请求


    本文部分内容参考Linux C/C++ 开发(学习笔记九 ):DNS协议与请求的实现_菊头蝙蝠的博客-CSDN博客_struct dns

    一、DNS的介绍

       域名系统(英文:Domain Name System,缩写:DNS的作用是将人类可读的域名 (如,www.example.com) 转换为机器可读的 IP 地址(如,192.0.2.44)

    DNS的分层 

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

    DNS服务类型

    授权型DNS——一种授权型DNS服务提供一种更新机制,供开发人员用于管理其公用DNS名称。然后,它响应DNS查询,将域名转换为IP地址,以便计算机可以相互通信。授权型DNS对域有最终授权且负责提供递归型DNS服务器对IP地址信息的响应。阿里云是一种授权型DNS系统。

    递归型DNS——客户端通常不会对授权型DNS服务直接进行查询。而是通常连接到称为解析程序的其他DNS服务,或递归型DNS服务。递归型DNS服务就像是旅馆的门童:尽管没有任何自身的DNS记录,但是可充当代表您获得DNS信息的中间程序。如果递归型DNS拥有已缓存或存储一段时间的DNS参考,那么它会通过提供源或IP信息来响应DNS查询。如果没有,则它会将查询传递到一个或多个授权型DNS服务器以查找信息。

    记录类型

      DNS server内的每一个网域都有自己的档案,这个档案一般会称为区域档案,如”named.ca”或”named.local” 档案…等等。 区域档案是由多个记录组成的,每一个记录称为资源记录(Resource Record,简称RR)。 当在设定DNS名称解析、反向解析及其他的管理目的时,需要使用不同类型的RR,底下将介绍常用的RR类型及表示法。

    NS 记录(域名服务) 指定解析域名或子域名的 DNS 服务器。
    MX 记录(邮件交换) 指定接收信息的邮件服务器。

    A 记录(地址) 指定域名对应的 IPv4 地址记录。

    AAAA 记录(地址) 指定域名对应的 IPv6 地址记录。

    CNAME (规范) 一个域名映射到另一个域名或 CNAME 记录( baidu.com 指向
    www.baidu.com )或映射到一个 A 记录。

     PTR 记录(反向记录) ─ PTR 记录用于定义与 IP 地址相关联的名称。 PTR 记录 是 A AAAA 记录的逆。 PTR 记录是唯一的,因为它们以 .arpa 根开始并被委派 给 IP 地址的所有者。(反向DNS查找反向DNS解析(rDNS)是查询域名系统(DNS)来确定IP地址关联的域名的技术——通常的“转发”的反向DNS查找域名的IP地址。反向DNS查询的过程使用PTR记录。互联网的反向DNS数据库植根于 .arpa 顶级域名。

    域名解析

     主机名到IP地址映射有两种方式:

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

     

    二、DNS协议 

      级别最低的域名写在左边,级别最高的域名写在右边。域名服务主要是基于UDP 实现的,服务器的端口号为 53。

    域名服务器

      域名需要由遍及全世界的域名服务器去解析,域名服务器实际上就是装有域名系统的主机。由高向低进行层次划分,可分为以下几大类:

    根域名服务器:最高层次的域名服务器,也是最重要的域名服务器,本地域名服务器如果解析不了域名就会向根域名服务器求助。全球共有 13 个不同 IP 地址的根域名服务器,它们的名称用一个英文字母命名,从 a 一直到 m。 这些服务器由各种组织控制,并由 ICANN(互联网名称和数字地址分配公司)授权,由于每分钟都要解析的名称数量多得令人难以置信,所以实际上每个根服务器都有镜像服务器, 每个根服务器与它的镜像 服务器共享同一个 IP 地址,中国大陆地区内只有 6 组根服务器镜像(F,I(3 台),J,L)。当你对某个根服务器发出请求时,请求会被路由到该根服务器离你最近的镜像服务器。所有的根域名服务器都知道所有的顶级域名服务器的域名和地址,如果向根服务器发出对 0voice.com 的请求,则根服务器是不能在它的记录文件中找到与0voice.com 匹配的记录。但是它会找到.com 的顶级域名记录,并把负责.com 地址的顶级域名服务器的地址发回给请求。
    顶级(TLD)域名服务器:负责管理在该顶级域名服务器下注册的二级域名。当根域名服务器告 诉查询者顶级域名服务器地址时,查询者紧接着就会到顶级域名服务器进行查询。比如还是查询 0voice.com,根域名服务器已经告诉了查询者 com 顶级域名服务器的地址, com 顶级域名服务器会找到 0voice.com 的域名服务器的记录,域名服务器检查其区域文件,并发现它有与 0voice.com 相关联的区域文件。在此文件的内部,有该主机的记录。此记录说明此主机所在的 IP 地址,并向请求者返回最终答案。

    权限(权威)域名服务器:当递归解析器收到来自 TLD 域名服务器的响应时,该响应会将解析器定向到权威性域名服务器。权威性域名服务器通常是解析器查找 IP 地址过程中的最后一步。权威名称服务器包含特定于其服务域名的信息(例如,google.com),并且它可为递归解析器提供在DNS A记录中找到的服务器的 IP 地址,或者如果该域具有 CNAME记录(别名),它将为递归解析器提供一个别名域,这时递归解析器将必须执行全新 DNS 查找,以便从权威性域名服务器获取记录(通常为包含 IP 地址的 A 记录)。

    本地域名服务器:当一个主机发出 DNS 查询请求的时候,这个查询请求首先就是发给本地域名服务器的。

    域名解析过程

    域名解析总体可分为两大步骤,第一个步骤是本机向本地域名服务器发出一个 DNS 请求报文,报文里携带需要查询的域名;第二个步骤是本地域名服务器向本机回应一个 DNS 响应 报文,里面包含域名对应的 IP 地址。从下面对 0voice.com 进行域名解析的报文中可明显 看出这两大步骤。注意:第二大步骤中采用的是迭代查询,其实是包含了很多小步骤的, 详情见下面的流程分析

    其具体的流程可描述如下:
    1. 主机 192.168.1.124 先向本地域名服务器 192.168.1.2 进行 递归查询
    2. 本地域名服务器采用 迭代查询 ,向一个根域名服务器进行查询
    3. 根域名服务器告诉本地域名服务器,下一次应该查询的顶级域名服务器 0voice.com
    IP 地址
    4. 本地域名服务器向顶级域名服务器 0voice.com 进行查询
    5. 顶级域名服务器 .com 告诉本地域名服务器,下一步查询权限服务器 www.0voice.com
    IP 地址
    6. 本地域名服务器向权限服务器 www.0voice.com 进行查询
    7. 权限服务器 www.0voice.com 告诉本地域名服务器所查询的主机的 IP 地址
    8. 本地域名服务器最后把查询结果告诉 122.152.222.180

    递归查询和迭代查询

     

     图2-18所示的例子利用了递归查询和迭代查询。从cse.nyu.edu到dns.nyu.edu发出的查询是递归查询,因为该查询以自己的名义请求dns.nyu.edu来获得该映射。而后继的3个查询是迭代查询,因为所有的回答都是直接返回给dns.nyu.edu。从理论上讲,任何DNS查询既可以是迭代的也能是递归的。例如,图2-19显示了一条DNS查询链,其中的所有查询都是递归的。实践中,查询通常遵循图2-18中的模式:从请求主机到本地DNS服务器的查询是递归的,其余的查询是迭代的。

    协议报文格式

    头部

    会话标识(2字节):是DNS报文的ID标识,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分DNS应答报文是哪个请求的响应。 

    标志(2字节):

    QR1bit 查询 / 响应标志, 0 为查询, 1 为响应
    opcode4bit 0 表示标准查询, 1 表示反向查询, 2 表示服务器状态请求
    AA1bit 表示授权回答
    TC1bit 表示可截断的
    RD1bit 表示期望递归
    RA1bit 表示可用递归
    rcode4bit 表示返回码, 0 表示没有差错, 3 表示名字差错, 2 表示服务器错误( Server
    Failure
    数量字段(总共 8 字节): Questions、Answer RRs、Authority RRs、Additional RRs 各自
    表示后面的四个区域的数目。Questions 表示查询问题区域节的数量,Answers 表示回答区
    域的数量,Authoritative namesversers 表示授权区域的数量,Additional recoreds 表示
    附加区域的数量

    正文

     

    查询名:长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为IP,反向查询即由IP地址反查域名),一般的格式如下图所示

     

    查询类:通常为 1,表明是 Internet 数据

    源记录

    源记录(RR)包括回答区域,授权区域和附加区域

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

    查询类型:表明资源记录的类型,如前文中查询类型表格所示

    查询类:对于Internet信息,总是IN

    生存时间(TTL):以秒为单位,表示的是资源记录的声明周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)

    资源数据: 该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数
    据。可以是 Address(表明查询报文想要的回应是一个 IP 地址)或者 CNAME(表明查询
    报文想要的回应是一个规范主机名)等。

    Wireshark抓取DNS报文

      从上到下分别是物理层、数据链路层、网络层、传输层、应用层

     可展开观察DNS协议的具体细节

    三、实现DNS请求

    1.首部header和question结构体

    1. struct dns_header{
    2. unsigned short id;
    3. unsigned short flags;
    4. unsigned short questions;
    5. unsigned short answers;
    6. unsigned short authority;
    7. unsigned short additional;
    8. };
    9. struct dns_question{
    10. int length;
    11. unsigned short qtype;
    12. unsigned short qclass;
    13. char* name;//域名
    14. };

    2.初始化dns首部

    1. //client send to dns server
    2. int dns_create_header(dns_header* header){
    3. if(header==NULL) return -1;
    4. memset(header,0,sizeof(header));
    5. //random
    6. srandom(time(NULL));//设置种子(因为种子根据时间有关,每次random也会变,因此这是一个线程不安全的)
    7. header->id=random();//获得随机数
    8. header->flags=htons(0x0100);//转化为网络字节序
    9. header->questions=htons(1);//每次查询一个域名
    10. return 0;
    11. }

    3. 初始化question

    strdup函数声明
    1. #include
    2. char *strdup(const char *s);

    函数介绍:

      strdup()函数是c语言中常用的一种字符串拷贝库函数,一般和free()函数成对出现。

    strdup()在内部调用了malloc()为变量分配内存,不需要使用返回的字符串时,需要用free()释放相应的内存空间,否则会造成内存泄漏。该函数的返回值是返回一个指针,指向为复制字符串分配的空间;如果分配空间失败,则返回NULL值。

    strtok() 函数声明

    char *strtok(char *str, const char *delim)
    • str -- 要被分解成一组小字符串的字符串。
    • delim -- 包含分隔符的 C 字符串。

    返回值:该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。 

    char *strncpy(char *dest, const char *src, size_t n)

    把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。

    • dest -- 指向用于存储复制内容的目标数组。
    • src -- 要复制的字符串。
    • n -- 要从源中复制的字符数。

    该函数返回最终复制的字符串。

    1. //创建question
    2. //hostname:www.baidu.com
    3. //name:3www5baidu3com'\0'
    4. int dns_create_question(dns_question* question,const char* hostname){
    5. if(question==NULL||hostname==NULL) return -1;
    6. memset(question,0,sizeof(question));
    7. question->name=(char*)malloc(strlen(hostname)+2);//因为要判断结尾'\0',然后再补充一个开头3
    8. if(question->name==NULL){//如果内存分配失败
    9. return -2;
    10. }
    11. question->length=strlen(hostname)+2;
    12. question->qtype=htons(1);//查询类型,(1表示:由域名获得 IPv4 地址)
    13. question->qclass=htons(1);//通常为 1,表明是 Internet 数据
    14. //hostname->name
    15. const char delim[2]=".";//分隔符,末尾补个'\0'
    16. char* qname=question->name;
    17. char* hostname_dup=strdup(hostname);//复制一份hostname --->malloc(所以后续要free)
    18. char* token=strtok(hostname_dup,delim);
    19. while(token!=NULL){
    20. size_t len=strlen(token);//第一个循环token为www,len=3
    21. *qname=len;//先把长度放上去
    22. qname++;
    23. strncpy(qname,token,len+1);//复制www,这里不+1也是可以的,这样是为了把最后的'\0'也复制过来,因为最后也会被覆盖的。(如果这边不+1,最后一步,需要额外加上'\0')
    24. qname+=len;
    25. token=strtok(NULL,delim);//因为上一次,token获取还未结束,因此可以指定NULL即可。(注意:要依赖上一次的结果,因此也是线程不安全的)
    26. }
    27. free(hostname_dup);
    28. }

    4.合并header和question

    1. //struct dns_header* header
    2. //struct dns_question* question
    3. //把上面两个合到request中 返回长度
    4. int dns_build_requestion(dns_header* header,dns_question* question,char* request,int rlen){
    5. if(header==NULL||question==NULL||request==NULL) return -1;
    6. memset(request,0,rlen);
    7. //header-->request
    8. memcpy(request,header,sizeof(dns_header));//把header的数据 拷贝 到request中
    9. int offset=sizeof(dns_header);
    10. //question-->request
    11. memcpy(request+offset,question->name,question->length);
    12. offset+=question->length;
    13. memcpy(request+offset,&question->qtype,sizeof(question->qtype));
    14. offset+=sizeof(question->qtype);
    15. memcpy(request+offset,&question->qclass,sizeof(question->qclass));
    16. offset+=sizeof(question->qclass);
    17. return offset;
    18. }

    5.通过socket实现dns请求

    1. int dns_client_commit(const char* domin){
    2. int sockfd=socket(AF_INET,SOCK_DGRAM,0);//创建sockfd,AF_INET表示ipv4, SOCK_DGRAM为报文方式(UDP);
    3. if(sockfd<0){//创建失败
    4. return -1;
    5. }
    6. struct sockaddr_in servaddr={0};//服务器地址(sockaddr_in存储)
    7. servaddr.sin_family=AF_INET;//协议簇
    8. servaddr.sin_port=htons(DNS_SERVER_PORT);//端口
    9. servaddr.sin_addr.s_addr=inet_addr(DNS_SERVER_IP);//添加dns服务器ip
    10. int ret=connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));//目的是为sendto开辟一条路径,
    11. printf("connect:%d",ret);
    12. dns_header header={0};
    13. dns_create_header(&header);
    14. dns_question question={0};
    15. dns_create_question(&question,domin);
    16. char request[1024]={0};//假设定义为1024长度
    17. int length = dns_build_requestion(&header,&question,request,1024);
    18. //request 发送请求
    19. int slen=sendto(sockfd,request,length,0,(struct sockaddr*)&servaddr,sizeof(struct sockaddr));
    20. //receive from 接受数据
    21. char response[1024]={0};
    22. struct sockaddr_in addr;
    23. size_t addr_len=sizeof(struct sockaddr_in);
    24. int n = recvfrom(sockfd,response,sizeof(response),0,(struct sockaddr*)&addr,(socklen_t*)&addr_len);
    25. printf("recvfrom:%d \n",n);
    26. for(int i=0;i
    27. printf("%c",response[i]);
    28. }
    29. for(int i=0;i
    30. printf("%x",response[i]);
    31. }
    32. printf("\n");
    33. return n;
    34. }

    6.解析结果

        C 库函数 void *calloc(size_t nitems, size_t size) 分配所需的内存空间,并返回一个指向它的指针。malloc 和 calloc 之间的不同点是,malloc 不会设置内存为零,而 calloc 会设置分配的内存为零。

    void *calloc(size_t nitems, size_t size)
    • nitems -- 要被分配的元素个数。
    • size -- 元素的大小。
     void bzero(void *s, int n);

    bzero()将参数s 所指的内存区域前n 个字节全部设为零

    1. typedef struct dns_item{
    2. char *domain;
    3. char *ip;
    4. }dns_item;
    5. int is_pointer(int in) { //in与0xc0相与,结果等于0xc0时为真 192
    6. return ((in & 0xC0) == 0xC0);
    7. }
    8. //参数1:存放DNS服务器返回结果的缓冲区 参数2:指向返回结果某一区域的指针 参数3存放Answer中name字段的缓冲区
    9. //将Answer中name字段放入缓冲区
    10. void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {
    11. int flag = 0, n = 0, alen = 0;
    12. char *pos = out + (*len); //pos指向
    13. while (1) {
    14. flag = (int)ptr[0];
    15. if (flag == 0) break;
    16. if (is_pointer(flag)) {
    17. n = (int)ptr[1];
    18. ptr = chunk + n;
    19. dns_parse_name(chunk, ptr, out, len);
    20. break;
    21. } else {
    22. ptr ++;
    23. memcpy(pos, ptr, flag);
    24. pos += flag;
    25. ptr += flag;
    26. *len += flag;
    27. if ((int)ptr[0] != 0) {
    28. memcpy(pos, ".", 1);
    29. pos += 1;
    30. (*len) += 1;
    31. }
    32. }
    33. }
    34. }
    35. // 参数1:存放dns服务器返回结果的缓冲区 参数2:指向dns_item结构体的指针
    36. int dns_parse_response(char *buffer, struct dns_item **domains){
    37. int i = 0;
    38. unsigned char *ptr = (unsigned char* )buffer; //ptr是指向buffer的指针
    39. ptr += 4; //向后移动4字节
    40. //将一个无符号短整型数从网络字节顺序转换为主机字节顺序
    41. int querys = ntohs(*(unsigned short*)ptr); //获取questions字段 查询问题区域节的数量
    42. ptr += 2;
    43. int answers = ntohs(*(unsigned short*)ptr);//获取Answer RRs字段
    44. ptr += 6; //指向Queries字段
    45. for (i = 0;i < querys;i ++) { //此处循环目的是跳过查询字段
    46. while (1) {
    47. int flag = (int)ptr[0]; //第一部分的长度
    48. ptr += (flag + 1); //ptr指向下一部分
    49. if (flag == 0) break; //查询名的结尾为0
    50. }
    51. ptr += 4; //指向回答区域
    52. }
    53. char cname[128], aname[128], ip[20], netip[4];
    54. int len, type, ttl, datalen;
    55. int cnt = 0;
    56. struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item)); //为dns_item结构体分配内存空间
    57. if (list == NULL) {
    58. return -1;
    59. }
    60. for (i = 0;i < answers;i ++) {
    61. bzero(aname, sizeof(aname)); //将aname空间元素置为0
    62. len = 0;
    63. //ptr指向Answers字段 将buffer中answer的Name字段放入aname中
    64. dns_parse_name((unsigned char* )buffer, ptr, aname, &len);
    65. ptr += 2;
    66. type = htons(*(unsigned short*)ptr);
    67. ptr += 4;
    68. ttl = htons(*(unsigned short*)ptr);
    69. ptr += 4;
    70. datalen = ntohs(*(unsigned short*)ptr);
    71. ptr += 2;
    72. if (type == DNS_CNAME) {
    73. bzero(cname, sizeof(cname));
    74. len = 0;
    75. dns_parse_name((unsigned char* )buffer, ptr, cname, &len);
    76. ptr += datalen;
    77. } else if (type == DNS_HOST) {
    78. bzero(ip, sizeof(ip));
    79. if (datalen == 4) {
    80. memcpy(netip, ptr, datalen);
    81. inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));
    82. printf("%s has address %s\n" , aname, ip);
    83. printf("\tTime to live: %d minutes , %d seconds\n", ttl / 60, ttl % 60);
    84. list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
    85. memcpy(list[cnt].domain, aname, strlen(aname));
    86. list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
    87. memcpy(list[cnt].ip, ip, strlen(ip));
    88. cnt ++;
    89. }
    90. ptr += datalen;
    91. }
    92. }
    93. *domains = list;
    94. ptr += 2;
    95. return cnt;
    96. }

    7.main函数

    1. int main(int argc,char* argv[]){
    2. if(argc<2) return -1;
    3. dns_client_commit(argv[1]);
    4. return 0;
    5. }

  • 相关阅读:
    五笔字根表
    LeetCode中等题之面试题 01.08. 零矩阵
    C语言 数组作为函数参数
    构建决策树
    【47C++STL-常用算法----5、常用算术生成算法
    x265 帧间预测
    【Oracle训练营】属于你的9天Oracle实战训练营!
    Mac nginx安装,通过源码安装教程
    第二十八章 车道线检测中的论文梳理(车道线感知)
    [附源码]Python计算机毕业设计Django网上鲜花购物系统
  • 原文地址:https://blog.csdn.net/weixin_45767431/article/details/127989135