diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..357232f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +.DS_Store +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6821fa8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - 2.6 + - 2.7 + - pypy +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq dnsutils build-essential + - pip install shadowsocks +script: + - python test.py -c tests/facebook.com + - python test.py -c tests/google.com + - python test.py -c tests/twitter.com + - python test.py -c tests/www.facebook.com diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..0ffd02d --- /dev/null +++ b/CHANGES @@ -0,0 +1,3 @@ +0.1 2014-06-22 +- Initial version + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98f608b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Shadowsocks + +Copyright (c) 2014 clowwindy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b73ebd1 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +ChinaDNS +========= + +[![PyPI version]][PyPI] [![Build Status]][Travis CI] + +A DNS forwarder that ignores incorrect(you know it) responses. + +ChinaDNS creates a DNS server at localhost. + +Install +------- + +#### OS X: + + pip install . + pip install chinadns + +#### Windows: + + easy_install pip + pip install chinadns + +#### Debian / Ubuntu: + + apt-get install python-pip + pip install chinadns + +#### CentOS: + + yum install python-setuptools + easy_install pip + pip install shadowdns + +Usage +----- + +Run `sudo chinadns` on your local machine. + +Set your DNS to 127.0.0.1. + +License +------- +MIT + +Bugs and Issues +---------------- +Please visit [Issue Tracker] + +Mailing list: http://groups.google.com/group/shadowsocks + + +[Build Status]: https://img.shields.io/travis/clowwindy/ChinaDNS/master.svg?style=flat +[Issue Tracker]: https://github.com/clowwindy/ChinaDNS/issues?state=open +[PyPI]: https://pypi.python.org/pypi/chinadns +[PyPI version]: https://img.shields.io/pypi/v/chinadns.svg?style=flat +[Shadowsocks]: https://github.com/clowwindy/shadowsocks +[Travis CI]: https://travis-ci.org/clowwindy/ChinaDNS diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f6d6b27 --- /dev/null +++ b/README.rst @@ -0,0 +1,62 @@ +ChinaDNS +======== + +A DNS forwarder that ignores incorrect(you know it) responses. + +ChinaDNS creates a DNS server at localhost. + +Install +------- + +OS X: +^^^^^ + +:: + + pip install . + pip install chinadns + +Windows: +^^^^^^^^ + +:: + + easy_install pip + pip install chinadns + +Debian / Ubuntu: +^^^^^^^^^^^^^^^^ + +:: + + apt-get install python-pip + pip install chinadns + +CentOS: +^^^^^^^ + +:: + + yum install python-setuptools + easy_install pip + pip install shadowdns + +Usage +----- + +Run ``sudo chinadns`` on your local machine. + +Set your DNS to 127.0.0.1. + +License +------- + +MIT + +Bugs and Issues +--------------- + +Please visit `Issue +Tracker `__ + +Mailing list: http://groups.google.com/group/shadowsocks diff --git a/chinadns/__init__.py b/chinadns/__init__.py new file mode 100644 index 0000000..013e4b7 --- /dev/null +++ b/chinadns/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python diff --git a/chinadns/dnsrelay.py b/chinadns/dnsrelay.py new file mode 100644 index 0000000..3405947 --- /dev/null +++ b/chinadns/dnsrelay.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import time +import socket +import errno +import logging +from shadowsocks import eventloop, asyncdns, lru_cache +from shadowsocks import utils as shadowsocks_utils + + +BUF_SIZE = 16384 + +CACHE_TIMEOUT = 10 + +GFW_LIST = set(["74.125.127.102", "74.125.155.102", "74.125.39.102", + "74.125.39.113", "209.85.229.138", "128.121.126.139", + "159.106.121.75", "169.132.13.103", "192.67.198.6", + "202.106.1.2", "202.181.7.85", "203.161.230.171", + "203.98.7.65", "207.12.88.98", "208.56.31.43", + "209.145.54.50", + "209.220.30.174", "209.36.73.33", "211.94.66.147", + "213.169.251.35", "216.221.188.182", "216.234.179.13", + "243.185.187.39", "37.61.54.158", "4.36.66.178", + "46.82.174.68", "59.24.3.173", "64.33.88.161", "64.33.99.47", + "64.66.163.251", "65.104.202.252", "65.160.219.113", + "66.45.252.237", "72.14.205.104", "72.14.205.99", + "78.16.49.15", "8.7.198.45", "93.46.8.89"]) + + +class DNSRelay(object): + def __init__(self, config): + self._loop = None + self._config = config + self._last_time = time.time() + + self._local_addr = (config['local_address'], 53) + self._remote_addr = (config['dns'], 53) + + addrs = socket.getaddrinfo(self._remote_addr[0], 53, 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if not addrs: + raise Exception("can't get addrinfo for DNS address") + + def add_to_loop(self, loop): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + loop.add_handler(self.handle_events) + + def handle_events(self, events): + pass + + +class UDPDNSRelay(DNSRelay): + def __init__(self, config): + DNSRelay.__init__(self, config) + + self._id_to_addr = lru_cache.LRUCache(CACHE_TIMEOUT) + self._local_sock = None + self._remote_sock = None + + sockets = [] + for addr in (self._local_addr, self._remote_addr): + addrs = socket.getaddrinfo(addr[0], addr[1], 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % addr) + af, socktype, proto, canonname, sa = addrs[0] + sock = socket.socket(af, socktype, proto) + sock.setblocking(False) + sockets.append(sock) + + self._local_sock, self._remote_sock = sockets + self._local_sock.bind(self._local_addr) + + def add_to_loop(self, loop): + DNSRelay.add_to_loop(self, loop) + + loop.add(self._local_sock, eventloop.POLL_IN) + loop.add(self._remote_sock, eventloop.POLL_IN) + + def _handle_local(self, sock): + data, addr = sock.recvfrom(BUF_SIZE) + header = asyncdns.parse_header(data) + if header: + try: + req_id = header[0] + req = asyncdns.parse_response(data) + self._id_to_addr[req_id] = addr + self._remote_sock.sendto(data, self._remote_addr) + logging.info('request %s', req.hostname) + except Exception as e: + import traceback + + traceback.print_exc() + logging.error(e) + + def _handle_remote(self, sock): + data, addr = sock.recvfrom(BUF_SIZE) + if data: + try: + header = asyncdns.parse_header(data) + if header: + req_id = header[0] + res = asyncdns.parse_response(data) + logging.info('response %s', res) + addr = self._id_to_addr.get(req_id, None) + if addr: + for answer in res.answers: + if answer and answer[0] in GFW_LIST: + return + self._local_sock.sendto(data, addr) + del self._id_to_addr[req_id] + except Exception as e: + import traceback + + traceback.print_exc() + logging.error(e) + + def handle_events(self, events): + for sock, fd, event in events: + if sock == self._local_sock: + self._handle_local(sock) + elif sock == self._remote_sock: + self._handle_remote(sock) + now = time.time() + if now - self._last_time > CACHE_TIMEOUT / 2: + self._id_to_addr.sweep() + + +class TCPDNSRelay(DNSRelay): + def __init__(self, config): + DNSRelay.__init__(self, config) + + self._local_to_remote = {} + self._remote_to_local = {} + + addrs = socket.getaddrinfo(self._local_addr[0], self._local_addr[1], 0, + socket.SOCK_STREAM, socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % self._local_addr) + af, socktype, proto, canonname, sa = addrs[0] + self._listen_sock = socket.socket(af, socktype, proto) + self._listen_sock.setblocking(False) + self._listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._listen_sock.bind(self._local_addr) + self._listen_sock.listen(1024) + + def _handle_conn(self, sock): + try: + local, addr = sock.accept() + addrs = socket.getaddrinfo(self._remote_addr[0], + self._remote_addr[1], 0, + socket.SOCK_STREAM, socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % + self._remote_addr) + af, socktype, proto, canonname, sa = addrs[0] + remote = socket.socket(af, socktype, proto) + local.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + remote.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + self._local_to_remote[local] = remote + self._remote_to_local[remote] = local + + self._loop.add(local, 0) + self._loop.add(remote, eventloop.POLL_OUT) + try: + remote.connect(self._remote_addr) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in (errno.EINPROGRESS, + errno.EAGAIN): + pass + else: + raise + except (OSError, IOError) as e: + logging.error(e) + + def _destroy(self, local, remote): + if local in self._local_to_remote: + self._loop.remove(local) + self._loop.remove(remote) + del self._local_to_remote[local] + del self._remote_to_local[remote] + local.close() + remote.close() + else: + logging.error('already destroyed') + + def _handle_local(self, local, event): + remote = self._local_to_remote[local] + if event & eventloop.POLL_ERR: + self._destroy(local, remote) + elif event & eventloop.POLL_IN: + try: + data = local.recv(BUF_SIZE) + if not data: + self._destroy(local, remote) + else: + remote.send(data) + except (OSError, IOError) as e: + self._destroy(local, self._local_to_remote[local]) + logging.error(e) + + def _handle_remote(self, remote, event): + local = self._remote_to_local[remote] + if event & eventloop.POLL_ERR: + self._destroy(local, remote) + elif event & eventloop.POLL_OUT: + self._loop.modify(remote, eventloop.POLL_IN) + self._loop.modify(local, eventloop.POLL_IN) + elif event & eventloop.POLL_IN: + try: + data = remote.recv(BUF_SIZE) + if not data: + self._destroy(local, remote) + else: + try: + res = asyncdns.parse_response(data[2:]) + if res: + logging.info('response %s', res) + except Exception as e: + logging.error(e) + local.send(data) + except (OSError, IOError) as e: + self._destroy(local, remote) + logging.error(e) + + def add_to_loop(self, loop): + DNSRelay.add_to_loop(self, loop) + loop.add(self._listen_sock, eventloop.POLL_IN) + + def handle_events(self, events): + for sock, fd, event in events: + if sock == self._listen_sock: + self._handle_conn(sock) + elif sock in self._local_to_remote: + self._handle_local(sock, event) + elif sock in self._remote_to_local: + self._handle_remote(sock, event) + # TODO implement timeout + + +def main(): + shadowsocks_utils.check_python() + + shadowsocks_utils.get_config(True) + config = {} + config['local_address'] = config.get('local_address', '127.0.0.1') + config['dns'] = config.get('dns', '8.8.8.8') + logging.info("starting dns at %s:%d" % (config['local_address'], 53)) + + loop = eventloop.EventLoop() + + udprelay = UDPDNSRelay(config) + udprelay.add_to_loop(loop) + tcprelay = TCPDNSRelay(config) + tcprelay.add_to_loop(loop) + + loop.run() + + +if __name__ == '__main__': + main() diff --git a/config.json b/config.json new file mode 100644 index 0000000..c10cd7b --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "server":"my_server_ip", + "server_port":8388, + "local_address": "127.0.0.1", + "local_port":53, + "password":null, + "method":"aes-256-cfb", + "dns":"8.8.8.8" +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7e2c949 --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +from setuptools import setup + + +with open('README.rst') as f: + long_description = f.read() + +setup( + name="chinadns", + version="0.1.0", + license='MIT', + description="A DNS forwarder that ignore corrupted responses", + author='clowwindy', + author_email='clowwindy42@gmail.com', + url='https://github.com/clowwindy/shadowdns', + packages=['shadowdns'], + package_data={ + 'chinadns': ['README.rst', 'LICENSE', 'config.json'] + }, + install_requires=[ + 'shadowsocks==2.0.7' + ], + entry_points=""" + [console_scripts] + ssdns = shadowdns.dnsrelay:main + """, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: Proxy Servers', + ], + long_description=long_description, +) diff --git a/test.py b/test.py new file mode 100755 index 0000000..c7865b1 --- /dev/null +++ b/test.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import sys +sys.path.insert(0, 'shadowsocks') +import os +import signal +import select +from subprocess import Popen, PIPE + +with open(sys.argv[-1]) as f: + dig_cmd = f.read() +p1 = Popen(['sudo', sys.executable, 'chinadns/dnsrelay.py'], shell=False, + bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) +p2 = None + +try: + local_ready = False + server_ready = False + fdset = [p1.stdout, p1.stderr] + while True: + r, w, e = select.select(fdset, [], fdset) + if e: + break + + for fd in r: + line = fd.readline() + sys.stdout.write(line) + if line.find('starting dns') >= 0: + local_ready = True + + if local_ready and p2 is None: + p2 = Popen(dig_cmd.split(), shell=False, bufsize=0, close_fds=True) + break + + if p2 is not None: + r = p2.wait() + if r == 0: + print 'test passed' + sys.exit(r) + +finally: + for p in [p1, p2]: + try: + os.kill(p.pid, signal.SIGTERM) + except OSError: + pass + +sys.exit(-1) diff --git a/tests/facebook.com b/tests/facebook.com new file mode 100644 index 0000000..b833255 --- /dev/null +++ b/tests/facebook.com @@ -0,0 +1 @@ +dig @127.0.0.1 any facebook.com diff --git a/tests/google.com b/tests/google.com new file mode 100644 index 0000000..dc6a5e8 --- /dev/null +++ b/tests/google.com @@ -0,0 +1 @@ +dig @127.0.0.1 +tcp any google.com \ No newline at end of file diff --git a/tests/twitter.com b/tests/twitter.com new file mode 100644 index 0000000..87ddaa5 --- /dev/null +++ b/tests/twitter.com @@ -0,0 +1 @@ +dig @127.0.0.1 any twitter.com \ No newline at end of file diff --git a/tests/www.facebook.com b/tests/www.facebook.com new file mode 100644 index 0000000..15126e4 --- /dev/null +++ b/tests/www.facebook.com @@ -0,0 +1 @@ +dig @127.0.0.1 any www.facebook.com