应用层协议实现系列——DNS服务器之设计与实现
Posted zhaoxy2850
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了应用层协议实现系列——DNS服务器之设计与实现相关的知识,希望对你有一定的参考价值。
在实现了HTTP、FTP服务器后,本人开始尝试实现DNS服务器,DNS协议的内容相比HTTP和FTP协议要多一些,但经过一番折腾之后,还是把自己的DNS服务器完成了,同时在自己的DNS服务器上实现了DNS劫持,即用户使用该DNS服务器后,访问例如www.taobao.com会加载另一个网站的内容。在这里把实现的过程分享给大家。
在实现DNS服务器之前,先介绍一下DNS协议。DNS协议主要用于实现域名和ip地址之间的转换,举个例子,当我们在浏览器输入www.baidu.com时,浏览器会向DNS服务器查询www.baidu.com对应的ip地址,如220.181.111.86,收到查询结果后,再通过HTTP协议访问ip对应的主机获取网页内容。大家可以试试,在地址栏输入www.baidu.com和220.181.111.86的效果是一样的。当然www.baidu.com对应的ip地址会有很多个,这么设计主要是为了实现负载均衡,但那是后话了。
DNS协议作为一个应用层的协议是通过UDP和TCP实现的,当DNS报文的长度小于512字节时,会使用UDP,否则使用TCP。熟悉TCP和UDP协议的朋友可以猜出来,这么做的目的主要是为了效率,DNS报文的长度不同于HTTP和TCP,一般都比较小,也就是说三次握手协议的通讯量相对于报文长度不能忽略不计。因此DNS协议在大多数情况下都采用UDP进行通讯,本文中也只实现了UDP的通信。
然后说明一下DNS服务器的种类和DNS的解析流程。互联网上的所有域名及其对应的ip地址都是存放在无数台大大小小的DNS服务器中的,DNS服务器主要分为以下几类:根DNS服务器、顶级域名DNS服务器、权威DNS服务器和本地DNS服务器。举个例子,当浏览器需要查询一个域名(www.baidu.com)时,会向本地DNS服务器发起一个请求,本地DNS服务器会在数据库中查找,如果没有找到则向根DNS服务器(.)发起一个请求,根域名服务器将根据所查询的域名,指定一个顶级域名服务器(.com)并将其ip地址返回给本地DNS服务器,本地DNS服务器再向顶级域名服务器(.com)发起相同的请求,顶级域名服务器指定该域名对应的权威DNS服务器(baidu.com)并将其ip返回给本地DNS服务器。本地DNS服务器再向权威DNS服务器(baidu.com)发起同样的请求,权威DNS服务器找到www.baidu.com对应的ip地址并将其返回给本地DNS服务器。本地DNS服务器得到ip地址后,再返回给浏览器打开。这就完成了一次域名查找。域名查找的过程又可以分为递归查找和迭代查找,有兴趣的朋友可以自己谷歌。
下面来具体说下DNS报文的格式。DNS报文主要由5个部分组成,包括Header、Question、Answer、Authority、Additional,一般请求报文只包括Header、Question两部分,而应答报文在没有错误的情况下,至少包括Header、Question、Answer三个部分。Header部分主要用于说明DNS报文的id、是请求还是应答、查询类型、是否截断、递归设置、状态码以及问题答案的数量等;Question部分主要用于列出需要查询的域名即类型;Answer部分主要用于列出对Question部分相应的回答,包括有效时间、数据长度、答案的数据等;Authority部分主要列出其他权威的DNS服务器,如果客户端需要的话可以直接查询;Additional部分主要是一些附加信息,如给出Authority部分权威服务器对应的ip地址等。至于每个部分的具体格式在这里不作说明,有兴趣的可以看看这篇博文。
在了解了以上内容之后,我们就可以开始实现DNS服务器了。本文的DNS服务器设计参考了这个链接,在这里对该作者表示感谢。
首先是Socket通信中的创建UDP套接字并监听,主要通过Server类来完成:
void Server::init(int &port)
struct sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr));
//protocol domain
servAddr.sin_family = AF_INET;
//default ip
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
//port
servAddr.sin_port = htons(port);
//create socket
if ((m_socketfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
printf("create socket error: %s(errno: %d)\\n",strerror(errno),errno);
return;
unsigned value = 1;
setsockopt(m_socketfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
//bind socket to port
if (bind(m_socketfd, (struct sockaddr *)&servAddr, sizeof(servAddr)))
printf("bind socket error: %s(errno: %d)\\n",strerror(errno),errno);
return;
//dynamically allocating a port
if (port == 0)
socklen_t namelen = sizeof(servAddr);
if (getsockname(m_socketfd, (struct sockaddr *)&servAddr, &namelen) == -1)
printf("getsockname error: %s(errno: %d)\\n",strerror(errno),errno);
return;
port = ntohs(servAddr.sin_port);
std::cout<<"server running on port:"<<port<<std::endl;
void Server::run()
char buff[512];
struct sockaddr_in clientAddr;
socklen_t len = sizeof(struct sockaddr_in);
while (1)
int n = (int)recvfrom(m_socketfd, buff, sizeof(buff), 0, (struct sockaddr *)&clientAddr, &len);
if (n <= 0) continue;
m_query.decode(buff, n);
std::cout<<m_query.to_string();
m_resolver.process(m_query, m_response);
memset(buff, 0, sizeof(buff));
n = m_response.encode(buff);
std::cout<<m_response.to_string();
sendto(m_socketfd, buff, n, 0, (struct sockaddr *)&clientAddr, len);
std::cout<<std::endl;
以上代码并没有使用多进程,仅仅是收到一个请求后进行解析,处理并回应。代码的核心主要是在Message类,它实现了DNS报文的解析和生成:
//encode an address seperated by '.' like 'www.google.com'
//to address seperated by substring length like '3www6google3com'
void encode_address(char *addr, char *&buff)
string address(addr);
int pos, current = 0;
while ((pos = (int)address.find('.')) != string::npos)
address.erase(0, pos+1);
*buff++ = pos;
memcpy(buff, addr+current, pos);
buff += pos;
current += pos+1;
*buff++ = address.size();
memcpy(buff, addr+current, address.size());
buff += address.size();
*buff++ = 0;
//encode the message to the buffer
void Message::encode_header(char *&buff)
MHeader header = 0;
header.hId = m_id;
header.hFlags += ((m_qr?1:0)<<15);
header.hFlags += (m_opcode<<11);
header.hFlags += (m_aa<<10);
header.hFlags += (m_tc<<9);
header.hFlags += (m_rd<<8);
header.hFlags += (m_ra<<7);
header.hFlags += m_rcode;
header.queryCount = m_qdCount;
header.answCount = m_anCount;
header.authCount = m_nsCount;
header.addiCount = m_arCount;
header.hId = htons(header.hId);
header.hFlags = htons(header.hFlags);
header.queryCount = htons(header.queryCount);
header.answCount = htons(header.answCount);
header.authCount = htons(header.authCount);
header.addiCount = htons(header.addiCount);
memcpy(buff, &header, sizeof(MHeader));
//offset
buff += sizeof(MHeader);
//encode the questions of message to the buffer
void Message::encode_questions(char *&buff)
//encode each question
for (int i=0; i<m_qdCount; i++)
MQuestion question = m_questions[i];
encode_address(question.qName, buff);
uint16_t nQType = htons(question.qType);
memcpy(buff, &nQType, sizeof(uint16_t));
buff+=sizeof(uint16_t);
uint16_t nQClass = htons(question.qClass);
memcpy(buff, &nQClass, sizeof(uint16_t));
buff+=sizeof(uint16_t);
//encode the answers of the message to the buffer
void Message::encode_answers(char *&buff)
//encode each answer
for (int i=0; i<m_anCount; i++)
MResource resource = m_answers[i];
encode_address(resource.rName, buff);
uint16_t nRType = htons(resource.rType);
memcpy(buff, &nRType, sizeof(uint16_t));
buff+=sizeof(uint16_t);
uint16_t nRClass = htons(resource.rClass);
memcpy(buff, &nRClass, sizeof(uint16_t));
buff+=sizeof(uint16_t);
uint32_t nTTL = htonl(resource.rTTL);
memcpy(buff, &nTTL, sizeof(uint32_t));
buff+=sizeof(uint32_t);
uint16_t nRDLen = htons(resource.rdLength);
memcpy(buff, &nRDLen, sizeof(uint16_t));
buff+=sizeof(uint16_t);
if (MT_A == resource.rType)
memcpy(buff, resource.rData, sizeof(uint32_t));
buff+=sizeof(uint32_t);
//decode the message header from the buffer
void Message::decode_header(const char *&buff)
MHeader header;
memcpy(&header, buff, sizeof(MHeader));
//network order to host order
header.hId = ntohs(header.hId);
header.hFlags = ntohs(header.hFlags);
header.queryCount = ntohs(header.queryCount);
header.answCount = ntohs(header.answCount);
header.authCount = ntohs(header.authCount);
header.addiCount = ntohs(header.addiCount);
//id
m_id = header.hId;
//flags
m_qr = header.hFlags&QR_MASK;
m_opcode = header.hFlags&OPCODE_MASK;
m_aa = header.hFlags&AA_MASK;
m_tc = header.hFlags&TC_MASK;
m_rd = header.hFlags&RD_MASK;
m_ra = header.hFlags&RA_MASK;
m_rcode = header.hFlags&RCODE_MASK;
//count
m_qdCount = header.queryCount;
m_anCount = header.answCount;
m_nsCount = header.authCount;
m_arCount = header.addiCount;
//offset
buff+= sizeof(MHeader);
//decode the questions of the message from the buffer
void Message::decode_questions(const char *&buff)
//reset
m_questions.clear();
//decode each question
for (int i=0; i<m_qdCount; i++)
MQuestion question = 0;
//name
while (1)
uint len = *buff++;
if (len==0) break;
if (strlen(question.qName)!=0) strcat(question.qName, ".");
memcpy(question.qName+strlen(question.qName), buff, len);
buff+=len;
//type
question.qType = ntohs(*((uint16_t *)buff));
buff+=sizeof(uint16_t);
//class
question.qClass = ntohs(*((uint16_t *)buff));
buff+=sizeof(uint16_t);
//add to list
m_questions.push_back(question);
代码中的DNS请求类Query和应答类Response都是继承于Message类,由于它们的结构类似。Resolver类中主要是针对查询的域名,在本地数据库(在这里是host文件)中查找,并根据结果生成相应的应答报文。
void Resolver::init(const std::string &fileName)
ifstream fstream(fileName);
char hostStr[128];
while (fstream.getline(hostStr, sizeof(hostStr)))
stringstream sstream;
sstream<<hostStr;
Host host;
sstream>>host.ipAddr;
sstream>>host.name;
m_hosts.push_back(host);
void Resolver::process(const Query &query, Response &response)
//clear
response.m_questions.clear();
response.m_answers.clear();
//find host and generate answers
vector<Response::MQuestion> questions = query.getQuestions();
for (vector<Response::MQuestion>::iterator qIter = questions.begin(); qIter != questions.end(); ++qIter)
Response::MQuestion question = *qIter;
Response::MResource resource;
for (vector<Host>::iterator hIter = m_hosts.begin(); hIter != m_hosts.end(); hIter++)
Host host = *hIter;
//if find
if (question.qName == host.name && question.qType == Message::MT_A)
strcpy(resource.rName, question.qName);
resource.rType = question.qType;
resource.rClass = question.qClass;
resource.rTTL = 10;
resource.rdLength = sizeof(uint32_t);
memcpy(resource.rIp, host.ipAddr.c_str(), host.ipAddr.size());
struct sockaddr_in adr_inet;
memset(&adr_inet, 0, sizeof(adr_inet));
inet_aton(host.ipAddr.c_str(), &adr_inet.sin_addr);
memcpy(resource.rData, &adr_inet.sin_addr.s_addr, sizeof(uint32_t));
response.m_answers.push_back(resource);
break;
response.m_questions.push_back(question);
response.m_id = query.getID();
response.m_qr = 1;
response.m_opcode = query.getOpcode();
response.m_aa = 0;
response.m_tc = 0;
response.m_rd = 0;
response.m_ra = 0;
if (response.m_answers.size()!=response.m_questions.size())
response.m_rcode = Message::MC_SERVER_ERROR;
else
response.m_rcode = Message::MC_NO_ERROR;
response.m_qdCount = (int)response.m_questions.size();
response.m_anCount = (int)response.m_answers.size();
response.m_nsCount = 0;
response.m_arCount = 0;
以上代码已经在GitHub上开源,有兴趣的朋友可以前往 下载。将这个代码运行起来后,需要Server类构造函数中的host文件路径,这个文件可以自己创建,格式与系统/etc/hosts中的相同,每行一个ip地址和域名。例如:
202.108.22.5 www.baidu.com
74.125.128.199 www.google.com.hk
另外,由于该代码中使用了1024以下的端口,运行时需要root权限,否则会提示权限不足。将该代码运行起来后,即可将其他设备的dns地址设为运行代码的机器ip,即可实现对指定的域名进行解析。如果要实现DNS劫持,将想劫持的域名指向自己设定的一个ip地址即可。注意,如果将www.baidu.com指向谷歌的ip地址会出现连接失败,由于HTTP协议会将域名发送到HTTP服务器,谷歌的HTTP服务器会拒绝host不是www.google.com.hk的HTTP请求。
以上是关于应用层协议实现系列——DNS服务器之设计与实现的主要内容,如果未能解决你的问题,请参考以下文章
netty系列之:在netty中使用TCP协议请求DNS服务器
应用层协议实现系列——HTTP服务器之仿nginx多进程和多路IO的实现
netty系列之:在netty中使用UDP协议请求DNS服务器