用C语言实现DNS
1关于DNS:
1.DNS是基于UDP实现的。
2.域名解析总体可分为两大步骤,第一个步骤是本机向本地域名服务器发出一个DNS请求报文,报文里携带需要查询的域名;第二个步骤是本地域名服务器向本机回应一个DNS响应报文,里面包含域名对应的IP地址。
2关于DNS报文:
如果要实现DNS,必须要理解透彻DNS报文的结构和功能!DNS报文结构如下:
DNS报文格式:
1首部:
1.1TransactionID(会话标识2byte)是DNS报文的ID标识,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分DNS应答报文是哪个请求的响应。
1.2Flags:
QR(1bit):查询/响应标志,0为查询,1为响应。
opcode(4bit):0表示标准查询,1表示反向查询,2表示服务器状态请求。
AA(1bit):表示授权回答。
TC(1bit):表示可截断的。
RD(1bit):表示期望递归。
RA(1bit):表示可用递归。
Z :保留值,暂时未使用。在所有的请求和应答报文中必须置为0。
rcode(4bit):
rcode:应答码(Response code) ,这4个比特位在应答报文中设置,代表的含义如下:
0 没有错误。
1 报文格式错误(Format error) - 服务器不能理解请求的报文。
2 服务器失败(Server failure) - 因为服务器的原因导致没办法处理这个请求。
3 名字错误(Name Error) - 只有对授权域名解析服务器有意义,指出解析的域名不存在。
4 没有实现(Not Implemented) - 域名服务器不支持查询类型。
5 拒绝(Refused) - 服务器由于设置的策略拒绝给出应答。比如,服务器不希望对某些请求者给出应答,或者服务器不希望进行某些操作(比如区域传送zone transfer)。
1.3数量字段(总共8字节):Questions、Answer RRs、Authority RRs、Additional RRs 各自表示后面的四个区域的数目。Questions表示查询问题区域节的数量,Answers表示回答区域的数量,Authoritative namesversers表示授权区域的数量,Additional recoreds表示附加区域的数量。
2查询问题区域中的报文格式:
2.1查询名:长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为IP,反向查询即由IP地址反查域名),一般的格式如下图所示。
2.2查询类型:每个问题有一个查询类型。2个字节表示查询类型,取值可以为任何可用的类型值,以及通配码来表示所有的资源记录。
类型 助记符 说明
1 A 由域名获得IPv4地址
2 NS 查询域名服务器
5 CNAME 查询规范名称
6 SOA 开始授权
11 WKS 熟知服务
12 PTR 把IP地址转换成域名
13 HINFO 主机信息
15 MX 邮件交换
28 AAAA 由域名获得IPv6地址
252 AXFR 传送整个区的请求
255 ANY 对所有记录的请求
2.3查询类:通常为1,表明是Internet数据
3回答区域的报文格式:
该区域有三个,但格式都是一样的。这三个区域分别是:回答区域,授权区域和附加区域。
3.1域名(2字节或不定长):它的格式和查询区域的查询名字字段是一样的。有一点不同就是,当报文中域名重复出现的时候,该字段使用2个字节的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。
3.2查询类型:表明资源纪录的类型。
3.3查询类:对于Internet信息,总是IN(1)。
3.4生存时间(TTL):以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值。
3.5资源数据:该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数据。可以是Address(表明查询报文想要的回应是一个IP地址)或者CNAME(表明查询报文想要的回应是一个规范主机名)等。
3算法流程:
4代码及注释:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #include<unistd.h> char dns_servers[1][16];//存放DNS服务器的IP int dns_server_count = 0; /* **DNS报文中查询区域的查询类型 */ #define A 1 //查询类型,表示由域名获得IPv4地址 void ngethostbyname(unsigned char*, int); void ChangetoDnsNameFormat(unsigned char*, unsigned char*); /* **DNS报文首部 **这里使用了位域 */ struct DNS_HEADER { unsigned short id; //会话标识 unsigned char rd :1; // 表示期望递归 unsigned char tc :1; // 表示可截断的 unsigned char aa :1; // 表示授权回答 unsigned char opcode :4; unsigned char qr :1; // 查询/响应标志,0为查询,1为响应 unsigned char rcode :4; //应答码 unsigned char cd :1; unsigned char ad :1; unsigned char z :1; //保留值 unsigned char ra :1; // 表示可用递归 unsigned short q_count; // 表示查询问题区域节的数量 unsigned short ans_count; // 表示回答区域的数量 unsigned short auth_count; // 表示授权区域的数量 unsigned short add_count; // 表示附加区域的数量 }; /* **DNS报文中查询问题区域 */ struct QUESTION { unsigned short qtype;//查询类型 unsigned short qclass;//查询类 }; typedef struct { unsigned char *name; struct QUESTION *ques; } QUERY; /* **DNS报文中回答区域的常量字段 */ //编译制导命令 #pragma pack(push, 1)//保存对齐状态,设定为1字节对齐 struct R_DATA { unsigned short type; //表示资源记录的类型 unsigned short _class; //类 unsigned int ttl; //表示资源记录可以缓存的时间 unsigned short data_len; //数据长度 }; #pragma pack(pop) //恢复对齐状态 /* **DNS报文中回答区域的资源数据字段 */ struct RES_RECORD { unsigned char *name;//资源记录包含的域名 struct R_DATA *resource;//资源数据 unsigned char *rdata; }; int main(int argc, char *argv[]) { unsigned char hostname[100]; unsigned char dns_servername[100]; printf("请输入DNS服务器的IP:"); scanf("%s", dns_servername); strcpy(dns_servers[0], dns_servername); printf("请输入要查询IP的主机名:"); scanf("%s", hostname); //由域名获得IPv4地址,A是查询类型 ngethostbyname(hostname, A); return 0; } /* **实现DNS查询功能 */ void ngethostbyname(unsigned char *host, int query_type) { unsigned char buf[65536], *qname, *reader; int i, j, stop, s; struct sockaddr_in a;//地址 struct RES_RECORD answers[20], auth[20], addit[20];//回答区域、授权区域、附加区域中的资源数据字段 struct sockaddr_in dest;//地址 struct DNS_HEADER *dns = NULL; struct QUESTION *qinfo = NULL; printf("\n所需解析域名:%s", host); s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //建立分配UDP套结字 dest.sin_family = AF_INET;//IPv4 dest.sin_port = htons(53);//53号端口 dest.sin_addr.s_addr = inet_addr(dns_servers[0]);//DNS服务器IP dns = (struct DNS_HEADER *) &buf; /*设置DNS报文首部*/ dns->id = (unsigned short) htons(getpid());//id设为进程标识符 dns->qr = 0; //查询 dns->opcode = 0; //标准查询 dns->aa = 0; //不授权回答 dns->tc = 0; //不可截断 dns->rd = 1; //期望递归 dns->ra = 0; //不可用递归 dns->z = 0; //必须为0 dns->ad = 0; dns->cd = 0; dns->rcode = 0;//没有差错 dns->q_count = htons(1); //1个问题 dns->ans_count = 0; dns->auth_count = 0; dns->add_count = 0; //qname指向查询问题区域的查询名字段 qname = (unsigned char*) &buf[sizeof(struct DNS_HEADER)]; ChangetoDnsNameFormat(qname, host);//修改域名格式 qinfo = (struct QUESTION*) &buf[sizeof(struct DNS_HEADER) + (strlen((const char*) qname) + 1)]; //qinfo指向问题查询区域的查询类型字段 qinfo->qtype = htons(query_type); //查询类型为A qinfo->qclass = htons(1); //查询类为1 //向DNS服务器发送DNS请求报文 printf("\n\n发送报文中..."); if (sendto(s, (char*) buf,sizeof(struct DNS_HEADER) + (strlen((const char*) qname) + 1)+ sizeof(struct QUESTION), 0, (struct sockaddr*) &dest,sizeof(dest)) < 0) { perror("发送失败!"); } printf("发送成功!"); //从DNS服务器接受DNS响应报文 i = sizeof dest; printf("\n接收报文中..."); if (recvfrom(s, (char*) buf, 65536, 0, (struct sockaddr*) &dest,(socklen_t*) &i) < 0) { perror("接收失败!"); } printf("接收成功!"); dns = (struct DNS_HEADER*) buf; //将reader指向接收报文的回答区域 reader = &buf[sizeof(struct DNS_HEADER) + (strlen((const char*) qname) + 1) + sizeof(struct QUESTION)]; printf("\n\n响应报文包含: "); printf("\n %d个问题", ntohs(dns->q_count)); printf("\n %d个回答", ntohs(dns->ans_count)); printf("\n %d个授权服务", ntohs(dns->auth_count)); printf("\n %d个附加记录\n\n", ntohs(dns->add_count)); /* **解析接收报文 */ reader = reader + sizeof(short);//short类型长度为32为,相当于域名字段长度,这时reader指向回答区域的查询类型字段 answers[i].resource = (struct R_DATA*) (reader); reader = reader + sizeof(struct R_DATA);//指向回答问题区域的资源数据字段 if (ntohs(answers[i].resource->type) == A) //判断资源类型是否为IPv4地址 { answers[i].rdata = (unsigned char*) malloc(ntohs(answers[i].resource->data_len));//资源数据 for (j = 0; j < ntohs(answers[i].resource->data_len); j++) { answers[i].rdata[j] = reader[j]; } answers[i].rdata[ntohs(answers[i].resource->data_len)] = '\0'; reader = reader + ntohs(answers[i].resource->data_len); } //显示查询结果 if (ntohs(answers[i].resource->type) == A) //判断查询类型IPv4地址 { long *p; p = (long*) answers[i].rdata; a.sin_addr.s_addr = *p; printf("IPv4地址:%s\n", inet_ntoa(a.sin_addr)); } return; } /* **从www.baidu.com转换到3www5baidu3com */ void ChangetoDnsNameFormat(unsigned char* dns, unsigned char* host) { int lock = 0, i; strcat((char*) host, "."); for (i = 0; i < strlen((char*) host); i++) { if (host[i] == '.') { *dns++ = i - lock; for (; lock < i; lock++) { *dns++ = host[lock]; } lock++; } } *dns++ = '\0'; }
程序中一些代码和函数的说明:
程序中使用编译制导命令#pragma pack(n)来设定变量以n字节对齐方式。
其中#pragma pack(push, 1) 保存对齐状态,设定为1字节对齐
#pragma pack(pop)恢复对齐状态
htons是将整型变量从主机字节顺序转变成网络字节顺序。
ntohs()由网络字节顺序转换为主机字节顺序。
5效果: