C实现DNS

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
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DNS中继服务器是指将DNS查询请求转发给其他DNS服务器的服务器。下面是一个简单的C语言实现示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #define BUF_SIZE 512 #define DNS_PORT 53 int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <dns_server>\n", argv[0]); exit(EXIT_FAILURE); } char *dns_server = argv[1]; int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) { perror("socket"); exit(EXIT_FAILURE); } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(DNS_PORT); addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); exit(EXIT_FAILURE); } char buf[BUF_SIZE]; struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); while (1) { int recv_len = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len); if (recv_len < 0) { perror("recvfrom"); exit(EXIT_FAILURE); } printf("Received query from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 将查询请求转发给DNS服务器 struct sockaddr_in dns_addr; memset(&dns_addr, 0, sizeof(dns_addr)); dns_addr.sin_family = AF_INET; dns_addr.sin_port = htons(DNS_PORT); dns_addr.sin_addr.s_addr = inet_addr(dns_server); int send_len = sendto(sockfd, buf, recv_len, 0, (struct sockaddr *)&dns_addr, sizeof(dns_addr)); if (send_len < 0) { perror("sendto"); exit(EXIT_FAILURE); } printf("Forwarded query to %s\n", dns_server); // 接收DNS服务器的响应 recv_len = recvfrom(sockfd, buf, BUF_SIZE, 0, NULL, NULL); if (recv_len < 0) { perror("recvfrom"); exit(EXIT_FAILURE); } printf("Received response from %s\n", dns_server); // 将响应发送给客户端 send_len = sendto(sockfd, buf, recv_len, 0, (struct sockaddr *)&client_addr, client_addr_len); if (send_len < 0) { perror("sendto"); exit(EXIT_FAILURE); } printf("Sent response to %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } close(sockfd); return 0; } ``` 在运行程序时,需要传入要转发DNS查询请求的DNS服务器的IP地址或域名,例如: ```bash ./dns_relay_server 8.8.8.8 ``` 这将启动一个DNS中继服务器,并将DNS查询请求转发给Google Public DNS服务器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值