diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..6c1b61e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +htmlcov +.coverage* +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +.DS_Store +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..535bc9a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.3 + - 3.4 +cache: + directories: + - dante-1.4.0 +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq build-essential 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 PySocks + - sudo tests/socksify/install.sh + - sudo tests/libsodium/install.sh + - sudo tests/setup_tc.sh +script: + - tests/jenkins.sh diff --git a/CHANGES b/CHANGES new file mode 100755 index 0000000..d6fe932 --- /dev/null +++ b/CHANGES @@ -0,0 +1,256 @@ +2.8.2 2015-08-10 +- Fix a encoding problem in manager + +2.8.1 2015-08-06 +- Respond ok to add and remove commands + +2.8 2015-08-06 +- Add Shadowsocks manager + +2.7 2015-08-02 +- Optimize speed for multiple ports + +2.6.11 2015-07-10 +- Fix a compatibility issue in UDP Relay + +2.6.10 2015-06-08 +- Optimize LRU cache +- Refine logging + +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 + +2.4.3 2014-11-10 +- Fix an issue on Python 3 +- Fix an issue with IPv6 + +2.4.2 2014-11-06 +- Fix command line arguments on Python 3 +- Support table on Python 3 +- Fix TCP Fast Open on Python 3 + +2.4.1 2014-11-01 +- Fix setup.py for non-utf8 locales on Python 3 + +2.4 2014-11-01 +- Python 3 support +- Performance improvement +- Fix LRU cache behavior + +2.3.2 2014-10-11 +- Fix OpenSSL on Windows + +2.3.1 2014-10-09 +- Does not require M2Crypto any more + +2.3 2014-09-23 +- Support CFB1, CFB8 and CTR mode of AES +- Do not require password config when using port_password +- Use SIGTERM instead of SIGQUIT on Windows + +2.2.2 2014-09-14 +- Fix when multiple DNS set, IPv6 only sites are broken + +2.2.1 2014-09-10 +- Support graceful shutdown +- Fix some bugs + +2.2.0 2014-09-09 +- Add RC4-MD5 encryption + +2.1.0 2014-08-10 +- Use only IPv4 DNS server +- Does not ship config.json +- Better error message + +2.0.12 2014-07-26 +- Support -q quiet mode +- Exit 0 when showing help with -h + +2.0.11 2014-07-12 +- Prefers IP addresses over hostnames, more friendly with socksify and openvpn + +2.0.10 2014-07-11 +- Fix UDP on local + +2.0.9 2014-07-06 +- Fix EWOULDBLOCK on Windows +- Fix Unicode config problem on some platforms + +2.0.8 2014-06-23 +- Use multiple DNS to query hostnames + +2.0.7 2014-06-21 +- Fix fastopen on local +- Fallback when fastopen is not available +- Add verbose logging mode -vv +- Verify if hostname is valid + +2.0.6 2014-06-19 +- Fix CPU 100% on POLL_HUP +- More friendly logging + +2.0.5 2014-06-18 +- Support a simple config format for multiple ports + +2.0.4 2014-06-12 +- Fix worker master + +2.0.3 2014-06-11 +- Fix table encryption with UDP + +2.0.2 2014-06-11 +- Add asynchronous DNS in TCP relay + +2.0.1 2014-06-05 +- Better logging +- Maybe fix bad file descriptor + +2.0 2014-06-05 +- Use a new event model +- Remove gevent +- Refuse to use default password +- Fix a problem when using multiple passwords with table encryption + +1.4.5 2014-05-24 +- Add timeout in TCP server +- Close sockets in master process + +1.4.4 2014-05-17 +- Support multiple workers + +1.4.3 2014-05-13 +- Fix Windows + +1.4.2 2014-05-10 +- Add salsa20-ctr cipher + +1.4.1 2014-05-03 +- Fix error log +- Fix EINPROGESS with some version of gevent + +1.4.0 2014-05-02 +- Adds UDP relay +- TCP fast open support on Linux 3.7+ + +1.3.7 2014-04-10 +- Fix a typo in help + +1.3.6 2014-04-10 +- Fix a typo in help + +1.3.5 2014-04-07 +- Add help +- Change default local binding address into 127.0.0.1 + +1.3.4 2014-02-17 +- Fix a bug when no config file exists +- Client now support multiple server ports and multiple server/port pairs +- Better error message with bad config.json format and wrong password + +1.3.3 2013-07-09 +- Fix default key length of rc2 + +1.3.2 2013-07-04 +- Server will listen at server IP specified in config +- Check config file and show some warning messages + +1.3.1 2013-06-29 +- Fix -c arg + +1.3.0 2013-06-22 +- Move to pypi + +1.2.3 2013-06-14 +- add bind address + +1.2.2 2013-05-31 +- local can listen at ::0 with -6 arg; bump 1.2.2 + +1.2.1 2013-05-23 +- Fix an OpenSSL crash + +1.2 2013-05-22 +- Use random iv, we finally have strong encryption + +1.1.1 2013-05-21 +- Add encryption, AES, blowfish, etc. + +1.1 2013-05-16 +- Support IPv6 addresses (type 4) +- Drop Python 2.5 support + +1.0 2013-04-03 +- Fix -6 IPv6 + +0.9.4 2013-03-04 +- Support Python 2.5 + +0.9.3 2013-01-14 +- Fix conn termination null data + +0.9.2 2013-01-05 +- Change default timeout + +0.9.1 2013-01-05 +- Add Travis-CI test + +0.9 2012-12-30 +- Replace send with sendall, fix FreeBSD + +0.6 2012-12-06 +- Support args + +0.5 2012-11-08 +- Fix encryption with negative md5sum + +0.4 2012-11-02 +- Move config into a JSON file +- Auto-detect config path + +0.3 2012-06-06 +- Move socks5 negotiation to local + +0.2 2012-05-11 +- Add -6 arg for IPv6 +- Fix socket.error + +0.1 2012-04-20 +- Initial version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000..5d94e0b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +How to Contribute +================= + +Notice this is the repository for Shadowsocks Python version. If you have problems with Android / iOS / Windows etc clients, please post your questions in their issue trackers. + +Pull Requests +------------- + +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 +------ + +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. + + +[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting +[mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "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 new file mode 100755 index 0000000..1882dd7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include shadowsocks *.py +include README.rst +include LICENSE diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 15db4b5..7cda7e7 --- a/README.md +++ b/README.md @@ -1 +1,113 @@ -Removed according to regulations. +shadowsocks +=========== + +[![PyPI version]][PyPI] +[![Build Status]][Travis CI] +[![Coverage Status]][Coverage] + +A fast tunnel proxy that helps you bypass firewalls. + +Features: +- TCP & UDP support +- User management API +- TCP Fast Open +- Workers and graceful restart +- Destination IP blacklist + +Server +------ + +### Install + +Debian / Ubuntu: + + 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] + +Use GUI clients on your local PC/phones. Check the README of your client +for more information. + +Documentation +------------- + +You can find all the documentation in the [Wiki]. + +License +------- + +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] +* [Issue Tracker] +* [Mailing list] + + + +[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 +[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help +[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 +[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 new file mode 100755 index 0000000..bf2a3ec --- /dev/null +++ b/README.rst @@ -0,0 +1,113 @@ +shadowsocks +=========== + +|PyPI version| |Build Status| |Coverage Status| + +A fast tunnel proxy that helps you bypass firewalls. + +Server +------ + +Install +~~~~~~~ + +Debian / Ubuntu: + +:: + + 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 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 +------------- + +You can find all the documentation in the +`Wiki `__. + +License +------- + +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 `__ +- `Issue + 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/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/debian/changelog b/debian/changelog new file mode 100755 index 0000000..4e7ad16 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +shadowsocks (2.1.0-1) unstable; urgency=low + + * Initial release (Closes: #758900) + + -- Shell.Xu Sat, 23 Aug 2014 00:56:04 +0800 diff --git a/debian/compat b/debian/compat new file mode 100755 index 0000000..45a4fb7 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/config.json b/debian/config.json new file mode 100755 index 0000000..35cb14a --- /dev/null +++ b/debian/config.json @@ -0,0 +1,11 @@ +{ + "server":"my_server_ip", + "server_port":8388, + "local_address": "127.0.0.1", + "local_port":1080, + "password":"mypassword", + "timeout":300, + "method":"aes-256-cfb", + "fast_open": false, + "workers": 1 +} \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100755 index 0000000..da00920 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: shadowsocks +Section: python +Priority: extra +Maintainer: Shell.Xu +Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools +Standards-Version: 3.9.5 +Homepage: https://github.com/clowwindy/shadowsocks +Vcs-Git: git://github.com/shell909090/shadowsocks.git +Vcs-Browser: http://github.com/shell909090/shadowsocks + +Package: shadowsocks +Architecture: all +Pre-Depends: dpkg (>= 1.15.6~) +Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto +Description: Fast tunnel proxy that helps you bypass firewalls + A secure socks5 proxy, designed to protect your Internet traffic. + . + This package contain local and server part of shadowsocks, a fast, + powerful tunnel proxy to bypass firewalls. \ No newline at end of file diff --git a/debian/copyright b/debian/copyright new file mode 100755 index 0000000..7be8162 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,30 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: shadowsocks +Source: https://github.com/clowwindy/shadowsocks + +Files: debian/* +Copyright: 2014 Shell.Xu +License: Expat + +Files: * +Copyright: 2014 clowwindy +License: Expat + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian/docs b/debian/docs new file mode 100755 index 0000000..0208fc1 --- /dev/null +++ b/debian/docs @@ -0,0 +1,2 @@ +README.md +README.rst diff --git a/debian/init.d b/debian/init.d new file mode 100755 index 0000000..2f4f352 --- /dev/null +++ b/debian/init.d @@ -0,0 +1,149 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: shadowsocks +# Required-Start: $network $local_fs $remote_fs +# Required-Stop: $network $local_fs $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Fast tunnel proxy that helps you bypass firewalls +# Description: A secure socks5 proxy, designed to protect your Internet traffic. +# This package contain local and server part of shadowsocks, a fast, +# powerful tunnel proxy to bypass firewalls. +### END INIT INFO + +# Author: Shell.Xu + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC=shadowsocks # Introduce a short description here +NAME=shadowsocks # Introduce the short server's name here +DAEMON=/usr/bin/ssserver # Introduce the server's location here +DAEMON_ARGS="" # Arguments to run the daemon with +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME +LOGFILE=/var/log/$NAME.log + +# Exit if the package is not installed +[ -x $DAEMON ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ + --background --make-pidfile --chdir / --chuid $USERID --no-close --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ + --background --make-pidfile --chdir / --chuid $USERID --no-close -- \ + $DAEMON_ARGS $DAEMON_OPTS >> $LOGFILE 2>&1 \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/debian/install b/debian/install new file mode 100755 index 0000000..a614864 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +debian/config.json etc/shadowsocks/ \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..62e2bb6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,5 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +%: + dh $@ --with python2 --buildsystem=python_distutils diff --git a/debian/shadowsocks.default b/debian/shadowsocks.default new file mode 100755 index 0000000..a520602 --- /dev/null +++ b/debian/shadowsocks.default @@ -0,0 +1,12 @@ +# Defaults for shadowsocks initscript +# sourced by /etc/init.d/shadowsocks +# installed at /etc/default/shadowsocks by the maintainer scripts + +USERID="nobody" + +# +# This is a POSIX shell fragment +# + +# Additional options that are passed to the Daemon. +DAEMON_OPTS="-q -c /etc/shadowsocks/config.json" diff --git a/debian/shadowsocks.manpages b/debian/shadowsocks.manpages new file mode 100755 index 0000000..3df8a33 --- /dev/null +++ b/debian/shadowsocks.manpages @@ -0,0 +1,2 @@ +debian/sslocal.1 +debian/ssserver.1 \ No newline at end of file diff --git a/debian/source/format b/debian/source/format new file mode 100755 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/sslocal.1 b/debian/sslocal.1 new file mode 100755 index 0000000..0c2cf51 --- /dev/null +++ b/debian/sslocal.1 @@ -0,0 +1,59 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" (C) Copyright 2014 Shell.Xu , +.\" +.TH SHADOWSOCKS 1 "August 23, 2014" +.SH NAME +shadowsocks \- Fast tunnel proxy that helps you bypass firewalls +.SH SYNOPSIS +.B ssserver +.RI [ options ] +.br +.B sslocal +.RI [ options ] +.SH DESCRIPTION +shadowsocks is a tunnel proxy helps you bypass firewall. +.B ssserver +is the server part, and +.B sslocal +is the local part. +.SH OPTIONS +.TP +.B \-h, \-\-help +Show this help message and exit. +.TP +.B \-s SERVER_ADDR +Server address, default: 0.0.0.0. +.TP +.B \-p SERVER_PORT +Server port, default: 8388. +.TP +.B \-k PASSWORD +Password. +.TP +.B \-m METHOD +Encryption method, default: aes-256-cfb. +.TP +.B \-t TIMEOUT +Timeout in seconds, default: 300. +.TP +.B \-c CONFIG +Path to config file. +.TP +.B \-\-fast-open +Use TCP_FASTOPEN, requires Linux 3.7+. +.TP +.B \-\-workers WORKERS +Number of workers, available on Unix/Linux. +.TP +.B \-v, \-vv +Verbose mode. +.TP +.B \-q, \-qq +Quiet mode, only show warnings/errors. +.SH SEE ALSO +.br +The programs are documented fully by +.IR "Shell Xu " +and +.IR "Clowwindy ", +available via the Info system. diff --git a/debian/ssserver.1 b/debian/ssserver.1 new file mode 100755 index 0000000..0c2cf51 --- /dev/null +++ b/debian/ssserver.1 @@ -0,0 +1,59 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" (C) Copyright 2014 Shell.Xu , +.\" +.TH SHADOWSOCKS 1 "August 23, 2014" +.SH NAME +shadowsocks \- Fast tunnel proxy that helps you bypass firewalls +.SH SYNOPSIS +.B ssserver +.RI [ options ] +.br +.B sslocal +.RI [ options ] +.SH DESCRIPTION +shadowsocks is a tunnel proxy helps you bypass firewall. +.B ssserver +is the server part, and +.B sslocal +is the local part. +.SH OPTIONS +.TP +.B \-h, \-\-help +Show this help message and exit. +.TP +.B \-s SERVER_ADDR +Server address, default: 0.0.0.0. +.TP +.B \-p SERVER_PORT +Server port, default: 8388. +.TP +.B \-k PASSWORD +Password. +.TP +.B \-m METHOD +Encryption method, default: aes-256-cfb. +.TP +.B \-t TIMEOUT +Timeout in seconds, default: 300. +.TP +.B \-c CONFIG +Path to config file. +.TP +.B \-\-fast-open +Use TCP_FASTOPEN, requires Linux 3.7+. +.TP +.B \-\-workers WORKERS +Number of workers, available on Unix/Linux. +.TP +.B \-v, \-vv +Verbose mode. +.TP +.B \-q, \-qq +Quiet mode, only show warnings/errors. +.SH SEE ALSO +.br +The programs are documented fully by +.IR "Shell Xu " +and +.IR "Clowwindy ", +available via the Info system. diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e6a7ff7 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +import codecs +from setuptools import setup + + +with codecs.open('README.rst', encoding='utf-8') as f: + long_description = f.read() + +setup( + name="shadowsocks", + version="2.8.2", + 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/shadowsocks/shadowsocks', + packages=['shadowsocks', 'shadowsocks.crypto'], + package_data={ + 'shadowsocks': ['README.rst', 'LICENSE'] + }, + install_requires=[], + entry_points=""" + [console_scripts] + sslocal = shadowsocks.local:main + ssserver = shadowsocks.server:main + """, + classifiers=[ + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Internet :: Proxy Servers', + ], + long_description=long_description, +) diff --git a/shadowsocks/__init__.py b/shadowsocks/__init__.py new file mode 100755 index 0000000..dc3abd4 --- /dev/null +++ b/shadowsocks/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python +# +# Copyright 2012-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 diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py new file mode 100755 index 0000000..c5fc99d --- /dev/null +++ b/shadowsocks/asyncdns.py @@ -0,0 +1,481 @@ +#!/usr/bin/env 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 socket +import struct +import re +import logging + +from shadowsocks import common, lru_cache, eventloop, shell + + +CACHE_SWEEP_INTERVAL = 30 + +VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(? 63: + return None + results.append(common.chr(l)) + results.append(label) + results.append(b'\0') + return b''.join(results) + + +def build_request(address, qtype): + request_id = 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 request_id + header + addr + qtype_qclass + + +def parse_ip(addrtype, data, length, offset): + if addrtype == QTYPE_A: + return socket.inet_ntop(socket.AF_INET, data[offset:offset + length]) + elif addrtype == QTYPE_AAAA: + return socket.inet_ntop(socket.AF_INET6, data[offset:offset + length]) + elif addrtype in [QTYPE_CNAME, QTYPE_NS]: + return parse_name(data, offset)[1] + else: + return data[offset:offset + length] + + +def parse_name(data, offset): + p = offset + labels = [] + l = common.ord(data[p]) + while l > 0: + if (l & (128 + 64)) == (128 + 64): + # pointer + pointer = struct.unpack('!H', data[p:p + 2])[0] + pointer &= 0x3FFF + r = parse_name(data, pointer) + labels.append(r[1]) + p += 2 + # pointer is the end + return p - offset, b'.'.join(labels) + else: + labels.append(data[p + 1:p + 1 + l]) + p += 1 + l + l = common.ord(data[p]) + return p - offset + 1, b'.'.join(labels) + + +# rfc1035 +# record +# 1 1 1 1 1 1 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | | +# / / +# / NAME / +# | | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | TYPE | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | CLASS | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | TTL | +# | | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +# | RDLENGTH | +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| +# / RDATA / +# / / +# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +def parse_record(data, offset, question=False): + nlen, name = parse_name(data, offset) + if not question: + record_type, record_class, record_ttl, record_rdlength = struct.unpack( + '!HHiH', data[offset + nlen:offset + nlen + 10] + ) + ip = parse_ip(record_type, data, record_rdlength, offset + nlen + 10) + return nlen + 10 + record_rdlength, \ + (name, ip, record_type, record_class, record_ttl) + else: + record_type, record_class = struct.unpack( + '!HH', data[offset + nlen:offset + nlen + 4] + ) + return nlen + 4, (name, None, record_type, record_class, None, None) + + +def parse_header(data): + if len(data) >= 12: + header = struct.unpack('!HBBHHHH', data[:12]) + res_id = header[0] + res_qr = header[1] & 128 + res_tc = header[1] & 2 + res_ra = header[2] & 128 + res_rcode = header[2] & 15 + # assert res_tc == 0 + # assert res_rcode in [0, 3] + res_qdcount = header[3] + res_ancount = header[4] + res_nscount = header[5] + res_arcount = header[6] + return (res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, + res_ancount, res_nscount, res_arcount) + return None + + +def parse_response(data): + try: + if len(data) >= 12: + header = parse_header(data) + if not header: + return None + res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, \ + res_ancount, res_nscount, res_arcount = header + + qds = [] + ans = [] + offset = 12 + for i in range(0, res_qdcount): + l, r = parse_record(data, offset, True) + offset += l + if r: + qds.append(r) + for i in range(0, res_ancount): + l, r = parse_record(data, offset) + offset += l + if r: + ans.append(r) + for i in range(0, res_nscount): + l, r = parse_record(data, offset) + offset += l + for i in range(0, res_arcount): + l, r = parse_record(data, offset) + offset += l + response = DNSResponse() + if qds: + response.hostname = qds[0][0] + for an in qds: + response.questions.append((an[1], an[2], an[3])) + for an in ans: + response.answers.append((an[1], an[2], an[3])) + return response + except Exception as e: + shell.print_exception(e) + return None + + +def is_valid_hostname(hostname): + if len(hostname) > 255: + return False + if hostname[-1] == b'.': + hostname = hostname[:-1] + return all(VALID_HOSTNAME.match(x) for x in hostname.split(b'.')) + + +class DNSResponse(object): + def __init__(self): + self.hostname = None + self.questions = [] # each: (addr, type, class) + self.answers = [] # each: (addr, type, class) + + def __str__(self): + return '%s: %s' % (self.hostname, str(self.answers)) + + +STATUS_IPV4 = 0 +STATUS_IPV6 = 1 + + +class DNSResolver(object): + + def __init__(self): + self._loop = None + self._hosts = {} + self._hostname_status = {} + self._hostname_to_cb = {} + self._cb_to_hostname = {} + self._cache = lru_cache.LRUCache(timeout=300) + self._sock = None + self._servers = None + self._parse_resolv() + self._parse_hosts() + # TODO monitor hosts change and reload hosts + # TODO parse /etc/gai.conf and follow its rules + + def _parse_resolv(self): + self._servers = [] + try: + with open('/etc/resolv.conf', 'rb') as f: + content = f.readlines() + for line in content: + line = line.strip() + if line: + if line.startswith(b'nameserver'): + parts = line.split() + if len(parts) >= 2: + server = parts[1] + if common.is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append(server) + except IOError: + pass + if not self._servers: + self._servers = ['8.8.4.4', '8.8.8.8'] + + def _parse_hosts(self): + etc_path = '/etc/hosts' + if 'WINDIR' in os.environ: + etc_path = os.environ['WINDIR'] + '/system32/drivers/etc/hosts' + try: + with open(etc_path, 'rb') as f: + for line in f.readlines(): + line = line.strip() + parts = line.split() + if len(parts) >= 2: + ip = parts[0] + if common.is_ip(ip): + for i in range(1, len(parts)): + hostname = parts[i] + if hostname: + self._hosts[hostname] = ip + except IOError: + self._hosts['localhost'] = '127.0.0.1' + + def add_to_loop(self, loop): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + loop.add(self._sock, eventloop.POLL_IN, self) + loop.add_periodic(self.handle_periodic) + + def _call_callback(self, hostname, ip, error=None): + callbacks = self._hostname_to_cb.get(hostname, []) + for callback in callbacks: + if callback in self._cb_to_hostname: + del self._cb_to_hostname[callback] + if ip or error: + callback((hostname, ip), error) + else: + callback((hostname, None), + Exception('unknown hostname %s' % hostname)) + if hostname in self._hostname_to_cb: + del self._hostname_to_cb[hostname] + if hostname in self._hostname_status: + del self._hostname_status[hostname] + + def _handle_data(self, data): + response = parse_response(data) + if response and response.hostname: + hostname = response.hostname + ip = None + for answer in response.answers: + if answer[1] in (QTYPE_A, QTYPE_AAAA) and \ + answer[2] == QCLASS_IN: + ip = answer[0] + break + if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ + == STATUS_IPV4: + self._hostname_status[hostname] = STATUS_IPV6 + self._send_req(hostname, QTYPE_AAAA) + else: + if ip: + self._cache[hostname] = ip + self._call_callback(hostname, ip) + elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + for question in response.questions: + if question[1] == QTYPE_AAAA: + self._call_callback(hostname, None) + break + + def handle_event(self, sock, fd, event): + if sock != self._sock: + return + if event & eventloop.POLL_ERR: + logging.error('dns socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN, self) + else: + data, addr = sock.recvfrom(1024) + if addr[0] not in self._servers: + logging.warn('received a packet other than our dns') + return + self._handle_data(data) + + def handle_periodic(self): + self._cache.sweep() + + def remove_callback(self, callback): + hostname = self._cb_to_hostname.get(callback) + if hostname: + del self._cb_to_hostname[callback] + arr = self._hostname_to_cb.get(hostname, None) + if arr: + arr.remove(callback) + if not arr: + del self._hostname_to_cb[hostname] + if hostname in self._hostname_status: + del self._hostname_status[hostname] + + def _send_req(self, hostname, qtype): + req = build_request(hostname, qtype) + for server in self._servers: + logging.debug('resolving %s with type %d using server %s', + hostname, qtype, server) + self._sock.sendto(req, (server, 53)) + + def resolve(self, hostname, callback): + if type(hostname) != bytes: + hostname = hostname.encode('utf8') + if not hostname: + callback(None, Exception('empty hostname')) + elif common.is_ip(hostname): + callback((hostname, hostname), None) + elif hostname in self._hosts: + logging.debug('hit hosts: %s', hostname) + ip = self._hosts[hostname] + callback((hostname, ip), None) + elif hostname in self._cache: + logging.debug('hit cache: %s', hostname) + ip = self._cache[hostname] + callback((hostname, ip), None) + else: + if not is_valid_hostname(hostname): + callback(None, Exception('invalid hostname: %s' % hostname)) + return + arr = self._hostname_to_cb.get(hostname, None) + if not arr: + self._hostname_status[hostname] = STATUS_IPV4 + self._send_req(hostname, QTYPE_A) + self._hostname_to_cb[hostname] = [callback] + self._cb_to_hostname[callback] = hostname + else: + arr.append(callback) + # TODO send again only if waited too long + self._send_req(hostname, QTYPE_A) + + def close(self): + if self._sock: + if self._loop: + self._loop.remove_periodic(self.handle_periodic) + self._loop.remove(self._sock) + self._sock.close() + self._sock = None + + +def test(): + dns_resolver = DNSResolver() + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + + global counter + counter = 0 + + def make_callback(): + global counter + + def callback(result, error): + global counter + # TODO: what can we assert? + print(result, error) + counter += 1 + if counter == 9: + dns_resolver.close() + loop.stop() + a_callback = callback + return a_callback + + assert(make_callback() != make_callback()) + + dns_resolver.resolve(b'google.com', make_callback()) + dns_resolver.resolve('google.com', make_callback()) + dns_resolver.resolve('example.com', make_callback()) + dns_resolver.resolve('ipv6.google.com', make_callback()) + dns_resolver.resolve('www.facebook.com', make_callback()) + dns_resolver.resolve('ns2.google.com', make_callback()) + dns_resolver.resolve('invalid.@!#$%^&$@.hostname', make_callback()) + dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'long.hostname', make_callback()) + dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' + 'long.hostname', make_callback()) + + loop.run() + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/common.py b/shadowsocks/common.py new file mode 100755 index 0000000..db4beea --- /dev/null +++ b/shadowsocks/common.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013-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 socket +import struct +import logging + + +def compat_ord(s): + if type(s) == int: + return s + return _ord(s) + + +def compat_chr(d): + if bytes == str: + return _chr(d) + return bytes([d]) + + +_ord = ord +_chr = chr +ord = compat_ord +chr = compat_chr + + +def to_bytes(s): + if bytes != str: + if type(s) == str: + return s.encode('utf-8') + return s + + +def to_str(s): + if bytes != str: + if type(s) == bytes: + return s.decode('utf-8') + return s + + +def inet_ntop(family, ipstr): + if family == socket.AF_INET: + return to_bytes(socket.inet_ntoa(ipstr)) + elif family == socket.AF_INET6: + import re + v6addr = ':'.join(('%02X%02X' % (ord(i), ord(j))).lstrip('0') + for i, j in zip(ipstr[::2], ipstr[1::2])) + v6addr = re.sub('::+', '::', v6addr, count=1) + return to_bytes(v6addr) + + +def inet_pton(family, addr): + addr = to_str(addr) + if family == socket.AF_INET: + return socket.inet_aton(addr) + elif family == socket.AF_INET6: + if '.' in addr: # a v4 addr + v4addr = addr[addr.rindex(':') + 1:] + v4addr = socket.inet_aton(v4addr) + v4addr = map(lambda x: ('%02X' % ord(x)), v4addr) + v4addr.insert(2, ':') + newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr) + return inet_pton(family, newaddr) + dbyts = [0] * 8 # 8 groups + grps = addr.split(':') + for i, v in enumerate(grps): + if v: + dbyts[i] = int(v, 16) + else: + for j, w in enumerate(grps[::-1]): + if w: + dbyts[7 - j] = int(w, 16) + else: + break + break + return b''.join((chr(i // 256) + chr(i % 256)) for i in dbyts) + else: + raise RuntimeError("What family?") + + +def 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 + + if not hasattr(socket, 'inet_ntop'): + socket.inet_ntop = inet_ntop + + +patch_socket() + + +ADDRTYPE_IPV4 = 1 +ADDRTYPE_IPV6 = 4 +ADDRTYPE_HOST = 3 + + +def pack_addr(address): + address_str = to_str(address) + for family in (socket.AF_INET, socket.AF_INET6): + try: + r = socket.inet_pton(family, address_str) + if family == socket.AF_INET6: + return b'\x04' + r + else: + return b'\x01' + r + except (TypeError, ValueError, OSError, IOError): + pass + if len(address) > 255: + address = address[:255] # TODO + return b'\x03' + chr(len(address)) + address + + +def parse_header(data): + addrtype = ord(data[0]) + dest_addr = None + dest_port = None + header_length = 0 + if addrtype == ADDRTYPE_IPV4: + if len(data) >= 7: + dest_addr = socket.inet_ntoa(data[1:5]) + dest_port = struct.unpack('>H', data[5:7])[0] + header_length = 7 + else: + logging.warn('header is too short') + elif addrtype == ADDRTYPE_HOST: + if len(data) > 2: + addrlen = ord(data[1]) + if len(data) >= 2 + addrlen: + dest_addr = data[2:2 + addrlen] + dest_port = struct.unpack('>H', data[2 + addrlen:4 + + addrlen])[0] + header_length = 4 + addrlen + else: + logging.warn('header is too short') + else: + logging.warn('header is too short') + elif addrtype == ADDRTYPE_IPV6: + if len(data) >= 19: + dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) + dest_port = struct.unpack('>H', data[17:19])[0] + header_length = 19 + else: + logging.warn('header is too short') + else: + logging.warn('unsupported addrtype %d, maybe wrong password or ' + 'encryption method' % addrtype) + if dest_addr is None: + return None + 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) + assert inet_ntop(socket.AF_INET, b) == ipv4 + ipv6 = b'2404:6800:4005:805::1011' + b = inet_pton(socket.AF_INET6, ipv6) + assert inet_ntop(socket.AF_INET6, b) == ipv6 + + +def test_parse_header(): + assert parse_header(b'\x03\x0ewww.google.com\x00\x50') == \ + (3, b'www.google.com', 80, 18) + assert parse_header(b'\x01\x08\x08\x08\x08\x00\x35') == \ + (1, b'8.8.8.8', 53, 7) + assert parse_header((b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00' + b'\x00\x10\x11\x00\x50')) == \ + (4, b'2404:6800:4005:805::1011', 80, 19) + + +def test_pack_header(): + assert pack_addr(b'8.8.8.8') == b'\x01\x08\x08\x08\x08' + assert pack_addr(b'2404:6800:4005:805::1011') == \ + b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00\x00\x10\x11' + assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com' + + +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 new file mode 100755 index 0000000..401c7b7 --- /dev/null +++ b/shadowsocks/crypto/__init__.py @@ -0,0 +1,18 @@ +#!/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py new file mode 100755 index 0000000..3775b6c --- /dev/null +++ b/shadowsocks/crypto/openssl.py @@ -0,0 +1,181 @@ +#!/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +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 +loaded = False + +buf_size = 2048 + + +def load_openssl(): + global loaded, libcrypto, buf + + libcrypto = util.find_library(('crypto', 'eay32'), + 'EVP_get_cipherbyname', + 'libcrypto') + if libcrypto is None: + raise Exception('libcrypto(OpenSSL) not found') + + libcrypto.EVP_get_cipherbyname.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p + + libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p, c_char_p, c_int) + + libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, + c_char_p, c_int) + + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): + libcrypto.OpenSSL_add_all_ciphers() + + buf = create_string_buffer(buf_size) + loaded = True + + +def load_cipher(cipher_name): + func_name = 'EVP_' + cipher_name.replace('-', '_') + if bytes != str: + func_name = str(func_name, 'utf-8') + cipher = getattr(libcrypto, func_name, None) + if cipher: + cipher.restype = c_void_p + return cipher() + return None + + +class OpenSSLCrypto(object): + def __init__(self, cipher_name, key, iv, op): + self._ctx = None + if not loaded: + load_openssl() + cipher_name = common.to_bytes(cipher_name) + cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + if not cipher: + cipher = load_cipher(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libcrypto' % cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + self._ctx = libcrypto.EVP_CIPHER_CTX_new() + if not self._ctx: + raise Exception('can not create cipher context') + r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def update(self, data): + global buf_size, buf + cipher_out_len = c_long(0) + l = len(data) + if buf_size < l: + buf_size = l * 2 + buf = create_string_buffer(buf_size) + libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l) + # buf is copied to a str object when we access buf.raw + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + libcrypto.EVP_CIPHER_CTX_free(self._ctx) + + +ciphers = { + '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): + + 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('aes-128-cfb') + + +def test_aes_256_cfb(): + run_method('aes-256-cfb') + + +def test_aes_128_cfb8(): + run_method('aes-128-cfb8') + + +def test_aes_256_ofb(): + run_method('aes-256-ofb') + + +def test_aes_256_ctr(): + run_method('aes-256-ctr') + + +def test_bf_cfb(): + run_method('bf-cfb') + + +def test_rc4(): + run_method('rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py new file mode 100755 index 0000000..1f07a82 --- /dev/null +++ b/shadowsocks/crypto/rc4_md5.py @@ -0,0 +1,51 @@ +#!/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import hashlib + +from shadowsocks.crypto import openssl + +__all__ = ['ciphers'] + + +def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, + i=1, padding=1): + md5 = hashlib.md5() + md5.update(key) + md5.update(iv) + rc4_key = md5.digest() + return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) + + +ciphers = { + 'rc4-md5': (16, 16, create_cipher), +} + + +def test(): + from shadowsocks.crypto import util + + 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) + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py new file mode 100755 index 0000000..ae86fef --- /dev/null +++ b/shadowsocks/crypto/sodium.py @@ -0,0 +1,120 @@ +#!/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +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 +loaded = False + +buf_size = 2048 + +# for salsa20 and chacha20 +BLOCK_SIZE = 64 + + +def load_libsodium(): + global loaded, libsodium, buf + + libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', + 'libsodium') + if libsodium is None: + raise Exception('libsodium not found') + + libsodium.crypto_stream_salsa20_xor_ic.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + libsodium.crypto_stream_chacha20_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + + buf = create_string_buffer(buf_size) + loaded = True + + +class SodiumCrypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_libsodium() + self.key = key + self.iv = iv + self.key_ptr = c_char_p(key) + self.iv_ptr = c_char_p(iv) + if cipher_name == 'salsa20': + self.cipher = libsodium.crypto_stream_salsa20_xor_ic + elif cipher_name == 'chacha20': + self.cipher = libsodium.crypto_stream_chacha20_xor_ic + else: + raise Exception('Unknown cipher') + # byte counter, not block counter + self.counter = 0 + + def update(self, data): + global buf_size, buf + l = len(data) + + # we can only prepend some padding to make the encryption align to + # blocks + padding = self.counter % BLOCK_SIZE + if buf_size < padding + l: + buf_size = (padding + l) * 2 + buf = create_string_buffer(buf_size) + + if padding: + data = (b'\0' * padding) + data + self.cipher(byref(buf), c_char_p(data), padding + l, + self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) + self.counter += l + # buf is copied to a str object when we access buf.raw + # strip off the padding + return buf.raw[padding:padding + l] + + +ciphers = { + 'salsa20': (32, 8, SodiumCrypto), + 'chacha20': (32, 8, SodiumCrypto), +} + + +def test_salsa20(): + 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(): + + 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) + + +if __name__ == '__main__': + test_chacha20() + test_salsa20() diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py new file mode 100755 index 0000000..bc693f5 --- /dev/null +++ b/shadowsocks/crypto/table.py @@ -0,0 +1,174 @@ +# !/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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import string +import struct +import hashlib + + +__all__ = ['ciphers'] + +cached_tables = {} + +if hasattr(string, 'maketrans'): + maketrans = string.maketrans + translate = string.translate +else: + maketrans = bytes.maketrans + translate = bytes.translate + + +def get_table(key): + m = hashlib.md5() + m.update(key) + s = m.digest() + a, b = struct.unpack(' 0: + # parent waits for its child + time.sleep(5) + sys.exit(0) + + # child signals its parent to exit + ppid = os.getppid() + pid = os.getpid() + if write_pid_file(pid_file, pid) != 0: + os.kill(ppid, signal.SIGINT) + sys.exit(1) + + os.setsid() + signal.signal(signal.SIG_IGN, signal.SIGHUP) + + print('started') + os.kill(ppid, signal.SIGTERM) + + sys.stdin.close() + try: + freopen(log_file, 'a', sys.stdout) + freopen(log_file, 'a', sys.stderr) + except IOError as e: + 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 new file mode 100755 index 0000000..4e87f41 --- /dev/null +++ b/shadowsocks/encrypt.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# +# Copyright 2012-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 hashlib +import logging + +from shadowsocks import common +from shadowsocks.crypto import rc4_md5, openssl, sodium, table + + +method_supported = {} +method_supported.update(rc4_md5.ciphers) +method_supported.update(openssl.ciphers) +method_supported.update(sodium.ciphers) +method_supported.update(table.ciphers) + + +def random_string(length): + return os.urandom(length) + + +cached_keys = {} + + +def try_cipher(key, method=None): + Encryptor(key, method) + + +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 + cached_key = '%s-%d-%d' % (password, key_len, iv_len) + r = cached_keys.get(cached_key, None) + if r: + return r + m = [] + i = 0 + while len(b''.join(m)) < (key_len + iv_len): + md5 = hashlib.md5() + data = password + if i > 0: + data = m[i - 1] + password + md5.update(data) + m.append(md5.digest()) + i += 1 + ms = b''.join(m) + key = ms[:key_len] + iv = ms[key_len:key_len + iv_len] + cached_keys[cached_key] = (key, iv) + return key, iv + + +class Encryptor(object): + def __init__(self, key, method): + self.key = key + self.method = method + self.iv = None + self.iv_sent = False + self.cipher_iv = b'' + self.decipher = None + method = method.lower() + self._method_info = self.get_method_info(method) + if self._method_info: + self.cipher = self.get_cipher(key, method, 1, + random_string(self._method_info[1])) + else: + logging.error('method %s not supported' % method) + sys.exit(1) + + def get_method_info(self, method): + method = method.lower() + m = method_supported.get(method) + return m + + def iv_len(self): + return len(self.cipher_iv) + + def get_cipher(self, password, method, op, iv): + password = common.to_bytes(password) + m = self._method_info + if m[0] > 0: + key, iv_ = EVP_BytesToKey(password, m[0], m[1]) + else: + # key_length == 0 indicates we should use the key directly + key, iv = password, b'' + + iv = iv[:m[1]] + if op == 1: + # this iv is for cipher not decipher + self.cipher_iv = iv[:m[1]] + return m[2](method, key, iv, op) + + def encrypt(self, buf): + if len(buf) == 0: + return buf + if self.iv_sent: + return self.cipher.update(buf) + else: + self.iv_sent = True + return self.cipher_iv + self.cipher.update(buf) + + def decrypt(self, buf): + if len(buf) == 0: + return buf + if self.decipher is None: + decipher_iv_len = self._method_info[1] + decipher_iv = buf[:decipher_iv_len] + self.decipher = self.get_cipher(self.key, self.method, 0, + iv=decipher_iv) + buf = buf[decipher_iv_len:] + if len(buf) == 0: + return buf + return self.decipher.update(buf) + + +def encrypt_all(password, method, op, data): + result = [] + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + if key_len > 0: + key, _ = EVP_BytesToKey(password, key_len, iv_len) + else: + key = password + if op: + iv = random_string(iv_len) + result.append(iv) + else: + iv = data[:iv_len] + data = data[iv_len:] + cipher = m(method, key, iv, op) + result.append(cipher.update(data)) + return b''.join(result) + + +CIPHERS_TO_TEST = [ + 'aes-128-cfb', + 'aes-256-cfb', + 'rc4-md5', + 'salsa20', + 'chacha20', + 'table', +] + + +def test_encryptor(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + encryptor = Encryptor(b'key', method) + decryptor = Encryptor(b'key', method) + cipher = encryptor.encrypt(plain) + plain2 = decryptor.decrypt(cipher) + assert plain == plain2 + + +def test_encrypt_all(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + cipher = encrypt_all(b'key', method, 1, plain) + plain2 = encrypt_all(b'key', method, 0, cipher) + assert plain == plain2 + + +if __name__ == '__main__': + test_encrypt_all() + test_encryptor() diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py new file mode 100755 index 0000000..78b532c --- /dev/null +++ b/shadowsocks/eventloop.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013-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 ssloop +# https://github.com/clowwindy/ssloop + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import time +import socket +import select +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'] + +POLL_NULL = 0x00 +POLL_IN = 0x01 +POLL_OUT = 0x04 +POLL_ERR = 0x08 +POLL_HUP = 0x10 +POLL_NVAL = 0x20 + + +EVENT_NAMES = { + POLL_NULL: 'POLL_NULL', + POLL_IN: 'POLL_IN', + POLL_OUT: 'POLL_OUT', + POLL_ERR: 'POLL_ERR', + POLL_HUP: 'POLL_HUP', + POLL_NVAL: 'POLL_NVAL', +} + +# we check timeouts every TIMEOUT_PRECISION seconds +TIMEOUT_PRECISION = 10 + + +class KqueueLoop(object): + + MAX_EVENTS = 1024 + + def __init__(self): + self._kqueue = select.kqueue() + self._fds = {} + + def _control(self, fd, mode, flags): + events = [] + if mode & POLL_IN: + events.append(select.kevent(fd, select.KQ_FILTER_READ, flags)) + if mode & POLL_OUT: + events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags)) + for e in events: + self._kqueue.control([e], 0) + + def poll(self, timeout): + if timeout < 0: + timeout = None # kqueue behaviour + events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout) + results = defaultdict(lambda: POLL_NULL) + for e in events: + fd = e.ident + if e.filter == select.KQ_FILTER_READ: + results[fd] |= POLL_IN + elif e.filter == select.KQ_FILTER_WRITE: + results[fd] |= POLL_OUT + return results.items() + + def register(self, fd, mode): + self._fds[fd] = mode + self._control(fd, mode, select.KQ_EV_ADD) + + def unregister(self, fd): + self._control(fd, self._fds[fd], select.KQ_EV_DELETE) + del self._fds[fd] + + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + self._kqueue.close() + + +class SelectLoop(object): + + def __init__(self): + self._r_list = set() + self._w_list = set() + self._x_list = set() + + def poll(self, timeout): + r, w, x = select.select(self._r_list, self._w_list, self._x_list, + timeout) + results = defaultdict(lambda: POLL_NULL) + for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]: + for fd in p[0]: + results[fd] |= p[1] + return results.items() + + def register(self, fd, mode): + if mode & POLL_IN: + self._r_list.add(fd) + if mode & POLL_OUT: + self._w_list.add(fd) + if mode & POLL_ERR: + self._x_list.add(fd) + + def unregister(self, fd): + if fd in self._r_list: + self._r_list.remove(fd) + if fd in self._w_list: + self._w_list.remove(fd) + if fd in self._x_list: + self._x_list.remove(fd) + + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + pass + + +class EventLoop(object): + def __init__(self): + if hasattr(select, 'epoll'): + self._impl = select.epoll() + model = 'epoll' + elif hasattr(select, 'kqueue'): + self._impl = KqueueLoop() + model = 'kqueue' + elif hasattr(select, 'select'): + self._impl = SelectLoop() + model = 'select' + else: + raise Exception('can not find any available functions in select ' + 'package') + self._fdmap = {} # (f, handler) + self._last_time = time.time() + self._periodic_callbacks = [] + self._stopping = False + logging.debug('using event model: %s', model) + + def poll(self, timeout=None): + events = self._impl.poll(timeout) + return [(self._fdmap[fd][0], fd, event) for fd, event in events] + + def add(self, f, mode, handler): + fd = f.fileno() + self._fdmap[fd] = (f, handler) + self._impl.register(fd, mode) + + def remove(self, f): + fd = f.fileno() + del self._fdmap[fd] + self._impl.unregister(fd) + + def add_periodic(self, callback): + self._periodic_callbacks.append(callback) + + def remove_periodic(self, callback): + self._periodic_callbacks.remove(callback) + + def modify(self, f, mode): + fd = f.fileno() + self._impl.modify(fd, mode) + + def stop(self): + self._stopping = True + + def run(self): + events = [] + while not self._stopping: + asap = False + try: + events = self.poll(TIMEOUT_PRECISION) + except (OSError, IOError) as e: + if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): + # EPIPE: Happens when the client closes the connection + # EINTR: Happens when received a signal + # handles them as soon as possible + asap = True + logging.debug('poll:%s', e) + else: + logging.error('poll:%s', e) + import traceback + traceback.print_exc() + continue + + for sock, fd, event in events: + handler = self._fdmap.get(fd, None) + if handler is not None: + handler = handler[1] + try: + handler.handle_event(sock, fd, event) + except (OSError, IOError) as e: + shell.print_exception(e) + now = time.time() + if asap or now - self._last_time >= TIMEOUT_PRECISION: + for callback in self._periodic_callbacks: + callback() + self._last_time = now + + def __del__(self): + self._impl.close() + + +# from tornado +def errno_from_exception(e): + """Provides the errno from an Exception object. + + There are cases that the errno attribute was not set so we pull + the errno out of the args but if someone instatiates an Exception + without any args you will get a tuple error. So this function + abstracts all that behavior to give you a safe way to get the + errno. + """ + + if hasattr(e, 'errno'): + return e.errno + elif e.args: + return e.args[0] + else: + return None + + +# from tornado +def get_sock_error(sock): + error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + return socket.error(error_number, os.strerror(error_number)) diff --git a/shadowsocks/local.py b/shadowsocks/local.py new file mode 100755 index 0000000..4255a2e --- /dev/null +++ b/shadowsocks/local.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2012-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 sys +import os +import logging +import signal + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns + + +def main(): + shell.check_python() + + # fix py2exe + if hasattr(sys, "frozen") and sys.frozen in \ + ("windows_exe", "console_exe"): + p = os.path.dirname(os.path.abspath(sys.executable)) + os.chdir(p) + + config = shell.get_config(True) + + daemon.daemon_exec(config) + + try: + logging.info("starting local at %s:%d" % + (config['local_address'], config['local_port'])) + + dns_resolver = asyncdns.DNSResolver() + tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) + udp_server = udprelay.UDPRelay(config, dns_resolver, True) + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + tcp_server.add_to_loop(loop) + udp_server.add_to_loop(loop) + + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tcp_server.close(next_tick=True) + udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + + def int_handler(signum, _): + sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + daemon.set_user(config.get('user', None)) + loop.run() + 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 new file mode 100755 index 0000000..401f19b --- /dev/null +++ b/shadowsocks/lru_cache.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import collections +import logging +import time + + +# this LRUCache is optimized for concurrency, not QPS +# n: concurrency, keys stored in the cache +# m: visits not timed out, proportional to QPS * timeout +# get & set is O(1), not O(n). thus we can support very large n +# TODO: if timeout or QPS is too large, then this cache is not very efficient, +# as sweep() causes long pause + + +class LRUCache(collections.MutableMapping): + """This class is not thread safe""" + + def __init__(self, timeout=60, close_callback=None, *args, **kwargs): + self.timeout = timeout + self.close_callback = close_callback + self._store = {} + self._time_to_keys = collections.defaultdict(list) + self._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): + # O(1) + t = time.time() + self._keys_to_last_time[key] = t + self._time_to_keys[t].append(key) + self._last_visits.append(t) + return self._store[key] + + def __setitem__(self, key, value): + # O(1) + t = time.time() + self._keys_to_last_time[key] = t + self._store[key] = value + self._time_to_keys[t].append(key) + self._last_visits.append(t) + + def __delitem__(self, key): + # O(1) + del self._store[key] + del self._keys_to_last_time[key] + + def __iter__(self): + return iter(self._store) + + def __len__(self): + return len(self._store) + + def sweep(self): + # O(m) + now = time.time() + c = 0 + while len(self._last_visits) > 0: + least = self._last_visits[0] + if now - least <= self.timeout: + break + if self.close_callback is not None: + for key in self._time_to_keys[least]: + if key in self._store: + if now - self._keys_to_last_time[key] > self.timeout: + value = self._store[key] + 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: + if now - self._keys_to_last_time[key] > self.timeout: + del self._store[key] + del self._keys_to_last_time[key] + c += 1 + del self._time_to_keys[least] + if c: + self._closed_values.clear() + logging.debug('%d keys swept' % c) + + +def test(): + c = LRUCache(timeout=0.3) + + c['a'] = 1 + assert c['a'] == 1 + + time.sleep(0.5) + c.sweep() + assert 'a' not in c + + c['a'] = 2 + c['b'] = 3 + time.sleep(0.2) + c.sweep() + assert c['a'] == 2 + assert c['b'] == 3 + + time.sleep(0.2) + c.sweep() + c['b'] + time.sleep(0.2) + c.sweep() + assert 'a' not in c + assert c['b'] == 3 + + time.sleep(0.5) + c.sweep() + assert 'a' not in c + assert 'b' not in c + + 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/manager.py b/shadowsocks/manager.py new file mode 100755 index 0000000..e8009b4 --- /dev/null +++ b/shadowsocks/manager.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import errno +import traceback +import socket +import logging +import json +import collections + +from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell + + +BUF_SIZE = 1506 +STAT_SEND_LIMIT = 100 + + +class Manager(object): + + def __init__(self, config): + self._config = config + self._relays = {} # (tcprelay, udprelay) + self._loop = eventloop.EventLoop() + self._dns_resolver = asyncdns.DNSResolver() + self._dns_resolver.add_to_loop(self._loop) + + self._statistics = collections.defaultdict(int) + self._control_client_addr = None + try: + manager_address = config['manager_address'] + if ':' in manager_address: + addr = manager_address.rsplit(':', 1) + addr = addr[0], int(addr[1]) + addrs = socket.getaddrinfo(addr[0], addr[1]) + if addrs: + family = addrs[0][0] + else: + logging.error('invalid address: %s', manager_address) + exit(1) + else: + addr = manager_address + family = socket.AF_UNIX + self._control_socket = socket.socket(family, + socket.SOCK_DGRAM) + self._control_socket.bind(addr) + self._control_socket.setblocking(False) + except (OSError, IOError) as e: + logging.error(e) + logging.error('can not bind to manager address') + exit(1) + self._loop.add(self._control_socket, + eventloop.POLL_IN, self) + self._loop.add_periodic(self.handle_periodic) + + port_password = config['port_password'] + del config['port_password'] + for port, password in port_password.items(): + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password + self.add_port(a_config) + + def add_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.error("server already exists at %s:%d" % (config['server'], + port)) + return + logging.info("adding server at %s:%d" % (config['server'], port)) + t = tcprelay.TCPRelay(config, self._dns_resolver, False, + self.stat_callback) + u = udprelay.UDPRelay(config, self._dns_resolver, False, + self.stat_callback) + t.add_to_loop(self._loop) + u.add_to_loop(self._loop) + self._relays[port] = (t, u) + + def remove_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.info("removing server at %s:%d" % (config['server'], port)) + t, u = servers + t.close(next_tick=False) + u.close(next_tick=False) + del self._relays[port] + else: + logging.error("server not exist at %s:%d" % (config['server'], + port)) + + def handle_event(self, sock, fd, event): + if sock == self._control_socket and event == eventloop.POLL_IN: + data, self._control_client_addr = sock.recvfrom(BUF_SIZE) + parsed = self._parse_command(data) + if parsed: + command, config = parsed + a_config = self._config.copy() + if config: + # let the command override the configuration file + a_config.update(config) + if 'server_port' not in a_config: + logging.error('can not find server_port in config') + else: + if command == 'add': + self.add_port(a_config) + self._send_control_data(b'ok') + elif command == 'remove': + self.remove_port(a_config) + self._send_control_data(b'ok') + elif command == 'ping': + self._send_control_data(b'pong') + else: + logging.error('unknown command %s', command) + + def _parse_command(self, data): + # commands: + # add: {"server_port": 8000, "password": "foobar"} + # remove: {"server_port": 8000"} + data = common.to_str(data) + parts = data.split(':', 1) + if len(parts) < 2: + return data, None + command, config_json = parts + try: + config = shell.parse_json_in_str(config_json) + return command, config + except Exception as e: + logging.error(e) + return None + + def stat_callback(self, port, data_len): + self._statistics[port] += data_len + + def handle_periodic(self): + r = {} + i = 0 + + def send_data(data_dict): + if data_dict: + # use compact JSON format (without space) + data = common.to_bytes(json.dumps(data_dict, + separators=(',', ':'))) + self._send_control_data(b'stat: ' + data) + + for k, v in self._statistics.items(): + r[k] = v + i += 1 + # split the data into segments that fit in UDP packets + if i >= STAT_SEND_LIMIT: + send_data(r) + r.clear() + send_data(r) + self._statistics.clear() + + def _send_control_data(self, data): + if self._control_client_addr: + try: + self._control_socket.sendto(data, self._control_client_addr) + except (socket.error, OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + + def run(self): + self._loop.run() + + +def run(config): + Manager(config).run() + + +def test(): + import time + import threading + import struct + from shadowsocks import encrypt + + logging.basicConfig(level=5, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + enc = [] + eventloop.TIMEOUT_PRECISION = 1 + + def run_server(): + config = { + 'server': '127.0.0.1', + 'local_port': 1081, + 'port_password': { + '8381': 'foobar1', + '8382': 'foobar2' + }, + 'method': 'aes-256-cfb', + 'manager_address': '127.0.0.1:6001', + 'timeout': 60, + 'fast_open': False, + 'verbose': 2 + } + manager = Manager(config) + enc.append(manager) + manager.run() + + t = threading.Thread(target=run_server) + t.start() + time.sleep(1) + manager = enc[0] + cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cli.connect(('127.0.0.1', 6001)) + + # test add and remove + time.sleep(1) + cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') + time.sleep(1) + assert 7001 in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + + cli.send(b'remove: {"server_port":8381}') + time.sleep(1) + assert 8381 not in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + logging.info('add and remove test passed') + + # test statistics for TCP + header = common.pack_addr(b'google.com') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, + header + b'GET /\r\n\r\n') + tcp_cli = socket.socket() + tcp_cli.connect(('127.0.0.1', 7001)) + tcp_cli.send(data) + tcp_cli.recv(4096) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = shell.parse_json_in_str(data) + assert '7001' in stats + logging.info('TCP statistics test passed') + + # test statistics for UDP + header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, + header + b'test') + udp_cli = socket.socket(type=socket.SOCK_DGRAM) + udp_cli.sendto(data, ('127.0.0.1', 8382)) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = json.loads(data) + assert '8382' in stats + logging.info('UDP statistics test passed') + + manager._loop.stop() + t.join() + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/server.py b/shadowsocks/server.py new file mode 100755 index 0000000..e25db4c --- /dev/null +++ b/shadowsocks/server.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os +import logging +import signal + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ + asyncdns, manager + + +def main(): + shell.check_python() + + config = shell.get_config(False) + + daemon.daemon_exec(config) + + if config['port_password']: + if config['password']: + logging.warn('warning: port_password should not be used with ' + 'server_port and password. server_port and password ' + 'will be ignored') + else: + config['port_password'] = {} + server_port = config['server_port'] + if type(server_port) == list: + for a_server_port in server_port: + config['port_password'][a_server_port] = config['password'] + else: + config['port_password'][str(server_port)] = config['password'] + + if config.get('manager_address', 0): + logging.info('entering manager mode') + manager.run(config) + return + + tcp_servers = [] + udp_servers = [] + dns_resolver = asyncdns.DNSResolver() + port_password = config['port_password'] + del config['port_password'] + for port, password in port_password.items(): + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password + logging.info("starting server at %s:%d" % + (a_config['server'], int(port))) + tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False)) + udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False)) + + def run_server(): + def child_handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + list(map(lambda s: s.close(next_tick=True), + tcp_servers + udp_servers)) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), + child_handler) + + 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 Exception as e: + shell.print_exception(e) + sys.exit(1) + + if int(config['workers']) > 1: + if os.name == 'posix': + children = [] + is_child = False + for i in range(0, int(config['workers'])): + r = os.fork() + if r == 0: + logging.info('worker started') + is_child = True + run_server() + break + else: + children.append(r) + if not is_child: + def handler(signum, _): + for pid in children: + try: + os.kill(pid, signum) + os.waitpid(pid, 0) + except OSError: # child may already exited + pass + 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: + a_tcp_server.close() + for a_udp_server in udp_servers: + a_udp_server.close() + dns_resolver.close() + + for child in children: + os.waitpid(child, 0) + else: + logging.warn('worker is only available on Unix/Linux') + run_server() + else: + run_server() + + +if __name__ == '__main__': + main() diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py new file mode 100755 index 0000000..c91fc22 --- /dev/null +++ b/shadowsocks/shell.py @@ -0,0 +1,365 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import json +import sys +import getopt +import logging +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 + if info[0] == 2 and not info[1] >= 6: + print('Python 2.6+ required') + sys.exit(1) + elif info[0] == 3 and not info[1] >= 3: + print('Python 3.3+ required') + sys.exit(1) + elif info[0] not in [2, 3]: + print('Python version not supported') + sys.exit(1) + + +def print_exception(e): + global verbose + logging.error(e) + if verbose > 0: + import traceback + traceback.print_exc() + + +def print_shadowsocks(): + version = '' + try: + import pkg_resources + version = pkg_resources.get_distribution('shadowsocks').version + except Exception: + pass + print('Shadowsocks %s' % version) + + +def find_config(): + config_path = 'config.json' + if os.path.exists(config_path): + return config_path + config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json') + if os.path.exists(config_path): + return config_path + return None + + +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 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() == 'rc4': + logging.warn('warning: RC4 is not safe; please use a safer cipher, ' + 'like AES-256-CFB') + if config.get('timeout', 300) < 100: + logging.warn('warning: your timeout %d seems too short' % + int(config.get('timeout'))) + if config.get('timeout', 300) > 600: + logging.warn('warning: your timeout %d seems too long' % + int(config.get('timeout'))) + if config.get('password') in [b'mypassword']: + logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' + 'config.json!') + 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 = 'hd:s:b:p:k:l:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', + 'version'] + else: + shortopts = 'hd:s:p:k:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', + 'forbidden-ip=', 'user=', 'manager-address=', 'version'] + try: + config_path = find_config() + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + for key, value in optlist: + if key == '-c': + config_path = value + + if config_path: + logging.info('loading config from %s' % config_path) + with open(config_path, 'rb') as f: + try: + config = parse_json_in_str(f.read().decode('utf8')) + except ValueError as e: + logging.error('found an error in config.json: %s', + e.message) + sys.exit(1) + else: + config = {} + + v_count = 0 + for key, value in optlist: + if key == '-p': + config['server_port'] = int(value) + elif key == '-k': + config['password'] = to_bytes(value) + elif key == '-l': + config['local_port'] = int(value) + elif key == '-s': + config['server'] = to_str(value) + elif key == '-m': + config['method'] = to_str(value) + elif key == '-b': + config['local_address'] = to_str(value) + elif key == '-v': + v_count += 1 + # '-vv' turns on more verbose mode + config['verbose'] = v_count + elif key == '-t': + config['timeout'] = int(value) + elif key == '--fast-open': + config['fast_open'] = True + elif key == '--workers': + config['workers'] = int(value) + elif key == '--manager-address': + config['manager_address'] = value + 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 + except getopt.GetoptError as e: + print(e, file=sys.stderr) + print_help(is_local) + sys.exit(2) + + if not config: + logging.error('config not specified') + print_help(is_local) + sys.exit(2) + + config['password'] = 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'] = 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'] = 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) + + logging.getLogger('').handlers = [] + logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') + if config['verbose'] >= 2: + level = VERBOSE_LEVEL + elif config['verbose'] == 1: + level = logging.DEBUG + elif config['verbose'] == -1: + level = logging.WARN + elif config['verbose'] <= -2: + level = logging.ERROR + else: + level = logging.INFO + verbose = config['verbose'] + logging.basicConfig(level=level, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + + check_config(config, is_local) + + return config + + +def print_help(is_local): + if is_local: + print_local_help() + else: + print_server_help() + + +def print_local_help(): + print('''usage: sslocal [OPTION]... +A fast tunnel proxy that helps you bypass firewalls. + +You can supply configurations via either config file or command line arguments. + +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 [OPTION]... +A fast tunnel proxy that helps you bypass firewalls. + +You can supply configurations via either config file or command line arguments. + +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 + --manager-address ADDR optional server manager UDP address, see wiki + +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 _decode_list(data): + rv = [] + for item in data: + if hasattr(item, 'encode'): + item = item.encode('utf-8') + elif isinstance(item, list): + item = _decode_list(item) + elif isinstance(item, dict): + item = _decode_dict(item) + rv.append(item) + return rv + + +def _decode_dict(data): + rv = {} + for key, value in data.items(): + if hasattr(value, 'encode'): + value = value.encode('utf-8') + elif isinstance(value, list): + value = _decode_list(value) + elif isinstance(value, dict): + value = _decode_dict(value) + rv[key] = value + return rv + + +def parse_json_in_str(data): + # parse json and convert everything from unicode to str + return json.loads(data, object_hook=_decode_dict) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py new file mode 100755 index 0000000..d11af31 --- /dev/null +++ b/shadowsocks/tcprelay.py @@ -0,0 +1,716 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import time +import socket +import errno +import struct +import logging +import traceback +import random + +from shadowsocks import encrypt, eventloop, shell, common +from shadowsocks.common import parse_header + +# we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time +TIMEOUTS_CLEAN_SIZE = 512 + +MSG_FASTOPEN = 0x20000000 + +# SOCKS command definition +CMD_CONNECT = 1 +CMD_BIND = 2 +CMD_UDP_ASSOCIATE = 3 + +# 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, it could be at one of several stages: + +# 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 +# stage 3 DNS resolved, connect to remote +# stage 4 still connecting, more data from local received +# stage 5 remote connected, piping local and remote + +# 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 +# stage 4 still connecting, more data from local received +# stage 5 remote connected, piping local and remote + +STAGE_INIT = 0 +STAGE_ADDR = 1 +STAGE_UDP_ASSOC = 2 +STAGE_DNS = 3 +STAGE_CONNECTING = 4 +STAGE_STREAM = 5 +STAGE_DESTROYED = -1 + +# 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 + +# for each stream, it's waiting for reading, or writing, or both +WAIT_STATUS_INIT = 0 +WAIT_STATUS_READING = 1 +WAIT_STATUS_WRITING = 2 +WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING + +BUF_SIZE = 32 * 1024 + + +class TCPRelayHandler(object): + def __init__(self, server, fd_to_handlers, loop, local_sock, config, + dns_resolver, is_local): + self._server = server + self._fd_to_handlers = fd_to_handlers + self._loop = loop + self._local_sock = local_sock + self._remote_sock = None + self._config = config + self._dns_resolver = dns_resolver + + # 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'], + config['method']) + self._fastopen_connected = False + self._data_to_write_to_local = [] + self._data_to_write_to_remote = [] + self._upstream_status = WAIT_STATUS_READING + self._downstream_status = WAIT_STATUS_INIT + self._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 + local_sock.setblocking(False) + local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR, + self._server) + self.last_activity = 0 + self._update_activity() + + def __hash__(self): + # default __hash__ is id / 16 + # we want to eliminate collisions + return id(self) + + @property + def remote_address(self): + return self._remote_address + + def _get_a_server(self): + server = self._config['server'] + server_port = self._config['server_port'] + if type(server_port) == list: + server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) + logging.debug('chosen server: %s:%d', server, server_port) + return server, server_port + + def _update_activity(self, data_len=0): + # tell the TCP Relay we have activities recently + # else it will think we are inactive and timed out + self._server.update_activity(self, data_len) + + def _update_stream(self, stream, status): + # update a stream to a new waiting status + + # check if status is changed + # only update if dirty + dirty = False + if stream == STREAM_DOWN: + if self._downstream_status != status: + self._downstream_status = status + dirty = True + elif stream == STREAM_UP: + if self._upstream_status != status: + self._upstream_status = status + dirty = True + if dirty: + if self._local_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + if self._upstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + self._loop.modify(self._local_sock, event) + if self._remote_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + if self._upstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + self._loop.modify(self._remote_sock, event) + + def _write_to_sock(self, data, sock): + # write data to sock + # if only some of the data are written, put remaining in the buffer + # and update the stream to wait for writing + if not data or not sock: + return False + uncomplete = False + try: + l = len(data) + s = sock.send(data) + if s < l: + data = data[s:] + uncomplete = True + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + uncomplete = True + else: + shell.print_exception(e) + self.destroy() + return False + if uncomplete: + if sock == self._local_sock: + self._data_to_write_to_local.append(data) + self._update_stream(STREAM_DOWN, WAIT_STATUS_WRITING) + elif sock == self._remote_sock: + self._data_to_write_to_remote.append(data) + self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) + else: + logging.error('write_all_to_sock:unknown socket') + else: + if sock == self._local_sock: + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + elif sock == self._remote_sock: + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + else: + logging.error('write_all_to_sock:unknown socket') + return True + + def _handle_stage_connecting(self, data): + if self._is_local: + data = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data) + if self._is_local and not self._fastopen_connected and \ + self._config['fast_open']: + # for sslocal and fastopen, we basically wait for data and use + # sendto to connect + try: + # only connect once + self._fastopen_connected = True + remote_sock = \ + self._create_remote_socket(self._chosen_server[0], + self._chosen_server[1]) + self._loop.add(remote_sock, eventloop.POLL_ERR, self._server) + data = b''.join(self._data_to_write_to_remote) + l = len(data) + s = remote_sock.sendto(data, MSG_FASTOPEN, self._chosen_server) + if s < l: + data = data[s:] + self._data_to_write_to_remote = [data] + else: + self._data_to_write_to_remote = [] + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == errno.EINPROGRESS: + # in this case data is not sent at all + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + elif eventloop.errno_from_exception(e) == errno.ENOTCONN: + logging.error('fast open not supported on this OS') + self._config['fast_open'] = False + self.destroy() + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + + def _handle_stage_addr(self, data): + try: + if self._is_local: + cmd = common.ord(data[1]) + if cmd == CMD_UDP_ASSOCIATE: + logging.debug('UDP associate') + if self._local_sock.family == socket.AF_INET6: + header = b'\x05\x00\x00\x04' + else: + header = b'\x05\x00\x00\x01' + addr, port = self._local_sock.getsockname()[:2] + addr_to_send = socket.inet_pton(self._local_sock.family, + addr) + port_to_send = struct.pack('>H', port) + self._write_to_sock(header + addr_to_send + port_to_send, + self._local_sock) + self._stage = STAGE_UDP_ASSOC + # just wait for the client to disconnect + return + elif cmd == CMD_CONNECT: + # just trim VER CMD RSV + data = data[3:] + else: + logging.error('unknown command %d', cmd) + self.destroy() + return + header_result = parse_header(data) + if header_result is None: + raise Exception('can not parse header') + addrtype, remote_addr, remote_port, header_length = header_result + logging.info('connecting %s:%d 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 + if self._is_local: + # forward address to remote + self._write_to_sock((b'\x05\x00\x00\x01' + b'\x00\x00\x00\x00\x10\x10'), + self._local_sock) + data_to_send = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data_to_send) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(self._chosen_server[0], + self._handle_dns_resolved) + else: + if len(data) > header_length: + self._data_to_write_to_remote.append(data[header_length:]) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(remote_addr, + self._handle_dns_resolved) + except Exception as e: + self._log_error(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + + def _create_remote_socket(self, ip, port): + addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, + socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) + af, socktype, proto, canonname, sa = addrs[0] + 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 + remote_sock.setblocking(False) + remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + return remote_sock + + def _handle_dns_resolved(self, result, error): + if error: + self._log_error(error) + self.destroy() + return + if result: + ip = result[1] + if ip: + + try: + self._stage = STAGE_CONNECTING + remote_addr = ip + if self._is_local: + remote_port = self._chosen_server[1] + else: + remote_port = self._remote_address[1] + + if self._is_local and self._config['fast_open']: + # for fastopen: + # wait for more data to arrive and send them in one SYN + self._stage = STAGE_CONNECTING + # we don't have to wait for remote since it's not + # created + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + # TODO when there is already data in this packet + else: + # else do connect + remote_sock = self._create_remote_socket(remote_addr, + remote_port) + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == \ + errno.EINPROGRESS: + pass + self._loop.add(remote_sock, + eventloop.POLL_ERR | eventloop.POLL_OUT, + self._server) + self._stage = STAGE_CONNECTING + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + return + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + self.destroy() + + def _on_local_read(self): + # handle all local read events and dispatch them to methods for + # each stage + if not self._local_sock: + return + is_local = self._is_local + data = None + try: + data = self._local_sock.recv(BUF_SIZE) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in \ + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + return + if not data: + self.destroy() + return + self._update_activity(len(data)) + if not is_local: + data = self._encryptor.decrypt(data) + if not data: + return + if self._stage == STAGE_STREAM: + if self._is_local: + data = self._encryptor.encrypt(data) + self._write_to_sock(data, self._remote_sock) + return + elif is_local and self._stage == STAGE_INIT: + # TODO check auth method + self._write_to_sock(b'\x05\00', self._local_sock) + self._stage = STAGE_ADDR + return + elif self._stage == STAGE_CONNECTING: + self._handle_stage_connecting(data) + elif (is_local and self._stage == STAGE_ADDR) or \ + (not is_local and self._stage == STAGE_INIT): + self._handle_stage_addr(data) + + def _on_remote_read(self): + # handle all remote read events + data = None + try: + data = self._remote_sock.recv(BUF_SIZE) + + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in \ + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + return + if not data: + self.destroy() + return + self._update_activity(len(data)) + if self._is_local: + data = self._encryptor.decrypt(data) + else: + data = self._encryptor.encrypt(data) + try: + self._write_to_sock(data, self._local_sock) + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + # TODO use logging when debug completed + self.destroy() + + def _on_local_write(self): + # handle local writable event + if self._data_to_write_to_local: + data = b''.join(self._data_to_write_to_local) + self._data_to_write_to_local = [] + self._write_to_sock(data, self._local_sock) + else: + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + + def _on_remote_write(self): + # handle remote writable event + self._stage = STAGE_STREAM + if self._data_to_write_to_remote: + data = b''.join(self._data_to_write_to_remote) + self._data_to_write_to_remote = [] + self._write_to_sock(data, self._remote_sock) + else: + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + + def _on_local_error(self): + logging.debug('got local error') + if self._local_sock: + logging.error(eventloop.get_sock_error(self._local_sock)) + self.destroy() + + def _on_remote_error(self): + logging.debug('got remote error') + if self._remote_sock: + logging.error(eventloop.get_sock_error(self._remote_sock)) + self.destroy() + + def handle_event(self, sock, event): + # handle all events in this handler and dispatch them to methods + if self._stage == STAGE_DESTROYED: + logging.debug('ignore handle_event: destroyed') + return + # order is important + if sock == self._remote_sock: + if event & eventloop.POLL_ERR: + self._on_remote_error() + if self._stage == STAGE_DESTROYED: + return + if event & (eventloop.POLL_IN | eventloop.POLL_HUP): + self._on_remote_read() + if self._stage == STAGE_DESTROYED: + return + if event & eventloop.POLL_OUT: + self._on_remote_write() + elif sock == self._local_sock: + if event & eventloop.POLL_ERR: + self._on_local_error() + if self._stage == STAGE_DESTROYED: + return + if event & (eventloop.POLL_IN | eventloop.POLL_HUP): + self._on_local_read() + if self._stage == STAGE_DESTROYED: + return + if event & eventloop.POLL_OUT: + self._on_local_write() + else: + logging.warn('unknown socket') + + def _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: + # 1. destroy won't make another destroy() call inside + # 2. destroy releases resources so it prevents future call to destroy + # 3. destroy won't raise any exceptions + # if any of the promises are broken, it indicates a bug has been + # introduced! mostly likely memory leaks, etc + if self._stage == STAGE_DESTROYED: + # this couldn't happen + logging.debug('already destroyed') + return + self._stage = STAGE_DESTROYED + if self._remote_address: + logging.debug('destroy: %s:%d' % + self._remote_address) + else: + logging.debug('destroy') + if self._remote_sock: + logging.debug('destroying remote') + self._loop.remove(self._remote_sock) + del self._fd_to_handlers[self._remote_sock.fileno()] + self._remote_sock.close() + self._remote_sock = None + if self._local_sock: + logging.debug('destroying local') + self._loop.remove(self._local_sock) + del self._fd_to_handlers[self._local_sock.fileno()] + self._local_sock.close() + self._local_sock = None + self._dns_resolver.remove_callback(self._handle_dns_resolved) + self._server.remove_handler(self) + + +class TCPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): + self._config = config + self._is_local = is_local + self._dns_resolver = dns_resolver + self._closed = False + self._eventloop = None + self._fd_to_handlers = {} + + self._timeout = config['timeout'] + self._timeouts = [] # a list for all the handlers + # we trim the timeouts once a while + self._timeout_offset = 0 # last checked position for timeout + self._handler_to_timeouts = {} # key: handler value: index in timeouts + + if is_local: + listen_addr = config['local_address'] + listen_port = config['local_port'] + else: + listen_addr = config['server'] + listen_port = config['server_port'] + self._listen_port = listen_port + + addrs = socket.getaddrinfo(listen_addr, listen_port, 0, + socket.SOCK_STREAM, socket.SOL_TCP) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % + (listen_addr, listen_port)) + af, socktype, proto, canonname, sa = addrs[0] + server_socket = socket.socket(af, socktype, proto) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(sa) + server_socket.setblocking(False) + if config['fast_open']: + try: + server_socket.setsockopt(socket.SOL_TCP, 23, 5) + except socket.error: + logging.error('warning: fast open is not available') + self._config['fast_open'] = False + server_socket.listen(1024) + self._server_socket = server_socket + self._stat_callback = stat_callback + + def add_to_loop(self, loop): + if self._eventloop: + raise Exception('already add to loop') + if self._closed: + raise Exception('already closed') + self._eventloop = loop + self._eventloop.add(self._server_socket, + eventloop.POLL_IN | eventloop.POLL_ERR, self) + self._eventloop.add_periodic(self.handle_periodic) + + def remove_handler(self, handler): + index = self._handler_to_timeouts.get(hash(handler), -1) + if index >= 0: + # delete is O(n), so we just set it to None + self._timeouts[index] = None + del self._handler_to_timeouts[hash(handler)] + + def update_activity(self, handler, data_len): + if data_len and self._stat_callback: + self._stat_callback(self._listen_port, data_len) + + # set handler to active + now = int(time.time()) + if now - handler.last_activity < eventloop.TIMEOUT_PRECISION: + # thus we can lower timeout modification frequency + return + handler.last_activity = now + index = self._handler_to_timeouts.get(hash(handler), -1) + if index >= 0: + # delete is O(n), so we just set it to None + self._timeouts[index] = None + length = len(self._timeouts) + self._timeouts.append(handler) + self._handler_to_timeouts[hash(handler)] = length + + def _sweep_timeout(self): + # tornado's timeout memory management is more flexible than we need + # we just need a sorted last_activity queue and it's faster than heapq + # in fact we can do O(1) insertion/remove so we invent our own + if self._timeouts: + logging.log(shell.VERBOSE_LEVEL, 'sweeping timeouts') + now = time.time() + length = len(self._timeouts) + pos = self._timeout_offset + while pos < length: + handler = self._timeouts[pos] + if handler: + if now - handler.last_activity < self._timeout: + break + else: + if handler.remote_address: + logging.warn('timed out: %s:%d' % + handler.remote_address) + else: + logging.warn('timed out') + handler.destroy() + self._timeouts[pos] = None # free memory + pos += 1 + else: + pos += 1 + if pos > TIMEOUTS_CLEAN_SIZE and pos > length >> 1: + # clean up the timeout queue when it gets larger than half + # of the queue + self._timeouts = self._timeouts[pos:] + for key in self._handler_to_timeouts: + self._handler_to_timeouts[key] -= pos + pos = 0 + self._timeout_offset = pos + + def handle_event(self, sock, fd, event): + # handle events and dispatch to handlers + if sock: + 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: + # TODO + raise Exception('server_socket error') + try: + logging.debug('accept') + conn = self._server_socket.accept() + TCPRelayHandler(self, self._fd_to_handlers, + self._eventloop, conn[0], self._config, + self._dns_resolver, self._is_local) + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + else: + if sock: + handler = self._fd_to_handlers.get(fd, None) + if handler: + handler.handle_event(sock, event) + else: + logging.warn('poll removed fd') + + def handle_periodic(self): + if self._closed: + if self._server_socket: + self._eventloop.remove(self._server_socket) + self._server_socket.close() + self._server_socket = None + logging.info('closed TCP port %d', self._listen_port) + if not self._fd_to_handlers: + logging.info('stopping') + self._eventloop.stop() + self._sweep_timeout() + + def close(self, next_tick=False): + logging.debug('TCP close') + self._closed = True + if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) + self._server_socket.close() + for handler in list(self._fd_to_handlers.values()): + handler.destroy() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py new file mode 100755 index 0000000..96d1fbd --- /dev/null +++ b/shadowsocks/udprelay.py @@ -0,0 +1,298 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +# SOCKS5 UDP Request +# +----+------+------+----------+----------+----------+ +# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +# +----+------+------+----------+----------+----------+ +# | 2 | 1 | 1 | Variable | 2 | Variable | +# +----+------+------+----------+----------+----------+ + +# SOCKS5 UDP Response +# +----+------+------+----------+----------+----------+ +# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +# +----+------+------+----------+----------+----------+ +# | 2 | 1 | 1 | Variable | 2 | Variable | +# +----+------+------+----------+----------+----------+ + +# shadowsocks UDP Request (before encrypted) +# +------+----------+----------+----------+ +# | ATYP | DST.ADDR | DST.PORT | DATA | +# +------+----------+----------+----------+ +# | 1 | Variable | 2 | Variable | +# +------+----------+----------+----------+ + +# shadowsocks UDP Response (before encrypted) +# +------+----------+----------+----------+ +# | ATYP | DST.ADDR | DST.PORT | DATA | +# +------+----------+----------+----------+ +# | 1 | Variable | 2 | Variable | +# +------+----------+----------+----------+ + +# shadowsocks UDP Request and Response (after encrypted) +# +-------+--------------+ +# | IV | PAYLOAD | +# +-------+--------------+ +# | Fixed | Variable | +# +-------+--------------+ + +# HOW TO NAME THINGS +# ------------------ +# `dest` means destination server, which is from DST fields in the SOCKS5 +# request +# `local` means local server of shadowsocks +# `remote` means remote server of shadowsocks +# `client` means UDP clients that connects to other servers +# `server` means the UDP server that handles user requests + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import socket +import logging +import struct +import errno +import random + +from shadowsocks import encrypt, eventloop, lru_cache, common, shell +from shadowsocks.common import parse_header, pack_addr + + +BUF_SIZE = 65536 + + +def client_key(source_addr, server_af): + # notice this is server af, not dest af + return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af) + + +class UDPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): + self._config = config + if is_local: + self._listen_addr = config['local_address'] + self._listen_port = config['local_port'] + self._remote_addr = config['server'] + self._remote_port = config['server_port'] + else: + self._listen_addr = config['server'] + self._listen_port = config['server_port'] + self._remote_addr = None + self._remote_port = None + self._dns_resolver = dns_resolver + self._password = common.to_bytes(config['password']) + self._method = config['method'] + self._timeout = config['timeout'] + self._is_local = is_local + self._cache = lru_cache.LRUCache(timeout=config['timeout'], + close_callback=self._close_client) + self._client_fd_to_server_addr = \ + lru_cache.LRUCache(timeout=config['timeout']) + self._dns_cache = lru_cache.LRUCache(timeout=300) + self._eventloop = None + self._closed = False + 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) + if len(addrs) == 0: + raise Exception("can't get addrinfo for %s:%d" % + (self._listen_addr, self._listen_port)) + af, socktype, proto, canonname, sa = addrs[0] + server_socket = socket.socket(af, socktype, proto) + server_socket.bind((self._listen_addr, self._listen_port)) + server_socket.setblocking(False) + self._server_socket = server_socket + self._stat_callback = stat_callback + + def _get_a_server(self): + server = self._config['server'] + server_port = self._config['server_port'] + if type(server_port) == list: + server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) + logging.debug('chosen server: %s:%d', server, server_port) + return server, server_port + + def _close_client(self, client): + if hasattr(client, 'close'): + self._sockets.remove(client.fileno()) + self._eventloop.remove(client) + client.close() + else: + # just an address + pass + + def _handle_server(self): + server = self._server_socket + data, r_addr = server.recvfrom(BUF_SIZE) + if not data: + logging.debug('UDP handle_server: data is empty') + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) + if self._is_local: + frag = common.ord(data[2]) + if frag != 0: + logging.warn('drop a message since frag is not 0') + return + else: + data = data[3:] + else: + data = encrypt.encrypt_all(self._password, self._method, 0, data) + # decrypt data + if not data: + logging.debug('UDP handle_server: data is empty after decrypt') + return + header_result = parse_header(data) + if header_result is None: + return + addrtype, dest_addr, dest_port, header_length = header_result + + if self._is_local: + server_addr, server_port = self._get_a_server() + else: + server_addr, server_port = dest_addr, dest_port + + addrs = self._dns_cache.get(server_addr, None) + if addrs is None: + addrs = socket.getaddrinfo(server_addr, server_port, 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if not addrs: + # drop + return + else: + self._dns_cache[server_addr] = addrs + + af, socktype, proto, canonname, sa = addrs[0] + key = client_key(r_addr, af) + client = self._cache.get(key, None) + if not client: + # TODO async getaddrinfo + 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 + self._client_fd_to_server_addr[client.fileno()] = r_addr + + self._sockets.add(client.fileno()) + self._eventloop.add(client, eventloop.POLL_IN, self) + + if self._is_local: + data = encrypt.encrypt_all(self._password, self._method, 1, data) + if not data: + return + else: + data = data[header_length:] + if not data: + return + try: + client.sendto(data, (server_addr, server_port)) + except IOError as e: + err = eventloop.errno_from_exception(e) + if err in (errno.EINPROGRESS, errno.EAGAIN): + pass + else: + shell.print_exception(e) + + def _handle_client(self, sock): + data, r_addr = sock.recvfrom(BUF_SIZE) + if not data: + logging.debug('UDP handle_client: data is empty') + return + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) + if not self._is_local: + addrlen = len(r_addr[0]) + if addrlen > 255: + # drop + return + data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data + response = encrypt.encrypt_all(self._password, self._method, 1, + data) + if not response: + return + else: + data = encrypt.encrypt_all(self._password, self._method, 0, + data) + if not data: + return + header_result = parse_header(data) + if header_result is None: + return + # addrtype, dest_addr, dest_port, header_length = header_result + response = b'\x00\x00\x00' + data + client_addr = self._client_fd_to_server_addr.get(sock.fileno()) + if client_addr: + self._server_socket.sendto(response, client_addr) + else: + # this packet is from somewhere else we know + # simply drop that packet + pass + + def add_to_loop(self, loop): + if self._eventloop: + raise Exception('already add to loop') + if self._closed: + raise Exception('already closed') + self._eventloop = loop + + server_socket = self._server_socket + self._eventloop.add(server_socket, + eventloop.POLL_IN | eventloop.POLL_ERR, self) + loop.add_periodic(self.handle_periodic) + + def handle_event(self, sock, fd, event): + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + logging.error('UDP server_socket err') + self._handle_server() + elif sock and (fd in self._sockets): + if event & eventloop.POLL_ERR: + logging.error('UDP client_socket err') + self._handle_client(sock) + + def handle_periodic(self): + if self._closed: + if self._server_socket: + self._server_socket.close() + self._server_socket = None + for sock in self._sockets: + sock.close() + logging.info('closed UDP port %d', self._listen_port) + self._cache.sweep() + self._client_fd_to_server_addr.sweep() + + def close(self, next_tick=False): + logging.debug('UDP close') + self._closed = True + if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) + self._server_socket.close() + for client in list(self._cache.values()): + client.close() diff --git a/tests/aes-cfb1.json b/tests/aes-cfb1.json new file mode 100755 index 0000000..40d0b21 --- /dev/null +++ b/tests/aes-cfb1.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb1", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-cfb8.json b/tests/aes-cfb8.json new file mode 100755 index 0000000..fb7014b --- /dev/null +++ b/tests/aes-cfb8.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb8", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-ctr.json b/tests/aes-ctr.json new file mode 100755 index 0000000..1fed8a8 --- /dev/null +++ b/tests/aes-ctr.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes.json b/tests/aes.json new file mode 100755 index 0000000..a3d95b9 --- /dev/null +++ b/tests/aes.json @@ -0,0 +1,10 @@ +{ + "server":"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/assert.sh b/tests/assert.sh new file mode 100755 index 0000000..b0c679c --- /dev/null +++ b/tests/assert.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# assert.sh 1.0 - bash unit testing framework +# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann +# +# http://github.com/lehmannro/assert.sh +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +export DISCOVERONLY=${DISCOVERONLY:-} +export DEBUG=${DEBUG:-} +export STOP=${STOP:-} +export INVARIANT=${INVARIANT:-} +export CONTINUE=${CONTINUE:-} + +args="$(getopt -n "$0" -l \ + verbose,help,stop,discover,invariant,continue vhxdic $*)" \ +|| exit -1 +for arg in $args; do + case "$arg" in + -h) + echo "$0 [-vxidc]" \ + "[--verbose] [--stop] [--invariant] [--discover] [--continue]" + echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" + exit 0;; + --help) + cat < [stdin] + (( tests_ran++ )) || : + [[ -n "$DISCOVERONLY" ]] && return || true + # printf required for formatting + printf -v expected "x${2:-}" # x required to overwrite older results + result="$(eval 2>/dev/null $1 <<< ${3:-})" || true + # Note: $expected is already decorated + if [[ "x$result" == "$expected" ]]; then + [[ -n "$DEBUG" ]] && echo -n . || true + return + fi + result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" + [[ -z "$result" ]] && result="nothing" || result="\"$result\"" + [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" + _assert_fail "expected $expected${_indent}got $result" "$1" "$3" +} + +assert_raises() { + # assert_raises [stdin] + (( tests_ran++ )) || : + [[ -n "$DISCOVERONLY" ]] && return || true + status=0 + (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? + expected=${2:-0} + if [[ "$status" -eq "$expected" ]]; then + [[ -n "$DEBUG" ]] && echo -n . || true + return + fi + _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" +} + +_assert_fail() { + # _assert_fail + [[ -n "$DEBUG" ]] && echo -n X + report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" + if [[ -n "$STOP" ]]; then + [[ -n "$DEBUG" ]] && echo + echo "$report" + exit 1 + fi + tests_errors[$tests_failed]="$report" + (( tests_failed++ )) || : +} + +_assert_reset +: ${tests_suite_status:=0} # remember if any of the tests failed so far +_assert_cleanup() { + local status=$? + # modify exit code if it's not already non-zero + [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status +} +trap _assert_cleanup EXIT diff --git a/tests/chacha20.json b/tests/chacha20.json new file mode 100755 index 0000000..541a9be --- /dev/null +++ b/tests/chacha20.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"chacha20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/client-multi-server-ip.json b/tests/client-multi-server-ip.json new file mode 100755 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 100755 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/fastopen.json b/tests/fastopen.json new file mode 100755 index 0000000..f3980b6 --- /dev/null +++ b/tests/fastopen.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"fastopen_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":true +} diff --git a/tests/gen_multiple_passwd.py b/tests/gen_multiple_passwd.py new file mode 100755 index 0000000..62586c2 --- /dev/null +++ b/tests/gen_multiple_passwd.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +import json + +with open('server-multi-passwd-performance.json', 'wb') as f: + r = { + 'server': '127.0.0.1', + 'local_port': 1081, + 'timeout': 60, + 'method': 'aes-256-cfb' + } + ports = {} + for i in range(7000, 9000): + ports[str(i)] = 'aes_password' + + r['port_password'] = ports + print(r) + f.write(json.dumps(r, indent=4).encode('utf-8')) diff --git a/tests/graceful.json b/tests/graceful.json new file mode 100755 index 0000000..7d94ea5 --- /dev/null +++ b/tests/graceful.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":15, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/graceful_cli.py b/tests/graceful_cli.py new file mode 100755 index 0000000..e58674b --- /dev/null +++ b/tests/graceful_cli.py @@ -0,0 +1,17 @@ +#!/usr/bin/python + +import socks +import time + + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 8001 + + +if __name__ == '__main__': + s = socks.socksocket() + s.set_proxy(socks.SOCKS5, SERVER_IP, 1081) + s.connect((SERVER_IP, SERVER_PORT)) + s.send(b'test') + time.sleep(30) + s.close() diff --git a/tests/graceful_server.py b/tests/graceful_server.py new file mode 100755 index 0000000..d7038f1 --- /dev/null +++ b/tests/graceful_server.py @@ -0,0 +1,13 @@ +#!/usr/bin/python + +import socket + + +if __name__ == '__main__': + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('127.0.0.1', 8001)) + s.listen(1024) + c = None + while True: + c = s.accept() diff --git a/tests/ipv6-client-side.json b/tests/ipv6-client-side.json new file mode 100755 index 0000000..6c3cfaf --- /dev/null +++ b/tests/ipv6-client-side.json @@ -0,0 +1,10 @@ +{ + "server":"::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/ipv6.json b/tests/ipv6.json new file mode 100755 index 0000000..d855f9c --- /dev/null +++ b/tests/ipv6.json @@ -0,0 +1,10 @@ +{ + "server":"::", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/jenkins.sh b/tests/jenkins.sh new file mode 100755 index 0000000..a85c461 --- /dev/null +++ b/tests/jenkins.sh @@ -0,0 +1,87 @@ +#!/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 + +if [ "a$JENKINS" != "a1" ] ; then + # jenkins blocked SIGQUIT with sigprocmask(), we have to skip this test on Jenkins + run_test tests/test_graceful_restart.sh +fi +run_test tests/test_udp_src.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/libsodium/install.sh b/tests/libsodium/install.sh new file mode 100755 index 0000000..b0e35fa --- /dev/null +++ b/tests/libsodium/install.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ ! -d libsodium-1.0.1 ]; then + wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 + tar xf libsodium-1.0.1.tar.gz || exit 1 +fi +pushd libsodium-1.0.1 +./configure && make -j2 && make install || exit 1 +sudo ldconfig +popd diff --git a/tests/nose_plugin.py b/tests/nose_plugin.py new file mode 100755 index 0000000..86b1a86 --- /dev/null +++ b/tests/nose_plugin.py @@ -0,0 +1,43 @@ +#!/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 + + +class ExtensionPlugin(Plugin): + + name = "ExtensionPlugin" + + def options(self, parser, env): + Plugin.options(self, parser, env) + + def configure(self, options, config): + Plugin.configure(self, options, config) + self.enabled = True + + def wantFile(self, file): + return file.endswith('.py') + + def wantDirectory(self, directory): + return True + + def wantModule(self, file): + return True + + +if __name__ == '__main__': + nose.main(addplugins=[ExtensionPlugin()]) diff --git a/tests/rc4-md5.json b/tests/rc4-md5.json new file mode 100755 index 0000000..26ba0df --- /dev/null +++ b/tests/rc4-md5.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"rc4-md5", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/salsa20-ctr.json b/tests/salsa20-ctr.json new file mode 100755 index 0000000..5ca6c45 --- /dev/null +++ b/tests/salsa20-ctr.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"salsa20-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/salsa20.json b/tests/salsa20.json new file mode 100755 index 0000000..7e30380 --- /dev/null +++ b/tests/salsa20.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"salsa20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/server-multi-passwd-client-side.json b/tests/server-multi-passwd-client-side.json new file mode 100755 index 0000000..c822c98 --- /dev/null +++ b/tests/server-multi-passwd-client-side.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "server_port": "8385", + "local_port": 1081, + "password": "foobar5", + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/server-multi-passwd-performance.json b/tests/server-multi-passwd-performance.json new file mode 100755 index 0000000..c9fbc37 --- /dev/null +++ b/tests/server-multi-passwd-performance.json @@ -0,0 +1,2008 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + "7582": "aes_password", + "7672": "aes_password", + "8923": "aes_password", + "8502": "aes_password", + "8282": "aes_password", + "8871": "aes_password", + "7732": "aes_password", + "8671": "aes_password", + "7018": "aes_password", + "8492": "aes_password", + "7748": "aes_password", + "8992": "aes_password", + "8246": "aes_password", + "7127": "aes_password", + "7775": "aes_password", + "8542": "aes_password", + "8488": "aes_password", + "7515": "aes_password", + "7659": "aes_password", + "8892": "aes_password", + "8028": "aes_password", + "7276": "aes_password", + "7959": "aes_password", + "7457": "aes_password", + "8635": "aes_password", + "7592": "aes_password", + "8764": "aes_password", + "8861": "aes_password", + "8842": "aes_password", + "8135": "aes_password", + "8140": "aes_password", + "8376": "aes_password", + "7733": "aes_password", + "8174": "aes_password", + "7265": "aes_password", + "8314": "aes_password", + "8772": "aes_password", + "8991": "aes_password", + "7183": "aes_password", + "7067": "aes_password", + "7730": "aes_password", + "8694": "aes_password", + "7629": "aes_password", + "7041": "aes_password", + "8507": "aes_password", + "8112": "aes_password", + "8491": "aes_password", + "7273": "aes_password", + "8811": "aes_password", + "8947": "aes_password", + "8612": "aes_password", + "8134": "aes_password", + "8422": "aes_password", + "8970": "aes_password", + "7051": "aes_password", + "8158": "aes_password", + "8934": "aes_password", + "7579": "aes_password", + "7140": "aes_password", + "8448": "aes_password", + "8536": "aes_password", + "7554": "aes_password", + "8168": "aes_password", + "8307": "aes_password", + "8946": "aes_password", + "7872": "aes_password", + "7330": "aes_password", + "8208": "aes_password", + "7955": "aes_password", + "8597": "aes_password", + "7025": "aes_password", + "7086": "aes_password", + "7534": "aes_password", + "7311": "aes_password", + "7758": "aes_password", + "7103": "aes_password", + "8408": "aes_password", + "7688": "aes_password", + "7073": "aes_password", + "8963": "aes_password", + "8578": "aes_password", + "7735": "aes_password", + "7657": "aes_password", + "7763": "aes_password", + "7680": "aes_password", + "8627": "aes_password", + "8205": "aes_password", + "7188": "aes_password", + "8743": "aes_password", + "8472": "aes_password", + "8823": "aes_password", + "7167": "aes_password", + "7008": "aes_password", + "7601": "aes_password", + "8603": "aes_password", + "8467": "aes_password", + "8803": "aes_password", + "7014": "aes_password", + "7233": "aes_password", + "7199": "aes_password", + "7192": "aes_password", + "7329": "aes_password", + "8031": "aes_password", + "8584": "aes_password", + "8041": "aes_password", + "8962": "aes_password", + "8824": "aes_password", + "8079": "aes_password", + "8049": "aes_password", + "7743": "aes_password", + "8035": "aes_password", + "8212": "aes_password", + "8452": "aes_password", + "8484": "aes_password", + "8232": "aes_password", + "8444": "aes_password", + "8410": "aes_password", + "7110": "aes_password", + "7505": "aes_password", + "8856": "aes_password", + "8293": "aes_password", + "7967": "aes_password", + "8267": "aes_password", + "7772": "aes_password", + "8864": "aes_password", + "8518": "aes_password", + "7520": "aes_password", + "7976": "aes_password", + "8407": "aes_password", + "8971": "aes_password", + "7389": "aes_password", + "7510": "aes_password", + "7373": "aes_password", + "8013": "aes_password", + "8310": "aes_password", + "7028": "aes_password", + "7874": "aes_password", + "7356": "aes_password", + "7729": "aes_password", + "7427": "aes_password", + "8312": "aes_password", + "7721": "aes_password", + "7020": "aes_password", + "8231": "aes_password", + "8188": "aes_password", + "8869": "aes_password", + "8595": "aes_password", + "8022": "aes_password", + "8911": "aes_password", + "7957": "aes_password", + "7141": "aes_password", + "7157": "aes_password", + "8471": "aes_password", + "8157": "aes_password", + "8795": "aes_password", + "7087": "aes_password", + "7470": "aes_password", + "7266": "aes_password", + "8072": "aes_password", + "8346": "aes_password", + "7163": "aes_password", + "8954": "aes_password", + "7046": "aes_password", + "7856": "aes_password", + "7883": "aes_password", + "8198": "aes_password", + "8443": "aes_password", + "8496": "aes_password", + "8900": "aes_password", + "8354": "aes_password", + "8758": "aes_password", + "8287": "aes_password", + "7574": "aes_password", + "8316": "aes_password", + "7539": "aes_password", + "8460": "aes_password", + "7616": "aes_password", + "8599": "aes_password", + "7795": "aes_password", + "7079": "aes_password", + "8468": "aes_password", + "8462": "aes_password", + "8645": "aes_password", + "8347": "aes_password", + "8776": "aes_password", + "7072": "aes_password", + "8781": "aes_password", + "7765": "aes_password", + "8048": "aes_password", + "7401": "aes_password", + "8718": "aes_password", + "8712": "aes_password", + "7801": "aes_password", + "8673": "aes_password", + "8791": "aes_password", + "7567": "aes_password", + "7003": "aes_password", + "7358": "aes_password", + "8916": "aes_password", + "7021": "aes_password", + "7487": "aes_password", + "7499": "aes_password", + "7108": "aes_password", + "7501": "aes_password", + "7313": "aes_password", + "8887": "aes_password", + "8724": "aes_password", + "7376": "aes_password", + "7153": "aes_password", + "7377": "aes_password", + "8426": "aes_password", + "8831": "aes_password", + "7380": "aes_password", + "7958": "aes_password", + "8250": "aes_password", + "8155": "aes_password", + "8435": "aes_password", + "7630": "aes_password", + "8026": "aes_password", + "7533": "aes_password", + "8704": "aes_password", + "8411": "aes_password", + "7645": "aes_password", + "7937": "aes_password", + "7488": "aes_password", + "8750": "aes_password", + "7196": "aes_password", + "8714": "aes_password", + "8677": "aes_password", + "7475": "aes_password", + "7625": "aes_password", + "8234": "aes_password", + "8870": "aes_password", + "7147": "aes_password", + "8417": "aes_password", + "7362": "aes_password", + "7341": "aes_password", + "8896": "aes_password", + "8423": "aes_password", + "8884": "aes_password", + "7220": "aes_password", + "8615": "aes_password", + "8719": "aes_password", + "8575": "aes_password", + "8891": "aes_password", + "8210": "aes_password", + "8289": "aes_password", + "7406": "aes_password", + "7692": "aes_password", + "7518": "aes_password", + "7244": "aes_password", + "8561": "aes_password", + "7325": "aes_password", + "7306": "aes_password", + "8266": "aes_password", + "8136": "aes_password", + "7991": "aes_password", + "8844": "aes_password", + "8259": "aes_password", + "7749": "aes_password", + "7238": "aes_password", + "7952": "aes_password", + "8528": "aes_password", + "8477": "aes_password", + "7555": "aes_password", + "7544": "aes_password", + "7478": "aes_password", + "7112": "aes_password", + "8931": "aes_password", + "8082": "aes_password", + "8189": "aes_password", + "8461": "aes_password", + "7740": "aes_password", + "8633": "aes_password", + "8322": "aes_password", + "7093": "aes_password", + "8415": "aes_password", + "8093": "aes_password", + "8682": "aes_password", + "7860": "aes_password", + "8580": "aes_password", + "8503": "aes_password", + "7794": "aes_password", + "8394": "aes_password", + "8487": "aes_password", + "8053": "aes_password", + "7277": "aes_password", + "7241": "aes_password", + "8430": "aes_password", + "8428": "aes_password", + "7805": "aes_password", + "8393": "aes_password", + "7711": "aes_password", + "7807": "aes_password", + "7600": "aes_password", + "8403": "aes_password", + "8141": "aes_password", + "8937": "aes_password", + "7559": "aes_password", + "7834": "aes_password", + "8837": "aes_password", + "7823": "aes_password", + "8928": "aes_password", + "7083": "aes_password", + "7590": "aes_password", + "8806": "aes_password", + "7130": "aes_password", + "7929": "aes_password", + "8684": "aes_password", + "8195": "aes_password", + "8706": "aes_password", + "7044": "aes_password", + "7403": "aes_password", + "8890": "aes_password", + "8364": "aes_password", + "8206": "aes_password", + "7882": "aes_password", + "8522": "aes_password", + "8958": "aes_password", + "7429": "aes_password", + "8586": "aes_password", + "8330": "aes_password", + "8922": "aes_password", + "7940": "aes_password", + "7379": "aes_password", + "8955": "aes_password", + "7168": "aes_password", + "7294": "aes_password", + "7949": "aes_password", + "7384": "aes_password", + "8832": "aes_password", + "7423": "aes_password", + "8763": "aes_password", + "7148": "aes_password", + "7029": "aes_password", + "7969": "aes_password", + "8190": "aes_password", + "8807": "aes_password", + "8889": "aes_password", + "7750": "aes_password", + "7348": "aes_password", + "7193": "aes_password", + "7459": "aes_password", + "7507": "aes_password", + "7536": "aes_password", + "8734": "aes_password", + "7174": "aes_password", + "8400": "aes_password", + "8630": "aes_password", + "7128": "aes_password", + "7261": "aes_password", + "7527": "aes_password", + "7232": "aes_password", + "7843": "aes_password", + "7326": "aes_password", + "8639": "aes_password", + "7830": "aes_password", + "7981": "aes_password", + "8404": "aes_password", + "8888": "aes_password", + "7920": "aes_password", + "7410": "aes_password", + "7204": "aes_password", + "8382": "aes_password", + "8355": "aes_password", + "7700": "aes_password", + "7606": "aes_password", + "7372": "aes_password", + "8106": "aes_password", + "8160": "aes_password", + "7511": "aes_password", + "8204": "aes_password", + "8732": "aes_password", + "8751": "aes_password", + "7727": "aes_password", + "7137": "aes_password", + "8311": "aes_password", + "8587": "aes_password", + "7336": "aes_password", + "7674": "aes_password", + "8009": "aes_password", + "7230": "aes_password", + "7383": "aes_password", + "8867": "aes_password", + "7260": "aes_password", + "7497": "aes_password", + "7390": "aes_password", + "8821": "aes_password", + "7274": "aes_password", + "7285": "aes_password", + "7857": "aes_password", + "8137": "aes_password", + "7114": "aes_password", + "7979": "aes_password", + "8726": "aes_password", + "7227": "aes_password", + "7714": "aes_password", + "8012": "aes_password", + "7613": "aes_password", + "8876": "aes_password", + "7622": "aes_password", + "8582": "aes_password", + "7120": "aes_password", + "7104": "aes_password", + "8785": "aes_password", + "8096": "aes_password", + "8129": "aes_password", + "8481": "aes_password", + "8695": "aes_password", + "7473": "aes_password", + "8163": "aes_password", + "8357": "aes_password", + "8501": "aes_password", + "7177": "aes_password", + "7931": "aes_password", + "8220": "aes_password", + "7399": "aes_password", + "7956": "aes_password", + "8801": "aes_password", + "7719": "aes_password", + "8042": "aes_password", + "7433": "aes_password", + "7827": "aes_password", + "8377": "aes_password", + "7745": "aes_password", + "7302": "aes_password", + "8399": "aes_password", + "7766": "aes_password", + "8720": "aes_password", + "8685": "aes_password", + "8558": "aes_password", + "7796": "aes_password", + "7319": "aes_password", + "7170": "aes_password", + "7342": "aes_password", + "7191": "aes_password", + "8747": "aes_password", + "7231": "aes_password", + "7817": "aes_password", + "7352": "aes_password", + "7057": "aes_password", + "8177": "aes_password", + "7221": "aes_password", + "7297": "aes_password", + "7686": "aes_password", + "7082": "aes_password", + "8414": "aes_password", + "8529": "aes_password", + "7257": "aes_password", + "7300": "aes_password", + "7159": "aes_password", + "8901": "aes_password", + "7578": "aes_password", + "8479": "aes_password", + "7225": "aes_password", + "8286": "aes_password", + "7182": "aes_password", + "8194": "aes_password", + "8850": "aes_password", + "7847": "aes_password", + "7665": "aes_password", + "8011": "aes_password", + "7702": "aes_password", + "8638": "aes_password", + "7116": "aes_password", + "7301": "aes_password", + "8936": "aes_password", + "8661": "aes_password", + "8333": "aes_password", + "8025": "aes_password", + "7368": "aes_password", + "8634": "aes_password", + "7154": "aes_password", + "8365": "aes_password", + "8736": "aes_password", + "8478": "aes_password", + "7436": "aes_password", + "7411": "aes_password", + "7913": "aes_password", + "8236": "aes_password", + "8854": "aes_password", + "8722": "aes_password", + "8227": "aes_password", + "7757": "aes_password", + "8835": "aes_password", + "8651": "aes_password", + "7417": "aes_password", + "7877": "aes_password", + "7200": "aes_password", + "8622": "aes_password", + "7004": "aes_password", + "8845": "aes_password", + "8159": "aes_password", + "8741": "aes_password", + "7106": "aes_password", + "8897": "aes_password", + "7968": "aes_password", + "7047": "aes_password", + "8860": "aes_password", + "8777": "aes_password", + "7597": "aes_password", + "8859": "aes_password", + "7117": "aes_password", + "8178": "aes_password", + "8642": "aes_password", + "7246": "aes_password", + "7557": "aes_password", + "7965": "aes_password", + "7699": "aes_password", + "8658": "aes_password", + "7442": "aes_password", + "8272": "aes_password", + "7821": "aes_password", + "7893": "aes_password", + "8665": "aes_password", + "8499": "aes_password", + "7897": "aes_password", + "7173": "aes_password", + "7007": "aes_password", + "8219": "aes_password", + "8040": "aes_password", + "7571": "aes_password", + "7526": "aes_password", + "8203": "aes_password", + "7810": "aes_password", + "8974": "aes_password", + "8200": "aes_password", + "7778": "aes_password", + "7987": "aes_password", + "7701": "aes_password", + "7443": "aes_password", + "7798": "aes_password", + "8995": "aes_password", + "8473": "aes_password", + "7132": "aes_password", + "7262": "aes_password", + "7720": "aes_password", + "7282": "aes_password", + "8066": "aes_password", + "7006": "aes_password", + "7197": "aes_password", + "7815": "aes_password", + "7933": "aes_password", + "8138": "aes_password", + "8418": "aes_password", + "7365": "aes_password", + "7786": "aes_password", + "7891": "aes_password", + "8317": "aes_password", + "8207": "aes_password", + "8416": "aes_password", + "7448": "aes_password", + "8843": "aes_password", + "7371": "aes_password", + "8780": "aes_password", + "7989": "aes_password", + "8043": "aes_password", + "7363": "aes_password", + "7550": "aes_password", + "8678": "aes_password", + "7837": "aes_password", + "8302": "aes_password", + "7907": "aes_password", + "8865": "aes_password", + "8153": "aes_password", + "8090": "aes_password", + "7268": "aes_password", + "8292": "aes_password", + "7919": "aes_password", + "8131": "aes_password", + "8815": "aes_password", + "8154": "aes_password", + "7777": "aes_password", + "8369": "aes_password", + "8929": "aes_password", + "7670": "aes_password", + "7484": "aes_password", + "8353": "aes_password", + "8017": "aes_password", + "8833": "aes_password", + "8001": "aes_password", + "8058": "aes_password", + "7918": "aes_password", + "7694": "aes_password", + "7485": "aes_password", + "8592": "aes_password", + "7584": "aes_password", + "8527": "aes_password", + "8285": "aes_password", + "8264": "aes_password", + "8879": "aes_password", + "7944": "aes_password", + "7000": "aes_password", + "7187": "aes_password", + "7039": "aes_password", + "7769": "aes_password", + "8063": "aes_password", + "8701": "aes_password", + "7432": "aes_password", + "8594": "aes_password", + "7052": "aes_password", + "7369": "aes_password", + "8474": "aes_password", + "7709": "aes_password", + "8296": "aes_password", + "8278": "aes_password", + "8395": "aes_password", + "8674": "aes_password", + "8733": "aes_password", + "7890": "aes_password", + "7080": "aes_password", + "7528": "aes_password", + "7782": "aes_password", + "7466": "aes_password", + "7903": "aes_password", + "8105": "aes_password", + "7144": "aes_password", + "8069": "aes_password", + "8254": "aes_password", + "8573": "aes_password", + "7787": "aes_password", + "7577": "aes_password", + "8918": "aes_password", + "8767": "aes_password", + "7611": "aes_password", + "8233": "aes_password", + "7802": "aes_password", + "7129": "aes_password", + "8968": "aes_password", + "8217": "aes_password", + "8176": "aes_password", + "8027": "aes_password", + "7941": "aes_password", + "8070": "aes_password", + "8697": "aes_password", + "8798": "aes_password", + "8553": "aes_password", + "8510": "aes_password", + "7254": "aes_password", + "7386": "aes_password", + "8800": "aes_password", + "7712": "aes_password", + "7844": "aes_password", + "7535": "aes_password", + "8273": "aes_password", + "8875": "aes_password", + "8675": "aes_password", + "7396": "aes_password", + "7649": "aes_password", + "8623": "aes_password", + "7186": "aes_password", + "8125": "aes_password", + "7734": "aes_password", + "8275": "aes_password", + "7521": "aes_password", + "7135": "aes_password", + "8546": "aes_password", + "7867": "aes_password", + "8989": "aes_password", + "8787": "aes_password", + "8225": "aes_password", + "8457": "aes_password", + "7405": "aes_password", + "7588": "aes_password", + "7854": "aes_password", + "7789": "aes_password", + "8133": "aes_password", + "7641": "aes_password", + "8535": "aes_password", + "7849": "aes_password", + "7594": "aes_password", + "8313": "aes_password", + "8097": "aes_password", + "8030": "aes_password", + "8737": "aes_password", + "8260": "aes_password", + "8950": "aes_password", + "7249": "aes_password", + "7644": "aes_password", + "7912": "aes_password", + "8979": "aes_password", + "8998": "aes_password", + "8731": "aes_password", + "8662": "aes_password", + "7983": "aes_password", + "7035": "aes_password", + "7841": "aes_password", + "8600": "aes_password", + "7228": "aes_password", + "7071": "aes_password", + "8080": "aes_password", + "8713": "aes_password", + "7210": "aes_password", + "7935": "aes_password", + "8057": "aes_password", + "8242": "aes_password", + "7084": "aes_password", + "7070": "aes_password", + "7494": "aes_password", + "8451": "aes_password", + "8626": "aes_password", + "8618": "aes_password", + "7741": "aes_password", + "7118": "aes_password", + "7387": "aes_password", + "7715": "aes_password", + "8143": "aes_password", + "7668": "aes_password", + "7716": "aes_password", + "8101": "aes_password", + "7234": "aes_password", + "8021": "aes_password", + "7156": "aes_password", + "8392": "aes_password", + "7900": "aes_password", + "7055": "aes_password", + "8566": "aes_password", + "7869": "aes_password", + "7864": "aes_password", + "8265": "aes_password", + "8216": "aes_password", + "7738": "aes_password", + "7467": "aes_password", + "8447": "aes_password", + "8564": "aes_password", + "7767": "aes_password", + "7811": "aes_password", + "7898": "aes_password", + "8182": "aes_password", + "8065": "aes_password", + "7561": "aes_password", + "8545": "aes_password", + "7253": "aes_password", + "8173": "aes_password", + "7922": "aes_password", + "7951": "aes_password", + "7612": "aes_password", + "7324": "aes_password", + "7549": "aes_password", + "7858": "aes_password", + "8655": "aes_password", + "8211": "aes_password", + "8469": "aes_password", + "7298": "aes_password", + "8380": "aes_password", + "7394": "aes_password", + "7089": "aes_password", + "8060": "aes_password", + "7591": "aes_password", + "7542": "aes_password", + "7540": "aes_password", + "7456": "aes_password", + "7768": "aes_password", + "8489": "aes_password", + "8089": "aes_password", + "7838": "aes_password", + "8644": "aes_password", + "8344": "aes_password", + "7739": "aes_password", + "7984": "aes_password", + "7909": "aes_password", + "8517": "aes_password", + "8056": "aes_password", + "8297": "aes_password", + "8647": "aes_password", + "8334": "aes_password", + "7056": "aes_password", + "7164": "aes_password", + "8878": "aes_password", + "7816": "aes_password", + "7444": "aes_password", + "8996": "aes_password", + "8359": "aes_password", + "7901": "aes_password", + "8127": "aes_password", + "7424": "aes_password", + "8116": "aes_password", + "7354": "aes_password", + "8919": "aes_password", + "7214": "aes_password", + "7589": "aes_password", + "8982": "aes_password", + "8244": "aes_password", + "8295": "aes_password", + "7669": "aes_password", + "7562": "aes_password", + "8470": "aes_password", + "7474": "aes_password", + "7345": "aes_password", + "7799": "aes_password", + "8269": "aes_password", + "8213": "aes_password", + "8786": "aes_password", + "7482": "aes_password", + "8261": "aes_password", + "8755": "aes_password", + "8882": "aes_password", + "7166": "aes_password", + "7428": "aes_password", + "8766": "aes_password", + "7458": "aes_password", + "8372": "aes_password", + "8045": "aes_password", + "8185": "aes_password", + "7602": "aes_password", + "8373": "aes_password", + "7826": "aes_password", + "8249": "aes_password", + "8881": "aes_password", + "8830": "aes_password", + "8044": "aes_password", + "7563": "aes_password", + "7509": "aes_password", + "7290": "aes_password", + "7019": "aes_password", + "8454": "aes_password", + "8637": "aes_password", + "7876": "aes_password", + "8500": "aes_password", + "8226": "aes_password", + "7744": "aes_password", + "7753": "aes_password", + "7017": "aes_password", + "8362": "aes_password", + "7642": "aes_password", + "8091": "aes_password", + "7512": "aes_password", + "7708": "aes_password", + "8335": "aes_password", + "7292": "aes_password", + "8281": "aes_password", + "8132": "aes_password", + "7683": "aes_password", + "7048": "aes_password", + "7316": "aes_password", + "7011": "aes_password", + "8299": "aes_password", + "8440": "aes_password", + "8147": "aes_password", + "8280": "aes_password", + "8130": "aes_password", + "8303": "aes_password", + "8610": "aes_password", + "8361": "aes_password", + "8339": "aes_password", + "8037": "aes_password", + "8102": "aes_password", + "7845": "aes_password", + "7307": "aes_password", + "8607": "aes_password", + "8523": "aes_password", + "7839": "aes_password", + "7279": "aes_password", + "7321": "aes_password", + "8032": "aes_password", + "8894": "aes_password", + "8166": "aes_password", + "7381": "aes_password", + "8113": "aes_password", + "8139": "aes_password", + "8290": "aes_password", + "7990": "aes_password", + "7388": "aes_password", + "8571": "aes_password", + "8730": "aes_password", + "8441": "aes_password", + "8074": "aes_password", + "7813": "aes_password", + "8555": "aes_password", + "8978": "aes_password", + "7835": "aes_password", + "7323": "aes_password", + "7293": "aes_password", + "8550": "aes_password", + "7617": "aes_password", + "8071": "aes_password", + "7998": "aes_password", + "8115": "aes_password", + "7419": "aes_password", + "8825": "aes_password", + "8412": "aes_password", + "8019": "aes_password", + "8142": "aes_password", + "8186": "aes_password", + "8909": "aes_password", + "8078": "aes_password", + "8952": "aes_password", + "8360": "aes_password", + "8336": "aes_password", + "7953": "aes_password", + "7005": "aes_password", + "8663": "aes_password", + "8866": "aes_password", + "7950": "aes_password", + "7248": "aes_password", + "8519": "aes_password", + "8099": "aes_password", + "8151": "aes_password", + "8959": "aes_password", + "7042": "aes_password", + "7939": "aes_password", + "8064": "aes_password", + "8165": "aes_password", + "8836": "aes_password", + "8965": "aes_password", + "7431": "aes_password", + "7223": "aes_password", + "7999": "aes_password", + "8913": "aes_password", + "7921": "aes_password", + "8883": "aes_password", + "8169": "aes_password", + "7441": "aes_password", + "7469": "aes_password", + "7666": "aes_password", + "8547": "aes_password", + "7993": "aes_password", + "7705": "aes_password", + "8103": "aes_password", + "8524": "aes_password", + "8240": "aes_password", + "7779": "aes_password", + "7344": "aes_password", + "7395": "aes_password", + "8420": "aes_password", + "7287": "aes_password", + "7926": "aes_password", + "8100": "aes_password", + "8874": "aes_password", + "7496": "aes_password", + "7626": "aes_password", + "7784": "aes_password", + "7880": "aes_password", + "7226": "aes_password", + "8405": "aes_password", + "7910": "aes_password", + "8693": "aes_password", + "7997": "aes_password", + "8585": "aes_password", + "8383": "aes_password", + "8429": "aes_password", + "7878": "aes_password", + "8098": "aes_password", + "7288": "aes_password", + "8509": "aes_password", + "8809": "aes_password", + "7973": "aes_password", + "7620": "aes_password", + "7115": "aes_password", + "8445": "aes_password", + "8977": "aes_password", + "8341": "aes_password", + "7859": "aes_password", + "8256": "aes_password", + "7119": "aes_password", + "8442": "aes_password", + "8606": "aes_password", + "7992": "aes_password", + "7270": "aes_password", + "8988": "aes_password", + "7375": "aes_password", + "7747": "aes_password", + "7100": "aes_password", + "7639": "aes_password", + "7296": "aes_password", + "7435": "aes_password", + "7889": "aes_password", + "7636": "aes_password", + "7946": "aes_password", + "7819": "aes_password", + "7978": "aes_password", + "7728": "aes_password", + "8152": "aes_password", + "7660": "aes_password", + "7464": "aes_password", + "8398": "aes_password", + "8804": "aes_password", + "7450": "aes_password", + "8020": "aes_password", + "7988": "aes_password", + "7398": "aes_password", + "7171": "aes_password", + "8315": "aes_password", + "7871": "aes_password", + "8513": "aes_password", + "7250": "aes_password", + "8181": "aes_password", + "7793": "aes_password", + "7414": "aes_password", + "7179": "aes_password", + "7445": "aes_password", + "7259": "aes_password", + "8779": "aes_password", + "7695": "aes_password", + "7149": "aes_password", + "8973": "aes_password", + "8258": "aes_password", + "7291": "aes_password", + "8301": "aes_password", + "7447": "aes_password", + "8276": "aes_password", + "8279": "aes_password", + "7572": "aes_password", + "7146": "aes_password", + "7764": "aes_password", + "7504": "aes_password", + "7604": "aes_password", + "7465": "aes_password", + "7565": "aes_password", + "8976": "aes_password", + "8016": "aes_password", + "7707": "aes_password", + "7627": "aes_password", + "8379": "aes_password", + "7523": "aes_password", + "7145": "aes_password", + "7971": "aes_password", + "8790": "aes_password", + "8475": "aes_password", + "7718": "aes_password", + "7481": "aes_password", + "7650": "aes_password", + "7808": "aes_password", + "7142": "aes_password", + "8816": "aes_password", + "7676": "aes_password", + "7873": "aes_password", + "7885": "aes_password", + "7049": "aes_password", + "8994": "aes_password", + "8863": "aes_password", + "7178": "aes_password", + "8625": "aes_password", + "7237": "aes_password", + "7936": "aes_password", + "7575": "aes_password", + "7673": "aes_password", + "7689": "aes_password", + "7455": "aes_password", + "7780": "aes_password", + "8687": "aes_password", + "7675": "aes_password", + "8431": "aes_password", + "7030": "aes_password", + "8756": "aes_password", + "8318": "aes_password", + "7088": "aes_password", + "8792": "aes_password", + "7454": "aes_password", + "7776": "aes_password", + "8531": "aes_password", + "8829": "aes_password", + "8350": "aes_password", + "7198": "aes_password", + "7438": "aes_password", + "8543": "aes_password", + "8981": "aes_password", + "8915": "aes_password", + "8446": "aes_password", + "7812": "aes_password", + "7570": "aes_password", + "7054": "aes_password", + "8903": "aes_password", + "7213": "aes_password", + "8670": "aes_password", + "7211": "aes_password", + "7327": "aes_password", + "7121": "aes_password", + "8926": "aes_password", + "7430": "aes_password", + "8413": "aes_password", + "7337": "aes_password", + "7771": "aes_password", + "7350": "aes_password", + "7284": "aes_password", + "8252": "aes_password", + "7138": "aes_password", + "7491": "aes_password", + "8774": "aes_password", + "8828": "aes_password", + "7069": "aes_password", + "7545": "aes_password", + "7360": "aes_password", + "8771": "aes_password", + "7915": "aes_password", + "8485": "aes_password", + "7002": "aes_password", + "8838": "aes_password", + "7040": "aes_password", + "8820": "aes_password", + "8033": "aes_password", + "7053": "aes_password", + "8640": "aes_password", + "8552": "aes_password", + "8180": "aes_password", + "7361": "aes_password", + "7560": "aes_password", + "8277": "aes_password", + "7243": "aes_password", + "8999": "aes_password", + "7391": "aes_password", + "8930": "aes_password", + "8957": "aes_password", + "8504": "aes_password", + "7122": "aes_password", + "7139": "aes_password", + "8789": "aes_password", + "8245": "aes_password", + "7493": "aes_password", + "8810": "aes_password", + "8521": "aes_password", + "7317": "aes_password", + "7476": "aes_password", + "8562": "aes_password", + "7724": "aes_password", + "8652": "aes_password", + "7434": "aes_password", + "8110": "aes_password", + "8533": "aes_password", + "7970": "aes_password", + "8745": "aes_password", + "7585": "aes_password", + "7085": "aes_password", + "8421": "aes_password", + "8370": "aes_password", + "7289": "aes_password", + "7996": "aes_password", + "8840": "aes_password", + "7194": "aes_password", + "7256": "aes_password", + "8609": "aes_password", + "8604": "aes_password", + "7062": "aes_password", + "7252": "aes_password", + "8076": "aes_password", + "7647": "aes_password", + "8271": "aes_password", + "7684": "aes_password", + "8539": "aes_password", + "8588": "aes_password", + "8611": "aes_password", + "8654": "aes_password", + "7836": "aes_password", + "8351": "aes_password", + "7962": "aes_password", + "8230": "aes_password", + "7332": "aes_password", + "7573": "aes_password", + "7825": "aes_password", + "7934": "aes_password", + "8572": "aes_password", + "8700": "aes_password", + "8328": "aes_password", + "8046": "aes_password", + "7425": "aes_password", + "8729": "aes_password", + "8886": "aes_password", + "7806": "aes_password", + "7239": "aes_password", + "8601": "aes_password", + "8717": "aes_password", + "7697": "aes_password", + "8352": "aes_password", + "8123": "aes_password", + "7205": "aes_password", + "8298": "aes_password", + "7490": "aes_password", + "7023": "aes_password", + "7964": "aes_password", + "8986": "aes_password", + "7101": "aes_password", + "7634": "aes_password", + "7269": "aes_password", + "8000": "aes_password", + "8172": "aes_password", + "7172": "aes_password", + "8107": "aes_password", + "8486": "aes_password", + "8716": "aes_password", + "8961": "aes_password", + "7685": "aes_password", + "7667": "aes_password", + "7359": "aes_password", + "7804": "aes_password", + "7012": "aes_password", + "8975": "aes_password", + "7598": "aes_password", + "7662": "aes_password", + "7043": "aes_password", + "8331": "aes_password", + "8617": "aes_password", + "7015": "aes_password", + "8390": "aes_password", + "8608": "aes_password", + "7346": "aes_password", + "7331": "aes_password", + "8984": "aes_password", + "7339": "aes_password", + "8465": "aes_password", + "7184": "aes_password", + "7851": "aes_password", + "7868": "aes_password", + "7663": "aes_password", + "8849": "aes_password", + "8667": "aes_password", + "7506": "aes_password", + "7351": "aes_password", + "7109": "aes_password", + "7426": "aes_password", + "7453": "aes_password", + "7737": "aes_password", + "7870": "aes_password", + "8308": "aes_password", + "7543": "aes_password", + "7299": "aes_password", + "8340": "aes_password", + "8243": "aes_password", + "7357": "aes_password", + "8641": "aes_password", + "7553": "aes_password", + "8464": "aes_password", + "7966": "aes_password", + "8581": "aes_password", + "7343": "aes_password", + "8759": "aes_password", + "7631": "aes_password", + "7181": "aes_password", + "7415": "aes_password", + "8187": "aes_password", + "8924": "aes_password", + "7452": "aes_password", + "8797": "aes_password", + "8525": "aes_password", + "7026": "aes_password", + "7322": "aes_password", + "7982": "aes_password", + "8906": "aes_password", + "8342": "aes_password", + "7977": "aes_password", + "7059": "aes_password", + "7986": "aes_password", + "7513": "aes_password", + "7134": "aes_password", + "7462": "aes_password", + "7800": "aes_password", + "8966": "aes_password", + "8453": "aes_password", + "7760": "aes_password", + "7155": "aes_password", + "8632": "aes_password", + "7679": "aes_password", + "8808": "aes_password", + "8508": "aes_password", + "7479": "aes_password", + "7263": "aes_password", + "8648": "aes_password", + "7972": "aes_password", + "8215": "aes_password", + "8948": "aes_password", + "7894": "aes_password", + "7364": "aes_password", + "8520": "aes_password", + "8754": "aes_password", + "7773": "aes_password", + "7255": "aes_password", + "7413": "aes_password", + "7524": "aes_password", + "8511": "aes_password", + "8914": "aes_password", + "8557": "aes_password", + "8085": "aes_password", + "7938": "aes_password", + "8549": "aes_password", + "8839": "aes_password", + "7628": "aes_password", + "7852": "aes_password", + "8788": "aes_password", + "8680": "aes_password", + "8371": "aes_password", + "8378": "aes_password", + "7525": "aes_password", + "7338": "aes_password", + "8969": "aes_password", + "8895": "aes_password", + "7280": "aes_password", + "8827": "aes_password", + "7502": "aes_password", + "8126": "aes_password", + "7208": "aes_password", + "8643": "aes_password", + "7175": "aes_password", + "8183": "aes_password", + "7314": "aes_password", + "8818": "aes_password", + "8039": "aes_password", + "7392": "aes_password", + "7902": "aes_password", + "7576": "aes_password", + "8537": "aes_password", + "7618": "aes_password", + "7203": "aes_password", + "8306": "aes_password", + "7855": "aes_password", + "8778": "aes_password", + "7928": "aes_password", + "7866": "aes_password", + "8483": "aes_password", + "7152": "aes_password", + "7309": "aes_password", + "7222": "aes_password", + "8023": "aes_password", + "7619": "aes_password", + "8985": "aes_password", + "7643": "aes_password", + "7440": "aes_password", + "7646": "aes_password", + "7892": "aes_password", + "7061": "aes_password", + "8941": "aes_password", + "7846": "aes_password", + "8683": "aes_password", + "7829": "aes_password", + "8711": "aes_password", + "8235": "aes_password", + "8463": "aes_password", + "7899": "aes_password", + "8602": "aes_password", + "8114": "aes_password", + "7593": "aes_password", + "7385": "aes_password", + "7624": "aes_password", + "8659": "aes_password", + "7460": "aes_password", + "7917": "aes_password", + "8705": "aes_password", + "7713": "aes_password", + "7875": "aes_password", + "7788": "aes_password", + "7548": "aes_password", + "7076": "aes_password", + "8770": "aes_password", + "8506": "aes_password", + "8769": "aes_password", + "8197": "aes_password", + "7655": "aes_password", + "7247": "aes_password", + "8432": "aes_password", + "7783": "aes_password", + "8327": "aes_password", + "8120": "aes_password", + "7131": "aes_password", + "8268": "aes_password", + "7696": "aes_password", + "8783": "aes_password", + "8406": "aes_password", + "8433": "aes_password", + "8024": "aes_password", + "8005": "aes_password", + "7097": "aes_password", + "8554": "aes_password", + "8967": "aes_password", + "8943": "aes_password", + "7792": "aes_password", + "8003": "aes_password", + "8698": "aes_password", + "7229": "aes_password", + "7687": "aes_password", + "8532": "aes_password", + "7304": "aes_password", + "8122": "aes_password", + "8202": "aes_password", + "7833": "aes_password", + "8679": "aes_password", + "7580": "aes_password", + "8498": "aes_password", + "8345": "aes_password", + "8696": "aes_password", + "7422": "aes_password", + "8650": "aes_password", + "8905": "aes_password", + "7495": "aes_password", + "7075": "aes_password", + "8912": "aes_password", + "8570": "aes_password", + "8847": "aes_password", + "8613": "aes_password", + "7551": "aes_password", + "7150": "aes_password", + "7218": "aes_password", + "7514": "aes_password", + "7195": "aes_password", + "8775": "aes_password", + "8757": "aes_password", + "8251": "aes_password", + "7209": "aes_password", + "7162": "aes_password", + "7564": "aes_password", + "8121": "aes_password", + "7960": "aes_password", + "7656": "aes_password", + "7397": "aes_password", + "8699": "aes_password", + "7421": "aes_password", + "8263": "aes_password", + "8598": "aes_password", + "7009": "aes_password", + "7861": "aes_password", + "7001": "aes_password", + "8744": "aes_password", + "8368": "aes_password", + "7975": "aes_password", + "7165": "aes_password", + "8530": "aes_password", + "8614": "aes_password", + "7258": "aes_password", + "8034": "aes_password", + "7449": "aes_password", + "7932": "aes_password", + "7906": "aes_password", + "8124": "aes_password", + "8526": "aes_password", + "8568": "aes_password", + "7754": "aes_password", + "8228": "aes_password", + "8904": "aes_password", + "8161": "aes_password", + "7547": "aes_password", + "7310": "aes_password", + "7558": "aes_password", + "8338": "aes_password", + "8799": "aes_password", + "8224": "aes_password", + "8563": "aes_password", + "7863": "aes_password", + "7682": "aes_password", + "8944": "aes_password", + "7914": "aes_password", + "7541": "aes_password", + "8953": "aes_password", + "7318": "aes_password", + "8560": "aes_password", + "8972": "aes_password", + "7608": "aes_password", + "8455": "aes_password", + "7468": "aes_password", + "7037": "aes_password", + "7690": "aes_password", + "8814": "aes_password", + "8664": "aes_password", + "7756": "aes_password", + "7569": "aes_password", + "7102": "aes_password", + "8119": "aes_password", + "7161": "aes_password", + "7378": "aes_password", + "7790": "aes_password", + "7638": "aes_password", + "7678": "aes_password", + "7483": "aes_password", + "7063": "aes_password", + "8348": "aes_password", + "7974": "aes_password", + "8596": "aes_password", + "8653": "aes_password", + "8319": "aes_password", + "8933": "aes_password", + "8877": "aes_password", + "7886": "aes_password", + "8459": "aes_password", + "8008": "aes_password", + "8631": "aes_password", + "8482": "aes_password", + "8868": "aes_password", + "7031": "aes_password", + "8036": "aes_password", + "7530": "aes_password", + "7498": "aes_password", + "8497": "aes_password", + "8505": "aes_password", + "8323": "aes_password", + "7566": "aes_password", + "7038": "aes_password", + "7640": "aes_password", + "8690": "aes_password", + "8150": "aes_password", + "7235": "aes_password", + "8921": "aes_password", + "8196": "aes_password", + "8938": "aes_password", + "7531": "aes_password", + "8002": "aes_password", + "7439": "aes_password", + "7451": "aes_password", + "7583": "aes_password", + "7400": "aes_password", + "7242": "aes_password", + "8589": "aes_password", + "8826": "aes_password", + "8050": "aes_password", + "8960": "aes_password", + "7409": "aes_password", + "8873": "aes_password", + "8710": "aes_password", + "8932": "aes_password", + "7751": "aes_password", + "8846": "aes_password", + "7420": "aes_password", + "8817": "aes_password", + "7884": "aes_password", + "7124": "aes_password", + "8047": "aes_password", + "8081": "aes_password", + "8144": "aes_password", + "7477": "aes_password", + "7066": "aes_password", + "7862": "aes_password", + "8389": "aes_password", + "8321": "aes_password", + "8239": "aes_password", + "8987": "aes_password", + "7032": "aes_password", + "7522": "aes_password", + "7691": "aes_password", + "8567": "aes_password", + "8425": "aes_password", + "8128": "aes_password", + "8853": "aes_password", + "7092": "aes_password", + "8624": "aes_password", + "7416": "aes_password", + "7461": "aes_password", + "7283": "aes_password", + "7463": "aes_password", + "8956": "aes_password", + "8574": "aes_password", + "8872": "aes_password", + "8480": "aes_password", + "8384": "aes_password", + "8540": "aes_password", + "8381": "aes_password", + "7064": "aes_password", + "8095": "aes_password", + "8349": "aes_password", + "8855": "aes_password", + "7881": "aes_password", + "7519": "aes_password", + "7961": "aes_password", + "8728": "aes_password", + "8222": "aes_password", + "8326": "aes_password", + "8218": "aes_password", + "8723": "aes_password", + "7068": "aes_password", + "7202": "aes_password", + "7923": "aes_password", + "7347": "aes_password", + "8794": "aes_password", + "7264": "aes_password", + "7437": "aes_password", + "8494": "aes_password", + "7335": "aes_password", + "7717": "aes_password", + "7295": "aes_password", + "8061": "aes_password", + "8702": "aes_password", + "7824": "aes_password", + "8111": "aes_password", + "7176": "aes_password", + "8332": "aes_password", + "8556": "aes_password", + "7818": "aes_password", + "8583": "aes_password", + "8920": "aes_password", + "8006": "aes_password", + "8108": "aes_password", + "7308": "aes_password", + "8708": "aes_password", + "7587": "aes_password", + "7219": "aes_password", + "7367": "aes_password", + "8681": "aes_password", + "8945": "aes_password", + "8337": "aes_password", + "7586": "aes_password", + "7095": "aes_password", + "8880": "aes_password", + "8388": "aes_password", + "7065": "aes_password", + "7045": "aes_password", + "7111": "aes_password", + "8762": "aes_password", + "7703": "aes_password", + "8686": "aes_password", + "8367": "aes_password", + "8796": "aes_password", + "7840": "aes_password", + "7945": "aes_password", + "8329": "aes_password", + "7107": "aes_password", + "8167": "aes_password", + "8862": "aes_password", + "8951": "aes_password", + "7963": "aes_password", + "8073": "aes_password", + "8068": "aes_password", + "7759": "aes_password", + "8175": "aes_password", + "7704": "aes_password", + "8083": "aes_password", + "7206": "aes_password", + "8054": "aes_password", + "7160": "aes_password", + "8727": "aes_password", + "7333": "aes_password", + "7094": "aes_password", + "8753": "aes_password", + "7755": "aes_password", + "8577": "aes_password", + "7099": "aes_password", + "7334": "aes_password", + "8055": "aes_password", + "8397": "aes_password", + "8214": "aes_password", + "8366": "aes_password", + "8541": "aes_password", + "7312": "aes_password", + "8283": "aes_password", + "8052": "aes_password", + "7653": "aes_password", + "7402": "aes_password", + "7224": "aes_password", + "7217": "aes_password", + "7126": "aes_password", + "7710": "aes_password", + "8668": "aes_password", + "8401": "aes_password", + "8628": "aes_password", + "8424": "aes_password", + "7658": "aes_password", + "7471": "aes_password", + "8813": "aes_password", + "7374": "aes_password", + "8067": "aes_password", + "8703": "aes_password", + "7681": "aes_password", + "7382": "aes_password", + "8538": "aes_password", + "8676": "aes_password", + "7033": "aes_password", + "7480": "aes_password", + "8841": "aes_password", + "8834": "aes_password", + "8666": "aes_password", + "8084": "aes_password", + "7925": "aes_password", + "7742": "aes_password", + "8669": "aes_password", + "7532": "aes_password", + "8657": "aes_password", + "8358": "aes_password", + "8590": "aes_password", + "7746": "aes_password", + "7605": "aes_password", + "7905": "aes_password", + "8294": "aes_password", + "8980": "aes_password", + "8735": "aes_password", + "8179": "aes_password", + "8760": "aes_password", + "7036": "aes_password", + "7404": "aes_password", + "8062": "aes_password", + "8201": "aes_password", + "7320": "aes_password", + "8964": "aes_password", + "8402": "aes_password", + "7303": "aes_password", + "8765": "aes_password", + "7803": "aes_password", + "8247": "aes_password", + "8449": "aes_password", + "7278": "aes_password", + "7189": "aes_password", + "8284": "aes_password", + "8848": "aes_password", + "7809": "aes_password", + "7503": "aes_password", + "8419": "aes_password", + "7723": "aes_password", + "7446": "aes_password", + "7556": "aes_password", + "8739": "aes_password", + "8514": "aes_password", + "7486": "aes_password", + "7143": "aes_password", + "8709": "aes_password", + "7105": "aes_password", + "7654": "aes_password", + "8436": "aes_password", + "8761": "aes_password", + "7595": "aes_password", + "8209": "aes_password", + "8029": "aes_password", + "7603": "aes_password", + "8257": "aes_password", + "8649": "aes_password", + "7370": "aes_password", + "8534": "aes_password", + "7267": "aes_password", + "8565": "aes_password", + "7664": "aes_password", + "8551": "aes_password", + "7888": "aes_password", + "8576": "aes_password", + "8893": "aes_password", + "8857": "aes_password", + "7599": "aes_password", + "7568": "aes_password", + "7791": "aes_password", + "7916": "aes_password", + "8885": "aes_password", + "7418": "aes_password", + "7245": "aes_password", + "8170": "aes_password", + "8466": "aes_password", + "7098": "aes_password", + "7489": "aes_password", + "8997": "aes_password", + "8014": "aes_password", + "8805": "aes_password", + "7774": "aes_password", + "8325": "aes_password", + "8262": "aes_password", + "8038": "aes_password", + "8512": "aes_password", + "7546": "aes_password", + "7614": "aes_password", + "8309": "aes_password", + "7123": "aes_password", + "8324": "aes_password", + "8591": "aes_password", + "8255": "aes_password", + "7623": "aes_password", + "8270": "aes_password", + "8715": "aes_password", + "8515": "aes_password", + "8088": "aes_password", + "8375": "aes_password", + "7693": "aes_password", + "8752": "aes_password", + "8059": "aes_password", + "7271": "aes_password", + "8917": "aes_password", + "7500": "aes_password", + "8656": "aes_password", + "7022": "aes_password", + "8812": "aes_password", + "8858": "aes_password", + "7980": "aes_password", + "7090": "aes_password", + "7637": "aes_password", + "7671": "aes_password", + "7904": "aes_password", + "8629": "aes_password", + "7034": "aes_password", + "7207": "aes_password", + "7096": "aes_password", + "7621": "aes_password", + "7770": "aes_password", + "8221": "aes_password", + "7077": "aes_password", + "7661": "aes_password", + "7994": "aes_password", + "7632": "aes_password", + "7848": "aes_password", + "8822": "aes_password", + "7328": "aes_password", + "8438": "aes_password", + "8939": "aes_password", + "8636": "aes_password", + "7995": "aes_password", + "8363": "aes_password", + "7010": "aes_password", + "7212": "aes_password", + "8559": "aes_password", + "8548": "aes_password", + "7275": "aes_password", + "8819": "aes_password", + "7927": "aes_password", + "8291": "aes_password", + "7652": "aes_password", + "7954": "aes_password", + "7832": "aes_password", + "8087": "aes_password", + "8646": "aes_password", + "7736": "aes_password", + "7581": "aes_password", + "7216": "aes_password", + "7190": "aes_password", + "7180": "aes_password", + "7412": "aes_password", + "8569": "aes_password", + "8010": "aes_password", + "7529": "aes_password", + "8118": "aes_password", + "8162": "aes_password", + "8925": "aes_password", + "8490": "aes_password", + "7762": "aes_password", + "7610": "aes_password", + "8439": "aes_password", + "7698": "aes_password", + "7648": "aes_password", + "8742": "aes_password", + "8993": "aes_password", + "7785": "aes_password", + "7609": "aes_password", + "7896": "aes_password", + "8851": "aes_password", + "7731": "aes_password", + "8248": "aes_password", + "8386": "aes_password", + "8621": "aes_password", + "7349": "aes_password", + "7366": "aes_password", + "7169": "aes_password", + "7942": "aes_password", + "7633": "aes_password", + "7781": "aes_password", + "8387": "aes_password", + "8117": "aes_password", + "8782": "aes_password", + "8192": "aes_password", + "8104": "aes_password", + "7615": "aes_password", + "7050": "aes_password", + "7315": "aes_password", + "7725": "aes_password", + "7058": "aes_password", + "7865": "aes_password", + "7133": "aes_password", + "8458": "aes_password", + "7552": "aes_password", + "7761": "aes_password", + "8229": "aes_password", + "7677": "aes_password", + "8619": "aes_password", + "8193": "aes_password", + "8164": "aes_password", + "8493": "aes_password", + "7943": "aes_password", + "8476": "aes_password", + "8427": "aes_password", + "7240": "aes_password", + "8773": "aes_password", + "8749": "aes_password", + "8983": "aes_password", + "8910": "aes_password", + "7722": "aes_password", + "8092": "aes_password", + "8086": "aes_password", + "7911": "aes_password", + "8793": "aes_password", + "8725": "aes_password", + "7201": "aes_password", + "7538": "aes_password", + "7016": "aes_password", + "8396": "aes_password", + "7726": "aes_password", + "8495": "aes_password", + "8300": "aes_password", + "7948": "aes_password", + "8237": "aes_password", + "8802": "aes_password", + "7074": "aes_password", + "8077": "aes_password", + "7516": "aes_password", + "8908": "aes_password", + "8616": "aes_password", + "7814": "aes_password", + "8688": "aes_password", + "7236": "aes_password", + "7537": "aes_password", + "8691": "aes_password", + "8109": "aes_password", + "8223": "aes_password", + "7895": "aes_password", + "8018": "aes_password", + "7820": "aes_password", + "7407": "aes_password", + "8544": "aes_password", + "7651": "aes_password", + "8288": "aes_password", + "7930": "aes_password", + "8004": "aes_password", + "8935": "aes_password", + "7272": "aes_password", + "8707": "aes_password", + "8907": "aes_password", + "8146": "aes_password", + "7850": "aes_password", + "8579": "aes_password", + "7125": "aes_password", + "8692": "aes_password", + "7828": "aes_password", + "7113": "aes_password", + "7027": "aes_password", + "8148": "aes_password", + "7078": "aes_password", + "7508": "aes_password", + "8434": "aes_password", + "8374": "aes_password", + "8356": "aes_password", + "8942": "aes_password", + "8437": "aes_password", + "8902": "aes_password", + "7887": "aes_password", + "7281": "aes_password", + "8593": "aes_password", + "7853": "aes_password", + "8898": "aes_password", + "8927": "aes_password", + "8184": "aes_password", + "7879": "aes_password", + "8409": "aes_password", + "8241": "aes_password", + "7013": "aes_password", + "8145": "aes_password", + "8094": "aes_password", + "8450": "aes_password", + "7353": "aes_password", + "7286": "aes_password", + "7393": "aes_password", + "8852": "aes_password", + "8456": "aes_password", + "8660": "aes_password", + "7908": "aes_password", + "7408": "aes_password", + "8784": "aes_password", + "8990": "aes_password", + "8899": "aes_password", + "8748": "aes_password", + "8385": "aes_password", + "7151": "aes_password", + "7355": "aes_password", + "8051": "aes_password", + "7842": "aes_password", + "7985": "aes_password", + "7081": "aes_password", + "8620": "aes_password", + "7924": "aes_password", + "7305": "aes_password", + "8949": "aes_password", + "7091": "aes_password", + "7947": "aes_password", + "8075": "aes_password", + "8721": "aes_password", + "8605": "aes_password", + "8305": "aes_password", + "7185": "aes_password", + "7831": "aes_password", + "8672": "aes_password", + "8253": "aes_password", + "8274": "aes_password", + "8746": "aes_password", + "8738": "aes_password", + "8015": "aes_password", + "8007": "aes_password", + "8768": "aes_password", + "7492": "aes_password", + "7752": "aes_password", + "8391": "aes_password", + "7596": "aes_password", + "7797": "aes_password", + "8940": "aes_password", + "8304": "aes_password", + "7706": "aes_password", + "7060": "aes_password", + "7251": "aes_password", + "7472": "aes_password", + "7340": "aes_password", + "7607": "aes_password", + "7215": "aes_password", + "7136": "aes_password", + "8171": "aes_password", + "8740": "aes_password", + "8343": "aes_password", + "7024": "aes_password", + "7635": "aes_password", + "8689": "aes_password", + "7158": "aes_password", + "8320": "aes_password", + "8516": "aes_password", + "8238": "aes_password", + "8156": "aes_password", + "8149": "aes_password", + "8199": "aes_password", + "7822": "aes_password", + "7517": "aes_password", + "8191": "aes_password" + }, + "method": "aes-256-cfb", + "timeout": 60 +} \ No newline at end of file diff --git a/tests/server-multi-passwd-table.json b/tests/server-multi-passwd-table.json new file mode 100755 index 0000000..a2c0a80 --- /dev/null +++ b/tests/server-multi-passwd-table.json @@ -0,0 +1,19 @@ +{ + "server": "127.0.0.1", + "server_port": 8384, + "local_port": 1081, + "password": "foobar4", + "port_password": { + "8381": "foobar1", + "8382": "foobar2", + "8383": "foobar3", + "8384": "foobar4", + "8385": "foobar5", + "8386": "foobar6", + "8387": "foobar7", + "8388": "foobar8", + "8389": "foobar9" + }, + "timeout": 60, + "method": "table" +} diff --git a/tests/server-multi-passwd.json b/tests/server-multi-passwd.json new file mode 100755 index 0000000..b1407f0 --- /dev/null +++ b/tests/server-multi-passwd.json @@ -0,0 +1,17 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + "8381": "foobar1", + "8382": "foobar2", + "8383": "foobar3", + "8384": "foobar4", + "8385": "foobar5", + "8386": "foobar6", + "8387": "foobar7", + "8388": "foobar8", + "8389": "foobar9" + }, + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/server-multi-ports.json b/tests/server-multi-ports.json new file mode 100755 index 0000000..5bdbcab --- /dev/null +++ b/tests/server-multi-ports.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "server_port": [8384, 8345, 8346, 8347], + "local_port": 1081, + "password": "foobar4", + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/setup_tc.sh b/tests/setup_tc.sh new file mode 100755 index 0000000..1a5fa20 --- /dev/null +++ b/tests/setup_tc.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +DEV=lo +PORT=8388 +DELAY=100ms + +type tc 2> /dev/null && ( + tc qdisc add dev $DEV root handle 1: htb + tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps + tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0 + tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6 + + tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6 + tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6 + + tc qdisc show dev lo +) + diff --git a/tests/socksify/install.sh b/tests/socksify/install.sh new file mode 100755 index 0000000..8eff72d --- /dev/null +++ b/tests/socksify/install.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ ! -d dante-1.4.0 ]; then + wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 + tar xf dante-1.4.0.tar.gz || exit 1 +fi +pushd dante-1.4.0 +./configure && make -j4 && make install || exit 1 +popd +cp tests/socksify/socks.conf /etc/ || exit 1 diff --git a/tests/socksify/socks.conf b/tests/socksify/socks.conf new file mode 100755 index 0000000..13db772 --- /dev/null +++ b/tests/socksify/socks.conf @@ -0,0 +1,5 @@ +route { + from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1081 + proxyprotocol: socks_v5 + method: none +} \ No newline at end of file diff --git a/tests/table.json b/tests/table.json new file mode 100755 index 0000000..cca6ac2 --- /dev/null +++ b/tests/table.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"table_password", + "timeout":60, + "method":"table", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/test.py b/tests/test.py new file mode 100755 index 0000000..29b57d4 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os +import signal +import select +import time +import argparse +from subprocess import Popen, PIPE + +python = ['python'] + +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() + +if config.with_coverage: + python = ['coverage', 'run', '-p', '-a'] + +client_args = python + ['shadowsocks/local.py', '-v'] +server_args = python + ['shadowsocks/server.py', '-v'] + +if config.client_conf: + client_args.extend(['-c', config.client_conf]) + if config.server_conf: + server_args.extend(['-c', config.server_conf]) + else: + server_args.extend(['-c', config.client_conf]) +if config.client_args: + client_args.extend(config.client_args.split()) + if config.server_args: + server_args.extend(config.server_args.split()) + else: + server_args.extend(config.client_args.split()) +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) +p3 = None +p4 = None +p3_fin = False +p4_fin = False + +# 1 shadowsocks started +# 2 curl started +# 3 curl finished +# 4 dig started +# 5 dig finished +stage = 1 + +try: + local_ready = False + server_ready = False + fdset = [p1.stdout, p2.stdout, p1.stderr, p2.stderr] + while True: + r, w, e = select.select(fdset, [], fdset) + if e: + break + + for fd in r: + line = fd.readline() + if not line: + if stage == 2 and fd == p3.stdout: + stage = 3 + if stage == 4 and fd == p4.stdout: + stage = 5 + if bytes != str: + line = str(line, 'utf8') + sys.stderr.write(line) + if line.find('starting local') >= 0: + local_ready = True + if line.find('starting server') >= 0: + server_ready = True + + if stage == 1: + time.sleep(2) + + p3 = Popen(['curl', 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) + if p3 is not None: + fdset.append(p3.stdout) + fdset.append(p3.stderr) + stage = 2 + else: + sys.exit(1) + + if stage == 3 and p3 is not None: + fdset.remove(p3.stdout) + fdset.remove(p3.stderr) + r = p3.wait() + if 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) + fdset.append(p4.stderr) + stage = 4 + else: + sys.exit(1) + + if stage == 5: + r = p4.wait() + 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.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_graceful_restart.sh b/tests/test_graceful_restart.sh new file mode 100755 index 0000000..d91ba92 --- /dev/null +++ b/tests/test_graceful_restart.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +PYTHON="coverage run -p -a" +URL=http://127.0.0.1/file + + +# setup processes +$PYTHON shadowsocks/local.py -c tests/graceful.json & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & +SERVER=$! + +python tests/graceful_server.py & +GSERVER=$! + +sleep 1 + +python tests/graceful_cli.py & +GCLI=$! + +sleep 1 + +# graceful restart server: send SIGQUIT to old process and start a new one +kill -s SIGQUIT $SERVER +sleep 0.5 +$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & +NEWSERVER=$! + +sleep 1 + +# check old server +ps x | grep -v grep | grep $SERVER +OLD_SERVER_RUNNING1=$? +# old server should not quit at this moment +echo old server running: $OLD_SERVER_RUNNING1 + +sleep 1 + +# close connections on old server +kill -s SIGKILL $GCLI +kill -s SIGKILL $GSERVER +kill -s SIGINT $LOCAL + +sleep 11 + +# check old server +ps x | grep -v grep | grep $SERVER +OLD_SERVER_RUNNING2=$? +# old server should quit at this moment +echo old server running: $OLD_SERVER_RUNNING2 + +kill -s SIGINT $SERVER +# new server is expected running +kill -s SIGINT $NEWSERVER || exit 1 + +if [ $OLD_SERVER_RUNNING1 -ne 0 ]; then + exit 1 +fi + +if [ $OLD_SERVER_RUNNING2 -ne 1 ]; then + sleep 1 + exit 1 +fi 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/tests/test_udp_src.py b/tests/test_udp_src.py new file mode 100755 index 0000000..e8fa505 --- /dev/null +++ b/tests/test_udp_src.py @@ -0,0 +1,83 @@ +#!/usr/bin/python + +import socket +import socks + + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 1081 + + +if __name__ == '__main__': + # Test 1: same source port IPv4 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('127.0.0.1', 9001)) + sock_in2.bind(('127.0.0.1', 9002)) + + sock_out.sendto(b'data', ('127.0.0.1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('127.0.0.1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # make sure they're from the same source port + assert result1 == result2 + + # Test 2: same source port IPv6 + # try again from the same port but IPv6 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('::1', 9001)) + sock_in2.bind(('::1', 9002)) + + sock_out.sendto(b'data', ('::1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('::1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # make sure they're from the same source port + assert result1 == result2 + + # Test 3: different source ports IPv6 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9003)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in1.bind(('::1', 9001)) + sock_out.sendto(b'data', ('::1', 9001)) + result3 = sock_in1.recvfrom(8) + + # make sure they're from different source ports + assert result1 != result3 + + sock_out.close() + sock_in1.close() diff --git a/tests/test_udp_src.sh b/tests/test_udp_src.sh new file mode 100755 index 0000000..d356581 --- /dev/null +++ b/tests/test_udp_src.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PYTHON="coverage run -p -a" + +mkdir -p tmp + +$PYTHON shadowsocks/local.py -c tests/aes.json -v & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & +SERVER=$! + +sleep 3 + +python tests/test_udp_src.py +r=$? + +kill -s SIGINT $LOCAL +kill -s SIGINT $SERVER + +sleep 2 + +exit $r diff --git a/tests/workers.json b/tests/workers.json new file mode 100755 index 0000000..2015ff6 --- /dev/null +++ b/tests/workers.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"workers_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "workers": 4 +} diff --git a/utils/README.md b/utils/README.md new file mode 100755 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) diff --git a/utils/fail2ban/shadowsocks.conf b/utils/fail2ban/shadowsocks.conf new file mode 100755 index 0000000..9b1c7ec --- /dev/null +++ b/utils/fail2ban/shadowsocks.conf @@ -0,0 +1,5 @@ +[Definition] + +_daemon = shadowsocks + +failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$