Linux C语言实现DNS请求

实际应用中基本用不到DNS自己解析
要获取域名在终端下用host 或者nslookup指令
在c里面使用gethostbyname或者getaddrinfo也能将域名解析为ip
如果不想看文章就用上面的函数。

文章目录

  • 一、DNS解析过程
  • 二、DNS协议报文格式
    • 1 Header(12字节)
      • 1.1 Transaction ID (会话标识)(2 字节)
      • 1.2 Flags(标志) (2 字节)
      • 1.3 数量字段(总共 8 字节)
    • 2 正文( 字节)
      • 2.1 Queries (查询)
      • 2.2 RR (源记录)
  • 三、C实现查询DNS


一、DNS解析过程

域名解析总体可分为两大步骤,
第一个步骤是本机向本地域名服务器发出一个 DNS 请求报文,报文里携带需要查询的域名;
第二个步骤是本地域名服务器向本机回应一个 DNS 响应报文,里面包含域名对应的 IP 地址。

从下面对 baidu.com 进行域名解析的报文中可明显看出这两大步骤。

其具体的流程可描述如下:

  1. 主机 192.168.1.123 先向本地域名服务器 192.168.1.1 进行递归查询
  2. 本地域名服务器采用迭代查询(192.168.1.1),向一个根域名服务器(114.114.114.114)进行查询
  3. 根域名服务器(114.114.114.114)告诉本地域名服务器(192.168.1.1),下一次应该查询的顶级域名服务器 baidu.com 的 IP 地址
  4. 本地域名服务器(192.168.1.1)向顶级域名服务器 baidu.com 进行查询
  5. 顶级域名服务器 .com 告诉本地域名服务器,下一步查询权限服务器 www.baidu.com 的 IP 地址
  6. 本地域名服务器(192.168.1.1)向权限服务器 www.baidu.com 进行查询
  7. 权限服务器 www.baidu.com 告诉本地域名服务器(192.168.1.1)所查询的主机的 IP 地址
  8. 本地域名服务器(192.168.1.1)最后把查询结果36.152.44.96告诉主机(192.168.1.123)

其中有两个概念递归查询和迭代查询:
递归查询: 本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域
名服务器无法解析,自己会以 DNS 客户机的身份向其它域名服务器查询,直到得到最
终的 IP 地址告诉本机

迭代查询: 本地域名服务器向根域名服务器查询,根域名服务器告诉它下一步到哪里去
查询,然后它再去查,每次它都是以客户机的身份去各个服务器查询

二、DNS协议报文格式

Linux C语言实现DNS请求_第1张图片

1 Header(12字节)

1.1 Transaction ID (会话标识)(2 字节)

是 DNS 报文的 ID 标识,对于请求报文和其对应的应答报文,这个字段请丢和响应时是相同的,通过它可以区分 DNS 应答报文是哪个请求的响应。

1.2 Flags(标志) (2 字节)

在这里插入图片描述

标识 解释
QR(1bit) 查询/响应标志,0 为查询,1 为响应
opcode(4bit) 0 表示标准查询,1 表示反向查询,2 表示服务器状态请求
AA(1bit) 表示授权回答
TC(1bit) 表示可截断的
RD(1bit) 表示期望递归
RA(1bit) 表示可用递归
rcode(4bit) 表示返回码,0 表示没有差错,3 表示名字差错,2 表示服务器错误(Server Failure)

1.3 数量字段(总共 8 字节)

Questions、Answer RRs、Authority RRs、Additional RRs 各自表示后面的四个区域的数目。

标识 解释
Questions 表示查询问题区域节的数量
Answers 表示回答区域的数量
Authoritative namesversers 表示授权区域的数量
Additional recoreds 表示附加区域的数量

2 正文( 字节)

2.1 Queries (查询)

Linux C语言实现DNS请求_第2张图片
Name(查询名): 长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为 IP,反向查询即由 IP 地址反查域名),一般的格式如下图所示。
Linux C语言实现DNS请求_第3张图片
Type(查询类型)

类型 助记符 说明
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 对所有记录的请求

Class(查询类)
通常为 1,表明是 Internet 数据

2.2 RR (源记录)

源记录(RR) 包括回答区域(Answers)授权区域(Authoritative nameservers)附加区域(Additional recoreds)
Linux C语言实现DNS请求_第4张图片
Name(域名)(2 字节或不定长)
格式和 Queries 区域的查询名字字段是一样的。
有一点不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。
比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。
其余的 14 位从 DNS 报文的开始 处 计 数 ( 从 0 开 始 ), 指出该报文中的相应字节数 。

**Type(查询类型)**表明资源纪录的类型
Class(查询类): 对于 Internet 信息,总是 IN
TTL(生存时间): 以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程
序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳
定程度,极为稳定的信息会被分配一个很大的值(比如 86400,这是一天的秒数)。
Data(资源数据): 该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数
据。可以是 Address(表明查询报文想要的回应是一个 IP 地址)或者 CNAME(表明查询
报文想要的回应是一个规范主机名)等。

