├── LICENSE ├── README ├── pytinydns.conf ├── pytinydns.host ├── pytinydns.py └── redis_import.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <2014> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | PyTinyDNS: 2 | 3 | Usage: pytinydns.py [OPTION]... 4 | -h, --help Print this message 5 | -c, --config=file Specify the config file to use 6 | -d, --default=ip Specify the default IP address to fall back on 7 | -l, --list=host_file Specify host file to use instead of redis 8 | -n, --noredis Specify not to use redis db. Default IP will be used 9 | -r, --resolve Specify to resolve non matches to actual IP 10 | 11 | 12 | This is a tiny DNS server that resolves A records to IPs. 13 | 14 | Sample Host File: 15 | # Comment 16 | google.com.:127.0.0.1 17 | yahoo.com.:192.168.1.1 18 | 19 | Sample Config File: 20 | [PyTinyDNS] 21 | DefaultIP = 192.168.1.99 22 | Use_Redis = yes 23 | Redis_Server = localhost 24 | # Resolve_Nonmatch will query your normal DNS for domains with no local match. 25 | # May cause delays in answering other requests. Default is no. 26 | # Resolve_Nonmatch = yes 27 | # Host_File = pytinydns.host 28 | 29 | 30 | PyTinyDNS redis import tool will import the host file and save the keys and values into the redis db. 31 | 32 | If you elect not to use redis, then you can either resolve every domain to the default IP or use a config file to supply A records. 33 | 34 | The above host file will resolve google to 127.0.0.1 and yahoo.com to 192.168.1.1 respectively 35 | 36 | Added redis_import.py: 37 | 38 | Usage: redis_import.py import_file 39 | 40 | Can be used to update live instance of the DNS server. 41 | 42 | Borrowed DNSQuery class from http://code.activestate.com/recipes/491264-mini-fake-dns-server/ 43 | -------------------------------------------------------------------------------- /pytinydns.conf: -------------------------------------------------------------------------------- 1 | [PyTinyDNS] 2 | DefaultIP = 192.168.1.99 3 | Use_Redis = yes 4 | Redis_Server = localhost 5 | # Resolve_Nonmatch will query your normal DNS for domains with no local match. 6 | # May cause delays in answering other requests. Default is no. 7 | # Resolve_Nonmatch = yes 8 | # Host_File = pytinydns.host 9 | -------------------------------------------------------------------------------- /pytinydns.host: -------------------------------------------------------------------------------- 1 | # This is a comment 2 | google.com.:192.168.1.2 3 | www.yahoo.com.:192.168.1.1 4 | -------------------------------------------------------------------------------- /pytinydns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """PyTinyDNS docstring. 3 | 4 | This script acts as a light A record DNS resolver. 5 | Use redis_import.py to import a host file into a live DB. 6 | You can also use pydns.conf as a flat file config with no DB. 7 | 8 | Example: 9 | # Comment 10 | google.com.:127.0.0.1 11 | 12 | The above would resolve any requests for google.com to 127.0.0.1 13 | """ 14 | import ConfigParser 15 | import getopt 16 | import redis 17 | import socket 18 | import sys 19 | 20 | try: 21 | socket.SO_REUSEPORT 22 | except AttributeError: 23 | socket.SO_REUSEPORT = 15 24 | 25 | #Global variables 26 | default_ip = '127.0.0.1' 27 | redis_server = 'localhost' 28 | use_redis = True 29 | resolve_nonmatch = False 30 | dns_dict = {} 31 | 32 | # DNSQuery class from http://code.activestate.com/recipes/491264-mini-fake-dns-server/ 33 | class DNSQuery: 34 | def __init__(self, data): 35 | self.data=data 36 | self.domain='' 37 | 38 | tipo = (ord(data[2]) >> 3) & 15 # Opcode bits 39 | if tipo == 0: # Standard query 40 | ini=12 41 | lon=ord(data[ini]) 42 | while lon != 0: 43 | self.domain+=data[ini+1:ini+lon+1]+'.' 44 | ini+=lon+1 45 | lon=ord(data[ini]) 46 | 47 | def build_reply(self, ip): 48 | packet='' 49 | if ip == '': # Taken from crypt0s (https://github.com/Crypt0s/FakeDns/blob/master/fakedns.py) 50 | # Build the response packet 51 | packet+=self.data[:2] + "\x81\x83" # Reply Code: No Such Name 52 | #0 answer rrs 0 additional, 0 auth 53 | packet+=self.data[4:6] + '\x00\x00' + '\x00\x00\x00\x00' # Questions and Answers Counts 54 | packet+=self.data[12:] # Original Domain Name Question 55 | 56 | if self.domain and packet == '': 57 | packet+=self.data[:2] + "\x81\x80" 58 | packet+=self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00' # Questions and Answers Counts 59 | packet+=self.data[12:] # Original Domain Name Question 60 | packet+='\xc0\x0c' # Pointer to domain name 61 | packet+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' # Response type, ttl and resource data length -> 4 bytes 62 | packet+=str.join('',map(lambda x: chr(int(x)), ip.split('.'))) # 4bytes of IP 63 | return packet 64 | 65 | def print_help(): 66 | print 'Usage: pytinydns.py [OPTION]...' 67 | print '\t-h, --help \t\tPrint this message' 68 | print '\t-c, --config=file\tSpecify the config file to use' 69 | print '\t-d, --default=ip\tSpecify the default IP address to fall back on' 70 | print '\t-l, --list=host_file\tSpecify host file to use instead of redis' 71 | print '\t-n, --noredis\t\tSpecify not to use redis db. Default IP will be used' 72 | print '\t-r, --resolve\t\tSpecify to resolve non matches to actual IP' 73 | 74 | def read_hosts(config): 75 | # Use global dns dictionary 76 | global dns_dict 77 | 78 | try: 79 | c_file = open(config,"r") 80 | except: 81 | print '[-] Host file %s not found.' % (config) 82 | sys.exit(1) 83 | 84 | for line in c_file: 85 | sline = line.split(':') 86 | if len(sline) != 2 and line[0] != '#': 87 | print 'Invalid config format.' 88 | print 'google.com.:127.0.0.1' 89 | sys.exit(1) 90 | else: 91 | if line[0] != '#': # Make sure the line is not a comment 92 | dns_dict[sline[0]] = sline[1][0:-1] # trim \n off at the end of the line 93 | 94 | def read_config(config): 95 | # Use global config variables 96 | global default_ip 97 | global redis_server 98 | global use_redis 99 | global resolve_nonmatch 100 | 101 | c_parse = ConfigParser.ConfigParser() 102 | 103 | try: 104 | c_parse.read(config) 105 | except: 106 | print '[-] Config file %s not found.' % (config) 107 | sys.exit(1) 108 | 109 | for item in c_parse.items('PyTinyDNS'): 110 | arg = item[1] 111 | opt = item[0] 112 | 113 | if opt == 'defaultip': 114 | default_ip = arg 115 | elif opt == 'use_redis': 116 | if arg == 'yes': 117 | use_redis = True 118 | elif arg == 'no': 119 | use_redis = False 120 | elif opt == 'redis_server': 121 | redis_server = arg 122 | elif opt == 'host_file': 123 | read_hosts(arg) 124 | elif opt == 'resolve_nonmatch': 125 | if arg == 'yes': 126 | resolve_nonmatch = True 127 | elif arg == 'no': 128 | resolve_nonmatch = False 129 | 130 | # Make request to external DNS (used when resolve_nonmatch = True) 131 | def ext_request(domain): 132 | try: 133 | return socket.gethostbyname(domain) 134 | except: # Domain doesn't exist 135 | print '[-] Unable to parse request' 136 | return '' 137 | 138 | def main(): 139 | # Use global config variables 140 | global default_ip 141 | global redis_server 142 | global use_redis 143 | global resolve_nonmatch 144 | global dns_dict 145 | 146 | try: 147 | opts, args = getopt.getopt(sys.argv[1:], "hrnc:d:l:", ["resolve", "config=", "list=", "noredis", "help", "default="]) 148 | except getopt.error, msg: 149 | print msg 150 | print_help() 151 | sys.exit(2) 152 | 153 | for opt, arg in opts: 154 | if opt in ('-h', '--help'): 155 | print_help() 156 | sys.exit(0) 157 | elif opt in ('-n', '--noredis'): 158 | use_redis = False 159 | elif opt in ('-d', '--default'): 160 | default_ip = arg 161 | elif opt in ('-l', '--list'): 162 | use_redis = False 163 | read_hosts(arg) 164 | elif opt in ('-c', '--config'): 165 | read_config(arg) 166 | elif opt in ('-r', 'resolve'): 167 | resolve_nonmatch = True 168 | 169 | print '[-] PyTinyDNS' 170 | 171 | if use_redis == True: 172 | r_server = redis.Redis(redis_server) 173 | 174 | udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 175 | 176 | #SO_REUSEPORT option allows multiple threads to bind to one port. 177 | # kernel >= 3.9 https://lwn.net/Articles/542629/ 178 | try: 179 | udps.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 180 | except socket.error: 181 | print '[-] SO_REUSEPORT not supported by your system.' 182 | 183 | udps.bind(('',53)) 184 | 185 | try: 186 | while 1: 187 | ip = '' 188 | data, src_addr = udps.recvfrom(1024) 189 | p=DNSQuery(data) 190 | if use_redis == True: # We're using redis. Check if the key exists. 191 | 192 | try: # Try to find domain using redis 193 | a_record = r_server.hget('pytinydns.domains', p.domain) 194 | except: 195 | print 'No redis server connection with %s.' % (redis_server) # No connection with redis: fall back to default 196 | a_record = default_ip 197 | 198 | if a_record is not None: # A record returned from redis DB 199 | ip = a_record 200 | else: # No record returned 201 | if resolve_nonmatch == True: 202 | ip = ext_request(p.domain) 203 | else: 204 | ip = default_ip 205 | 206 | else: # Not using redis: fall back to file or default. 207 | if p.domain in dns_dict: 208 | ip = dns_dict[p.domain] 209 | else: 210 | if resolve_nonmatch == True: 211 | ip = ext_request(p.domain) 212 | else: 213 | ip = default_ip 214 | 215 | udps.sendto(p.build_reply(ip), src_addr) 216 | print '[+] Request from %s: %s -> %s' % (src_addr[0], p.domain, ip) 217 | except KeyboardInterrupt: 218 | print '[-] Ending' 219 | udps.close() 220 | 221 | if __name__ == '__main__': 222 | main() 223 | -------------------------------------------------------------------------------- /redis_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | redis_import.py 4 | 5 | Import a host file into a redis database for live updating of PyTinyDNS 6 | 7 | Host files have the following format host:ip 8 | 9 | google.com.:192.168.1.1 10 | 11 | resolves google.com to 192.168.1.1 12 | """ 13 | import getopt 14 | import redis 15 | import sys 16 | 17 | def import_config(config, redis_server): 18 | print '[+] Opening File %s' % (config) 19 | 20 | try: 21 | cfile = open(config,"r") 22 | except: 23 | print '[-] File %s could not be found.' % (config) 24 | sys.exit(1) 25 | 26 | for line in cfile: 27 | sline = line.split(':') 28 | if len(sline) != 2 and line[0] != '#': 29 | print 'Invalid config format.' 30 | print 'google.com.:127.0.0.1' 31 | sys.exit(1) 32 | else: 33 | if line[0] != '#': 34 | domain = sline[0] 35 | ip = sline[1][0:-1] 36 | insert_record(domain,ip,redis_server) 37 | 38 | def insert_record(domain, ip, redis_server): 39 | r_server = redis.Redis(redis_server) 40 | 41 | try: 42 | print '[+] Importing record: %s -> %s' % (domain,ip) 43 | r_server.hset('pytinydns.domains', domain, ip) 44 | except: 45 | print '[-] Connection failed with server %s' % (redis_addr) 46 | sys.exit(1) 47 | 48 | def print_help(): 49 | print 'Usage: redis_import.py OPTIONS' 50 | print '\t-h, --help\t\tPrint this message' 51 | print '\t-l, --list=host_file\tImport host file' 52 | print '\t-u, --update=host:ip\tUpdate one record' 53 | 54 | def main(): 55 | redis_server = 'localhost' 56 | 57 | try: 58 | opts, args = getopt.getopt(sys.argv[1:], "hu:l:", ["update=","list=", "help"]) 59 | except getopt.error, msg: 60 | print msg 61 | print_help() 62 | sys.exit(2) 63 | 64 | print '[-] PyTinyDNS Redis Import Tool' 65 | 66 | for opt, arg in opts: 67 | if opt in ('-h', '--help'): 68 | print_help() 69 | sys.exit(0) 70 | elif opt in ('-u', '--update'): 71 | sarg = arg.split(':') 72 | insert_record(sarg[0],sarg[1],redis_server) 73 | elif opt in ('-l', '--list'): 74 | print arg 75 | import_config(arg,redis_server) 76 | 77 | print '[-] Import Complete' 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | --------------------------------------------------------------------------------