Thursday, June 8, 2017

Secure DNS Conversion


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.

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 response
Lines 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, question
Lines 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, records
Lines 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 flags
Lines 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.

Full source code here.

Copyright ©1993-2024 Joey E Whelan, All rights reserved.