├── .gitignore ├── .travis.yml ├── ChangeLog.txt ├── README.md ├── __init__.py ├── bg_worker.py ├── cache.sqlite ├── caches.py ├── config.py ├── dnsproxy.py ├── dnsserver.py ├── requirements.txt ├── run.bat ├── run.sh ├── servers.py ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .idea 37 | *.iml 38 | 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 5 | install: pip install -r requirements.txt --use-mirrors 6 | # command to run tests, e.g. python setup.py test 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | # BlackHolePy changelog 2 | 3 | ## 2013-07-15 1.0 4 | * added sqlite as a backend for cache 5 | * fixed a bug which can freeze BlackHolePy 6 | * fixed a bug with error message:"OperationalError: Could not open database" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BlackHolePy 2 | =========== 3 | 4 | BlackHolePy是一个迷你型的DNS(代理)服务器。 5 | 它的主要特色: 6 | 7 | 1) 支持TCP DNS并支持白名单,从而在防止DNS污染的同时支持了内部私有域名解析。 8 | 如果你的企业或组织在内部架设了自己的DNS Server,那么可以配置 config.py 里面的 WHITE_DNSS 。 9 | 2) 内置了Cache, 带给你飞一般的感觉。尤其是访问国外网站的时候。 10 | 11 | 12 | 13 | 运行需求 14 | =========== 15 | Python 2.7 或者 PyPy 2.0.2 16 | 如果能安装 GEvent 和 dnspython 那就最好了。不装也能跑。 17 | 安装GEvent以后,BlackHolePy运行在单线程模式,快捷并节约系统资源。 18 | 在 PyPy 2.0.2 下测试通过。但是PyPy的coroutine暂时还没有支持。 19 | 20 | 运行 21 | =========== 22 | sudo ./run.sh (Linux or Mac) 当以root用户运行时,sudo 是不需要的。 23 | ./run.bat (Windows) 24 | 25 | 然后把你的DNS服务器配置到 127.0.0.1 即可。 26 | 27 | 感谢 28 | =========== 29 | 本项目是基于以下两个项目的思路,重新编写的。 30 | 31 | https://github.com/henices/Tcp-DNS-proxy 32 | https://github.com/code4craft/blackhole 33 | 34 | 在此感谢这两个项目的作者! 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | import struct 6 | import socket 7 | import traceback as tb 8 | 9 | reload(sys) 10 | sys.setdefaultencoding("utf-8") -------------------------------------------------------------------------------- /bg_worker.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # cody by linker.lin@me.com 4 | 5 | __author__ = 'linkerlin' 6 | 7 | import threading 8 | import Queue 9 | import time 10 | 11 | 12 | class BGWorker(threading.Thread): 13 | def __init__(self): 14 | threading.Thread.__init__(self) 15 | self.q = Queue.Queue() 16 | 17 | def post(self, job): 18 | self.q.put(job) 19 | 20 | def run(self): 21 | while 1: 22 | job = None 23 | try: 24 | job = self.q.get(block=True) 25 | if job: 26 | job() 27 | except Exception as ex: 28 | print "Error,job exception:", ex.message, type(ex) 29 | time.sleep(0.005) 30 | else: 31 | #print "job: ", job, " done" 32 | print ".", 33 | pass 34 | finally: 35 | time.sleep(0.005) 36 | 37 | 38 | bgworker = BGWorker() 39 | bgworker.setDaemon(True) 40 | bgworker.start() 41 | -------------------------------------------------------------------------------- /cache.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkerlin/BlackHolePy/4ce5047c31c3d0750c31192737b0bcde1d3e0a99/cache.sqlite -------------------------------------------------------------------------------- /caches.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | 6 | reload(sys) 7 | sys.setdefaultencoding("utf-8") 8 | import collections 9 | import functools 10 | from itertools import ifilterfalse 11 | from heapq import nsmallest 12 | from operator import itemgetter 13 | import threading 14 | import cPickle as p 15 | import datetime as dt 16 | import base64 17 | import random 18 | import time 19 | from hashlib import md5 20 | 21 | 22 | class Counter(dict): 23 | 'Mapping where default values are zero' 24 | 25 | def __missing__(self, key): 26 | return 0 27 | 28 | 29 | def sqlite_cache(timeout_seconds=100, cache_none=True, ignore_args={}): 30 | import sqlite3 31 | 32 | def decorating_function(user_function, 33 | len=len, iter=iter, tuple=tuple, sorted=sorted, KeyError=KeyError): 34 | with sqlite3.connect(u"cache.sqlite") as cache_db: 35 | cache_cursor = cache_db.cursor() 36 | cache_table = u"table_" + user_function.func_name 37 | #print cache_table 38 | cache_cursor.execute( 39 | u"CREATE TABLE IF NOT EXISTS " + cache_table 40 | + u" (key CHAR(36) PRIMARY KEY, value TEXT, update_time timestamp);") 41 | cache_db.commit() 42 | kwd_mark = object() # separate positional and keyword args 43 | lock = threading.Lock() 44 | 45 | @functools.wraps(user_function) 46 | def wrapper(*args, **kwds): 47 | result = None 48 | cache_db = None 49 | # cache key records both positional and keyword args 50 | key = args 51 | if kwds: 52 | real_kwds = [] 53 | for k in kwds: 54 | if k not in ignore_args: 55 | real_kwds.append((k, kwds[k])) 56 | key += (kwd_mark,) 57 | if len(real_kwds) > 0: 58 | key += tuple(sorted(real_kwds)) 59 | while cache_db is None: 60 | try: 61 | with lock: 62 | cache_db = sqlite3.connect(u"cache.sqlite") 63 | except sqlite3.OperationalError as ex: 64 | print ex 65 | time.sleep(0.05) 66 | # get cache entry or compute if not found 67 | try: 68 | cache_cursor = cache_db.cursor() 69 | key_str = str(key) # 更加宽泛的Key,只检查引用的地址,而不管内容,“浅”检查,更好的配合方法,但是可能会出现过于宽泛的问题 70 | key_str = md5(key_str).hexdigest() # base64.b64encode(key_str) 71 | #print "key_str:", key_str[:60] 72 | with lock: 73 | cache_cursor.execute( 74 | u"select * from " + cache_table 75 | + u" where key = ? order by update_time desc", (key_str,)) 76 | for record in cache_cursor: 77 | dump_data = base64.b64decode(record[1]) 78 | result = p.loads(dump_data) 79 | #print "cached:", md5(result).hexdigest() 80 | break 81 | if result is not None: 82 | with lock: 83 | wrapper.hits += 1 84 | print "hits", wrapper.hits, "miss", wrapper.misses, wrapper 85 | else: 86 | result = user_function(*args, **kwds) 87 | if result is None and cache_none == False: 88 | return 89 | value = base64.b64encode(p.dumps(result, p.HIGHEST_PROTOCOL)) 90 | while 1: 91 | try: 92 | cache_cursor.execute(u"REPLACE INTO " + cache_table + u" VALUES(?,?,?)", 93 | (key_str, value, dt.datetime.now())) 94 | except sqlite3.OperationalError as ex: 95 | print ex, "retry update db." 96 | else: 97 | cache_db.commit() 98 | with lock: 99 | wrapper.misses += 1 100 | break 101 | finally: 102 | if random.random() > 0.999: 103 | timeout = dt.datetime.now() - dt.timedelta(seconds=timeout_seconds) 104 | with lock: 105 | cache_cursor.execute(u"DELETE FROM " + cache_table + u" WHERE update_time < datetime(?)", 106 | (str(timeout),)) 107 | with lock: 108 | cache_db.commit() 109 | cache_db.close() 110 | return result 111 | 112 | def clear(): 113 | with lock: 114 | wrapper.hits = wrapper.misses = 0 115 | 116 | wrapper.hits = wrapper.misses = 0 117 | wrapper.clear = clear 118 | return wrapper 119 | 120 | return decorating_function 121 | 122 | 123 | def lru_cache(maxsize=100, cache_none=True, ignore_args=[]): 124 | '''Least-recently-used cache decorator. 125 | 126 | Arguments to the cached function must be hashable. 127 | Cache performance statistics stored in f.hits and f.misses. 128 | Clear the cache with f.clear(). 129 | http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used 130 | 131 | ''' 132 | maxqueue = maxsize * 10 133 | 134 | def decorating_function(user_function, 135 | len=len, iter=iter, tuple=tuple, sorted=sorted, KeyError=KeyError): 136 | cache = {} # mapping of args to results 137 | queue = collections.deque() # order that keys have been used 138 | refcount = Counter() # times each key is in the queue 139 | sentinel = object() # marker for looping around the queue 140 | kwd_mark = object() # separate positional and keyword args 141 | 142 | # lookup optimizations (ugly but fast) 143 | queue_append, queue_popleft = queue.append, queue.popleft 144 | queue_appendleft, queue_pop = queue.appendleft, queue.pop 145 | lock = threading.RLock() 146 | 147 | @functools.wraps(user_function) 148 | def wrapper(*args, **kwds): 149 | with lock: 150 | # cache key records both positional and keyword args 151 | key = args 152 | if kwds: 153 | real_kwds = [] 154 | for k in kwds: 155 | if k not in ignore_args: 156 | real_kwds.append((k, kwds[k])) 157 | key += (kwd_mark,) 158 | if len(real_kwds) > 0: 159 | key += tuple(sorted(real_kwds)) 160 | #print "key", key 161 | 162 | # record recent use of this key 163 | queue_append(key) 164 | refcount[key] += 1 165 | 166 | # get cache entry or compute if not found 167 | try: 168 | result = cache[key] 169 | wrapper.hits += 1 170 | #print "hits", wrapper.hits, "miss", wrapper.misses, wrapper 171 | except KeyError: 172 | result = user_function(*args, **kwds) 173 | if result is None and cache_none == False: 174 | return 175 | cache[key] = result 176 | wrapper.misses += 1 177 | 178 | # purge least recently used cache entry 179 | if len(cache) > maxsize: 180 | key = queue_popleft() 181 | refcount[key] -= 1 182 | while refcount[key]: 183 | key = queue_popleft() 184 | refcount[key] -= 1 185 | if key in cache: 186 | del cache[key] 187 | if key in refcount: 188 | refcount[key] 189 | finally: 190 | pass 191 | 192 | # periodically compact the queue by eliminating duplicate keys 193 | # while preserving order of most recent access 194 | if len(queue) > maxqueue: 195 | refcount.clear() 196 | queue_appendleft(sentinel) 197 | for key in ifilterfalse(refcount.__contains__, 198 | iter(queue_pop, sentinel)): 199 | queue_appendleft(key) 200 | refcount[key] = 1 201 | 202 | return result 203 | 204 | def clear(): 205 | cache.clear() 206 | queue.clear() 207 | refcount.clear() 208 | wrapper.hits = wrapper.misses = 0 209 | 210 | wrapper.hits = wrapper.misses = 0 211 | wrapper.clear = clear 212 | return wrapper 213 | 214 | return decorating_function 215 | 216 | 217 | def lfu_cache(maxsize=100): 218 | '''Least-frequenty-used cache decorator. 219 | 220 | Arguments to the cached function must be hashable. 221 | Cache performance statistics stored in f.hits and f.misses. 222 | Clear the cache with f.clear(). 223 | http://en.wikipedia.org/wiki/Least_Frequently_Used 224 | 225 | ''' 226 | 227 | def decorating_function(user_function): 228 | cache = {} # mapping of args to results 229 | use_count = Counter() # times each key has been accessed 230 | kwd_mark = object() # separate positional and keyword args 231 | lock = threading.RLock() 232 | 233 | @functools.wraps(user_function) 234 | def wrapper(*args, **kwds): 235 | with lock: 236 | key = args 237 | if kwds: 238 | key += (kwd_mark,) + tuple(sorted(kwds.items())) 239 | use_count[key] += 1 240 | 241 | # get cache entry or compute if not found 242 | try: 243 | result = cache[key] 244 | wrapper.hits += 1 245 | except KeyError: 246 | result = user_function(*args, **kwds) 247 | cache[key] = result 248 | wrapper.misses += 1 249 | 250 | # purge least frequently used cache entry 251 | if len(cache) > maxsize: 252 | for key, _ in nsmallest(maxsize // 10, 253 | use_count.iteritems(), 254 | key=itemgetter(1)): 255 | del cache[key], use_count[key] 256 | 257 | return result 258 | 259 | def clear(): 260 | cache.clear() 261 | use_count.clear() 262 | wrapper.hits = wrapper.misses = 0 263 | 264 | wrapper.hits = wrapper.misses = 0 265 | wrapper.clear = clear 266 | return wrapper 267 | 268 | return decorating_function 269 | 270 | 271 | if __name__ == '__main__': 272 | 273 | @lru_cache(maxsize=20, ignore_args=["y"]) 274 | def f(x, y): 275 | return 3 * x + y 276 | 277 | domain = range(5) 278 | from random import choice 279 | 280 | for i in range(1000): 281 | r = f(choice(domain), y=choice(domain)) 282 | 283 | print(f.hits, f.misses) 284 | 285 | @lfu_cache(maxsize=20) 286 | def f(x, y): 287 | return 3 * x + y 288 | 289 | domain = range(5) 290 | from random import choice 291 | 292 | for i in range(1000): 293 | r = f(choice(domain), choice(domain)) 294 | 295 | print(f.hits, f.misses) 296 | 297 | @sqlite_cache() 298 | def f2(x, y): 299 | return 3 * x + y 300 | 301 | domain = range(50) 302 | for i in range(1000): 303 | r = f2(choice(domain), y=choice(domain)) 304 | print(f2.hits, f2.misses) -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | 6 | 7 | reload(sys) 8 | sys.setdefaultencoding("utf-8") 9 | 10 | DNSS = [ 11 | ('8.8.8.8', 53, {"tcp",}), 12 | ('8.8.4.4', 53, {"tcp",}), 13 | ('208.67.222.222', 53, {"tcp",}), 14 | ('208.67.220.220', 53, {"tcp",}), 15 | 16 | 17 | ] 18 | 19 | 20 | 21 | # a white dns server will service white list domains 22 | WHITE_DNSS = [ 23 | ("61.152.248.83", 53, {"udp",}, ["baidu.com", "qq.com"]), 24 | ] 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /dnsproxy.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | import struct 6 | import threading 7 | import SocketServer 8 | import optparse 9 | try: 10 | from dns import message as m 11 | except ImportError as ex: 12 | print "cannot find dnspython" 13 | try: 14 | from gevent import monkey 15 | monkey.patch_all() 16 | except ImportError as ex: 17 | print "cannot find gevent" 18 | 19 | import config 20 | from dnsserver import DNSServer 21 | from servers import Servers 22 | 23 | 24 | reload(sys) 25 | sys.setdefaultencoding("utf-8") 26 | 27 | from dnsserver import bytetodomain 28 | 29 | 30 | class DNSProxy(SocketServer.ThreadingMixIn, SocketServer.UDPServer): 31 | SocketServer.ThreadingMixIn.daemon_threads = True 32 | allow_reuse_address = True 33 | 34 | def __init__(self, address=("0.0.0.0", 53), VERBOSE=2): 35 | self.VERBOSE = VERBOSE 36 | print "listening at:", address 37 | SELF = self 38 | 39 | class ProxyHandle(SocketServer.BaseRequestHandler): 40 | # Ctrl-C will cleanly kill all spawned threads 41 | daemon_threads = True 42 | # much faster rebinding 43 | allow_reuse_address = True 44 | 45 | def handle(self): 46 | data = self.request[0] 47 | socket = self.request[1] 48 | addr = self.client_address 49 | DNSProxy.transfer(SELF, data, addr, socket) 50 | 51 | SocketServer.UDPServer.__init__(self, address, ProxyHandle) 52 | 53 | def loadConfig(self, config): 54 | self.DNSS = config.DNSS 55 | self.servers = Servers() 56 | for s in self.DNSS: 57 | assert len(s) == 3 58 | ip, port, type_of_server = s 59 | self.servers.addDNSServer(DNSServer(ip, port, type_of_server, self.VERBOSE)) 60 | self.WHITE_DNSS = config.WHITE_DNSS 61 | for ws in self.WHITE_DNSS: 62 | assert len(ws) == 4 63 | ip, port, type_of_server, white_list = ws 64 | self.servers.addWhiteDNSServer(DNSServer(ip, port, type_of_server, self.VERBOSE, white_list)) 65 | 66 | 67 | def transfer(self, query_data, addr, server): 68 | if not query_data: return 69 | domain = bytetodomain(query_data[12:-4]) 70 | qtype = struct.unpack('!h', query_data[-4:-2])[0] 71 | #print 'domain:%s, qtype:%x, thread:%d' % (domain, qtype, threading.activeCount()) 72 | sys.stdout.flush() 73 | response = None 74 | for i in range(9): 75 | response = self.servers.query(query_data) 76 | if response: 77 | # udp dns packet no length 78 | server.sendto(response[2:], addr) 79 | break 80 | if response is None: 81 | print "[ERROR] Tried 9 times and failed to resolve %s" % domain 82 | return 83 | 84 | 85 | 86 | def run_server(): 87 | print '>> Please wait program init....' 88 | print '>> Init finished!' 89 | print '>> Now you can set dns server to 127.0.0.1' 90 | 91 | parser = optparse.OptionParser() 92 | parser.add_option("-v", dest="verbose", default="0", help="Verbosity level, 0-2, default is 0") 93 | options, _ = parser.parse_args() 94 | 95 | proxy = DNSProxy(VERBOSE=options.verbose) 96 | proxy.loadConfig(config) 97 | 98 | proxy.serve_forever() 99 | proxy.shutdown() 100 | 101 | if __name__ == '__main__': 102 | run_server() -------------------------------------------------------------------------------- /dnsserver.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | import struct 6 | import socket 7 | import traceback as tb 8 | import re 9 | 10 | reload(sys) 11 | sys.setdefaultencoding("utf-8") 12 | 13 | #--------------------------------------------------------------- 14 | # bytetodomain 15 | # 03www06google02cn00 => www.google.cn 16 | #-------------------------------------------------------------- 17 | def bytetodomain(s): 18 | domain = '' 19 | i = 0 20 | length = struct.unpack('!B', s[0:1])[0] 21 | 22 | while length != 0: 23 | i += 1 24 | domain += s[i:i + length] 25 | i += length 26 | length = struct.unpack('!B', s[i:i + 1])[0] 27 | if length != 0: 28 | domain += '.' 29 | 30 | return domain 31 | 32 | 33 | class DNSServer(object): 34 | def __init__(self, ip, port=53, type_of_server=("tcp", "udp"), VERBOSE=0, white_list=None): 35 | if white_list is None: # ref: http://blog.amir.rachum.com/post/54770419679/python-common-newbie-mistakes-part-1 36 | white_list = [] # Default values for functions in Python are instantiated when the function is defined, not when it’s called. 37 | self.VERBOSE = VERBOSE 38 | self.white_list = white_list 39 | if len(self.white_list) > 0: 40 | self.initWhiteList() 41 | self.type_of_server = type_of_server 42 | self.ip = ip 43 | if type(port) == "str" or type(port) == "unicode": 44 | port = int(port) 45 | self.port = port 46 | self.TIMEOUT = 20 47 | self.ok = 0 48 | self.error = 0 49 | 50 | def initWhiteList(self): 51 | self.patterns = [] 52 | for w in self.white_list: 53 | p = re.compile(w, re.IGNORECASE) 54 | self.patterns.append(p) 55 | 56 | def __str__(self): 57 | return "DNS Server @ %s:%d %s" % (self.ip, self.port, str(self.type_of_server)) 58 | 59 | def isUDPServer(self): 60 | return "udp" in self.type_of_server 61 | 62 | def isTCPServer(self): 63 | return "tcp" in self.type_of_server 64 | 65 | def address(self): 66 | return (self.ip, int(self.port)) 67 | 68 | def suppressed(self): 69 | self.error -= 1 70 | print self, "suppressed" 71 | return None 72 | 73 | def needToSuppress(self): 74 | return self.error > (self.ok * 10) and self.error > 10 75 | 76 | def checkQuery(self, query_data): 77 | m = None 78 | domain = bytetodomain(query_data[12:-4]) 79 | for p in self.patterns: 80 | m = p.match(domain) 81 | if m: break 82 | if not m: 83 | return None 84 | print "white list match:", domain, self 85 | return self.query(query_data) 86 | 87 | 88 | def query(self, query_data): 89 | if self.needToSuppress(): 90 | return self.suppressed() 91 | buffer_length = struct.pack('!h', len(query_data)) 92 | data = None 93 | s = None 94 | try: 95 | if self.isTCPServer(): 96 | #print "tcp",len(query_data) 97 | sendbuf = buffer_length + query_data 98 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 99 | s.settimeout(self.TIMEOUT) # set socket timeout 100 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 101 | #print "connect to ", self.address() 102 | s.connect(self.address()) 103 | #print "send", len(sendbuf) 104 | s.send(sendbuf) 105 | data = s.recv(2048) 106 | #print "data:", data 107 | elif self.isUDPServer(): 108 | #print "udp" 109 | sendbuf = query_data 110 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 111 | s.settimeout(self.TIMEOUT) # set socket timeout 112 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 113 | #print type(querydata),(server, int(port)) 114 | s.sendto(sendbuf, self.address()) 115 | r, serv = s.recvfrom(1024) 116 | data = struct.pack('!h', len(r)) + r 117 | except Exception, e: 118 | self.error += 1 119 | print '[ERROR] QueryDNS: %s %s' % e.message, type(e) 120 | tb.print_stack() 121 | else: 122 | self.ok += 1 123 | finally: 124 | if s: s.close() 125 | if int(self.VERBOSE) > 0: 126 | try: 127 | self.showInfo(query_data, 0) 128 | self.showInfo(data[2:], 1) 129 | except TypeError as ex: 130 | #print ex 131 | pass 132 | finally: 133 | pass 134 | return data 135 | 136 | #---------------------------------------------------- 137 | # show dns packet information 138 | #---------------------------------------------------- 139 | def showInfo(self, data, direction): 140 | try: 141 | from dns import message as m 142 | except ImportError: 143 | print "Install dnspython module will give you more response infomation." 144 | else: 145 | if direction == 0: 146 | print "query:\n\t", "\n\t".join(str(m.from_wire(data)).split("\n")) 147 | print "\n================" 148 | elif direction == 1: 149 | print "response:\n\t", "\n\t".join(str(m.from_wire(data)).split("\n")) 150 | print "\n================" 151 | 152 | 153 | if __name__ == "__main__": 154 | s = DNSServer("8.8.8.8") 155 | print s -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gevent 2 | dnspython 3 | 4 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | python dnsproxy.py -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python dnsproxy.py -------------------------------------------------------------------------------- /servers.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | import struct 6 | try: 7 | from dns import message as m 8 | except ImportError as ex: 9 | print "cannot find dnspython" 10 | 11 | from dnsserver import bytetodomain 12 | from caches import * 13 | 14 | 15 | reload(sys) 16 | sys.setdefaultencoding("utf-8") 17 | 18 | from dnsserver import DNSServer 19 | from random import sample 20 | import base64 21 | 22 | 23 | class Servers(object): 24 | def __init__(self): 25 | self.dns_servers = {} 26 | self.white_servers = [] 27 | 28 | def addDNSServer(self, dns_server): 29 | assert isinstance(dns_server, DNSServer) 30 | self.dns_servers[dns_server.address()] = dns_server 31 | 32 | def addWhiteDNSServer(self, dns_server): 33 | assert isinstance(dns_server, DNSServer) 34 | self.white_servers.append(dns_server) 35 | 36 | def whiteListFirst(self, query_data): 37 | if len(self.white_servers): 38 | for s in self.white_servers: 39 | ret = s.checkQuery(query_data) 40 | if ret: 41 | return ret 42 | return None 43 | 44 | def query(self, query_data): 45 | domain = bytetodomain(query_data[12:-4]) 46 | qtype = struct.unpack('!h', query_data[-4:-2])[0] 47 | id = struct.unpack('!h', query_data[0:2])[0] 48 | #print "id", id 49 | #msg = [line for line in str(m.from_wire(query_data)).split('\n') if line.find("id", 0, -1) < 0] 50 | msg = query_data[4:] 51 | responce = self._query(tuple(msg), 52 | query_data=query_data) # query_data must be written as a named argument, because of cache's ignore_args 53 | if responce: 54 | return responce[0:2] + query_data[0:2] + responce[4:] 55 | else: 56 | return responce 57 | 58 | @sqlite_cache(timeout_seconds=800000, cache_none=False, ignore_args={"query_data"}) 59 | def _query(self, msg, query_data): 60 | #print msg 61 | ret = self.whiteListFirst(query_data) 62 | if ret: 63 | return ret 64 | # random select a server 65 | key = sample(self.dns_servers, 1)[0] 66 | #print key 67 | server = self.dns_servers[key] 68 | return server.query(query_data) 69 | 70 | 71 | if __name__ == "__main__": 72 | ss = Servers() 73 | s = DNSServer("8.8.8.8") 74 | ss.addDNSServer(s) 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import find_packages 3 | 4 | URL = "https://github.com/linkerlin/BlackHolePy" 5 | 6 | setup( 7 | name='BlackHolePy', 8 | version='1.0', 9 | packages=find_packages(exclude=["tests.*", "tests"]), 10 | py_modules=['dnsproxy','servers','config','caches','bg_worker','__init__','dnsserver'], 11 | license='MIT', 12 | author='linkerlin', 13 | author_email='linker.lin@me.com', 14 | url=URL, 15 | description='A DNS proxy which using tcp as transport protocol.' 16 | 'It also has a cache which can speed up the process of dns queries.', 17 | long_description=file("README.md").readlines(), 18 | ) 19 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'linkerlin' 4 | import sys 5 | import struct 6 | try: 7 | from dns import message as m 8 | except ImportError as ex: 9 | print "cannot find dnspython" 10 | 11 | from dnsserver import bytetodomain 12 | from caches import * 13 | import collections 14 | import functools 15 | from itertools import ifilterfalse 16 | from heapq import nsmallest 17 | from operator import itemgetter 18 | import threading 19 | import cPickle as p 20 | import datetime as dt 21 | import base64 22 | import random 23 | from random import choice 24 | 25 | def testSqliteCache(): 26 | @sqlite_cache() 27 | def f2(x, y): 28 | return 3 * x + y 29 | 30 | domain = range(50) 31 | for i in range(1000): 32 | r = f2(choice(domain), y=choice(domain)) 33 | print(f2.hits, f2.misses) 34 | assert f2.hits>0 35 | 36 | 37 | class R(object): 38 | def __init__(self, name): 39 | if isinstance(name, str): 40 | self.name = unicode(name) 41 | else: 42 | self.name = name 43 | 44 | def __str__(self): 45 | return self.name.decode("utf-8") 46 | def __unicode__(self): 47 | return self.name 48 | 49 | def __enter__(self): 50 | print "enter:",self 51 | def __exit__(self, exc_type, exc_val, exc_tb): 52 | print "exit:",self 53 | 54 | with R("A") as a, R("B") as b: # require A then require B 55 | print "..." --------------------------------------------------------------------------------