logo头像

学如逆水行舟

网络杂谈——聊聊NDS解析

网络杂谈——聊聊NDS解析

一、引言

在浏览器中输入一个地址,点击回车之后发生了什么?这是一个面试中常见的问题 ,这个看似常见简单的操作,其中却隐藏了大量复杂的互联网技术。本篇博客,我们就聊一聊网上冲浪的第一步:DNS解析。

DNS解析是一种服务,其又被称为域名解析。它的作用是将域名解析到具体的网络IP地址,以便进行后续的网络连接操作。DNS解析说起来也简单,从表面上看,就是通过一个查询服务,将域名映射成IP地址,可以往深处推敲,你就会发现其实并没有那么简单,世界上有无数的终端接入互联网,DNS服务是如何从浩如烟海的数据中找到目标数据的,DNS服务是如何保证搜索性能的等等都是值得讨论的话题。

二、分层的网络

在深入了解DNS解析之前,首先需要对我们当下使用的网络系统有大致的了解。关于网络系统的分层,流行的有OSI的7层模型与TCP/IP的五层模型,其中关系如下图所示:

以OSI的7层网络模型为例,其中每一层都负责对应的协议,两台终端在进行交互时,同层之间进行交互。我们从上到下来看:

应用层:顾名思义应用层的主要作用是搭建应用,其负责实现应用层面的协议,例如文件传输FTP协议,还有我们最常用的HTTP协议以及邮箱相关协议等。

表示层:表示层用来对应用层的数据进行封装处理,如压缩与解压。

会话层:会话层位于传输层之上,其用来管理一对会话,即会话的连接开始,同步,中断等等。

传输层:传输层述责数据的分段传输与接收重组,这一层有TCP、UDP等传输协议。

网络层:网路层负责根据IP来将数据传递到指定的目的主机,其会确定传输路由等问题。

数据链路层:将数据进行MAC地址的封装与解封等。

物理层:定义物理媒介的协议,以二进制的方式传输数据,定义数据的传输速率等参数。

当数据真正的通过7层网络模型传输之前,首要确定的是数据要传输到哪里,我们知道通过IP地址确定数据要到达的目的终端,然而IP地址是有一串数字(字母)与点符号构成的,可读性很差且难于记忆,因此采用别名的方式来代替直接使用IP进行地址确定,这个别名就是域名,将域名解析为IP的过程就是域名解析。

三、DNS服务器

既然需要将域名解析成为IP地址,则就需要有一个服务器提供这样的解析功能,这个服务器需要维护这一张域名与IP地址映射的表,在客户提出解析需求时,从表中查询出域名对应的IP地址返回给客户,如下图所示:

然而在实际应用中,上图中的设计结构明显是不切实际的,由一台服务器来维护所有的域名与IP映射关系明显是不现实的。首先,世界上的域名和IP数量非常庞大,并且更新也非常频繁,维护成本很多。另一方面,大量的客户同时进行域名解析请求,也会使解析的效率和速度出现瓶颈。现实中的DNS解析是采用层层递进,多级缓存,递归查询的结构组织而成的,下图很好的描述了这一过程:

从图中可以看到首先当客户端发起DNS解析时,会从本机NDS缓存中进行查找,同样也会查找本机的Hosts文件中是否有指定对应的解析规则,由于本机的Hosts文件具有最高的优先级,因此我们想在本机将某个域名强制指向一个固定的IP,则可以采用修改Hosts文件的方式,在Mac系统中,此Hosts文件在etc文件夹下。

当本机缓存中没有解析出此域名的信息且Hosts文件中也没有指定时,会想本地DNS服务器发起查询,本地DNS服务器也会维护一张缓存表用来提高查询效率,如果本地DNS服务器没有查到,会向根DNS服务器发起请求,根DNS服务器会采用递归迭代的方式进行搜索,全球有13台根DNS服务器,根DNS服务器会根据域名后缀返回对应的顶级域名服务器,顶级域名服务器会再次根据域名分类将指定的主DNS地址返回,如此迭代直到解析到对应的IP地址再一步步返回给客户机。

四、域名服务器类型

根域名服务器

根域名服务器是域名解析系统中最高级别的域名服务器,其复杂返回顶级域名服务器,他们是互联网的基础。目前,全球有13个根域名服务器地址(并非实际的服务器数量),可以在如下网站查找到这些根域名服务器的信息。

https://zh.wikipedia.org/wiki/%E6%A0%B9%E7%B6%B2%E5%9F%9F%E5%90%8D%E7%A8%B1%E4%BC%BA%E6%9C%8D%E5%99%A8

顶级域名服务器

