From 16cb75e31e91d497905ce14c26f4c0b08d0eb952 Mon Sep 17 00:00:00 2001 From: Zhongke Chen Date: Sun, 24 Mar 2013 11:59:46 +0800 Subject: [PATCH] add SMART DIRECT feature --- README.md | 4 +++ shadowsocks/isdirect.py | 75 +++++++++++++++++++++++++++++++++++++++++ shadowsocks/local.py | 40 ++++++++++++++++------ 3 files changed, 109 insertions(+), 10 deletions(-) create mode 100755 shadowsocks/isdirect.py diff --git a/README.md b/README.md index b42145e..409f9de 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,10 @@ Please notice that some encryption methods are not available on some environment Performance ------------ +You may use -d option to enable SMART DRIRECT feature. If SMART DIRECT is enabled, connection will be established directly to destination IPs in China. + + python local.py -d + You may want to install gevent for better performance. $ sudo apt-get install python-gevent diff --git a/shadowsocks/isdirect.py b/shadowsocks/isdirect.py new file mode 100755 index 0000000..076e464 --- /dev/null +++ b/shadowsocks/isdirect.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import sys +import socket +import bisect + +def ip2int(ip): + return reduce(lambda x,y: x*256+y, [int(x) for x in ip.split('.')]) + +def init_geolite(): + import urllib2 + import zipfile + import StringIO + buf = StringIO.StringIO(urllib2.urlopen("http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip").read()) + geolite_begin = [] + geolite_end = [] + for line in zipfile.ZipFile(buf).open('GeoIPCountryWhois.csv').readlines(): + ip_begin, ip_end, int_begin, int_end, code = line.strip().split(',')[0:5] + if code == '"CN"': + geolite_begin.append(int(int_begin[1:-1])) + geolite_end.append(int(int_end[1:-1])) + for ip_begin, ip_end in internal_ip: + ipb = ip2int(ip_begin) + ipe = ip2int(ip_end) + i = bisect.bisect(geolite_begin, ipb) + geolite_begin.insert(i, ipb) + geolite_end.insert(i, ipe) + return (geolite_begin, geolite_end) + +def ischina(ip_int): + if geolite: + i = bisect.bisect_right(geolite[0], ip_int) -1 + if i == 0 or ip_int > geolite[1][i]: + return False + else: + return True + else: + return False + +def isdirect(hostname): + hostname = hostname.strip() + try: + ip = socket.gethostbyname(hostname) + except Exception, e: + ip = "0.0.0.0" + ip_int = ip2int(ip) + try: + is_china = ischina(ip_int) + except Exception, e: + is_china = False + + return is_china and \ + (ip_int not in blacklist_ip) and \ + (not any([hostname.endswith(i) for i in blacklist_domain])) \ + or \ + (any([hostname.endswith(i) for i in whitelist_domain])) + +#bad IPs returned by domestic DNS servers +blacklist_ip =[ip2int(ip) for ip in ['60.191.124.236', '180.168.41.175', '93.46.8.89', '203.98.7.65', '8.7.198.45', '78.16.49.15', '46.82.174.68', '243.185.187.39', '243.185.187.30', '159.106.121.75', '37.61.54.158', '159.24.3.173', '0.0.0.0']] +#domains hijacked +blacklist_domain = ['skype.com', 'youtube.com'] +#private IP ranges +internal_ip = [("127.0.0.0", "127.255.255.255"), ("192.168.0.0", "192.168.255.255"), ("172.16.0.0", "172.31.255.255"), ("10.0.0.0", "10.255.255.255")] +#private domain names +whitelist_domain = ['local', 'localhost' ] + +geolite = init_geolite() + +if __name__ == "__main__": + while 1: + line=sys.stdin.readline() + if line == "": + break + print("OK" if isdirect(line) else "ERR") + sys.stdout.flush() diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 3cbb28a..e1e46eb 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -35,6 +35,12 @@ except ImportError: gevent = None print >>sys.stderr, 'warning: gevent not found, using threading instead' +try: + import isdirect +except ImportError: + isdirect = None + print >>sys.stderr, 'error importing isdirect, SMART_DIRECT feature disabled' + import socket import select import SocketServer @@ -62,13 +68,16 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): class Socks5Server(SocketServer.StreamRequestHandler): - def handle_tcp(self, sock, remote): + def handle_tcp(self, sock, remote, direct): try: fdset = [sock, remote] while True: r, w, e = select.select(fdset, [], []) if sock in r: - data = self.encrypt(sock.recv(4096)) + if direct: + data = sock.recv(4096) + else: + data = self.encrypt(sock.recv(4096)) if len(data) <= 0: break result = send_all(remote, data) @@ -76,7 +85,10 @@ class Socks5Server(SocketServer.StreamRequestHandler): raise Exception('failed to send all data') if remote in r: - data = self.decrypt(remote.recv(4096)) + if direct: + data = remote.recv(4096) + else: + data = self.decrypt(remote.recv(4096)) if len(data) <= 0: break result = send_all(sock, data) @@ -127,24 +139,29 @@ class Socks5Server(SocketServer.StreamRequestHandler): addr_port = self.rfile.read(2) addr_to_send += addr_port port = struct.unpack('>H', addr_port) + direct = SMART_DIRECT and isdirect.isdirect(addr) try: reply = "\x05\x00\x00\x01" reply += socket.inet_aton('0.0.0.0') + struct.pack(">H", 2222) self.wfile.write(reply) # reply immediately - remote = socket.create_connection((SERVER, REMOTE_PORT)) - self.send_encrypt(remote, addr_to_send) - logging.info('connecting %s:%d' % (addr, port[0])) + if direct: + remote = socket.create_connection((addr, port[0])) + logging.info('direct connecting %s:%d' % (addr, port[0])) + else: + remote = socket.create_connection((SERVER, REMOTE_PORT)) + self.send_encrypt(remote, addr_to_send) + logging.info('connecting %s:%d' % (addr, port[0])) except socket.error, e: logging.warn(e) return - self.handle_tcp(sock, remote) + self.handle_tcp(sock, remote, direct) except socket.error, e: logging.warn(e) def main(): - global SERVER, REMOTE_PORT, PORT, KEY, METHOD, LOCAL, IPv6 + global SERVER, REMOTE_PORT, PORT, KEY, METHOD, LOCAL, IPv6, SMART_DIRECT logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s', @@ -167,9 +184,10 @@ def main(): METHOD = None LOCAL = '' IPv6 = False + SMART_DIRECT = False config_path = utils.find_config() - optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:6') + optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:6d') for key, value in optlist: if key == '-c': config_path = value @@ -178,8 +196,10 @@ def main(): logging.info('loading config from %s' % config_path) with open(config_path, 'rb') as f: config = json.load(f) - optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:6') + optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:6d') for key, value in optlist: + if key == '-d': + SMART_DIRECT = (isdirect is not None) if key == '-p': config['server_port'] = int(value) elif key == '-k':