應用層協議實現系列(四)——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個部分組成,包括HeaderQuestionAnswerAuthorityAdditional,一般請求報文只包括HeaderQuestion兩部分,而應答報文在沒有錯誤的情況下,至少包括HeaderQuestionAnswer三個部分。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請求。


如果大家覺得對自己有幫助的話,還希望能幫頂一下,謝謝:)
轉載請註明出處,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章