Summary
I'll be discussing a little distraction I was working on lately in this article. There are a large number of articles out there about the inherent insecurity of DNS. Your requests/responses are clear text that runs (predominantly) over UDP. Every ISP, DNS provider, etc basically has goods on whatever Internet sites you may visit.
In a few lines of code (~200), I show one way to keep your DNS traffic private. Part 1 hinges on Google's HTTPS DNS services. I convert the UDP DNS request into an HTTPS request to Google for an encrypted DNS request/response system. To raise this to another level of paranoia (for those that don't even trust Google with their DNS traffic), I show how to tunnel those requests to Google through Tor.
I made little attempt to faithfully follow the DNS RFC in this implementation. Just wasn't necessary for my limited use-case.
I made little attempt to faithfully follow the DNS RFC in this implementation. Just wasn't necessary for my limited use-case.
Implementation - Part 1
Below is a diagram of the overall architecture I was looking for. I use a local caching DNS server for my own network. I reconfigure that DNS server to send all of its requests to my DNS converter that translates DNS UDP requests into HTTP calls to Google's DNS over HTTPS service.
Python Implementation
cfgParser = configparser.ConfigParser() cfgParser.optionxform = str cfgParser.read('sdns.cfg') host = cfgParser.get('ConfigData', 'host') port = int(cfgParser.get('ConfigData', 'port')) server = socketserver.UDPServer((host, port), DNSHandler) server.serve_forever()Lines 1-5: Read the host IP address and Port for the new DNS converter/forwarder. DNS runs on port 53 typically.
Lines 8-9: Start a UDP server on that IP address and port. DNSHandler is a class that contains the code to process the DNS request and generate the response.
class DNSHandler(socketserver.BaseRequestHandler): def handle(self): data = self.request[0] socket = self.request[1] response = self.__createResponse(data) socket.sendto(response, self.client_address)Lines 3-7: Main code body. Accepts the request, processes it, and sends out the response.
def __createResponse(self, data): tid = data[0:2] #transaction id opcode = data[2] & 0b01111000 #data[2] is the flags field. bits 2-5 is the opcode name, queryType, question = self.__processQuestion(data[12:]) if opcode == 0 and queryType == '1': #RFC departure. Only processing standard queries (0) and 'A' query types. flags, numbers, records = self.__getRecords(name) response = tid + flags + numbers + question + records else: #qr (response), recursion desired, recursion avail bits set. set the rcode to 'not implemented' flags = ((0b100000011000 << 4) | 4).to_bytes(2, byteorder='big') numbers = (0).to_bytes(8, byteorder='big') response = tid + flags + numbers return responseLines 2-3: Parse out the DNS transaction ID and flag field from the request.
Line 4: Send the rest of the DNS request to another function for parsing out the question.
Lines 6-8: If it's a DNS request I chose to implement, standard 'A' type, process it.
Lines 10-13: If it's not a request I implemented, return a DNS error.
def __processQuestion(self, quesData): i = 0 name = '' while True: count = int.from_bytes(quesData[i:i+1], byteorder='big') i = i+1 if count == 0: break else: name = name + str(quesData[i:i+count],'utf-8') + '.' i = i + count name = name[:-1] queryType = str(int.from_bytes(quesData[i:i+2], byteorder='big')) question = quesData[0:i+4] return name, queryType, questionLines 5-12: Loop thru the labels in the DNS question (it has a specific format, see RFC).
Lines 14-16: Set up 3 return variables with the domain name in question, query type, and the entire question byte array (for the response to be sent later).
def __getRecords(self, name): payload = {'name' : name, 'type' : '1'} data = requests.get(GOOGLE_DNS, params=payload).json() flags = self.__getFlags(data) records = bytes(0) count = 0 if 'Answer' in data: for answer in data['Answer']: if answer['type'] == 1: count = count + 1 name = (0xc00c).to_bytes(2, byteorder='big') #RFC departure. Hard-coded offset to domain name in initial question. rectype = (1).to_bytes(2, byteorder='big') classtype = (1).to_bytes(2, byteorder='big') ttl = answer['TTL'].to_bytes(4, byteorder='big') length = (4).to_bytes(2, byteorder='big') #4 byte IP addresses only quad = list(map(int, answer['data'].split('.'))) res = bytes(0) for i in quad: res = res + i.to_bytes(1, byteorder='big') records = records + name + rectype + classtype + ttl + length + res nques = (1).to_bytes(2, byteorder='big') #hard coded to 1 nans = (count).to_bytes(2, byteorder='big') nath = (0).to_bytes(2, byteorder='big') #hard coded to 0 nadd = (0).to_bytes(2, byteorder='big') #hard coded to 0 numbers = nques + nans + nath + nadd return flags, numbers, recordsLines 2-3: Send the DNS request to Google's HTTPS service.
Line 5: Construct the flags field for the response based on the request flags field.
Lines 8-21: Construct the Return Records fields for the response from the JSON response from Google's DNS/HTTPS service.
Lines 23-27: Construct the number of records field for the response.
def __getFlags(self, data): flags = 0b100000 #qr=1, opcode=0000, aa=0 flags = (flags << 1) | data['TC'] #set tc bit flags = (flags << 1) | data['RD'] #set rd bit flags = (flags << 1) | data['RA'] #set ra bit flags = flags << 1 #One zero flags = (flags << 1) | data['AD'] #set ad bit flags = (flags << 1) | data['CD'] #set cd bit flags = ((flags << 4) | data['Status']).to_bytes(2, byteorder='big') return flagsLines 2-9: Construct the flags field of the DNS response based on the JSON response from Google (and some hard-coded values).
Implementation - Part 2
Now to prevent even Google to track your DNS activity, we can send all this HTTPS traffic on a world-wide trip through the Tor network. You need to set up a local Tor controller instance and then add the lines of code below to direct the HTTPS request through that network. Here's an excellent explanation for that.
Python Implementation
PROXIES = {'http': 'socks5://127.0.0.1:9050', 'https': 'socks5://127.0.0.1:9050'} data = requests.get(GOOGLE_DNS, params=payload, proxies=PROXIES).json()Lines 1-2: Tor uses port 9050 as its local SOCKS port.
Line 4: Simply modify the HTTPS Get to Google (from __getRecords()) with these proxies.
def renew(): with Controller.from_port(port = 9051) as controller: controller.authenticate(password="test") controller.signal(Signal.NEWNYM) scheduler = BackgroundScheduler() scheduler.add_job(renew, 'interval', hours=1) scheduler.start()Lines 1-4: For some extra security, signal your Tor controller to change its IP address periodically.
Lines 7-9: Set that change interval with a scheduler.
Summary
This was a fairly simplistic DNS server implementation in Python. I didn't attempt to implement the full RFC as it's not necessary for my use case. You get secure DNS traffic from this but at the price of latency. The HTTPS conversion and Tor overhead equates to a factor of 10 increase in DNS latency (the vast majority of that is attributable to Tor). I don't find it noticeable though for my use.
Copyright ©1993-2024 Joey E Whelan, All rights reserved.