现在的位置: 首页 > 综合 > 正文

C实现DNS

2018年07月16日 ⁄ 综合 ⁄ 共 7954字 ⁄ 字号 评论关闭

DNS(Domain Name System)域名系统提供了主机名和IP地址之间的转换。通常我们在应用程序中使用库函数gethostbyname()gethostbyaddr()来完成两者之间的转换。但是为了更深入的学习网络底层知识,有必要从源代码级别来分析和实现

RFC 1034说明了DNS的概念和功能,RFC
1035
详细说明了DNS的规范和实现。通过阅读RFC,我们知道明白了,应用程序对DNS的访问是通过解析器来(resolver)完成的,解析器并不像TCP/IP协议那样是OS的内核,而是通过网络访问DNS服务器来得到名字和地址的对应关系。OS的TCP/IP协议簇对DNS一点都知道。


工欲善其事必先利其器,先得进行些基础知识的复习:《bit与byte的区别》和 《bit与byte的联系》及《位运算》,一个int是4个byte(十六进制中01
02 03 04
转化为十进制为16909060),一个char是1个byte(十六进制中97转化为字符为a)。例如在十六进制中0x80,用bit来表示就是1000
0000
,此时如果我们对它实施位(>>5)运算,得到的结果就是0000 0100,十六进制值为0x04


在Linux的内核代码中,经常可以看见形如#define do{ }while(0)的宏定义,是否感到疑惑呢?宏定义只是帮助我们进行替换而已,当定义多条语句时,会在if...else...语句中产生歧义,详细解释参考链接。(小插曲,我在测试中只#include
<stdlib.h>
,忘记了#include <stdio.h>,然后在后面使用了printf等,结果编译的时候产生警告:
warning: implicit declaration of function ‘printf’
warning: incompatible implicit declaration of built-in function ‘printf’
经过查找才知道警告的原因是没有包含printf函数的明确定义,那么就隐式定义了。而编译时库里恰好有这个函数,虽然不会出错,但会给出警告。从代码习惯上讲,所有函数都应该被明确定义,切记)


一般的DNS是基于UDP,报文格式如下图:

500)this.width=500;"
border=0>
前面是固定的12byte首部,后面是4个长度可变的字段。


从首部开始,0~15位bit刚好是2个byte,由客户程序设置并由服务器返回,客户程序通过它来确定响应是否与查询匹配(例如,客户程序在这里输入的是十六进制的0xD8B4,那么服务器的该字段也会填入相同的值。这个标识又称为Transaction
ID
,在《DNS欺骗技术原理与安全防范技术》中有更详细的讨论)。


接下来的16~31位bit刚好也是2个byte,用作协议的标志位
500)this.width=500;"
border=0>
  • QR是1个bit位:0代表查询报文,1代表相应报文
  • opcode是4个bit位字段:0代表标准查询,1代表反向查询,2代表服务器状态请求
  • AA是1个bit位,是Authoritative Answer的缩写,指明名字服务器是授权于该域的
  • TC是1个bit位,是Truncated的缩写,意为可截断的,指明在UDP中应答报文超过512字节时,只返回512字节
  • RD是1个bit位,是Recursion Desired的缩写,意为期望递归,期望名字服务器必须处理这个查询,而不是给出一个迭代查询服务器的列表
  • RA是1个bit位,是Recursion Available的缩写,意为可用递归,如果名字服务器支持递归查询,这会将此位设置为1
  • zero是3个bit位,设置为0
  • rcode是4个bit位,表示名字差错,0为无差错,3为有差错。当查询中指定的域不存在的时候,就返回3

现在通过抓包来加深对上面的理解:

通过上图我们可以,二进制格式就为0000 0001 0000 0000,16位bit两个byte,其十六进制值为0x0100,这是一个标准的DNS查询请求的标志位


再接着是4段16位bit:
  • QuestionCount 查询问题记录数由客户端填写,服务器端按原值返回
  • AnswerCount 资源记录数由服务器端填写,代表有多少适应这个问题记录的对应IP
  • NameServerCount 授权资源记录数,一般为0
  • AdditionalCount 额外资源记录数,一般为0

现在通过抓包来加深对上面的理解:

500)this.width=500;"
border=0>



分析完报文头,现在该是报文体了。分为四大块:

  • 查询问题
  • 回答
  • 授权
  • 额外信息

先看查询问题,它通常只有一个问题,当然也可以有多个问题,问题数由QuestionCount确定:


