diff --git a/.gitignore b/.gitignore index 357232f..fb96264 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ develop-eggs pip-log.txt # Unit test / coverage reports +htmlcov .coverage .tox diff --git a/.jenkins.sh b/.jenkins.sh new file mode 100755 index 0000000..4c85f1c --- /dev/null +++ b/.jenkins.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +result=0 + +function run_test { + printf '\e[0;36m' + echo "running test: $command $@" + printf '\e[0m' + + $command "$@" + status=$? + if [ $status -ne 0 ]; then + printf '\e[0;31m' + echo "test failed: $command $@" + printf '\e[0m' + echo + result=1 + else + printf '\e[0;32m' + echo OK + printf '\e[0m' + echo + fi + return 0 +} + +coverage erase +mkdir tmp +run_test pep8 . +run_test pyflakes . +run_test coverage run tests/nose_plugin.py -v +run_test python setup.py sdist +run_test tests/test_daemon.sh +run_test python tests/test.py --with-coverage -c tests/aes.json +run_test python tests/test.py --with-coverage -c tests/aes-ctr.json +run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json +run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json +run_test python tests/test.py --with-coverage -c tests/rc4-md5.json +run_test python tests/test.py --with-coverage -c tests/salsa20.json +run_test python tests/test.py --with-coverage -c tests/chacha20.json +run_test python tests/test.py --with-coverage -c tests/salsa20-ctr.json +run_test python tests/test.py --with-coverage -c tests/table.json +run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json +run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json +run_test python tests/test.py --with-coverage -c tests/workers.json +run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json +run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv" +run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" + +if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then + if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then + # we have to run it twice: + # the first time there's no syn cookie + # the second time there is syn cookie + run_test python tests/test.py --with-coverage -c tests/fastopen.json + run_test python tests/test.py --with-coverage -c tests/fastopen.json + fi +fi + +run_test tests/test_large_file.sh + +run_test tests/test_command.sh + +coverage combine && coverage report --include=shadowsocks/* +rm -rf htmlcov +rm -rf tmp +coverage html --include=shadowsocks/* + +coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage + +exit $result diff --git a/.travis.yml b/.travis.yml index 20710fc..4fbe78c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,19 @@ language: python python: - 2.6 - 2.7 + - 3.3 + - 3.4 +cache: + directories: + - dante-1.4.0 before_install: - sudo apt-get update -qq - - sudo apt-get install -qq build-essential libssl-dev swig libevent-dev python-gevent python-m2crypto python-numpy - - pip install m2crypto gevent salsa20 + - sudo apt-get install -qq build-essential libssl-dev swig python-m2crypto python-numpy dnsutils iproute nginx bc + - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 + - sudo service nginx restart + - pip install m2crypto salsa20 pep8 pyflakes nose coverage + - sudo tests/socksify/install.sh + - sudo tests/libsodium/install.sh + - sudo tests/setup_tc.sh script: - - python test.py -c test/table.json - - python test.py -c test/aes.json - - python test.py -c test/salsa20.json - - python test.py -c test/server-multi-passwd.json + - ./.jenkins.sh diff --git a/CHANGES b/CHANGES index 4f19880..6822082 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,105 @@ +2.6.1 2014-12-26 +- Fix a problem with TCP Fast Open on local side +- Fix sometimes daemon_start returns wrong exit status + +2.6 2014-12-21 +- Add daemon support + +2.5 2014-12-11 +- Add salsa20 and chacha20 + +2.4.3 2014-11-10 +- Fix an issue on Python 3 +- Fix an issue with IPv6 + +2.4.2 2014-11-06 +- Fix command line arguments on Python 3 +- Support table on Python 3 +- Fix TCP Fast Open on Python 3 + +2.4.1 2014-11-01 +- Fix setup.py for non-utf8 locales on Python 3 + +2.4 2014-11-01 +- Python 3 support +- Performance improvement +- Fix LRU cache behavior + +2.3.2 2014-10-11 +- Fix OpenSSL on Windows + +2.3.1 2014-10-09 +- Does not require M2Crypto any more + +2.3 2014-09-23 +- Support CFB1, CFB8 and CTR mode of AES +- Do not require password config when using port_password +- Use SIGTERM instead of SIGQUIT on Windows + +2.2.2 2014-09-14 +- Fix when multiple DNS set, IPv6 only sites are broken + +2.2.1 2014-09-10 +- Support graceful shutdown +- Fix some bugs + +2.2.0 2014-09-09 +- Add RC4-MD5 encryption + +2.1.0 2014-08-10 +- Use only IPv4 DNS server +- Does not ship config.json +- Better error message + +2.0.12 2014-07-26 +- Support -q quiet mode +- Exit 0 when showing help with -h + +2.0.11 2014-07-12 +- Prefers IP addresses over hostnames, more friendly with socksify and openvpn + +2.0.10 2014-07-11 +- Fix UDP on local + +2.0.9 2014-07-06 +- Fix EWOULDBLOCK on Windows +- Fix Unicode config problem on some platforms + +2.0.8 2014-06-23 +- Use multiple DNS to query hostnames + +2.0.7 2014-06-21 +- Fix fastopen on local +- Fallback when fastopen is not available +- Add verbose logging mode -vv +- Verify if hostname is valid + +2.0.6 2014-06-19 +- Fix CPU 100% on POLL_HUP +- More friendly logging + +2.0.5 2014-06-18 +- Support a simple config format for multiple ports + +2.0.4 2014-06-12 +- Fix worker master + +2.0.3 2014-06-11 +- Fix table encryption with UDP + +2.0.2 2014-06-11 +- Add asynchronous DNS in TCP relay + +2.0.1 2014-06-05 +- Better logging +- Maybe fix bad file descriptor + +2.0 2014-06-05 +- Use a new event model +- Remove gevent +- Refuse to use default password +- Fix a problem when using multiple passwords with table encryption + 1.4.5 2014-05-24 - Add timeout in TCP server - Close sockets in master process diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c164423 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +How to contribute +================= + +在你提交问题前,请先[自行诊断]一下。提交时附上诊断过程中的问题和下列结果, +否则如果我们无法重现你的问题,也就不能帮助你。 + +Before you submit issues, please read [Troubleshooting] and take a few minutes +to read this guide. + +问题反馈 +------- + +请提交下面的信息: + +1. 你是如何搭建环境的?(操作系统,Shadowsocks 版本) +2. 操作步骤是什么? +3. 浏览器里的现象是什么?一直转菊花,还是有提示错误? +4. 发生错误时,客户端和服务端最后一部分日志。 +5. 其它你认为可能和问题有关的信息。 + +如果你不清楚其中某条的含义, 可以直接跳过那一条。 + +Issues +------ + +Please include the following information in your submission: + +1. How did you set up your environment? (OS, version of Shadowsocks) +2. Steps to reproduce the problem. +3. What happened in your browser? Just no response, or any error message? +4. 10 lines of log on the local side of shadowsocks when the error happened. +5. 10 lines of log on the server side of shadowsocks when the error happened. +6. Any other useful information. + +Skip any of them if you don't know its meaning. + +[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting +[自行诊断]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting diff --git a/LICENSE b/LICENSE index 0d9e69a..98f608b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Shadowsocks -Copyright (c) 2013 clowwindy +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 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1dc4c8e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include *.py +include README.rst +include LICENSE diff --git a/README.md b/README.md index 0ce6560..4365844 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,53 @@ shadowsocks =========== -Current version: 1.4.5 [![Build Status][1]][0] +[![PyPI version]][PyPI] +[![Build Status]][Travis CI] +[![Coverage Status]][Coverage] -shadowsocks is a lightweight tunnel proxy which can help you get through firewalls. +A fast tunnel proxy that helps you bypass firewalls. -Both TCP CONNECT and UDP ASSOCIATE are implemented. - -[中文说明][3] +[中文说明][Chinese Readme] Install ------- -First, make sure you have Python 2.6 or 2.7. +You'll have a client on your local side, and setup a server on a +remote server. - $ python --version - Python 2.6.8 +### Client -Install Shadowsocks. +* [Windows] / [OS X] +* [Android] / [iOS] +* [OpenWRT] + +### Server #### Debian / Ubuntu: - apt-get install python-pip python-gevent python-m2crypto + apt-get install python-pip pip install shadowsocks +Or simply `apt-get install shadowsocks` if you have [Debian sid] in your +source list. + #### CentOS: - yum install m2crypto python-setuptools + yum install python-setuptools easy_install pip pip install shadowsocks -#### OS X: - - git clone https://github.com/clowwindy/M2Crypto.git - cd M2Crypto - pip install . - pip install shadowsocks - #### Windows: -Choose a [GUI client][7] +Download [OpenSSL for Windows] and install. Then install shadowsocks via +easy_install and pip as Linux. If you don't know how to use them, you can +directly download [the package], and use `python shadowsocks/server.py` +instead of `ssserver` command below. -Usage ------ +Configuration +------------- -Create a config file `/etc/shadowsocks.json` (or put it in other path). +On your server create a config file `/etc/shadowsocks.json`. Example: { @@ -55,8 +58,7 @@ Example: "password":"mypassword", "timeout":300, "method":"aes-256-cfb", - "fast_open": false, - "workers": 1 + "fast_open": false } Explanation of the fields: @@ -69,51 +71,40 @@ Explanation of the fields: | local_port | local port | | password | password used for encryption | | timeout | in seconds | -| method | encryption method, "aes-256-cfb" is recommended | -| fast_open | use [TCP_FASTOPEN][2], true / false | +| method | default: "aes-256-cfb", see [Encryption] | +| fast_open | use [TCP_FASTOPEN], true / false | | workers | number of workers, available on Unix/Linux | -Run `ssserver -c /etc/shadowsocks.json` on your server. To run it in the background, [use supervisor][8]. +On your server: -On your client machine, run `sslocal -c /etc/shadowsocks.json`. +To run in the foreground: -Change the proxy settings in your browser to + ssserver -c /etc/shadowsocks.json - protocol: socks5 - hostname: 127.0.0.1 - port: your local_port +To run in the background: -**Notice: If you want to use encryption methods other than "table", please install M2Crypto (See Encryption Section).** + ssserver -c /etc/shadowsocks.json -d start + ssserver -c /etc/shadowsocks.json -d stop -It's recommended to use shadowsocks with AutoProxy or Proxy SwitchySharp. +On your client machine, use the same configuration as your server. Check the +README of your client for more information. -Command line args ------------------- +Command Line Options +-------------------- -You can use args to override settings from `config.json`. +Check the options via `-h`.You can use args to override settings from +`config.json`. sslocal -s server_name -p server_port -l local_port -k password -m bf-cfb ssserver -p server_port -k password -m bf-cfb --workers 2 - ssserver -c /etc/shadowsocks/config.json + ssserver -c /etc/shadowsocks/config.json -d start --pid-file=/tmp/shadowsocks.pid + ssserver -c /etc/shadowsocks/config.json -d stop --pid-file=/tmp/shadowsocks.pid -Salsa20 -------- +Documentation +------------- -Salsa20 is a fast stream cipher. - -Use "salsa20-ctr" in shadowsocks.json. - -And install these packages: - -#### Debian / Ubuntu: - - apt-get install python-numpy - pip install salsa20 - -Wiki ----- - -https://github.com/clowwindy/shadowsocks/wiki +You can find all the documentation in the wiki: +https://github.com/shadowsocks/shadowsocks/wiki License ------- @@ -121,18 +112,29 @@ MIT Bugs and Issues ---------------- -Please visit [issue tracker][5] -Mailing list: http://groups.google.com/group/shadowsocks +* [Troubleshooting] +* [Issue Tracker] +* [Mailing list] -Also see [troubleshooting][6] -[0]: https://travis-ci.org/clowwindy/shadowsocks -[1]: https://travis-ci.org/clowwindy/shadowsocks.png?branch=master -[2]: https://github.com/clowwindy/shadowsocks/wiki/TCP-Fast-Open -[3]: https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E -[4]: http://chandlerproject.org/Projects/MeTooCrypto -[5]: https://github.com/clowwindy/shadowsocks/issues?state=open -[6]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting -[7]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients -[8]: https://github.com/clowwindy/shadowsocks/wiki/Configure-Shadowsocks-with-Supervisor +[Android]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#android +[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat +[Chinese Readme]: https://github.com/shadowsocks/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E +[Coverage Status]: http://192.81.132.184/result/shadowsocks +[Coverage]: http://192.81.132.184/job/Shadowsocks/ws/htmlcov/index.html +[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks +[the package]: https://pypi.python.org/pypi/shadowsocks +[Encryption]: https://github.com/shadowsocks/shadowsocks/wiki/Encryption +[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help +[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open +[Mailing list]: http://groups.google.com/group/shadowsocks +[OpenSSL for Windows]: http://slproweb.com/products/Win32OpenSSL.html +[OpenWRT]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#openwrt +[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help +[PyPI]: https://pypi.python.org/pypi/shadowsocks +[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat +[TCP_FASTOPEN]: https://github.com/shadowsocks/shadowsocks/wiki/TCP-Fast-Open +[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks +[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting +[Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#windows diff --git a/README.rst b/README.rst index a2bc336..41afc36 100644 --- a/README.rst +++ b/README.rst @@ -1,63 +1,66 @@ shadowsocks =========== -shadowsocks is a lightweight tunnel proxy which can help you get through -firewalls. +|PyPI version| |Build Status| |Coverage Status| -Both TCP CONNECT and UDP ASSOCIATE are implemented. +A fast tunnel proxy that helps you bypass firewalls. -`中文说明 `__ +`中文说明 `__ Install ------- -First, make sure you have Python 2.6 or 2.7. +You'll have a client on your local side, and setup a server on a remote +server. -:: +Client +~~~~~~ - $ python --version - Python 2.6.8 +- `Windows `__ + / `OS + X `__ +- `Android `__ + / `iOS `__ +- `OpenWRT `__ -Install Shadowsocks. +Server +~~~~~~ Debian / Ubuntu: ^^^^^^^^^^^^^^^^ :: - apt-get install python-pip python-gevent python-m2crypto + apt-get install python-pip pip install shadowsocks +Or simply ``apt-get install shadowsocks`` if you have `Debian +sid `__ in your +source list. + CentOS: ^^^^^^^ :: - yum install m2crypto python-setuptools + yum install python-setuptools easy_install pip pip install shadowsocks -OS X: -^^^^^ - -:: - - git clone https://github.com/clowwindy/M2Crypto.git - cd M2Crypto - pip install . - pip install shadowsocks - Windows: ^^^^^^^^ -Choose a `GUI -client `__ +Download `OpenSSL for +Windows `__ and install. +Then install shadowsocks via easy\_install and pip as Linux. If you +don't know how to use them, you can directly download `the +package `__, and use +``python shadowsocks/server.py`` instead of ``ssserver`` command below. -Usage ------ +Configuration +------------- -Create a config file ``/etc/shadowsocks.json`` (or put it in other -path). Example: +On your server create a config file ``/etc/shadowsocks.json``. Example: :: @@ -69,86 +72,69 @@ path). Example: "password":"mypassword", "timeout":300, "method":"aes-256-cfb", - "fast_open": false, - "workers": 1 + "fast_open": false } Explanation of the fields: -+------------------+-----------------------------------------------------------------------------------------------------+ -| Name | Explanation | -+==================+=====================================================================================================+ -| server | the address your server listens | -+------------------+-----------------------------------------------------------------------------------------------------+ -| server\_port | server port | -+------------------+-----------------------------------------------------------------------------------------------------+ -| local\_address | the address your local listens | -+------------------+-----------------------------------------------------------------------------------------------------+ -| local\_port | local port | -+------------------+-----------------------------------------------------------------------------------------------------+ -| password | password used for encryption | -+------------------+-----------------------------------------------------------------------------------------------------+ -| timeout | in seconds | -+------------------+-----------------------------------------------------------------------------------------------------+ -| method | encryption method, "aes-256-cfb" is recommended | -+------------------+-----------------------------------------------------------------------------------------------------+ -| fast\_open | use `TCP\_FASTOPEN `__, true / false | -+------------------+-----------------------------------------------------------------------------------------------------+ -| workers | number of workers, available on Unix/Linux | -+------------------+-----------------------------------------------------------------------------------------------------+ ++------------------+-----------------------------------------------------------------------------------------------------------+ +| Name | Explanation | ++==================+===========================================================================================================+ +| server | the address your server listens | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| server\_port | server port | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| local\_address | the address your local listens | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| local\_port | local port | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| password | password used for encryption | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| timeout | in seconds | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| method | default: "aes-256-cfb", see `Encryption `__ | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| fast\_open | use `TCP\_FASTOPEN `__, true / false | ++------------------+-----------------------------------------------------------------------------------------------------------+ +| workers | number of workers, available on Unix/Linux | ++------------------+-----------------------------------------------------------------------------------------------------------+ -Run ``ssserver -c /etc/shadowsocks.json`` on your server. To run it in -the background, `use -supervisor `__. +On your server: -On your client machine, run ``sslocal -c /etc/shadowsocks.json``. - -Change the proxy settings in your browser to +To run in the foreground: :: - protocol: socks5 - hostname: 127.0.0.1 - port: your local_port + ssserver -c /etc/shadowsocks.json -**Notice: If you want to use encryption methods other than "table", -please install M2Crypto (See Encryption Section).** +To run in the background: -It's recommended to use shadowsocks with AutoProxy or Proxy -SwitchySharp. +:: -Command line args ------------------ + ssserver -c /etc/shadowsocks.json -d start + ssserver -c /etc/shadowsocks.json -d stop -You can use args to override settings from ``config.json``. +On your client machine, use the same configuration as your server. Check +the README of your client for more information. + +Command Line Options +-------------------- + +Check the options via ``-h``.You can use args to override settings from +``config.json``. :: sslocal -s server_name -p server_port -l local_port -k password -m bf-cfb ssserver -p server_port -k password -m bf-cfb --workers 2 - ssserver -c /etc/shadowsocks/config.json + ssserver -c /etc/shadowsocks/config.json -d start --pid-file=/tmp/shadowsocks.pid + ssserver -c /etc/shadowsocks/config.json -d stop --pid-file=/tmp/shadowsocks.pid -Salsa20 -------- +Documentation +------------- -Salsa20 is a fast stream cipher. - -Use "salsa20-ctr" in shadowsocks.json. - -And install these packages: - -Debian / Ubuntu: -^^^^^^^^^^^^^^^^ - -:: - - apt-get install python-numpy - pip install salsa20 - -Wiki ----- - -https://github.com/clowwindy/shadowsocks/wiki +You can find all the documentation in the wiki: +https://github.com/shadowsocks/shadowsocks/wiki License ------- @@ -158,13 +144,14 @@ MIT Bugs and Issues --------------- -Please visit `issue -tracker `__ +- `Troubleshooting `__ +- `Issue + Tracker `__ +- `Mailing list `__ -Mailing list: http://groups.google.com/group/shadowsocks - -Also see -`troubleshooting `__ - -.. |Build Status| image:: https://travis-ci.org/clowwindy/shadowsocks.png?branch=master - :target: https://travis-ci.org/clowwindy/shadowsocks +.. |PyPI version| image:: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat + :target: https://pypi.python.org/pypi/shadowsocks +.. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat + :target: https://travis-ci.org/shadowsocks/shadowsocks +.. |Coverage Status| image:: http://192.81.132.184/result/shadowsocks + :target: http://192.81.132.184/job/Shadowsocks/ws/htmlcov/index.html diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..4e7ad16 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +shadowsocks (2.1.0-1) unstable; urgency=low + + * Initial release (Closes: #758900) + + -- Shell.Xu Sat, 23 Aug 2014 00:56:04 +0800 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..45a4fb7 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/config.json b/debian/config.json new file mode 100644 index 0000000..35cb14a --- /dev/null +++ b/debian/config.json @@ -0,0 +1,11 @@ +{ + "server":"my_server_ip", + "server_port":8388, + "local_address": "127.0.0.1", + "local_port":1080, + "password":"mypassword", + "timeout":300, + "method":"aes-256-cfb", + "fast_open": false, + "workers": 1 +} \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..da00920 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: shadowsocks +Section: python +Priority: extra +Maintainer: Shell.Xu +Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools +Standards-Version: 3.9.5 +Homepage: https://github.com/clowwindy/shadowsocks +Vcs-Git: git://github.com/shell909090/shadowsocks.git +Vcs-Browser: http://github.com/shell909090/shadowsocks + +Package: shadowsocks +Architecture: all +Pre-Depends: dpkg (>= 1.15.6~) +Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto +Description: Fast tunnel proxy that helps you bypass firewalls + A secure socks5 proxy, designed to protect your Internet traffic. + . + This package contain local and server part of shadowsocks, a fast, + powerful tunnel proxy to bypass firewalls. \ No newline at end of file diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..7be8162 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,30 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: shadowsocks +Source: https://github.com/clowwindy/shadowsocks + +Files: debian/* +Copyright: 2014 Shell.Xu +License: Expat + +Files: * +Copyright: 2014 clowwindy +License: Expat + +License: Expat + 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. diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..0208fc1 --- /dev/null +++ b/debian/docs @@ -0,0 +1,2 @@ +README.md +README.rst diff --git a/debian/init.d b/debian/init.d new file mode 100644 index 0000000..2f4f352 --- /dev/null +++ b/debian/init.d @@ -0,0 +1,149 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: shadowsocks +# Required-Start: $network $local_fs $remote_fs +# Required-Stop: $network $local_fs $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Fast tunnel proxy that helps you bypass firewalls +# Description: A secure socks5 proxy, designed to protect your Internet traffic. +# This package contain local and server part of shadowsocks, a fast, +# powerful tunnel proxy to bypass firewalls. +### END INIT INFO + +# Author: Shell.Xu + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC=shadowsocks # Introduce a short description here +NAME=shadowsocks # Introduce the short server's name here +DAEMON=/usr/bin/ssserver # Introduce the server's location here +DAEMON_ARGS="" # Arguments to run the daemon with +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME +LOGFILE=/var/log/$NAME.log + +# Exit if the package is not installed +[ -x $DAEMON ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ + --background --make-pidfile --chdir / --chuid $USERID --no-close --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ + --background --make-pidfile --chdir / --chuid $USERID --no-close -- \ + $DAEMON_ARGS $DAEMON_OPTS >> $LOGFILE 2>&1 \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..a614864 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +debian/config.json etc/shadowsocks/ \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..62e2bb6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,5 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +%: + dh $@ --with python2 --buildsystem=python_distutils diff --git a/debian/shadowsocks.default b/debian/shadowsocks.default new file mode 100644 index 0000000..a520602 --- /dev/null +++ b/debian/shadowsocks.default @@ -0,0 +1,12 @@ +# Defaults for shadowsocks initscript +# sourced by /etc/init.d/shadowsocks +# installed at /etc/default/shadowsocks by the maintainer scripts + +USERID="nobody" + +# +# This is a POSIX shell fragment +# + +# Additional options that are passed to the Daemon. +DAEMON_OPTS="-q -c /etc/shadowsocks/config.json" diff --git a/debian/shadowsocks.manpages b/debian/shadowsocks.manpages new file mode 100644 index 0000000..3df8a33 --- /dev/null +++ b/debian/shadowsocks.manpages @@ -0,0 +1,2 @@ +debian/sslocal.1 +debian/ssserver.1 \ No newline at end of file diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/sslocal.1 b/debian/sslocal.1 new file mode 100644 index 0000000..0c2cf51 --- /dev/null +++ b/debian/sslocal.1 @@ -0,0 +1,59 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" (C) Copyright 2014 Shell.Xu , +.\" +.TH SHADOWSOCKS 1 "August 23, 2014" +.SH NAME +shadowsocks \- Fast tunnel proxy that helps you bypass firewalls +.SH SYNOPSIS +.B ssserver +.RI [ options ] +.br +.B sslocal +.RI [ options ] +.SH DESCRIPTION +shadowsocks is a tunnel proxy helps you bypass firewall. +.B ssserver +is the server part, and +.B sslocal +is the local part. +.SH OPTIONS +.TP +.B \-h, \-\-help +Show this help message and exit. +.TP +.B \-s SERVER_ADDR +Server address, default: 0.0.0.0. +.TP +.B \-p SERVER_PORT +Server port, default: 8388. +.TP +.B \-k PASSWORD +Password. +.TP +.B \-m METHOD +Encryption method, default: aes-256-cfb. +.TP +.B \-t TIMEOUT +Timeout in seconds, default: 300. +.TP +.B \-c CONFIG +Path to config file. +.TP +.B \-\-fast-open +Use TCP_FASTOPEN, requires Linux 3.7+. +.TP +.B \-\-workers WORKERS +Number of workers, available on Unix/Linux. +.TP +.B \-v, \-vv +Verbose mode. +.TP +.B \-q, \-qq +Quiet mode, only show warnings/errors. +.SH SEE ALSO +.br +The programs are documented fully by +.IR "Shell Xu " +and +.IR "Clowwindy ", +available via the Info system. diff --git a/debian/ssserver.1 b/debian/ssserver.1 new file mode 100644 index 0000000..0c2cf51 --- /dev/null +++ b/debian/ssserver.1 @@ -0,0 +1,59 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" (C) Copyright 2014 Shell.Xu , +.\" +.TH SHADOWSOCKS 1 "August 23, 2014" +.SH NAME +shadowsocks \- Fast tunnel proxy that helps you bypass firewalls +.SH SYNOPSIS +.B ssserver +.RI [ options ] +.br +.B sslocal +.RI [ options ] +.SH DESCRIPTION +shadowsocks is a tunnel proxy helps you bypass firewall. +.B ssserver +is the server part, and +.B sslocal +is the local part. +.SH OPTIONS +.TP +.B \-h, \-\-help +Show this help message and exit. +.TP +.B \-s SERVER_ADDR +Server address, default: 0.0.0.0. +.TP +.B \-p SERVER_PORT +Server port, default: 8388. +.TP +.B \-k PASSWORD +Password. +.TP +.B \-m METHOD +Encryption method, default: aes-256-cfb. +.TP +.B \-t TIMEOUT +Timeout in seconds, default: 300. +.TP +.B \-c CONFIG +Path to config file. +.TP +.B \-\-fast-open +Use TCP_FASTOPEN, requires Linux 3.7+. +.TP +.B \-\-workers WORKERS +Number of workers, available on Unix/Linux. +.TP +.B \-v, \-vv +Verbose mode. +.TP +.B \-q, \-qq +Quiet mode, only show warnings/errors. +.SH SEE ALSO +.br +The programs are documented fully by +.IR "Shell Xu " +and +.IR "Clowwindy ", +available via the Info system. diff --git a/packaging/py2exe/setup.py b/packaging/py2exe/setup.py deleted file mode 100644 index 30340aa..0000000 --- a/packaging/py2exe/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from distutils.core import setup -# NOTICE!! -# This setup.py is written for py2exe -# Don't make a python package using this file! - -try: - import py2exe -except ImportError: - pass - -setup(name='shadowsocks', - version='1.2.3', - description='a lightweight tunnel proxy which can help you get through firewalls', - author='clowwindy', - author_email='clowwindy42@gmail.com', - url='https://github.com/clowwindy/shadowsocks', - options = {'py2exe': {'bundle_files': 1, 'compressed': True}}, - windows = [{"script":"local.py", "dest_base": "shadowsocks_local",}], - zipfile = None) diff --git a/setup.py b/setup.py index 259c629..c2b7d88 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,23 @@ +import codecs from setuptools import setup -with open('README.rst') as f: +with codecs.open('README.rst', encoding='utf-8') as f: long_description = f.read() setup( name="shadowsocks", - version="1.4.5", + version="2.6.1", license='MIT', - description="a lightweight tunnel proxy", + description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', author_email='clowwindy42@gmail.com', - url='https://github.com/clowwindy/shadowsocks', - packages=['shadowsocks'], + url='https://github.com/shadowsocks/shadowsocks', + packages=['shadowsocks', 'shadowsocks.crypto'], package_data={ - 'shadowsocks': ['README.rst', 'LICENSE', 'config.json'] + 'shadowsocks': ['README.rst', 'LICENSE'] }, - install_requires=['setuptools'], + install_requires=[], entry_points=""" [console_scripts] sslocal = shadowsocks.local:main @@ -27,6 +28,11 @@ setup( 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: Proxy Servers', ], long_description=long_description, diff --git a/shadowsocks/__init__.py b/shadowsocks/__init__.py index 013e4b7..5ba5908 100644 --- a/shadowsocks/__init__.py +++ b/shadowsocks/__init__.py @@ -1 +1,24 @@ #!/usr/bin/python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py new file mode 100644 index 0000000..18222a6 --- /dev/null +++ b/shadowsocks/asyncdns.py @@ -0,0 +1,506 @@ +#!/usr/bin/env 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import time +import os +import socket +import struct +import re +import logging + +from shadowsocks import common, lru_cache, eventloop + + +CACHE_SWEEP_INTERVAL = 30 + +VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(? 63: + return None + results.append(common.chr(l)) + results.append(label) + results.append(b'\0') + return b''.join(results) + + +def build_request(address, qtype, request_id): + header = struct.pack('!HBBHHHH', request_id, 1, 0, 1, 0, 0, 0) + addr = build_address(address) + qtype_qclass = struct.pack('!HH', qtype, QCLASS_IN) + return header + addr + qtype_qclass + + +def parse_ip(addrtype, data, length, offset): + if addrtype == QTYPE_A: + return socket.inet_ntop(socket.AF_INET, data[offset:offset + length]) + elif addrtype == QTYPE_AAAA: + return socket.inet_ntop(socket.AF_INET6, data[offset:offset + length]) + elif addrtype in [QTYPE_CNAME, QTYPE_NS]: + return parse_name(data, offset)[1] + else: + return data[offset:offset + length] + + +def parse_name(data, offset): + p = offset + labels = [] + l = common.ord(data[p]) + while l > 0: + if (l & (128 + 64)) == (128 + 64): + # pointer + pointer = struct.unpack('!H', data[p:p + 2])[0] + pointer &= 0x3FFF + r = parse_name(data, pointer) + labels.append(r[1]) + p += 2 + # pointer is the end + return p - offset, b'.'.join(labels) + else: + labels.append(data[p + 1:p + 1 + l]) + p += 1 + l + l = common.ord(data[p]) + return p - offset + 1, b'.'.join(labels) + + +# rfc1035 +# record +# 1 1 1 1 1 1 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | | +# / / +# / NAME / +# | | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | TYPE | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | CLASS | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | TTL | +# | | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | RDLENGTH | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| +# / RDATA / +# / / +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +def parse_record(data, offset, question=False): + nlen, name = parse_name(data, offset) + if not question: + record_type, record_class, record_ttl, record_rdlength = struct.unpack( + '!HHiH', data[offset + nlen:offset + nlen + 10] + ) + ip = parse_ip(record_type, data, record_rdlength, offset + nlen + 10) + return nlen + 10 + record_rdlength, \ + (name, ip, record_type, record_class, record_ttl) + else: + record_type, record_class = struct.unpack( + '!HH', data[offset + nlen:offset + nlen + 4] + ) + return nlen + 4, (name, None, record_type, record_class, None, None) + + +def parse_header(data): + if len(data) >= 12: + header = struct.unpack('!HBBHHHH', data[:12]) + res_id = header[0] + res_qr = header[1] & 128 + res_tc = header[1] & 2 + res_ra = header[2] & 128 + res_rcode = header[2] & 15 + # assert res_tc == 0 + # assert res_rcode in [0, 3] + res_qdcount = header[3] + res_ancount = header[4] + res_nscount = header[5] + res_arcount = header[6] + return (res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, + res_ancount, res_nscount, res_arcount) + return None + + +def parse_response(data): + try: + if len(data) >= 12: + header = parse_header(data) + if not header: + return None + res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, \ + res_ancount, res_nscount, res_arcount = header + + qds = [] + ans = [] + offset = 12 + for i in range(0, res_qdcount): + l, r = parse_record(data, offset, True) + offset += l + if r: + qds.append(r) + for i in range(0, res_ancount): + l, r = parse_record(data, offset) + offset += l + if r: + ans.append(r) + for i in range(0, res_nscount): + l, r = parse_record(data, offset) + offset += l + for i in range(0, res_arcount): + l, r = parse_record(data, offset) + offset += l + response = DNSResponse() + if qds: + response.hostname = qds[0][0] + for an in qds: + response.questions.append((an[1], an[2], an[3])) + for an in ans: + response.answers.append((an[1], an[2], an[3])) + return response + except Exception as e: + import traceback + traceback.print_exc() + logging.error(e) + return None + + +def is_ip(address): + for family in (socket.AF_INET, socket.AF_INET6): + try: + if type(address) != str: + address = address.decode('utf8') + socket.inet_pton(family, address) + return family + except (TypeError, ValueError, OSError, IOError): + pass + return False + + +def is_valid_hostname(hostname): + if len(hostname) > 255: + return False + if hostname[-1] == b'.': + hostname = hostname[:-1] + return all(VALID_HOSTNAME.match(x) for x in hostname.split(b'.')) + + +class DNSResponse(object): + def __init__(self): + self.hostname = None + self.questions = [] # each: (addr, type, class) + self.answers = [] # each: (addr, type, class) + + def __str__(self): + return '%s: %s' % (self.hostname, str(self.answers)) + + +STATUS_IPV4 = 0 +STATUS_IPV6 = 1 + + +class DNSResolver(object): + + def __init__(self): + self._loop = None + self._request_id = 1 + self._hosts = {} + self._hostname_status = {} + self._hostname_to_cb = {} + self._cb_to_hostname = {} + self._cache = lru_cache.LRUCache(timeout=300) + self._last_time = time.time() + self._sock = None + self._servers = None + self._parse_resolv() + self._parse_hosts() + # TODO monitor hosts change and reload hosts + # TODO parse /etc/gai.conf and follow its rules + + def _parse_resolv(self): + self._servers = [] + try: + with open('/etc/resolv.conf', 'rb') as f: + content = f.readlines() + for line in content: + line = line.strip() + if line: + if line.startswith(b'nameserver'): + parts = line.split() + if len(parts) >= 2: + server = parts[1] + if is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append(server) + except IOError: + pass + if not self._servers: + self._servers = ['8.8.4.4', '8.8.8.8'] + + def _parse_hosts(self): + etc_path = '/etc/hosts' + if 'WINDIR' in os.environ: + etc_path = os.environ['WINDIR'] + '/system32/drivers/etc/hosts' + try: + with open(etc_path, 'rb') as f: + for line in f.readlines(): + line = line.strip() + parts = line.split() + if len(parts) >= 2: + ip = parts[0] + if is_ip(ip): + for i in range(1, len(parts)): + hostname = parts[i] + if hostname: + self._hosts[hostname] = ip + except IOError: + self._hosts['localhost'] = '127.0.0.1' + + def add_to_loop(self, loop, ref=False): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + loop.add(self._sock, eventloop.POLL_IN) + loop.add_handler(self.handle_events, ref=ref) + + def _call_callback(self, hostname, ip, error=None): + callbacks = self._hostname_to_cb.get(hostname, []) + for callback in callbacks: + if callback in self._cb_to_hostname: + del self._cb_to_hostname[callback] + if ip or error: + callback((hostname, ip), error) + else: + callback((hostname, None), + Exception('unknown hostname %s' % hostname)) + if hostname in self._hostname_to_cb: + del self._hostname_to_cb[hostname] + if hostname in self._hostname_status: + del self._hostname_status[hostname] + + def _handle_data(self, data): + response = parse_response(data) + if response and response.hostname: + hostname = response.hostname + ip = None + for answer in response.answers: + if answer[1] in (QTYPE_A, QTYPE_AAAA) and \ + answer[2] == QCLASS_IN: + ip = answer[0] + break + if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ + == STATUS_IPV4: + self._hostname_status[hostname] = STATUS_IPV6 + self._send_req(hostname, QTYPE_AAAA) + else: + if ip: + self._cache[hostname] = ip + self._call_callback(hostname, ip) + elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + for question in response.questions: + if question[1] == QTYPE_AAAA: + self._call_callback(hostname, None) + break + + def handle_events(self, events): + for sock, fd, event in events: + if sock != self._sock: + continue + if event & eventloop.POLL_ERR: + logging.error('dns socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN) + else: + data, addr = sock.recvfrom(1024) + if addr[0] not in self._servers: + logging.warn('received a packet other than our dns') + break + self._handle_data(data) + break + now = time.time() + if now - self._last_time > CACHE_SWEEP_INTERVAL: + self._cache.sweep() + self._last_time = now + + def remove_callback(self, callback): + hostname = self._cb_to_hostname.get(callback) + if hostname: + del self._cb_to_hostname[callback] + arr = self._hostname_to_cb.get(hostname, None) + if arr: + arr.remove(callback) + if not arr: + del self._hostname_to_cb[hostname] + if hostname in self._hostname_status: + del self._hostname_status[hostname] + + def _send_req(self, hostname, qtype): + self._request_id += 1 + if self._request_id > 32768: + self._request_id = 1 + req = build_request(hostname, qtype, self._request_id) + for server in self._servers: + logging.debug('resolving %s with type %d using server %s', + hostname, qtype, server) + self._sock.sendto(req, (server, 53)) + + def resolve(self, hostname, callback): + if type(hostname) != bytes: + hostname = hostname.encode('utf8') + if not hostname: + callback(None, Exception('empty hostname')) + elif is_ip(hostname): + callback((hostname, hostname), None) + elif hostname in self._hosts: + logging.debug('hit hosts: %s', hostname) + ip = self._hosts[hostname] + callback((hostname, ip), None) + elif hostname in self._cache: + logging.debug('hit cache: %s', hostname) + ip = self._cache[hostname] + callback((hostname, ip), None) + else: + if not is_valid_hostname(hostname): + callback(None, Exception('invalid hostname: %s' % hostname)) + return + arr = self._hostname_to_cb.get(hostname, None) + if not arr: + self._hostname_status[hostname] = STATUS_IPV4 + self._send_req(hostname, QTYPE_A) + self._hostname_to_cb[hostname] = [callback] + self._cb_to_hostname[callback] = hostname + else: + arr.append(callback) + # TODO send again only if waited too long + self._send_req(hostname, QTYPE_A) + + def close(self): + if self._sock: + self._sock.close() + self._sock = None + + +def test(): + dns_resolver = DNSResolver() + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop, ref=True) + + global counter + counter = 0 + + def make_callback(): + global counter + + def callback(result, error): + global counter + # TODO: what can we assert? + print(result, error) + counter += 1 + if counter == 9: + loop.remove_handler(dns_resolver.handle_events) + dns_resolver.close() + a_callback = callback + return a_callback + + assert(make_callback() != make_callback()) + + dns_resolver.resolve(b'google.com', make_callback()) + dns_resolver.resolve('google.com', make_callback()) + dns_resolver.resolve('example.com', make_callback()) + dns_resolver.resolve('ipv6.google.com', make_callback()) + dns_resolver.resolve('www.facebook.com', make_callback()) + dns_resolver.resolve('ns2.google.com', make_callback()) + dns_resolver.resolve('invalid.@!#$%^&$@.hostname', make_callback()) + dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'long.hostname', make_callback()) + dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'long.hostname', make_callback()) + + loop.run() + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/common.py b/shadowsocks/common.py new file mode 100644 index 0000000..e4f698c --- /dev/null +++ b/shadowsocks/common.py @@ -0,0 +1,204 @@ +#!/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 __future__ import absolute_import, division, print_function, \ + with_statement + +import socket +import struct +import logging + + +def compat_ord(s): + if type(s) == int: + return s + return _ord(s) + + +def compat_chr(d): + if bytes == str: + return _chr(d) + return bytes([d]) + + +_ord = ord +_chr = chr +ord = compat_ord +chr = compat_chr + + +def to_bytes(s): + if bytes != str: + if type(s) == str: + return s.encode('utf-8') + return s + + +def to_str(s): + if bytes != str: + if type(s) == bytes: + return s.decode('utf-8') + return s + + +def inet_ntop(family, ipstr): + if family == socket.AF_INET: + return to_bytes(socket.inet_ntoa(ipstr)) + elif family == socket.AF_INET6: + import re + v6addr = ':'.join(('%02X%02X' % (ord(i), ord(j))).lstrip('0') + for i, j in zip(ipstr[::2], ipstr[1::2])) + v6addr = re.sub('::+', '::', v6addr, count=1) + return to_bytes(v6addr) + + +def inet_pton(family, addr): + addr = to_str(addr) + if family == socket.AF_INET: + return socket.inet_aton(addr) + elif family == socket.AF_INET6: + if '.' in addr: # a v4 addr + v4addr = addr[addr.rindex(':') + 1:] + v4addr = socket.inet_aton(v4addr) + v4addr = map(lambda x: ('%02X' % ord(x)), v4addr) + v4addr.insert(2, ':') + newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr) + return inet_pton(family, newaddr) + dbyts = [0] * 8 # 8 groups + grps = addr.split(':') + for i, v in enumerate(grps): + if v: + dbyts[i] = int(v, 16) + else: + for j, w in enumerate(grps[::-1]): + if w: + dbyts[7 - j] = int(w, 16) + else: + break + break + return b''.join((chr(i // 256) + chr(i % 256)) for i in dbyts) + else: + raise RuntimeError("What family?") + + +def patch_socket(): + if not hasattr(socket, 'inet_pton'): + socket.inet_pton = inet_pton + + if not hasattr(socket, 'inet_ntop'): + socket.inet_ntop = inet_ntop + + +patch_socket() + + +ADDRTYPE_IPV4 = 1 +ADDRTYPE_IPV6 = 4 +ADDRTYPE_HOST = 3 + + +def pack_addr(address): + address_str = to_str(address) + for family in (socket.AF_INET, socket.AF_INET6): + try: + r = socket.inet_pton(family, address_str) + if family == socket.AF_INET6: + return b'\x04' + r + else: + return b'\x01' + r + except (TypeError, ValueError, OSError, IOError): + pass + if len(address) > 255: + address = address[:255] # TODO + return b'\x03' + chr(len(address)) + address + + +def parse_header(data): + addrtype = ord(data[0]) + dest_addr = None + dest_port = None + header_length = 0 + if addrtype == ADDRTYPE_IPV4: + if len(data) >= 7: + dest_addr = socket.inet_ntoa(data[1:5]) + dest_port = struct.unpack('>H', data[5:7])[0] + header_length = 7 + else: + logging.warn('header is too short') + elif addrtype == ADDRTYPE_HOST: + if len(data) > 2: + addrlen = ord(data[1]) + if len(data) >= 2 + addrlen: + dest_addr = data[2:2 + addrlen] + dest_port = struct.unpack('>H', data[2 + addrlen:4 + + addrlen])[0] + header_length = 4 + addrlen + else: + logging.warn('header is too short') + else: + logging.warn('header is too short') + elif addrtype == ADDRTYPE_IPV6: + if len(data) >= 19: + dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) + dest_port = struct.unpack('>H', data[17:19])[0] + header_length = 19 + else: + logging.warn('header is too short') + else: + logging.warn('unsupported addrtype %d, maybe wrong password' % + addrtype) + if dest_addr is None: + return None + return addrtype, to_bytes(dest_addr), dest_port, header_length + + +def test_inet_conv(): + ipv4 = b'8.8.4.4' + b = inet_pton(socket.AF_INET, ipv4) + assert inet_ntop(socket.AF_INET, b) == ipv4 + ipv6 = b'2404:6800:4005:805::1011' + b = inet_pton(socket.AF_INET6, ipv6) + assert inet_ntop(socket.AF_INET6, b) == ipv6 + + +def test_parse_header(): + assert parse_header(b'\x03\x0ewww.google.com\x00\x50') == \ + (3, b'www.google.com', 80, 18) + assert parse_header(b'\x01\x08\x08\x08\x08\x00\x35') == \ + (1, b'8.8.8.8', 53, 7) + assert parse_header((b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00' + b'\x00\x10\x11\x00\x50')) == \ + (4, b'2404:6800:4005:805::1011', 80, 19) + + +def test_pack_header(): + assert pack_addr(b'8.8.8.8') == b'\x01\x08\x08\x08\x08' + assert pack_addr(b'2404:6800:4005:805::1011') == \ + b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00\x00\x10\x11' + assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com' + + +if __name__ == '__main__': + test_inet_conv() + test_parse_header() + test_pack_header() diff --git a/shadowsocks/crypto/__init__.py b/shadowsocks/crypto/__init__.py new file mode 100644 index 0000000..6251321 --- /dev/null +++ b/shadowsocks/crypto/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement diff --git a/shadowsocks/crypto/ctypes_libsodium.py b/shadowsocks/crypto/ctypes_libsodium.py new file mode 100644 index 0000000..3598e3b --- /dev/null +++ b/shadowsocks/crypto/ctypes_libsodium.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ + create_string_buffer, c_void_p + +__all__ = ['ciphers', 'auths'] + +libsodium = None +loaded = False + +buf_size = 2048 + +# for salsa20 and chacha20 +BLOCK_SIZE = 64 + + +def load_libsodium(): + global loaded, libsodium, buf + + from ctypes.util import find_library + for p in ('sodium', 'libsodium'): + libsodium_path = find_library(p) + if libsodium_path: + break + else: + raise Exception('libsodium not found') + logging.info('loading libsodium from %s', libsodium_path) + libsodium = CDLL(libsodium_path) + libsodium.sodium_init.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + libsodium.crypto_stream_chacha20_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + + libsodium.crypto_onetimeauth.restype = c_int + libsodium.crypto_onetimeauth.argtypes = (c_void_p, c_char_p, + c_ulonglong, c_char_p) + + libsodium.crypto_onetimeauth_verify.restype = c_int + libsodium.crypto_onetimeauth_verify.argtypes = (c_char_p, c_char_p, + c_ulonglong, c_char_p) + + libsodium.sodium_init() + + buf = create_string_buffer(buf_size) + loaded = True + + +class Salsa20Crypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_libsodium() + self.key = key + self.iv = iv + self.key_ptr = c_char_p(key) + self.iv_ptr = c_char_p(iv) + if cipher_name == b'salsa20': + self.cipher = libsodium.crypto_stream_salsa20_xor_ic + elif cipher_name == b'chacha20': + self.cipher = libsodium.crypto_stream_chacha20_xor_ic + else: + raise Exception('Unknown cipher') + # byte counter, not block counter + self.counter = 0 + + def update(self, data): + global buf_size, buf + l = len(data) + + # we can only prepend some padding to make the encryption align to + # blocks + padding = self.counter % BLOCK_SIZE + if buf_size < padding + l: + buf_size = (padding + l) * 2 + buf = create_string_buffer(buf_size) + + if padding: + data = (b'\0' * padding) + data + self.cipher(byref(buf), c_char_p(data), padding + l, + self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) + self.counter += l + # buf is copied to a str object when we access buf.raw + # strip off the padding + return buf.raw[padding:padding + l] + + +class Poly1305(object): + @staticmethod + def auth(method, key, data): + if not loaded: + load_libsodium() + tag_buf = create_string_buffer(16) + libsodium.crypto_onetimeauth(byref(tag_buf), data, len(data), key) + return tag_buf.raw + + @staticmethod + def verify(method, key, data, tag): + if not loaded: + load_libsodium() + r = libsodium.crypto_onetimeauth_verify(tag, data, len(data), key) + return r == 0 + + +ciphers = { + b'salsa20': (32, 8, Salsa20Crypto), + b'chacha20': (32, 8, Salsa20Crypto), +} + +auths = { + b'poly1305': (32, 16, Poly1305) +} + + +def test_salsa20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +if __name__ == '__main__': + test_chacha20() + test_salsa20() diff --git a/shadowsocks/crypto/ctypes_openssl.py b/shadowsocks/crypto/ctypes_openssl.py new file mode 100644 index 0000000..0ef8ce0 --- /dev/null +++ b/shadowsocks/crypto/ctypes_openssl.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ + create_string_buffer, c_void_p + +__all__ = ['ciphers'] + +libcrypto = None +loaded = False + +buf_size = 2048 + + +def load_openssl(): + global loaded, libcrypto, buf + + from ctypes.util import find_library + for p in ('crypto', 'eay32', 'libeay32'): + libcrypto_path = find_library(p) + if libcrypto_path: + break + else: + raise Exception('libcrypto(OpenSSL) not found') + logging.info('loading libcrypto from %s', libcrypto_path) + libcrypto = CDLL(libcrypto_path) + libcrypto.EVP_get_cipherbyname.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p + + libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p, c_char_p, c_int) + + libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, + c_char_p, c_int) + + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): + libcrypto.OpenSSL_add_all_ciphers() + + buf = create_string_buffer(buf_size) + loaded = True + + +def load_cipher(cipher_name): + func_name = b'EVP_' + cipher_name.replace(b'-', b'_') + if bytes != str: + func_name = str(func_name, 'utf-8') + cipher = getattr(libcrypto, func_name, None) + if cipher: + cipher.restype = c_void_p + return cipher() + return None + + +class CtypesCrypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_openssl() + self._ctx = None + cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + if not cipher: + cipher = load_cipher(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libcrypto' % cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + self._ctx = libcrypto.EVP_CIPHER_CTX_new() + if not self._ctx: + raise Exception('can not create cipher context') + r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def update(self, data): + global buf_size, buf + cipher_out_len = c_long(0) + l = len(data) + if buf_size < l: + buf_size = l * 2 + buf = create_string_buffer(buf_size) + libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l) + # buf is copied to a str object when we access buf.raw + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + libcrypto.EVP_CIPHER_CTX_free(self._ctx) + + +ciphers = { + b'aes-128-cfb': (16, 16, CtypesCrypto), + b'aes-192-cfb': (24, 16, CtypesCrypto), + b'aes-256-cfb': (32, 16, CtypesCrypto), + b'aes-128-ofb': (16, 16, CtypesCrypto), + b'aes-192-ofb': (24, 16, CtypesCrypto), + b'aes-256-ofb': (32, 16, CtypesCrypto), + b'aes-128-ctr': (16, 16, CtypesCrypto), + b'aes-192-ctr': (24, 16, CtypesCrypto), + b'aes-256-ctr': (32, 16, CtypesCrypto), + b'aes-128-cfb8': (16, 16, CtypesCrypto), + b'aes-192-cfb8': (24, 16, CtypesCrypto), + b'aes-256-cfb8': (32, 16, CtypesCrypto), + b'aes-128-cfb1': (16, 16, CtypesCrypto), + b'aes-192-cfb1': (24, 16, CtypesCrypto), + b'aes-256-cfb1': (32, 16, CtypesCrypto), + b'bf-cfb': (16, 8, CtypesCrypto), + b'camellia-128-cfb': (16, 16, CtypesCrypto), + b'camellia-192-cfb': (24, 16, CtypesCrypto), + b'camellia-256-cfb': (32, 16, CtypesCrypto), + b'cast5-cfb': (16, 8, CtypesCrypto), + b'des-cfb': (8, 8, CtypesCrypto), + b'idea-cfb': (16, 8, CtypesCrypto), + b'rc2-cfb': (16, 8, CtypesCrypto), + b'rc4': (16, 0, CtypesCrypto), + b'seed-cfb': (16, 16, CtypesCrypto), +} + + +def run_method(method): + from shadowsocks.crypto import util + + cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_aes_128_cfb(): + run_method(b'aes-128-cfb') + + +def test_aes_256_cfb(): + run_method(b'aes-256-cfb') + + +def test_aes_128_cfb8(): + run_method(b'aes-128-cfb8') + + +def test_aes_256_ofb(): + run_method(b'aes-256-ofb') + + +def test_aes_256_ctr(): + run_method(b'aes-256-ctr') + + +def test_bf_cfb(): + run_method(b'bf-cfb') + + +def test_rc4(): + run_method(b'rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/crypto/hmac.py b/shadowsocks/crypto/hmac.py new file mode 100644 index 0000000..ce36540 --- /dev/null +++ b/shadowsocks/crypto/hmac.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import hmac +import hashlib + +from shadowsocks import common + +__all__ = ['auths'] + + +class HMAC(object): + @staticmethod + def auth(method, key, data): + digest = common.to_str(method.replace(b'hmac-', b'')) + return hmac.new(key, data, getattr(hashlib, digest)).digest() + + @staticmethod + def verify(method, key, data, tag): + digest = common.to_str(method.replace(b'hmac-', b'')) + t = hmac.new(key, data, getattr(hashlib, digest)).digest() + if hasattr(hmac, 'compare_digest'): + return hmac.compare_digest(t, tag) + else: + return _time_independent_equals(t, tag) + + +# from tornado +def _time_independent_equals(a, b): + if len(a) != len(b): + return False + result = 0 + if type(a[0]) is int: # python3 byte strings + for x, y in zip(a, b): + result |= x ^ y + else: # python2 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + + +auths = { + b'hmac-md5': (32, 16, HMAC), + b'hmac-sha256': (32, 32, HMAC), +} diff --git a/shadowsocks/crypto/m2.py b/shadowsocks/crypto/m2.py new file mode 100644 index 0000000..5ad48a8 --- /dev/null +++ b/shadowsocks/crypto/m2.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import logging + +__all__ = ['ciphers'] + +has_m2 = True +try: + __import__('M2Crypto') +except ImportError: + has_m2 = False +if bytes != str: + has_m2 = False + + +def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, + padding=1): + + import M2Crypto.EVP + return M2Crypto.EVP.Cipher(alg.replace('-', '_'), key, iv, op, + key_as_bytes=0, d='md5', salt=None, i=1, + padding=1) + + +def err(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1): + logging.error(('M2Crypto is required to use %s, please run' + ' `apt-get install python-m2crypto`') % alg) + sys.exit(1) + + +if has_m2: + ciphers = { + b'aes-128-cfb': (16, 16, create_cipher), + b'aes-192-cfb': (24, 16, create_cipher), + b'aes-256-cfb': (32, 16, create_cipher), + b'bf-cfb': (16, 8, create_cipher), + b'camellia-128-cfb': (16, 16, create_cipher), + b'camellia-192-cfb': (24, 16, create_cipher), + b'camellia-256-cfb': (32, 16, create_cipher), + b'cast5-cfb': (16, 8, create_cipher), + b'des-cfb': (8, 8, create_cipher), + b'idea-cfb': (16, 8, create_cipher), + b'rc2-cfb': (16, 8, create_cipher), + b'rc4': (16, 0, create_cipher), + b'seed-cfb': (16, 16, create_cipher), + } +else: + ciphers = {} + + +def run_method(method): + from shadowsocks.crypto import util + + cipher = create_cipher(method, b'k' * 32, b'i' * 16, 1) + decipher = create_cipher(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def check_env(): + # skip this test on pypy and Python 3 + try: + import __pypy__ + del __pypy__ + from nose.plugins.skip import SkipTest + raise SkipTest + except ImportError: + pass + if bytes != str: + from nose.plugins.skip import SkipTest + raise SkipTest + + +def test_aes_128_cfb(): + check_env() + run_method(b'aes-128-cfb') + + +def test_aes_256_cfb(): + check_env() + run_method(b'aes-256-cfb') + + +def test_bf_cfb(): + check_env() + run_method(b'bf-cfb') + + +def test_rc4(): + check_env() + run_method(b'rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py new file mode 100644 index 0000000..3062dcc --- /dev/null +++ b/shadowsocks/crypto/rc4_md5.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement + +import hashlib + + +__all__ = ['ciphers'] + + +def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, + i=1, padding=1): + md5 = hashlib.md5() + md5.update(key) + md5.update(iv) + rc4_key = md5.digest() + + try: + from shadowsocks.crypto import ctypes_openssl + return ctypes_openssl.CtypesCrypto(b'rc4', rc4_key, b'', op) + except: + import M2Crypto.EVP + return M2Crypto.EVP.Cipher(b'rc4', rc4_key, b'', op, + key_as_bytes=0, d='md5', salt=None, i=1, + padding=1) + + +ciphers = { + b'rc4-md5': (16, 16, create_cipher), +} + + +def test(): + from shadowsocks.crypto import util + + cipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 1) + decipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/encrypt_salsa20.py b/shadowsocks/crypto/salsa20_ctr.py similarity index 53% rename from shadowsocks/encrypt_salsa20.py rename to shadowsocks/crypto/salsa20_ctr.py index d69010f..0ea13b8 100644 --- a/shadowsocks/encrypt_salsa20.py +++ b/shadowsocks/crypto/salsa20_ctr.py @@ -1,6 +1,28 @@ -#!/usr/bin/python +#!/usr/bin/env python + +# 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 __future__ import absolute_import, division, print_function, \ + with_statement -import time import struct import logging import sys @@ -8,6 +30,9 @@ import sys slow_xor = False imported = False +salsa20 = None +numpy = None + BLOCK_SIZE = 16384 @@ -16,13 +41,13 @@ def run_imports(): if not imported: imported = True try: - import numpy + numpy = __import__('numpy') except ImportError: logging.error('can not import numpy, using SLOW XOR') logging.error('please install numpy if you use salsa20') slow_xor = True try: - import salsa20 + salsa20 = __import__('salsa20') except ImportError: logging.error('you have to install salsa20 before you use salsa20') sys.exit(1) @@ -46,9 +71,14 @@ def numpy_xor(a, b): def py_xor_str(a, b): c = [] - for i in xrange(0, len(a)): - c.append(chr(ord(a[i]) ^ ord(b[i]))) - return ''.join(c) + if bytes == str: + for i in range(0, len(a)): + c.append(chr(ord(a[i]) ^ ord(b[i]))) + return ''.join(c) + else: + for i in range(0, len(a)): + c.append(a[i] ^ b[i]) + return bytes(c) class Salsa20Cipher(object): @@ -57,7 +87,7 @@ class Salsa20Cipher(object): def __init__(self, alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1): run_imports() - if alg != 'salsa20-ctr': + if alg != b'salsa20-ctr': raise Exception('unknown algorithm') self._key = key self._nonce = struct.unpack(' 0: + # parent waits for its child + time.sleep(5) + sys.exit(0) + + # child signals its parent to exit + ppid = os.getppid() + pid = os.getpid() + if write_pid_file(pid_file, pid) != 0: + os.kill(ppid, signal.SIGINT) + sys.exit(1) + + os.setsid() + signal.signal(signal.SIG_IGN, signal.SIGHUP) + + print('started') + os.kill(ppid, signal.SIGTERM) + + sys.stdin.close() + try: + freopen(log_file, 'a', sys.stdout) + freopen(log_file, 'a', sys.stderr) + except IOError as e: + logging.error(e) + sys.exit(1) + + +def daemon_stop(pid_file): + import errno + try: + with open(pid_file) as f: + buf = f.read() + pid = common.to_str(buf) + if not buf: + logging.error('not running') + except IOError as e: + logging.error(e) + if e.errno == errno.ENOENT: + # always exit 0 if we are sure daemon is not running + logging.error('not running') + return + sys.exit(1) + pid = int(pid) + if pid > 0: + try: + os.kill(pid, signal.SIGTERM) + except OSError as e: + if e.errno == errno.ESRCH: + logging.error('not running') + # always exit 0 if we are sure daemon is not running + return + logging.error(e) + sys.exit(1) + else: + logging.error('pid is not positive: %d', pid) + + # sleep for maximum 10s + for i in range(0, 200): + try: + # query for the pid + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + break + time.sleep(0.05) + else: + logging.error('timed out when stopping pid %d', pid) + sys.exit(1) + print('stopped') + os.unlink(pid_file) diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 267565c..632f147 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -20,13 +20,32 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import absolute_import, division, print_function, \ + with_statement + import os import sys import hashlib -import string -import struct import logging -import encrypt_salsa20 + +from shadowsocks.crypto import m2, rc4_md5, salsa20_ctr,\ + ctypes_openssl, ctypes_libsodium, table, hmac +from shadowsocks import common + + +ciphers_supported = {} +ciphers_supported.update(rc4_md5.ciphers) +ciphers_supported.update(salsa20_ctr.ciphers) +ciphers_supported.update(ctypes_openssl.ciphers) +ciphers_supported.update(ctypes_libsodium.ciphers) +# let M2Crypto override ctypes_openssl +ciphers_supported.update(m2.ciphers) +ciphers_supported.update(table.ciphers) + + +auths_supported = {} +auths_supported.update(hmac.auths) +auths_supported.update(ctypes_libsodium.auths) def random_string(length): @@ -34,60 +53,20 @@ def random_string(length): import M2Crypto.Rand return M2Crypto.Rand.rand_bytes(length) except ImportError: - # TODO really strong enough on Linux? return os.urandom(length) -cached_tables = {} -cached_keys = {} - - -def get_table(key): - m = hashlib.md5() - m.update(key) - s = m.digest() - (a, b) = struct.unpack(' 0: @@ -95,132 +74,186 @@ def EVP_BytesToKey(password, key_len, iv_len): md5.update(data) m.append(md5.digest()) i += 1 - ms = ''.join(m) + ms = b''.join(m) key = ms[:key_len] iv = ms[key_len:key_len + iv_len] - cached_keys[password] = (key, iv) - return (key, iv) - - -method_supported = { - 'aes-128-cfb': (16, 16), - 'aes-192-cfb': (24, 16), - 'aes-256-cfb': (32, 16), - 'bf-cfb': (16, 8), - 'camellia-128-cfb': (16, 16), - 'camellia-192-cfb': (24, 16), - 'camellia-256-cfb': (32, 16), - 'cast5-cfb': (16, 8), - 'des-cfb': (8, 8), - 'idea-cfb': (16, 8), - 'rc2-cfb': (16, 8), - 'rc4': (16, 0), - 'seed-cfb': (16, 16), - 'salsa20-ctr': (32, 8), -} + return key, iv class Encryptor(object): - def __init__(self, key, method=None): - if method == 'table': - method = None + def __init__(self, key, method): self.key = key self.method = method self.iv = None self.iv_sent = False - self.cipher_iv = '' + self.cipher_iv = b'' self.decipher = None - if method: - self.cipher = self.get_cipher(key, method, 1, iv=random_string(32)) - else: - self.encrypt_table, self.decrypt_table = init_table(key) - self.cipher = None - - def get_cipher_len(self, method): method = method.lower() - m = method_supported.get(method, None) + self._method_info = self.get_method_info(method) + if self._method_info: + self.cipher = self.get_cipher(key, method, 1, + random_string(self._method_info[1])) + else: + logging.error('method %s not supported' % method) + sys.exit(1) + + def get_method_info(self, method): + method = method.lower() + m = ciphers_supported.get(method) return m def iv_len(self): return len(self.cipher_iv) - def get_cipher(self, password, method, op, iv=None): - password = password.encode('utf-8') - method = method.lower() - m = self.get_cipher_len(method) - if m: + def get_cipher(self, password, method, op, iv): + password = common.to_bytes(password) + m = self._method_info + if m[0] > 0: key, iv_ = EVP_BytesToKey(password, m[0], m[1]) - if iv is None: - iv = iv_ - iv = iv[:m[1]] - if op == 1: - self.cipher_iv = iv[:m[1]] # this iv is for cipher not decipher - if method != 'salsa20-ctr': - import M2Crypto.EVP - return M2Crypto.EVP.Cipher(method.replace('-', '_'), key, iv, op, - key_as_bytes=0, d='md5', salt=None, i=1, - padding=1) - else: - return encrypt_salsa20.Salsa20Cipher(method, key, iv, op) + else: + # key_length == 0 indicates we should use the key directly + key, iv = password, b'' - logging.error('method %s not supported' % method) - sys.exit(1) + iv = iv[:m[1]] + if op == 1: + # this iv is for cipher not decipher + self.cipher_iv = iv[:m[1]] + return m[2](method, key, iv, op) def encrypt(self, buf): if len(buf) == 0: return buf - if not self.method: - return string.translate(buf, self.encrypt_table) + if self.iv_sent: + return self.cipher.update(buf) else: - if self.iv_sent: - return self.cipher.update(buf) - else: - self.iv_sent = True - return self.cipher_iv + self.cipher.update(buf) + self.iv_sent = True + return self.cipher_iv + self.cipher.update(buf) def decrypt(self, buf): if len(buf) == 0: return buf - if not self.method: - return string.translate(buf, self.decrypt_table) - else: - if self.decipher is None: - decipher_iv_len = self.get_cipher_len(self.method)[1] - decipher_iv = buf[:decipher_iv_len] - self.decipher = self.get_cipher(self.key, self.method, 0, - iv=decipher_iv) - buf = buf[decipher_iv_len:] - if len(buf) == 0: - return buf - return self.decipher.update(buf) + if self.decipher is None: + decipher_iv_len = self._method_info[1] + decipher_iv = buf[:decipher_iv_len] + self.decipher = self.get_cipher(self.key, self.method, 0, + iv=decipher_iv) + buf = buf[decipher_iv_len:] + if len(buf) == 0: + return buf + return self.decipher.update(buf) def encrypt_all(password, method, op, data): - if method is not None and method.lower() == 'table': - method = None - if not method: - [encrypt_table, decrypt_table] = init_table(password) - if op: - return string.translate(encrypt_table, data) - else: - return string.translate(decrypt_table, data) + result = [] + method = method.lower() + password = common.to_bytes(password) + (key_len, iv_len, m) = ciphers_supported[method] + if key_len > 0: + key, _ = EVP_BytesToKey(password, key_len, iv_len) else: - import M2Crypto.EVP - result = [] - method = method.lower() - (key_len, iv_len) = method_supported[method] - (key, _) = EVP_BytesToKey(password, key_len, iv_len) - if op: - iv = random_string(iv_len) - result.append(iv) - else: - iv = data[:iv_len] - data = data[iv_len:] - cipher = M2Crypto.EVP.Cipher(method.replace('-', '_'), key, iv, op, - key_as_bytes=0, d='md5', salt=None, i=1, - padding=1) - result.append(cipher.update(data)) - f = cipher.final() - if f: - result.append(f) - return ''.join(result) + key = password + if op: + iv = random_string(iv_len) + result.append(iv) + else: + iv = data[:iv_len] + data = data[iv_len:] + cipher = m(method, key, iv, op) + result.append(cipher.update(data)) + return b''.join(result) + + +def auth_create(data, password, iv, method): + if method is None: + return data + # prepend hmac to data + password = common.to_bytes(password) + method = method.lower() + method_info = auths_supported.get(method) + if not method_info: + logging.error('method %s not supported' % method) + sys.exit(1) + key_len, tag_len, m = method_info + key, _ = EVP_BytesToKey(password + iv, key_len, 0) + tag = m.auth(method, key, data) + return tag + data + + +def auth_open(data, password, iv, method): + if not method: + return data + # verify hmac and remove the hmac or return None + password = common.to_bytes(password) + method = method.lower() + method_info = auths_supported.get(method) + if not method_info: + logging.error('method %s not supported' % method) + sys.exit(1) + key_len, tag_len, m = method_info + key, _ = EVP_BytesToKey(password + iv, key_len, 0) + if len(data) <= tag_len: + return None + result = data[tag_len:] + if not m.verify(method, key, result, data[:tag_len]): + return None + return result + + +CIPHERS_TO_TEST = [ + b'aes-128-cfb', + b'aes-256-cfb', + b'rc4-md5', + b'salsa20', + b'chacha20', + b'table', +] + +AUTHS_TO_TEST = [ + None, + b'hmac-md5', + b'hmac-sha256', + b'poly1305', +] + + +def test_encryptor(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + encryptor = Encryptor(b'key', method) + decryptor = Encryptor(b'key', method) + cipher = encryptor.encrypt(plain) + plain2 = decryptor.decrypt(cipher) + assert plain == plain2 + + +def test_encrypt_all(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + cipher = encrypt_all(b'key', method, 1, plain) + plain2 = encrypt_all(b'key', method, 0, cipher) + assert plain == plain2 + + +def test_auth(): + from os import urandom + plain = urandom(10240) + for method in AUTHS_TO_TEST: + logging.warn(method) + boxed = auth_create(plain, b'key', b'iv', method) + unboxed = auth_open(boxed, b'key', b'iv', method) + assert plain == unboxed + if method is not None: + b = common.ord(boxed[0]) + b ^= 1 + attack = common.chr(b) + boxed[1:] + assert auth_open(attack, b'key', b'iv', method) is None + + +if __name__ == '__main__': + test_encrypt_all() + test_encryptor() + test_auth() diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 89ffbcb..55c30bb 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -24,13 +24,19 @@ # from ssloop # https://github.com/clowwindy/ssloop +from __future__ import absolute_import, division, print_function, \ + with_statement +import os +import socket import select +import errno +import logging from collections import defaultdict __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', - 'POLL_HUP', 'POLL_NVAL'] + 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] POLL_NULL = 0x00 POLL_IN = 0x01 @@ -40,6 +46,16 @@ POLL_HUP = 0x10 POLL_NVAL = 0x20 +EVENT_NAMES = { + POLL_NULL: 'POLL_NULL', + POLL_IN: 'POLL_IN', + POLL_OUT: 'POLL_OUT', + POLL_ERR: 'POLL_ERR', + POLL_HUP: 'POLL_HUP', + POLL_NVAL: 'POLL_NVAL', +} + + class EpollLoop(object): def __init__(self): @@ -86,7 +102,7 @@ class KqueueLoop(object): results[fd] |= POLL_IN elif e.filter == select.KQ_FILTER_WRITE: results[fd] |= POLL_OUT - return results.iteritems() + return results.items() def add_fd(self, fd, mode): self._fds[fd] = mode @@ -140,20 +156,28 @@ class SelectLoop(object): class EventLoop(object): def __init__(self): + self._iterating = False if hasattr(select, 'epoll'): self._impl = EpollLoop() + model = 'epoll' elif hasattr(select, 'kqueue'): self._impl = KqueueLoop() + model = 'kqueue' elif hasattr(select, 'select'): self._impl = SelectLoop() + model = 'select' else: raise Exception('can not find any available functions in select ' 'package') self._fd_to_f = {} + self._handlers = [] + self._ref_handlers = [] + self._handlers_to_remove = [] + logging.debug('using event model: %s', model) def poll(self, timeout=None): events = self._impl.poll(timeout) - return ((self._fd_to_f[fd], event) for fd, event in events) + return [(self._fd_to_f[fd], fd, event) for fd, event in events] def add(self, f, mode): fd = f.fileno() @@ -162,13 +186,57 @@ class EventLoop(object): def remove(self, f): fd = f.fileno() - self._fd_to_f[fd] = None + del self._fd_to_f[fd] self._impl.remove_fd(fd) def modify(self, f, mode): fd = f.fileno() self._impl.modify_fd(fd, mode) + def add_handler(self, handler, ref=True): + self._handlers.append(handler) + if ref: + # when all ref handlers are removed, loop stops + self._ref_handlers.append(handler) + + def remove_handler(self, handler): + if handler in self._ref_handlers: + self._ref_handlers.remove(handler) + if self._iterating: + self._handlers_to_remove.append(handler) + else: + self._handlers.remove(handler) + + def run(self): + events = [] + while self._ref_handlers: + try: + events = self.poll(1) + except (OSError, IOError) as e: + if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): + # EPIPE: Happens when the client closes the connection + # EINTR: Happens when received a signal + # handles them as soon as possible + logging.debug('poll:%s', e) + else: + logging.error('poll:%s', e) + import traceback + traceback.print_exc() + continue + self._iterating = True + for handler in self._handlers: + # TODO when there are a lot of handlers + try: + handler(events) + except (OSError, IOError) as e: + logging.error(e) + import traceback + traceback.print_exc() + for handler in self._handlers_to_remove: + self._handlers.remove(handler) + self._handlers_to_remove = [] + self._iterating = False + # from tornado def errno_from_exception(e): @@ -187,3 +255,9 @@ def errno_from_exception(e): return e.args[0] else: return None + + +# from tornado +def get_sock_error(sock): + error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + return socket.error(error_number, os.strerror(error_number)) diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 049c8ea..44e9fca 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -21,354 +21,65 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import with_statement +from __future__ import absolute_import, division, print_function, \ + with_statement + import sys -if sys.version_info < (2, 6): - import simplejson as json -else: - import json - -try: - import gevent - import gevent.monkey - gevent.monkey.patch_all(dns=gevent.version_info[0] >= 1) -except ImportError: - gevent = None - print >>sys.stderr, 'warning: gevent not found, using threading instead' - -import socket -import eventloop -import errno -import select -import SocketServer -import struct import os -import random -import re import logging -import getopt -import encrypt -import utils -import udprelay +import signal - -MSG_FASTOPEN = 0x20000000 - - -def send_all(sock, data): - bytes_sent = 0 - while True: - r = sock.send(data[bytes_sent:]) - if r < 0: - return r - bytes_sent += r - if bytes_sent == len(data): - return bytes_sent - - -class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): - allow_reuse_address = True - - def get_request(self): - connection = self.socket.accept() - connection[0].settimeout(config_timeout) - return connection - - -class Socks5Server(SocketServer.StreamRequestHandler): - @staticmethod - def get_server(): - a_port = config_server_port - a_server = config_server - if isinstance(config_server_port, list): - # support config like "server_port": [8081, 8082] - a_port = random.choice(config_server_port) - if isinstance(config_server, list): - # support config like "server": ["123.123.123.1", "123.123.123.2"] - a_server = random.choice(config_server) - - r = re.match(r'^(.*):(\d+)$', a_server) - if r: - # support config like "server": "123.123.123.1:8381" - # or "server": ["123.123.123.1:8381", "123.123.123.2:8381"] - a_server = r.group(1) - a_port = int(r.group(2)) - return a_server, a_port - - @staticmethod - def handle_tcp(sock, remote, encryptor, pending_data=None, - server=None, port=None): - connected = False - try: - if config_fast_open: - fdset = [sock] - else: - fdset = [sock, remote] - while True: - should_break = False - r, w, e = select.select(fdset, [], [], config_timeout) - if not r: - logging.warn('read time out') - break - if sock in r: - if not connected and config_fast_open: - data = sock.recv(4096) - data = encryptor.encrypt(pending_data + data) - pending_data = None - logging.info('fast open %s:%d' % (server, port)) - try: - remote.sendto(data, MSG_FASTOPEN, (server, port)) - except (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == errno.EINPROGRESS: - pass - else: - raise e - connected = True - fdset = [sock, remote] - else: - data = sock.recv(4096) - if pending_data: - data = pending_data + data - pending_data = None - data = encryptor.encrypt(data) - if len(data) <= 0: - should_break = True - else: - result = send_all(remote, data) - if result < len(data): - raise Exception('failed to send all data') - - if remote in r: - data = encryptor.decrypt(remote.recv(4096)) - if len(data) <= 0: - should_break = True - else: - result = send_all(sock, data) - if result < len(data): - raise Exception('failed to send all data') - if should_break: - # make sure all data are read before we close the sockets - # TODO: we haven't read ALL the data, actually - # http://cs.ecs.baylor.edu/~donahoo/practical/CSockets/TCPRST.pdf - break - finally: - sock.close() - remote.close() - - def handle(self): - try: - encryptor = encrypt.Encryptor(config_password, config_method) - sock = self.connection - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - data = sock.recv(262) - if not data: - sock.close() - return - if len(data) < 3: - return - method = ord(data[2]) - if method == 2: - logging.warn('client tries to use username/password auth, prete' - 'nding the password is OK') - sock.send('\x05\x02') - try: - ver_ulen = sock.recv(2) - ulen = ord(ver_ulen[1]) - if ulen: - username = sock.recv(ulen) - assert(ulen == len(username)) - plen = ord(sock.recv(1)) - if plen: - _password = sock.recv(plen) - assert(plen == len(_password)) - sock.send('\x01\x00') - except Exception as e: - logging.error(e) - return - elif method == 0: - sock.send("\x05\x00") - else: - logging.error('unsupported method %d' % method) - return - data = self.rfile.read(4) or '\x00' * 4 - mode = ord(data[1]) - if mode == 1: - pass - elif mode == 3: - # UDP - logging.debug('UDP assc request') - if sock.family == socket.AF_INET6: - header = '\x05\x00\x00\x04' - else: - header = '\x05\x00\x00\x01' - addr, port = sock.getsockname() - addr_to_send = socket.inet_pton(sock.family, addr) - port_to_send = struct.pack('>H', port) - sock.send(header + addr_to_send + port_to_send) - while True: - data = sock.recv(4096) - if not data: - break - return - else: - logging.warn('unknown mode %d' % mode) - return - addrtype = ord(data[3]) - addr_to_send = data[3] - if addrtype == 1: - addr_ip = self.rfile.read(4) - addr = socket.inet_ntoa(addr_ip) - addr_to_send += addr_ip - elif addrtype == 3: - addr_len = self.rfile.read(1) - addr = self.rfile.read(ord(addr_len)) - addr_to_send += addr_len + addr - elif addrtype == 4: - addr_ip = self.rfile.read(16) - addr = socket.inet_ntop(socket.AF_INET6, addr_ip) - addr_to_send += addr_ip - else: - logging.warn('addr_type not supported') - # not supported - return - addr_port = self.rfile.read(2) - addr_to_send += addr_port - port = struct.unpack('>H', addr_port) - try: - reply = "\x05\x00\x00\x01" - reply += socket.inet_aton('0.0.0.0') + struct.pack(">H", 2222) - self.wfile.write(reply) - # reply immediately - a_server, a_port = Socks5Server.get_server() - addrs = socket.getaddrinfo(a_server, a_port) - if addrs: - af, socktype, proto, canonname, sa = addrs[0] - if config_fast_open: - remote = socket.socket(af, socktype, proto) - remote.setsockopt(socket.IPPROTO_TCP, - socket.TCP_NODELAY, 1) - Socks5Server.handle_tcp(sock, remote, encryptor, - addr_to_send, a_server, a_port) - else: - logging.info('connecting %s:%d' % (addr, port[0])) - remote = socket.create_connection((a_server, a_port), - timeout=config_timeout) - remote.settimeout(config_timeout) - remote.setsockopt(socket.IPPROTO_TCP, - socket.TCP_NODELAY, 1) - Socks5Server.handle_tcp(sock, remote, encryptor, - addr_to_send) - except (OSError, IOError) as e: - logging.warn(e) - return - except (OSError, IOError) as e: - raise e - logging.warn(e) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ + asyncdns def main(): - global config_server, config_server_port, config_password, config_method,\ - config_fast_open, config_timeout - - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)-8s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') + utils.check_python() # fix py2exe if hasattr(sys, "frozen") and sys.frozen in \ ("windows_exe", "console_exe"): p = os.path.dirname(os.path.abspath(sys.executable)) os.chdir(p) - version = '' + + config = utils.get_config(True) + + daemon.daemon_exec(config) + + utils.print_shadowsocks() + + encrypt.try_cipher(config['password'], config['method'], config['auth']) + try: - import pkg_resources - version = pkg_resources.get_distribution('shadowsocks').version - except: - pass - print 'shadowsocks %s' % version - - config_password = None - config_method = None - - config_path = utils.find_config() - try: - optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:t:', - ['fast-open']) - for key, value in optlist: - if key == '-c': - config_path = value - - if config_path: - logging.info('loading config from %s' % config_path) - with open(config_path, 'rb') as f: - try: - config = json.load(f) - except ValueError as e: - logging.error('found an error in config.json: %s', - e.message) - sys.exit(1) - else: - config = {} - - optlist, args = getopt.getopt(sys.argv[1:], 's:b:p:k:l:m:c:t:', - ['fast-open']) - for key, value in optlist: - if key == '-p': - config['server_port'] = int(value) - elif key == '-k': - config['password'] = value - elif key == '-l': - config['local_port'] = int(value) - elif key == '-s': - config['server'] = value - elif key == '-m': - config['method'] = value - elif key == '-b': - config['local_address'] = value - elif key == '--fast-open': - config['fast_open'] = True - except getopt.GetoptError as e: - logging.error(e) - utils.print_local_help() - sys.exit(2) - - config_server = config['server'] - config_server_port = config['server_port'] - config_local_port = config['local_port'] - config_password = config['password'] - config_method = config.get('method', None) - config_local_address = config.get('local_address', '127.0.0.1') - config_timeout = int(config.get('timeout', 300)) - config_fast_open = config.get('fast_open', False) - - if not config_password and not config_path: - sys.exit('config not specified, please read ' - 'https://github.com/clowwindy/shadowsocks') - - utils.check_config(config) - - encrypt.init_table(config_password, config_method) - - addrs = socket.getaddrinfo(config_local_address, config_local_port) - if not addrs: - logging.error('cant resolve listen address') - sys.exit(1) - ThreadingTCPServer.address_family = addrs[0][0] - try: - udprelay.UDPRelay(config_local_address, int(config_local_port), - config_server, config_server_port, config_password, - config_method, int(config_timeout), True).start() - server = ThreadingTCPServer((config_local_address, config_local_port), - Socks5Server) - server.timeout = int(config_timeout) logging.info("starting local at %s:%d" % - tuple(server.server_address[:2])) - server.serve_forever() - except socket.error, e: + (config['local_address'], config['local_port'])) + + dns_resolver = asyncdns.DNSResolver() + tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) + udp_server = udprelay.UDPRelay(config, dns_resolver, True) + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + tcp_server.add_to_loop(loop) + udp_server.add_to_loop(loop) + + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tcp_server.close(next_tick=True) + udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + + def int_handler(signum, _): + sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + loop.run() + except (KeyboardInterrupt, IOError, OSError) as e: logging.error(e) - except KeyboardInterrupt: - server.shutdown() - sys.exit(0) + if config['verbose']: + import traceback + traceback.print_exc() + os._exit(1) if __name__ == '__main__': main() diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index ce40d17..4523399 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -1,64 +1,136 @@ #!/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 __future__ import absolute_import, division, print_function, \ + with_statement + import collections import logging -import heapq import time +# this LRUCache is optimized for concurrency, not QPS +# n: concurrency, keys stored in the cache +# m: visits not timed out, proportional to QPS * timeout +# get & set is O(1), not O(n). thus we can support very large n +# TODO: if timeout or QPS is too large, then this cache is not very efficient, +# as sweep() causes long pause + + class LRUCache(collections.MutableMapping): """This class is not thread safe""" def __init__(self, timeout=60, close_callback=None, *args, **kwargs): self.timeout = timeout self.close_callback = close_callback - self.store = {} - self.time_to_keys = collections.defaultdict(list) - self.last_visits = [] + self._store = {} + self._time_to_keys = collections.defaultdict(list) + self._keys_to_last_time = {} + self._last_visits = collections.deque() self.update(dict(*args, **kwargs)) # use the free update to set keys def __getitem__(self, key): - "O(logm)" + # O(1) t = time.time() - self.time_to_keys[t].append(key) - heapq.heappush(self.last_visits, t) - return self.store[key] + self._keys_to_last_time[key] = t + self._time_to_keys[t].append(key) + self._last_visits.append(t) + return self._store[key] def __setitem__(self, key, value): - "O(logm)" + # O(1) t = time.time() - self.store[key] = value - self.time_to_keys[t].append(key) - heapq.heappush(self.last_visits, t) + self._keys_to_last_time[key] = t + self._store[key] = value + self._time_to_keys[t].append(key) + self._last_visits.append(t) def __delitem__(self, key): - "O(1)" - del self.store[key] + # O(1) + del self._store[key] + del self._keys_to_last_time[key] def __iter__(self): - return iter(self.store) + return iter(self._store) def __len__(self): - return len(self.store) + return len(self._store) def sweep(self): - "O(m)" + # O(m) now = time.time() c = 0 - while len(self.last_visits) > 0: - least = self.last_visits[0] + while len(self._last_visits) > 0: + least = self._last_visits[0] if now - least <= self.timeout: break - for key in self.time_to_keys[least]: - heapq.heappop(self.last_visits) - if self.store.__contains__(key): - value = self.store[key] - if self.close_callback is not None: - self.close_callback(value) - - del self.store[key] - c += 1 - del self.time_to_keys[least] + if self.close_callback is not None: + for key in self._time_to_keys[least]: + if key in self._store: + if now - self._keys_to_last_time[key] > self.timeout: + value = self._store[key] + self.close_callback(value) + for key in self._time_to_keys[least]: + self._last_visits.popleft() + if key in self._store: + if now - self._keys_to_last_time[key] > self.timeout: + del self._store[key] + del self._keys_to_last_time[key] + c += 1 + del self._time_to_keys[least] if c: logging.debug('%d keys swept' % c) + + +def test(): + c = LRUCache(timeout=0.3) + + c['a'] = 1 + assert c['a'] == 1 + + time.sleep(0.5) + c.sweep() + assert 'a' not in c + + c['a'] = 2 + c['b'] = 3 + time.sleep(0.2) + c.sweep() + assert c['a'] == 2 + assert c['b'] == 3 + + time.sleep(0.2) + c.sweep() + c['b'] + time.sleep(0.2) + c.sweep() + assert 'a' not in c + assert c['b'] == 3 + + time.sleep(0.5) + c.sweep() + assert 'a' not in c + assert 'b' not in c + +if __name__ == '__main__': + test() diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 29cd99e..0ddcda5 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -21,268 +21,84 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import with_statement +from __future__ import absolute_import, division, print_function, \ + with_statement + import sys -if sys.version_info < (2, 6): - import simplejson as json -else: - import json - - -# TODO remove gevent -try: - import gevent - import gevent.monkey - gevent.monkey.patch_all(dns=gevent.version_info[0] >= 1) -except ImportError: - gevent = None - print >>sys.stderr, 'warning: gevent not found, using threading instead' - - -import socket -import select -import threading -import SocketServer -import struct -import logging -import getopt -import encrypt import os -import utils -import udprelay +import logging +import signal - -def send_all(sock, data): - bytes_sent = 0 - while True: - r = sock.send(data[bytes_sent:]) - if r < 0: - return r - bytes_sent += r - if bytes_sent == len(data): - return bytes_sent - - -class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): - allow_reuse_address = True - - def server_activate(self): - if config_fast_open: - try: - self.socket.setsockopt(socket.SOL_TCP, 23, 5) - except socket.error: - logging.error('warning: fast open is not available') - self.socket.listen(self.request_queue_size) - - def get_request(self): - connection = self.socket.accept() - connection[0].settimeout(config_timeout) - return connection - - -class Socks5Server(SocketServer.StreamRequestHandler): - def handle_tcp(self, sock, remote): - try: - fdset = [sock, remote] - while True: - should_break = False - r, w, e = select.select(fdset, [], [], config_timeout) - if not r: - logging.warn('read time out') - break - if sock in r: - data = self.decrypt(sock.recv(4096)) - if len(data) <= 0: - should_break = True - else: - result = send_all(remote, data) - if result < len(data): - raise Exception('failed to send all data') - if remote in r: - data = self.encrypt(remote.recv(4096)) - if len(data) <= 0: - should_break = True - else: - result = send_all(sock, data) - if result < len(data): - raise Exception('failed to send all data') - if should_break: - # make sure all data are read before we close the sockets - # TODO: we haven't read ALL the data, actually - # http://cs.ecs.baylor.edu/~donahoo/practical/CSockets/TCPRST.pdf - break - - finally: - sock.close() - remote.close() - - def encrypt(self, data): - return self.encryptor.encrypt(data) - - def decrypt(self, data): - return self.encryptor.decrypt(data) - - def handle(self): - try: - self.encryptor = encrypt.Encryptor(self.server.key, - self.server.method) - sock = self.connection - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - iv_len = self.encryptor.iv_len() - data = sock.recv(iv_len) - if iv_len > 0 and not data: - sock.close() - return - if iv_len: - self.decrypt(data) - data = sock.recv(1) - if not data: - sock.close() - return - addrtype = ord(self.decrypt(data)) - if addrtype == 1: - addr = socket.inet_ntoa(self.decrypt(self.rfile.read(4))) - elif addrtype == 3: - addr = self.decrypt( - self.rfile.read(ord(self.decrypt(sock.recv(1))))) - elif addrtype == 4: - addr = socket.inet_ntop(socket.AF_INET6, - self.decrypt(self.rfile.read(16))) - else: - # not supported - logging.warn('addr_type not supported, maybe wrong password') - return - port = struct.unpack('>H', self.decrypt(self.rfile.read(2))) - try: - logging.info('connecting %s:%d' % (addr, port[0])) - remote = socket.create_connection((addr, port[0]), - timeout=config_timeout) - remote.settimeout(config_timeout) - remote.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - except socket.error, e: - # Connection refused - logging.warn(e) - return - self.handle_tcp(sock, remote) - except socket.error, e: - logging.warn(e) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ + asyncdns def main(): - global config_server, config_server_port, config_method, config_fast_open, \ - config_timeout + utils.check_python() - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)-8s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') + config = utils.get_config(False) + daemon.daemon_exec(config) - version = '' - try: - import pkg_resources - version = pkg_resources.get_distribution('shadowsocks').version - except: - pass - print 'shadowsocks %s' % version + utils.print_shadowsocks() - config_path = utils.find_config() - try: - optlist, args = getopt.getopt(sys.argv[1:], 's:p:k:m:c:t:', - ['fast-open', 'workers:']) - for key, value in optlist: - if key == '-c': - config_path = value - - if config_path: - logging.info('loading config from %s' % config_path) - with open(config_path, 'rb') as f: - try: - config = json.load(f) - except ValueError as e: - logging.error('found an error in config.json: %s', - e.message) - sys.exit(1) - else: - config = {} - - optlist, args = getopt.getopt(sys.argv[1:], 's:p:k:m:c:t:', - ['fast-open', 'workers=']) - for key, value in optlist: - if key == '-p': - config['server_port'] = int(value) - elif key == '-k': - config['password'] = value - elif key == '-s': - config['server'] = value - elif key == '-m': - config['method'] = value - elif key == '-t': - config['timeout'] = value - elif key == '--fast-open': - config['fast_open'] = True - elif key == '--workers': - config['workers'] = value - except getopt.GetoptError: - utils.print_server_help() - sys.exit(2) - - config_server = config['server'] - config_server_port = config['server_port'] - config_key = config['password'] - config_method = config.get('method', None) - config_port_password = config.get('port_password', None) - config_timeout = int(config.get('timeout', 300)) - config_fast_open = config.get('fast_open', False) - config_workers = config.get('workers', 1) - - if not config_key and not config_path: - sys.exit('config not specified, please read ' - 'https://github.com/clowwindy/shadowsocks') - - utils.check_config(config) - - if config_port_password: - if config_server_port or config_key: + if config['port_password']: + if config['password']: logging.warn('warning: port_password should not be used with ' 'server_port and password. server_port and password ' 'will be ignored') else: - config_port_password = {} - config_port_password[str(config_server_port)] = config_key + config['port_password'] = {} + server_port = config['server_port'] + if type(server_port) == list: + for a_server_port in server_port: + config['port_password'][a_server_port] = config['password'] + else: + config['port_password'][str(server_port)] = config['password'] - encrypt.init_table(config_key, config_method) - addrs = socket.getaddrinfo(config_server, int(8387)) - if not addrs: - logging.error('cant resolve listen address') - sys.exit(1) - ThreadingTCPServer.address_family = addrs[0][0] + encrypt.try_cipher(config['password'], config['method'], config['auth']) tcp_servers = [] udp_servers = [] - for port, key in config_port_password.items(): - tcp_server = ThreadingTCPServer((config_server, int(port)), - Socks5Server) - tcp_server.key = key - tcp_server.method = config_method - tcp_server.timeout = int(config_timeout) + dns_resolver = asyncdns.DNSResolver() + for port, password in config['port_password'].items(): + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password logging.info("starting server at %s:%d" % - tuple(tcp_server.server_address[:2])) - tcp_servers.append(tcp_server) - udp_server = udprelay.UDPRelay(config_server, int(port), None, None, - key, config_method, int(config_timeout), - False) - udp_servers.append(udp_server) + (a_config['server'], int(port))) + tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False)) + udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False)) def run_server(): - for tcp_server in tcp_servers: - threading.Thread(target=tcp_server.serve_forever).start() - for udp_server in udp_servers: - udp_server.start() + def child_handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + list(map(lambda s: s.close(next_tick=True), + tcp_servers + udp_servers)) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), + child_handler) - if int(config_workers) > 1: + def int_handler(signum, _): + sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + try: + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) + loop.run() + except (KeyboardInterrupt, IOError, OSError) as e: + logging.error(e) + if config['verbose']: + import traceback + traceback.print_exc() + os._exit(1) + + if int(config['workers']) > 1: if os.name == 'posix': children = [] is_child = False - for i in xrange(0, int(config_workers)): + for i in range(0, int(config['workers'])): r = os.fork() if r == 0: logging.info('worker started') @@ -292,19 +108,24 @@ def main(): else: children.append(r) if not is_child: - def handler(signum, frame): + def handler(signum, _): for pid in children: - os.kill(pid, signum) - os.waitpid(pid, 0) + try: + os.kill(pid, signum) + os.waitpid(pid, 0) + except OSError: # child may already exited + pass sys.exit() - import signal signal.signal(signal.SIGTERM, handler) + signal.signal(signal.SIGQUIT, handler) + signal.signal(signal.SIGINT, handler) # master - for tcp_server in tcp_servers: - tcp_server.server_close() - for udp_server in udp_servers: - udp_server.close() + for a_tcp_server in tcp_servers: + a_tcp_server.close() + for a_udp_server in udp_servers: + a_udp_server.close() + dns_resolver.close() for child in children: os.waitpid(child, 0) @@ -316,7 +137,4 @@ def main(): if __name__ == '__main__': - try: - main() - except socket.error, e: - logging.error(e) + main() diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py new file mode 100644 index 0000000..567f515 --- /dev/null +++ b/shadowsocks/tcprelay.py @@ -0,0 +1,703 @@ +#!/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 __future__ import absolute_import, division, print_function, \ + with_statement + +import time +import socket +import errno +import struct +import logging +import traceback +import random + +from shadowsocks import encrypt, eventloop, utils, common +from shadowsocks.common import parse_header + +# we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time +TIMEOUTS_CLEAN_SIZE = 512 + +# we check timeouts every TIMEOUT_PRECISION seconds +TIMEOUT_PRECISION = 4 + +MSG_FASTOPEN = 0x20000000 + +# SOCKS CMD defination +CMD_CONNECT = 1 +CMD_BIND = 2 +CMD_UDP_ASSOCIATE = 3 + +# TCP Relay can be either sslocal or ssserver +# for sslocal it is called is_local=True + +# for each opening port, we have a TCP Relay +# for each connection, we have a TCP Relay Handler to handle the connection + +# for each handler, we have 2 sockets: +# local: connected to the client +# remote: connected to remote server + +# for each handler, we have 2 streams: +# upstream: from client to server direction +# read local and write to remote +# downstream: from server to client direction +# read remote and write to local + +# for each handler, it could be at one of several stages: + +# sslocal: +# stage 0 SOCKS hello received from local, send hello to local +# stage 1 addr received from local, query DNS for remote +# stage 2 UDP assoc +# stage 3 DNS resolved, connect to remote +# stage 4 still connecting, more data from local received +# stage 5 remote connected, piping local and remote + +# ssserver: +# stage 0 just jump to stage 1 +# stage 1 addr received from local, query DNS for remote +# stage 3 DNS resolved, connect to remote +# stage 4 still connecting, more data from local received +# stage 5 remote connected, piping local and remote + +STAGE_INIT = 0 +STAGE_ADDR = 1 +STAGE_UDP_ASSOC = 2 +STAGE_DNS = 3 +STAGE_CONNECTING = 4 +STAGE_STREAM = 5 +STAGE_DESTROYED = -1 + +# stream direction +STREAM_UP = 0 +STREAM_DOWN = 1 + +# stream wait status, indicating it's waiting for reading, etc +WAIT_STATUS_INIT = 0 +WAIT_STATUS_READING = 1 +WAIT_STATUS_WRITING = 2 +WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING + +BUF_SIZE = 32 * 1024 + + +class TCPRelayHandler(object): + def __init__(self, server, fd_to_handlers, loop, local_sock, config, + dns_resolver, is_local): + self._server = server + self._fd_to_handlers = fd_to_handlers + self._loop = loop + self._local_sock = local_sock + self._remote_sock = None + self._config = config + self._dns_resolver = dns_resolver + self._is_local = is_local + self._stage = STAGE_INIT + self._encryptor = encrypt.Encryptor(config['password'], + config['method']) + self._fastopen_connected = False + self._data_to_write_to_local = [] + self._data_to_write_to_remote = [] + self._upstream_status = WAIT_STATUS_READING + self._downstream_status = WAIT_STATUS_INIT + self._remote_address = None + if is_local: + self._chosen_server = self._get_a_server() + fd_to_handlers[local_sock.fileno()] = self + local_sock.setblocking(False) + local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR) + self.last_activity = 0 + self._update_activity() + + def __hash__(self): + # default __hash__ is id / 16 + # we want to eliminate collisions + return id(self) + + @property + def remote_address(self): + return self._remote_address + + def _get_a_server(self): + server = self._config['server'] + server_port = self._config['server_port'] + if type(server_port) == list: + server_port = random.choice(server_port) + logging.debug('chosen server: %s:%d', server, server_port) + # TODO support multiple server IP + return server, server_port + + def _update_activity(self): + # tell the TCP Relay we have activities recently + # else it will think we are inactive and timed out + self._server.update_activity(self) + + def _update_stream(self, stream, status): + # update a stream to a new waiting status + + # check if status is changed + # only update if dirty + dirty = False + if stream == STREAM_DOWN: + if self._downstream_status != status: + self._downstream_status = status + dirty = True + elif stream == STREAM_UP: + if self._upstream_status != status: + self._upstream_status = status + dirty = True + if dirty: + if self._local_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + if self._upstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + self._loop.modify(self._local_sock, event) + if self._remote_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + if self._upstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + self._loop.modify(self._remote_sock, event) + + def _write_to_sock(self, data, sock): + # write data to sock + # if only some of the data are written, put remaining in the buffer + # and update the stream to wait for writing + if not data or not sock: + return False + uncomplete = False + try: + l = len(data) + s = sock.send(data) + if s < l: + data = data[s:] + uncomplete = True + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + uncomplete = True + else: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + return False + if uncomplete: + if sock == self._local_sock: + self._data_to_write_to_local.append(data) + self._update_stream(STREAM_DOWN, WAIT_STATUS_WRITING) + elif sock == self._remote_sock: + self._data_to_write_to_remote.append(data) + self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) + else: + logging.error('write_all_to_sock:unknown socket') + else: + if sock == self._local_sock: + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + elif sock == self._remote_sock: + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + else: + logging.error('write_all_to_sock:unknown socket') + return True + + def _handle_stage_connecting(self, data): + if self._is_local: + data = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data) + if self._is_local and not self._fastopen_connected and \ + self._config['fast_open']: + # for sslocal and fastopen, we basically wait for data and use + # sendto to connect + try: + # only connect once + self._fastopen_connected = True + remote_sock = \ + self._create_remote_socket(self._chosen_server[0], + self._chosen_server[1]) + self._loop.add(remote_sock, eventloop.POLL_ERR) + data = b''.join(self._data_to_write_to_remote) + l = len(data) + s = remote_sock.sendto(data, MSG_FASTOPEN, self._chosen_server) + if s < l: + data = data[s:] + self._data_to_write_to_remote = [data] + else: + self._data_to_write_to_remote = [] + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == errno.EINPROGRESS: + # in this case data is not sent at all + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + elif eventloop.errno_from_exception(e) == errno.ENOTCONN: + logging.error('fast open not supported on this OS') + self._config['fast_open'] = False + self.destroy() + else: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + + def _handle_stage_addr(self, data): + try: + if self._is_local: + cmd = common.ord(data[1]) + if cmd == CMD_UDP_ASSOCIATE: + logging.debug('UDP associate') + if self._local_sock.family == socket.AF_INET6: + header = b'\x05\x00\x00\x04' + else: + header = b'\x05\x00\x00\x01' + addr, port = self._local_sock.getsockname()[:2] + addr_to_send = socket.inet_pton(self._local_sock.family, + addr) + port_to_send = struct.pack('>H', port) + self._write_to_sock(header + addr_to_send + port_to_send, + self._local_sock) + self._stage = STAGE_UDP_ASSOC + # just wait for the client to disconnect + return + elif cmd == CMD_CONNECT: + # just trim VER CMD RSV + data = data[3:] + else: + logging.error('unknown command %d', cmd) + self.destroy() + return + header_result = parse_header(data) + if header_result is None: + raise Exception('can not parse header') + addrtype, remote_addr, remote_port, header_length = header_result + logging.info('connecting %s:%d' % (common.to_str(remote_addr), + remote_port)) + self._remote_address = (remote_addr, remote_port) + # pause reading + self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) + self._stage = STAGE_DNS + if self._is_local: + # forward address to remote + self._write_to_sock((b'\x05\x00\x00\x01' + b'\x00\x00\x00\x00\x10\x10'), + self._local_sock) + data_to_send = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data_to_send) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(self._chosen_server[0], + self._handle_dns_resolved) + else: + if len(data) > header_length: + self._data_to_write_to_remote.append(data[header_length:]) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(remote_addr, + self._handle_dns_resolved) + except Exception as e: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + # TODO use logging when debug completed + self.destroy() + + def _create_remote_socket(self, ip, port): + addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, + socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) + af, socktype, proto, canonname, sa = addrs[0] + remote_sock = socket.socket(af, socktype, proto) + self._remote_sock = remote_sock + self._fd_to_handlers[remote_sock.fileno()] = self + remote_sock.setblocking(False) + remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + return remote_sock + + def _handle_dns_resolved(self, result, error): + if error: + logging.error(error) + self.destroy() + return + if result: + ip = result[1] + if ip: + try: + self._stage = STAGE_CONNECTING + remote_addr = ip + if self._is_local: + remote_port = self._chosen_server[1] + else: + remote_port = self._remote_address[1] + + if self._is_local and self._config['fast_open']: + # for fastopen: + # wait for more data to arrive and send them in one SYN + self._stage = STAGE_CONNECTING + # we don't have to wait for remote since it's not + # created + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + # TODO when there is already data in this packet + else: + # else do connect + remote_sock = self._create_remote_socket(remote_addr, + remote_port) + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == \ + errno.EINPROGRESS: + pass + self._loop.add(remote_sock, + eventloop.POLL_ERR | eventloop.POLL_OUT) + self._stage = STAGE_CONNECTING + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + return + except (OSError, IOError) as e: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + + def _on_local_read(self): + # handle all local read events and dispatch them to methods for + # each stage + self._update_activity() + if not self._local_sock: + return + is_local = self._is_local + data = None + try: + data = self._local_sock.recv(BUF_SIZE) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in \ + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + return + if not data: + self.destroy() + return + if not is_local: + data = self._encryptor.decrypt(data) + if not data: + return + if self._stage == STAGE_STREAM: + if self._is_local: + data = self._encryptor.encrypt(data) + self._write_to_sock(data, self._remote_sock) + return + elif is_local and self._stage == STAGE_INIT: + # TODO check auth method + self._write_to_sock(b'\x05\00', self._local_sock) + self._stage = STAGE_ADDR + return + elif self._stage == STAGE_CONNECTING: + self._handle_stage_connecting(data) + elif (is_local and self._stage == STAGE_ADDR) or \ + (not is_local and self._stage == STAGE_INIT): + self._handle_stage_addr(data) + + def _on_remote_read(self): + # handle all remote read events + self._update_activity() + data = None + try: + data = self._remote_sock.recv(BUF_SIZE) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in \ + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + return + if not data: + self.destroy() + return + if self._is_local: + data = self._encryptor.decrypt(data) + else: + data = self._encryptor.encrypt(data) + try: + self._write_to_sock(data, self._local_sock) + except Exception as e: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + # TODO use logging when debug completed + self.destroy() + + def _on_local_write(self): + # handle local writable event + if self._data_to_write_to_local: + data = b''.join(self._data_to_write_to_local) + self._data_to_write_to_local = [] + self._write_to_sock(data, self._local_sock) + else: + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + + def _on_remote_write(self): + # handle remote writable event + self._stage = STAGE_STREAM + if self._data_to_write_to_remote: + data = b''.join(self._data_to_write_to_remote) + self._data_to_write_to_remote = [] + self._write_to_sock(data, self._remote_sock) + else: + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + + def _on_local_error(self): + logging.debug('got local error') + if self._local_sock: + logging.error(eventloop.get_sock_error(self._local_sock)) + self.destroy() + + def _on_remote_error(self): + logging.debug('got remote error') + if self._remote_sock: + logging.error(eventloop.get_sock_error(self._remote_sock)) + self.destroy() + + def handle_event(self, sock, event): + # handle all events in this handler and dispatch them to methods + if self._stage == STAGE_DESTROYED: + logging.debug('ignore handle_event: destroyed') + return + # order is important + if sock == self._remote_sock: + if event & eventloop.POLL_ERR: + self._on_remote_error() + if self._stage == STAGE_DESTROYED: + return + if event & (eventloop.POLL_IN | eventloop.POLL_HUP): + self._on_remote_read() + if self._stage == STAGE_DESTROYED: + return + if event & eventloop.POLL_OUT: + self._on_remote_write() + elif sock == self._local_sock: + if event & eventloop.POLL_ERR: + self._on_local_error() + if self._stage == STAGE_DESTROYED: + return + if event & (eventloop.POLL_IN | eventloop.POLL_HUP): + self._on_local_read() + if self._stage == STAGE_DESTROYED: + return + if event & eventloop.POLL_OUT: + self._on_local_write() + else: + logging.warn('unknown socket') + + def destroy(self): + # destroy the handler and release any resources + # promises: + # 1. destroy won't make another destroy() call inside + # 2. destroy releases resources so it prevents future call to destroy + # 3. destroy won't raise any exceptions + # if any of the promises are broken, it indicates a bug has been + # introduced! mostly likely memory leaks, etc + if self._stage == STAGE_DESTROYED: + # this couldn't happen + logging.debug('already destroyed') + return + self._stage = STAGE_DESTROYED + if self._remote_address: + logging.debug('destroy: %s:%d' % + self._remote_address) + else: + logging.debug('destroy') + if self._remote_sock: + logging.debug('destroying remote') + self._loop.remove(self._remote_sock) + del self._fd_to_handlers[self._remote_sock.fileno()] + self._remote_sock.close() + self._remote_sock = None + if self._local_sock: + logging.debug('destroying local') + self._loop.remove(self._local_sock) + del self._fd_to_handlers[self._local_sock.fileno()] + self._local_sock.close() + self._local_sock = None + self._dns_resolver.remove_callback(self._handle_dns_resolved) + self._server.remove_handler(self) + + +class TCPRelay(object): + def __init__(self, config, dns_resolver, is_local): + self._config = config + self._is_local = is_local + self._dns_resolver = dns_resolver + self._closed = False + self._eventloop = None + self._fd_to_handlers = {} + self._last_time = time.time() + + self._timeout = config['timeout'] + self._timeouts = [] # a list for all the handlers + # we trim the timeouts once a while + self._timeout_offset = 0 # last checked position for timeout + self._handler_to_timeouts = {} # key: handler value: index in timeouts + + if is_local: + listen_addr = config['local_address'] + listen_port = config['local_port'] + else: + listen_addr = config['server'] + listen_port = config['server_port'] + self._listen_port = listen_port + + addrs = socket.getaddrinfo(listen_addr, listen_port, 0, + socket.SOCK_STREAM, socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % + (listen_addr, listen_port)) + af, socktype, proto, canonname, sa = addrs[0] + server_socket = socket.socket(af, socktype, proto) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(sa) + server_socket.setblocking(False) + if config['fast_open']: + try: + server_socket.setsockopt(socket.SOL_TCP, 23, 5) + except socket.error: + logging.error('warning: fast open is not available') + self._config['fast_open'] = False + server_socket.listen(1024) + self._server_socket = server_socket + + def add_to_loop(self, loop): + if self._eventloop: + raise Exception('already add to loop') + if self._closed: + raise Exception('already closed') + self._eventloop = loop + loop.add_handler(self._handle_events) + + self._eventloop.add(self._server_socket, + eventloop.POLL_IN | eventloop.POLL_ERR) + + def remove_handler(self, handler): + index = self._handler_to_timeouts.get(hash(handler), -1) + if index >= 0: + # delete is O(n), so we just set it to None + self._timeouts[index] = None + del self._handler_to_timeouts[hash(handler)] + + def update_activity(self, handler): + # set handler to active + now = int(time.time()) + if now - handler.last_activity < TIMEOUT_PRECISION: + # thus we can lower timeout modification frequency + return + handler.last_activity = now + index = self._handler_to_timeouts.get(hash(handler), -1) + if index >= 0: + # delete is O(n), so we just set it to None + self._timeouts[index] = None + length = len(self._timeouts) + self._timeouts.append(handler) + self._handler_to_timeouts[hash(handler)] = length + + def _sweep_timeout(self): + # tornado's timeout memory management is more flexible than we need + # we just need a sorted last_activity queue and it's faster than heapq + # in fact we can do O(1) insertion/remove so we invent our own + if self._timeouts: + logging.log(utils.VERBOSE_LEVEL, 'sweeping timeouts') + now = time.time() + length = len(self._timeouts) + pos = self._timeout_offset + while pos < length: + handler = self._timeouts[pos] + if handler: + if now - handler.last_activity < self._timeout: + break + else: + if handler.remote_address: + logging.warn('timed out: %s:%d' % + handler.remote_address) + else: + logging.warn('timed out') + handler.destroy() + self._timeouts[pos] = None # free memory + pos += 1 + else: + pos += 1 + if pos > TIMEOUTS_CLEAN_SIZE and pos > length >> 1: + # clean up the timeout queue when it gets larger than half + # of the queue + self._timeouts = self._timeouts[pos:] + for key in self._handler_to_timeouts: + self._handler_to_timeouts[key] -= pos + pos = 0 + self._timeout_offset = pos + + def _handle_events(self, events): + # handle events and dispatch to handlers + for sock, fd, event in events: + if sock: + logging.log(utils.VERBOSE_LEVEL, 'fd %d %s', fd, + eventloop.EVENT_NAMES.get(event, event)) + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + # TODO + raise Exception('server_socket error') + try: + logging.debug('accept') + conn = self._server_socket.accept() + TCPRelayHandler(self, self._fd_to_handlers, + self._eventloop, conn[0], self._config, + self._dns_resolver, self._is_local) + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + continue + else: + logging.error(e) + if self._config['verbose']: + traceback.print_exc() + else: + if sock: + handler = self._fd_to_handlers.get(fd, None) + if handler: + handler.handle_event(sock, event) + else: + logging.warn('poll removed fd') + + now = time.time() + if now - self._last_time > TIMEOUT_PRECISION: + self._sweep_timeout() + self._last_time = now + if self._closed: + if self._server_socket: + self._eventloop.remove(self._server_socket) + self._server_socket.close() + self._server_socket = None + logging.info('closed listen port %d', self._listen_port) + if not self._fd_to_handlers: + self._eventloop.remove_handler(self._handle_events) + + def close(self, next_tick=False): + self._closed = True + if not next_tick: + self._server_socket.close() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 6012661..2b8b12f 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -65,79 +65,53 @@ # `client` means UDP clients that connects to other servers # `server` means the UDP server that handles user requests +from __future__ import absolute_import, division, print_function, \ + with_statement import time -import threading import socket import logging import struct -import encrypt -import eventloop -import lru_cache import errno +import random + +from shadowsocks import encrypt, eventloop, lru_cache, common +from shadowsocks.common import parse_header, pack_addr BUF_SIZE = 65536 -def parse_header(data): - addrtype = ord(data[0]) - dest_addr = None - dest_port = None - header_length = 0 - if addrtype == 1: - if len(data) >= 7: - dest_addr = socket.inet_ntoa(data[1:5]) - dest_port = struct.unpack('>H', data[5:7])[0] - header_length = 7 - else: - logging.warn('[udp] header is too short') - elif addrtype == 3: - if len(data) > 2: - addrlen = ord(data[1]) - if len(data) >= 2 + addrlen: - dest_addr = data[2:2 + addrlen] - dest_port = struct.unpack('>H', data[2 + addrlen:4 + - addrlen])[0] - header_length = 4 + addrlen - else: - logging.warn('[udp] header is too short') - else: - logging.warn('[udp] header is too short') - elif addrtype == 4: - if len(data) >= 19: - dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) - dest_port = struct.unpack('>H', data[17:19])[0] - header_length = 19 - else: - logging.warn('[udp] header is too short') - else: - logging.warn('unsupported addrtype %d' % addrtype) - if dest_addr is None: - return None - return (addrtype, dest_addr, dest_port, header_length) - - def client_key(a, b, c, d): return '%s:%s:%s:%s' % (a, b, c, d) 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 + def __init__(self, config, dns_resolver, is_local): + self._config = config + if is_local: + self._listen_addr = config['local_address'] + self._listen_port = config['local_port'] + self._remote_addr = config['server'] + self._remote_port = config['server_port'] + else: + self._listen_addr = config['server'] + self._listen_port = config['server_port'] + self._remote_addr = None + self._remote_port = None + self._dns_resolver = dns_resolver + self._password = config['password'] + self._method = config['method'] + self._timeout = config['timeout'] self._is_local = is_local - self._cache = lru_cache.LRUCache(timeout=timeout, + self._cache = lru_cache.LRUCache(timeout=config['timeout'], close_callback=self._close_client) - self._client_fd_to_server_addr = lru_cache.LRUCache(timeout=timeout) + self._client_fd_to_server_addr = \ + lru_cache.LRUCache(timeout=config['timeout']) + self._eventloop = None self._closed = False + self._last_time = time.time() + self._sockets = set() addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) @@ -150,8 +124,18 @@ class UDPRelay(object): server_socket.setblocking(False) self._server_socket = server_socket + def _get_a_server(self): + server = self._config['server'] + server_port = self._config['server_port'] + if type(server_port) == list: + server_port = random.choice(server_port) + logging.debug('chosen server: %s:%d', server, server_port) + # TODO support multiple server IP + return server, server_port + def _close_client(self, client): if hasattr(client, 'close'): + self._sockets.remove(client.fileno()) self._eventloop.remove(client) client.close() else: @@ -161,17 +145,20 @@ class UDPRelay(object): def _handle_server(self): server = self._server_socket data, r_addr = server.recvfrom(BUF_SIZE) + if not data: + logging.debug('UDP handle_server: data is empty') if self._is_local: - frag = ord(data[2]) + frag = common.ord(data[2]) if frag != 0: logging.warn('drop a message since frag is not 0') return else: data = data[3:] else: - # decrypt data data = encrypt.encrypt_all(self._password, self._method, 0, data) + # decrypt data if not data: + logging.debug('UDP handle_server: data is empty after decrypt') return header_result = parse_header(data) if header_result is None: @@ -179,7 +166,7 @@ class UDPRelay(object): addrtype, dest_addr, dest_port, header_length = header_result if self._is_local: - server_addr, server_port = self._remote_addr, self._remote_port + server_addr, server_port = self._get_a_server() else: server_addr, server_port = dest_addr, dest_port @@ -198,15 +185,17 @@ class UDPRelay(object): else: # drop return + self._sockets.add(client.fileno()) self._eventloop.add(client, eventloop.POLL_IN) - data = data[header_length:] - if not data: - return if self._is_local: data = encrypt.encrypt_all(self._password, self._method, 1, data) if not data: return + else: + data = data[header_length:] + if not data: + return try: client.sendto(data, (server_addr, server_port)) except IOError as e: @@ -218,13 +207,15 @@ class UDPRelay(object): def _handle_client(self, sock): data, r_addr = sock.recvfrom(BUF_SIZE) + if not data: + logging.debug('UDP handle_client: data is empty') + return if not self._is_local: addrlen = len(r_addr[0]) if addrlen > 255: # drop return - data = '\x03' + chr(addrlen) + r_addr[0] + \ - struct.pack('>H', r_addr[1]) + data + data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data response = encrypt.encrypt_all(self._password, self._method, 1, data) if not response: @@ -238,8 +229,8 @@ class UDPRelay(object): if header_result is None: return # addrtype, dest_addr, dest_port, header_length = header_result - response = '\x00\x00\x00' + data - client_addr = self._client_fd_to_server_addr.get(sock.fileno(), None) + response = b'\x00\x00\x00' + data + client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: self._server_socket.sendto(response, client_addr) else: @@ -247,45 +238,40 @@ class UDPRelay(object): # simply drop that packet pass - def _run(self): - server_socket = self._server_socket - self._eventloop = eventloop.EventLoop() - self._eventloop.add(server_socket, eventloop.POLL_IN) - last_time = time.time() - while not self._closed: - try: - events = self._eventloop.poll(10) - except (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == errno.EPIPE: - # Happens when the client closes the connection - continue - else: - logging.error(e) - continue - for sock, event in events: - if sock == self._server_socket: - self._handle_server() - else: - self._handle_client(sock) - now = time.time() - if now - last_time > 3.5: - self._cache.sweep() - if now - last_time > 7: - self._client_fd_to_server_addr.sweep() - last_time = now - - def start(self): + def add_to_loop(self, loop): + if self._eventloop: + raise Exception('already add to loop') if self._closed: - raise Exception('closed') - t = threading.Thread(target=self._run) - t.setName('UDPThread') - t.setDaemon(False) - t.start() - self._thread = t + raise Exception('already closed') + self._eventloop = loop + loop.add_handler(self._handle_events) - def close(self): + server_socket = self._server_socket + self._eventloop.add(server_socket, + eventloop.POLL_IN | eventloop.POLL_ERR) + + def _handle_events(self, events): + for sock, fd, event in events: + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + logging.error('UDP server_socket err') + self._handle_server() + elif sock and (fd in self._sockets): + if event & eventloop.POLL_ERR: + logging.error('UDP client_socket err') + self._handle_client(sock) + now = time.time() + if now - self._last_time > 3: + self._cache.sweep() + self._client_fd_to_server_addr.sweep() + self._last_time = now + if self._closed: + self._server_socket.close() + for sock in self._sockets: + sock.close() + self._eventloop.remove_handler(self._handle_events) + + def close(self, next_tick=False): self._closed = True - self._server_socket.close() - - def thread(self): - return self._thread + if not next_tick: + self._server_socket.close() diff --git a/shadowsocks/utils.py b/shadowsocks/utils.py index f37eed9..07daf44 100644 --- a/shadowsocks/utils.py +++ b/shadowsocks/utils.py @@ -21,53 +21,42 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import absolute_import, division, print_function, \ + with_statement + import os -import socket +import json +import sys +import getopt import logging +from shadowsocks.common import to_bytes, to_str -def inet_ntop(family, ipstr): - if family == socket.AF_INET: - return socket.inet_ntoa(ipstr) - elif family == socket.AF_INET6: - v6addr = ':'.join(('%02X%02X' % (ord(i), ord(j))) - for i, j in zip(ipstr[::2], ipstr[1::2])) - return v6addr +VERBOSE_LEVEL = 5 -def inet_pton(family, addr): - if family == socket.AF_INET: - return socket.inet_aton(addr) - elif family == socket.AF_INET6: - if '.' in addr: # a v4 addr - v4addr = addr[addr.rindex(':') + 1:] - v4addr = socket.inet_aton(v4addr) - v4addr = map(lambda x: ('%02X' % ord(x)), v4addr) - v4addr.insert(2, ':') - newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr) - return inet_pton(family, newaddr) - dbyts = [0] * 8 # 8 groups - grps = addr.split(':') - for i, v in enumerate(grps): - if v: - dbyts[i] = int(v, 16) - else: - for j, w in enumerate(grps[::-1]): - if w: - dbyts[7 - j] = int(w, 16) - else: - break - break - return ''.join((chr(i // 256) + chr(i % 256)) for i in dbyts) - else: - raise RuntimeError("What family?") +def check_python(): + info = sys.version_info + if info[0] == 2 and not info[1] >= 6: + print('Python 2.6+ required') + sys.exit(1) + elif info[0] == 3 and not info[1] >= 3: + print('Python 3.3+ required') + sys.exit(1) + elif info[0] not in [2, 3]: + print('Python version not supported') + sys.exit(1) -if not hasattr(socket, 'inet_pton'): - socket.inet_pton = inet_pton +def print_shadowsocks(): + version = '' + try: + import pkg_resources + version = pkg_resources.get_distribution('shadowsocks').version + except Exception: + pass + print('shadowsocks %s' % version) -if not hasattr(socket, 'inet_ntop'): - socket.inet_ntop = inet_ntop def find_config(): config_path = 'config.json' @@ -80,53 +69,257 @@ def find_config(): def check_config(config): - if config.get('local_address', '') in ['0.0.0.0']: - logging.warn('warning: local set to listen 0.0.0.0, which is not safe') - if config.get('server', '') in ['127.0.0.1', 'localhost']: - logging.warn('warning: server set to listen %s:%s, are you sure?' % - (config['server'], config['server_port'])) - if (config.get('method', '') or '').lower() == 'rc4': + if config.get('local_address', '') in [b'0.0.0.0']: + logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') + if config.get('server', '') in [b'127.0.0.1', b'localhost']: + logging.warn('warning: server set to listen on %s:%s, are you sure?' % + (to_str(config['server']), config['server_port'])) + if (config.get('method', '') or '').lower() == b'table': + logging.warn('warning: table is not safe; please use a safer cipher, ' + 'like AES-256-CFB') + if (config.get('method', '') or '').lower() == b'rc4': logging.warn('warning: RC4 is not safe; please use a safer cipher, ' 'like AES-256-CFB') - if (int(config.get('timeout', 300)) or 300) < 100: + if config.get('timeout', 300) < 100: logging.warn('warning: your timeout %d seems too short' % int(config.get('timeout'))) - if (int(config.get('timeout', 300)) or 300) > 600: + if config.get('timeout', 300) > 600: logging.warn('warning: your timeout %d seems too long' % int(config.get('timeout'))) + if config.get('password') in [b'mypassword']: + logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' + 'config.json!') + exit(1) + + +def get_config(is_local): + logging.basicConfig(level=logging.INFO, + format='%(levelname)-s: %(message)s') + if is_local: + shortopts = 'hd:s:b:p:k:l:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file='] + else: + shortopts = 'hd:s:p:k:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers='] + try: + config_path = find_config() + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + for key, value in optlist: + if key == '-c': + config_path = value + + if config_path: + logging.info('loading config from %s' % config_path) + with open(config_path, 'rb') as f: + try: + config = json.loads(f.read().decode('utf8'), + object_hook=_decode_dict) + except ValueError as e: + logging.error('found an error in config.json: %s', + e.message) + sys.exit(1) + else: + config = {} + + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + v_count = 0 + for key, value in optlist: + if key == '-p': + config['server_port'] = int(value) + elif key == '-k': + config['password'] = to_bytes(value) + elif key == '-l': + config['local_port'] = int(value) + elif key == '-s': + config['server'] = to_bytes(value) + elif key == '-m': + config['method'] = to_bytes(value) + elif key == '-b': + config['local_address'] = to_bytes(value) + elif key == '-v': + v_count += 1 + # '-vv' turns on more verbose mode + config['verbose'] = v_count + elif key == '-t': + config['timeout'] = int(value) + elif key == '--fast-open': + config['fast_open'] = True + elif key == '--workers': + config['workers'] = int(value) + elif key in ('-h', '--help'): + if is_local: + print_local_help() + else: + print_server_help() + sys.exit(0) + elif key == '-d': + config['daemon'] = value + elif key == '--pid-file': + config['pid-file'] = value + elif key == '--log-file': + config['log-file'] = value + elif key == '-q': + v_count -= 1 + config['verbose'] = v_count + except getopt.GetoptError as e: + print(e, file=sys.stderr) + print_help(is_local) + sys.exit(2) + + if not config: + logging.error('config not specified') + print_help(is_local) + sys.exit(2) + + config['password'] = config.get('password', '') + config['method'] = config.get('method', 'aes-256-cfb') + config['auth'] = config.get('auth', None) + config['port_password'] = config.get('port_password', None) + config['timeout'] = int(config.get('timeout', 300)) + config['fast_open'] = config.get('fast_open', False) + config['workers'] = config.get('workers', 1) + config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid') + config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log') + config['workers'] = config.get('workers', 1) + config['verbose'] = config.get('verbose', False) + config['local_address'] = config.get('local_address', '127.0.0.1') + config['local_port'] = config.get('local_port', 1080) + if is_local: + if config.get('server', None) is None: + logging.error('server addr not specified') + print_local_help() + sys.exit(2) + else: + config['server'] = config.get('server', '0.0.0.0') + config['server_port'] = config.get('server_port', 8388) + + if is_local and not config.get('password', None): + logging.error('password not specified') + print_help(is_local) + sys.exit(2) + + if not is_local and not config.get('password', None) \ + and not config.get('port_password', None): + logging.error('password or port_password not specified') + print_help(is_local) + sys.exit(2) + + if 'local_port' in config: + config['local_port'] = int(config['local_port']) + + if 'server_port' in config and type(config['server_port']) != list: + config['server_port'] = int(config['server_port']) + + logging.getLogger('').handlers = [] + logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') + if config['verbose'] >= 2: + level = VERBOSE_LEVEL + elif config['verbose'] == 1: + level = logging.DEBUG + elif config['verbose'] == -1: + level = logging.WARN + elif config['verbose'] <= -2: + level = logging.ERROR + else: + level = logging.INFO + logging.basicConfig(level=level, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + + check_config(config) + + return config + + +def print_help(is_local): + if is_local: + print_local_help() + else: + print_server_help() def print_local_help(): - print '''usage: sslocal [-h] -s SERVER_ADDR -p SERVER_PORT [-b LOCAL_ADDR] - -l LOCAL_PORT -k PASSWORD -m METHOD [-t TIMEOUT] [-c CONFIG] - [--fast-open] + print('''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT] + [-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD] + [-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] -[d] [-q] +A fast tunnel proxy that helps you bypass firewalls. -optional arguments: - -h, --help show this help message and exit - -s SERVER_ADDR server address - -p SERVER_PORT server port - -b LOCAL_ADDR local binding address, default is 127.0.0.1 - -l LOCAL_PORT local port - -k PASSWORD password - -m METHOD encryption method, for example, aes-256-cfb - -t TIMEOUT timeout in seconds - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ -''' +You can supply configurations via either config file or command line arguments. + +Proxy options: + -h, --help show this help message and exit + -c CONFIG path to config file + -s SERVER_ADDR server address + -p SERVER_PORT server port, default: 8388 + -b LOCAL_ADDR local binding address, default: 127.0.0.1 + -l LOCAL_PORT local port, default: 1080 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + +General options: + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + +Online help: +''') def print_server_help(): - print '''usage: ssserver [-h] -s SERVER_ADDR -p SERVER_PORT -k PASSWORD + print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD -m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open] + [--workers WORKERS] [-v] [-d start] [-q] +A fast tunnel proxy that helps you bypass firewalls. -optional arguments: - -h, --help show this help message and exit - -s SERVER_ADDR server address - -p SERVER_PORT server port - -k PASSWORD password - -m METHOD encryption method, for example, aes-256-cfb - -t TIMEOUT timeout in seconds - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ - --workers WORKERS number of workers, available on Unix/Linux -''' \ No newline at end of file +You can supply configurations via either config file or command line arguments. + +Proxy options: + -h, --help show this help message and exit + -c CONFIG path to config file + -s SERVER_ADDR server address, default: 0.0.0.0 + -p SERVER_PORT server port, default: 8388 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + --workers WORKERS number of workers, available on Unix/Linux + +General options: + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + +Online help: +''') + + +def _decode_list(data): + rv = [] + for item in data: + if hasattr(item, 'encode'): + item = item.encode('utf-8') + elif isinstance(item, list): + item = _decode_list(item) + elif isinstance(item, dict): + item = _decode_dict(item) + rv.append(item) + return rv + + +def _decode_dict(data): + rv = {} + for key, value in data.items(): + if hasattr(value, 'encode'): + value = value.encode('utf-8') + elif isinstance(value, list): + value = _decode_list(value) + elif isinstance(value, dict): + value = _decode_dict(value) + rv[key] = value + return rv diff --git a/test.py b/test.py deleted file mode 100755 index 0b46b33..0000000 --- a/test.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import os -import signal -import sys -import select -import struct -import hashlib -import string -import time -from subprocess import Popen, PIPE - -target1 = [ - [60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181, 255, 143, 83, 247, 162, 16, 31, 209, 190, - 171, 115, 65, 38, 41, 21, 245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216, 173, 29, 241, 119, - 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204, 56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11 - , 254, 213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178, 10, 198, 71, 77, 36, 113, 167, 48, 2, - 117, 140, 142, 66, 199, 232, 243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253, 130, 8, 184, 14, - 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96, 163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, - 127, 0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79, 74, 210, 30, 73, 201, 151, 208, 114, 101, - 174, 92, 52, 120, 240, 15, 169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9, 191, 45, 6, 25, 4 - , 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97, 202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, - 118, 135, 148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164, 237, 111, 125, 207, 160, 187, 246, 234 - , 161, 188, 193, 249, 252], - [151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124, 180, 21, 191, 138, 83, 217, 30, 86, 7, 70, - 200, 56, 62, 218, 47, 168, 22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33, 230, 98, 43, 148, - 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35, 215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130 - , 94, 164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176, 220, 5, 153, 135, 213, 154, 189, 238 - , 174, 226, 53, 222, 146, 162, 236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34, 234, 108, - 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228, 155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, - 66, 116, 171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14, 36, 97, 163, 181, 72, 25, 144, 46, - 175, 89, 145, 113, 90, 159, 190, 15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69, 117, 57, 92, - 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167, 160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, - 185, 241, 79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179, 48, 9, 106, 216, 31, 249, 19, 85, - 254, 156, 115, 255, 120, 75, 16]] - -target2 = [ - [124, 30, 170, 247, 27, 127, 224, 59, 13, 22, 196, 76, 72, 154, 32, 209, 4, 2, 131, 62, 101, 51, 230, 9, 166, 11, 99 - , 80, 208, 112, 36, 248, 81, 102, 130, 88, 218, 38, 168, 15, 241, 228, 167, 117, 158, 41, 10, 180, 194, 50, 204, - 243, 246, 251, 29, 198, 219, 210, 195, 21, 54, 91, 203, 221, 70, 57, 183, 17, 147, 49, 133, 65, 77, 55, 202, 122, - 162, 169, 188, 200, 190, 125, 63, 244, 96, 31, 107, 106, 74, 143, 116, 148, 78, 46, 1, 137, 150, 110, 181, 56, 95, - 139, 58, 3, 231, 66, 165, 142, 242, 43, 192, 157, 89, 175, 109, 220, 128, 0, 178, 42, 255, 20, 214, 185, 83, 160, - 253, 7, 23, 92, 111, 153, 26, 226, 33, 176, 144, 18, 216, 212, 28, 151, 71, 206, 222, 182, 8, 174, 205, 201, 152, - 240, 155, 108, 223, 104, 239, 98, 164, 211, 184, 34, 193, 14, 114, 187, 40, 254, 12, 67, 93, 217, 6, 94, 16, 19, 82 - , 86, 245, 24, 197, 134, 132, 138, 229, 121, 5, 235, 238, 85, 47, 103, 113, 179, 69, 250, 45, 135, 156, 25, 61, - 75, 44, 146, 189, 84, 207, 172, 119, 53, 123, 186, 120, 171, 68, 227, 145, 136, 100, 90, 48, 79, 159, 149, 39, 213, - 236, 126, 52, 60, 225, 199, 105, 73, 233, 252, 118, 215, 35, 115, 64, 37, 97, 129, 161, 177, 87, 237, 141, 173, 191 - , 163, 140, 234, 232, 249], - [117, 94, 17, 103, 16, 186, 172, 127, 146, 23, 46, 25, 168, 8, 163, 39, 174, 67, 137, 175, 121, 59, 9, 128, 179, 199 - , 132, 4, 140, 54, 1, 85, 14, 134, 161, 238, 30, 241, 37, 224, 166, 45, 119, 109, 202, 196, 93, 190, 220, 69, 49 - , 21, 228, 209, 60, 73, 99, 65, 102, 7, 229, 200, 19, 82, 240, 71, 105, 169, 214, 194, 64, 142, 12, 233, 88, 201 - , 11, 72, 92, 221, 27, 32, 176, 124, 205, 189, 177, 246, 35, 112, 219, 61, 129, 170, 173, 100, 84, 242, 157, 26, - 218, 20, 33, 191, 155, 232, 87, 86, 153, 114, 97, 130, 29, 192, 164, 239, 90, 43, 236, 208, 212, 185, 75, 210, 0, - 81, 227, 5, 116, 243, 34, 18, 182, 70, 181, 197, 217, 95, 183, 101, 252, 248, 107, 89, 136, 216, 203, 68, 91, 223, - 96, 141, 150, 131, 13, 152, 198, 111, 44, 222, 125, 244, 76, 251, 158, 106, 24, 42, 38, 77, 2, 213, 207, 249, 147, - 113, 135, 245, 118, 193, 47, 98, 145, 66, 160, 123, 211, 165, 78, 204, 80, 250, 110, 162, 48, 58, 10, 180, 55, 231, - 79, 149, 74, 62, 50, 148, 143, 206, 28, 15, 57, 159, 139, 225, 122, 237, 138, 171, 36, 56, 115, 63, 144, 154, 6, - 230, 133, 215, 41, 184, 22, 104, 254, 234, 253, 187, 226, 247, 188, 156, 151, 40, 108, 51, 83, 178, 52, 3, 31, 255, - 195, 53, 235, 126, 167, 120]] - - -def get_table(key): - m = hashlib.md5() - m.update(key) - s = m.digest() - (a, b) = struct.unpack('= 0: - local_ready = True - if line.find('starting server') >= 0: - server_ready = True - - if local_ready and server_ready and p3 is None: - time.sleep(1) - p3 = Popen(['curl', 'http://www.example.com/', '-v', '-L', - '--socks5-hostname', '127.0.0.1:1080'], shell=False, - bufsize=0, close_fds=True) - break - - if p3 is not None: - r = p3.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/test_latency.py b/test_latency.py deleted file mode 100644 index 9066e9a..0000000 --- a/test_latency.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/python - -import sys -import time - - -before = time.time() - -for line in sys.stdin: - if 'HTTP/1.1 ' in line: - diff = time.time() - before - print 'headline %dms' % (diff * 1000) - diff --git a/tests/aes-cfb1.json b/tests/aes-cfb1.json new file mode 100644 index 0000000..40d0b21 --- /dev/null +++ b/tests/aes-cfb1.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb1", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-cfb8.json b/tests/aes-cfb8.json new file mode 100644 index 0000000..fb7014b --- /dev/null +++ b/tests/aes-cfb8.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb8", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-ctr.json b/tests/aes-ctr.json new file mode 100644 index 0000000..1fed8a8 --- /dev/null +++ b/tests/aes-ctr.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/test/aes.json b/tests/aes.json similarity index 62% rename from test/aes.json rename to tests/aes.json index 42ba376..a3d95b9 100644 --- a/test/aes.json +++ b/tests/aes.json @@ -1,9 +1,9 @@ { "server":"127.0.0.1", "server_port":8388, - "local_port":1080, - "password":"barfoo!", - "timeout":300, + "local_port":1081, + "password":"aes_password", + "timeout":60, "method":"aes-256-cfb", "local_address":"127.0.0.1", "fast_open":false diff --git a/tests/assert.sh b/tests/assert.sh new file mode 100644 index 0000000..b0c679c --- /dev/null +++ b/tests/assert.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# assert.sh 1.0 - bash unit testing framework +# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann +# +# http://github.com/lehmannro/assert.sh +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +export DISCOVERONLY=${DISCOVERONLY:-} +export DEBUG=${DEBUG:-} +export STOP=${STOP:-} +export INVARIANT=${INVARIANT:-} +export CONTINUE=${CONTINUE:-} + +args="$(getopt -n "$0" -l \ + verbose,help,stop,discover,invariant,continue vhxdic $*)" \ +|| exit -1 +for arg in $args; do + case "$arg" in + -h) + echo "$0 [-vxidc]" \ + "[--verbose] [--stop] [--invariant] [--discover] [--continue]" + echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" + exit 0;; + --help) + cat < [stdin] + (( tests_ran++ )) || : + [[ -n "$DISCOVERONLY" ]] && return || true + # printf required for formatting + printf -v expected "x${2:-}" # x required to overwrite older results + result="$(eval 2>/dev/null $1 <<< ${3:-})" || true + # Note: $expected is already decorated + if [[ "x$result" == "$expected" ]]; then + [[ -n "$DEBUG" ]] && echo -n . || true + return + fi + result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" + [[ -z "$result" ]] && result="nothing" || result="\"$result\"" + [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" + _assert_fail "expected $expected${_indent}got $result" "$1" "$3" +} + +assert_raises() { + # assert_raises [stdin] + (( tests_ran++ )) || : + [[ -n "$DISCOVERONLY" ]] && return || true + status=0 + (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? + expected=${2:-0} + if [[ "$status" -eq "$expected" ]]; then + [[ -n "$DEBUG" ]] && echo -n . || true + return + fi + _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" +} + +_assert_fail() { + # _assert_fail + [[ -n "$DEBUG" ]] && echo -n X + report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" + if [[ -n "$STOP" ]]; then + [[ -n "$DEBUG" ]] && echo + echo "$report" + exit 1 + fi + tests_errors[$tests_failed]="$report" + (( tests_failed++ )) || : +} + +_assert_reset +: ${tests_suite_status:=0} # remember if any of the tests failed so far +_assert_cleanup() { + local status=$? + # modify exit code if it's not already non-zero + [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status +} +trap _assert_cleanup EXIT diff --git a/tests/chacha20.json b/tests/chacha20.json new file mode 100644 index 0000000..541a9be --- /dev/null +++ b/tests/chacha20.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"chacha20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/coverage_server.py b/tests/coverage_server.py new file mode 100644 index 0000000..2df55e3 --- /dev/null +++ b/tests/coverage_server.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + import tornado.ioloop + import tornado.web + import urllib + + class MainHandler(tornado.web.RequestHandler): + def get(self, project): + try: + with open('/tmp/%s-coverage' % project, 'rb') as f: + coverage = f.read().strip() + n = int(coverage.strip('%')) + if n > 80: + color = 'brightgreen' + else: + color = 'yellow' + self.redirect(('https://img.shields.io/badge/' + 'coverage-%s-%s.svg' + '?style=flat') % + (urllib.quote(coverage), color)) + except IOError: + raise tornado.web.HTTPError(404) + + application = tornado.web.Application([ + (r"/([a-zA-Z0-9\-_]+)", MainHandler), + ]) + + if __name__ == "__main__": + application.listen(8888, address='127.0.0.1') + tornado.ioloop.IOLoop.instance().start() diff --git a/tests/fastopen.json b/tests/fastopen.json new file mode 100644 index 0000000..f3980b6 --- /dev/null +++ b/tests/fastopen.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"fastopen_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":true +} diff --git a/config.json b/tests/ipv6-client-side.json similarity index 52% rename from config.json rename to tests/ipv6-client-side.json index 026ed38..6c3cfaf 100644 --- a/config.json +++ b/tests/ipv6-client-side.json @@ -1,9 +1,9 @@ { - "server":"0.0.0.0", + "server":"::1", "server_port":8388, - "local_port":1080, - "password":"barfoo!", - "timeout":300, + "local_port":1081, + "password":"aes_password", + "timeout":60, "method":"aes-256-cfb", "local_address":"127.0.0.1", "fast_open":false diff --git a/tests/ipv6.json b/tests/ipv6.json new file mode 100644 index 0000000..d855f9c --- /dev/null +++ b/tests/ipv6.json @@ -0,0 +1,10 @@ +{ + "server":"::", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/libsodium/install.sh b/tests/libsodium/install.sh new file mode 100755 index 0000000..b0e35fa --- /dev/null +++ b/tests/libsodium/install.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ ! -d libsodium-1.0.1 ]; then + wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 + tar xf libsodium-1.0.1.tar.gz || exit 1 +fi +pushd libsodium-1.0.1 +./configure && make -j2 && make install || exit 1 +sudo ldconfig +popd diff --git a/tests/nose_plugin.py b/tests/nose_plugin.py new file mode 100644 index 0000000..ad32cf0 --- /dev/null +++ b/tests/nose_plugin.py @@ -0,0 +1,27 @@ +import nose +from nose.plugins.base import Plugin + + +class ExtensionPlugin(Plugin): + + name = "ExtensionPlugin" + + def options(self, parser, env): + Plugin.options(self, parser, env) + + def configure(self, options, config): + Plugin.configure(self, options, config) + self.enabled = True + + def wantFile(self, file): + return file.endswith('.py') + + def wantDirectory(self, directory): + return True + + def wantModule(self, file): + return True + + +if __name__ == '__main__': + nose.main(addplugins=[ExtensionPlugin()]) diff --git a/tests/rc4-md5.json b/tests/rc4-md5.json new file mode 100644 index 0000000..26ba0df --- /dev/null +++ b/tests/rc4-md5.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"rc4-md5", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/test/salsa20.json b/tests/salsa20-ctr.json similarity index 61% rename from test/salsa20.json rename to tests/salsa20-ctr.json index 8035f06..5ca6c45 100644 --- a/test/salsa20.json +++ b/tests/salsa20-ctr.json @@ -1,9 +1,9 @@ { "server":"127.0.0.1", "server_port":8388, - "local_port":1080, - "password":"barfoo!", - "timeout":300, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, "method":"salsa20-ctr", "local_address":"127.0.0.1", "fast_open":false diff --git a/tests/salsa20.json b/tests/salsa20.json new file mode 100644 index 0000000..7e30380 --- /dev/null +++ b/tests/salsa20.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"salsa20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/server-multi-passwd-client-side.json b/tests/server-multi-passwd-client-side.json new file mode 100644 index 0000000..c822c98 --- /dev/null +++ b/tests/server-multi-passwd-client-side.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "server_port": "8385", + "local_port": 1081, + "password": "foobar5", + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/test/server-multi-passwd.json b/tests/server-multi-passwd-table.json similarity index 68% rename from test/server-multi-passwd.json rename to tests/server-multi-passwd-table.json index 2c0609e..a2c0a80 100644 --- a/test/server-multi-passwd.json +++ b/tests/server-multi-passwd-table.json @@ -1,7 +1,7 @@ { "server": "127.0.0.1", "server_port": 8384, - "local_port": 1080, + "local_port": 1081, "password": "foobar4", "port_password": { "8381": "foobar1", @@ -12,11 +12,8 @@ "8386": "foobar6", "8387": "foobar7", "8388": "foobar8", - "8389": "foobar9", - "8390": "foobar10", - "8391": "foobar11", - "8392": "foobar12" + "8389": "foobar9" }, "timeout": 60, - "method": "aes-256-cfb" + "method": "table" } diff --git a/tests/server-multi-passwd.json b/tests/server-multi-passwd.json new file mode 100644 index 0000000..b1407f0 --- /dev/null +++ b/tests/server-multi-passwd.json @@ -0,0 +1,17 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + "8381": "foobar1", + "8382": "foobar2", + "8383": "foobar3", + "8384": "foobar4", + "8385": "foobar5", + "8386": "foobar6", + "8387": "foobar7", + "8388": "foobar8", + "8389": "foobar9" + }, + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/server-multi-ports.json b/tests/server-multi-ports.json new file mode 100644 index 0000000..5bdbcab --- /dev/null +++ b/tests/server-multi-ports.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "server_port": [8384, 8345, 8346, 8347], + "local_port": 1081, + "password": "foobar4", + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/setup_tc.sh b/tests/setup_tc.sh new file mode 100755 index 0000000..1a5fa20 --- /dev/null +++ b/tests/setup_tc.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +DEV=lo +PORT=8388 +DELAY=100ms + +type tc 2> /dev/null && ( + tc qdisc add dev $DEV root handle 1: htb + tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps + tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0 + tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6 + + tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6 + tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6 + + tc qdisc show dev lo +) + diff --git a/tests/socksify/install.sh b/tests/socksify/install.sh new file mode 100755 index 0000000..8eff72d --- /dev/null +++ b/tests/socksify/install.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ ! -d dante-1.4.0 ]; then + wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 + tar xf dante-1.4.0.tar.gz || exit 1 +fi +pushd dante-1.4.0 +./configure && make -j4 && make install || exit 1 +popd +cp tests/socksify/socks.conf /etc/ || exit 1 diff --git a/tests/socksify/socks.conf b/tests/socksify/socks.conf new file mode 100644 index 0000000..13db772 --- /dev/null +++ b/tests/socksify/socks.conf @@ -0,0 +1,5 @@ +route { + from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1081 + proxyprotocol: socks_v5 + method: none +} \ No newline at end of file diff --git a/test/table.json b/tests/table.json similarity index 60% rename from test/table.json rename to tests/table.json index 4b1c984..cca6ac2 100644 --- a/test/table.json +++ b/tests/table.json @@ -1,9 +1,9 @@ { "server":"127.0.0.1", "server_port":8388, - "local_port":1080, - "password":"barfoo!", - "timeout":300, + "local_port":1081, + "password":"table_password", + "timeout":60, "method":"table", "local_address":"127.0.0.1", "fast_open":false diff --git a/tests/test.py b/tests/test.py new file mode 100755 index 0000000..0b63a18 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,144 @@ +#!/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 __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os +import signal +import select +import time +import argparse +from subprocess import Popen, PIPE + +python = ['python'] + +parser = argparse.ArgumentParser(description='test Shadowsocks') +parser.add_argument('-c', '--client-conf', type=str, default=None) +parser.add_argument('-s', '--server-conf', type=str, default=None) +parser.add_argument('-a', '--client-args', type=str, default=None) +parser.add_argument('-b', '--server-args', type=str, default=None) +parser.add_argument('--with-coverage', action='store_true', default=None) + +config = parser.parse_args() + +if config.with_coverage: + python = ['coverage', 'run', '-p', '-a'] + +client_args = python + ['shadowsocks/local.py', '-v'] +server_args = python + ['shadowsocks/server.py', '-v'] + +if config.client_conf: + client_args.extend(['-c', config.client_conf]) + if config.server_conf: + server_args.extend(['-c', config.server_conf]) + else: + server_args.extend(['-c', config.client_conf]) +if config.client_args: + client_args.extend(config.client_args.split()) + if config.server_args: + server_args.extend(config.server_args.split()) + else: + server_args.extend(config.client_args.split()) + +p1 = Popen(server_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) +p2 = Popen(client_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) +p3 = None +p4 = None +p3_fin = False +p4_fin = False + +# 1 shadowsocks started +# 2 curl started +# 3 curl finished +# 4 dig started +# 5 dig finished +stage = 1 + +try: + local_ready = False + server_ready = False + fdset = [p1.stdout, p2.stdout, p1.stderr, p2.stderr] + while True: + r, w, e = select.select(fdset, [], fdset) + if e: + break + + for fd in r: + line = fd.readline() + if not line: + if stage == 2 and fd == p3.stdout: + stage = 3 + if stage == 4 and fd == p4.stdout: + stage = 5 + if bytes != str: + line = str(line, 'utf8') + sys.stdout.write(line) + if line.find('starting local') >= 0: + local_ready = True + if line.find('starting server') >= 0: + server_ready = True + + if stage == 1: + time.sleep(2) + + p3 = Popen(['curl', 'http://www.example.com/', '-v', '-L', + '--socks5-hostname', '127.0.0.1:1081', + '-m', '15', '--connect-timeout', '10'], + stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) + if p3 is not None: + fdset.append(p3.stdout) + fdset.append(p3.stderr) + stage = 2 + else: + sys.exit(1) + + if stage == 3 and p3 is not None: + fdset.remove(p3.stdout) + fdset.remove(p3.stderr) + r = p3.wait() + if r != 0: + sys.exit(1) + p4 = Popen(['socksify', 'dig', '@8.8.8.8', 'www.google.com'], + stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) + if p4 is not None: + fdset.append(p4.stdout) + fdset.append(p4.stderr) + stage = 4 + else: + sys.exit(1) + + if stage == 5: + r = p4.wait() + if r != 0: + sys.exit(1) + print('test passed') + break +finally: + for p in [p1, p2]: + try: + os.kill(p.pid, signal.SIGINT) + os.waitpid(p.pid, 0) + except OSError: + pass diff --git a/tests/test_command.sh b/tests/test_command.sh new file mode 100755 index 0000000..eba4c8c --- /dev/null +++ b/tests/test_command.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +. tests/assert.sh + +PYTHON="coverage run -a -p" +LOCAL="$PYTHON shadowsocks/local.py" +SERVER="$PYTHON shadowsocks/server.py" + +assert "$LOCAL 2>&1 | grep ERROR" "ERROR: config not specified" +assert "$LOCAL 2>&1 | grep usage | cut -d: -f1" "usage" + +assert "$SERVER 2>&1 | grep ERROR" "ERROR: config not specified" +assert "$SERVER 2>&1 | grep usage | cut -d: -f1" "usage" + +assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: server set to listen on 127.0.0.1:8388, are you sure?" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 0.0.0.0 -p 8388 -t10 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 10 seems too short" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 0.0.0.0 -p 8388 -t1000 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 1000 seems too long" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4 -k testrc4 -s 0.0.0.0 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: RC4 is not safe; please use a safer cipher, like AES-256-CFB" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4-md5 -k mypassword -s 0.0.0.0 -p 8388 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " DON'T USE DEFAULT PASSWORD! Please change it in your config.json!" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": password not specified" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + +assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": password or port_password not specified" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop + + +assert_end command diff --git a/tests/test_daemon.sh b/tests/test_daemon.sh new file mode 100755 index 0000000..40f35ef --- /dev/null +++ b/tests/test_daemon.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +function run_test { + expected=$1 + shift + echo "running test: $command $@" + $command $@ + status=$? + if [ $status -ne $expected ]; then + echo "exit $status != $expected" + exit 1 + fi + echo "exit status $status == $expected" + echo OK + return +} + +for module in local server +do + +command="coverage run -p -a shadowsocks/$module.py" + +mkdir -p tmp + +run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log + +run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log + +run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 1 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log + +run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log + +run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log +run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log + +run_test 1 -c tests/aes.json -d start --pid-file tmp/not_exist/shadowsocks.pid --log-file tmp/shadowsocks.log + +done diff --git a/tests/test_large_file.sh b/tests/test_large_file.sh new file mode 100755 index 0000000..e8acd79 --- /dev/null +++ b/tests/test_large_file.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +PYTHON="coverage run -p -a" +URL=http://127.0.0.1/file + +mkdir -p tmp + +$PYTHON shadowsocks/local.py -c tests/aes.json & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/aes.json & +SERVER=$! + +sleep 3 + +time curl -o tmp/expected $URL +time curl -o tmp/result --socks5-hostname 127.0.0.1:1081 $URL + +kill -s SIGINT $LOCAL +kill -s SIGINT $SERVER + +sleep 2 + +diff tmp/expected tmp/result || exit 1 diff --git a/test/workers.json b/tests/workers.json similarity index 60% rename from test/workers.json rename to tests/workers.json index c3d699b..2015ff6 100644 --- a/test/workers.json +++ b/tests/workers.json @@ -1,9 +1,9 @@ { "server":"127.0.0.1", "server_port":8388, - "local_port":1080, - "password":"barfoo!", - "timeout":300, + "local_port":1081, + "password":"workers_password", + "timeout":60, "method":"aes-256-cfb", "local_address":"127.0.0.1", "workers": 4