diff --git a/.gitignore b/.gitignore index 357232f..6c1b61e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,8 @@ develop-eggs pip-log.txt # Unit test / coverage reports -.coverage +htmlcov +.coverage* .tox #Translations diff --git a/.travis.yml b/.travis.yml index 9d7a9bb..f29cb96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,28 +9,13 @@ cache: - dante-1.4.0 before_install: - sudo apt-get update -qq - - sudo apt-get install -qq build-essential libssl-dev swig python-m2crypto python-numpy dnsutils - - pip install m2crypto salsa20 pep8 pyflakes nose coverage + - sudo apt-get install -qq build-essential dnsutils iproute nginx bc + - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 + - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" + - sudo service nginx restart + - pip install pep8 pyflakes nose coverage - sudo tests/socksify/install.sh - sudo tests/libsodium/install.sh + - sudo tests/setup_tc.sh script: - - pep8 . - - pyflakes . - - coverage run tests/nose_plugin.py -v - - python setup.py sdist - - python tests/test.py --with-coverage -c tests/aes.json - - python tests/test.py --with-coverage -c tests/aes-ctr.json - - python tests/test.py --with-coverage -c tests/aes-cfb1.json - - python tests/test.py --with-coverage -c tests/aes-cfb8.json - - python tests/test.py --with-coverage -c tests/rc4-md5.json - - python tests/test.py --with-coverage -c tests/salsa20.json - - python tests/test.py --with-coverage -c tests/chacha20.json - - python tests/test.py --with-coverage -c tests/salsa20-ctr.json - - python tests/test.py --with-coverage -c tests/table.json - - python tests/test.py --with-coverage -c tests/server-multi-ports.json - - python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json - - python tests/test.py --with-coverage -c tests/workers.json - - python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json - - python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081" - - python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081" - - coverage combine && coverage report --include=shadowsocks/* + - tests/jenkins.sh diff --git a/CHANGES b/CHANGES index 4c07066..602a0a4 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,40 @@ +2.6.9 2015-05-19 +- Fix a stability issue on Windows + +2.6.8 2015-02-10 +- Support multiple server ip on client side +- Support --version +- Minor fixes + +2.6.7 2015-02-02 +- Support --user +- Support CIDR format in --forbidden-ip +- Minor fixes + +2.6.6 2015-01-23 +- Fix a crash in forbidden list + +2.6.5 2015-01-18 +- Try both 32 bit and 64 bit dll on Windows + +2.6.4 2015-01-14 +- Also search lib* when searching libraries + +2.6.3 2015-01-12 +- Support --forbidden-ip to ban some IP, i.e. localhost +- Search OpenSSL and libsodium harder +- Now works on OpenWRT + +2.6.2 2015-01-03 +- Log client IP + +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c164423..fbdb9c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,38 +1,29 @@ -How to contribute +How to Contribute ================= -在你提交问题前,请先[自行诊断]一下。提交时附上诊断过程中的问题和下列结果, -否则如果我们无法重现你的问题,也就不能帮助你。 +Pull Requests +------------- -Before you submit issues, please read [Troubleshooting] and take a few minutes -to read this guide. - -问题反馈 -------- - -请提交下面的信息: - -1. 你是如何搭建环境的?(操作系统,Shadowsocks 版本) -2. 操作步骤是什么? -3. 浏览器里的现象是什么?一直转菊花,还是有提示错误? -4. 发生错误时,客户端和服务端最后一部分日志。 -5. 其它你认为可能和问题有关的信息。 - -如果你不清楚其中某条的含义, 可以直接跳过那一条。 +1. Pull requests are welcome. If you would like to add a large feature +or make a significant change, make sure to open an issue to discuss with +people first. +2. Follow PEP8. +3. Make sure to pass the unit tests. Write unit tests for new modules if +needed. Issues ------ -Please include the following information in your submission: +1. Only bugs and feature requests are accepted here. +2. We'll only work on important features. If the feature you're asking only +benefits a few people, you'd better implement the feature yourself and send us +a pull request, or ask some of your friends to do so. +3. We don't answer questions of any other types here. Since very few people +are watching the issue tracker here, you'll probably get no help from here. +Read [Troubleshooting] and get help from forums or [mailing lists]. +4. Issues in languages other than English will be Google translated into English +later. -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 +[mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks diff --git a/LICENSE b/LICENSE index 98f608b..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -Shadowsocks -Copyright (c) 2014 clowwindy + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + 1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in index 1dc4c8e..1882dd7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -recursive-include *.py +recursive-include shadowsocks *.py include README.rst include LICENSE diff --git a/README.md b/README.md index 7ee4bea..76d759a 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,81 @@ shadowsocks =========== -[![PyPI version]][PyPI] [![Build Status]][Travis CI] +[![PyPI version]][PyPI] +[![Build Status]][Travis CI] +[![Coverage Status]][Coverage] A fast tunnel proxy that helps you bypass firewalls. -[中文说明][Chinese Readme] +Server +------ -Install -------- +### Install -You'll have a client on your local side, and setup a server on a -remote server. +Debian / Ubuntu: -### Client + apt-get install python-pip + pip install shadowsocks + +CentOS: + + yum install python-setuptools && easy_install pip + pip install shadowsocks + +Windows: + +See [Install Server on Windows] + +### Usage + + ssserver -p 443 -k password -m aes-256-cfb + +To run in the background: + + sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start + +To stop: + + sudo ssserver -d stop + +To check the log: + + sudo less /var/log/shadowsocks.log + +Check all the options via `-h`. You can also use a [Configuration] file +instead. + +Client +------ * [Windows] / [OS X] * [Android] / [iOS] * [OpenWRT] -### Server +Use GUI clients on your local PC/phones. Check the README of your client +for more information. -#### Debian / Ubuntu: - - 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 python-setuptools - easy_install pip - pip install shadowsocks - -#### Windows: - -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. - -Configuration +Documentation ------------- -On your server create a config file `/etc/shadowsocks.json`. -Example: - - { - "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 - } - -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 | 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 client machine, use the same configuration as your server, and -start your client. - -If you use Chrome, it's recommended to use [SwitchySharp]. Change the proxy -settings to - - protocol: socks5 - hostname: 127.0.0.1 - port: your local_port - -If you can't install [SwitchySharp], you can launch Chrome with the following -arguments to force Chrome to use the proxy: - - Chrome.exe --proxy-server="socks5://127.0.0.1:1080" --host-resolver-rules="MAP * 0.0.0.0 , EXCLUDE localhost" - -If you can't even download Chrome, find a friend to download a -[Chrome Standalone] installer for you. - -Command line args ------------------- - -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 - -List all available args with `-h`. - -Wiki ----- - -You can find all the documentation in the wiki: -https://github.com/clowwindy/shadowsocks/wiki +You can find all the documentation in the [Wiki]. License ------- -MIT + +Copyright 2015 clowwindy + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. Bugs and Issues ---------------- @@ -124,24 +85,22 @@ Bugs and Issues * [Mailing list] -[Android]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#android -[Build Status]: https://img.shields.io/travis/clowwindy/shadowsocks/master.svg?style=flat -[Chinese Readme]: https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E -[Chrome Standalone]: https://support.google.com/installer/answer/126299 + +[Android]: https://github.com/shadowsocks/shadowsocks-android +[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat +[Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File +[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks +[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html [Debian sid]: https://packages.debian.org/unstable/python/shadowsocks -[the package]: https://pypi.python.org/pypi/shadowsocks -[Encryption]: https://github.com/clowwindy/shadowsocks/wiki/Encryption [iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help -[Issue Tracker]: https://github.com/clowwindy/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/clowwindy/shadowsocks/wiki/Ports-and-Clients#openwrt +[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open +[Install Server on Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows +[Mailing list]: https://groups.google.com/group/shadowsocks +[OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks [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 -[Supervisor]: https://github.com/clowwindy/shadowsocks/wiki/Configure-Shadowsocks-with-Supervisor -[TCP_FASTOPEN]: https://github.com/clowwindy/shadowsocks/wiki/TCP-Fast-Open -[Travis CI]: https://travis-ci.org/clowwindy/shadowsocks -[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting -[SwitchySharp]: https://chrome.google.com/webstore/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm -[Windows]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#windows +[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks +[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting +[Wiki]: https://github.com/shadowsocks/shadowsocks/wiki +[Windows]: https://github.com/shadowsocks/shadowsocks-csharp diff --git a/README.rst b/README.rst index 19d5b2a..bf2a3ec 100644 --- a/README.rst +++ b/README.rst @@ -1,167 +1,113 @@ shadowsocks =========== -|PyPI version| |Build Status| +|PyPI version| |Build Status| |Coverage Status| A fast tunnel proxy that helps you bypass firewalls. -`中文说明 `__ +Server +------ Install -------- - -You'll have a client on your local side, and setup a server on a remote -server. - -Client -~~~~~~ - -- `Windows `__ - / `OS - X `__ -- `Android `__ - / `iOS `__ -- `OpenWRT `__ - -Server -~~~~~~ +~~~~~~~ Debian / Ubuntu: -^^^^^^^^^^^^^^^^ :: 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 python-setuptools - easy_install pip + yum install python-setuptools && easy_install pip pip install shadowsocks Windows: -^^^^^^^^ -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. +See `Install Server on +Windows `__ -Configuration +Usage +~~~~~ + +:: + + ssserver -p 443 -k password -m rc4-md5 + +To run in the background: + +:: + + sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start + +To stop: + +:: + + sudo ssserver -d stop + +To check the log: + +:: + + sudo less /var/log/shadowsocks.log + +Check all the options via ``-h``. You can also use a +`Configuration `__ +file instead. + +Client +------ + +- `Windows `__ + / `OS + X `__ +- `Android `__ + / `iOS `__ +- `OpenWRT `__ + +Use GUI clients on your local PC/phones. Check the README of your client +for more information. + +Documentation ------------- -On your server create a config file ``/etc/shadowsocks.json``. Example: - -:: - - { - "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 - } - -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 | 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 client machine, use the same configuration as your server, and -start your client. - -If you use Chrome, it's recommended to use -`SwitchySharp `__. -Change the proxy settings to - -:: - - protocol: socks5 - hostname: 127.0.0.1 - port: your local_port - -If you can't install -`SwitchySharp `__, -you can launch Chrome with the following arguments to force Chrome to -use the proxy: - -:: - - Chrome.exe --proxy-server="socks5://127.0.0.1:1080" --host-resolver-rules="MAP * 0.0.0.0 , EXCLUDE localhost" - -If you can't even download Chrome, find a friend to download a `Chrome -Standalone `__ -installer for you. - -Command line args ------------------ - -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 - -List all available args with ``-h``. - -Wiki ----- - -You can find all the documentation in the wiki: -https://github.com/clowwindy/shadowsocks/wiki +You can find all the documentation in the +`Wiki `__. License ------- -MIT +Copyright 2015 clowwindy + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + +:: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. Bugs and Issues --------------- -- `Troubleshooting `__ +- `Troubleshooting `__ - `Issue - Tracker `__ -- `Mailing list `__ + Tracker `__ +- `Mailing list `__ .. |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/clowwindy/shadowsocks/master.svg?style=flat - :target: https://travis-ci.org/clowwindy/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:: https://jenkins.shadowvpn.org/result/shadowsocks + :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html diff --git a/setup.py b/setup.py index 5103c70..38def84 100644 --- a/setup.py +++ b/setup.py @@ -7,12 +7,12 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.5", - license='MIT', + version="2.6.10", + license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', author_email='clowwindy42@gmail.com', - url='https://github.com/clowwindy/shadowsocks', + url='https://github.com/shadowsocks/shadowsocks', packages=['shadowsocks', 'shadowsocks.crypto'], package_data={ 'shadowsocks': ['README.rst', 'LICENSE'] @@ -24,7 +24,7 @@ setup( ssserver = shadowsocks.server:main """, classifiers=[ - 'License :: OSI Approved :: MIT License', + 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', diff --git a/shadowsocks/__init__.py b/shadowsocks/__init__.py index 5ba5908..dc3abd4 100644 --- a/shadowsocks/__init__.py +++ b/shadowsocks/__init__.py @@ -1,24 +1,18 @@ #!/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: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 18222a6..7e4a4ed 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2014-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -31,7 +25,7 @@ import struct import re import logging -from shadowsocks import common, lru_cache, eventloop +from shadowsocks import common, lru_cache, eventloop, shell CACHE_SWEEP_INTERVAL = 30 @@ -93,11 +87,12 @@ def build_address(address): return b''.join(results) -def build_request(address, qtype, request_id): - header = struct.pack('!HBBHHHH', request_id, 1, 0, 1, 0, 0, 0) +def build_request(address, qtype): + request_id = os.urandom(2) + header = struct.pack('!BBHHHH', 1, 0, 1, 0, 0, 0) addr = build_address(address) qtype_qclass = struct.pack('!HH', qtype, QCLASS_IN) - return header + addr + qtype_qclass + return request_id + header + addr + qtype_qclass def parse_ip(addrtype, data, length, offset): @@ -226,24 +221,10 @@ def parse_response(data): response.answers.append((an[1], an[2], an[3])) return response except Exception as e: - import traceback - traceback.print_exc() - logging.error(e) + shell.print_exception(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 @@ -270,7 +251,6 @@ class DNSResolver(object): def __init__(self): self._loop = None - self._request_id = 1 self._hosts = {} self._hostname_status = {} self._hostname_to_cb = {} @@ -296,7 +276,7 @@ class DNSResolver(object): parts = line.split() if len(parts) >= 2: server = parts[1] - if is_ip(server) == socket.AF_INET: + if common.is_ip(server) == socket.AF_INET: if type(server) != str: server = server.decode('utf8') self._servers.append(server) @@ -316,7 +296,7 @@ class DNSResolver(object): parts = line.split() if len(parts) >= 2: ip = parts[0] - if is_ip(ip): + if common.is_ip(ip): for i in range(1, len(parts)): hostname = parts[i] if hostname: @@ -412,10 +392,7 @@ class DNSResolver(object): 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) + req = build_request(hostname, qtype) for server in self._servers: logging.debug('resolving %s with type %d using server %s', hostname, qtype, server) @@ -426,7 +403,7 @@ class DNSResolver(object): hostname = hostname.encode('utf8') if not hostname: callback(None, Exception('empty hostname')) - elif is_ip(hostname): + elif common.is_ip(hostname): callback((hostname, hostname), None) elif hostname in self._hosts: logging.debug('hit hosts: %s', hostname) diff --git a/shadowsocks/common.py b/shadowsocks/common.py index e4f698c..1977dcd 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2013-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -101,6 +95,18 @@ def inet_pton(family, addr): raise RuntimeError("What family?") +def is_ip(address): + for family in (socket.AF_INET, socket.AF_INET6): + try: + if type(address) != str: + address = address.decode('utf8') + inet_pton(family, address) + return family + except (TypeError, ValueError, OSError, IOError): + pass + return False + + def patch_socket(): if not hasattr(socket, 'inet_pton'): socket.inet_pton = inet_pton @@ -172,6 +178,61 @@ def parse_header(data): return addrtype, to_bytes(dest_addr), dest_port, header_length +class IPNetwork(object): + ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0} + + def __init__(self, addrs): + self._network_list_v4 = [] + self._network_list_v6 = [] + if type(addrs) == str: + addrs = addrs.split(',') + list(map(self.add_network, addrs)) + + def add_network(self, addr): + if addr is "": + return + block = addr.split('/') + addr_family = is_ip(block[0]) + addr_len = IPNetwork.ADDRLENGTH[addr_family] + if addr_family is socket.AF_INET: + ip, = struct.unpack("!I", socket.inet_aton(block[0])) + elif addr_family is socket.AF_INET6: + hi, lo = struct.unpack("!QQ", inet_pton(addr_family, block[0])) + ip = (hi << 64) | lo + else: + raise Exception("Not a valid CIDR notation: %s" % addr) + if len(block) is 1: + prefix_size = 0 + while (ip & 1) == 0 and ip is not 0: + ip >>= 1 + prefix_size += 1 + logging.warn("You did't specify CIDR routing prefix size for %s, " + "implicit treated as %s/%d" % (addr, addr, addr_len)) + elif block[1].isdigit() and int(block[1]) <= addr_len: + prefix_size = addr_len - int(block[1]) + ip >>= prefix_size + else: + raise Exception("Not a valid CIDR notation: %s" % addr) + if addr_family is socket.AF_INET: + self._network_list_v4.append((ip, prefix_size)) + else: + self._network_list_v6.append((ip, prefix_size)) + + def __contains__(self, addr): + addr_family = is_ip(addr) + if addr_family is socket.AF_INET: + ip, = struct.unpack("!I", socket.inet_aton(addr)) + return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], + self._network_list_v4)) + elif addr_family is socket.AF_INET6: + hi, lo = struct.unpack("!QQ", inet_pton(addr_family, addr)) + ip = (hi << 64) | lo + return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], + self._network_list_v6)) + else: + return False + + def test_inet_conv(): ipv4 = b'8.8.4.4' b = inet_pton(socket.AF_INET, ipv4) @@ -198,7 +259,23 @@ def test_pack_header(): assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com' +def test_ip_network(): + ip_network = IPNetwork('127.0.0.0/24,::ff:1/112,::1,192.168.1.1,192.0.2.0') + assert '127.0.0.1' in ip_network + assert '127.0.1.1' not in ip_network + assert ':ff:ffff' in ip_network + assert '::ffff:1' not in ip_network + assert '::1' in ip_network + assert '::2' not in ip_network + assert '192.168.1.1' in ip_network + assert '192.168.1.2' not in ip_network + assert '192.0.2.1' in ip_network + assert '192.0.3.1' in ip_network # 192.0.2.0 is treated as 192.0.2.0/23 + assert 'www.google.com' not in ip_network + + if __name__ == '__main__': test_inet_conv() test_parse_header() test_pack_header() + test_ip_network() diff --git a/shadowsocks/crypto/__init__.py b/shadowsocks/crypto/__init__.py index 6251321..401c7b7 100644 --- a/shadowsocks/crypto/__init__.py +++ b/shadowsocks/crypto/__init__.py @@ -1,24 +1,18 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement diff --git a/shadowsocks/crypto/m2.py b/shadowsocks/crypto/m2.py deleted file mode 100644 index 5ad48a8..0000000 --- a/shadowsocks/crypto/m2.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/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/ctypes_openssl.py b/shadowsocks/crypto/openssl.py similarity index 50% rename from shadowsocks/crypto/ctypes_openssl.py rename to shadowsocks/crypto/openssl.py index 0ef8ce0..3775b6c 100644 --- a/shadowsocks/crypto/ctypes_openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -1,32 +1,28 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement -import logging -from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ +from ctypes import c_char_p, c_int, c_long, byref,\ create_string_buffer, c_void_p +from shadowsocks import common +from shadowsocks.crypto import util + __all__ = ['ciphers'] libcrypto = None @@ -38,15 +34,12 @@ 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: + libcrypto = util.find_library(('crypto', 'eay32'), + 'EVP_get_cipherbyname', + 'libcrypto') + if libcrypto is None: 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 @@ -66,7 +59,7 @@ def load_openssl(): def load_cipher(cipher_name): - func_name = b'EVP_' + cipher_name.replace(b'-', b'_') + func_name = 'EVP_' + cipher_name.replace('-', '_') if bytes != str: func_name = str(func_name, 'utf-8') cipher = getattr(libcrypto, func_name, None) @@ -76,11 +69,12 @@ def load_cipher(cipher_name): return None -class CtypesCrypto(object): +class OpenSSLCrypto(object): def __init__(self, cipher_name, key, iv, op): + self._ctx = None if not loaded: load_openssl() - self._ctx = None + cipher_name = common.to_bytes(cipher_name) cipher = libcrypto.EVP_get_cipherbyname(cipher_name) if not cipher: cipher = load_cipher(cipher_name) @@ -119,69 +113,68 @@ class CtypesCrypto(object): 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), + 'aes-128-cfb': (16, 16, OpenSSLCrypto), + 'aes-192-cfb': (24, 16, OpenSSLCrypto), + 'aes-256-cfb': (32, 16, OpenSSLCrypto), + 'aes-128-ofb': (16, 16, OpenSSLCrypto), + 'aes-192-ofb': (24, 16, OpenSSLCrypto), + 'aes-256-ofb': (32, 16, OpenSSLCrypto), + 'aes-128-ctr': (16, 16, OpenSSLCrypto), + 'aes-192-ctr': (24, 16, OpenSSLCrypto), + 'aes-256-ctr': (32, 16, OpenSSLCrypto), + 'aes-128-cfb8': (16, 16, OpenSSLCrypto), + 'aes-192-cfb8': (24, 16, OpenSSLCrypto), + 'aes-256-cfb8': (32, 16, OpenSSLCrypto), + 'aes-128-cfb1': (16, 16, OpenSSLCrypto), + 'aes-192-cfb1': (24, 16, OpenSSLCrypto), + 'aes-256-cfb1': (32, 16, OpenSSLCrypto), + 'bf-cfb': (16, 8, OpenSSLCrypto), + 'camellia-128-cfb': (16, 16, OpenSSLCrypto), + 'camellia-192-cfb': (24, 16, OpenSSLCrypto), + 'camellia-256-cfb': (32, 16, OpenSSLCrypto), + 'cast5-cfb': (16, 8, OpenSSLCrypto), + 'des-cfb': (8, 8, OpenSSLCrypto), + 'idea-cfb': (16, 8, OpenSSLCrypto), + 'rc2-cfb': (16, 8, OpenSSLCrypto), + 'rc4': (16, 0, OpenSSLCrypto), + 'seed-cfb': (16, 16, OpenSSLCrypto), } 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) + cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) def test_aes_128_cfb(): - run_method(b'aes-128-cfb') + run_method('aes-128-cfb') def test_aes_256_cfb(): - run_method(b'aes-256-cfb') + run_method('aes-256-cfb') def test_aes_128_cfb8(): - run_method(b'aes-128-cfb8') + run_method('aes-128-cfb8') def test_aes_256_ofb(): - run_method(b'aes-256-ofb') + run_method('aes-256-ofb') def test_aes_256_ctr(): - run_method(b'aes-256-ctr') + run_method('aes-256-ctr') def test_bf_cfb(): - run_method(b'bf-cfb') + run_method('bf-cfb') def test_rc4(): - run_method(b'rc4') + run_method('rc4') if __name__ == '__main__': diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 3062dcc..1f07a82 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -1,30 +1,25 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement import hashlib +from shadowsocks.crypto import openssl __all__ = ['ciphers'] @@ -35,27 +30,19 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, 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) + return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) ciphers = { - b'rc4-md5': (16, 16, create_cipher), + '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) + cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1) + decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/salsa20_ctr.py b/shadowsocks/crypto/salsa20_ctr.py deleted file mode 100644 index 0ea13b8..0000000 --- a/shadowsocks/crypto/salsa20_ctr.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/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 struct -import logging -import sys - -slow_xor = False -imported = False - -salsa20 = None -numpy = None - -BLOCK_SIZE = 16384 - - -def run_imports(): - global imported, slow_xor, salsa20, numpy - if not imported: - imported = True - try: - 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: - salsa20 = __import__('salsa20') - except ImportError: - logging.error('you have to install salsa20 before you use salsa20') - sys.exit(1) - - -def numpy_xor(a, b): - if slow_xor: - return py_xor_str(a, b) - dtype = numpy.byte - if len(a) % 4 == 0: - dtype = numpy.uint32 - elif len(a) % 2 == 0: - dtype = numpy.uint16 - - ab = numpy.frombuffer(a, dtype=dtype) - bb = numpy.frombuffer(b, dtype=dtype) - c = numpy.bitwise_xor(ab, bb) - r = c.tostring() - return r - - -def py_xor_str(a, b): - 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): - """a salsa20 CTR implemetation, provides m2crypto like cipher API""" - - def __init__(self, alg, key, iv, op, key_as_bytes=0, d=None, salt=None, - i=1, padding=1): - run_imports() - if alg != b'salsa20-ctr': - raise Exception('unknown algorithm') - self._key = key - self._nonce = struct.unpack('= BLOCK_SIZE: - self._next_stream() - self._pos = 0 - if not data: - break - return b''.join(results) - - -ciphers = { - b'salsa20-ctr': (32, 8, Salsa20Cipher), -} - - -def test(): - from shadowsocks.crypto import util - - cipher = Salsa20Cipher(b'salsa20-ctr', b'k' * 32, b'i' * 8, 1) - decipher = Salsa20Cipher(b'salsa20-ctr', b'k' * 32, b'i' * 8, 1) - - util.run_cipher(cipher, decipher) - - -if __name__ == '__main__': - test() diff --git a/shadowsocks/crypto/ctypes_libsodium.py b/shadowsocks/crypto/sodium.py similarity index 55% rename from shadowsocks/crypto/ctypes_libsodium.py rename to shadowsocks/crypto/sodium.py index efecfd4..ae86fef 100644 --- a/shadowsocks/crypto/ctypes_libsodium.py +++ b/shadowsocks/crypto/sodium.py @@ -1,32 +1,27 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement -import logging -from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ +from ctypes import c_char_p, c_int, c_ulonglong, byref, \ create_string_buffer, c_void_p +from shadowsocks.crypto import util + __all__ = ['ciphers'] libsodium = None @@ -41,16 +36,11 @@ BLOCK_SIZE = 64 def load_libsodium(): global loaded, libsodium, buf - from ctypes.util import find_library - for p in ('sodium',): - libsodium_path = find_library(p) - if libsodium_path: - break - else: + libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', + 'libsodium') + if libsodium is None: 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, @@ -62,13 +52,11 @@ def load_libsodium(): c_char_p, c_ulonglong, c_char_p) - libsodium.sodium_init() - buf = create_string_buffer(buf_size) loaded = True -class Salsa20Crypto(object): +class SodiumCrypto(object): def __init__(self, cipher_name, key, iv, op): if not loaded: load_libsodium() @@ -76,9 +64,9 @@ class Salsa20Crypto(object): self.iv = iv self.key_ptr = c_char_p(key) self.iv_ptr = c_char_p(iv) - if cipher_name == b'salsa20': + if cipher_name == 'salsa20': self.cipher = libsodium.crypto_stream_salsa20_xor_ic - elif cipher_name == b'chacha20': + elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic else: raise Exception('Unknown cipher') @@ -107,25 +95,22 @@ class Salsa20Crypto(object): ciphers = { - b'salsa20': (32, 8, Salsa20Crypto), - b'chacha20': (32, 8, Salsa20Crypto), + 'salsa20': (32, 8, SodiumCrypto), + 'chacha20': (32, 8, SodiumCrypto), } 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) + cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('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) + cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index 08c1205..bc693f5 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -1,24 +1,18 @@ # !/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -73,7 +67,7 @@ class TableCipher(object): ciphers = { - b'table': (0, 0, TableCipher) + 'table': (0, 0, TableCipher) } @@ -169,8 +163,8 @@ def test_table_result(): def test_encryption(): from shadowsocks.crypto import util - cipher = TableCipher(b'table', b'test', b'', 1) - decipher = TableCipher(b'table', b'test', b'', 0) + cipher = TableCipher('table', b'test', b'', 1) + decipher = TableCipher('table', b'test', b'', 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index 3bac1db..e579455 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -1,24 +1,95 @@ #!/usr/bin/env python +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. -# 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 os +import logging + + +def find_library_nt(name): + # modified from ctypes.util + # ctypes.util.find_library just returns first result he found + # but we want to try them all + # because on Windows, users may have both 32bit and 64bit version installed + results = [] + for directory in os.environ['PATH'].split(os.pathsep): + fname = os.path.join(directory, name) + if os.path.isfile(fname): + results.append(fname) + if fname.lower().endswith(".dll"): + continue + fname = fname + ".dll" + if os.path.isfile(fname): + results.append(fname) + return results + + +def find_library(possible_lib_names, search_symbol, library_name): + import ctypes.util + from ctypes import CDLL + + paths = [] + + if type(possible_lib_names) not in (list, tuple): + possible_lib_names = [possible_lib_names] + + lib_names = [] + for lib_name in possible_lib_names: + lib_names.append(lib_name) + lib_names.append('lib' + lib_name) + + for name in lib_names: + if os.name == "nt": + paths.extend(find_library_nt(name)) + else: + path = ctypes.util.find_library(name) + if path: + paths.append(path) + + if not paths: + # We may get here when find_library fails because, for example, + # the user does not have sufficient privileges to access those + # tools underlying find_library on linux. + import glob + + for name in lib_names: + patterns = [ + '/usr/local/lib*/lib%s.*' % name, + '/usr/lib*/lib%s.*' % name, + 'lib%s.*' % name, + '%s.dll' % name] + + for pat in patterns: + files = glob.glob(pat) + if files: + paths.extend(files) + for path in paths: + try: + lib = CDLL(path) + if hasattr(lib, search_symbol): + logging.info('loading %s from %s', library_name, path) + return lib + else: + logging.warn('can\'t find symbol %s in %s', search_symbol, + path) + except Exception: + pass + return None def run_cipher(cipher, decipher): @@ -49,3 +120,19 @@ def run_cipher(cipher, decipher): end = time.time() print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start))) assert b''.join(results) == plain + + +def test_find_library(): + assert find_library('c', 'strcpy', 'libc') is not None + assert find_library(['c'], 'strcpy', 'libc') is not None + assert find_library(('c',), 'strcpy', 'libc') is not None + assert find_library(('crypto', 'eay32'), 'EVP_CipherUpdate', + 'libcrypto') is not None + assert find_library('notexist', 'strcpy', 'libnotexist') is None + assert find_library('c', 'symbol_not_exist', 'c') is None + assert find_library(('notexist', 'c', 'crypto', 'eay32'), + 'EVP_CipherUpdate', 'libc') is not None + + +if __name__ == '__main__': + test_find_library() diff --git a/shadowsocks/daemon.py b/shadowsocks/daemon.py new file mode 100644 index 0000000..8dc5608 --- /dev/null +++ b/shadowsocks/daemon.py @@ -0,0 +1,208 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014-2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import logging +import signal +import time +from shadowsocks import common, shell + +# this module is ported from ShadowVPN daemon.c + + +def daemon_exec(config): + if 'daemon' in config: + if os.name != 'posix': + raise Exception('daemon mode is only supported on Unix') + command = config['daemon'] + if not command: + command = 'start' + pid_file = config['pid-file'] + log_file = config['log-file'] + if command == 'start': + daemon_start(pid_file, log_file) + elif command == 'stop': + daemon_stop(pid_file) + # always exit after daemon_stop + sys.exit(0) + elif command == 'restart': + daemon_stop(pid_file) + daemon_start(pid_file, log_file) + else: + raise Exception('unsupported daemon command %s' % command) + + +def write_pid_file(pid_file, pid): + import fcntl + import stat + + try: + fd = os.open(pid_file, os.O_RDWR | os.O_CREAT, + stat.S_IRUSR | stat.S_IWUSR) + except OSError as e: + shell.print_exception(e) + return -1 + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + assert flags != -1 + flags |= fcntl.FD_CLOEXEC + r = fcntl.fcntl(fd, fcntl.F_SETFD, flags) + assert r != -1 + # There is no platform independent way to implement fcntl(fd, F_SETLK, &fl) + # via fcntl.fcntl. So use lockf instead + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET) + except IOError: + r = os.read(fd, 32) + if r: + logging.error('already started at pid %s' % common.to_str(r)) + else: + logging.error('already started') + os.close(fd) + return -1 + os.ftruncate(fd, 0) + os.write(fd, common.to_bytes(str(pid))) + return 0 + + +def freopen(f, mode, stream): + oldf = open(f, mode) + oldfd = oldf.fileno() + newfd = stream.fileno() + os.close(newfd) + os.dup2(oldfd, newfd) + + +def daemon_start(pid_file, log_file): + + def handle_exit(signum, _): + if signum == signal.SIGTERM: + sys.exit(0) + sys.exit(1) + + signal.signal(signal.SIGINT, handle_exit) + signal.signal(signal.SIGTERM, handle_exit) + + # fork only once because we are sure parent will exit + pid = os.fork() + assert pid != -1 + + if pid > 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: + shell.print_exception(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: + shell.print_exception(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 + shell.print_exception(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) + + +def set_user(username): + if username is None: + return + + import pwd + import grp + + try: + pwrec = pwd.getpwnam(username) + except KeyError: + logging.error('user not found: %s' % username) + raise + user = pwrec[0] + uid = pwrec[2] + gid = pwrec[3] + + cur_uid = os.getuid() + if uid == cur_uid: + return + if cur_uid != 0: + logging.error('can not set user as nonroot user') + # will raise later + + # inspired by supervisor + if hasattr(os, 'setgroups'): + groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] + groups.insert(0, gid) + os.setgroups(groups) + os.setgid(gid) + os.setuid(uid) diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index ba02101..4e87f41 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -1,24 +1,18 @@ #!/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: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -28,26 +22,19 @@ import sys import hashlib import logging -from shadowsocks.crypto import m2, rc4_md5, salsa20_ctr,\ - ctypes_openssl, ctypes_libsodium, table +from shadowsocks import common +from shadowsocks.crypto import rc4_md5, openssl, sodium, table method_supported = {} method_supported.update(rc4_md5.ciphers) -method_supported.update(salsa20_ctr.ciphers) -method_supported.update(ctypes_openssl.ciphers) -method_supported.update(ctypes_libsodium.ciphers) -# let M2Crypto override ctypes_openssl -method_supported.update(m2.ciphers) +method_supported.update(openssl.ciphers) +method_supported.update(sodium.ciphers) method_supported.update(table.ciphers) def random_string(length): - try: - import M2Crypto.Rand - return M2Crypto.Rand.rand_bytes(length) - except ImportError: - return os.urandom(length) + return os.urandom(length) cached_keys = {} @@ -60,8 +47,6 @@ def try_cipher(key, method=None): def EVP_BytesToKey(password, key_len, iv_len): # equivalent to OpenSSL's EVP_BytesToKey() with count 1 # so that we make the same key and iv as nodejs version - if hasattr(password, 'encode'): - password = password.encode('utf-8') cached_key = '%s-%d-%d' % (password, key_len, iv_len) r = cached_keys.get(cached_key, None) if r: @@ -109,8 +94,7 @@ class Encryptor(object): return len(self.cipher_iv) def get_cipher(self, password, method, op, iv): - if hasattr(password, 'encode'): - password = password.encode('utf-8') + password = common.to_bytes(password) m = self._method_info if m[0] > 0: key, iv_ = EVP_BytesToKey(password, m[0], m[1]) @@ -167,12 +151,12 @@ def encrypt_all(password, method, op, data): CIPHERS_TO_TEST = [ - b'aes-128-cfb', - b'aes-256-cfb', - b'rc4-md5', - b'salsa20', - b'chacha20', - b'table', + 'aes-128-cfb', + 'aes-256-cfb', + 'rc4-md5', + 'salsa20', + 'chacha20', + 'table', ] diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 55c30bb..42f9205 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2013-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. # from ssloop # https://github.com/clowwindy/ssloop @@ -34,6 +28,8 @@ import errno import logging from collections import defaultdict +from shadowsocks import shell + __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] @@ -229,11 +225,10 @@ class EventLoop(object): 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) + shell.print_exception(e) + if self._handlers_to_remove: + for handler in self._handlers_to_remove: + self._handlers.remove(handler) self._handlers_to_remove = [] self._iterating = False diff --git a/shadowsocks/local.py b/shadowsocks/local.py index e778ea7..4255a2e 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -30,11 +24,11 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns def main(): - utils.check_python() + shell.check_python() # fix py2exe if hasattr(sys, "frozen") and sys.frozen in \ @@ -42,11 +36,9 @@ def main(): p = os.path.dirname(os.path.abspath(sys.executable)) os.chdir(p) - config = utils.get_config(True) + config = shell.get_config(True) - utils.print_shadowsocks() - - encrypt.try_cipher(config['password'], config['method']) + daemon.daemon_exec(config) try: logging.info("starting local at %s:%d" % @@ -65,13 +57,16 @@ def main(): 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) + + daemon.set_user(config.get('user', None)) loop.run() - except (KeyboardInterrupt, IOError, OSError) as e: - logging.error(e) - if config['verbose']: - import traceback - traceback.print_exc() - os._exit(1) + except Exception as e: + shell.print_exception(e) + sys.exit(1) if __name__ == '__main__': main() diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index 4523399..401f19b 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -47,6 +41,7 @@ class LRUCache(collections.MutableMapping): self._time_to_keys = collections.defaultdict(list) self._keys_to_last_time = {} self._last_visits = collections.deque() + self._closed_values = set() self.update(dict(*args, **kwargs)) # use the free update to set keys def __getitem__(self, key): @@ -89,7 +84,9 @@ class LRUCache(collections.MutableMapping): if key in self._store: if now - self._keys_to_last_time[key] > self.timeout: value = self._store[key] - self.close_callback(value) + if value not in self._closed_values: + self.close_callback(value) + self._closed_values.add(value) for key in self._time_to_keys[least]: self._last_visits.popleft() if key in self._store: @@ -99,6 +96,7 @@ class LRUCache(collections.MutableMapping): c += 1 del self._time_to_keys[least] if c: + self._closed_values.clear() logging.debug('%d keys swept' % c) @@ -132,5 +130,21 @@ def test(): assert 'a' not in c assert 'b' not in c + global close_cb_called + close_cb_called = False + + def close_cb(t): + global close_cb_called + assert not close_cb_called + close_cb_called = True + + c = LRUCache(timeout=0.1, close_callback=close_cb) + c['s'] = 1 + c['s'] + time.sleep(0.1) + c['s'] + time.sleep(0.3) + c.sweep() + if __name__ == '__main__': test() diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 9abdf9c..429a20a 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -30,15 +24,15 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns def main(): - utils.check_python() + shell.check_python() - config = utils.get_config(False) + config = shell.get_config(False) - utils.print_shadowsocks() + daemon.daemon_exec(config) if config['port_password']: if config['password']: @@ -54,7 +48,6 @@ def main(): else: config['port_password'][str(server_port)] = config['password'] - encrypt.try_cipher(config['password'], config['method']) tcp_servers = [] udp_servers = [] dns_resolver = asyncdns.DNSResolver() @@ -74,17 +67,21 @@ def main(): tcp_servers + udp_servers)) signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), child_handler) + + 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)) + + daemon.set_user(config.get('user', None)) loop.run() - except (KeyboardInterrupt, IOError, OSError) as e: - logging.error(e) - if config['verbose']: - import traceback - traceback.print_exc() - os._exit(1) + except Exception as e: + shell.print_exception(e) + sys.exit(1) if int(config['workers']) > 1: if os.name == 'posix': @@ -110,6 +107,7 @@ def main(): sys.exit() signal.signal(signal.SIGTERM, handler) signal.signal(signal.SIGQUIT, handler) + signal.signal(signal.SIGINT, handler) # master for a_tcp_server in tcp_servers: diff --git a/shadowsocks/utils.py b/shadowsocks/shell.py similarity index 53% rename from shadowsocks/utils.py rename to shadowsocks/shell.py index 7808d8f..f8ae81f 100644 --- a/shadowsocks/utils.py +++ b/shadowsocks/shell.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -29,11 +23,14 @@ import json import sys import getopt import logging -from shadowsocks.common import to_bytes +from shadowsocks.common import to_bytes, to_str, IPNetwork +from shadowsocks import encrypt VERBOSE_LEVEL = 5 +verbose = 0 + def check_python(): info = sys.version_info @@ -48,6 +45,14 @@ def check_python(): sys.exit(1) +def print_exception(e): + global verbose + logging.error(e) + if verbose > 0: + import traceback + traceback.print_exc() + + def print_shadowsocks(): version = '' try: @@ -55,7 +60,7 @@ def print_shadowsocks(): version = pkg_resources.get_distribution('shadowsocks').version except Exception: pass - print('shadowsocks %s' % version) + print('Shadowsocks %s' % version) def find_config(): @@ -68,16 +73,37 @@ def find_config(): return None -def check_config(config): +def check_config(config, is_local): + if config.get('daemon', None) == 'stop': + # no need to specify configuration for daemon stop + return + + 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']) + if config.get('local_address', '') in [b'0.0.0.0']: - logging.warn('warning: local set to listen 0.0.0.0, which is not safe') - if config.get('server', '') in [b'127.0.0.1', b'localhost']: - logging.warn('warning: server set to listen %s:%s, are you sure?' % - (config['server'], config['server_port'])) - if (config.get('method', '') or '').lower() == b'table': + logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') + if config.get('server', '') in ['127.0.0.1', '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() == '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': + if (config.get('method', '') or '').lower() == 'rc4': logging.warn('warning: RC4 is not safe; please use a safer cipher, ' 'like AES-256-CFB') if config.get('timeout', 300) < 100: @@ -89,18 +115,28 @@ def check_config(config): if config.get('password') in [b'mypassword']: logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' 'config.json!') - exit(1) + sys.exit(1) + if config.get('user', None) is not None: + if os.name != 'posix': + logging.error('user can be used only on Unix') + sys.exit(1) + + encrypt.try_cipher(config['password'], config['method']) def get_config(is_local): + global verbose + logging.basicConfig(level=logging.INFO, format='%(levelname)-s: %(message)s') if is_local: - shortopts = 'hs:b:p:k:l:m:c:t:vq' - longopts = ['fast-open'] + shortopts = 'hd:s:b:p:k:l:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', + 'version'] else: - shortopts = 'hs:p:k:m:c:t:vq' - longopts = ['fast-open', 'workers='] + shortopts = 'hd:s:p:k:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', + 'forbidden-ip=', 'user=', 'version'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -121,7 +157,6 @@ def get_config(is_local): else: config = {} - optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) v_count = 0 for key, value in optlist: if key == '-p': @@ -131,11 +166,11 @@ def get_config(is_local): elif key == '-l': config['local_port'] = int(value) elif key == '-s': - config['server'] = to_bytes(value) + config['server'] = to_str(value) elif key == '-m': - config['method'] = to_bytes(value) + config['method'] = to_str(value) elif key == '-b': - config['local_address'] = to_bytes(value) + config['local_address'] = to_str(value) elif key == '-v': v_count += 1 # '-vv' turns on more verbose mode @@ -146,12 +181,25 @@ def get_config(is_local): config['fast_open'] = True elif key == '--workers': config['workers'] = int(value) - elif key == '-h': + elif key == '--user': + config['user'] = to_str(value) + elif key == '--forbidden-ip': + config['forbidden_ip'] = to_str(value).split(',') + elif key in ('-h', '--help'): if is_local: print_local_help() else: print_server_help() sys.exit(0) + elif key == '--version': + print_shadowsocks() + sys.exit(0) + elif key == '-d': + config['daemon'] = to_str(value) + elif key == '--pid-file': + config['pid-file'] = to_str(value) + elif key == '--log-file': + config['log-file'] = to_str(value) elif key == '-q': v_count -= 1 config['verbose'] = v_count @@ -165,41 +213,34 @@ def get_config(is_local): print_help(is_local) sys.exit(2) - config['password'] = config.get('password', '') - config['method'] = config.get('method', 'aes-256-cfb') + config['password'] = to_bytes(config.get('password', b'')) + config['method'] = to_str(config.get('method', 'aes-256-cfb')) 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['verbose'] = config.get('verbose', False) - config['local_address'] = config.get('local_address', '127.0.0.1') + config['local_address'] = to_str(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'] = to_str(config['server']) else: - config['server'] = config.get('server', '0.0.0.0') + config['server'] = to_str(config.get('server', '0.0.0.0')) + try: + config['forbidden_ip'] = \ + IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128')) + except Exception as e: + logging.error(e) + sys.exit(2) 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: @@ -212,11 +253,12 @@ def get_config(is_local): level = logging.ERROR else: level = logging.INFO + verbose = config['verbose'] logging.basicConfig(level=level, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - check_config(config) + check_config(config, is_local) return config @@ -229,47 +271,64 @@ def print_help(is_local): 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] [-v] [-q] + print('''usage: sslocal [OPTION]... +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, 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 - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ - -v, -vv verbose mode - -q, -qq quiet mode, only show warnings/errors +You can supply configurations via either config file or command line arguments. -Online help: +Proxy options: + -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: + -h, --help show this help message and exit + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + --user USER username to run as + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + --version show version information + +Online help: ''') def print_server_help(): - print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD - -m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open] - [--workers WORKERS] [-v] [-q] + print('''usage: ssserver [OPTION]... +A fast tunnel proxy that helps you bypass firewalls. -optional arguments: - -h, --help show this help message and exit - -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 - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ - --workers WORKERS number of workers, available on Unix/Linux - -v, -vv verbose mode - -q, -qq quiet mode, only show warnings/errors +You can supply configurations via either config file or command line arguments. -Online help: +Proxy options: + -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 + --forbidden-ip IPLIST comma seperated IP list forbidden to connect + +General options: + -h, --help show this help message and exit + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + --user USER username to run as + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + --version show version information + +Online help: ''') diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 146714a..4834883 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -32,7 +26,7 @@ import logging import traceback import random -from shadowsocks import encrypt, eventloop, utils, common +from shadowsocks import encrypt, eventloop, shell, common from shadowsocks.common import parse_header # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time @@ -43,30 +37,22 @@ TIMEOUT_PRECISION = 4 MSG_FASTOPEN = 0x20000000 -# SOCKS CMD defination +# SOCKS command definition 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: +# as 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 @@ -74,7 +60,7 @@ CMD_UDP_ASSOCIATE = 3 # stage 4 still connecting, more data from local received # stage 5 remote connected, piping local and remote -# ssserver: +# as 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 @@ -89,11 +75,16 @@ STAGE_CONNECTING = 4 STAGE_STREAM = 5 STAGE_DESTROYED = -1 -# stream direction +# for each handler, we have 2 stream directions: +# upstream: from client to server direction +# read local and write to remote +# downstream: from server to client direction +# read remote and write to local + STREAM_UP = 0 STREAM_DOWN = 1 -# stream wait status, indicating it's waiting for reading, etc +# for each stream, it's waiting for reading, or writing, or both WAIT_STATUS_INIT = 0 WAIT_STATUS_READING = 1 WAIT_STATUS_WRITING = 2 @@ -112,6 +103,9 @@ class TCPRelayHandler(object): self._remote_sock = None self._config = config self._dns_resolver = dns_resolver + + # TCP Relay works as either sslocal or ssserver + # if is_local, this is sslocal self._is_local = is_local self._stage = STAGE_INIT self._encryptor = encrypt.Encryptor(config['password'], @@ -121,7 +115,12 @@ class TCPRelayHandler(object): self._data_to_write_to_remote = [] self._upstream_status = WAIT_STATUS_READING self._downstream_status = WAIT_STATUS_INIT + self._client_address = local_sock.getpeername()[:2] self._remote_address = None + if 'forbidden_ip' in config: + self._forbidden_iplist = config['forbidden_ip'] + else: + self._forbidden_iplist = None if is_local: self._chosen_server = self._get_a_server() fd_to_handlers[local_sock.fileno()] = self @@ -145,8 +144,9 @@ class TCPRelayHandler(object): server_port = self._config['server_port'] if type(server_port) == list: server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) logging.debug('chosen server: %s:%d', server, server_port) - # TODO support multiple server IP return server, server_port def _update_activity(self): @@ -203,9 +203,7 @@ class TCPRelayHandler(object): errno.EWOULDBLOCK): uncomplete = True else: - logging.error(e) - if self._config['verbose']: - traceback.print_exc() + shell.print_exception(e) self.destroy() return False if uncomplete: @@ -241,26 +239,25 @@ class TCPRelayHandler(object): 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_local) + 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_local = [data] - self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + self._data_to_write_to_remote = [data] else: - self._data_to_write_to_local = [] - self._update_stream(STREAM_UP, WAIT_STATUS_READING) - self._stage = STAGE_STREAM + 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) + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() self.destroy() @@ -295,9 +292,10 @@ class TCPRelayHandler(object): 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) + logging.info('connecting %s:%d from %s:%d' % + (common.to_str(remote_addr), remote_port, + self._client_address[0], self._client_address[1])) + self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS @@ -318,7 +316,7 @@ class TCPRelayHandler(object): self._dns_resolver.resolve(remote_addr, self._handle_dns_resolved) except Exception as e: - logging.error(e) + self._log_error(e) if self._config['verbose']: traceback.print_exc() # TODO use logging when debug completed @@ -328,8 +326,12 @@ class TCPRelayHandler(object): 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)) + raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) af, socktype, proto, canonname, sa = addrs[0] + if self._forbidden_iplist: + if common.to_str(sa[0]) in self._forbidden_iplist: + raise Exception('IP %s is in forbidden list, reject' % + common.to_str(sa[0])) remote_sock = socket.socket(af, socktype, proto) self._remote_sock = remote_sock self._fd_to_handlers[remote_sock.fileno()] = self @@ -339,12 +341,13 @@ class TCPRelayHandler(object): def _handle_dns_resolved(self, result, error): if error: - logging.error(error) + self._log_error(error) self.destroy() return if result: ip = result[1] if ip: + try: self._stage = STAGE_CONNECTING remote_addr = ip @@ -377,8 +380,8 @@ class TCPRelayHandler(object): 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) + except Exception as e: + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() self.destroy() @@ -440,7 +443,7 @@ class TCPRelayHandler(object): try: self._write_to_sock(data, self._local_sock) except Exception as e: - logging.error(e) + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() # TODO use logging when debug completed @@ -508,6 +511,10 @@ class TCPRelayHandler(object): else: logging.warn('unknown socket') + def _log_error(self, e): + logging.error('%s when handling connection from %s:%d' % + (e, self._client_address[0], self._client_address[1])) + def destroy(self): # destroy the handler and release any resources # promises: @@ -623,7 +630,7 @@ class TCPRelay(object): # 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') + logging.log(shell.VERBOSE_LEVEL, 'sweeping timeouts') now = time.time() length = len(self._timeouts) pos = self._timeout_offset @@ -656,7 +663,7 @@ class TCPRelay(object): # handle events and dispatch to handlers for sock, fd, event in events: if sock: - logging.log(utils.VERBOSE_LEVEL, 'fd %d %s', fd, + logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd, eventloop.EVENT_NAMES.get(event, event)) if sock == self._server_socket: if event & eventloop.POLL_ERR: @@ -674,7 +681,7 @@ class TCPRelay(object): errno.EWOULDBLOCK): continue else: - logging.error(e) + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() else: diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 2b8b12f..98bfaaa 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. # SOCKS5 UDP Request # +----+------+------+----------+----------+----------+ @@ -75,7 +69,7 @@ import struct import errno import random -from shadowsocks import encrypt, eventloop, lru_cache, common +from shadowsocks import encrypt, eventloop, lru_cache, common, shell from shadowsocks.common import parse_header, pack_addr @@ -112,6 +106,10 @@ class UDPRelay(object): self._closed = False self._last_time = time.time() self._sockets = set() + if 'forbidden_ip' in config: + self._forbidden_iplist = config['forbidden_ip'] + else: + self._forbidden_iplist = None addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) @@ -129,8 +127,9 @@ class UDPRelay(object): server_port = self._config['server_port'] if type(server_port) == list: server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) logging.debug('chosen server: %s:%d', server, server_port) - # TODO support multiple server IP return server, server_port def _close_client(self, client): @@ -178,6 +177,12 @@ class UDPRelay(object): socket.SOCK_DGRAM, socket.SOL_UDP) if addrs: af, socktype, proto, canonname, sa = addrs[0] + if self._forbidden_iplist: + if common.to_str(sa[0]) in self._forbidden_iplist: + logging.debug('IP %s is in forbidden list, drop' % + common.to_str(sa[0])) + # drop + return client = socket.socket(af, socktype, proto) client.setblocking(False) self._cache[key] = client @@ -203,7 +208,7 @@ class UDPRelay(object): if err in (errno.EINPROGRESS, errno.EAGAIN): pass else: - logging.error(e) + shell.print_exception(e) def _handle_client(self, sock): data, r_addr = sock.recvfrom(BUF_SIZE) 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/client-multi-server-ip.json b/tests/client-multi-server-ip.json new file mode 100644 index 0000000..1823c2a --- /dev/null +++ b/tests/client-multi-server-ip.json @@ -0,0 +1,10 @@ +{ + "server":["127.0.0.1", "127.0.0.1"], + "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/coverage_server.py b/tests/coverage_server.py new file mode 100644 index 0000000..23cc8cd --- /dev/null +++ b/tests/coverage_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +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/jenkins.sh b/tests/jenkins.sh new file mode 100755 index 0000000..71d5b1c --- /dev/null +++ b/tests/jenkins.sh @@ -0,0 +1,82 @@ +#!/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 +} + +python --version +coverage erase +mkdir tmp +run_test pep8 --ignore=E402 . +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/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/aes.json -c tests/client-multi-server-ip.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" +run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" + +# test if DNS works +run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" + +# test localhost is in the forbidden list by default +run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" + +# test localhost is available when forbidden list is empty +run_test python tests/test.py --with-coverage --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -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/tests/nose_plugin.py b/tests/nose_plugin.py index ad32cf0..86b1a86 100644 --- a/tests/nose_plugin.py +++ b/tests/nose_plugin.py @@ -1,3 +1,19 @@ +#!/usr/bin/env python +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import nose from nose.plugins.base import Plugin 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/test.py b/tests/test.py index 5314d6e..29b57d4 100755 --- a/tests/test.py +++ b/tests/test.py @@ -1,25 +1,19 @@ #!/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: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# 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. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -34,12 +28,18 @@ from subprocess import Popen, PIPE python = ['python'] +default_url = 'http://localhost/' + 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) +parser.add_argument('--should-fail', action='store_true', default=None) +parser.add_argument('--tcp-only', action='store_true', default=None) +parser.add_argument('--url', type=str, default=default_url) +parser.add_argument('--dns', type=str, default='8.8.8.8') config = parser.parse_args() @@ -61,6 +61,8 @@ if config.client_args: server_args.extend(config.server_args.split()) else: server_args.extend(config.client_args.split()) +if config.url == default_url: + server_args.extend(['--forbidden-ip', '']) 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) @@ -94,7 +96,7 @@ try: stage = 5 if bytes != str: line = str(line, 'utf8') - sys.stdout.write(line) + sys.stderr.write(line) if line.find('starting local') >= 0: local_ready = True if line.find('starting server') >= 0: @@ -103,7 +105,7 @@ try: if stage == 1: time.sleep(2) - p3 = Popen(['curl', 'http://www.example.com/', '-v', '-L', + p3 = Popen(['curl', config.url, '-v', '-L', '--socks5-hostname', '127.0.0.1:1081', '-m', '15', '--connect-timeout', '10'], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) @@ -118,9 +120,16 @@ try: 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'], + if config.should_fail: + if r == 0: + sys.exit(1) + else: + if r != 0: + sys.exit(1) + if config.tcp_only: + break + p4 = Popen(['socksify', 'dig', '@%s' % config.dns, + 'www.google.com'], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) if p4 is not None: fdset.append(p4.stdout) @@ -131,14 +140,19 @@ try: if stage == 5: r = p4.wait() - if r != 0: - sys.exit(1) - print('test passed') + if config.should_fail: + if r == 0: + sys.exit(1) + print('test passed (expecting failure)') + else: + if r != 0: + sys.exit(1) + print('test passed') break finally: for p in [p1, p2]: try: - os.kill(p.pid, signal.SIGQUIT) + 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..be05704 --- /dev/null +++ b/tests/test_command.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +. tests/assert.sh + +PYTHON="coverage run -a -p" +LOCAL="$PYTHON shadowsocks/local.py" +SERVER="$PYTHON shadowsocks/server.py" + +assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" +assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" + +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 "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": Not a valid CIDR notation: 127.0.0.1/4a" +$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..33bcb59 --- /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 --forbidden-ip "" & +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/utils/README.md b/utils/README.md new file mode 100644 index 0000000..f624309 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,9 @@ +Useful Tools +=========== + +autoban.py +---------- + +Automatically ban IPs that try to brute force crack the server. + +See https://github.com/shadowsocks/shadowsocks/wiki/Ban-Brute-Force-Crackers diff --git a/utils/autoban.py b/utils/autoban.py new file mode 100755 index 0000000..1bbb65c --- /dev/null +++ b/utils/autoban.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 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 os +import sys +import argparse + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='See README') + parser.add_argument('-c', '--count', default=3, type=int, + help='with how many failure times it should be ' + 'considered as an attack') + config = parser.parse_args() + ips = {} + banned = set() + for line in sys.stdin: + if 'can not parse header when' in line: + ip = line.split()[-1].split(':')[0] + if ip not in ips: + ips[ip] = 1 + print(ip) + sys.stdout.flush() + else: + ips[ip] += 1 + if ip not in banned and ips[ip] >= config.count: + banned.add(ip) + cmd = 'iptables -A INPUT -s %s -j DROP' % ip + print(cmd, file=sys.stderr) + sys.stderr.flush() + os.system(cmd)