查询名是要查找的名字,它是一个或多个标识符的序列。每个标识符以首字节的计数值,来说明随后标识符的字节长度,每个名字以最后字节为0结束,长度为0的标识符是根标识符。计数字节的值必须是0~63的数,因为标识符的最大长度仅为63。该字段无需以整32bit边界结束,即无需填充字节。这种编码格式非常象BT协议中bencode编码,例如查询twistedmatrix.com:

500)this.width=500;"
border=0>

第一位为计数位,从首字母到第一个.号,一共是13位(twistedmatrix),然后是第二个计数位,值为3,由com计算得到,最后是结束符号0。

接着就是查询类型,该类型就是针对查询问题的,在RFC中有详细的描述,一般使用如下表:

类型

描述

A

1

IP地址

NS

2

名字服务器

MD

3

邮件目的的(已过时,请用MX)

MF

4

邮件中转站(已过时,请用MX)

CNAME

5

规范名词

SOA

6

 xxx

MB

7

邮箱记录名(实验性质)

MG

8

邮件组成员(实验性质)

MR

9

邮件更改后记录名(实验性质)

NULL

10

RR(实验性质)

WKS

11

众所皆知的服务描述

PTR

12

指针记录

HINFO

13

主机信息

MINFO

14

邮箱或者邮件列表信息

MX

15

邮件交换记录

TXT

16

文本字符串

最常用的查询类型为A,表示期望获得查询名对应的IP地址。最后的查询类,通常是1,指互联网地址


剩下的3个字段是:回答授权额外信息,均采用资源记录RR(Resource
Record
)格式,如下图:

500)this.width=500;" border=0>

域名是记录中资源数据对应的名字。它的格式和前面介绍的查询名字段格式相同。类型说明RR的类型码。它的值和前面介绍的查询类型值是一样的。类通常为1,指Internet数据。生存时间字段是客户程序保留该资源记录的秒数。资源记录通常的生存时间值为2天。资源数据长度说明资源数据的数量。该数据的格式依赖于类型字段的值。对于类型1(A记录)资源数据是4字节的IP地址。


=============================================================================

上文已提过,通常进行域名和IP地址的转换时,使用gethostbyname()gethostbyaddr()(在W.Richard Stevens的《Unix Network Programming》第11章有详细的说明)。当仔细分析了DNS的协议后,我们自己动手来写写看。

下例执行的环境在Debian4.0上,编译工具为gcc,DNS服务器地址为192.168.1.1(通常该服务的默认监听端口为53),文件名为DNSClient.c:

#include <stdio.h>

#include <stdlib.h>

#include <error.h>

#include <string.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <time.h>

 

static void printmessage(unsigned char *buf);

static unsigned char *printnamestring(unsigned char *p,unsignedchar *buf);

 

#define GETWORD(__w,__p) do{__w=*(__p++)<<8;__w|=*(p++);}while(0)

#define GETLONG(__l,__p) do{__l=*(__p++)<<24;__l|=*(__p++)<<16;__l|=*(__p++)<<8;__l|=*(p++);}while(0)

 

int main(int argc,char*
argv[])

{

    if(argc != 2)

    {

       printf("usage: dnsclient
<host_name>\n"
);

       return -1;

    }

 

    time_t ident;

    int fd;

    int rc;

    int serveraddrlent;

    char *q;

    unsigned char *p;

    unsigned char *countp;

    unsigned char reqBuf[512]
= {0};

    unsigned char rplBuf[512]
= {0};

    struct sockaddr_in
serveraddr;

 

    //udp

    fd = socket(AF_INET, SOCK_DGRAM, 0);

    if(fd == -1)

    {

       perror("error create
udp socket"
);

       return -1;

    }

   

    time(&ident);

    //copy

    p = reqBuf;

    //Transaction ID

    *(p++) = ident;

    *(p++) = ident>>8;

    //Header section

    //flag word = 0x0100

    *(p++) = 0x01;

    *(p++) = 0x00;

    //Questions = 0x0001

    //just one query

    *(p++) = 0x00;

    *(p++) = 0x01;

    //Answer RRs = 0x0000

    //no answers in this
message

    *(p++) = 0x00;

    *(p++) = 0x00;

    //Authority RRs = 0x0000

    *(p++) = 0x00;

    *(p++) = 0x00;

    //Additional RRs = 0x0000

    *(p++) = 0x00;

    *(p++) = 0x00;

    //Query section

    countp = p;  

    *(p++) = 0;

    for(q=argv[1];
*q!=0; q++)

    {

       if(*q != '.')

       {

           (*countp)++;

           *(p++) = *q;

       }

       else if(*countp
!= 0)

       {

           countp = p;

           *(p++) = 0;

       }

    }

    if(*countp != 0)

       *(p++) = 0;

 

    //Type=1(A):host address

    *(p++)=0;

    *(p++)=1;

    //Class=1(IN):internet

    *(p++)=0;

    *(p++)=1;

 

    printf("\nRequest:\n");

    printmessage(reqBuf);

 

    //fill

    bzero(&serveraddr, sizeof(serveraddr));

    serveraddr.sin_family = AF_INET;

    serveraddr.sin_port = htons(53);

    serveraddr.sin_addr.s_addr = inet_addr("192.168.1.1");

 

    //send to DNS Serv

    if(sendto(fd,reqBuf,p-reqBuf,0,(void*)&serveraddr,sizeof(serveraddr))
< 0)

    {

       perror("error sending
request"
);

       return -1;

    }

 

    //recev the reply

    bzero(&serveraddr,sizeof(serveraddr));

    serveraddrlent = sizeof(serveraddr);

    rc = recvfrom(fd,&rplBuf,sizeof(rplBuf),0,(void*)&serveraddr,&serveraddrlent);

    if(rc < 0)

    {

       perror("error receiving
request\n"
);

       return -1;

    }  

 

    //print out results

    printf("\nReply:\n");

    printmessage(rplBuf);

 

    //exit

    printf("Program Exit\n");

    return 0; 

}

 

