初级项目_本地DNS服务器

资料参考:通过Wireshark抓包分析谈谈DNS域名解析的那些事儿 - 朱季谦 - 博客园 (cnblogs.com)

源码链接:Gintoki-jpg/Local_DNS_Server: 大二下学期根据老师要求实现一个简易DNS服务器,了解并掌握相关知识点 (github.com)

一、实验准备

1.DNS简介

1.1 DNS服务器

DNS,全称Domain Name System,即域名系统,它提供的作用是将域名和IP地址相互映射。最通俗的理解,它就像是Java里key-value形式的Map,key是域名,value是对应映射的IP地址,通过map.get(域名),可得到域名对应的IP地址。

DNS服务器也是类似域名空间树一样的树结构,依次分为根域名服务器(知道所有的顶级域名服务器的域名和IP,最重要,它要是瘫痪,整个DNS就完蛋),然后是顶级域名服务器(管理二级域名),其次是权限域名服务器(负责区的域名服务器),最后是本地域名服务器(也叫默认域名服务器),本地域名服务器离主机很近(书上说不超过几个路由器),速度很快,其实本地域名服务器本质不属于域名服务器架构。

DNS实际上是由一个分层的DNS服务器实现的分布式数据库和一个让主机能够查询分布式数据库的应用层协议组成。因此,要了解DNS的工作原理,需要从以上两个方面入手:

  • 在实际工作中,DNS服务器是带缓存的。即DNS服务器在每次收到DNS请求时,都会先查询自身数据库包括缓存中有无要查询的主机名的ip,若有且没有过期,则直接响应该ip,否则才会按流程进行查询;而服务器在每次收到响应信息后,都会将响应信息缓存起来;
  • 若在最近的DNS服务器上,无法解析到域名对应的IP地址时,那么最近的DNS服务器就会类似充当一个中介角色,帮助客户端去其他DNS服务器寻找,看看哪台DNS服务器上可以找到该域名对应的IP;
  • 任何一台DNS服务器,都存储了根域名的IP地址,根域名服务器不做解析,更像是一位指路人;

1.2 查询方式

下面是DNS常用的两种查询方式,具体选择哪个可以自己决定:

DNS递归查询:如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,那么本地域名服务器就以DNS客户端的身份(递归思想),向根域名服务器继续发出查询报文(替主机查询),不让主机自己进行查询。递归查询返回的结果或者是IP,或者报错。这是从上到下的递归查询过程

DNS迭代+递归查询:当根域名服务器收到本地域名服务器的查询请求,要么给出ip,要么通知本地域名服务器下一步应该去请求哪一个顶级域名服务器查询(并告知本地域名服务器自己知道的顶级域名的IP),让本地域名服务器继续查询,而不是替他查询。同理,顶级域名服务器无法返回IP的时候,也会通知本地域名服务器下一步向谁查询(查询哪一个权限域名服务器)……这是一个迭代过程。

1.3 DNS报文格式

DNS请求与响应的格式是一致的,其整体分为Header、Question、Answer、Authority、Additional5部分

我们也可以将DNS简单分为基础结构部分(Header)、问题部分(Question)、资源记录部分(3A)

a)Header部分

b)Question部分

Question部分的每一个实体的格式如下

c)Answer、Authority、Additional部分

Answer、Authority、Additional部分格式一致,每部分都由若干实体组成,每个实体即为一条RR

1.4 ID映射表

程序需要考虑如下两个问题:

  • 多客户端并发:允许多个客户端(可能会位于不同的多个计算机)的并发查询,即:允许第一个查询尚未得到答案前就启动处理另外一个客户端查询请求(DNS报头中ID字段的作用)
  • 超时处理:由于UDP的不可靠性,考虑求助外部DNS服务器(中继)却不能得到应答或者收到迟到应答的情形

对于第一个问题,UDP本身支持并发,所以只需要实现ID转换的功能即可

我们只需要借助ID转换表即可实现上述功能

2.域名解析的步骤

  1. 首先,会根据域名从浏览器缓存当中获取,若能获取到,直接返回对应的IP地址;若获取失败,会尝试获取操作系统本地的域名解析系统,即在hosts文件检查是否有对应的域名映射,若能找到,直接获取其映射的IP地址返回(在hosts文件里存储的域名与IP地址映射,一般都是针对IP比较稳定且经常用的,例如工作当中的一些线上开发环境或者测试环境等域名,如果是IP变化比较频繁或者是根本就不知道IP是啥的,这类情况就无法通过hosts文件进行配置获取,只能通过网络访问DNS服务器去获取)
  2. 通过网络访问DNS服务器的方式获取IP首先会先去本地区域的DNS服务器找(即PC网络设置中配置的DNS服务器);
  3. 理论上,若在最近的DNS服务器上,无法解析到域名对应的IP地址时,那么最近的DNS服务器就会类似充当一个中介角色,帮助客户端去其他DNS服务器寻找,看看哪台DNS服务器上可以找到该域名对应的IP;

3.实验要求

设计一个DNS服务器程序,读入“域名-IP地址”对照表,当客户端查询域名对应的IP地址时,用域名检索该对照表,实现下列三种情况:

  • 检索结果为普通IP地址,则向客户返回这个地址(即DNS服务器功能)

  • 检索结果为IP地址0.0.0.0,则向客户端返回“域名不存在”的报错消息(即不良网站拦截功能)

  • 表中未检到该域名,则向实际的本地DNS服务器发出查询,并将结果返给客户端(即DNS中继功能)

注意:应考虑多个计算机上的客户端同时查询的情况,需要进行消息ID的转换

二、代码剖析

这部分直接分析c代码,头文件放在最后说或者直接忽略,主要掌握每个函数的作用以及DNS服务器整体的运作流程;