三、C实现查询DNS

#include 
#include 
#include 

#include 
#include 
#include 

#define DNS_SERVER_PORT		53
#define DNS_SERVER_IP		"114.114.114.114"

#define DNS_HOST			0x01
#define DNS_CNAME			0x05

struct dns_header {

	unsigned short id;
	unsigned short flags;

	unsigned short questions; // 问题数 1 
	unsigned short answer;    // 答案数

	unsigned short authority; // 权威答案数
	unsigned short additional;// 附加答案数

};


struct dns_question {
	int length;
	unsigned char *name; // 
	unsigned short qtype;
	unsigned short qclass;
};

struct dns_item {
	char *domain;
	char *ip;
};


//client sendto dns server
int dns_create_header(struct dns_header *header) {

	if (header == NULL) return -1;
	//把传进来的header置空
	memset(header, 0, sizeof(struct dns_header));

	//给Header的ID随机附值
	srandom(time(NULL));
	header->id = random();

	//标识符大部分好像都是0x0100
	header->flags = htons(0x0100);
	//只有一个问题区域节
	header->questions = htons(1); 
	return 0;
}

// hostname: www.0voice.com
// www
// 0voice
// com

// name: 3www60voice3com0

int dns_create_question(struct dns_question *question, const char *hostname) {

	if (question == NULL || hostname == NULL) return -1;
	memset(question, 0, sizeof(struct dns_question));	

	//申请查询名的内存空间,因为name的长度是不固定的 所以多加2个冗余位
	question->name = (char*)malloc(strlen(hostname) + 2);
	if (question->name == NULL) {
		return -2;
	}

	//设置question的数据长度信息
	question->length = strlen(hostname) + 2;
	//设置question的类型 A类
	question->qtype = htons(1); 
	//设置question的class 通常为1
	question->qclass = htons(1);

	// name 
	const char delim[2] = ".";
	char *qname = question->name;
	
	//strdup()会先用maolloc()配置与参数s 字符串相同的空间大小,然后将参数hostname字符串的内容复制到该内存地址,然后把该地址返回。
	char *hostname_dup = strdup(hostname);
	//分解字符串 hostname_dup 为一组字符串,delim 为分隔符。
	char *token = strtok(hostname_dup, delim); // www.0voice.com 

	//继续分割剩下的字符串
	while (token != NULL) {

		//size_t 为 long unsigned int
		size_t len = strlen(token); //第一次是www 长度为3

		// 更新qname //3www.4xxxx.3com
		// 先把数字塞进去
		*qname = len;

		// 指向下一个位置 填充后面的内容
		qname ++;
		//把token放到qname里面,复制长度是len+1 是把结尾的/0也放进去了
		strncpy(qname, token, len + 1);
		qname += len;
		
		//此处此一个参数填NULL,会继续以delim为分隔符分割上一步分割后的字符串
		token = strtok(NULL, delim); //xxxx.com ,  com
	}

	free(hostname_dup);
}

// struct dns_header *header
// struct dns_question *question
// char *request

int dns_build_request(struct dns_header *header, struct dns_question *question, char *request, int rlen) {

	if (header == NULL || question == NULL || request == NULL) return -1;
	memset(request, 0, rlen);

	// header --> request
	memcpy(request, header, sizeof(struct dns_header));
	int offset = sizeof(struct dns_header);

	// question --> request
	// 把question的name放进request
	memcpy(request+offset, question->name, question->length);
	offset += question->length;
	// 把question的type放进request
	memcpy(request+offset, &question->qtype, sizeof(question->qtype));
	offset += sizeof(question->qtype);
	// 把question的class放进request
	memcpy(request+offset, &question->qclass, sizeof(question->qclass));
	offset += sizeof(question->qclass);

	//返回request的长度
	return offset;
}


static int is_pointer(int in) {
	return ((in & 0xC0) == 0xC0);
}


static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {

	int flag = 0, n = 0, alen = 0;
	char *pos = out + (*len);

	while (1) {

		flag = (int)ptr[0];
		if (flag == 0) break;

		if (is_pointer(flag)) {
			
			n = (int)ptr[1];
			ptr = chunk + n;
			dns_parse_name(chunk, ptr, out, len);
			break;
			
		} else {

			ptr ++;
			memcpy(pos, ptr, flag);
			pos += flag;
			ptr += flag;

			*len += flag;
			if ((int)ptr[0] != 0) {
				memcpy(pos, ".", 1);
				pos += 1;
				(*len) += 1;
			}
		}
	
	}
	
}



