不说废话,直接进入正题。先说说本文本文的主要内容,好让你决定是否看下去:html
关于DNS是啥,想必学过计算机网络的应该都知道,它是Domain Name System的简写,中文翻译过来就是域名系统,是用来将主机名转换为ip的。事实上,除了进行主机名到IP地址的转换外,DNS一般还提供主机名到如下几项的转换服务:python
问:为何会有DNS,或者说为何要弄出两种方式(主机名和IP地址)来标识一台主机呢?git
答:这是由于主机名便于人的记忆,而IP地址便于计算机网络设备的处理,因而须要DNS来作前者到后者的转换。github
DNS其实是由一个分层的DNS服务器实现的分布式数据库和一个让主机可以查询分布式数据库的应用层协议组成。所以,要了解DNS的工做原理,须要从以上两个方便入手。redis
先来了解DNS的分布式架构。算法
DNS服务器根据域名命名空间(domian name space)组织成以下图所示的树形结构(固然,只给出部分DNS服务器,只为显示出DNS服务器的层次结构):shell
在图中,根节点表明的是根DNS服务器,因特网上共有13台,编号从A到M;根DNS服务器之下的一层被称为顶级DNS服务器;再往下一层被称为权威DNS服务器。数据库
当一个应用要经过DNS来查询某个主机名,好比www.google.com的ip时,粗略地说,查询过程是这样的:它先与根服务器之一联系,根服务器根据顶级域名com,会响应命名空间为com的顶级域服务器的ip;因而该应用接着向com顶级域服务器发出请求,com顶级域服务器会响应命名空间为google.com的权威DNS服务器的ip地址;最后该应用将请求命名空间为google.com的权威DNS服务器,该权威DNS服务器会响应主机名为www.google.com的ip。编程
实际上,除了上图层次结构中所展现的DNS外,还有一类与咱们接触更为密切的DNS服务器,它们是本地DNS服务器,咱们常常在电脑上配置的DNS服务器一般就是此类。它们通常由某公司,某大学,或某居民区提供,好比Google提供的DNS服务器8.8.8.8;好比常被人诟病的114.114.114.114等。api
加入了本地DNS的查询过程跟以前的查询过程基本上是一致的,查询流程以下图所示:
在实际工做中,DNS服务器是带缓存的。即DNS服务器在每次收到DNS请求时,都会先查询自身数据库包括缓存中有无要查询的主机名的ip,如有且没有过时,则直接响应该ip,不然才会按上图流程进行查询;而服务器在每次收到响应信息后,都会将响应信息缓存起来;
在介绍DNS层协议以前,先了解一下DNS服务器存储的资源记录(Resource Records,RRs),一条资源记录(RR)记载着一个映射关系。每条RR一般包含以下表所示的一些信息:
字段 | 含义 |
---|---|
NAME | 名字 |
TYPE | 类型 |
CLASS | 类 |
TTL | 生存时间 |
RDLENGTH | RDATA所占的字节数 |
RDATA | 数据 |
NAME和RDATA表示的含义根据TYPE的取值不一样而不一样,常见的:
TYPE实际上还有其余类型,全部可能的type及其约定的数值表示以下:
TYPE | value | meaning |
---|---|---|
A | 1 | a host address |
NS | 2 | an authoritative name server |
MD | 3 | a mail destination (Obsolete - use MX) |
MF | 4 | a mail forwarder (Obsolete - use MX) |
CNAME | 5 | the canonical name for an alias |
SOA | 6 | marks the start of a zone of authority |
MB | 7 | a mailbox domain name (EXPERIMENTAL) |
MG | 8 | a mail group member (EXPERIMENTAL) |
MR | 9 | a mail rename domain name (EXPERIMENTAL) |
NULL | 10 | a null RR (EXPERIMENTAL) |
WKS | 11 | a well known service description |
PTR | 12 | a domain name pointer |
HINFO | 13 | host information |
MINFO | 14 | mailbox or mail list information |
MX | 15 | mail exchange |
TXT | 16 | text strings |
下面介绍第二个方面,DNS协议。
DNS请求与响应的格式是一致的,其总体分为Header、Question、Answer、Authority、Additional5部分,以下图所示:
Header部分是必定有的,长度固定为12个字节;其他4部分可能有也可能没有,而且长度也不必定,这个在Header部分中有指明。Header的结构以下:
下面说明一下各个字段的含义:
Question部分的每个实体的格式以下图所示:
Answer、Authority、Additional部分格式一致,每部分都由若干实体组成,每一个实体即为一条RR,以前有过介绍,格式以下图所示:
DNS协议是工做在应用层的,运输层依赖的是UDP协议。下面尝试使用Python3.6来实现一个简单的DNS服务器。
在此以前先用Wireshark抓一下DNS包,验证一下上面的DNS协议的格式,也便于以后的实现。Wireshark的用法就不作介绍了,相信装好随便点点就知道怎么用了。先打开监听,添加过滤条件,而后用nslookup命令发送一个DNS包,好比咱们尝试查询www.baidu.com的ip:
nslookup www.baidu.com
而后能够在Wireshark中看到以下图所示的请求数据包:
响应数据以下图所示:
下面用Python来实现一个很是简单的DNS服务器。
首先,它应该具备最基本的“代理”功能,即咱们的DNS服务器在接到DNS请求后,直接将请求转发到某DNS服务器(如114.114.114.114)上,而后再将那台DNS的响应结果返回给DNS客户端:
import threading import socket import socketserver class Handler(socketserver.BaseRequestHandler): def handle(self): request_data = self.request[0] # 将请求转发到 114 DNS redirect_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) redirect_socket.sendto(request_data, ('114.114.114.114', 53)) response_data, address = redirect_socket.recvfrom(1024) # 将114响应响应给客户 client_socket = self.request[1] client_socket.sendto(response_data, self.client_address) class Server(socketserver.ThreadingMixIn, socketserver.UDPServer): pass if __name__ == "__main__": # 一下ip需换成本身电脑的ip server = Server(('172.16.42.254', 53), Handler) with server: server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() print('The DNS server is running at 172.16.42.254...') server_thread.join()
如今咱们的DNS服务器就能够进行转发工做了。运行以上程序(需root权限),而后用nsloop命令,向咱们的服务器发送DNS请求,一切OK:
$ nslookup baidu.com 172.16.42.254 Server: 172.16.42.254 Address: 172.16.42.254#53 Non-authoritative answer: Name: baidu.com Address: 123.125.114.144 Name: baidu.com Address: 180.149.132.47 Name: baidu.com Address: 111.13.101.208 Name: baidu.com Address: 220.181.57.217
若是仅仅作一下代理转发,那也太无聊了。如今咱们再加上缓存功能,即它可以将其余DNS服务器的响应结果缓存起来。当收到请求时,若请求主机号在缓存中,且没有过时,则直接响应缓存结果;不然进行上一功能中的操做。这一功能的关键在于对DNS消息的解析,代码以下:
class Message: u"""All communications inside of the domain protocol are carried in a single format called a message""" def __init__(self, header, question=None, answer=None, authority=None, additional=None): self.header = header self.question = question self.answer = answer self.authority = authority self.additional = additional @classmethod def from_bytes(cls, data): scanner = Scanner(data) # 读取header header = dict() header['ID'] = scanner.next_bytes(2) header['QR'] = scanner.next_bits(1) header['OPCODE'] = scanner.next_bits(4) header['AA'] = scanner.next_bits(1) header['TC'] = scanner.next_bits(1) header['RD'] = scanner.next_bits(1) header['RA'] = scanner.next_bits(1) header['Z'] = scanner.next_bits(3) header['RCODE'] = scanner.next_bits(4) header['QDCOUNT'] = scanner.next_bytes(2) header['ANCOUNT'] = scanner.next_bytes(2) header['NSCOUNT'] = scanner.next_bytes(2) header['ARCOUNT'] = scanner.next_bytes(2) print('header:', header) # 读取question questions = list() for _ in range(header['QDCOUNT']): question = dict() question['QNAME'] = scanner.next_bytes_until(lambda current, _: current == 0) scanner.next_bytes(1) # 跳过0 question['QTYPE'] = scanner.next_bytes(2) question['QCLASS'] = scanner.next_bytes(2) questions.append(question) print('questions:', questions) message = Message(header) # 读取answer、authority、additional rrs = list() for i in range(header['ANCOUNT'] + header['NSCOUNT'] + header['ARCOUNT']): rr = dict() rr['NAME'] = cls.handle_compression(scanner) rr['TYPE'] = scanner.next_bytes(2) rr['CLASS'] = scanner.next_bytes(2) rr['TTL'] = scanner.next_bytes(4) rr['RDLENGTH'] = scanner.next_bytes(2) # 处理data if rr['TYPE'] == 1: # A记录 r_data = scanner.next_bytes(rr['RDLENGTH'], False) rr['RDATA'] = reduce(lambda x, y: y if (len(x) == 0) else x + '.' + y, map(lambda num: str(num), r_data)) elif rr['TYPE'] == 2 or rr['TYPE'] == 5: # NS与CNAME记录 rr['RDATA'] = cls.handle_compression(scanner, rr['RDLENGTH']) rrs.append(rr) answer, authority, additional = list(), list(), list() for i, rr in enumerate(rrs): if i < header['ANCOUNT']: answer.append(rr) elif i < header['ANCOUNT'] + header['NSCOUNT']: authority.append(rr) else: additional.append(rr) print('answer:', answer) print('authority:', authority) print('additional:', additional) return message @classmethod def handle_compression(cls, scanner, length=float("inf")): """ The compression scheme allows a domain name in a message to be represented as either: - a pointer - a sequence of labels ending in a zero octet - a sequence of labels ending with a pointer """ byte = scanner.next_bytes() if byte >> 6 == 3: # a pointer pointer = (byte & 0x3F << 8) + scanner.next_bytes() return cls.handle_compression(Scanner(scanner.data, pointer)) data = scanner.next_bytes_until(lambda current, offset: current == 0 or current >> 6 == 3 or offset > length) if scanner.next_bytes(move=False) == 0: # a sequence of labels ending in a zero octet scanner.next_bytes() return data # a sequence of labels ending with a pointer result = data + '.' + cls.handle_compression(Scanner(scanner.data, *scanner.position())) scanner.next_bytes(2) # 跳过2个字节的指针 return result
其中用到了一个自定义的Scanner类,用来帮助咱们从bytes中按字节或位读取数据,其定义以下:
class Scanner: """scan bytes""" __mark_offset_byte, __mark_offset_bit = 0, 0 def __init__(self, data: bytes, offset_byte=0, offset_bit=0): self.data = data self.__offset_byte = offset_byte self.__offset_bit = offset_bit def next_bits(self, n=1): if n > (len(self.data) - self.__offset_byte) * 8 - self.__offset_bit: raise RuntimeError('剩余数据不足{}位'.format(n)) if n > 8 - self.__offset_bit: raise RuntimeError('不能跨字节读取读取位') result = self.data[self.__offset_byte] >> 8 - self.__offset_bit - n & (1 << n) - 1 self.__offset_bit += n if self.__offset_bit == 8: self.__offset_bit = 0 self.__offset_byte += 1 return result def next_bytes(self, n=1, convert=True, move=True): if not self.__offset_bit == 0: raise RuntimeError('当前字节不完整,请先读取完当前字节的全部位') if n > len(self.data) - self.__offset_byte: raise RuntimeError('剩余数据不足{}字节'.format(n)) result = self.data[self.__offset_byte: self.__offset_byte + n] if move: self.__offset_byte += n if convert: result = int.from_bytes(result, 'big') return result def next_bytes_until(self, stop, convert=True): if not self.__offset_bit == 0: raise RuntimeError('当前字节不完整,请先读取完当前字节的全部位') end = self.__offset_byte while not stop(self.data[end], end - self.__offset_byte): end += 1 result = self.data[self.__offset_byte: end] self.__offset_byte = end if convert: if result: result = reduce(lambda x, y: y if (x == '.') else x + y, map(lambda x: chr(x) if (31 < x < 127) else '.', result)) else: result = '' return result
而后须要作的是当收到114 DNS服务器的响应消息后,将消息缓存起来:
# 缓存响应结果 message = Message.from_bytes(response_data) message.save()
以上save方法就是将message中包含的各条RR保存起来,能够直接用一个集合来保存,也能够保存在一些专业的缓存设施中,好比redis。须要注意的是TTL的处理,若用redis缓存,它自带了TTL功能,能够直接使用。如果本身实现的,须要在保存的时候记录当前的时间,以便取出的时候可以判断是否过时。这些应该很容易实现,可是本人比较懒,这里就不写了……
最后,它须要具有可以读取咱们自定义的记录,并将记录加入缓存。。这个也不想写了……
另外,Message类还应该有一个to_bytes
方法,它能将一个Message对象转换为bytes对象,用于将 从缓存中取出的数据(即RR记录)转换为bytes,返回给用户。这个其实就是from_bytes
的逆过程,但实现起来应该比from_bytes
简单许多,由于你能够不使用指针来压缩数据,这样处理起来就没什么难度了。一样不想写了……
最后稍微作一下测试,算是作个结束:
if __name__ == "__main__": server = Server('172.16.42.254') server.start()
使用nsloop发送DNS请求到咱们本身写的服务器上,响应结果以下:
$ nslookup api.sina.com.cn 172.16.42.254 Server: 172.16.42.254 Address: 172.16.42.254#53 Non-authoritative answer: api.sina.com.cn canonical name = common6.dpool.sina.com.cn. Name: common6.dpool.sina.com.cn Address: 123.126.56.253
在运行的控制台中,打印出了从114 DNS返回的数据的解析结果:
$ sudo python3 dns.py The DNS server is running at 172.16.42.254... header: {'ID': 25835, 'QR': 1, 'OPCODE': 0, 'AA': 0, 'TC': 0, 'RD': 1, 'RA': 1, 'Z': 0, 'RCODE': 0, 'QDCOUNT': 1, 'ANCOUNT': 2, 'NSCOUNT': 4, 'ARCOUNT': 4} questions: [{'QNAME': 'api.sina.com.cn', 'QTYPE': 1, 'QCLASS': 1}] answer: [{'NAME': 'api.sina.com.cn', 'TYPE': 5, 'CLASS': 1, 'TTL': 56, 'RDLENGTH': 16, 'RDATA': 'common6.dpool.sina.com.cn'}, {'NAME': 'common6.dpool.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 34, 'RDLENGTH': 4, 'RDATA': '123.126.56.253'}] authority: [{'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns1.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns3.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns2.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns4.sina.com.cn'}] additional: [{'NAME': 'ns1.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26674, 'RDLENGTH': 4, 'RDATA': '202.106.184.166'}, {'NAME': 'ns2.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26652, 'RDLENGTH': 4, 'RDATA': '61.172.201.254'}, {'NAME': 'ns3.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26509, 'RDLENGTH': 4, 'RDATA': '123.125.29.99'}, {'NAME': 'ns4.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26497, 'RDLENGTH': 4, 'RDATA': '121.14.1.22'}]
以上完整代码,见这里