参考资料(18条消息) MAKEWORD(2,2)使用_bingqingsuimeng的博客-CSDN博客

首先是main函数入口,主要进行了服务器初始化操作以及启动服务器持续监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "handle_message.h"

//条件编译实现linux和win下的编译
#ifdef _WIN32
extern SOCKET my_socket; // 套接字外部引用声明
#elif __linux__
extern int my_socket;
#endif

int main(int argc, char* argv[])
{
// 处理shell命令,初始化调试信息、外部DNS服务器IP地址以及配置文件路径
handle_arguments(argc, argv);
// 创建dns_table表
initTable();
// 读取配置文件dnsrelay.txt,存入内存中的url-ip映射表中
insert_to_inter();
// 初始化id表
initialize_id_table();
#ifdef _WIN32
//声明结构体,WSAData功能是存放windows socket初始化信息
struct WSAData wsaData;
// 调用WSAStartup函数,启用SOCKET的动态链接库,连接应用程序与winsock.dll;
//第一个参数是WINSOCK版本号,第二个参数是指向WSADATA的指针.该函数返回一个INT型值,通过检查这个值来确定初始化是否成功;
//wsaData用来存储系统传回的关于WINSOCK的资料;
WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
// 初始化套接字
initialize_socket();
// 声明缓冲区,这个缓冲区通于缓存来自客户端和来自外部服务器的报文
char buf[BUFFER_SIZE];
// 初始化一个常用地址结构体SOCKADDR_IN(注意通用地址结构体为sockaddr)
struct sockaddr_in tmp_sockaddr;
// 声明报文的长度length
int length;
// 常用地址结构体长度
int sockaddr_in_size = sizeof(struct sockaddr_in);
// 无限循环实现服务器持续监听
while (1)
{
// 清空本地服务器的缓冲区
memset(buf, '\0', BUFFER_SIZE);
length = -1; // 重置数据报长度

// 服务器将会一致阻塞直到收到数据
// 将recvfrom函数的返回值用length接收
length = recvfrom(my_socket, buf, sizeof(buf), 0, (struct sockaddr*)&tmp_sockaddr, &sockaddr_in_size);
//recvfrom()函数:接收一个数据报,将数据存至buf中,并保存源地址
/*
local_socket:已连接的本地套接口
buf:接收数据缓冲区
sizeof(buf):接收缓冲区大小
0:标志位flag,表示调用操作方式,默认设为0
client:捕获到的数据发送源地址(Socket地址)
sockaddr_in_size:地址长度
返回值:recvLength:成功接收到的数据的字符数(长度),接收失败返回SOCKET_ERROR(-1)
*/

if (length > 0)// 数据报的长度是正数表正确收到了数据
{
// 若该数据报的源地址端口号为UDP 53可以确定这是来自DNS服务器的数据报
if (tmp_sockaddr.sin_port == htons(53))
{
// 接收外部服务器报文并进行处理
handle_server_message(buf, length, tmp_sockaddr);
}
//否则认为这是本地客户端(Shell、浏览器解析器等)发送的报文(客户端的端口号是随机分配的)
else
{
// 接收本地客户端报文并进行处理
handle_client_message(buf, length, tmp_sockaddr);
}
}
}
//回收内存
deleteTable();
return 0;
}

C程序按照从上到下的默认顺序进行编译,所以我们参照编译器的思维方式逐个分析main函数中使用到的函数层层深入

1.-handle_arguments()

处理⽤户shell窗口输⼊,根据参数配置本地DNS服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//传入参数分别是argument count以及argument vector,即命令行参数个数和存放命令行字符串参数的指针数组
//argv[0] 指向程序运行的全路径名,argv[1] 指向在DOS命令行中执行程序名后的第一个字符串,argv[2] 指向执行程序名后的第二个字符串,argv[argc]为NULL
void handle_arguments(int argc, char* argv[])
{
int is_assigned = 0; // 默认用户使用默认DNS配置
if (argc > 1 && argv[1][0] == '-')
{
if (argv[1][1] == 'd')
{
debug_level = 1; // 调试等级:1
}
if (argv[1][2] == 'd')
{
debug_level = 2; // 调试等级:2
}
// 若参数数量大于2,即用户还指定了外部DNS服务器的IP地址
if (argc > 2)
{
is_assigned = 1; // 标记用户进行了设置,并未使用默认DNS
//strcpy() 函数用来复制字符串,strncpy()用来复制字符串的前n个字符,成功执行后返回目标数组指针 dest
strcpy(server_ip, argv[2]); // 使用指定外部DNS服务器IP地址
printf("指定DNS服务器的IP地址: %s\n", server_ip);
if (argc == 4) // 不仅指定了外部DNS服务器的IP地址,还指定了配置文件的路径
{
strcpy(file_path, argv[3]);
}
}
}

if (!is_assigned) // 未指定外部服务器,则使用默认服务器
{
printf("使用默认DNS服务器IP地址: %s \n", server_ip);
}
printf("debug=%d\n", debug_level);
}

2.+initTable()

初始化“域名-IP 地址”对照表table内存以及cache高速缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void initTable() {
// 为table表头分配空间
table = (Node*)malloc(sizeof(Node));
memset(table, 0, sizeof(Node));
INIT_LIST_HEAD(&table->list);

// 为cache表头分配空间
cache = (Node*)malloc(sizeof(Node));
memset(cache, 0, sizeof(Node));
INIT_LIST_HEAD(&cache->list);

// 为字典树表头分配空间
tableTrie = (TrieNode*)malloc(sizeof(TrieNode));
memset(tableTrie, 0, sizeof(TrieNode));
cacheTrie = (TrieNode*)malloc(sizeof(TrieNode));
memset(cacheTrie, 0, sizeof(TrieNode));
lastFlashTime = time(NULL);

cache_size = 0;
printf("初始化“域名-IP 地址”对照表完成。\n");
}

