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
Lines 1-5: Read the host IP address and Port for the new DNS converter/forwarder. DNS runs on port 53 typically.
- 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 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.
Lines 3-7: Main code body. Accepts the request, processes it, and sends out 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 2-3: Parse out the DNS transaction ID and flag field from the request.
- 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 response
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.
Lines 5-12: Loop thru the labels in the DNS question (it has a specific format, see RFC).
- 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, question
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).
Lines 2-3: Send the DNS request to Google's HTTPS service.
- 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, records
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.
Lines 2-9: Construct the flags field of the DNS response based on the JSON response from Google (and some hard-coded values).
- 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 flags
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
Lines 1-2: Tor uses port 9050 as its local SOCKS port.
- 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()
Line 4: Simply modify the HTTPS Get to Google (from __getRecords()) with these proxies.
Lines 1-4: For some extra security, signal your Tor controller to change its IP address periodically.
- 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 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.