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,报文格式如下图:
前面是固定的 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中有详细的描述,一般使用如下表:
< --if gte mso > Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE MicrosoftInternetExplorer4 < endif-->< --if gte mso > < endif-->
类型
|
值
|
描述
|
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)格式,如下图:
域名是记录中资源数据对应的名字。它的格式和前面介绍的查询名字段格式相同。类型说明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,unsigned char *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;
-
-
- fd = socket(AF_INET, SOCK_DGRAM, 0);
- if(fd == -1)
- {
- perror("error create udp socket");
- return -1;
- }
-
- time(&ident);
-
- p = reqBuf;
-
- *(p++) = ident;
- *(p++) = ident>>8;
-
-
- *(p++) = 0x01;
- *(p++) = 0x00;
-
-
- *(p++) = 0x00;
- *(p++) = 0x01;
-
-
- *(p++) = 0x00;
- *(p++) = 0x00;
-
- *(p++) = 0x00;
- *(p++) = 0x00;
-
- *(p++) = 0x00;
- *(p++) = 0x00;
-
- 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;
-
-
- *(p++)=0;
- *(p++)=1;
-
- *(p++)=0;
- *(p++)=1;
-
- printf("\nRequest:\n");
- printmessage(reqBuf);
-
-
- 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");
-
-
- if(sendto(fd,reqBuf,p-reqBuf,0,(void *)&serveraddr,sizeof(serveraddr)) < 0)
- {
- perror("error sending request");
- return -1;
- }
-
-
- 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;
- }
-
-
- printf("\nReply:\n");
- printmessage(rplBuf);
-
-
- 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);
-
- 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,unsigned char *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);
- }
ddd
编译命令为
lsj@debian007:~$ gcc -g -Wall -o DNSClient DNSClient.c
然后执行:
lsj@debian007:~$ ./DNSClient rg4.net
就可以看见结果啦,这里使用的是UDP的数据格式,我们知道UDP的头部有一个16bit的长度,那么能表示的最大长度为2的16次方65536,再减去包头20,所以UDP包最大长度为65536-20=65516,但是在实际应用中,最好不要超过1K
(秩名) |