3.+insert_to_inter()

启动DNS服务器时,读取txt文件中的url-ip映射数据,并将这些数据加入到table中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void insert_to_inter()
{
FILE* file;
if ((file = fopen(file_path, "r")) == NULL) //从file_path读取文件失败
{
printf("读取配置文件失败\n");
return;
}

char url[100] = "", ip[16] = "";
printf("加载配置文件成功,结果如下:\n");
while (fscanf(file, "%s %s", ip, url) > 0)
{
printf("<域名: %s, IP地址 : %s>\n", url, ip);
addDataToTable(url, ip, 0); // 添加到DNS_table中
}
fclose(file);
}

4.*initialize_id_table()

初始化ID表,这部分和内存机制没什么关系,与报文处理相关;

1
2
3
4
5
6
7
8
9
10
void initialize_id_table()
{
for (int i = 0; i < ID_TABLE_SIZE; i++)
{//因为ID映射表是一个结构体,所以需要递归索引进行初始化
ID_table[i].client_id = 0;
ID_table[i].finished = 1;
ID_table[i].survival_time = 0;
memset(&(ID_table[i].client_addr), 0, sizeof(struct sockaddr_in));
}
}

前面介绍过,ID表的图示如下(这只是其中一个结构体,本质上ID表是一个结构体数组)

ID表的结构定义如下

1
2
3
4
5
6
7
typedef struct
{
unsigned short client_id; // 客户端发给本地DNS服务器的请求报文中的ID
struct sockaddr_in client_addr; // 客户端地址结构体
int survival_time; // id表存活时间
int finished; // 本次查询请求是否完成
}id_table; // id表(结构体)

5.-initialize_socket()

标准的socket网络编程步骤

初始化之前先声明两个常用地址结构体,网络编程中IP地址和端口号等都是封装在一个结构体当中的;

1
2
struct sockaddr_in client_addr;  // 客户端套接字地址结构体                
struct sockaddr_in server_addr; // 服务器套接字地址结构体

初始化操作实际就是进行一些绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void initialize_socket()
{
my_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//若创建套接字失败,则退出程序
if (my_socket < 0)
{
printf("socket连接建立失败!\n");
exit(1);
}
printf("socket连接建立成功!\n");

client_addr.sin_family = AF_INET; //IPv4
client_addr.sin_addr.s_addr = INADDR_ANY; //客户端随机选取网卡的ip地址
client_addr.sin_port = htons(53); //作为客户端绑定到53端口,注意这里不是指PC发消息的客户端(浏览器解析器),那个我们没办法指定,而是本地DNS作为客户端时使用的端口

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);//绑定到外部服务器ip即之前我们指定的192.168.3.1,否则用默认的
server_addr.sin_port = htons(53);

/*bian()预处理:本地端口号,通用端口号,允许复用 */
int reuse = 0;
setsockopt(my_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse));

//将端口号与socket关联
if (bind(my_socket, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0)
{
printf("socket端口绑定失败!\n");
exit(1);
}
printf("socket端口绑定成功!\n");
}