static void printmessage(unsigned char *buf)

{

    unsigned char *p;

    unsigned int ident,flags,qdcount,ancount,nscount,arcount;

    unsigned int i,j,type,class,ttl,rdlength;

 

    p = buf;

    GETWORD(ident,p);

    printf("ident=%#x\n",ident);

 

    GETWORD(flags,p);

    printf("flags=%#x\n",flags);

    //printf("qr=%u\n",(flags>>15)&1);

    printf("qr=%u\n",flags>>15);

 

    printf("opcode=%u\n",(flags>>11)&15);

    printf("aa=%u\n",(flags>>10)&1);

    printf("tc=%u\n",(flags>>9)&1);

    printf("rd=%u\n",(flags>>8)&1);

    printf("ra=%u\n",(flags>>7)&1);

    printf("z=%u\n",(flags>>4)&7);

    printf("rcode=%u\n",flags&15); 

 

    GETWORD(qdcount,p);

    printf("qdcount=%u\n",qdcount);

 

    GETWORD(ancount,p);

    printf("ancount=%u\n",ancount);

 

    GETWORD(nscount,p);

    printf("nscount=%u\n",nscount);

 

    GETWORD(arcount,p);

    printf("arcount=%u\n",arcount);

 

    for(i=0; i<qdcount;
i++)

    {

       printf("qd[%u]:\n",i);

       while(*p!=0)

       {

           p = printnamestring(p,buf);

           if(*p !=
0)

              printf(".");

       }

       p++;

       printf("\n");

       GETWORD(type,p);

       printf("type=%u\n",type);

       GETWORD(class,p);

       printf("class=%u\n",class);

    }

 

    for(i=0; i<ancount;
i++)

    {

       printf("an[%u]:\n",i);

       p = printnamestring(p,buf);

       printf("\n");

       GETWORD(type,p);

       printf("type=%u\n",type);

       GETWORD(class,p);

       printf("class=%u\n",class);

       GETLONG(ttl,p);

       printf("ttl=%u\n",ttl);

       GETWORD(rdlength,p);

       printf("rdlength=%u\n",rdlength);

       printf("rd=");

       for(j=0; j<rdlength;
j++)

       {

           printf("%2.2x(%u)",*p,*p);

           p++;

       }

       printf("\n");

    }

}

 

static unsigned char *printnamestring(unsigned char *p,unsignedchar *buf)

{

    unsigned int nchars,offset;

 

    nchars = *(p++);

    if((nchars & 0xc0)
== 0xc0)

    {

       offset = (nchars & 0x3f) << 8;

       offset |= *(p++);

       nchars = buf[offset++];

       printf("%*.*s",nchars,nchars,buf+offset);

    }

    else

    {

       printf("%*.*s",nchars,nchars,p);

       p += nchars;

    }

 

    return (p);

}

 

编译命令为
lsj@debian007:~$ gcc -g -Wall -o DNSClient DNSClient.c

然后执行:
lsj@debian007:~$ ./DNSClient bigdogchina.cublog.cn

就可以看见结果啦,这里使用的是UDP的数据格式,我们知道UDP的头部有一个16bit的长度,那么能表示的最大长度为2的16次方65536,再减去包头20,所以UDP包最大长度为65536-20=65516,但是在实际应用中,最好不要超过1K

抱歉!评论已关闭.