관리 메뉴

사과하는 제라스

12강. DNS 본문

대학 전공 공부/네트워크 프로그래밍

12강. DNS

Xerath(제라스) 2022. 5. 13. 01:42

목차

    728x90
    반응형

    - DNS란?

    컴퓨터나 시스템에 이름을 할당하는데에 쓰임. IP 주소를 외우지 않아도 hostname으로 IP주소를 받아올 수 있는 서비스.

    즉, hostname과 IP주소를 link시켜주는 역할을 함.

     

    - 이전에 배운 getaddrinfo() 함수의 구동 과정

    1) OS에서 과거 이 도메인 주소에 대한 IP주소를 local cache에 저장되어 있는지 확인함.

    2) 없을 경우, OS가 DNS서버에 IP주소를 물어보고 응답받는 IP주소로 접근할 수 있음.

     

    - 도메인 이름은 계층적(Hierarchical)으로 관리가 됨.

    각 노드들을 domain 혹은 subdomain이라고 함.

     

    - DNS zone이란?

    관련있는 domain과 subdomain들을 묶어둔 것. 성격에 따라서 domain name들을 관리할 수 있음.

    - 각 DNS zone에는 zone에 대한 정보를 publish하는 authoritative(권위적인) nameserver가 존재함.

    -> DNS 쿼리에 대한 original하고 명확한 답을 해줌.

    authoritative nameserver는 master server(모든 zone에 대한 정보를 관리하는 중앙 서버)가 될 수도 있고 slave server(유사 시 master record를 복구할 수 있는 정보를 갖고 있는 서버)가 될 수도 있음.

     

    - DNS 프로토콜이란?

    주로 UDP를 통해 동작하고 port 번호는 53을 씀.(쿼리가 길어지면 TCP를 쓰기도 함.)

    client는 최소 하나 이상의 DNS 서버의 IP주소를 알아야 함.

    DNS서버는 ISP를 통해서 디폴트로 세팅이 되기도 함. 혹은 사설 DNS서버를 사용하기도 함.

     

    - Local DNS 파일 검색 방법

    : cat /etc/hosts

     

    - DNS 흐름 순서

    DNS 서버에 IP주소 물어보는 명령어: dig

    ex) dig @a.root-servers.net www.example.net

    -> 이 결과 IP주소를 알려주거나 모르는 경우엔 물어볼 DNS 서버들을 알려줌.

     

    - DNS 응답 내 section 종류 4가지

    1) Question section : 내가 물어봤던 question에 대한 정보를 복붙해서 알려줌

    2) Answer section : DNS 서버가 쿼리에 대한 답을 알 경우 IP주소를 알려줌.

    3) Authority section : 답을 모를 경우 다른 물어볼 서버를 포인팅해 줌.

    4) Additional section : 쿼리와 관련된 추가적인 섹션.

     

    - DNS 메세지 포맷

    1) Header

    12byte이고 DNS query나 DNS response 모두 길이는 동일함.

    ID : 16bit, DNS 메세지를 식별하기 위해 쓰임.

    QR : 1bit, DNS query(0)인지 response(1)인지 식별하기 위해 쓰임.

    Opcode : 4bit, 쿼리의 타입을 명세함. 

    AA(Authority Answer) : Authority answer가 세팅됨.

    TC : 메세지가 truncate되었음을 알려줌. 즉, 쿼리가 너무 길기에 TCP를 통해 다시 전송해야 함.

    RD : recursion과 관련된 부분. 쿼리 날렸을 때 IP주소가 세팅이 안 되어 있다면 계속 추가적으로 DNS쿼리를 보냄.

    RA : DNS 서버가 recursion을 지원하는지 여부를 알려줌.

    Z : unused 비트

    RCODE : DNS의 에러 컨디션을 의미함. 

    QDCOUNT : 날리는 쿼리의 개수. 보통 1개임. 

    ANCOUNT : answer section을 의미. 즉, ip주소이고 복수개일 수 있음.

    ...

    NSCOUNT

    ARCOUNT

     

    2) Question

    NAME : host name이 들어감. ex)www.example.com

    -> encoding 룰이 있음. 닷으로 구분해서 섹션을 나누고 각 섹션마다 Length, value 순으로 작성하고 마지막엔 0을 작성해 줌.

    ex) 

    QTYPE : host name이 어떤 타입으로 question을 물어보는지. IPv4주소?, IPv6주소? ex) A(IPv4에 대한 address record)

    QCLASS : 보통 1로 세팅하고 인터넷을 의미함. ex) 1

     

    3) Answer

    NAME, TYPE, CLASS : Question format과 동일함.

    TTL : 32bit임, 답안이 얼마나 나의 cache에 머무를 수 있느냐를 의미-> 그 시간동안 내가 DNS 정보를 임시 저장할 수 있음.

    RDLENGTH : 데이터 길이.

    RDATA : 데이터. 데이터 타입에 따라 다른 형태로 데이터가 보내짐.

    4) Authority

    5) Additional

     

    - DNS 쿼리 프로그램

    DNS query(Header, Question) ->

    DNS Response(Header, Question, Answer, Authority, Additional) ->

    DNS Response print

     

    - DNS name에 대한 Encoding Rule : LV

    3 www 6 google 3 com        0

    L    V    L     V       L   V    terminate

     

    중복될 경우 같은 것을 가리키는 것에 대한 pointer를 사용. 이때 Length값이 0xc0(11)일 때 뒤에 있는 Value는 pointing 값

     

    ex)

    ABCDEF 123456 ABCDEF -> 6 ABCDEF 6 123456 11(0xc0) pointer

     

    /*
     * MIT License
     *
     * Copyright (c) 2018 Lewis Van Winkle
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy
     * of this software and associated documentation files (the "Software"), to deal
     * in the Software without restriction, including without limitation the rights
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     * copies of the Software, and to permit persons to whom the Software is
     * furnished to do so, subject to the following conditions:
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */
    
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <netdb.h>
    #include <unistd.h>
    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define ISVALIDSOCKET(s) ((s) >= 0)
    #define CLOSESOCKET(s) close(s)
    #define SOCKET int
    #define GETSOCKETERRNO() (errno)
    
    
    const unsigned char *print_name(const unsigned char *msg, //msg = DNS 메세지, p = 출력할 부분의 포인터, end = 메세지의 끝
            const unsigned char *p, const unsigned char *end) { //msg-> Header 시작 부분, p-> Question 혹은 Answer의 Name 부분, end-> DNS 메세지 끝나는 부분 
    
        if (p + 2 > end) { //메세지의 끝인지 확인, Length, Value 각각 최소 1바이트 ∴2를 더함
            fprintf(stderr, "End of message.\n"); exit(1);}
    
        if ((*p & 0xC0) == 0xC0) { //프린트하고자 하는 Name이 포인팅하고 있는 포인터가 있는지 확인.
            const int k = ((*p & 0x3F) << 8) + p[1]; //  6bit + 8bit
            p += 2;
            printf(" (pointer %d) ", k);
            print_name(msg, msg+k, end);
            return p;
    
        } else { //기본적인 LV tagging으로 출력을 진행하면 됨.
            const int len = *p++; // length 읽고 ++로 p는 value를 가리킴
            if (p + len + 1 > end) {
                fprintf(stderr, "End of message.\n"); exit(1);}
    
            printf("%.*s", len, p); //len길이 만큼 p를 출력함. 
            p += len; // 다음 value를 출력한 후 p는 다음을 가리킴
            if (*p) { // 가리킨게 0, 즉, terminate가 아니면 .을 찍고 다음 LV tagging을 진행함.
                printf(".");
                return print_name(msg, p, end);
            } else { // 0이면 +1을 해서 p의 다음 위치를 리턴함.
                return p+1;
            }
        }
    }
    
    void print_dns_message(const char *message, int msg_length) { //message = 패킷에 저장한 배열의 주소, msg_length = message 크기
    
        if (msg_length < 12) { // msg_length는 무조건 12byte 이상이어야 함.∵Header가 12byte니까
            fprintf(stderr, "Message is too short to be valid.\n");
            exit(1);
        }
    
        const unsigned char *msg = (const unsigned char *)message;
    
        printf("ID = %0X %0X\n", msg[0], msg[1]); //ID는 총 2byte임. ∴8bit, 8bit로 출력해 줌.
    
        const int qr = (msg[2] & 0x80) >> 7; //1000 0000과 msg[2]를 비교 후 첫bit위치로 끌고 온 후 qr에 넣음.
        printf("QR = %d %s\n", qr, qr ? "response" : "query"); // qr이 1->response, 0->query
    
        const int opcode = (msg[2] & 0x78) >> 3; //0111 1000과 msg[2]를 비교 후 첫 bit로 끌고 온 후 opcode에 넣음.(opcode는 4bit)
        printf("OPCODE = %d ", opcode);
        switch(opcode) { // opcode 종류대로 출력함.
            case 0: printf("standard\n"); break;
            case 1: printf("reverse\n"); break;
            case 2: printf("status\n"); break;
            default: printf("?\n"); break;
        }
    
        const int aa = (msg[2] & 0x04) >> 2; // 0000 0100과 msg[2] 비교 후 첫 bit로 끌고와서 aa에 넣어줌.
        printf("AA = %d %s\n", aa, aa ? "authoritative" : "");
    
        const int tc = (msg[2] & 0x02) >> 1; // 0000 0010과 msg[2] 비교 후 첫 bit로 끌고와서 tc에 넣어줌.
        printf("TC = %d %s\n", tc, tc ? "message truncated" : "");
    
        const int rd = (msg[2] & 0x01); // 0000 0001과 msg[2] 비교 후 첫 bit로 끌고와서 rd에 넣어줌.
        printf("RD = %d %s\n", rd, rd ? "recursion desired" : "");
    
        if (qr) { // response라면...
            const int rcode = msg[3] & 0x0F; // 0000 1111과 msg[3] 비교 후 rcode에 값 넣어줌.
            printf("RCODE = %d ", rcode);
            switch(rcode) { // rcode 종류대로 출력함.
                case 0: printf("success\n"); break;
                case 1: printf("format error\n"); break;
                case 2: printf("server failure\n"); break;
                case 3: printf("name error\n"); break;
                case 4: printf("not implemented\n"); break;
                case 5: printf("refused\n"); break;
                default: printf("?\n"); break;
            }
            if (rcode != 0) return; // 이건 뭘까???
        }
    
        const int qdcount = (msg[4] << 8) + msg[5]; // 2byte로 묶어서 넣어줌
        const int ancount = (msg[6] << 8) + msg[7]; // 2byte로 묶어서 넣어줌
        const int nscount = (msg[8] << 8) + msg[9]; // 2byte로 묶어서 넣어줌
        const int arcount = (msg[10] << 8) + msg[11]; // 2byte로 묶어서 넣어줌
    
        printf("QDCOUNT = %d\n", qdcount);
        printf("ANCOUNT = %d\n", ancount);
        printf("NSCOUNT = %d\n", nscount);
        printf("ARCOUNT = %d\n", arcount);
    
    
        const unsigned char *p = msg + 12; // Header로부터 12byte 내려옴 -> Question
        const unsigned char *end = msg + msg_length;
    
        if (qdcount) {
            int i;
            for (i = 0; i < qdcount; ++i) {
                if (p >= end) {
                    fprintf(stderr, "End of message.\n"); exit(1);}
    
                printf("Query %2d\n", i + 1); // (i+1)번째 쿼리 name:
                printf("  name: "); 
    
                p = print_name(msg, p, end); printf("\n");
    
                if (p + 4 > end) {
                    fprintf(stderr, "End of message.\n"); exit(1);}
    
                const int type = (p[0] << 8) + p[1];
                printf("  type: %d\n", type);
                p += 2; // type 출력했으니 2byte 넘기고
    
                const int qclass = (p[0] << 8) + p[1];
                printf(" class: %d\n", qclass);
                p += 2; // class 출력했으니 2byte 넘기고
            }
        }
    
        if (ancount || nscount || arcount) {
            int i;
            for (i = 0; i < ancount + nscount + arcount; ++i) {
                if (p >= end) {
                    fprintf(stderr, "End of message.\n"); exit(1);}
    
                printf("Answer %2d\n", i + 1); // (i+1)번째 Answer name:
                printf("  name: ");
    
                p = print_name(msg, p, end); printf("\n"); //이제 pointing 하고 있는 것이 존재할 것임.
    
                if (p + 10 > end) { // 이건 뭘까?
                    fprintf(stderr, "End of message.\n"); exit(1);}
    
                const int type = (p[0] << 8) + p[1]; // 2byte 묶어서 넣어줌.
                printf("  type: %d\n", type);
                p += 2; // type 출력했으니 2byte 넘기고
    
                const int qclass = (p[0] << 8) + p[1]; // 2byte 묶어서 넣어줌.
                printf(" class: %d\n", qclass);
                p += 2; // class 출력했으니 2byte 넘기고
    
                const unsigned int ttl = (p[0] << 24) + (p[1] << 16) +
                    (p[2] << 8) + p[3]; // 4byte 묶어서 넣어줌.(TTL은 32bit임.)
                printf("   ttl: %u\n", ttl);
                p += 4; // ttl 출력했으니 4byte 넘기고
    
                const int rdlen = (p[0] << 8) + p[1]; // 2byte 묶어서 넣어줌.
                printf(" rdlen: %d\n", rdlen);
                p += 2; // rdlength 출력했으니 2byte 넘기고
    
                if (p + rdlen > end) {
                    fprintf(stderr, "End of message.\n"); exit(1);}
    
                if (rdlen == 4 && type == 1) { //A type, IPv4를 의미
                    /* A Record */
                    printf("Address ");
                    printf("%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);
    
                } else if (rdlen == 16 && type == 28) { //AAAA type, IPv6
                    /* AAAA Record */
                    printf("Address ");
                    int j;
                    for (j = 0; j < rdlen; j+=2) {
                        printf("%02x%02x", p[j], p[j+1]);
                        if (j + 2 < rdlen) printf(":");
                    }
                    printf("\n");
    //_______________________이하 타입은 잘 몰라도 ㄱㅊ__________________________
                } else if (type == 15 && rdlen > 3) { // MX type, Mail exchange record
                    /* MX Record */
                    const int preference = (p[0] << 8) + p[1];
                    printf("  pref: %d\n", preference);
                    printf("MX: ");
                    print_name(msg, p+2, end); printf("\n");
    
                } else if (type == 16) { // TXT type, text record
                    /* TXT Record */
                    printf("TXT: '%.*s'\n", rdlen-1, p+1);
    
                } else if (type == 5) { // CNNAME type, alternative name
                    /* CNAME Record */
                    printf("CNAME: ");
                    print_name(msg, p, end); printf("\n");
                }
    
                p += rdlen;
            }
        }
    
        if (p != end) {
            printf("There is some unread data left over.\n");
        }
    
        printf("\n");
    }
    
    
    int main(int argc, char *argv[]) {
    
        if (argc < 3) {
            printf("Usage:\n\tdns_query hostname type\n");
            printf("Example:\n\tdns_query example.com aaaa\n");
            exit(0);
        }
    
        if (strlen(argv[1]) > 255) {
            fprintf(stderr, "Hostname too long.");
            exit(1);
        }
    
        unsigned char type;
        if (strcmp(argv[2], "a") == 0) { // 입력받는 type값에 따라 QTYPE을 정할 수 있음.
            type = 1;
        } else if (strcmp(argv[2], "mx") == 0) {
            type = 15;
        } else if (strcmp(argv[2], "txt") == 0) {
            type = 16;
        } else if (strcmp(argv[2], "aaaa") == 0) {
            type = 28;
        } else if (strcmp(argv[2], "any") == 0) {
            type = 255;
        } else {
            fprintf(stderr, "Unknown type '%s'. Use a, aaaa, txt, mx, or any.",
                    argv[2]);
            exit(1);
        }
    
        printf("Configuring remote address...\n"); //입력한 hostname에 대한 IP주소를 물어봄 to DNS 서버
        struct addrinfo hints;
        memset(&hints, 0, sizeof(hints));
        hints.ai_socktype = SOCK_DGRAM; //UDP로 세팅
        struct addrinfo *peer_address;
        if (getaddrinfo("8.8.8.8", "53", &hints, &peer_address)) { //구글 제공 DNS서버, DNS 서버 포트번호 53
            fprintf(stderr, "getaddrinfo() failed. (%d)\n", GETSOCKETERRNO());
            return 1;
        }
    
    
        printf("Creating socket...\n"); //socket 생성 
        SOCKET socket_peer;
        socket_peer = socket(peer_address->ai_family,
                peer_address->ai_socktype, peer_address->ai_protocol);
        if (!ISVALIDSOCKET(socket_peer)) {
            fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
            return 1;
        }
    
    
        char query[1024] = {0xAB, 0xCD, /* ID */
                            0x01, 0x00, /* Set recursion */
                            0x00, 0x01, /* QDCOUNT */
                            0x00, 0x00, /* ANCOUNT */
                            0x00, 0x00, /* NSCOUNT */
                            0x00, 0x00 /* ARCOUNT */};
                            
    	//쿼리에 hostname LV encoding 하는 과정
        char *p = query + 12; // msg start + header length
        char *h = argv[1]; // hostname
    
        while(*h) { // hostname을 NAME 부분에 넣어주는 구간.
            char *len = p;
            p++; //length 값 넣을 공간 남겨두는 부분
            if (h != argv[1]) ++h; //.을 제끼는 부분
    
            while(*h && *h != '.') *p++ = *h++;
            *len = p - len - 1;
        }
    
        *p++ = 0; // termination 0
        *p++ = 0x00; *p++ = type; /* QTYPE */ //받았던 type값을 지정.
        *p++ = 0x00; *p++ = 0x01; /* QCLASS */ //1로 하드세팅함.
    
    
        const int query_size = p - query; //쿼리의 크기는 p위치에서 query(즉,제일 처음 query 시작점)을 빼줌
    
        int bytes_sent = sendto(socket_peer, //쿼리를 쿼리사이즈만큼 DNS서버에 보냄
                query, query_size,
                0,
                peer_address->ai_addr, peer_address->ai_addrlen);
        printf("Sent %d bytes.\n", bytes_sent); // 얼만큼 보냄~
    
        print_dns_message(query, query_size); // 뭘 보냄~
    
        char read[1024];
        int bytes_received = recvfrom(socket_peer, // response값을 받음
                read, 1024, 0, 0, 0);
    
        printf("Received %d bytes.\n", bytes_received); // 받은 메세지 길이
    
        print_dns_message(read, bytes_received); // 받은 read를 길이만큼 읽어와서 출력함.
        printf("\n");
    
    
        freeaddrinfo(peer_address);
        CLOSESOCKET(socket_peer);
    
        return 0;
    }

     <실행 결과>

     

    dns_query.c 실행 결과

     

     

     

    728x90
    반응형

    '대학 전공 공부 > 네트워크 프로그래밍' 카테고리의 다른 글

    16. Basic OpenSSL  (0) 2022.06.08
    15. 네트워크 보안  (0) 2022.06.08
    14강. 보안 기본 개념  (0) 2022.05.21
    13강. HTTP and Web Client  (0) 2022.05.13
    11강. Error, IPv6 and Packet Socket  (0) 2022.05.12