在實現了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請求。