6.*handle_server_message()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
void handle_server_message(char* buf, int length, struct sockaddr_in server_addr)
{
count_s++; // 外部DNS响应与本地客户端请求对应
char url[200]; // 声明用于保存url的数组
if (debug_level)
{
print_debug_time();
printf("\n#%d:收到来自外部服务器的响应", count_s);
if (debug_level == 2) {
// 显示完整响应报文
show_message(buf, length);
}
}

// 1.解析第一部分,Header头部
unsigned short ID;
// 从接收报文(buf区中)中获取ID号
memcpy(&ID, buf, sizeof(unsigned short));
int cur_id = ID - 1;

//从ID映射表查找原来的ID号
// ID映射表中的old_id字段就是原来本地客户端的id号
memcpy(buf, &ID_table[cur_id].client_id, sizeof(unsigned short));
// 将id映射表中的状态改了,表明该次向外部服务器发送的请求成功
ID_table[cur_id].finished = 1;
// 获取old_id映射表中的client_addr字段,也就是本地客户端的地址结构体,后续与它进行通信
struct sockaddr_in client_temp = ID_table[cur_id].client_addr;
// QDCOUNT字段在第四个字节开始第六个字节结束,表示Question实体的数量,buf仅仅是缓冲数组的首地址
int num_query = ntohs(*((unsigned short*)(buf + 4)));
// ANCOUNT字段在第六个字节开始第八个字节结束,表示Answer实体的数量
int num_response = ntohs(*((unsigned short*)(buf + 6)));
// 移动指针,令p指向Question实体(12个字节的Header后面就是Question)
char* p = buf + 12;

// 2.解析第二部分,Question
for (int i = 0; i < num_query; i++)
// 将QUESTION的num_query个实体的url全部转换为正常的url
{
// 将字符计数法表示的的域名转换为正常形式的域名
url_transform(p, url);

while (*p > 0)
{
p += (*p) + 1; // 直到*p=0即指向字符串结尾\0
}
p += 5; // p=p+sizeof(char)*5(1字节\0,2字节QTYPE,2字节QCLASS),p指向下一个QUESTION实体

}

if (num_response > 0 && debug_level == 2)
{
// 这个url就是我们需要查询的url
printf("\n#%d:外部DNS服务器查询<URL:%s>成功,解析结果如下:", count_s, url);
}
if (num_response == 0 && debug_level == 2)
{
printf("\n#%d:外部DNS服务器查询<URL:%s>失败", count_s, url);
}

// 3.解析第三部分,分析来自外部DNS服务器的ANSWER,其中ANSWER|AUTHORITY|ADDITIONAL的实体均为RR(这里只分析ANSWER)
for (int i = 0; i < num_response; ++i)
{
// 分析所有的ANSWER
// NAME字段使用指针偏移表示,只占两个字节(规定)
if ((unsigned char)*p == 0xc0)
{
p += 2;
}
else
{
while (*p > 0)
{
p += (*p) + 1;
}
++p;
}

// 获取ANSWER资源记录域(RR)的各个字段(不需要获取NAME,因为NAME就是前面获取的url)
unsigned short type = ntohs(*(unsigned short*)p); // TYPE字段
p += sizeof(unsigned short);
unsigned short _class = ntohs(*(unsigned short*)p); // CLASS字段
p += sizeof(unsigned short);
unsigned short ttl_high_byte = ntohs(*(unsigned short*)p); // TTL高位字节
p += sizeof(unsigned short);
unsigned short ttl_low_byte = ntohs(*(unsigned short*)p); // TTL低位字节
p += sizeof(unsigned short);
// 将TTL高位和低位合并为int类型数据
int ttl = (((int)ttl_high_byte) << 16) | ttl_low_byte;
// 前面几个字段(TYPE CLASS TTL)的长度,此处令人混淆,并不代表DATALENGTH
int rdlength = ntohs(*(unsigned short*)p);
// TYPE A的资源数据Value是4字节的IP地址,即RDATA字段的字节数为4,现在p指向DATA字段
p += sizeof(unsigned short);
// 调试等级为2输出冗余信息
if (debug_level == 2)
{
printf("\n#%d:收到的响应报文属性 TYPE: %d, CLASS: %d, TTL: %d", count_s, type, _class, ttl);
}
char ip[16];
int ip1, ip2, ip3, ip4; // 因为每个ip字段占一个字节,所以我们需要用四个ip段来接收,最后拼接

// TYPE=1表示TYPE A,TYPE=5表示TYPE CNAME
if (type == 1)
{
ip1 = (unsigned char)*p++;
ip2 = (unsigned char)*p++;
ip3 = (unsigned char)*p++;
ip4 = (unsigned char)*p++;
sprintf(ip, "%d.%d.%d.%d", ip1, ip2, ip3, ip4);

if (debug_level == 2)
{
printf("\n#%d:IPV4: %d.%d.%d.%d", count_s, ip1, ip2, ip3, ip4);
}

addDataToTable(url, ip, 1);
break;
}
// 若TYPE不为A则直接跳过获取DATAip的步骤
else
{
p += rdlength;
}
}
// 将报文发送给客户端
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&client_temp, sizeof(client_temp));
// 响应消息
if (debug_level == 2)
{
printf("\n#%d:成功向本地客户端转发响应", count_s);
}
}

7.*handle_client_message()