//解析响应信息				  buffer为response返回的信息						
static int dns_parse_response(char *buffer, struct dns_item **domains) {

	int i = 0;
	//初始化一个工作指针 指向reponse返回过来的信息的头部
	unsigned char *ptr = buffer;

	//
	ptr += 4;
	int querys = ntohs(*(unsigned short*)ptr);

	ptr += 2;
	int answers = ntohs(*(unsigned short*)ptr);

	ptr += 6;
	for (i = 0; i < querys; i++) {
		while (1) {
			int flag = (int)ptr[0];
			ptr += (flag + 1);

			if (flag == 0) break;
		}
		ptr += 4;
	}

	char cname[128], aname[128], ip[20], netip[4];
	int len, type, ttl, datalen;

	int cnt = 0;
	struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item));
	if (list == NULL) {
		return -1;
	}

	for (i = 0;i < answers;i ++) {
		
		bzero(aname, sizeof(aname));
		len = 0;

		dns_parse_name(buffer, ptr, aname, &len);
		ptr += 2;

		type = htons(*(unsigned short*)ptr);
		ptr += 4;

		ttl = htons(*(unsigned short*)ptr);
		ptr += 4;

		datalen = ntohs(*(unsigned short*)ptr);
		ptr += 2;

		if (type == DNS_CNAME) {

			bzero(cname, sizeof(cname));
			len = 0;
			dns_parse_name(buffer, ptr, cname, &len);
			ptr += datalen;
			
		} else if (type == DNS_HOST) {

			bzero(ip, sizeof(ip));

			if (datalen == 4) {
				memcpy(netip, ptr, datalen);
				inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));

				printf("%s has address %s\n" , aname, ip);
				printf("\tTime to live: %d minutes , %d seconds\n", ttl / 60, ttl % 60);

				list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
				memcpy(list[cnt].domain, aname, strlen(aname));
				
				list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
				memcpy(list[cnt].ip, ip, strlen(ip));
				
				cnt ++;
			}
			ptr += datalen;
		}
	}

	*domains = list;
	ptr += 2;

	return cnt;
}


int dns_client_commit(const char *domain) {

    //套接字(socket 文件描述符) AF_INET指 IPv4 SOCK_DGRAM是UDP套接字的名字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0) {
		return  -1;
	}

    //把目标服务器的信息存在sockaddr_in结构体中
	struct sockaddr_in servaddr = {0}; //初始化置空
	servaddr.sin_family = AF_INET;  //IPv4协议
	servaddr.sin_port = htons(DNS_SERVER_PORT); //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序
	//IP地址 是在结构体内嵌套结构体 最内层的s_addr是一个unsigned long类型
    //inet_addr()的功能是将一个点分十进制的IP转换成一个长整数型数(unsigned long类型)
    servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP); 

    //使用connect()将套接字与特定的IP地址和端口绑定起来,建立这样绑定好数据的连接
    //                套接字   sockaddr 结构体变量的指针     addr 变量的大小
	int ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
	//connect() Return 0 on success, -1 for errors.
	printf("connect : %d\n", ret);

	struct dns_header header = {0};	//Hearder先全部置零
	//给Header赋上信息值
	dns_create_header(&header);

	struct dns_question question = {0}; //Question先全部置零
	//给question赋上信息值
	dns_create_question(&question, domain);

	//构造一个请求(request)先全部置零
	char request[1024] = {0};
	int length = dns_build_request(&header, &question, request, 1024);

	/**开始请求
	sendto() 用来将数据由指定的socket 传给对方主机
	参数1 为已建好连线的socket, 如果利用UDP协议则不需经过连线操作
	参数2 指向欲连线的数据内容,
	参数3 为数据内容的长度
	参数4 一般设0
	参数5 用来指定欲传送的网络地址, 结构sockaddr 请参考bind(). 
	参数6 为sockaddr 的结果长度
	返回值:成功则返回实际传送出去的字符数, 失败返回-1, 错误原因存于errno 中.**/
	int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr));
	
	//构造一个响应(response)先全部置零
	char response[1024] = {0};
	//返回的地址
	struct sockaddr_in addr;
	size_t addr_len = sizeof(struct sockaddr_in);
	
	/**开始接收响应
	recv()用来接收远程主机经指定的socket 传来的数据, 
	参数1 为已建好连线的socket, 如果利用UDP协议则不需经过连线操作
	参数2 为数据存到由指向的内存空间 
	参数3 为可接收数据的最大长度
	参数4 一般设0
	参数5 用来指定欲传送的网络地址, 结构sockaddr
	参数6 为sockaddr 的结构长度.
	返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.**/
	int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);

	//处理接收到的响应
	struct dns_item *dns_domain = NULL;
	// dns_parse_response(response, &dns_domain);

	for(int i = 0; i < n; i++)
	{
		printf("%x",response[i]);
	}
	printf("\n");

	free(dns_domain);
	//返回接收到的字符数
	return n;
}

int main(int argc, char *argv[]) {
	if (argc < 2) return -1;
	dns_client_commit(argv[1]);
}

你可能感兴趣的:(LINUX,C/C++,网络协议,网络协议,c语言,ubuntu,linux)