顶级域名服务器用于某个顶级域名下的DNS解析,例如有专门负责.com后缀的顶级域名服务器,负责.edu后缀的顶级域名服务器等,顶级域名服务器将查询到的主域名服务器返回。

主域名服务器

主域名服务器负责某个区域的域名解析。同样,主域名服务器会配套辅助域名服务器进行备份与分担负载。

五、手动进行DNS解析

一次完整的HTTP请求首先要做的就是DNS解析(如果是通过域名进行请求)。平时在开发中,我们很少关注是因为发起HTTP的网络请求层帮我们封装好了这一部分逻辑。有时候,为了提高请求的效率或防止DNS劫持,我们也可以自己进行DNS解析。

以iOS中的编程为例,可以直接使用CoreFoundation框架中的接口进行NDS解析:
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
Boolean result;
CFHostRef hostRef;
CFArrayRef addresses = NULL;
CFArrayRef names = NULL;
NSMutableArray * ipsArr = [[NSMutableArray alloc] init];
CFStringRef hostNameRef = CFStringCreateWithCString(kCFAllocatorDefault, "www.baidu.com", kCFStringEncodingASCII);
hostRef = CFHostCreateWithName(kCFAllocatorDefault, hostNameRef);
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
result = CFHostStartInfoResolution(hostRef, kCFHostAddresses, NULL);
if (result == true) {
addresses = CFHostGetAddressing(hostRef, &result);
names = CFHostGetNames(hostRef, &result);
}
if(result)
{
struct sockaddr_in* remoteAddr;
for(int i = 0; i < CFArrayGetCount(addresses); i++)
{
CFDataRef saData = (CFDataRef)CFArrayGetValueAtIndex(addresses, i);
remoteAddr = (struct sockaddr_in*)CFDataGetBytePtr(saData);
if(remoteAddr != NULL)
{
//获取IP地址
char ip[16];
strcpy(ip, inet_ntoa(remoteAddr->sin_addr));
NSString * ipStr = [NSString stringWithCString:ip encoding:NSUTF8StringEncoding];
[ipsArr addObject:ipStr];
}
}
}
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"name:%@", names);
NSLog(@"IP:%@ \n time cost: %0.3fs", ipsArr,end - start);
CFRelease(hostNameRef);
CFRelease(hostRef);

运行上面代码,即可将指定域名的IP地址解析出来,其中CFHostCreateWithName方法根据域名创建一个主机引用对象,CFHostStartInfoResolution方法用来进行主机信息的解析,如果解析成功,CFHostGetAddressing方法用来获取具体的IP地址数据。

上面进行NDS解析的方法比较上层,还有一种方式可以获取到更多的信息,示例代码如下:
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
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
char *ptr, **pptr;
struct hostent *hptr;
char str[32];
ptr = "www.baidu.com";
NSMutableArray * ips = [NSMutableArray array];
NSMutableArray * alis = [NSMutableArray array];
if((hptr = gethostbyname(ptr)) == NULL)
{
return 0;
}
for(pptr=hptr->h_addr_list; *pptr!=NULL; pptr++) {
// inet_ntop方法是将二进制数据转换成IP字符串
NSString * ipStr = [NSString stringWithCString:inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)) encoding:NSUTF8StringEncoding];
[ips addObject:ipStr?:@""];
}
// 获取主机别名
for(pptr=hptr->h_aliases; *pptr!=NULL; pptr++) {
NSString * ali = [NSString stringWithCString:*pptr encoding:NSUTF8StringEncoding];
[alis addObject:ali?:@""];
}

CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
// 获取主机名
NSLog(@"%s", hptr->h_name);
NSLog(@"alias:%@", alis);
NSLog(@"ip:%@\ntime cost: %0.3fs", ips,end - start);

gethostbyname方法可以方便的获取指定域名的主机信息,其只支持IPV4,若支持解析IPV6,需要使用gethostbyname2方法,这两个方法都会返回hostent结构体,其中封装了主机的信息。

除了上面介绍的两种方便的方式外,也可以直接通过C语言的Socket接口来进行DNS解析,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
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
212
213
214
215
216
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>


#define DNS_SVR "198.41.0.4"

#define DNS_HOST 0x01
#define DNS_CNAME 0x05

int socketfd;
struct sockaddr_in dest;

static void
send_dns_request(const char *dns_name);

static void
parse_dns_response();

/**
* Generate DNS question chunk
*/
static void
generate_question(const char *dns_name
, unsigned char *buf , int *len);

static int
is_pointer(int in);

static void
parse_dns_name(unsigned char *chunk , unsigned char *ptr
, char *out , int *len);

