九、Linux C/C++ 实现DNS客户端请求域名IP
文章目录
- 一、DNS介绍
- 二、DNS协议
- 1、头部
- 2、问题部分
- 3、资源记录部分
- 三、代码实现
一、DNS介绍
全称:Domain Name System,域名解析系统。是互联网的一项服务。它实质上是一个 域名 和 IP 相互映射的分布式数据库,有了它,我们就可以通过域名更方便的访问互联网。
**功能:**每个IP地址都可以有一个主机名,主机名由一个或多个字符串组成,字符串之间用小数点隔开。有了主机名,就不要死记硬背每台IP设备的IP地址,只要记住相对直观有意义的主机名就行了。这就是DNS协议所要完成的功能。
主机名到IP地址的映射有两种方式:
- 静态映射,每台设备上都配置主机到IP地址的映射,各设备独立维护自己的映射表,而且只供本设备使用;
- 动态映射,建立一套域名解析系统(DNS),只在专门的DNS服务器上配置.主机到IP地址的映射,网络上需要使用主机名通信的设备,首先需要到DNS服务器查询主机所对应的IP地址。
通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。在解析域名时,可以首先采用静态域名解析的方法,如果静态域名解析不成功,再采用动态域名解析的方法。可以将一些常用的域名放入静态域名解析表中,这样可以大大提高域名解析效率。
- DNS 有以下特点:
- 分布式的
- 协议支持 TCP 和 UDP,常用端口是 53
- 每一级域名的长度限制是 63
- 域名总长度限制是 253
二、DNS协议
1、头部
-
TransactionID(事务ID):DNS 报文的 ID 标识。对于请求报文和其对应的应答报文,该字段的值是相同的,通过它可以区分 DNS 应答报文是对哪个请求进行响应的。
-
Flags(标志):DNS 报文中的标志字段。
- 第15位:QR(Response),查询请求/响应的标志信息。0为请求(query) 1为响应(response)。
- 第14-11位:Opcode, 操作码。0 表示标准查询;1 表示反向查询;2 表示服务器状态请求。
- 第10位:AA(Authoritative),授权应答,该字段在响应报文中有效。值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。
- 第9位:TC(Truncated):表示是否被截断。值为 1 时,表示响应已超过 512 字节并已被截断(一个UDP报文为512字节),只返回前 512 个字节。
- 第8位:RD(Recursion Desired):是否请求递归(这个比特位被请求设置,应答的时候使用的相同的值返回)。该字段能在一个查询中设置,并在响应中返回。如果该位为 1,告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。
- 第7位:RA(Recursion Available):可用递归。该字段只出现在响应报文中。当值为 1 时,表示DNS服务器支持递归查询。
- 第6-4位:Z:保留字段,在所有的请求和应答报文中,它的值必须为 0。
- 第3-0位:rcode(Reply code):返回码字段,表示响应的差错状态。
- 当值为 0 时,表示 没有错误;
- 当值为 1 时,表示 报文格式错误(Format error),服务器不能理解请求的报文;
- 当值为 2 时,表示 域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;
- 当值为 3 时,表示 名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;
- 当值为 4 时,表示 查询类型不支持(Not Implemented),即域名服务器不支持查询类型;
- 当值为 5 时,表示 拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答。
- 当值为 6-15 时:保留值
-
Questions(问题计数):DNS查询请求的数目
-
Answer RRs(回答资源记录数):DNS响应的数目
-
Authority RRs(权威名称服务器计数):权威名称服务器的数目
-
Additional RRs(附加资源记录数):额外的记录数目(权威名称服务器对应 IP 地址的数目)
2、问题部分
问题部分指的是报文格式中查询问题区域(Queries)部分。该部分是用来显示 DNS 查询请求的问题,通常只有一个问题。该部分包含正在进行的查询信息,包含查询名(被查询主机名字)、查询类型、查询类。
问题部分格式如图所示。
该部分中每个字段含义如下:
- Name(查询的域名):不定长(例子:www.baidu.com 需写作:3www5baidu3com0,这里的3,5,3分别指的是后面域名的长度,最后的0是‘\0’,是字符串结尾标志),有时也会是 IP 地址,用于反向查询。
- Type(查询类型):DNS 查询请求的资源类型。通常查询类型为 A 类型,表示由域名获取对应的 IP 地址。
- Class(查询类):指定信息的协议组,通常为互联网地址(值为 1)。
//查询类型(查询的资源记录类型)
enum QueryType
{ A = 0x01, //指定计算机 IP 地址。 NS = 0x02, //指定用于命名区域的 DNS 名称服务器。 MD = 0x03, //指定邮件接收站(此类型已经过时了,使用MX代替) MF = 0x04, //指定邮件中转站(此类型已经过时了,使用MX代替) CNAME = 0x05, //指定用于别名的规范名称。 SOA = 0x06, //指定用于 DNS 区域的“起始授权机构”。 MB = 0x07, //指定邮箱域名。 MG = 0x08, //指定邮件组成员。 MR = 0x09, //指定邮件重命名域名。 NULL = 0x0A, //指定空的(NULL)资源记录 WKS = 0x0B, //描述已知服务。 PTR = 0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。 HINFO = 0x0D, //指定计算机 CPU 以及操作系统类型。 MINFO = 0x0E, //指定邮箱或邮件列表信息。 MX = 0x0F, //指定邮件交换器。 TXT = 0x10, //指定文本信息。 UINFO = 0x64, //指定用户信息。 UID = 0x65, //指定用户标识符。 GID = 0x66, //指定组名的组标识符。 ANY = 0xFF //指定所有数据类型。
}; //查询类(指定信息的协议组)
enum QueryClass
{ IN = 0x01, //指定 Internet 类别。 CSNET = 0x02, //指定 CSNET 类别。(已过时) CHAOS = 0x03, //指定 Chaos 类别。 HESIOD = 0x04, //指定 MIT Athena Hesiod 类别。 ANY = 0xFF //指定任何以前列出的通配符。
};
3、资源记录部分
资源记录部分是指 DNS 报文格式中的最后三个字段,包括 回答问题区域字段、权威名称服务器区域字段、附加信息区域字段。这三个字段均采用一种称为资源记录的格式,格式如图所示。
-
回答区域(Answers): 响应的内容,可以有0-n条(请求时为空即可)
1、Name:域名(2字节或不定长)
- 它的格式和Queries区域的查询名字字段是一样的。
- 有一点不同就是,当报文中域名重复出现的时候,该字段使用2个字节 的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。
- 一个典型的例子,0xC00C(1100000000001100),12(00000000001100)正好是头部的长度,其正好指向Queries区域的查询名字字段)。
2、Type:2字节 响应的资源记录的类型
与问题部分中的查询类型值是一样的。
3、Class:2字节 地址类型
与问题部分中的查询类的值是一样的。
4、TTL:4字节 表示的是资源记录的生命周期(以秒为单位)
一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)。
5、Datalength:2字节,资源数据的长度
指接下来的data长度,单位为字节。
6、Address/CNAME:资源数
4字节地址/不定长域名
-
授权区域(Authoritativenameservers):
1、Name:域名(2字节或不定长),同上
2、Type:2字节 同上,响应的资源记录类型。此处为2(NS),表示要获取该域名的权威名称服务器名称
3、Class:2字节 同上,地址类型,即查询类(指定信息的协议组)
4、TTL:4字节 同上
5、Datalength:2字节,资源数据的长度,指接下来的data长度,单位为字节。
6、nameserver:此处为6字节,表示该域名对应的权威名称服务器的名称。 -
附加区域(Additionalrecords):
1、Name:域名(2字节或不定长),同上,在这里指的是 权威名称服务器 的名称
2、Type:2字节 同上,响应的资源记录类型。
3、Class:2字节,同上,地址类型,即查询类(指定信息的协议组)
4、TTL:4字节 同上
5、Datalength:2字节,指接下来的data长度,单位为字节。
6、Address:此处为4字节地址,指的是 权威名称服务器 的IP地址
三、代码实现
实现了同步方式与异步方式。
#include <iostream>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/epoll.h>using namespace std;//域名数组
const char* domainAddr[] = {"www.0voice.com","draw.0voice.wang","www.baidu.com","tieba.baidu.com","news.baidu.com","zhidao.baidu.com","music.baidu.com","image.baidu.com","v.baidu.com","map.baidu.com","baijiahao.baidu.com","xueshu.baidu.com","cloud.baidu.com","www.163.com","open.163.com","auto.163.com","gov.163.com","money.163.com","sports.163.com","tech.163.com","edu.163.com","www.taobao.com","q.taobao.com","sf.taobao.com","yun.taobao.com","baoxian.taobao.com","www.tmall.com","suning.tmall.com","www.tencent.com","www.qq.com","www.aliyun.com","www.ctrip.com","hotels.ctrip.com","hotels.ctrip.com","vacations.ctrip.com","flights.ctrip.com","trains.ctrip.com","bus.ctrip.com","car.ctrip.com","piao.ctrip.com","tuan.ctrip.com","you.ctrip.com","g.ctrip.com","lipin.ctrip.com","ct.ctrip.com"
};//DNS头部
struct DNS_HEADER {uint16_t transID; //事务ID:DNS 报文的 ID 标识。//对于请求报文和其对应的应答报文,该字段的值是相同的,//通过它可以区分 DNS 应答报文是对哪个请求进行响应的。uint16_t flags; //标志:DNS 报文中的标志字段/*第15位:QR(Response),查询请求/响应的标志信息。0为请求(query) 1为响应(response)。第14-11位:Opcode, 操作码。0 表示标准查询;1 表示反向查询;2 表示服务器状态请求。第10位:AA(Authoritative),授权应答,该字段在响应报文中有效。值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。第9位:TC(Truncated):表示是否被截断。值为 1 时,表示响应已超过 512 字节并已被截断(一个UDP报文为512字节),只返回前 512 个字节。第8位:RD(Recursion Desired):期望递归。该字段能在一个查询中设置,并在响应中返回。如果该位为 1,告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。是否请求递归(这个比特位被请求设置,应答的时候使用的相同的值返回)。第7位:RA(Recursion Available):可用递归。该字段只出现在响应报文中。当值为 1 时,表示DNS服务器支持递归查询。第6-4位:Z:保留字段,在所有的请求和应答报文中,它的值必须为 0。第3-0位:rcode(Reply code):返回码字段,表示响应的差错状态。当值为 0 时,表示 没有错误;当值为 1 时,表示 报文格式错误(Format error),服务器不能理解请求的报文;当值为 2 时,表示 域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;当值为 3 时,表示 名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;当值为 4 时,表示 查询类型不支持(Not Implemented),即域名服务器不支持查询类型;当值为 5 时,表示 拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答。当值为 6-15 时:保留值*/uint16_t questions; //问题计数:DNS查询请求的数目uint16_t answerRRs; //回答资源记录数:DNS响应的数目uint16_t authorityRRs; //权威名称服务器计数:权威名称服务器的数目uint16_t additionalRRs; //附加资源记录数:额外的记录数目(权威名称服务器对应 IP 地址的数目)
};//DNS查询问题区域
struct DNS_QUERIES {string qName; //查询的域名,不定长(例子:www.baidu.com 需写作:3www5baidu3com0)uint16_t qType; //查询类型:查询的资源记录类型uint16_t qClass; //查询类:指定信息的协议组
};//查询类型(查询的资源记录类型)
enum QueryType
{ A = 0x01, //指定计算机 IP 地址。 NS = 0x02, //指定用于命名区域的 DNS 名称服务器。 MD = 0x03, //指定邮件接收站(此类型已经过时了,使用MX代替) MF = 0x04, //指定邮件中转站(此类型已经过时了,使用MX代替) CNAME = 0x05, //指定用于别名的规范名称。 SOA = 0x06, //指定用于 DNS 区域的“起始授权机构”。 MB = 0x07, //指定邮箱域名。 MG = 0x08, //指定邮件组成员。 MR = 0x09, //指定邮件重命名域名。 _NULL_ = 0x0A, //指定空的(NULL)资源记录 WKS = 0x0B, //描述已知服务。 PTR = 0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。 HINFO = 0x0D, //指定计算机 CPU 以及操作系统类型。 MINFO = 0x0E, //指定邮箱或邮件列表信息。 MX = 0x0F, //指定邮件交换器。 TXT = 0x10, //指定文本信息。 UINFO = 0x64, //指定用户信息。 UID = 0x65, //指定用户标识符。 GID = 0x66, //指定组名的组标识符。 _ANY_ = 0xFF //指定所有数据类型。
}; //查询类(指定信息的协议组)
enum QueryClass
{ IN = 0x01, //指定 Internet 类别。 CSNET = 0x02, //指定 CSNET 类别。(已过时) CHAOS = 0x03, //指定 Chaos 类别。 HESIOD = 0x04, //指定 MIT Athena Hesiod 类别。 ANY = 0xFF //指定任何以前列出的通配符。
}; /*
回答区域:
Answers:查询响应内容,可以有0-n条(请求时为空即可)Name:域名(2字节或不定长):它的格式和Queries区域的查询名字字段是一样的。有一点不同就是,当报文中域名重复出现的时候,该字段使用2个字节的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。一个典型的例子,0xC00C(1100000000001100),12(00000000001100)正好是头部的长度,其正好指向Queries区域的查询名字字段)。Type:2字节 响应的资源记录的类型,与问题部分中的查询类型值是一样的。Class:2字节 地址类型,与问题部分中的查询类的值是一样的。TTL:4字节 以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)。Datalength:2字节,资源数据的长度,指接下来的data长度,单位为字节。Address/CNAME:资源数据,4字节地址/不定长域名授权区域
Authoritativenameservers:Name:域名(2字节或不定长),同上Type:2字节 同上,响应的资源记录类型。此处为2(NS),表示要获取该域名的权威名称服务器名称Class:2字节 同上,地址类型,即查询类(指定信息的协议组)TTL:4字节 同上Datalength:2字节,资源数据的长度,指接下来的data长度,单位为字节。nameserver:此处为6字节,表示该域名对应的权威名称服务器的名称。//附加区域
Additionalrecords:Name:域名(2字节或不定长),同上,在这里指的是 权威名称服务器 的名称Type:2字节 同上,响应的资源记录类型。Class:2字节,同上,地址类型,即查询类(指定信息的协议组)TTL:4字节 同上Datalength:2字节,指接下来的data长度,单位为字节。Address:此处为4字节地址,指的是 权威名称服务器 的IP地址
*///构建DNS头部
DNS_HEADER create_dns_header() {DNS_HEADER header;bzero(&header, sizeof(header));srandom(time(NULL));header.transID = random();header.flags = htons(0x0100);header.questions = htons(0x0001);return move(header);
}//构建DNS查询问题区域
DNS_QUERIES create_dns_queries(const string& qName, QueryType qType, QueryClass qClass) {DNS_QUERIES queries;char* tmpName = strdup(qName.c_str());char* token = strtok(tmpName, ".");while (token != NULL) {size_t len = strlen(token);queries.qName.append((char*)(&len), 1);queries.qName.append(token, len);token = strtok(NULL, ".");}//queries.qName.append("\0");free(tmpName);queries.qType = htons(qType);queries.qClass = htons(qClass);return std::move(queries);
}//构建一个DNS请求包
string create_dns_request(const DNS_HEADER& header, const DNS_QUERIES& queries) {string request = "";request.append((char*)&header, sizeof(header));request.append(queries.qName.data(), queries.qName.length() + 1);request.append((char*)&queries.qType, sizeof(queries.qType));request.append((char*)&queries.qClass, sizeof(queries.qClass));return std::move(request);
}//解析域名(3www5baidu3com0 ---> www.baidu.com)
string parse_name(const string& name) {if (name.empty()) {return "";}string domain = "";int pos = 0;while (pos < name.length() - 1) {int len = name.at(pos);pos += 1;domain.append(name.data() + pos, len);pos += len;if (pos >= name.length()) {break;}domain.append(".");}return std::move(domain);
}//解析收到的DNS响应
void parse_dns_response(const char* data, uint16_t len) {if (len == 0) {return;}const char* ptr = data;ptr += 4;//问题数int questions = ntohs(*((uint16_t*)ptr));ptr += 2;//回答资源记录数:DNS响应的数目int answerRRs = ntohs(*((uint16_t*)ptr));printf("\nanswerRRs:%d\n", answerRRs);ptr += 2;//跳过查询问题区域ptr += 4;for (int i = 0; i < questions; i++) {while(1) {int flag = *ptr;ptr += (1 + flag);if (flag == 0) {break;}}ptr += 4;}//解析回答区域for (int i = 0; i < answerRRs; i++) {//解析域名string domain = "", ip = "";uint8_t flag = *ptr;//该字段使用是的2个字节的偏移指针来表示if ((flag & 0xC0) == 0xC0) {int offsetBytes = ntohs(*(uint16_t*)ptr) & (~0xC000);const char* pName = data + offsetBytes;domain = parse_name(pName);ptr += 2;}else {domain = parse_name(ptr);ptr += (strlen(ptr) + 1);}//响应的资源记录的类型uint16_t type = ntohs(*(uint16_t*)ptr);ptr += 4;//资源记录的生命周期uint32_t ttl = ntohs(*(uint32_t*)ptr);ptr += 4;//资源数据的长度uint16_t dataLength = ntohs(*(uint16_t*)ptr);ptr += 2;//资源数据//不定长域名if (type == QueryType::CNAME) {ip = parse_name(ptr);}//4字节地址else if (type == QueryType::A) {if (dataLength == 4) {in_addr addr = {.s_addr = *(uint32_t*)ptr};ip = inet_ntoa(addr);ptr += 4;}}cout << domain << " : " << ip << endl;}
}//设置socket为非阻塞
void setSocketNonBlock(int socketfd, bool nonBlock) {if (socketfd < 0) {return;}int flags = fcntl(socketfd, F_GETFL, 0);if (flags < 0) {return;}if (nonBlock) {flags |= O_NONBLOCK;}else {flags &= ~O_NONBLOCK;}fcntl(socketfd, F_SETFL, flags);
}//向DNS服务端提交DNS请求(同步方式)
void dns_client_commit_sync() {//创建socketint fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0) {perror("socket");exit(-1);}setSocketNonBlock(fd, true);sockaddr_in destAddr;destAddr.sin_family = AF_INET;destAddr.sin_port = htons(53);destAddr.sin_addr.s_addr = inet_addr("114.114.114.114"); //DNS服务器地址//连接connect(fd, (sockaddr*)&destAddr, sizeof(destAddr));for (int i = 0; i < sizeof(domainAddr) / sizeof(domainAddr[0]); i++) {//构建DNS头部DNS_HEADER header = create_dns_header();//构建DNS查询问题区域DNS_QUERIES queries = create_dns_queries(domainAddr[i], QueryType::A, QueryClass::IN);//构建一个DNS请求包string req = create_dns_request(header, queries);sendto(fd, req.data(), req.length(), 0, (sockaddr*)&destAddr, sizeof(destAddr));char buffer[2048] = {0};sockaddr_in addr;socklen_t addrLen = sizeof(addr);while (1) {int recvLen = recvfrom(fd, buffer, sizeof(buffer), 0, (sockaddr*)&addr, &addrLen);if (recvLen <= 0) {continue;}//解析收到的DNS响应parse_dns_response(buffer, recvLen);break;}}close(fd);
}//处理DNS响应的线程(异步方式)
void *thread_process_async_response(void * arg) {int epfd = *(int*)arg;epoll_event events[1024] = {0};while (1) {int nready = epoll_wait(epfd, events, 1024, -1);if (nready < 0) {if (errno == EINTR || errno == EAGAIN) {continue;}else {break;}}else if (nready == 0) {continue;}for (int i = 0; i < nready; i++) {int fd = events[i].data.fd;char recvBuf[1024] = {0};if (events[i].events & EPOLLIN) {sockaddr_in srcAddr;socklen_t addrLen;int recvSize = recvfrom(fd, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&srcAddr, &addrLen);if (recvSize <= 0) {continue;}parse_dns_response(recvBuf, recvSize);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);close(fd);}}}return NULL;
}//向DNS服务端提交DNS请求(异步方式)
void dns_client_commit_async() {int epfd = epoll_create(1);//创建一个线程来处理接收到的数据pthread_t threadID;pthread_create(&threadID, NULL, thread_process_async_response, &epfd);for (int i = 0; i < sizeof(domainAddr) / sizeof(domainAddr[0]); i++) {int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0) {perror("socket");exit(-1);}setSocketNonBlock(fd, true);//将此fd加入epollepoll_event event;event.events = EPOLL_EVENTS::EPOLLIN;event.data.fd = fd;epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);sockaddr_in destAddr;bzero(&destAddr, sizeof(destAddr));destAddr.sin_family = AF_INET;destAddr.sin_addr.s_addr = inet_addr("114.114.114.114");destAddr.sin_port = htons(53);connect(fd, (sockaddr*)&destAddr, sizeof(destAddr));//构建DNS头部DNS_HEADER header = create_dns_header();//构建DNS查询问题区域DNS_QUERIES queries = create_dns_queries(domainAddr[i], QueryType::A, QueryClass::IN);//构建一个DNS请求包string req = create_dns_request(header, queries);sendto(fd, req.data(), req.length(), 0, (sockaddr*)&destAddr, sizeof(destAddr));}
}int main(int argc, char* argv[]) {
#if 1dns_client_commit_sync(); //同步实现
#elsedns_client_commit_async(); //异步实现getchar();
#endifreturn 0;
}