不說廢話,直接進入正題。先說說本文本文的主要內容,好讓你決定是否看下去: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'}]
以上完整代碼,見這裏