Bootstrap

LinuxC/C++ UDP编程实现DNS客户端

LinuxC/C++ UDP编程实现DNS客户端



我们在浏览器中输入一个域名时,其实浏览器会先去访问域名服务器,然后域名服务器会把对应的IP地址返回给浏览器,浏览器再去访问这个IP地址.

在Linux或者Windows环境命令行中输入nslookup [域名]就会返回对应的IP地址(Linux需要先使用yum -y install bind-utils安装bind-utils才能使用这个命令).

在这里插入图片描述

现在我们来简单实现客户端使用nslookup [域名]这个命令访问DNS服务器这一过程.

在实现之前,我们需要先了解一下DNS协议UDP编程.

DNS协议

域名解析过程

假设现在有一台主机,,他想去访问www.bilibili.com这个域名.

  • 主机会先向它的本地域名服务器www.bilibili.com进行递归查询.
  • 本地域名服务器采用迭代查询,先向一个根域名服务器进行查询.
  • 根域名服务器告诉本地域名服务器,下一次应该查询的顶级域名服务器bilibili.com的IP地址.
  • 本地域名服务器向顶级域名服务器bilibili.com进行查询.
  • 顶级域名服务器bilibili.com告诉本地域名服务器,下一次应该查询的权限域名服务器www.bilibili.com 的IP地址.
  • 本地域名服务器向权限域名服务器www.bilibili.com进行查询.
  • 权限域名服务器www.bilibili.com告诉本地域名服务器,所查询的主机的IP地址.
  • 本地域名服务器最后把查询结果告诉主机.

DNS报文格式

在这里插入图片描述
其中DNS报文的头部一共有6个部分,每个部分占2字节,一共12字节,是固定的.

然后是DNS正文部分的Queries(查询问题区域),该区域包含了客户请求的问题,客户可以一次性提出多个问题.

在这里插入图片描述
Name(域名):该部分的长度不是固定的,且一般采用以下格式(比如我们要查询www.bilibili.com):

3www8bilibili3com

Type(查询类型):

类型助记符说明
1A由域名获得 IPv4 地址
2NS查询域名服务器
5CNAME查询规范名称
6SOA开始授权
11WKS熟知服务
12PTR把 IP 地址转换成域名
13HINFO主机信息
15MX邮件交换
28AAAA由域名获得 IPv6 地址
252AXFR传送整个区的请求
255ANY对所有记录的请求

Class(查询类):

通常为 1,表明是 Internet 数据.

剩下的三个部分依次是回答区管理机构区附加信息区.

UDP编程

读写系统调用

socket编程接口中用于UDP数据报读写的系统调用是:

#include <sys/types.h>
#include <sys/socket.h>

size_t sendto(int sockfd, const void *buf, sizeof_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
size_t recvfrom(int sockfd, void *buf, sizeof_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

sendtosockfd上写入数据,buflen参数分别指定写缓冲区的位置和大小. dest_addr参数指定接收端的socket地址,addrlen 参数则指定该地址的长度.

recvfrom读取sockfd上的数据,buflen参数分别指定读缓冲区的位置和大小. 因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_ addr 所指的内容,addrlen 参数则指定该地址的长度.

创建socket

Unix/Linux的一个哲学是:所有东西都是文件. socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符. 下面是创建socket的系统调用:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain告诉系统使用哪个底层协议族,IPv4 网络协议的套接字类型应该使用AF_INET .
  • type指定服务类型,TCP使用SOCK_STREAM,UDP使用SOCK_DGRAM.
  • protocol是在前两个参数构成的协议集合下,再选择一个具体的协议,通常是唯一的(使用默认协议),设为0.

socket地址

socket地址分为通用socket地址和专用socket地址(Linux特供),我们一般使用Linux提供的专用地址.

其中IPv4的专用地址定义如下:

struct sockaddr_in {
	sa_family_t sin_family; 	/*地址族: AF_INET */
	u_int16_t sin_port;			/*端口号: 要用网络字节序表示*/
	struct in_addr sin_addr;	/*IPv4地址结构体,见下面*/
};
struct in_addr {
	u_int32_t s_addr;			/*IPv4地址,要用网络字节序表示*/
};

网络字节序指的是大端字节序,就是一个整数的高位字节(23-31bit)储存在内存的低地址处,低位字节(0-7bit)储存在内存的高地址处.

我们使用Linux提供的:

#include <netinet/in.h>

unsigned short int htons(unsigned short int hostshort);

来将主机字节序转化为网络字节序.

具体实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>

#define DNS_SERVER_PORT     53
#define DNS_SERVER_IP       "114.114.114.114"

#define DNS_HOST			0x01
#define DNS_CNAME			0x05

// DNS报文头部
struct dns_header {
    unsigned short id;          // 会话标识
    unsigned short flags;       // 标志位

    unsigned short questions;   // 问题数
    unsigned short answer;      // 回答,资源记录数

    unsigned short authority;   // 授权,资源记录数
    unsigned short additional;  // 附加,资源记录数
};

// DNS报文正文
struct dns_question {
    int length;
    unsigned char *name;
    unsigned short qtype;
    unsigned short qclass;
};

// DNS服务器返回的ip信息
struct dns_item {
	char *domain;
	char *ip;
};

// header填充与函数实现
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();

    // 统一转化成网络字节序
    header->flags = htons(0x0100);
    header->questions = htons(1);
    return 0;
}

// question填充与函数实现
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的长度是不固定的,c字符串还要占一个'/0',多开两个空间备用
    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是深拷贝,里面会调用malloc函数,最后要记得释放
    char *hostname_dup = strdup(hostname);
    //分解字符串 hostname_dup 为一组字符串,delim 为分隔符
    char *token = strtok(hostname_dup, delim);

    // 继续分割字符串
    while (token != NULL) {
        // 得到上一次截取的长度
        size_t len = strlen(token);
        // 把长度加到字符串中
        *qname = len;
        // 后移指针
        qname++;
        // 把token放到qname里面,复制长度是len+1,是把结尾的/0也放进去了
        strncpy(qname, token, len + 1);
        qname += len;

        // 从上次结束的地方开始截取
        token = strtok(NULL, delim);
    }

    free(hostname_dup);
}

// 对头部和问题区做一个打包
int dns_build_requestion(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
    memcpy(request + offset, question->name, question->length);
    offset += question->length;

    memcpy(request + offset, &question->qtype, sizeof(question->qtype));
    offset += sizeof(question->qtype);

    memcpy(request + offset, &question->qclass, sizeof(question->qclass));
    offset += sizeof(question->qclass);

    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) {
    printf("domain: %s\n", domain);
    //套接字(socket 文件描述符) AF_INET指 IPv4 SOCK_DGRAM是UDP套接字的名字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        return -1;
    }

    // 创建套接字地址
    struct sockaddr_in servaddr = {0};
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(DNS_SERVER_PORT);
    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};
    dns_create_header(&header);

    struct dns_question question = {0};
    dns_create_question(&question, domain);

    char request[1024] = {0};
    int length = dns_build_requestion(&header, &question, request, 1024);

    // request
    int slen = sendto(sockfd, request, length, 0, (struct sockaddr *)&servaddr, sizeof(struct sockaddr));
    printf("sendto status: %d\n", slen);

    // recvfrom
    char response[1024] = {0};
    struct sockaddr_in addr;
    size_t addr_len = sizeof(struct sockaddr_in);
    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);
    // int i = 0;
    // for (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]);
}

CMakeLists.txt文件

PROJECT(DNS)
ADD_EXECUTABLE(dns dns.c)

运行结果

在这里插入图片描述

;