diff --git a/README.md b/README.md index 1fed484..bd33fa7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ shadowsocks =========== -Current version: 1.3.7 [![Build Status](https://travis-ci.org/clowwindy/shadowsocks.png?branch=master)](https://travis-ci.org/clowwindy/shadowsocks) +Current version: 1.4.0 [![Build Status](https://travis-ci.org/clowwindy/shadowsocks.png?branch=master)](https://travis-ci.org/clowwindy/shadowsocks) shadowsocks is a lightweight tunnel proxy which can help you get through firewalls. diff --git a/setup.py b/setup.py index 86d7a0c..5ec5185 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,22 @@ -from setuptools import setup +from setuptools import setup with open('README.rst') as f: long_description = f.read() setup( - name = "shadowsocks", - version = "1.3.7", - license = 'MIT', - description = "a lightweight tunnel proxy", - author = 'clowwindy', - author_email = 'clowwindy42@gmail.com', - url = 'https://github.com/clowwindy/shadowsocks', - packages = ['shadowsocks'], + name="shadowsocks", + version="1.4.0", + license='MIT', + description="a lightweight tunnel proxy", + author='clowwindy', + author_email='clowwindy42@gmail.com', + url='https://github.com/clowwindy/shadowsocks', + packages=['shadowsocks'], package_data={ 'shadowsocks': ['README.rst', 'LICENSE', 'config.json'] }, - install_requires = ['setuptools', - ], + install_requires=['setuptools'], entry_points=""" [console_scripts] sslocal = shadowsocks.local:main @@ -29,6 +28,6 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: Proxy Servers', - ], + ], long_description=long_description, ) diff --git a/shadowsocks/event.py b/shadowsocks/event.py new file mode 100644 index 0000000..737d039 --- /dev/null +++ b/shadowsocks/event.py @@ -0,0 +1,147 @@ +#!/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. + +# from ssloop +# https://github.com/clowwindy/ssloop + + +import select +from collections import defaultdict + + +MODE_NULL = 0x00 +MODE_IN = 0x01 +MODE_OUT = 0x04 +MODE_ERR = 0x08 +MODE_HUP = 0x10 +MODE_NVAL = 0x20 + + +class EpollLoop(object): + + def __init__(self): + self._epoll = select.epoll() + + def poll(self, timeout): + return self._epoll.poll(timeout) + + def add_fd(self, fd, mode): + self._epoll.register(fd, mode) + + def remove_fd(self, fd): + self._epoll.unregister(fd) + + def modify_fd(self, fd, mode): + self._epoll.modify(fd, mode) + + +class KqueueLoop(object): + + MAX_EVENTS = 1024 + + def __init__(self): + self._kqueue = select.kqueue() + self._fds = {} + + def _control(self, fd, mode, flags): + events = [] + if mode & MODE_IN: + events.append(select.kevent(fd, select.KQ_FILTER_READ, flags)) + if mode & MODE_OUT: + events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags)) + for e in events: + self._kqueue.control([e], 0) + + def poll(self, timeout): + if timeout < 0: + timeout = None # kqueue behaviour + events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout) + results = defaultdict(lambda: MODE_NULL) + for e in events: + fd = e.ident + if e.filter == select.KQ_FILTER_READ: + results[fd] |= MODE_IN + elif e.filter == select.KQ_FILTER_WRITE: + results[fd] |= MODE_OUT + return results.iteritems() + + def add_fd(self, fd, mode): + self._fds[fd] = mode + self._control(fd, mode, select.KQ_EV_ADD) + + def remove_fd(self, fd): + self._control(fd, self._fds[fd], select.KQ_EV_DELETE) + del self._fds[fd] + + def modify_fd(self, fd, mode): + self.remove_fd(fd) + self.add_fd(fd, mode) + + +class SelectLoop(object): + + def __init__(self): + self._r_list = set() + self._w_list = set() + self._x_list = set() + + def poll(self, timeout): + r, w, x = select.select(self._r_list, self._w_list, self._x_list, + timeout) + results = defaultdict(lambda: MODE_NULL) + for p in [(r, MODE_IN), (w, MODE_OUT), (x, MODE_ERR)]: + for fd in p[0]: + results[fd] |= p[1] + return results.items() + + def add_fd(self, fd, mode): + if mode & MODE_IN: + self._r_list.add(fd) + if mode & MODE_OUT: + self._w_list.add(fd) + if mode & MODE_ERR: + self._x_list.add(fd) + + def remove_fd(self, fd): + if fd in self._r_list: + self._r_list.remove(fd) + if fd in self._w_list: + self._w_list.remove(fd) + if fd in self._x_list: + self._x_list.remove(fd) + + def modify_fd(self, fd, mode): + self.remove_fd(fd) + self.add_fd(fd, mode) + + +EventLoop = None + +if hasattr(select, 'epoll'): + EventLoop = EpollLoop +elif hasattr(select, 'kqueue'): + EventLoop = KqueueLoop +elif hasattr(select, 'select'): + EventLoop = SelectLoop +else: + raise Exception('can not find any available functions in select package') diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 11394c9..0453ea2 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # Copyright (c) 2014 clowwindy # diff --git a/shadowsocks/server.py b/shadowsocks/server.py index ba161fd..6b2c887 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # Copyright (c) 2014 clowwindy # @@ -27,6 +28,9 @@ if sys.version_info < (2, 6): else: import json +# do this before monkey patch +import udprelay + try: import gevent import gevent.monkey @@ -40,7 +44,6 @@ import select import threading import SocketServer import struct -import os import logging import getopt import encrypt @@ -205,6 +208,7 @@ def main(): server.key, server.method, server.timeout = key, METHOD, int(TIMEOUT) logging.info("starting server at %s:%d" % tuple(server.server_address[:2])) threading.Thread(target=server.serve_forever).start() + if __name__ == '__main__': try: diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py new file mode 100644 index 0000000..841c035 --- /dev/null +++ b/shadowsocks/udprelay.py @@ -0,0 +1,117 @@ +#!/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. + +# SOCKS5 UDP Request +# +----+------+------+----------+----------+----------+ +# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +# +----+------+------+----------+----------+----------+ +# | 2 | 1 | 1 | Variable | 2 | Variable | +# +----+------+------+----------+----------+----------+ + +# SOCKS5 UDP Response +# +----+------+------+----------+----------+----------+ +# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +# +----+------+------+----------+----------+----------+ +# | 2 | 1 | 1 | Variable | 2 | Variable | +# +----+------+------+----------+----------+----------+ + +# shadowsocks UDP Request (before encrypted) +# +------+----------+----------+----------+ +# | ATYP | DST.ADDR | DST.PORT | DATA | +# +------+----------+----------+----------+ +# | 1 | Variable | 2 | Variable | +# +------+----------+----------+----------+ + +# shadowsocks UDP Response (before encrypted) +# +------+----------+----------+----------+ +# | ATYP | DST.ADDR | DST.PORT | DATA | +# +------+----------+----------+----------+ +# | 1 | Variable | 2 | Variable | +# +------+----------+----------+----------+ + +# shadowsocks UDP Request and Response (after encrypted) +# +-------+--------------+ +# | IV | PAYLOAD | +# +-------+--------------+ +# | Fixed | Variable | +# +-------+--------------+ + +# HOW TO NAME THINGS +# ------------------ +# `dest` means destination server, which is from DST fields in the SOCKS5 +# request +# `local` means local server of shadowsocks +# `remote` means remote server of shadowsocks +# `client` means UDP clients that connects to other servers +# `server` means the UDP server that handle user requests + + +import threading +import socket +import event + + +class UDPRelay(object): + def __init__(self, listen_addr='127.0.0.1', listen_port=1080, + remote_addr='127.0.0.1', remote_port=8387, password=None, + method='table', timeout=300, is_local=True): + self._listen_addr = listen_addr + self._listen_port = listen_port + self._remote_addr = remote_addr + self._remote_port = remote_port + self._password = password + self._method = method + self._timeout = timeout + self._is_local = is_local + self._eventloop = event.EventLoop() + + def _handle_server(self, addr, sock, data): + # TODO + pass + + def _handle_client(self, addr, sock, data): + # TODO + pass + + def _run(self): + eventloop = self._eventloop + server_socket = self._server_socket + eventloop.add_fd(server_socket, event.MODE_IN) + is_local = self._is_local + while True: + r = eventloop.poll() + # TODO + + def start(self): + addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % + (self._listen_addr, self._listen_port)) + af, socktype, proto, canonname, sa = addrs[0] + server_socket = socket.socket(af, socktype, proto) + server_socket.bind((self._listen_addr, self._listen_port)) + server_socket.setblocking(False) + self._server_socket = server_socket + + threading.Thread(target=self._run).start()