下面这两个部分应该是核心代码部分了,所以会分篇幅详细介绍,这里先简单展示一下相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
void handle_client_message(char* buf, int length, struct sockaddr_in client_addr)
{

char old_url[200]; // 转换前的url
char new_url[200]; // 转换后的url
char ip[100];
char inter_result[200];
char cache_result[200];
char sendbuf[BUFFER_SIZE];
int pos = 0;
char res_rec[16];
int point = 0;
count_c++;

if (debug_level)
{
print_debug_time();
printf("\n#%d:收到来自本地客户端的请求", count_c);
if (debug_level == 2) {
show_message(buf, length); // 显示完整报文
}
}

// 从请求报文的QNAME中获得url,HEADER长度为12字节,故直接访问buf数组的第12个元素即QNAME字段
memcpy(old_url, &(buf[12]), length);
// 将请求报文中的字符串格式的url转换成普通模式的url
url_transform(old_url, new_url);


// 这一步是判断客户端发来的是IPV6报文还是IPV4报文,请求IPV6的报文只能中继给外部的服务器处理
while (*(buf + 12 + point)) // QNAME以0X00为结束符
{
point++;
}
point++; // 跳过长度为i的QNAME,现在buf+12+i代表的就是QTYPE
int getResult;
char* temip = ip;
getResult = getData(new_url, &temip);
// 若QTYPE字段非空
if (buf + 12 + point != NULL)
{
unsigned short type = ntohs(*(unsigned short*)(buf + 12 + point)); // 获取TYPE字段

if (type == 28 && strcmp(ip, "0.0.0.0") != 0)
{
if (debug_level == 2) {
printf("\n#%d:客户端发出的IPV6请求,将向外部服务器寻求帮助", count_c, new_url);
}
// 声明ID标识符(用于处理和id有关的信息)
unsigned short ID;
// 获取客户端的id(因为是第一个信息直接获取buf即可)
memcpy(&ID, buf, sizeof(unsigned short));
// 将原id存入id映射表并获得新的id(为了后续服务器代理功能准备-这个顺序就该放在后面)
unsigned short new_id = id_transform(ID, client_addr, 0);

if (new_id == 0)
{
if (debug_level == 2)
{
printf("\n#%d:向外部服务器发送请求失败,本地ID转换表已满!", count_c);
}
}
// 假设请求到了新的id,注意这个id是位置+1得到的
else
{
memcpy(buf, &new_id, sizeof(unsigned short)); // 将新id加入缓冲区
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 将域名查询请求发送至外部dns服务器
if (debug_level == 2)
{
printf("\n#%d:成功向外部服务器发送请求", count_c);

}
}
return;
}

}

// 从本地(dns_table或者chache)中查询是否有该记录
// 若域名在dns_table、cache中均无法找到,需要向外部dns服务器请求帮助
if (getResult == 0)
{
if (debug_level == 2) {
printf("\n#%d:本地服务器查询<URL:%s>失败,即将向外部服务器发送请求", count_c, new_url);
}
unsigned short ID;
memcpy(&ID, buf, sizeof(unsigned short));
// 将原客户端id进行转换得到新本地服务器id
unsigned short new_id = id_transform(ID, client_addr, 0);
// 假如id转换失败则报错并退出
if (new_id == 0)
{
if (debug_level == 2)
{
printf("\n#%d:向外部服务器发送请求失败,本地ID转换表已满!", count_c);
}
}
// 将域名查询请求发送至外部dns服务器
else
{
memcpy(buf, &new_id, sizeof(unsigned short));
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (debug_level == 2)
{
printf("\n#%d:成功向外部服务器发送请求,需要查询的域名为<URL:%s>", count_c, new_url);
}
}
}
// 若在本地查询到了域名和ip的映射
else
{
// 如果是从dns_table中查到
if (getResult == 1)
{
count_s++;
if (debug_level == 2) {
printf("\n#%d:从内存中查到<URL:%s>对应的IP,结果如下:\n", count_c, new_url);
printf("<URL: %s , IP: %s>", new_url, ip);

}
}
// 如果是从cache中查到
else
{
count_s++; // 为了保持请求和响应同步

if (debug_level == 2)
{
printf("\n#%d:从cache中查到<URL:%s>对应的IP,结果如下:\n", count_c, new_url);
printf("<URL: %s , IP: %s>", new_url, ip);

}
}

if (debug_level == 2)
{
printf("\n#%d:构造并发送响应报文", count_c);

}
memcpy(sendbuf, buf, length); // 构造响应报文
// 1.构造头部
unsigned short num;
if (strcmp(ip, "0.0.0.0") == 0) // 判断此ip是否应该被墙
{
num = htons(0x8183);
memcpy(&sendbuf[2], &num, sizeof(unsigned short));
}
else
{
num = htons(0x8180);
memcpy(&sendbuf[2], &num, sizeof(unsigned short));
}

if (strcmp(ip, "0.0.0.0") == 0) // 此ip是否合法
{

num = htons(0x0); // 假如非法直接不回答
}
else
{
num = htons(0x1); // 合法则回答数为1
}
memcpy(&sendbuf[6], &num, sizeof(unsigned short));
// 2.构造RR资源记录
unsigned short name = htons(0xc00c);
memcpy(res_rec, &name, sizeof(unsigned short));
pos += sizeof(unsigned short);

// TYPE字段
unsigned short type = htons(0x0001);
memcpy(res_rec + pos, &type, sizeof(unsigned short));
pos += sizeof(unsigned short);

// CLASS字段
unsigned short _class = htons(0x0001);
memcpy(res_rec + pos, &_class, sizeof(unsigned short));
pos += sizeof(unsigned short);

// TTL字段
unsigned long ttl = htonl(0x00000080);
memcpy(res_rec + pos, &ttl, sizeof(unsigned long));
pos += sizeof(unsigned long);

// RDLENGTH字段
unsigned short RDlength = htons(0x0004);
memcpy(res_rec + pos, &RDlength, sizeof(unsigned short));
pos += sizeof(unsigned short);

// RDATA字段
unsigned long IP = (unsigned long)inet_addr(ip);
memcpy(res_rec + pos, &IP, sizeof(unsigned long));
pos += sizeof(unsigned long);
pos += length;

// 将HEADER和RR共同组成的DNS响应报文存入sendbuf准备发送(此处不考虑QUESTION部分)
memcpy(sendbuf + length, res_rec, sizeof(res_rec));
// 将构造好的报文段发给客户端
length = sendto(my_socket, sendbuf, pos, 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
if (length < 0 && debug_level == 2)
{
printf("\n#%d:向客户端发送响应失败", count_c);
}
if (debug_level == 2)
{
printf("\n#%d:成功向本地客户端发送响应", count_c);
}

}

}

三、核心部分详解

1.处理来自服务器的消息

疑问:是否添加了相关的函数能够缓存来自外部服务器的响应的URL-IP映射?

有的,addDataToTable(url, ip, 1);

1.1 解析Header头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned short ID;
// 从接收报文(buf区中)中获取ID号,因为ID是前16bit故直接获取即可
//void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制(并非覆盖) n 个字节到存储区 str1
memcpy(&ID, buf, sizeof(unsigned short));
//ID逆转换
int cur_id = ID - 1;

//从ID映射表查找原来的ID号
// ID映射表中的old_id/client_id字段就是原来本地客户端的id号,我们将该id存放在buf区中
memcpy(buf, &ID_table[cur_id].client_id, sizeof(unsigned short));
// 将id映射表中的状态改了,表明该次向外部服务器发送的请求成功
ID_table[cur_id].finished = 1;
// 获取old_id映射表中的client_addr字段,也就是本地客户端的地址结构体,后续将与它进行通信
struct sockaddr_in client_temp = ID_table[cur_id].client_addr;

// QDCOUNT字段在第四个字节开始第六个字节结束,表示Question实体的数量,buf是缓冲数组的首地址
int num_query = ntohs(*((unsigned short*)(buf + 4)));
// ANCOUNT字段在第六个字节开始第八个字节结束,表示Answer实体的数量
int num_response = ntohs(*((unsigned short*)(buf + 6)));
// 移动指针,令p指向Question实体(12个字节的Header后面就是Question)
char* p = buf + 12;

1.2 解析Question实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将QUESTION的num_query个实体的url全部转换为正常的url
for (int i = 0; i < num_query; i++)
{
// 将字符计数法表示的的域名转换为正常形式的域名
url_transform(p, url);

while (*p > 0)
{
p += (*p) + 1; // 直到*p=0即指向字符串结尾\0
}
p += 5; // p=p+sizeof(char)*5(1字节"\0",2字节QTYPE,2字节QCLASS),即移动p使其指向下一个QUESTION实体(一般只存在一个QUESTION实体)
}

if (num_response > 0 && debug_level == 2)
{
// 这个url就是我们需要查询的url
printf("\n#%d:外部DNS服务器查询<URL:%s>成功,解析结果如下:", count_s, url);
}

if (num_response == 0 && debug_level == 2)
{
printf("\n#%d:外部DNS服务器查询<URL:%s>失败", count_s, url);
}

1.2.1 URL转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

//url转换函数,调用一次该函数将一个QNAME转换为一个URL
void url_transform(char* str, char* result)
{
int i = 0, j = 0, k = 0, len = strlen(str);
while (i < len)
{
if (str[i] > 0 && str[i] <= 63) //ASCLL1-63表特殊符号以及数字1~9,即直接跳过十六进制前缀0x以及0找到长度标识,记作j
{
for (j = str[i], i++; j > 0; j--, i++, k++) //将字符计数法中的url提取出来放入result中,同时移动str、result数组下标
{
result[k] = str[i]; //result[0]=str[2],result[1]=str[3],result[2]=str[4]
}
}
if (str[i] != 0) //上一轮循环结束,得到字符串api,若str[5]不是0(也就是"."),在result数组中添加"."作为分隔符
{
result[k] = '.';
k++;
}
}
result[k] = '\0'; //给转换完成的url添加结束符构成完整的字符串,我们得到最终URL"api.sina.com.cn\0"
}

1.3 解析ANSWER实体

分析来自外部DNS服务器的ANSWER,其中ANSWER|AUTHORITY|ADDITIONAL的实体均为RR(这里规定只分析ANSWER即可,因为AUTHORITY是权威/域服务器的记录,ADDITIONAL是可以被使用的附加有用信息,这些现阶段来说没用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 分析所有的ANSWER	
for (int i = 0; i < num_response; ++i)
{
//此处移动P指针使其指向TYPE字段
if ((unsigned char)*p == 0xc0)// NAME字段通常使用指针偏移表示,只占两个字节(规定);
{
p += 2;
}
else
{
while (*p > 0)
{
p += (*p) + 1;//移动P指针直到指向"\0"
}
++p; //"\0"占一个字节
}

// 获取ANSWER资源记录域(RR)的各个字段(不需要重复获取NAME,因为NAME就是前面获取的url)
unsigned short type = ntohs(*(unsigned short*)p); // TYPE字段
p += sizeof(unsigned short); //P指针移动两个字节到CLASS字段
unsigned short _class = ntohs(*(unsigned short*)p); // CLASS字段
p += sizeof(unsigned short);
unsigned short ttl_high_byte = ntohs(*(unsigned short*)p); // TTL高位字节
p += sizeof(unsigned short);
unsigned short ttl_low_byte = ntohs(*(unsigned short*)p); // TTL低位字节
p += sizeof(unsigned short);
// 将TTL高位和低位通过位运算合并为int类型数据
int ttl = (((int)ttl_high_byte) << 16) | ttl_low_byte;
// 现在P指向RDLENGTH字段,rdlength就是RDATA的字节数
int rdlength = ntohs(*(unsigned short*)p);
// TYPE A对应的资源数据RDATA是4字节的IP地址,即RDATA字段的字节数为4,现在p指向DATA字段
p += sizeof(unsigned short);
// 调试等级为2输出冗余信息
if (debug_level == 2)
{
printf("\n#%d:收到的响应报文属性 TYPE: %d, CLASS: %d, TTL: %d", count_s, type, _class, ttl);
}
char ip[16];
int ip1, ip2, ip3, ip4; // 因为每个ip字段占一个字节,所以我们需要用四个ip段来接收,最后拼接

// TYPE=1表示TYPE A,TYPE=5表示TYPE CNAME,这里只分析TYPE=A的情况
if (type == 1)
{
ip1 = (unsigned char)*p++;//p是char类型,依次移动一个字节向下读取
ip2 = (unsigned char)*p++;
ip3 = (unsigned char)*p++;
ip4 = (unsigned char)*p++;
sprintf(ip, "%d.%d.%d.%d", ip1, ip2, ip3, ip4);

if (debug_level == 2)
{
printf("\n#%d:IPV4: %d.%d.%d.%d", count_s, ip1, ip2, ip3, ip4);
}

addDataToTable(url, ip, 1);
break;
}
// 若TYPE不为A则直接跳过获取RDATA中ip的步骤
else
{
p += rdlength;//根据rdlength标识的长度直接跳过RDATA字段
}
}

1.4 转发报文

本地DNS服务器需要将收到的来自外部服务器的响应转发给对应的DNS客户端(代理作用的体现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将报文发送给客户端
//sendto()函数:发送一个数据报,数据在buf中,需要提供地址
/*
local_socket:外部服务器套接字
buf:发送数据缓冲区
sizeof(buf):接收缓冲区大小
0:标志位flag,表示调用操作方式,默认设为0
client:目标数据发送源地址
sizeof(client):地址大小
返回值:length:成功接收到的数据的字符数(长度),接收失败返回SOCKET_ERROR(-1)
*/
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&client_temp, sizeof(client_temp));
// 响应消息
if (debug_level == 2)
{
printf("\n#%d:成功向本地客户端转发响应", count_s);
}
}

2.处理来自客户端的消息

这里我们还是忽略一些变量的定义,主要抽象的介绍以下本地DNS服务器如何处理来自客户端(Shell、浏览器)的请求

2.1 解析Question实体

下面直接略过请求的Header(没什么意义),获取Question中有价值的信息

1
2
3
4
// 1.从请求报文的QNAME中获得url,HEADER长度为12字节,故直接访问buf数组的第12个元素即QNAME字段
memcpy(old_url, &(buf[12]), length);
// 2.将请求报文中的字符串格式的url转换成普通模式的url
url_transform(old_url, new_url);

2.2 判断报文类型

判断客户端发来的是IPV6报文还是IPV4报文,请求IPV6的报文只能中继给外部的服务器处理(咱们的DNS只做了处理IPV4的功能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
while (*(buf + 12 + point))  // QNAME以0X00为结束符
{
point++;
}
point++; // QNAME长度为point,则buf+12+point指向的就是QTYPE
int getResult;
char* temip = ip;
//调用接口函数获取ip(这个函数必须在这个时候调,因为接下来就涉及判断是否是"0.0.0.0")
getResult = getData(new_url, &temip);
// 若QTYPE字段非空
if (buf + 12 + point != NULL)
{
unsigned short type = ntohs(*(unsigned short*)(buf + 12 + point)); // 获取TYPE字段
//type=28表明这是IPV6的请求,同时如果这个URL没有被屏蔽
if (type == 28 && strcmp(ip, "0.0.0.0") != 0)
{
if (debug_level == 2) {
printf("\n#%d:客户端发出的IPV6请求,将向外部服务器寻求帮助", count_c, new_url);
}
// 声明ID标识符(用于处理和id有关的信息)
unsigned short ID;
// 获取客户端的id(ID位于BUF的前两个字节)
memcpy(&ID, buf, sizeof(unsigned short));
// 将原id存入id映射表并获得新的id(为了后续服务器代理功能准备)
unsigned short new_id = id_transform(ID, client_addr, 0);
//假如返回新id失败说明ID表满了
if (new_id == 0)
{
if (debug_level == 2)
{
printf("\n#%d:向外部服务器发送请求失败,本地ID转换表已满!", count_c);
}
}
// 假设请求到了新的id,注意这个id是位置+1得到的,这样处理是根据讲义来的(原理不是很清楚)
else
{ //代理功能的体现
memcpy(buf, &new_id, sizeof(unsigned short)); // 将新id加入缓冲区
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 将域名查询请求发送至外部dns服务器
if (debug_level == 2)
{
printf("\n#%d:成功向外部服务器发送请求", count_c);

}
}
return;
}

}

2.2.1 ID转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ID转换函数
unsigned short id_transform(unsigned short ID, struct sockaddr_in client_addr, int finished)
{
int i = 0;
for (i = 0; i != ID_TABLE_SIZE; ++i)//只有当id转换表没有满的时候才能存入客户端id
{
//存入客户端的之前,需要先找到一个合适的位置(假如找不到合适的位置就一直循环就行了慢慢找,因为ID表不满所以肯定找得到一个空位)
if ((ID_table[i].survival_time > 0 && ID_table[i].survival_time < time(NULL))
|| ID_table[i].finished == 1) //合适的位置:指超时失效或者已完成的位置
{
if (ID_table[i].client_id != i + 1) //确保新旧ID不同,所以+1处理
{
ID_table[i].client_id = ID; //保存ID
ID_table[i].client_addr = client_addr; //保存客户端套接字
ID_table[i].finished = finished; //标记该客户端的请求是否查询已经完成
ID_table[i].survival_time = time(NULL) + 5; //生存时间设置为5秒
break;
}
}
}
if (i == ID_TABLE_SIZE) //登记失败,当前id转换表已满
{
return 0;
}
return (unsigned short)i + 1; //返回ID号,注意这个ID号是+1处理的(讲义规定)
}

2.3 本地查询

本地查询是指在Cache或Table中查找需要的URL-IP映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 从本地(dns_table或者chache)中查询是否有该记录
// 1.若域名在dns_table、cache中均无法找到,需要向外部dns服务器请求帮助,这一步和上面IPV6的处理是一样的
if (getResult == 0)
{
if (debug_level == 2) {
printf("\n#%d:本地服务器查询<URL:%s>失败,即将向外部服务器发送请求", count_c, new_url);
}
unsigned short ID;
memcpy(&ID, buf, sizeof(unsigned short));
// 将原客户端id进行转换得到新本地服务器id
unsigned short new_id = id_transform(ID, client_addr, 0);
// 假如id转换失败则报错并退出
if (new_id == 0)
{
if (debug_level == 2)
{
printf("\n#%d:向外部服务器发送请求失败,本地ID转换表已满!", count_c);
}
}
// 将域名查询请求发送至外部dns服务器
else
{
memcpy(buf, &new_id, sizeof(unsigned short));
length = sendto(my_socket, buf, length, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (debug_level == 2)
{
printf("\n#%d:成功向外部服务器发送请求,需要查询的域名为<URL:%s>", count_c, new_url);
}
}
}
// 2.若在本地查询到了域名和ip的映射
else
{
// 2.1 如果是从dns_table中查到(函数中已经封装好了将table中查到的数据加入到cache中的流程)
if (getResult == 1)
{
count_s++;
if (debug_level == 2) {
printf("\n#%d:从内存中查到<URL:%s>对应的IP,结果如下:\n", count_c, new_url);
printf("<URL: %s , IP: %s>", new_url, ip);

}
}
// 2.2 如果是从cache中查到
else
{
count_s++; // 为了保持请求和响应同步

if (debug_level == 2)
{
printf("\n#%d:从cache中查到<URL:%s>对应的IP,结果如下:\n", count_c, new_url);
printf("<URL: %s , IP: %s>", new_url, ip);

}
}

2.4 *构造响应报文

Header首部包含事物ID、标志等字段

其中标志字段又分为如下若干字段

QR:响应报文值为1

……

rcode(Reply code):返回码字段,表示响应的差错状态。

  • 当值为 0 时,表示没有错误;

  • 当值为 1 时,表示报文格式错误(Format error),服务器不能理解请求的报文;

  • 当值为 2 时,表示域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;

  • 当值为 3 时,表示名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;

  • 当值为 4 时,表示查询类型不支持(Not Implemented),即域名服务器不支持查询类型;

  • 当值为 5 时,表示拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答;

当然这一步是基于若在本地查询到URL-IP映射,要是本地查不到就参照1所述中转外部DNS的响应即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
		memcpy(sendbuf, buf, length);  // 构造响应报文,先创建一个发送缓冲区缓存要发送的数据
// 1.构造头部Header
unsigned short num;
// 1.1 判断此ip是否应该被墙
//DNS报文的报头中存在一个RCODE响应码(Response coded)字段,仅用于响应报文。当其值为0时代表,代表没有差错。当其值为3时,表示名字差错。从权威名字服务器返回,表示在查询中指定域名不存在。即我们需要将返回客户端的报文中该字段值修改为3。
if (strcmp(ip, "0.0.0.0") == 0)
{
num = htons(0x8183);//0x8180转为二进制为Flag=1000 0001 1000 0000
memcpy(&sendbuf[2], &num, sizeof(unsigned short));//sendbuf[] char数组一个元素占一个字节,short数组一个元素占两个字节以此类推
}
else
{
num = htons(0x8180);
memcpy(&sendbuf[2], &num, sizeof(unsigned short));
}
// 1.2 确定回答ANSWER数量
if (strcmp(ip, "0.0.0.0") == 0) // 此ip是否合法
{

num = htons(0x0); // 假如非法直接不回答
}
else
{
num = htons(0x1); // 合法则回答数ANCOUNT为1,十六进制表示
}
memcpy(&sendbuf[6], &num, sizeof(unsigned short));

// 2.构造RR资源记录,用res_rec字符数组暂存,结合pos指针移动实现定向存储
unsigned short name = htons(0xc00c);//固定写法,c0即1100 0000前两bit固定为1是偏移指针的标志写法,0000 1100表示字符串在整个DNS包中偏移量,Answer中的NAME在Qname中出现过
memcpy(res_rec, &name, sizeof(unsigned short));
pos += sizeof(unsigned short);

// TYPE字段
unsigned short type = htons(0x0001);//type=1也就是type=A
memcpy(res_rec + pos, &type, sizeof(unsigned short));
pos += sizeof(unsigned short);

// CLASS字段
unsigned short _class = htons(0x0001);//CLASS=1也就是CLASS=IN因特网
memcpy(res_rec + pos, &_class, sizeof(unsigned short));
pos += sizeof(unsigned short);

// TTL字段
unsigned long ttl = htonl(0x00000080);//这里设置TTL生存时间
memcpy(res_rec + pos, &ttl, sizeof(unsigned long));
pos += sizeof(unsigned long);//ttl为long类型,占四字节

// RDLENGTH字段
unsigned short RDlength = htons(0x0004);//RDATA是4字节IP地址,所以RDATA长度为4
memcpy(res_rec + pos, &RDlength, sizeof(unsigned short));
pos += sizeof(unsigned short);

// RDATA字段
unsigned long IP = (unsigned long)inet_addr(ip);//inet_addr将一个点分十进制的IP转换成一个长整型(32bit)数
memcpy(res_rec + pos, &IP, sizeof(unsigned long));
pos += sizeof(unsigned long);
pos += length;

// 3.将HEADER和RR共同组成的DNS响应报文存入sendbuf准备发送(QUESTION部分是直接拿的buf以前的内容来填充的)
memcpy(sendbuf + length, res_rec, sizeof(res_rec));//sendbuf+length不是很懂,大概是防止内存不够,sendbuf+len(RR)更加准确

// 4.将构造好的报文段发给客户端
//sendto()函数:发送一个数据报,数据在buf中,需要提供地址
/*
local_socket:外部服务器套接字
buf:发送数据缓冲区
sizeof(buf):接收缓冲区大小
0:标志位flag,表示调用操作方式,默认设为0
client:目标数据发送源地址
sizeof(client):地址大小
返回值:length:成功接收到的数据的字符数(长度),接收失败返回SOCKET_ERROR(-1)
*/
length = sendto(my_socket, sendbuf, pos, 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
if (length < 0 && debug_level == 2)
{
printf("\n#%d:向客户端发送响应失败", count_c);
}
if (debug_level == 2)
{
printf("\n#%d:成功向本地客户端发送响应", count_c);
}

}

}

四、演示过程

2022/7/22 14:28 刚刚在演示的时候显示socket绑定失败,猜测原因大概是端口号被占用,所以重启电脑之后重新启动了一次发现绑定成功,使用的是D:\My_code\C++\项目集合\DNS服务器演讲版\bin\Debug下的dnsrelay.exe作为演示服务器

1.启动

2.测试

2.1 中继功能

在配置⽂件中不存在www.baidu.com对应的IP地址,故服务器进⾏中继并将从外部收到的响应报⽂ 转发给客户端

2.2 屏蔽功能

在配置⽂件中我们将008.cn域名对应的IP地址屏蔽,故尽管能在本地DNS服务器中查询到该域名对 应的IP,但是在构造响应报⽂进⾏响应时会进⾏屏蔽处理

2.3 服务器功能

在配置⽂件中添加了sohu对应的IP地址为61.135.181.175,当客户端请求该域名对应的IP地址时服 务器可以成功进⾏响应

3.说明

从上面的测试我们可以看出基本功能是能够全部正常实现的,但是我们仔细查看返回值可能还会有一点懵逼不知道是怎么回事,这里简单做一下说明

nslookup会分别发送ipv4和ipv6的请求报文,假如都当作ipv4来处理则导致最后返回的结果都是ipv4的


初级项目_本地DNS服务器
https://gintoki-jpg.github.io/2022/07/15/项目_DNS服务器/
作者
杨再俨
发布于
2022年7月15日
许可协议