int main(int argc , char *argv[]){
socketfd = socket(AF_INET , SOCK_DGRAM , 0);
if(socketfd < 0){
perror("create socket failed");
exit(-1);
}
bzero(&dest , sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(53);
dest.sin_addr.s_addr = inet_addr(DNS_SVR);


send_dns_request("www.baidu.com");

parse_dns_response();

return 0;
}

static void parse_dns_response(){

unsigned char buf[1024];
unsigned char *ptr = buf;
struct sockaddr_in addr;
char *src_ip;
int n , i , flag , querys , answers;
int type , ttl , datalen , len;
char cname[128] , aname[128] , ip[20] , *cname_ptr;
unsigned char netip[4];
size_t addr_len = sizeof(struct sockaddr_in);

n = recvfrom(socketfd , buf , sizeof(buf) , 0
, (struct sockaddr*)&addr , &addr_len);
ptr += 4; /* move ptr to Questions */
querys = ntohs(*((unsigned short*)ptr));
ptr += 2; /* move ptr to Answer RRs */
answers = ntohs(*((unsigned short*)ptr));
ptr += 6; /* move ptr to Querys */
/* move over Querys */
for(i= 0 ; i < querys ; i ++){
for(;;){
flag = (int)ptr[0];
ptr += (flag + 1);
if(flag == 0)
break;
}
ptr += 4;
}
printf("-------------------------------\n");
/* now ptr points to Answers */
for(i = 0 ; i < answers ; i ++){
bzero(aname , sizeof(aname));
len = 0;
parse_dns_name(buf , ptr , aname , &len);
ptr += 2; /* move ptr to Type*/
type = htons(*((unsigned short*)ptr));
ptr += 4; /* move ptr to Time to live */
ttl = htonl(*((unsigned int*)ptr));
ptr += 4; /* move ptr to Data lenth */
datalen = ntohs(*((unsigned short*)ptr));
ptr += 2; /* move ptr to Data*/
if(type == DNS_CNAME){
bzero(cname , sizeof(cname));
len = 0;
parse_dns_name(buf , ptr , cname , &len);
printf("%s is an alias for %s\n" , aname , cname);
ptr += datalen;
}
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);
}
ptr += datalen;
}

}
ptr += 2;
}

static void
parse_dns_name(unsigned char *chunk
, unsigned char *ptr , char *out , int *len){
int n , alen , flag;
char *pos = out + (*len);

for(;;){
flag = (int)ptr[0];
if(flag == 0)
break;
if(is_pointer(flag)){
n = (int)ptr[1];
ptr = chunk + n;
parse_dns_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;
}
}
}

}

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

static void send_dns_request(const char *dns_name){

unsigned char request[256];
unsigned char *ptr = request;
unsigned char question[128];
int question_len;


generate_question(dns_name , question , &question_len);

*((unsigned short*)ptr) = htons(0xff00);
ptr += 2;
*((unsigned short*)ptr) = htons(0x0100);
ptr += 2;
*((unsigned short*)ptr) = htons(1);
ptr += 2;
*((unsigned short*)ptr) = 0;
ptr += 2;
*((unsigned short*)ptr) = 0;
ptr += 2;
*((unsigned short*)ptr) = 0;
ptr += 2;
memcpy(ptr , question , question_len);
ptr += question_len;

sendto(socketfd , request , question_len + 12 , 0
, (struct sockaddr*)&dest , sizeof(struct sockaddr));
}

static void
generate_question(const char *dns_name , unsigned char *buf , int *len){
char *pos;
unsigned char *ptr;
int n;

*len = 0;
ptr = buf;
pos = (char*)dns_name;
for(;;){
n = strlen(pos) - (strstr(pos , ".") ? strlen(strstr(pos , ".")) : 0);
*ptr ++ = (unsigned char)n;
memcpy(ptr , pos , n);
*len += n + 1;
ptr += n;
if(!strstr(pos , ".")){
*ptr = (unsigned char)0;
ptr ++;
*len += 1;
break;
}
pos += n + 1;
}
*((unsigned short*)ptr) = htons(1);
*len += 2;
ptr += 2;
*((unsigned short*)ptr) = htons(1);
*len += 2;
}

其中198.41.0.4是任意选择的一个根域名服务器的地址,如果解析成功,将在控制台看到如下的打印信息:

1
2
3
4
5
6
-------------------------------
www.baidu.com is an alias for www.a.shifen.com
www.a.shifen.com has address 112.80.248.76
Time to live: 1 minutes , 7 seconds
www.a.shifen.com has address 112.80.248.75
Time to live: 1 minutes , 7 seconds