├── .gitignore ├── .travis.yml ├── CHANGES ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── debian ├── changelog ├── compat ├── config.json ├── control ├── copyright ├── docs ├── init.d ├── install ├── rules ├── shadowsocks.default ├── shadowsocks.manpages ├── source │ └── format ├── sslocal.1 └── ssserver.1 ├── setup.py ├── shadowsocks ├── __init__.py ├── asyncdns.py ├── common.py ├── crypto │ ├── __init__.py │ ├── openssl.py │ ├── rc4_md5.py │ ├── sodium.py │ ├── table.py │ └── util.py ├── daemon.py ├── encrypt.py ├── eventloop.py ├── local.py ├── lru_cache.py ├── manager.py ├── server.py ├── shell.py ├── tcprelay.py └── udprelay.py ├── tests ├── aes-cfb1.json ├── aes-cfb8.json ├── aes-ctr.json ├── aes.json ├── assert.sh ├── chacha20.json ├── client-multi-server-ip.json ├── coverage_server.py ├── fastopen.json ├── gen_multiple_passwd.py ├── graceful.json ├── graceful_cli.py ├── graceful_server.py ├── ipv6-client-side.json ├── ipv6.json ├── jenkins.sh ├── libsodium │ └── install.sh ├── nose_plugin.py ├── rc4-md5.json ├── salsa20-ctr.json ├── salsa20.json ├── server-dnsserver.json ├── server-multi-passwd-client-side.json ├── server-multi-passwd-empty.json ├── server-multi-passwd-performance.json ├── server-multi-passwd-table.json ├── server-multi-passwd.json ├── server-multi-ports.json ├── setup_tc.sh ├── socksify │ ├── install.sh │ └── socks.conf ├── table.json ├── test.py ├── test_command.sh ├── test_daemon.sh ├── test_graceful_restart.sh ├── test_large_file.sh ├── test_udp_src.py ├── test_udp_src.sh └── workers.json └── utils ├── README.md ├── autoban.py └── fail2ban └── shadowsocks.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | htmlcov 21 | .coverage* 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | .DS_Store 31 | .idea 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.3 6 | - 3.4 7 | cache: 8 | directories: 9 | - dante-1.4.0 10 | before_install: 11 | - sudo apt-get update -qq 12 | - sudo apt-get install -qq build-essential dnsutils iproute nginx bc 13 | - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 14 | - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" 15 | - sudo service nginx restart 16 | - pip install pep8 pyflakes nose coverage PySocks 17 | - sudo tests/socksify/install.sh 18 | - sudo tests/libsodium/install.sh 19 | - sudo tests/setup_tc.sh 20 | script: 21 | - tests/jenkins.sh 22 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2.8.2 2015-08-10 2 | - Fix a encoding problem in manager 3 | 4 | 2.8.1 2015-08-06 5 | - Respond ok to add and remove commands 6 | 7 | 2.8 2015-08-06 8 | - Add Shadowsocks manager 9 | 10 | 2.7 2015-08-02 11 | - Optimize speed for multiple ports 12 | 13 | 2.6.11 2015-07-10 14 | - Fix a compatibility issue in UDP Relay 15 | 16 | 2.6.10 2015-06-08 17 | - Optimize LRU cache 18 | - Refine logging 19 | 20 | 2.6.9 2015-05-19 21 | - Fix a stability issue on Windows 22 | 23 | 2.6.8 2015-02-10 24 | - Support multiple server ip on client side 25 | - Support --version 26 | - Minor fixes 27 | 28 | 2.6.7 2015-02-02 29 | - Support --user 30 | - Support CIDR format in --forbidden-ip 31 | - Minor fixes 32 | 33 | 2.6.6 2015-01-23 34 | - Fix a crash in forbidden list 35 | 36 | 2.6.5 2015-01-18 37 | - Try both 32 bit and 64 bit dll on Windows 38 | 39 | 2.6.4 2015-01-14 40 | - Also search lib* when searching libraries 41 | 42 | 2.6.3 2015-01-12 43 | - Support --forbidden-ip to ban some IP, i.e. localhost 44 | - Search OpenSSL and libsodium harder 45 | - Now works on OpenWRT 46 | 47 | 2.6.2 2015-01-03 48 | - Log client IP 49 | 50 | 2.6.1 2014-12-26 51 | - Fix a problem with TCP Fast Open on local side 52 | - Fix sometimes daemon_start returns wrong exit status 53 | 54 | 2.6 2014-12-21 55 | - Add daemon support 56 | 57 | 2.5 2014-12-11 58 | - Add salsa20 and chacha20 59 | 60 | 2.4.3 2014-11-10 61 | - Fix an issue on Python 3 62 | - Fix an issue with IPv6 63 | 64 | 2.4.2 2014-11-06 65 | - Fix command line arguments on Python 3 66 | - Support table on Python 3 67 | - Fix TCP Fast Open on Python 3 68 | 69 | 2.4.1 2014-11-01 70 | - Fix setup.py for non-utf8 locales on Python 3 71 | 72 | 2.4 2014-11-01 73 | - Python 3 support 74 | - Performance improvement 75 | - Fix LRU cache behavior 76 | 77 | 2.3.2 2014-10-11 78 | - Fix OpenSSL on Windows 79 | 80 | 2.3.1 2014-10-09 81 | - Does not require M2Crypto any more 82 | 83 | 2.3 2014-09-23 84 | - Support CFB1, CFB8 and CTR mode of AES 85 | - Do not require password config when using port_password 86 | - Use SIGTERM instead of SIGQUIT on Windows 87 | 88 | 2.2.2 2014-09-14 89 | - Fix when multiple DNS set, IPv6 only sites are broken 90 | 91 | 2.2.1 2014-09-10 92 | - Support graceful shutdown 93 | - Fix some bugs 94 | 95 | 2.2.0 2014-09-09 96 | - Add RC4-MD5 encryption 97 | 98 | 2.1.0 2014-08-10 99 | - Use only IPv4 DNS server 100 | - Does not ship config.json 101 | - Better error message 102 | 103 | 2.0.12 2014-07-26 104 | - Support -q quiet mode 105 | - Exit 0 when showing help with -h 106 | 107 | 2.0.11 2014-07-12 108 | - Prefers IP addresses over hostnames, more friendly with socksify and openvpn 109 | 110 | 2.0.10 2014-07-11 111 | - Fix UDP on local 112 | 113 | 2.0.9 2014-07-06 114 | - Fix EWOULDBLOCK on Windows 115 | - Fix Unicode config problem on some platforms 116 | 117 | 2.0.8 2014-06-23 118 | - Use multiple DNS to query hostnames 119 | 120 | 2.0.7 2014-06-21 121 | - Fix fastopen on local 122 | - Fallback when fastopen is not available 123 | - Add verbose logging mode -vv 124 | - Verify if hostname is valid 125 | 126 | 2.0.6 2014-06-19 127 | - Fix CPU 100% on POLL_HUP 128 | - More friendly logging 129 | 130 | 2.0.5 2014-06-18 131 | - Support a simple config format for multiple ports 132 | 133 | 2.0.4 2014-06-12 134 | - Fix worker master 135 | 136 | 2.0.3 2014-06-11 137 | - Fix table encryption with UDP 138 | 139 | 2.0.2 2014-06-11 140 | - Add asynchronous DNS in TCP relay 141 | 142 | 2.0.1 2014-06-05 143 | - Better logging 144 | - Maybe fix bad file descriptor 145 | 146 | 2.0 2014-06-05 147 | - Use a new event model 148 | - Remove gevent 149 | - Refuse to use default password 150 | - Fix a problem when using multiple passwords with table encryption 151 | 152 | 1.4.5 2014-05-24 153 | - Add timeout in TCP server 154 | - Close sockets in master process 155 | 156 | 1.4.4 2014-05-17 157 | - Support multiple workers 158 | 159 | 1.4.3 2014-05-13 160 | - Fix Windows 161 | 162 | 1.4.2 2014-05-10 163 | - Add salsa20-ctr cipher 164 | 165 | 1.4.1 2014-05-03 166 | - Fix error log 167 | - Fix EINPROGESS with some version of gevent 168 | 169 | 1.4.0 2014-05-02 170 | - Adds UDP relay 171 | - TCP fast open support on Linux 3.7+ 172 | 173 | 1.3.7 2014-04-10 174 | - Fix a typo in help 175 | 176 | 1.3.6 2014-04-10 177 | - Fix a typo in help 178 | 179 | 1.3.5 2014-04-07 180 | - Add help 181 | - Change default local binding address into 127.0.0.1 182 | 183 | 1.3.4 2014-02-17 184 | - Fix a bug when no config file exists 185 | - Client now support multiple server ports and multiple server/port pairs 186 | - Better error message with bad config.json format and wrong password 187 | 188 | 1.3.3 2013-07-09 189 | - Fix default key length of rc2 190 | 191 | 1.3.2 2013-07-04 192 | - Server will listen at server IP specified in config 193 | - Check config file and show some warning messages 194 | 195 | 1.3.1 2013-06-29 196 | - Fix -c arg 197 | 198 | 1.3.0 2013-06-22 199 | - Move to pypi 200 | 201 | 1.2.3 2013-06-14 202 | - add bind address 203 | 204 | 1.2.2 2013-05-31 205 | - local can listen at ::0 with -6 arg; bump 1.2.2 206 | 207 | 1.2.1 2013-05-23 208 | - Fix an OpenSSL crash 209 | 210 | 1.2 2013-05-22 211 | - Use random iv, we finally have strong encryption 212 | 213 | 1.1.1 2013-05-21 214 | - Add encryption, AES, blowfish, etc. 215 | 216 | 1.1 2013-05-16 217 | - Support IPv6 addresses (type 4) 218 | - Drop Python 2.5 support 219 | 220 | 1.0 2013-04-03 221 | - Fix -6 IPv6 222 | 223 | 0.9.4 2013-03-04 224 | - Support Python 2.5 225 | 226 | 0.9.3 2013-01-14 227 | - Fix conn termination null data 228 | 229 | 0.9.2 2013-01-05 230 | - Change default timeout 231 | 232 | 0.9.1 2013-01-05 233 | - Add Travis-CI test 234 | 235 | 0.9 2012-12-30 236 | - Replace send with sendall, fix FreeBSD 237 | 238 | 0.6 2012-12-06 239 | - Support args 240 | 241 | 0.5 2012-11-08 242 | - Fix encryption with negative md5sum 243 | 244 | 0.4 2012-11-02 245 | - Move config into a JSON file 246 | - Auto-detect config path 247 | 248 | 0.3 2012-06-06 249 | - Move socks5 negotiation to local 250 | 251 | 0.2 2012-05-11 252 | - Add -6 arg for IPv6 253 | - Fix socket.error 254 | 255 | 0.1 2012-04-20 256 | - Initial version 257 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | 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. 5 | 6 | Pull Requests 7 | ------------- 8 | 9 | 1. Pull requests are welcome. If you would like to add a large feature 10 | or make a significant change, make sure to open an issue to discuss with 11 | people first. 12 | 2. Follow PEP8. 13 | 3. Make sure to pass the unit tests. Write unit tests for new modules if 14 | needed. 15 | 16 | Issues 17 | ------ 18 | 19 | 1. Only bugs and feature requests are accepted here. 20 | 2. We'll only work on important features. If the feature you're asking only 21 | benefits a few people, you'd better implement the feature yourself and send us 22 | a pull request, or ask some of your friends to do so. 23 | 3. We don't answer questions of any other types here. Since very few people 24 | are watching the issue tracker here, you'll probably get no help from here. 25 | Read [Troubleshooting] and get help from forums or [mailing lists]. 26 | 4. Issues in languages other than English will be Google translated into English 27 | later. 28 | 29 | 30 | [Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting 31 | [mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include shadowsocks *.py 2 | include README.rst 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shadowsocks 2 | =========== 3 | 4 | A fast tunnel proxy that helps you bypass firewalls. 5 | 6 | Features: 7 | - TCP & UDP support 8 | - User management API 9 | - TCP Fast Open 10 | - Workers and graceful restart 11 | - Destination IP blacklist 12 | 13 | Server 14 | ------ 15 | 16 | ### Install 17 | 18 | Debian / Ubuntu: 19 | 20 | apt-get install python-pip 21 | pip install shadowsocks 22 | 23 | CentOS: 24 | 25 | yum install python-setuptools && easy_install pip 26 | pip install shadowsocks 27 | 28 | Windows: 29 | 30 | See [Install Server on Windows] 31 | 32 | ### Usage 33 | 34 | ssserver -p 443 -k password -m aes-256-cfb 35 | 36 | To run in the background: 37 | 38 | sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start 39 | 40 | To stop: 41 | 42 | sudo ssserver -d stop 43 | 44 | To check the log: 45 | 46 | sudo less /var/log/shadowsocks.log 47 | 48 | Check all the options via `-h`. You can also use a [Configuration](https://github.com/neo-shadowsocks/shadowsocks/wiki/Configuration-via-Config-File) file 49 | instead. 50 | 51 | Documentation 52 | ------------- 53 | 54 | You can find all the documentation in the [Wiki](https://github.com/neo-shadowsocks/shadowsocks/wiki). 55 | 56 | License 57 | ------- 58 | 59 | Apache License 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | shadowsocks 2 | =========== 3 | 4 | |PyPI version| |Build Status| |Coverage Status| 5 | 6 | A fast tunnel proxy that helps you bypass firewalls. 7 | 8 | Server 9 | ------ 10 | 11 | Install 12 | ~~~~~~~ 13 | 14 | Debian / Ubuntu: 15 | 16 | :: 17 | 18 | apt-get install python-pip 19 | pip install shadowsocks 20 | 21 | CentOS: 22 | 23 | :: 24 | 25 | yum install python-setuptools && easy_install pip 26 | pip install shadowsocks 27 | 28 | Windows: 29 | 30 | See `Install Server on 31 | Windows `__ 32 | 33 | Usage 34 | ~~~~~ 35 | 36 | :: 37 | 38 | ssserver -p 443 -k password -m rc4-md5 39 | 40 | To run in the background: 41 | 42 | :: 43 | 44 | sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start 45 | 46 | To stop: 47 | 48 | :: 49 | 50 | sudo ssserver -d stop 51 | 52 | To check the log: 53 | 54 | :: 55 | 56 | sudo less /var/log/shadowsocks.log 57 | 58 | Check all the options via ``-h``. You can also use a 59 | `Configuration `__ 60 | file instead. 61 | 62 | Client 63 | ------ 64 | 65 | - `Windows `__ 66 | / `OS 67 | X `__ 68 | - `Android `__ 69 | / `iOS `__ 70 | - `OpenWRT `__ 71 | 72 | Use GUI clients on your local PC/phones. Check the README of your client 73 | for more information. 74 | 75 | Documentation 76 | ------------- 77 | 78 | You can find all the documentation in the 79 | `Wiki `__. 80 | 81 | License 82 | ------- 83 | 84 | Copyright 2015 clowwindy 85 | 86 | Licensed under the Apache License, Version 2.0 (the "License"); you may 87 | not use this file except in compliance with the License. You may obtain 88 | a copy of the License at 89 | 90 | :: 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | 100 | Bugs and Issues 101 | --------------- 102 | 103 | - `Troubleshooting `__ 104 | - `Issue 105 | Tracker `__ 106 | - `Mailing list `__ 107 | 108 | .. |PyPI version| image:: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat 109 | :target: https://pypi.python.org/pypi/shadowsocks 110 | .. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat 111 | :target: https://travis-ci.org/shadowsocks/shadowsocks 112 | .. |Coverage Status| image:: https://jenkins.shadowvpn.org/result/shadowsocks 113 | :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html 114 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | shadowsocks (2.1.0-1) unstable; urgency=low 2 | 3 | * Initial release (Closes: #758900) 4 | 5 | -- Shell.Xu Sat, 23 Aug 2014 00:56:04 +0800 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"my_server_ip", 3 | "server_port":8388, 4 | "local_address": "127.0.0.1", 5 | "local_port":1080, 6 | "password":"mypassword", 7 | "timeout":300, 8 | "method":"aes-256-cfb", 9 | "fast_open": false, 10 | "workers": 1 11 | } -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: shadowsocks 2 | Section: python 3 | Priority: extra 4 | Maintainer: Shell.Xu 5 | Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools 6 | Standards-Version: 3.9.5 7 | Homepage: https://github.com/clowwindy/shadowsocks 8 | Vcs-Git: git://github.com/shell909090/shadowsocks.git 9 | Vcs-Browser: http://github.com/shell909090/shadowsocks 10 | 11 | Package: shadowsocks 12 | Architecture: all 13 | Pre-Depends: dpkg (>= 1.15.6~) 14 | Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto 15 | Description: Fast tunnel proxy that helps you bypass firewalls 16 | A secure socks5 proxy, designed to protect your Internet traffic. 17 | . 18 | This package contain local and server part of shadowsocks, a fast, 19 | powerful tunnel proxy to bypass firewalls. -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: shadowsocks 3 | Source: https://github.com/clowwindy/shadowsocks 4 | 5 | Files: debian/* 6 | Copyright: 2014 Shell.Xu 7 | License: Expat 8 | 9 | Files: * 10 | Copyright: 2014 clowwindy 11 | License: Expat 12 | 13 | License: Expat 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | . 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | . 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | README.rst 3 | -------------------------------------------------------------------------------- /debian/init.d: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: shadowsocks 4 | # Required-Start: $network $local_fs $remote_fs 5 | # Required-Stop: $network $local_fs $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Fast tunnel proxy that helps you bypass firewalls 9 | # Description: A secure socks5 proxy, designed to protect your Internet traffic. 10 | # This package contain local and server part of shadowsocks, a fast, 11 | # powerful tunnel proxy to bypass firewalls. 12 | ### END INIT INFO 13 | 14 | # Author: Shell.Xu 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 18 | DESC=shadowsocks # Introduce a short description here 19 | NAME=shadowsocks # Introduce the short server's name here 20 | DAEMON=/usr/bin/ssserver # Introduce the server's location here 21 | DAEMON_ARGS="" # Arguments to run the daemon with 22 | PIDFILE=/var/run/$NAME.pid 23 | SCRIPTNAME=/etc/init.d/$NAME 24 | LOGFILE=/var/log/$NAME.log 25 | 26 | # Exit if the package is not installed 27 | [ -x $DAEMON ] || exit 0 28 | 29 | # Read configuration variable file if it is present 30 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 31 | 32 | # Load the VERBOSE setting and other rcS variables 33 | . /lib/init/vars.sh 34 | 35 | # Define LSB log_* functions. 36 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 37 | . /lib/lsb/init-functions 38 | 39 | # 40 | # Function that starts the daemon/service 41 | # 42 | do_start() 43 | { 44 | # Return 45 | # 0 if daemon has been started 46 | # 1 if daemon was already running 47 | # 2 if daemon could not be started 48 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ 49 | --background --make-pidfile --chdir / --chuid $USERID --no-close --test > /dev/null \ 50 | || return 1 51 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ 52 | --background --make-pidfile --chdir / --chuid $USERID --no-close -- \ 53 | $DAEMON_ARGS $DAEMON_OPTS >> $LOGFILE 2>&1 \ 54 | || return 2 55 | # Add code here, if necessary, that waits for the process to be ready 56 | # to handle requests from services started subsequently which depend 57 | # on this one. As a last resort, sleep for some time. 58 | } 59 | 60 | # 61 | # Function that stops the daemon/service 62 | # 63 | do_stop() 64 | { 65 | # Return 66 | # 0 if daemon has been stopped 67 | # 1 if daemon was already stopped 68 | # 2 if daemon could not be stopped 69 | # other if a failure occurred 70 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 71 | RETVAL="$?" 72 | [ "$RETVAL" = 2 ] && return 2 73 | # Many daemons don't delete their pidfiles when they exit. 74 | rm -f $PIDFILE 75 | return "$RETVAL" 76 | } 77 | 78 | # 79 | # Function that sends a SIGHUP to the daemon/service 80 | # 81 | do_reload() { 82 | # 83 | # If the daemon can reload its configuration without 84 | # restarting (for example, when it is sent a SIGHUP), 85 | # then implement that here. 86 | # 87 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 88 | return 0 89 | } 90 | 91 | case "$1" in 92 | start) 93 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" 94 | do_start 95 | case "$?" in 96 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 97 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 98 | esac 99 | ;; 100 | stop) 101 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 102 | do_stop 103 | case "$?" in 104 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 105 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 106 | esac 107 | ;; 108 | status) 109 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 110 | ;; 111 | #reload|force-reload) 112 | # 113 | # If do_reload() is not implemented then leave this commented out 114 | # and leave 'force-reload' as an alias for 'restart'. 115 | # 116 | #log_daemon_msg "Reloading $DESC" "$NAME" 117 | #do_reload 118 | #log_end_msg $? 119 | #;; 120 | restart|force-reload) 121 | # 122 | # If the "reload" option is implemented then remove the 123 | # 'force-reload' alias 124 | # 125 | log_daemon_msg "Restarting $DESC" "$NAME" 126 | do_stop 127 | case "$?" in 128 | 0|1) 129 | do_start 130 | case "$?" in 131 | 0) log_end_msg 0 ;; 132 | 1) log_end_msg 1 ;; # Old process is still running 133 | *) log_end_msg 1 ;; # Failed to start 134 | esac 135 | ;; 136 | *) 137 | # Failed to stop 138 | log_end_msg 1 139 | ;; 140 | esac 141 | ;; 142 | *) 143 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 144 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 145 | exit 3 146 | ;; 147 | esac 148 | 149 | : 150 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | debian/config.json etc/shadowsocks/ -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | 4 | %: 5 | dh $@ --with python2 --buildsystem=python_distutils 6 | -------------------------------------------------------------------------------- /debian/shadowsocks.default: -------------------------------------------------------------------------------- 1 | # Defaults for shadowsocks initscript 2 | # sourced by /etc/init.d/shadowsocks 3 | # installed at /etc/default/shadowsocks by the maintainer scripts 4 | 5 | USERID="nobody" 6 | 7 | # 8 | # This is a POSIX shell fragment 9 | # 10 | 11 | # Additional options that are passed to the Daemon. 12 | DAEMON_OPTS="-q -c /etc/shadowsocks/config.json" 13 | -------------------------------------------------------------------------------- /debian/shadowsocks.manpages: -------------------------------------------------------------------------------- 1 | debian/sslocal.1 2 | debian/ssserver.1 -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/sslocal.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" (C) Copyright 2014 Shell.Xu , 3 | .\" 4 | .TH SHADOWSOCKS 1 "August 23, 2014" 5 | .SH NAME 6 | shadowsocks \- Fast tunnel proxy that helps you bypass firewalls 7 | .SH SYNOPSIS 8 | .B ssserver 9 | .RI [ options ] 10 | .br 11 | .B sslocal 12 | .RI [ options ] 13 | .SH DESCRIPTION 14 | shadowsocks is a tunnel proxy helps you bypass firewall. 15 | .B ssserver 16 | is the server part, and 17 | .B sslocal 18 | is the local part. 19 | .SH OPTIONS 20 | .TP 21 | .B \-h, \-\-help 22 | Show this help message and exit. 23 | .TP 24 | .B \-s SERVER_ADDR 25 | Server address, default: 0.0.0.0. 26 | .TP 27 | .B \-p SERVER_PORT 28 | Server port, default: 8388. 29 | .TP 30 | .B \-k PASSWORD 31 | Password. 32 | .TP 33 | .B \-m METHOD 34 | Encryption method, default: aes-256-cfb. 35 | .TP 36 | .B \-t TIMEOUT 37 | Timeout in seconds, default: 300. 38 | .TP 39 | .B \-c CONFIG 40 | Path to config file. 41 | .TP 42 | .B \-\-fast-open 43 | Use TCP_FASTOPEN, requires Linux 3.7+. 44 | .TP 45 | .B \-\-workers WORKERS 46 | Number of workers, available on Unix/Linux. 47 | .TP 48 | .B \-v, \-vv 49 | Verbose mode. 50 | .TP 51 | .B \-q, \-qq 52 | Quiet mode, only show warnings/errors. 53 | .SH SEE ALSO 54 | .br 55 | The programs are documented fully by 56 | .IR "Shell Xu " 57 | and 58 | .IR "Clowwindy ", 59 | available via the Info system. 60 | -------------------------------------------------------------------------------- /debian/ssserver.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" (C) Copyright 2014 Shell.Xu , 3 | .\" 4 | .TH SHADOWSOCKS 1 "August 23, 2014" 5 | .SH NAME 6 | shadowsocks \- Fast tunnel proxy that helps you bypass firewalls 7 | .SH SYNOPSIS 8 | .B ssserver 9 | .RI [ options ] 10 | .br 11 | .B sslocal 12 | .RI [ options ] 13 | .SH DESCRIPTION 14 | shadowsocks is a tunnel proxy helps you bypass firewall. 15 | .B ssserver 16 | is the server part, and 17 | .B sslocal 18 | is the local part. 19 | .SH OPTIONS 20 | .TP 21 | .B \-h, \-\-help 22 | Show this help message and exit. 23 | .TP 24 | .B \-s SERVER_ADDR 25 | Server address, default: 0.0.0.0. 26 | .TP 27 | .B \-p SERVER_PORT 28 | Server port, default: 8388. 29 | .TP 30 | .B \-k PASSWORD 31 | Password. 32 | .TP 33 | .B \-m METHOD 34 | Encryption method, default: aes-256-cfb. 35 | .TP 36 | .B \-t TIMEOUT 37 | Timeout in seconds, default: 300. 38 | .TP 39 | .B \-c CONFIG 40 | Path to config file. 41 | .TP 42 | .B \-\-fast-open 43 | Use TCP_FASTOPEN, requires Linux 3.7+. 44 | .TP 45 | .B \-\-workers WORKERS 46 | Number of workers, available on Unix/Linux. 47 | .TP 48 | .B \-v, \-vv 49 | Verbose mode. 50 | .TP 51 | .B \-q, \-qq 52 | Quiet mode, only show warnings/errors. 53 | .SH SEE ALSO 54 | .br 55 | The programs are documented fully by 56 | .IR "Shell Xu " 57 | and 58 | .IR "Clowwindy ", 59 | available via the Info system. 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from setuptools import setup 3 | 4 | 5 | with codecs.open('README.rst', encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="shadowsocks", 10 | version="2.8.2", 11 | license='http://www.apache.org/licenses/LICENSE-2.0', 12 | description="A fast tunnel proxy that help you get through firewalls", 13 | author='clowwindy', 14 | author_email='clowwindy42@gmail.com', 15 | url='https://github.com/shadowsocks/shadowsocks', 16 | packages=['shadowsocks', 'shadowsocks.crypto'], 17 | package_data={ 18 | 'shadowsocks': ['README.rst', 'LICENSE'] 19 | }, 20 | install_requires=[], 21 | entry_points=""" 22 | [console_scripts] 23 | sslocal = shadowsocks.local:main 24 | ssserver = shadowsocks.server:main 25 | """, 26 | classifiers=[ 27 | 'License :: OSI Approved :: Apache Software License', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.6', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Programming Language :: Python :: Implementation :: CPython', 35 | 'Programming Language :: Python :: Implementation :: PyPy', 36 | 'Topic :: Internet :: Proxy Servers', 37 | ], 38 | long_description=long_description, 39 | ) 40 | -------------------------------------------------------------------------------- /shadowsocks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2012-2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /shadowsocks/asyncdns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2014-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import os 22 | import socket 23 | import struct 24 | import re 25 | import logging 26 | 27 | from shadowsocks import common, lru_cache, eventloop, shell 28 | 29 | 30 | CACHE_SWEEP_INTERVAL = 30 31 | 32 | VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(? 63: 82 | return None 83 | results.append(common.chr(l)) 84 | results.append(label) 85 | results.append(b'\0') 86 | return b''.join(results) 87 | 88 | 89 | def build_request(address, qtype): 90 | request_id = os.urandom(2) 91 | header = struct.pack('!BBHHHH', 1, 0, 1, 0, 0, 0) 92 | addr = build_address(address) 93 | qtype_qclass = struct.pack('!HH', qtype, QCLASS_IN) 94 | return request_id + header + addr + qtype_qclass 95 | 96 | 97 | def parse_ip(addrtype, data, length, offset): 98 | if addrtype == QTYPE_A: 99 | return socket.inet_ntop(socket.AF_INET, data[offset:offset + length]) 100 | elif addrtype == QTYPE_AAAA: 101 | return socket.inet_ntop(socket.AF_INET6, data[offset:offset + length]) 102 | elif addrtype in [QTYPE_CNAME, QTYPE_NS]: 103 | return parse_name(data, offset)[1] 104 | else: 105 | return data[offset:offset + length] 106 | 107 | 108 | def parse_name(data, offset): 109 | p = offset 110 | labels = [] 111 | l = common.ord(data[p]) 112 | while l > 0: 113 | if (l & (128 + 64)) == (128 + 64): 114 | # pointer 115 | pointer = struct.unpack('!H', data[p:p + 2])[0] 116 | pointer &= 0x3FFF 117 | r = parse_name(data, pointer) 118 | labels.append(r[1]) 119 | p += 2 120 | # pointer is the end 121 | return p - offset, b'.'.join(labels) 122 | else: 123 | labels.append(data[p + 1:p + 1 + l]) 124 | p += 1 + l 125 | l = common.ord(data[p]) 126 | return p - offset + 1, b'.'.join(labels) 127 | 128 | 129 | # rfc1035 130 | # record 131 | # 1 1 1 1 1 1 132 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 133 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 134 | # | | 135 | # / / 136 | # / NAME / 137 | # | | 138 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 139 | # | TYPE | 140 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 141 | # | CLASS | 142 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 143 | # | TTL | 144 | # | | 145 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 146 | # | RDLENGTH | 147 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| 148 | # / RDATA / 149 | # / / 150 | # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 151 | def parse_record(data, offset, question=False): 152 | nlen, name = parse_name(data, offset) 153 | if not question: 154 | record_type, record_class, record_ttl, record_rdlength = struct.unpack( 155 | '!HHiH', data[offset + nlen:offset + nlen + 10] 156 | ) 157 | ip = parse_ip(record_type, data, record_rdlength, offset + nlen + 10) 158 | return nlen + 10 + record_rdlength, \ 159 | (name, ip, record_type, record_class, record_ttl) 160 | else: 161 | record_type, record_class = struct.unpack( 162 | '!HH', data[offset + nlen:offset + nlen + 4] 163 | ) 164 | return nlen + 4, (name, None, record_type, record_class, None, None) 165 | 166 | 167 | def parse_header(data): 168 | if len(data) >= 12: 169 | header = struct.unpack('!HBBHHHH', data[:12]) 170 | res_id = header[0] 171 | res_qr = header[1] & 128 172 | res_tc = header[1] & 2 173 | res_ra = header[2] & 128 174 | res_rcode = header[2] & 15 175 | # assert res_tc == 0 176 | # assert res_rcode in [0, 3] 177 | res_qdcount = header[3] 178 | res_ancount = header[4] 179 | res_nscount = header[5] 180 | res_arcount = header[6] 181 | return (res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, 182 | res_ancount, res_nscount, res_arcount) 183 | return None 184 | 185 | 186 | def parse_response(data): 187 | try: 188 | if len(data) >= 12: 189 | header = parse_header(data) 190 | if not header: 191 | return None 192 | res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, \ 193 | res_ancount, res_nscount, res_arcount = header 194 | 195 | qds = [] 196 | ans = [] 197 | offset = 12 198 | for i in range(0, res_qdcount): 199 | l, r = parse_record(data, offset, True) 200 | offset += l 201 | if r: 202 | qds.append(r) 203 | for i in range(0, res_ancount): 204 | l, r = parse_record(data, offset) 205 | offset += l 206 | if r: 207 | ans.append(r) 208 | for i in range(0, res_nscount): 209 | l, r = parse_record(data, offset) 210 | offset += l 211 | for i in range(0, res_arcount): 212 | l, r = parse_record(data, offset) 213 | offset += l 214 | response = DNSResponse() 215 | if qds: 216 | response.hostname = qds[0][0] 217 | for an in qds: 218 | response.questions.append((an[1], an[2], an[3])) 219 | for an in ans: 220 | response.answers.append((an[1], an[2], an[3])) 221 | return response 222 | except Exception as e: 223 | shell.print_exception(e) 224 | return None 225 | 226 | 227 | def is_valid_hostname(hostname): 228 | if len(hostname) > 255: 229 | return False 230 | if hostname[-1] == b'.': 231 | hostname = hostname[:-1] 232 | return all(VALID_HOSTNAME.match(x) for x in hostname.split(b'.')) 233 | 234 | 235 | class DNSResponse(object): 236 | def __init__(self): 237 | self.hostname = None 238 | self.questions = [] # each: (addr, type, class) 239 | self.answers = [] # each: (addr, type, class) 240 | 241 | def __str__(self): 242 | return '%s: %s' % (self.hostname, str(self.answers)) 243 | 244 | 245 | STATUS_IPV4 = 0 246 | STATUS_IPV6 = 1 247 | 248 | 249 | class DNSResolver(object): 250 | 251 | def __init__(self, server_list=None): 252 | self._loop = None 253 | self._hosts = {} 254 | self._hostname_status = {} 255 | self._hostname_to_cb = {} 256 | self._cb_to_hostname = {} 257 | self._cache = lru_cache.LRUCache(timeout=300) 258 | self._sock = None 259 | if server_list is None: 260 | self._servers = None 261 | self._parse_resolv() 262 | else: 263 | self._servers = server_list 264 | self._parse_hosts() 265 | # TODO monitor hosts change and reload hosts 266 | # TODO parse /etc/gai.conf and follow its rules 267 | 268 | def _parse_resolv(self): 269 | self._servers = [] 270 | try: 271 | with open('/etc/resolv.conf', 'rb') as f: 272 | content = f.readlines() 273 | for line in content: 274 | line = line.strip() 275 | if line: 276 | if line.startswith(b'nameserver'): 277 | parts = line.split() 278 | if len(parts) >= 2: 279 | server = parts[1] 280 | if common.is_ip(server) == socket.AF_INET: 281 | if type(server) != str: 282 | server = server.decode('utf8') 283 | self._servers.append(server) 284 | except IOError: 285 | pass 286 | if not self._servers: 287 | self._servers = ['8.8.4.4', '8.8.8.8'] 288 | 289 | def _parse_hosts(self): 290 | etc_path = '/etc/hosts' 291 | if 'WINDIR' in os.environ: 292 | etc_path = os.environ['WINDIR'] + '/system32/drivers/etc/hosts' 293 | try: 294 | with open(etc_path, 'rb') as f: 295 | for line in f.readlines(): 296 | line = line.strip() 297 | parts = line.split() 298 | if len(parts) >= 2: 299 | ip = parts[0] 300 | if common.is_ip(ip): 301 | for i in range(1, len(parts)): 302 | hostname = parts[i] 303 | if hostname: 304 | self._hosts[hostname] = ip 305 | except IOError: 306 | self._hosts['localhost'] = '127.0.0.1' 307 | 308 | def add_to_loop(self, loop): 309 | if self._loop: 310 | raise Exception('already add to loop') 311 | self._loop = loop 312 | # TODO when dns server is IPv6 313 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 314 | socket.SOL_UDP) 315 | self._sock.setblocking(False) 316 | loop.add(self._sock, eventloop.POLL_IN, self) 317 | loop.add_periodic(self.handle_periodic) 318 | 319 | def _call_callback(self, hostname, ip, error=None): 320 | callbacks = self._hostname_to_cb.get(hostname, []) 321 | for callback in callbacks: 322 | if callback in self._cb_to_hostname: 323 | del self._cb_to_hostname[callback] 324 | if ip or error: 325 | callback((hostname, ip), error) 326 | else: 327 | callback((hostname, None), 328 | Exception('unknown hostname %s' % hostname)) 329 | if hostname in self._hostname_to_cb: 330 | del self._hostname_to_cb[hostname] 331 | if hostname in self._hostname_status: 332 | del self._hostname_status[hostname] 333 | 334 | def _handle_data(self, data): 335 | response = parse_response(data) 336 | if response and response.hostname: 337 | hostname = response.hostname 338 | ip = None 339 | for answer in response.answers: 340 | if answer[1] in (QTYPE_A, QTYPE_AAAA) and \ 341 | answer[2] == QCLASS_IN: 342 | ip = answer[0] 343 | break 344 | if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ 345 | == STATUS_IPV4: 346 | self._hostname_status[hostname] = STATUS_IPV6 347 | self._send_req(hostname, QTYPE_AAAA) 348 | else: 349 | if ip: 350 | self._cache[hostname] = ip 351 | self._call_callback(hostname, ip) 352 | elif self._hostname_status.get(hostname, None) == STATUS_IPV6: 353 | for question in response.questions: 354 | if question[1] == QTYPE_AAAA: 355 | self._call_callback(hostname, None) 356 | break 357 | 358 | def handle_event(self, sock, fd, event): 359 | if sock != self._sock: 360 | return 361 | if event & eventloop.POLL_ERR: 362 | logging.error('dns socket err') 363 | self._loop.remove(self._sock) 364 | self._sock.close() 365 | # TODO when dns server is IPv6 366 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 367 | socket.SOL_UDP) 368 | self._sock.setblocking(False) 369 | self._loop.add(self._sock, eventloop.POLL_IN, self) 370 | else: 371 | data, addr = sock.recvfrom(1024) 372 | if addr[0] not in self._servers: 373 | logging.warn('received a packet other than our dns') 374 | return 375 | self._handle_data(data) 376 | 377 | def handle_periodic(self): 378 | self._cache.sweep() 379 | 380 | def remove_callback(self, callback): 381 | hostname = self._cb_to_hostname.get(callback) 382 | if hostname: 383 | del self._cb_to_hostname[callback] 384 | arr = self._hostname_to_cb.get(hostname, None) 385 | if arr: 386 | arr.remove(callback) 387 | if not arr: 388 | del self._hostname_to_cb[hostname] 389 | if hostname in self._hostname_status: 390 | del self._hostname_status[hostname] 391 | 392 | def _send_req(self, hostname, qtype): 393 | req = build_request(hostname, qtype) 394 | for server in self._servers: 395 | logging.debug('resolving %s with type %d using server %s', 396 | hostname, qtype, server) 397 | self._sock.sendto(req, (server, 53)) 398 | 399 | def resolve(self, hostname, callback): 400 | if type(hostname) != bytes: 401 | hostname = hostname.encode('utf8') 402 | if not hostname: 403 | callback(None, Exception('empty hostname')) 404 | elif common.is_ip(hostname): 405 | callback((hostname, hostname), None) 406 | elif hostname in self._hosts: 407 | logging.debug('hit hosts: %s', hostname) 408 | ip = self._hosts[hostname] 409 | callback((hostname, ip), None) 410 | elif hostname in self._cache: 411 | logging.debug('hit cache: %s', hostname) 412 | ip = self._cache[hostname] 413 | callback((hostname, ip), None) 414 | else: 415 | if not is_valid_hostname(hostname): 416 | callback(None, Exception('invalid hostname: %s' % hostname)) 417 | return 418 | arr = self._hostname_to_cb.get(hostname, None) 419 | if not arr: 420 | self._hostname_status[hostname] = STATUS_IPV4 421 | self._send_req(hostname, QTYPE_A) 422 | self._hostname_to_cb[hostname] = [callback] 423 | self._cb_to_hostname[callback] = hostname 424 | else: 425 | arr.append(callback) 426 | # TODO send again only if waited too long 427 | self._send_req(hostname, QTYPE_A) 428 | 429 | def close(self): 430 | if self._sock: 431 | if self._loop: 432 | self._loop.remove_periodic(self.handle_periodic) 433 | self._loop.remove(self._sock) 434 | self._sock.close() 435 | self._sock = None 436 | 437 | 438 | def test(): 439 | dns_resolver = DNSResolver() 440 | loop = eventloop.EventLoop() 441 | dns_resolver.add_to_loop(loop) 442 | 443 | global counter 444 | counter = 0 445 | 446 | def make_callback(): 447 | global counter 448 | 449 | def callback(result, error): 450 | global counter 451 | # TODO: what can we assert? 452 | print(result, error) 453 | counter += 1 454 | if counter == 9: 455 | dns_resolver.close() 456 | loop.stop() 457 | a_callback = callback 458 | return a_callback 459 | 460 | assert(make_callback() != make_callback()) 461 | 462 | dns_resolver.resolve(b'google.com', make_callback()) 463 | dns_resolver.resolve('google.com', make_callback()) 464 | dns_resolver.resolve('example.com', make_callback()) 465 | dns_resolver.resolve('ipv6.google.com', make_callback()) 466 | dns_resolver.resolve('www.facebook.com', make_callback()) 467 | dns_resolver.resolve('ns2.google.com', make_callback()) 468 | dns_resolver.resolve('invalid.@!#$%^&$@.hostname', make_callback()) 469 | dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' 470 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 471 | 'long.hostname', make_callback()) 472 | dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo' 473 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 474 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 475 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 476 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 477 | 'ooooooooooooooooooooooooooooooooooooooooooooooooooo' 478 | 'long.hostname', make_callback()) 479 | 480 | loop.run() 481 | 482 | 483 | if __name__ == '__main__': 484 | test() 485 | -------------------------------------------------------------------------------- /shadowsocks/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2013-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import socket 22 | import struct 23 | import logging 24 | 25 | 26 | def compat_ord(s): 27 | if type(s) == int: 28 | return s 29 | return _ord(s) 30 | 31 | 32 | def compat_chr(d): 33 | if bytes == str: 34 | return _chr(d) 35 | return bytes([d]) 36 | 37 | 38 | _ord = ord 39 | _chr = chr 40 | ord = compat_ord 41 | chr = compat_chr 42 | 43 | 44 | def to_bytes(s): 45 | if bytes != str: 46 | if type(s) == str: 47 | return s.encode('utf-8') 48 | return s 49 | 50 | 51 | def to_str(s): 52 | if bytes != str: 53 | if type(s) == bytes: 54 | return s.decode('utf-8') 55 | return s 56 | 57 | 58 | def inet_ntop(family, ipstr): 59 | if family == socket.AF_INET: 60 | return to_bytes(socket.inet_ntoa(ipstr)) 61 | elif family == socket.AF_INET6: 62 | import re 63 | v6addr = ':'.join(('%02X%02X' % (ord(i), ord(j))).lstrip('0') 64 | for i, j in zip(ipstr[::2], ipstr[1::2])) 65 | v6addr = re.sub('::+', '::', v6addr, count=1) 66 | return to_bytes(v6addr) 67 | 68 | 69 | def inet_pton(family, addr): 70 | addr = to_str(addr) 71 | if family == socket.AF_INET: 72 | return socket.inet_aton(addr) 73 | elif family == socket.AF_INET6: 74 | if '.' in addr: # a v4 addr 75 | v4addr = addr[addr.rindex(':') + 1:] 76 | v4addr = socket.inet_aton(v4addr) 77 | v4addr = map(lambda x: ('%02X' % ord(x)), v4addr) 78 | v4addr.insert(2, ':') 79 | newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr) 80 | return inet_pton(family, newaddr) 81 | dbyts = [0] * 8 # 8 groups 82 | grps = addr.split(':') 83 | for i, v in enumerate(grps): 84 | if v: 85 | dbyts[i] = int(v, 16) 86 | else: 87 | for j, w in enumerate(grps[::-1]): 88 | if w: 89 | dbyts[7 - j] = int(w, 16) 90 | else: 91 | break 92 | break 93 | return b''.join((chr(i // 256) + chr(i % 256)) for i in dbyts) 94 | else: 95 | raise RuntimeError("What family?") 96 | 97 | 98 | def is_ip(address): 99 | for family in (socket.AF_INET, socket.AF_INET6): 100 | try: 101 | if type(address) != str: 102 | address = address.decode('utf8') 103 | inet_pton(family, address) 104 | return family 105 | except (TypeError, ValueError, OSError, IOError): 106 | pass 107 | return False 108 | 109 | 110 | def patch_socket(): 111 | if not hasattr(socket, 'inet_pton'): 112 | socket.inet_pton = inet_pton 113 | 114 | if not hasattr(socket, 'inet_ntop'): 115 | socket.inet_ntop = inet_ntop 116 | 117 | 118 | patch_socket() 119 | 120 | 121 | ADDRTYPE_IPV4 = 1 122 | ADDRTYPE_IPV6 = 4 123 | ADDRTYPE_HOST = 3 124 | 125 | 126 | def pack_addr(address): 127 | address_str = to_str(address) 128 | for family in (socket.AF_INET, socket.AF_INET6): 129 | try: 130 | r = socket.inet_pton(family, address_str) 131 | if family == socket.AF_INET6: 132 | return b'\x04' + r 133 | else: 134 | return b'\x01' + r 135 | except (TypeError, ValueError, OSError, IOError): 136 | pass 137 | if len(address) > 255: 138 | address = address[:255] # TODO 139 | return b'\x03' + chr(len(address)) + address 140 | 141 | 142 | def parse_header(data): 143 | addrtype = ord(data[0]) 144 | dest_addr = None 145 | dest_port = None 146 | header_length = 0 147 | if addrtype == ADDRTYPE_IPV4: 148 | if len(data) >= 7: 149 | dest_addr = socket.inet_ntoa(data[1:5]) 150 | dest_port = struct.unpack('>H', data[5:7])[0] 151 | header_length = 7 152 | else: 153 | logging.warn('header is too short') 154 | elif addrtype == ADDRTYPE_HOST: 155 | if len(data) > 2: 156 | addrlen = ord(data[1]) 157 | if len(data) >= 2 + addrlen: 158 | dest_addr = data[2:2 + addrlen] 159 | dest_port = struct.unpack('>H', data[2 + addrlen:4 + 160 | addrlen])[0] 161 | header_length = 4 + addrlen 162 | else: 163 | logging.warn('header is too short') 164 | else: 165 | logging.warn('header is too short') 166 | elif addrtype == ADDRTYPE_IPV6: 167 | if len(data) >= 19: 168 | dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) 169 | dest_port = struct.unpack('>H', data[17:19])[0] 170 | header_length = 19 171 | else: 172 | logging.warn('header is too short') 173 | else: 174 | logging.warn('unsupported addrtype %d, maybe wrong password or ' 175 | 'encryption method' % addrtype) 176 | if dest_addr is None: 177 | return None 178 | return addrtype, to_bytes(dest_addr), dest_port, header_length 179 | 180 | 181 | class IPNetwork(object): 182 | ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0} 183 | 184 | def __init__(self, addrs): 185 | self._network_list_v4 = [] 186 | self._network_list_v6 = [] 187 | if type(addrs) == str: 188 | addrs = addrs.split(',') 189 | list(map(self.add_network, addrs)) 190 | 191 | def add_network(self, addr): 192 | if addr is "": 193 | return 194 | block = addr.split('/') 195 | addr_family = is_ip(block[0]) 196 | addr_len = IPNetwork.ADDRLENGTH[addr_family] 197 | if addr_family is socket.AF_INET: 198 | ip, = struct.unpack("!I", socket.inet_aton(block[0])) 199 | elif addr_family is socket.AF_INET6: 200 | hi, lo = struct.unpack("!QQ", inet_pton(addr_family, block[0])) 201 | ip = (hi << 64) | lo 202 | else: 203 | raise Exception("Not a valid CIDR notation: %s" % addr) 204 | if len(block) is 1: 205 | prefix_size = 0 206 | while (ip & 1) == 0 and ip is not 0: 207 | ip >>= 1 208 | prefix_size += 1 209 | logging.warn("You did't specify CIDR routing prefix size for %s, " 210 | "implicit treated as %s/%d" % (addr, addr, addr_len)) 211 | elif block[1].isdigit() and int(block[1]) <= addr_len: 212 | prefix_size = addr_len - int(block[1]) 213 | ip >>= prefix_size 214 | else: 215 | raise Exception("Not a valid CIDR notation: %s" % addr) 216 | if addr_family is socket.AF_INET: 217 | self._network_list_v4.append((ip, prefix_size)) 218 | else: 219 | self._network_list_v6.append((ip, prefix_size)) 220 | 221 | def __contains__(self, addr): 222 | addr_family = is_ip(addr) 223 | if addr_family is socket.AF_INET: 224 | ip, = struct.unpack("!I", socket.inet_aton(addr)) 225 | return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], 226 | self._network_list_v4)) 227 | elif addr_family is socket.AF_INET6: 228 | hi, lo = struct.unpack("!QQ", inet_pton(addr_family, addr)) 229 | ip = (hi << 64) | lo 230 | return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], 231 | self._network_list_v6)) 232 | else: 233 | return False 234 | 235 | 236 | def test_inet_conv(): 237 | ipv4 = b'8.8.4.4' 238 | b = inet_pton(socket.AF_INET, ipv4) 239 | assert inet_ntop(socket.AF_INET, b) == ipv4 240 | ipv6 = b'2404:6800:4005:805::1011' 241 | b = inet_pton(socket.AF_INET6, ipv6) 242 | assert inet_ntop(socket.AF_INET6, b) == ipv6 243 | 244 | 245 | def test_parse_header(): 246 | assert parse_header(b'\x03\x0ewww.google.com\x00\x50') == \ 247 | (3, b'www.google.com', 80, 18) 248 | assert parse_header(b'\x01\x08\x08\x08\x08\x00\x35') == \ 249 | (1, b'8.8.8.8', 53, 7) 250 | assert parse_header((b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00' 251 | b'\x00\x10\x11\x00\x50')) == \ 252 | (4, b'2404:6800:4005:805::1011', 80, 19) 253 | 254 | 255 | def test_pack_header(): 256 | assert pack_addr(b'8.8.8.8') == b'\x01\x08\x08\x08\x08' 257 | assert pack_addr(b'2404:6800:4005:805::1011') == \ 258 | b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00\x00\x10\x11' 259 | assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com' 260 | 261 | 262 | def test_ip_network(): 263 | ip_network = IPNetwork('127.0.0.0/24,::ff:1/112,::1,192.168.1.1,192.0.2.0') 264 | assert '127.0.0.1' in ip_network 265 | assert '127.0.1.1' not in ip_network 266 | assert ':ff:ffff' in ip_network 267 | assert '::ffff:1' not in ip_network 268 | assert '::1' in ip_network 269 | assert '::2' not in ip_network 270 | assert '192.168.1.1' in ip_network 271 | assert '192.168.1.2' not in ip_network 272 | assert '192.0.2.1' in ip_network 273 | assert '192.0.3.1' in ip_network # 192.0.2.0 is treated as 192.0.2.0/23 274 | assert 'www.google.com' not in ip_network 275 | 276 | 277 | if __name__ == '__main__': 278 | test_inet_conv() 279 | test_parse_header() 280 | test_pack_header() 281 | test_ip_network() 282 | -------------------------------------------------------------------------------- /shadowsocks/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /shadowsocks/crypto/openssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | from ctypes import c_char_p, c_int, c_long, byref,\ 21 | create_string_buffer, c_void_p 22 | 23 | from shadowsocks import common 24 | from shadowsocks.crypto import util 25 | 26 | __all__ = ['ciphers'] 27 | 28 | libcrypto = None 29 | loaded = False 30 | 31 | buf_size = 2048 32 | 33 | 34 | def load_openssl(): 35 | global loaded, libcrypto, buf 36 | 37 | libcrypto = util.find_library(('crypto', 'eay32'), 38 | 'EVP_get_cipherbyname', 39 | 'libcrypto') 40 | if libcrypto is None: 41 | raise Exception('libcrypto(OpenSSL) not found') 42 | 43 | libcrypto.EVP_get_cipherbyname.restype = c_void_p 44 | libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p 45 | 46 | libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, 47 | c_char_p, c_char_p, c_int) 48 | 49 | libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, 50 | c_char_p, c_int) 51 | 52 | libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) 53 | libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) 54 | if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): 55 | libcrypto.OpenSSL_add_all_ciphers() 56 | 57 | buf = create_string_buffer(buf_size) 58 | loaded = True 59 | 60 | 61 | def load_cipher(cipher_name): 62 | func_name = 'EVP_' + cipher_name.replace('-', '_') 63 | if bytes != str: 64 | func_name = str(func_name, 'utf-8') 65 | cipher = getattr(libcrypto, func_name, None) 66 | if cipher: 67 | cipher.restype = c_void_p 68 | return cipher() 69 | return None 70 | 71 | 72 | class OpenSSLCrypto(object): 73 | def __init__(self, cipher_name, key, iv, op): 74 | self._ctx = None 75 | if not loaded: 76 | load_openssl() 77 | cipher_name = common.to_bytes(cipher_name) 78 | cipher = libcrypto.EVP_get_cipherbyname(cipher_name) 79 | if not cipher: 80 | cipher = load_cipher(cipher_name) 81 | if not cipher: 82 | raise Exception('cipher %s not found in libcrypto' % cipher_name) 83 | key_ptr = c_char_p(key) 84 | iv_ptr = c_char_p(iv) 85 | self._ctx = libcrypto.EVP_CIPHER_CTX_new() 86 | if not self._ctx: 87 | raise Exception('can not create cipher context') 88 | r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, 89 | key_ptr, iv_ptr, c_int(op)) 90 | if not r: 91 | self.clean() 92 | raise Exception('can not initialize cipher context') 93 | 94 | def update(self, data): 95 | global buf_size, buf 96 | cipher_out_len = c_long(0) 97 | l = len(data) 98 | if buf_size < l: 99 | buf_size = l * 2 100 | buf = create_string_buffer(buf_size) 101 | libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), 102 | byref(cipher_out_len), c_char_p(data), l) 103 | # buf is copied to a str object when we access buf.raw 104 | return buf.raw[:cipher_out_len.value] 105 | 106 | def __del__(self): 107 | self.clean() 108 | 109 | def clean(self): 110 | if self._ctx: 111 | libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) 112 | libcrypto.EVP_CIPHER_CTX_free(self._ctx) 113 | 114 | 115 | ciphers = { 116 | 'aes-128-cfb': (16, 16, OpenSSLCrypto), 117 | 'aes-192-cfb': (24, 16, OpenSSLCrypto), 118 | 'aes-256-cfb': (32, 16, OpenSSLCrypto), 119 | 'aes-128-ofb': (16, 16, OpenSSLCrypto), 120 | 'aes-192-ofb': (24, 16, OpenSSLCrypto), 121 | 'aes-256-ofb': (32, 16, OpenSSLCrypto), 122 | 'aes-128-ctr': (16, 16, OpenSSLCrypto), 123 | 'aes-192-ctr': (24, 16, OpenSSLCrypto), 124 | 'aes-256-ctr': (32, 16, OpenSSLCrypto), 125 | 'aes-128-cfb8': (16, 16, OpenSSLCrypto), 126 | 'aes-192-cfb8': (24, 16, OpenSSLCrypto), 127 | 'aes-256-cfb8': (32, 16, OpenSSLCrypto), 128 | 'aes-128-cfb1': (16, 16, OpenSSLCrypto), 129 | 'aes-192-cfb1': (24, 16, OpenSSLCrypto), 130 | 'aes-256-cfb1': (32, 16, OpenSSLCrypto), 131 | 'bf-cfb': (16, 8, OpenSSLCrypto), 132 | 'camellia-128-cfb': (16, 16, OpenSSLCrypto), 133 | 'camellia-192-cfb': (24, 16, OpenSSLCrypto), 134 | 'camellia-256-cfb': (32, 16, OpenSSLCrypto), 135 | 'cast5-cfb': (16, 8, OpenSSLCrypto), 136 | 'des-cfb': (8, 8, OpenSSLCrypto), 137 | 'idea-cfb': (16, 8, OpenSSLCrypto), 138 | 'rc2-cfb': (16, 8, OpenSSLCrypto), 139 | 'rc4': (16, 0, OpenSSLCrypto), 140 | 'seed-cfb': (16, 16, OpenSSLCrypto), 141 | } 142 | 143 | 144 | def run_method(method): 145 | 146 | cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) 147 | decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0) 148 | 149 | util.run_cipher(cipher, decipher) 150 | 151 | 152 | def test_aes_128_cfb(): 153 | run_method('aes-128-cfb') 154 | 155 | 156 | def test_aes_256_cfb(): 157 | run_method('aes-256-cfb') 158 | 159 | 160 | def test_aes_128_cfb8(): 161 | run_method('aes-128-cfb8') 162 | 163 | 164 | def test_aes_256_ofb(): 165 | run_method('aes-256-ofb') 166 | 167 | 168 | def test_aes_256_ctr(): 169 | run_method('aes-256-ctr') 170 | 171 | 172 | def test_bf_cfb(): 173 | run_method('bf-cfb') 174 | 175 | 176 | def test_rc4(): 177 | run_method('rc4') 178 | 179 | 180 | if __name__ == '__main__': 181 | test_aes_128_cfb() 182 | -------------------------------------------------------------------------------- /shadowsocks/crypto/rc4_md5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import hashlib 21 | 22 | from shadowsocks.crypto import openssl 23 | 24 | __all__ = ['ciphers'] 25 | 26 | 27 | def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, 28 | i=1, padding=1): 29 | md5 = hashlib.md5() 30 | md5.update(key) 31 | md5.update(iv) 32 | rc4_key = md5.digest() 33 | return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) 34 | 35 | 36 | ciphers = { 37 | 'rc4-md5': (16, 16, create_cipher), 38 | } 39 | 40 | 41 | def test(): 42 | from shadowsocks.crypto import util 43 | 44 | cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1) 45 | decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0) 46 | 47 | util.run_cipher(cipher, decipher) 48 | 49 | 50 | if __name__ == '__main__': 51 | test() 52 | -------------------------------------------------------------------------------- /shadowsocks/crypto/sodium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | from ctypes import c_char_p, c_int, c_ulonglong, byref, \ 21 | create_string_buffer, c_void_p 22 | 23 | from shadowsocks.crypto import util 24 | 25 | __all__ = ['ciphers'] 26 | 27 | libsodium = None 28 | loaded = False 29 | 30 | buf_size = 2048 31 | 32 | # for salsa20 and chacha20 33 | BLOCK_SIZE = 64 34 | 35 | 36 | def load_libsodium(): 37 | global loaded, libsodium, buf 38 | 39 | libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', 40 | 'libsodium') 41 | if libsodium is None: 42 | raise Exception('libsodium not found') 43 | 44 | libsodium.crypto_stream_salsa20_xor_ic.restype = c_int 45 | libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, 46 | c_ulonglong, 47 | c_char_p, c_ulonglong, 48 | c_char_p) 49 | libsodium.crypto_stream_chacha20_xor_ic.restype = c_int 50 | libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, 51 | c_ulonglong, 52 | c_char_p, c_ulonglong, 53 | c_char_p) 54 | 55 | buf = create_string_buffer(buf_size) 56 | loaded = True 57 | 58 | 59 | class SodiumCrypto(object): 60 | def __init__(self, cipher_name, key, iv, op): 61 | if not loaded: 62 | load_libsodium() 63 | self.key = key 64 | self.iv = iv 65 | self.key_ptr = c_char_p(key) 66 | self.iv_ptr = c_char_p(iv) 67 | if cipher_name == 'salsa20': 68 | self.cipher = libsodium.crypto_stream_salsa20_xor_ic 69 | elif cipher_name == 'chacha20': 70 | self.cipher = libsodium.crypto_stream_chacha20_xor_ic 71 | else: 72 | raise Exception('Unknown cipher') 73 | # byte counter, not block counter 74 | self.counter = 0 75 | 76 | def update(self, data): 77 | global buf_size, buf 78 | l = len(data) 79 | 80 | # we can only prepend some padding to make the encryption align to 81 | # blocks 82 | padding = self.counter % BLOCK_SIZE 83 | if buf_size < padding + l: 84 | buf_size = (padding + l) * 2 85 | buf = create_string_buffer(buf_size) 86 | 87 | if padding: 88 | data = (b'\0' * padding) + data 89 | self.cipher(byref(buf), c_char_p(data), padding + l, 90 | self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) 91 | self.counter += l 92 | # buf is copied to a str object when we access buf.raw 93 | # strip off the padding 94 | return buf.raw[padding:padding + l] 95 | 96 | 97 | ciphers = { 98 | 'salsa20': (32, 8, SodiumCrypto), 99 | 'chacha20': (32, 8, SodiumCrypto), 100 | } 101 | 102 | 103 | def test_salsa20(): 104 | cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) 105 | decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) 106 | 107 | util.run_cipher(cipher, decipher) 108 | 109 | 110 | def test_chacha20(): 111 | 112 | cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) 113 | decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) 114 | 115 | util.run_cipher(cipher, decipher) 116 | 117 | 118 | if __name__ == '__main__': 119 | test_chacha20() 120 | test_salsa20() 121 | -------------------------------------------------------------------------------- /shadowsocks/crypto/table.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import string 21 | import struct 22 | import hashlib 23 | 24 | 25 | __all__ = ['ciphers'] 26 | 27 | cached_tables = {} 28 | 29 | if hasattr(string, 'maketrans'): 30 | maketrans = string.maketrans 31 | translate = string.translate 32 | else: 33 | maketrans = bytes.maketrans 34 | translate = bytes.translate 35 | 36 | 37 | def get_table(key): 38 | m = hashlib.md5() 39 | m.update(key) 40 | s = m.digest() 41 | a, b = struct.unpack(' 0: 108 | # parent waits for its child 109 | time.sleep(5) 110 | sys.exit(0) 111 | 112 | # child signals its parent to exit 113 | ppid = os.getppid() 114 | pid = os.getpid() 115 | if write_pid_file(pid_file, pid) != 0: 116 | os.kill(ppid, signal.SIGINT) 117 | sys.exit(1) 118 | 119 | os.setsid() 120 | signal.signal(signal.SIGHUP, signal.SIG_IGN) 121 | 122 | print('started') 123 | os.kill(ppid, signal.SIGTERM) 124 | 125 | sys.stdin.close() 126 | try: 127 | freopen(log_file, 'a', sys.stdout) 128 | freopen(log_file, 'a', sys.stderr) 129 | except IOError as e: 130 | shell.print_exception(e) 131 | sys.exit(1) 132 | 133 | 134 | def daemon_stop(pid_file): 135 | import errno 136 | try: 137 | with open(pid_file) as f: 138 | buf = f.read() 139 | pid = common.to_str(buf) 140 | if not buf: 141 | logging.error('not running') 142 | except IOError as e: 143 | shell.print_exception(e) 144 | if e.errno == errno.ENOENT: 145 | # always exit 0 if we are sure daemon is not running 146 | logging.error('not running') 147 | return 148 | sys.exit(1) 149 | pid = int(pid) 150 | if pid > 0: 151 | try: 152 | os.kill(pid, signal.SIGTERM) 153 | except OSError as e: 154 | if e.errno == errno.ESRCH: 155 | logging.error('not running') 156 | # always exit 0 if we are sure daemon is not running 157 | return 158 | shell.print_exception(e) 159 | sys.exit(1) 160 | else: 161 | logging.error('pid is not positive: %d', pid) 162 | 163 | # sleep for maximum 10s 164 | for i in range(0, 200): 165 | try: 166 | # query for the pid 167 | os.kill(pid, 0) 168 | except OSError as e: 169 | if e.errno == errno.ESRCH: 170 | break 171 | time.sleep(0.05) 172 | else: 173 | logging.error('timed out when stopping pid %d', pid) 174 | sys.exit(1) 175 | print('stopped') 176 | os.unlink(pid_file) 177 | 178 | 179 | def set_user(username): 180 | if username is None: 181 | return 182 | 183 | import pwd 184 | import grp 185 | 186 | try: 187 | pwrec = pwd.getpwnam(username) 188 | except KeyError: 189 | logging.error('user not found: %s' % username) 190 | raise 191 | user = pwrec[0] 192 | uid = pwrec[2] 193 | gid = pwrec[3] 194 | 195 | cur_uid = os.getuid() 196 | if uid == cur_uid: 197 | return 198 | if cur_uid != 0: 199 | logging.error('can not set user as nonroot user') 200 | # will raise later 201 | 202 | # inspired by supervisor 203 | if hasattr(os, 'setgroups'): 204 | groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] 205 | groups.insert(0, gid) 206 | os.setgroups(groups) 207 | os.setgid(gid) 208 | os.setuid(uid) 209 | -------------------------------------------------------------------------------- /shadowsocks/encrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2012-2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | 25 | from shadowsocks import common 26 | from shadowsocks.crypto import rc4_md5, openssl, sodium, table 27 | 28 | 29 | method_supported = {} 30 | method_supported.update(rc4_md5.ciphers) 31 | method_supported.update(openssl.ciphers) 32 | method_supported.update(sodium.ciphers) 33 | method_supported.update(table.ciphers) 34 | 35 | 36 | def random_string(length): 37 | return os.urandom(length) 38 | 39 | 40 | cached_keys = {} 41 | 42 | 43 | def try_cipher(key, method=None): 44 | Encryptor(key, method) 45 | 46 | 47 | def EVP_BytesToKey(password, key_len, iv_len): 48 | # equivalent to OpenSSL's EVP_BytesToKey() with count 1 49 | # so that we make the same key and iv as nodejs version 50 | cached_key = '%s-%d-%d' % (password, key_len, iv_len) 51 | r = cached_keys.get(cached_key, None) 52 | if r: 53 | return r 54 | m = [] 55 | i = 0 56 | while len(b''.join(m)) < (key_len + iv_len): 57 | md5 = hashlib.md5() 58 | data = password 59 | if i > 0: 60 | data = m[i - 1] + password 61 | md5.update(data) 62 | m.append(md5.digest()) 63 | i += 1 64 | ms = b''.join(m) 65 | key = ms[:key_len] 66 | iv = ms[key_len:key_len + iv_len] 67 | cached_keys[cached_key] = (key, iv) 68 | return key, iv 69 | 70 | 71 | class Encryptor(object): 72 | def __init__(self, key, method): 73 | self.key = key 74 | self.method = method 75 | self.iv = None 76 | self.iv_sent = False 77 | self.cipher_iv = b'' 78 | self.decipher = None 79 | method = method.lower() 80 | self._method_info = self.get_method_info(method) 81 | if self._method_info: 82 | self.cipher = self.get_cipher(key, method, 1, 83 | random_string(self._method_info[1])) 84 | else: 85 | logging.error('method %s not supported' % method) 86 | sys.exit(1) 87 | 88 | def get_method_info(self, method): 89 | method = method.lower() 90 | m = method_supported.get(method) 91 | return m 92 | 93 | def iv_len(self): 94 | return len(self.cipher_iv) 95 | 96 | def get_cipher(self, password, method, op, iv): 97 | password = common.to_bytes(password) 98 | m = self._method_info 99 | if m[0] > 0: 100 | key, iv_ = EVP_BytesToKey(password, m[0], m[1]) 101 | else: 102 | # key_length == 0 indicates we should use the key directly 103 | key, iv = password, b'' 104 | 105 | iv = iv[:m[1]] 106 | if op == 1: 107 | # this iv is for cipher not decipher 108 | self.cipher_iv = iv[:m[1]] 109 | return m[2](method, key, iv, op) 110 | 111 | def encrypt(self, buf): 112 | if len(buf) == 0: 113 | return buf 114 | if self.iv_sent: 115 | return self.cipher.update(buf) 116 | else: 117 | self.iv_sent = True 118 | return self.cipher_iv + self.cipher.update(buf) 119 | 120 | def decrypt(self, buf): 121 | if len(buf) == 0: 122 | return buf 123 | if self.decipher is None: 124 | decipher_iv_len = self._method_info[1] 125 | decipher_iv = buf[:decipher_iv_len] 126 | self.decipher = self.get_cipher(self.key, self.method, 0, 127 | iv=decipher_iv) 128 | buf = buf[decipher_iv_len:] 129 | if len(buf) == 0: 130 | return buf 131 | return self.decipher.update(buf) 132 | 133 | 134 | def encrypt_all(password, method, op, data): 135 | result = [] 136 | method = method.lower() 137 | (key_len, iv_len, m) = method_supported[method] 138 | if key_len > 0: 139 | key, _ = EVP_BytesToKey(password, key_len, iv_len) 140 | else: 141 | key = password 142 | if op: 143 | iv = random_string(iv_len) 144 | result.append(iv) 145 | else: 146 | iv = data[:iv_len] 147 | data = data[iv_len:] 148 | cipher = m(method, key, iv, op) 149 | result.append(cipher.update(data)) 150 | return b''.join(result) 151 | 152 | 153 | CIPHERS_TO_TEST = [ 154 | 'aes-128-cfb', 155 | 'aes-256-cfb', 156 | 'rc4-md5', 157 | 'salsa20', 158 | 'chacha20', 159 | 'table', 160 | ] 161 | 162 | 163 | def test_encryptor(): 164 | from os import urandom 165 | plain = urandom(10240) 166 | for method in CIPHERS_TO_TEST: 167 | logging.warn(method) 168 | encryptor = Encryptor(b'key', method) 169 | decryptor = Encryptor(b'key', method) 170 | cipher = encryptor.encrypt(plain) 171 | plain2 = decryptor.decrypt(cipher) 172 | assert plain == plain2 173 | 174 | 175 | def test_encrypt_all(): 176 | from os import urandom 177 | plain = urandom(10240) 178 | for method in CIPHERS_TO_TEST: 179 | logging.warn(method) 180 | cipher = encrypt_all(b'key', method, 1, plain) 181 | plain2 = encrypt_all(b'key', method, 0, cipher) 182 | assert plain == plain2 183 | 184 | 185 | if __name__ == '__main__': 186 | test_encrypt_all() 187 | test_encryptor() 188 | -------------------------------------------------------------------------------- /shadowsocks/eventloop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2013-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # from ssloop 19 | # https://github.com/clowwindy/ssloop 20 | 21 | from __future__ import absolute_import, division, print_function, \ 22 | with_statement 23 | 24 | import os 25 | import time 26 | import socket 27 | import select 28 | import errno 29 | import logging 30 | from collections import defaultdict 31 | 32 | from shadowsocks import shell 33 | 34 | 35 | __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', 36 | 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] 37 | 38 | POLL_NULL = 0x00 39 | POLL_IN = 0x01 40 | POLL_OUT = 0x04 41 | POLL_ERR = 0x08 42 | POLL_HUP = 0x10 43 | POLL_NVAL = 0x20 44 | 45 | 46 | EVENT_NAMES = { 47 | POLL_NULL: 'POLL_NULL', 48 | POLL_IN: 'POLL_IN', 49 | POLL_OUT: 'POLL_OUT', 50 | POLL_ERR: 'POLL_ERR', 51 | POLL_HUP: 'POLL_HUP', 52 | POLL_NVAL: 'POLL_NVAL', 53 | } 54 | 55 | # we check timeouts every TIMEOUT_PRECISION seconds 56 | TIMEOUT_PRECISION = 10 57 | 58 | 59 | class KqueueLoop(object): 60 | 61 | MAX_EVENTS = 1024 62 | 63 | def __init__(self): 64 | self._kqueue = select.kqueue() 65 | self._fds = {} 66 | 67 | def _control(self, fd, mode, flags): 68 | events = [] 69 | if mode & POLL_IN: 70 | events.append(select.kevent(fd, select.KQ_FILTER_READ, flags)) 71 | if mode & POLL_OUT: 72 | events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags)) 73 | for e in events: 74 | self._kqueue.control([e], 0) 75 | 76 | def poll(self, timeout): 77 | if timeout < 0: 78 | timeout = None # kqueue behaviour 79 | events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout) 80 | results = defaultdict(lambda: POLL_NULL) 81 | for e in events: 82 | fd = e.ident 83 | if e.filter == select.KQ_FILTER_READ: 84 | results[fd] |= POLL_IN 85 | elif e.filter == select.KQ_FILTER_WRITE: 86 | results[fd] |= POLL_OUT 87 | return results.items() 88 | 89 | def register(self, fd, mode): 90 | self._fds[fd] = mode 91 | self._control(fd, mode, select.KQ_EV_ADD) 92 | 93 | def unregister(self, fd): 94 | self._control(fd, self._fds[fd], select.KQ_EV_DELETE) 95 | del self._fds[fd] 96 | 97 | def modify(self, fd, mode): 98 | self.unregister(fd) 99 | self.register(fd, mode) 100 | 101 | def close(self): 102 | self._kqueue.close() 103 | 104 | 105 | class SelectLoop(object): 106 | 107 | def __init__(self): 108 | self._r_list = set() 109 | self._w_list = set() 110 | self._x_list = set() 111 | 112 | def poll(self, timeout): 113 | r, w, x = select.select(self._r_list, self._w_list, self._x_list, 114 | timeout) 115 | results = defaultdict(lambda: POLL_NULL) 116 | for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]: 117 | for fd in p[0]: 118 | results[fd] |= p[1] 119 | return results.items() 120 | 121 | def register(self, fd, mode): 122 | if mode & POLL_IN: 123 | self._r_list.add(fd) 124 | if mode & POLL_OUT: 125 | self._w_list.add(fd) 126 | if mode & POLL_ERR: 127 | self._x_list.add(fd) 128 | 129 | def unregister(self, fd): 130 | if fd in self._r_list: 131 | self._r_list.remove(fd) 132 | if fd in self._w_list: 133 | self._w_list.remove(fd) 134 | if fd in self._x_list: 135 | self._x_list.remove(fd) 136 | 137 | def modify(self, fd, mode): 138 | self.unregister(fd) 139 | self.register(fd, mode) 140 | 141 | def close(self): 142 | pass 143 | 144 | 145 | class EventLoop(object): 146 | def __init__(self): 147 | if hasattr(select, 'epoll'): 148 | self._impl = select.epoll() 149 | model = 'epoll' 150 | elif hasattr(select, 'kqueue'): 151 | self._impl = KqueueLoop() 152 | model = 'kqueue' 153 | elif hasattr(select, 'select'): 154 | self._impl = SelectLoop() 155 | model = 'select' 156 | else: 157 | raise Exception('can not find any available functions in select ' 158 | 'package') 159 | self._fdmap = {} # (f, handler) 160 | self._last_time = time.time() 161 | self._periodic_callbacks = [] 162 | self._stopping = False 163 | logging.debug('using event model: %s', model) 164 | 165 | def poll(self, timeout=None): 166 | events = self._impl.poll(timeout) 167 | return [(self._fdmap[fd][0], fd, event) for fd, event in events] 168 | 169 | def add(self, f, mode, handler): 170 | fd = f.fileno() 171 | self._fdmap[fd] = (f, handler) 172 | self._impl.register(fd, mode) 173 | 174 | def remove(self, f): 175 | fd = f.fileno() 176 | del self._fdmap[fd] 177 | self._impl.unregister(fd) 178 | 179 | def add_periodic(self, callback): 180 | self._periodic_callbacks.append(callback) 181 | 182 | def remove_periodic(self, callback): 183 | self._periodic_callbacks.remove(callback) 184 | 185 | def modify(self, f, mode): 186 | fd = f.fileno() 187 | self._impl.modify(fd, mode) 188 | 189 | def stop(self): 190 | self._stopping = True 191 | 192 | def run(self): 193 | events = [] 194 | while not self._stopping: 195 | asap = False 196 | try: 197 | events = self.poll(TIMEOUT_PRECISION) 198 | except (OSError, IOError) as e: 199 | if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): 200 | # EPIPE: Happens when the client closes the connection 201 | # EINTR: Happens when received a signal 202 | # handles them as soon as possible 203 | asap = True 204 | logging.debug('poll:%s', e) 205 | else: 206 | logging.error('poll:%s', e) 207 | import traceback 208 | traceback.print_exc() 209 | continue 210 | 211 | for sock, fd, event in events: 212 | handler = self._fdmap.get(fd, None) 213 | if handler is not None: 214 | handler = handler[1] 215 | try: 216 | handler.handle_event(sock, fd, event) 217 | except (OSError, IOError) as e: 218 | shell.print_exception(e) 219 | now = time.time() 220 | if asap or now - self._last_time >= TIMEOUT_PRECISION: 221 | for callback in self._periodic_callbacks: 222 | callback() 223 | self._last_time = now 224 | 225 | def __del__(self): 226 | self._impl.close() 227 | 228 | 229 | # from tornado 230 | def errno_from_exception(e): 231 | """Provides the errno from an Exception object. 232 | 233 | There are cases that the errno attribute was not set so we pull 234 | the errno out of the args but if someone instatiates an Exception 235 | without any args you will get a tuple error. So this function 236 | abstracts all that behavior to give you a safe way to get the 237 | errno. 238 | """ 239 | 240 | if hasattr(e, 'errno'): 241 | return e.errno 242 | elif e.args: 243 | return e.args[0] 244 | else: 245 | return None 246 | 247 | 248 | # from tornado 249 | def get_sock_error(sock): 250 | error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 251 | return socket.error(error_number, os.strerror(error_number)) 252 | -------------------------------------------------------------------------------- /shadowsocks/local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2012-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import sys 22 | import os 23 | import logging 24 | import signal 25 | 26 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) 27 | from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns 28 | 29 | 30 | def main(): 31 | shell.check_python() 32 | 33 | # fix py2exe 34 | if hasattr(sys, "frozen") and sys.frozen in \ 35 | ("windows_exe", "console_exe"): 36 | p = os.path.dirname(os.path.abspath(sys.executable)) 37 | os.chdir(p) 38 | 39 | config = shell.get_config(True) 40 | 41 | daemon.daemon_exec(config) 42 | 43 | try: 44 | logging.info("starting local at %s:%d" % 45 | (config['local_address'], config['local_port'])) 46 | 47 | dns_resolver = asyncdns.DNSResolver() 48 | tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) 49 | udp_server = udprelay.UDPRelay(config, dns_resolver, True) 50 | loop = eventloop.EventLoop() 51 | dns_resolver.add_to_loop(loop) 52 | tcp_server.add_to_loop(loop) 53 | udp_server.add_to_loop(loop) 54 | 55 | def handler(signum, _): 56 | logging.warn('received SIGQUIT, doing graceful shutting down..') 57 | tcp_server.close(next_tick=True) 58 | udp_server.close(next_tick=True) 59 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) 60 | 61 | def int_handler(signum, _): 62 | sys.exit(1) 63 | signal.signal(signal.SIGINT, int_handler) 64 | 65 | daemon.set_user(config.get('user', None)) 66 | loop.run() 67 | except Exception as e: 68 | shell.print_exception(e) 69 | sys.exit(1) 70 | 71 | if __name__ == '__main__': 72 | main() 73 | -------------------------------------------------------------------------------- /shadowsocks/lru_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import collections 22 | import logging 23 | import time 24 | 25 | 26 | # this LRUCache is optimized for concurrency, not QPS 27 | # n: concurrency, keys stored in the cache 28 | # m: visits not timed out, proportional to QPS * timeout 29 | # get & set is O(1), not O(n). thus we can support very large n 30 | # TODO: if timeout or QPS is too large, then this cache is not very efficient, 31 | # as sweep() causes long pause 32 | 33 | 34 | class LRUCache(collections.MutableMapping): 35 | """This class is not thread safe""" 36 | 37 | def __init__(self, timeout=60, close_callback=None, *args, **kwargs): 38 | self.timeout = timeout 39 | self.close_callback = close_callback 40 | self._store = {} 41 | self._time_to_keys = collections.defaultdict(list) 42 | self._keys_to_last_time = {} 43 | self._last_visits = collections.deque() 44 | self._closed_values = set() 45 | self.update(dict(*args, **kwargs)) # use the free update to set keys 46 | 47 | def __getitem__(self, key): 48 | # O(1) 49 | t = time.time() 50 | self._keys_to_last_time[key] = t 51 | self._time_to_keys[t].append(key) 52 | self._last_visits.append(t) 53 | return self._store[key] 54 | 55 | def __setitem__(self, key, value): 56 | # O(1) 57 | t = time.time() 58 | self._keys_to_last_time[key] = t 59 | self._store[key] = value 60 | self._time_to_keys[t].append(key) 61 | self._last_visits.append(t) 62 | 63 | def __delitem__(self, key): 64 | # O(1) 65 | del self._store[key] 66 | del self._keys_to_last_time[key] 67 | 68 | def __iter__(self): 69 | return iter(self._store) 70 | 71 | def __len__(self): 72 | return len(self._store) 73 | 74 | def sweep(self): 75 | # O(m) 76 | now = time.time() 77 | c = 0 78 | while len(self._last_visits) > 0: 79 | least = self._last_visits[0] 80 | if now - least <= self.timeout: 81 | break 82 | if self.close_callback is not None: 83 | for key in self._time_to_keys[least]: 84 | if key in self._store: 85 | if now - self._keys_to_last_time[key] > self.timeout: 86 | value = self._store[key] 87 | if value not in self._closed_values: 88 | self.close_callback(value) 89 | self._closed_values.add(value) 90 | for key in self._time_to_keys[least]: 91 | self._last_visits.popleft() 92 | if key in self._store: 93 | if now - self._keys_to_last_time[key] > self.timeout: 94 | del self._store[key] 95 | del self._keys_to_last_time[key] 96 | c += 1 97 | del self._time_to_keys[least] 98 | if c: 99 | self._closed_values.clear() 100 | logging.debug('%d keys swept' % c) 101 | 102 | 103 | def test(): 104 | c = LRUCache(timeout=0.3) 105 | 106 | c['a'] = 1 107 | assert c['a'] == 1 108 | 109 | time.sleep(0.5) 110 | c.sweep() 111 | assert 'a' not in c 112 | 113 | c['a'] = 2 114 | c['b'] = 3 115 | time.sleep(0.2) 116 | c.sweep() 117 | assert c['a'] == 2 118 | assert c['b'] == 3 119 | 120 | time.sleep(0.2) 121 | c.sweep() 122 | c['b'] 123 | time.sleep(0.2) 124 | c.sweep() 125 | assert 'a' not in c 126 | assert c['b'] == 3 127 | 128 | time.sleep(0.5) 129 | c.sweep() 130 | assert 'a' not in c 131 | assert 'b' not in c 132 | 133 | global close_cb_called 134 | close_cb_called = False 135 | 136 | def close_cb(t): 137 | global close_cb_called 138 | assert not close_cb_called 139 | close_cb_called = True 140 | 141 | c = LRUCache(timeout=0.1, close_callback=close_cb) 142 | c['s'] = 1 143 | c['s'] 144 | time.sleep(0.1) 145 | c['s'] 146 | time.sleep(0.3) 147 | c.sweep() 148 | 149 | if __name__ == '__main__': 150 | test() 151 | -------------------------------------------------------------------------------- /shadowsocks/manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import errno 22 | import traceback 23 | import socket 24 | import logging 25 | import json 26 | import collections 27 | 28 | from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell 29 | 30 | 31 | BUF_SIZE = 1506 32 | STAT_SEND_LIMIT = 100 33 | 34 | 35 | class Manager(object): 36 | 37 | def __init__(self, config): 38 | self._config = config 39 | self._relays = {} # (tcprelay, udprelay) 40 | self._loop = eventloop.EventLoop() 41 | self._dns_resolver = asyncdns.DNSResolver() 42 | self._dns_resolver.add_to_loop(self._loop) 43 | 44 | self._statistics = collections.defaultdict(int) 45 | self._control_client_addr = None 46 | try: 47 | manager_address = config['manager_address'] 48 | if ':' in manager_address: 49 | addr = manager_address.rsplit(':', 1) 50 | addr = addr[0], int(addr[1]) 51 | addrs = socket.getaddrinfo(addr[0], addr[1]) 52 | if addrs: 53 | family = addrs[0][0] 54 | else: 55 | logging.error('invalid address: %s', manager_address) 56 | exit(1) 57 | else: 58 | addr = manager_address 59 | family = socket.AF_UNIX 60 | self._control_socket = socket.socket(family, 61 | socket.SOCK_DGRAM) 62 | self._control_socket.bind(addr) 63 | self._control_socket.setblocking(False) 64 | except (OSError, IOError) as e: 65 | logging.error(e) 66 | logging.error('can not bind to manager address') 67 | exit(1) 68 | self._loop.add(self._control_socket, 69 | eventloop.POLL_IN, self) 70 | self._loop.add_periodic(self.handle_periodic) 71 | 72 | port_password = config['port_password'] 73 | del config['port_password'] 74 | for port, password in port_password.items(): 75 | a_config = config.copy() 76 | a_config['server_port'] = int(port) 77 | a_config['password'] = password 78 | self.add_port(a_config) 79 | 80 | def add_port(self, config): 81 | port = int(config['server_port']) 82 | servers = self._relays.get(port, None) 83 | if servers: 84 | logging.error("server already exists at %s:%d" % (config['server'], 85 | port)) 86 | return 87 | logging.info("adding server at %s:%d" % (config['server'], port)) 88 | t = tcprelay.TCPRelay(config, self._dns_resolver, False, 89 | self.stat_callback) 90 | u = udprelay.UDPRelay(config, self._dns_resolver, False, 91 | self.stat_callback) 92 | t.add_to_loop(self._loop) 93 | u.add_to_loop(self._loop) 94 | self._relays[port] = (t, u) 95 | 96 | def remove_port(self, config): 97 | port = int(config['server_port']) 98 | servers = self._relays.get(port, None) 99 | if servers: 100 | logging.info("removing server at %s:%d" % (config['server'], port)) 101 | t, u = servers 102 | t.close(next_tick=False) 103 | u.close(next_tick=False) 104 | del self._relays[port] 105 | else: 106 | logging.error("server not exist at %s:%d" % (config['server'], 107 | port)) 108 | 109 | def handle_event(self, sock, fd, event): 110 | if sock == self._control_socket and event == eventloop.POLL_IN: 111 | data, self._control_client_addr = sock.recvfrom(BUF_SIZE) 112 | parsed = self._parse_command(data) 113 | if parsed: 114 | command, config = parsed 115 | a_config = self._config.copy() 116 | if config: 117 | # let the command override the configuration file 118 | a_config.update(config) 119 | if 'server_port' not in a_config: 120 | logging.error('can not find server_port in config') 121 | else: 122 | if command == 'add': 123 | self.add_port(a_config) 124 | self._send_control_data(b'ok') 125 | elif command == 'remove': 126 | self.remove_port(a_config) 127 | self._send_control_data(b'ok') 128 | elif command == 'ping': 129 | self._send_control_data(b'pong') 130 | else: 131 | logging.error('unknown command %s', command) 132 | 133 | def _parse_command(self, data): 134 | # commands: 135 | # add: {"server_port": 8000, "password": "foobar"} 136 | # remove: {"server_port": 8000"} 137 | data = common.to_str(data) 138 | parts = data.split(':', 1) 139 | if len(parts) < 2: 140 | return data, None 141 | command, config_json = parts 142 | try: 143 | config = shell.parse_json_in_str(config_json) 144 | return command, config 145 | except Exception as e: 146 | logging.error(e) 147 | return None 148 | 149 | def stat_callback(self, port, data_len): 150 | self._statistics[port] += data_len 151 | 152 | def handle_periodic(self): 153 | r = {} 154 | i = 0 155 | 156 | def send_data(data_dict): 157 | if data_dict: 158 | # use compact JSON format (without space) 159 | data = common.to_bytes(json.dumps(data_dict, 160 | separators=(',', ':'))) 161 | self._send_control_data(b'stat: ' + data) 162 | 163 | for k, v in self._statistics.items(): 164 | r[k] = v 165 | i += 1 166 | # split the data into segments that fit in UDP packets 167 | if i >= STAT_SEND_LIMIT: 168 | send_data(r) 169 | r.clear() 170 | send_data(r) 171 | self._statistics.clear() 172 | 173 | def _send_control_data(self, data): 174 | if self._control_client_addr: 175 | try: 176 | self._control_socket.sendto(data, self._control_client_addr) 177 | except (socket.error, OSError, IOError) as e: 178 | error_no = eventloop.errno_from_exception(e) 179 | if error_no in (errno.EAGAIN, errno.EINPROGRESS, 180 | errno.EWOULDBLOCK): 181 | return 182 | else: 183 | shell.print_exception(e) 184 | if self._config['verbose']: 185 | traceback.print_exc() 186 | 187 | def run(self): 188 | self._loop.run() 189 | 190 | 191 | def run(config): 192 | Manager(config).run() 193 | 194 | 195 | def test(): 196 | import time 197 | import threading 198 | import struct 199 | from shadowsocks import encrypt 200 | 201 | logging.basicConfig(level=5, 202 | format='%(asctime)s %(levelname)-8s %(message)s', 203 | datefmt='%Y-%m-%d %H:%M:%S') 204 | enc = [] 205 | eventloop.TIMEOUT_PRECISION = 1 206 | 207 | def run_server(): 208 | config = { 209 | 'server': '127.0.0.1', 210 | 'local_port': 1081, 211 | 'port_password': { 212 | '8381': 'foobar1', 213 | '8382': 'foobar2' 214 | }, 215 | 'method': 'aes-256-cfb', 216 | 'manager_address': '127.0.0.1:6001', 217 | 'timeout': 60, 218 | 'fast_open': False, 219 | 'verbose': 2 220 | } 221 | manager = Manager(config) 222 | enc.append(manager) 223 | manager.run() 224 | 225 | t = threading.Thread(target=run_server) 226 | t.start() 227 | time.sleep(1) 228 | manager = enc[0] 229 | cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 230 | cli.connect(('127.0.0.1', 6001)) 231 | 232 | # test add and remove 233 | time.sleep(1) 234 | cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') 235 | time.sleep(1) 236 | assert 7001 in manager._relays 237 | data, addr = cli.recvfrom(1506) 238 | assert b'ok' in data 239 | 240 | cli.send(b'remove: {"server_port":8381}') 241 | time.sleep(1) 242 | assert 8381 not in manager._relays 243 | data, addr = cli.recvfrom(1506) 244 | assert b'ok' in data 245 | logging.info('add and remove test passed') 246 | 247 | # test statistics for TCP 248 | header = common.pack_addr(b'google.com') + struct.pack('>H', 80) 249 | data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, 250 | header + b'GET /\r\n\r\n') 251 | tcp_cli = socket.socket() 252 | tcp_cli.connect(('127.0.0.1', 7001)) 253 | tcp_cli.send(data) 254 | tcp_cli.recv(4096) 255 | tcp_cli.close() 256 | 257 | data, addr = cli.recvfrom(1506) 258 | data = common.to_str(data) 259 | assert data.startswith('stat: ') 260 | data = data.split('stat:')[1] 261 | stats = shell.parse_json_in_str(data) 262 | assert '7001' in stats 263 | logging.info('TCP statistics test passed') 264 | 265 | # test statistics for UDP 266 | header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) 267 | data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, 268 | header + b'test') 269 | udp_cli = socket.socket(type=socket.SOCK_DGRAM) 270 | udp_cli.sendto(data, ('127.0.0.1', 8382)) 271 | tcp_cli.close() 272 | 273 | data, addr = cli.recvfrom(1506) 274 | data = common.to_str(data) 275 | assert data.startswith('stat: ') 276 | data = data.split('stat:')[1] 277 | stats = json.loads(data) 278 | assert '8382' in stats 279 | logging.info('UDP statistics test passed') 280 | 281 | manager._loop.stop() 282 | t.join() 283 | 284 | 285 | if __name__ == '__main__': 286 | test() 287 | -------------------------------------------------------------------------------- /shadowsocks/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import sys 22 | import os 23 | import logging 24 | import signal 25 | 26 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) 27 | from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ 28 | asyncdns, manager 29 | 30 | 31 | def main(): 32 | shell.check_python() 33 | 34 | config = shell.get_config(False) 35 | 36 | daemon.daemon_exec(config) 37 | 38 | if config['port_password']: 39 | if config['password']: 40 | logging.warn('warning: port_password should not be used with ' 41 | 'server_port and password. server_port and password ' 42 | 'will be ignored') 43 | else: 44 | config['port_password'] = {} 45 | server_port = config.get('server_port', None) 46 | if server_port: 47 | if type(server_port) == list: 48 | for a_server_port in server_port: 49 | config['port_password'][a_server_port] = config['password'] 50 | else: 51 | config['port_password'][str(server_port)] = config['password'] 52 | 53 | if config.get('manager_address', 0): 54 | logging.info('entering manager mode') 55 | manager.run(config) 56 | return 57 | 58 | tcp_servers = [] 59 | udp_servers = [] 60 | 61 | if 'dns_server' in config: # allow override settings in resolv.conf 62 | dns_resolver = asyncdns.DNSResolver(config['dns_server']) 63 | else: 64 | dns_resolver = asyncdns.DNSResolver() 65 | 66 | port_password = config['port_password'] 67 | del config['port_password'] 68 | for port, password in port_password.items(): 69 | a_config = config.copy() 70 | a_config['server_port'] = int(port) 71 | a_config['password'] = password 72 | logging.info("starting server at %s:%d" % 73 | (a_config['server'], int(port))) 74 | tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False)) 75 | udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False)) 76 | 77 | def run_server(): 78 | def child_handler(signum, _): 79 | logging.warn('received SIGQUIT, doing graceful shutting down..') 80 | list(map(lambda s: s.close(next_tick=True), 81 | tcp_servers + udp_servers)) 82 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), 83 | child_handler) 84 | 85 | def int_handler(signum, _): 86 | sys.exit(1) 87 | signal.signal(signal.SIGINT, int_handler) 88 | 89 | try: 90 | loop = eventloop.EventLoop() 91 | dns_resolver.add_to_loop(loop) 92 | list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) 93 | 94 | daemon.set_user(config.get('user', None)) 95 | loop.run() 96 | except Exception as e: 97 | shell.print_exception(e) 98 | sys.exit(1) 99 | 100 | if int(config['workers']) > 1: 101 | if os.name == 'posix': 102 | children = [] 103 | is_child = False 104 | for i in range(0, int(config['workers'])): 105 | r = os.fork() 106 | if r == 0: 107 | logging.info('worker started') 108 | is_child = True 109 | run_server() 110 | break 111 | else: 112 | children.append(r) 113 | if not is_child: 114 | def handler(signum, _): 115 | for pid in children: 116 | try: 117 | os.kill(pid, signum) 118 | os.waitpid(pid, 0) 119 | except OSError: # child may already exited 120 | pass 121 | sys.exit() 122 | signal.signal(signal.SIGTERM, handler) 123 | signal.signal(signal.SIGQUIT, handler) 124 | signal.signal(signal.SIGINT, handler) 125 | 126 | # master 127 | for a_tcp_server in tcp_servers: 128 | a_tcp_server.close() 129 | for a_udp_server in udp_servers: 130 | a_udp_server.close() 131 | dns_resolver.close() 132 | 133 | for child in children: 134 | os.waitpid(child, 0) 135 | else: 136 | logging.warn('worker is only available on Unix/Linux') 137 | run_server() 138 | else: 139 | run_server() 140 | 141 | 142 | if __name__ == '__main__': 143 | main() 144 | -------------------------------------------------------------------------------- /shadowsocks/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import os 22 | import json 23 | import sys 24 | import getopt 25 | import logging 26 | from shadowsocks.common import to_bytes, to_str, IPNetwork 27 | from shadowsocks import encrypt 28 | 29 | 30 | VERBOSE_LEVEL = 5 31 | 32 | verbose = 0 33 | 34 | 35 | def check_python(): 36 | info = sys.version_info 37 | if info[0] == 2 and not info[1] >= 6: 38 | print('Python 2.6+ required') 39 | sys.exit(1) 40 | elif info[0] == 3 and not info[1] >= 3: 41 | print('Python 3.3+ required') 42 | sys.exit(1) 43 | elif info[0] not in [2, 3]: 44 | print('Python version not supported') 45 | sys.exit(1) 46 | 47 | 48 | def print_exception(e): 49 | global verbose 50 | logging.error(e) 51 | if verbose > 0: 52 | import traceback 53 | traceback.print_exc() 54 | 55 | 56 | def print_shadowsocks(): 57 | version = '' 58 | try: 59 | import pkg_resources 60 | version = pkg_resources.get_distribution('shadowsocks').version 61 | except Exception: 62 | pass 63 | print('Shadowsocks %s' % version) 64 | 65 | 66 | def find_config(): 67 | config_path = 'config.json' 68 | if os.path.exists(config_path): 69 | return config_path 70 | config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json') 71 | if os.path.exists(config_path): 72 | return config_path 73 | return None 74 | 75 | 76 | def check_config(config, is_local): 77 | if config.get('daemon', None) == 'stop': 78 | # no need to specify configuration for daemon stop 79 | return 80 | 81 | if is_local and not config.get('password', None): 82 | logging.error('password not specified') 83 | print_help(is_local) 84 | sys.exit(2) 85 | 86 | if not is_local and not config.get('password', None) \ 87 | and not config.get('port_password', None) \ 88 | and not config.get('manager_address'): 89 | logging.error('password or port_password not specified') 90 | print_help(is_local) 91 | sys.exit(2) 92 | 93 | if 'local_port' in config: 94 | config['local_port'] = int(config['local_port']) 95 | 96 | if config.get('server_port', None) and type(config['server_port']) != list: 97 | config['server_port'] = int(config['server_port']) 98 | 99 | if config.get('local_address', '') in [b'0.0.0.0']: 100 | logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') 101 | if config.get('server', '') in ['127.0.0.1', 'localhost']: 102 | logging.warn('warning: server set to listen on %s:%s, are you sure?' % 103 | (to_str(config['server']), config['server_port'])) 104 | if (config.get('method', '') or '').lower() == 'table': 105 | logging.warn('warning: table is not safe; please use a safer cipher, ' 106 | 'like AES-256-CFB') 107 | if (config.get('method', '') or '').lower() == 'rc4': 108 | logging.warn('warning: RC4 is not safe; please use a safer cipher, ' 109 | 'like AES-256-CFB') 110 | if config.get('timeout', 300) < 100: 111 | logging.warn('warning: your timeout %d seems too short' % 112 | int(config.get('timeout'))) 113 | if config.get('timeout', 300) > 600: 114 | logging.warn('warning: your timeout %d seems too long' % 115 | int(config.get('timeout'))) 116 | if config.get('password') in [b'mypassword']: 117 | logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' 118 | 'config.json!') 119 | sys.exit(1) 120 | if config.get('user', None) is not None: 121 | if os.name != 'posix': 122 | logging.error('user can be used only on Unix') 123 | sys.exit(1) 124 | 125 | encrypt.try_cipher(config['password'], config['method']) 126 | 127 | 128 | def get_config(is_local): 129 | global verbose 130 | 131 | logging.basicConfig(level=logging.INFO, 132 | format='%(levelname)-s: %(message)s') 133 | if is_local: 134 | shortopts = 'hd:s:b:p:k:l:m:c:t:vq' 135 | longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', 136 | 'version'] 137 | else: 138 | shortopts = 'hd:s:p:k:m:c:t:vq' 139 | longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', 140 | 'forbidden-ip=', 'user=', 'manager-address=', 'version'] 141 | try: 142 | config_path = find_config() 143 | optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) 144 | for key, value in optlist: 145 | if key == '-c': 146 | config_path = value 147 | 148 | if config_path: 149 | logging.info('loading config from %s' % config_path) 150 | with open(config_path, 'rb') as f: 151 | try: 152 | config = parse_json_in_str(f.read().decode('utf8')) 153 | except ValueError as e: 154 | logging.error('found an error in config.json: %s', 155 | e.message) 156 | sys.exit(1) 157 | else: 158 | config = {} 159 | 160 | v_count = 0 161 | for key, value in optlist: 162 | if key == '-p': 163 | config['server_port'] = int(value) 164 | elif key == '-k': 165 | config['password'] = to_bytes(value) 166 | elif key == '-l': 167 | config['local_port'] = int(value) 168 | elif key == '-s': 169 | config['server'] = to_str(value) 170 | elif key == '-m': 171 | config['method'] = to_str(value) 172 | elif key == '-b': 173 | config['local_address'] = to_str(value) 174 | elif key == '-v': 175 | v_count += 1 176 | # '-vv' turns on more verbose mode 177 | config['verbose'] = v_count 178 | elif key == '-t': 179 | config['timeout'] = int(value) 180 | elif key == '--fast-open': 181 | config['fast_open'] = True 182 | elif key == '--workers': 183 | config['workers'] = int(value) 184 | elif key == '--manager-address': 185 | config['manager_address'] = value 186 | elif key == '--user': 187 | config['user'] = to_str(value) 188 | elif key == '--forbidden-ip': 189 | config['forbidden_ip'] = to_str(value).split(',') 190 | elif key in ('-h', '--help'): 191 | if is_local: 192 | print_local_help() 193 | else: 194 | print_server_help() 195 | sys.exit(0) 196 | elif key == '--version': 197 | print_shadowsocks() 198 | sys.exit(0) 199 | elif key == '-d': 200 | config['daemon'] = to_str(value) 201 | elif key == '--pid-file': 202 | config['pid-file'] = to_str(value) 203 | elif key == '--log-file': 204 | config['log-file'] = to_str(value) 205 | elif key == '-q': 206 | v_count -= 1 207 | config['verbose'] = v_count 208 | except getopt.GetoptError as e: 209 | print(e, file=sys.stderr) 210 | print_help(is_local) 211 | sys.exit(2) 212 | 213 | if not config: 214 | logging.error('config not specified') 215 | print_help(is_local) 216 | sys.exit(2) 217 | 218 | config['password'] = to_bytes(config.get('password', b'')) 219 | config['method'] = to_str(config.get('method', 'aes-256-cfb')) 220 | config['port_password'] = config.get('port_password', None) 221 | config['timeout'] = int(config.get('timeout', 300)) 222 | config['fast_open'] = config.get('fast_open', False) 223 | config['workers'] = config.get('workers', 1) 224 | config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid') 225 | config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log') 226 | config['verbose'] = config.get('verbose', False) 227 | config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) 228 | config['local_port'] = config.get('local_port', 1080) 229 | if is_local: 230 | if config.get('server', None) is None: 231 | logging.error('server addr not specified') 232 | print_local_help() 233 | sys.exit(2) 234 | else: 235 | config['server'] = to_str(config['server']) 236 | else: 237 | config['server'] = to_str(config.get('server', '0.0.0.0')) 238 | try: 239 | config['forbidden_ip'] = \ 240 | IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128')) 241 | except Exception as e: 242 | logging.error(e) 243 | sys.exit(2) 244 | config['server_port'] = config.get('server_port', None) 245 | 246 | logging.getLogger('').handlers = [] 247 | logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') 248 | if config['verbose'] >= 2: 249 | level = VERBOSE_LEVEL 250 | elif config['verbose'] == 1: 251 | level = logging.DEBUG 252 | elif config['verbose'] == -1: 253 | level = logging.WARN 254 | elif config['verbose'] <= -2: 255 | level = logging.ERROR 256 | else: 257 | level = logging.INFO 258 | verbose = config['verbose'] 259 | logging.basicConfig(level=level, 260 | format='%(asctime)s %(levelname)-8s %(message)s', 261 | datefmt='%Y-%m-%d %H:%M:%S') 262 | 263 | check_config(config, is_local) 264 | 265 | return config 266 | 267 | 268 | def print_help(is_local): 269 | if is_local: 270 | print_local_help() 271 | else: 272 | print_server_help() 273 | 274 | 275 | def print_local_help(): 276 | print('''usage: sslocal [OPTION]... 277 | A fast tunnel proxy that helps you bypass firewalls. 278 | 279 | You can supply configurations via either config file or command line arguments. 280 | 281 | Proxy options: 282 | -c CONFIG path to config file 283 | -s SERVER_ADDR server address 284 | -p SERVER_PORT server port, default: 8388 285 | -b LOCAL_ADDR local binding address, default: 127.0.0.1 286 | -l LOCAL_PORT local port, default: 1080 287 | -k PASSWORD password 288 | -m METHOD encryption method, default: aes-256-cfb 289 | -t TIMEOUT timeout in seconds, default: 300 290 | --fast-open use TCP_FASTOPEN, requires Linux 3.7+ 291 | 292 | General options: 293 | -h, --help show this help message and exit 294 | -d start/stop/restart daemon mode 295 | --pid-file PID_FILE pid file for daemon mode 296 | --log-file LOG_FILE log file for daemon mode 297 | --user USER username to run as 298 | -v, -vv verbose mode 299 | -q, -qq quiet mode, only show warnings/errors 300 | --version show version information 301 | 302 | Online help: 303 | ''') 304 | 305 | 306 | def print_server_help(): 307 | print('''usage: ssserver [OPTION]... 308 | A fast tunnel proxy that helps you bypass firewalls. 309 | 310 | You can supply configurations via either config file or command line arguments. 311 | 312 | Proxy options: 313 | -c CONFIG path to config file 314 | -s SERVER_ADDR server address, default: 0.0.0.0 315 | -p SERVER_PORT server port, default: 8388 316 | -k PASSWORD password 317 | -m METHOD encryption method, default: aes-256-cfb 318 | -t TIMEOUT timeout in seconds, default: 300 319 | --fast-open use TCP_FASTOPEN, requires Linux 3.7+ 320 | --workers WORKERS number of workers, available on Unix/Linux 321 | --forbidden-ip IPLIST comma seperated IP list forbidden to connect 322 | --manager-address ADDR optional server manager UDP address, see wiki 323 | 324 | General options: 325 | -h, --help show this help message and exit 326 | -d start/stop/restart daemon mode 327 | --pid-file PID_FILE pid file for daemon mode 328 | --log-file LOG_FILE log file for daemon mode 329 | --user USER username to run as 330 | -v, -vv verbose mode 331 | -q, -qq quiet mode, only show warnings/errors 332 | --version show version information 333 | 334 | Online help: 335 | ''') 336 | 337 | 338 | def _decode_list(data): 339 | rv = [] 340 | for item in data: 341 | if hasattr(item, 'encode'): 342 | item = item.encode('utf-8') 343 | elif isinstance(item, list): 344 | item = _decode_list(item) 345 | elif isinstance(item, dict): 346 | item = _decode_dict(item) 347 | rv.append(item) 348 | return rv 349 | 350 | 351 | def _decode_dict(data): 352 | rv = {} 353 | for key, value in data.items(): 354 | if hasattr(value, 'encode'): 355 | value = value.encode('utf-8') 356 | elif isinstance(value, list): 357 | value = _decode_list(value) 358 | elif isinstance(value, dict): 359 | value = _decode_dict(value) 360 | rv[key] = value 361 | return rv 362 | 363 | 364 | def parse_json_in_str(data): 365 | # parse json and convert everything from unicode to str 366 | return json.loads(data, object_hook=_decode_dict) 367 | -------------------------------------------------------------------------------- /shadowsocks/udprelay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # SOCKS5 UDP Request 19 | # +----+------+------+----------+----------+----------+ 20 | # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 21 | # +----+------+------+----------+----------+----------+ 22 | # | 2 | 1 | 1 | Variable | 2 | Variable | 23 | # +----+------+------+----------+----------+----------+ 24 | 25 | # SOCKS5 UDP Response 26 | # +----+------+------+----------+----------+----------+ 27 | # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 28 | # +----+------+------+----------+----------+----------+ 29 | # | 2 | 1 | 1 | Variable | 2 | Variable | 30 | # +----+------+------+----------+----------+----------+ 31 | 32 | # shadowsocks UDP Request (before encrypted) 33 | # +------+----------+----------+----------+ 34 | # | ATYP | DST.ADDR | DST.PORT | DATA | 35 | # +------+----------+----------+----------+ 36 | # | 1 | Variable | 2 | Variable | 37 | # +------+----------+----------+----------+ 38 | 39 | # shadowsocks UDP Response (before encrypted) 40 | # +------+----------+----------+----------+ 41 | # | ATYP | DST.ADDR | DST.PORT | DATA | 42 | # +------+----------+----------+----------+ 43 | # | 1 | Variable | 2 | Variable | 44 | # +------+----------+----------+----------+ 45 | 46 | # shadowsocks UDP Request and Response (after encrypted) 47 | # +-------+--------------+ 48 | # | IV | PAYLOAD | 49 | # +-------+--------------+ 50 | # | Fixed | Variable | 51 | # +-------+--------------+ 52 | 53 | # HOW TO NAME THINGS 54 | # ------------------ 55 | # `dest` means destination server, which is from DST fields in the SOCKS5 56 | # request 57 | # `local` means local server of shadowsocks 58 | # `remote` means remote server of shadowsocks 59 | # `client` means UDP clients that connects to other servers 60 | # `server` means the UDP server that handles user requests 61 | 62 | from __future__ import absolute_import, division, print_function, \ 63 | with_statement 64 | 65 | import socket 66 | import logging 67 | import struct 68 | import errno 69 | import random 70 | 71 | from shadowsocks import encrypt, eventloop, lru_cache, common, shell 72 | from shadowsocks.common import parse_header, pack_addr 73 | 74 | 75 | BUF_SIZE = 65536 76 | 77 | 78 | def client_key(source_addr, server_af): 79 | # notice this is server af, not dest af 80 | return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af) 81 | 82 | 83 | class UDPRelay(object): 84 | def __init__(self, config, dns_resolver, is_local, stat_callback=None): 85 | self._config = config 86 | if is_local: 87 | self._listen_addr = config['local_address'] 88 | self._listen_port = config['local_port'] 89 | self._remote_addr = config['server'] 90 | self._remote_port = config['server_port'] 91 | else: 92 | self._listen_addr = config['server'] 93 | self._listen_port = config['server_port'] 94 | self._remote_addr = None 95 | self._remote_port = None 96 | self._dns_resolver = dns_resolver 97 | self._password = common.to_bytes(config['password']) 98 | self._method = config['method'] 99 | self._timeout = config['timeout'] 100 | self._is_local = is_local 101 | self._cache = lru_cache.LRUCache(timeout=config['timeout'], 102 | close_callback=self._close_client) 103 | self._client_fd_to_server_addr = \ 104 | lru_cache.LRUCache(timeout=config['timeout']) 105 | self._dns_cache = lru_cache.LRUCache(timeout=300) 106 | self._eventloop = None 107 | self._closed = False 108 | self._sockets = set() 109 | if 'forbidden_ip' in config: 110 | self._forbidden_iplist = config['forbidden_ip'] 111 | else: 112 | self._forbidden_iplist = None 113 | 114 | addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, 115 | socket.SOCK_DGRAM, socket.SOL_UDP) 116 | if len(addrs) == 0: 117 | raise Exception("can't get addrinfo for %s:%d" % 118 | (self._listen_addr, self._listen_port)) 119 | af, socktype, proto, canonname, sa = addrs[0] 120 | server_socket = socket.socket(af, socktype, proto) 121 | server_socket.bind((self._listen_addr, self._listen_port)) 122 | server_socket.setblocking(False) 123 | self._server_socket = server_socket 124 | self._stat_callback = stat_callback 125 | 126 | def _get_a_server(self): 127 | server = self._config['server'] 128 | server_port = self._config['server_port'] 129 | if type(server_port) == list: 130 | server_port = random.choice(server_port) 131 | if type(server) == list: 132 | server = random.choice(server) 133 | logging.debug('chosen server: %s:%d', server, server_port) 134 | return server, server_port 135 | 136 | def _close_client(self, client): 137 | if hasattr(client, 'close'): 138 | self._sockets.remove(client.fileno()) 139 | self._eventloop.remove(client) 140 | client.close() 141 | else: 142 | # just an address 143 | pass 144 | 145 | def _handle_server(self): 146 | server = self._server_socket 147 | data, r_addr = server.recvfrom(BUF_SIZE) 148 | if not data: 149 | logging.debug('UDP handle_server: data is empty') 150 | if self._stat_callback: 151 | self._stat_callback(self._listen_port, len(data)) 152 | if self._is_local: 153 | frag = common.ord(data[2]) 154 | if frag != 0: 155 | logging.warn('drop a message since frag is not 0') 156 | return 157 | else: 158 | data = data[3:] 159 | else: 160 | data = encrypt.encrypt_all(self._password, self._method, 0, data) 161 | # decrypt data 162 | if not data: 163 | logging.debug('UDP handle_server: data is empty after decrypt') 164 | return 165 | header_result = parse_header(data) 166 | if header_result is None: 167 | return 168 | addrtype, dest_addr, dest_port, header_length = header_result 169 | 170 | if self._is_local: 171 | server_addr, server_port = self._get_a_server() 172 | else: 173 | server_addr, server_port = dest_addr, dest_port 174 | 175 | addrs = self._dns_cache.get(server_addr, None) 176 | if addrs is None: 177 | addrs = socket.getaddrinfo(server_addr, server_port, 0, 178 | socket.SOCK_DGRAM, socket.SOL_UDP) 179 | if not addrs: 180 | # drop 181 | return 182 | else: 183 | self._dns_cache[server_addr] = addrs 184 | 185 | af, socktype, proto, canonname, sa = addrs[0] 186 | key = client_key(r_addr, af) 187 | client = self._cache.get(key, None) 188 | if not client: 189 | # TODO async getaddrinfo 190 | if self._forbidden_iplist: 191 | if common.to_str(sa[0]) in self._forbidden_iplist: 192 | logging.debug('IP %s is in forbidden list, drop' % 193 | common.to_str(sa[0])) 194 | # drop 195 | return 196 | client = socket.socket(af, socktype, proto) 197 | client.setblocking(False) 198 | self._cache[key] = client 199 | self._client_fd_to_server_addr[client.fileno()] = r_addr 200 | 201 | self._sockets.add(client.fileno()) 202 | self._eventloop.add(client, eventloop.POLL_IN, self) 203 | 204 | if self._is_local: 205 | data = encrypt.encrypt_all(self._password, self._method, 1, data) 206 | if not data: 207 | return 208 | else: 209 | data = data[header_length:] 210 | if not data: 211 | return 212 | try: 213 | client.sendto(data, (server_addr, server_port)) 214 | except IOError as e: 215 | err = eventloop.errno_from_exception(e) 216 | if err in (errno.EINPROGRESS, errno.EAGAIN): 217 | pass 218 | else: 219 | shell.print_exception(e) 220 | 221 | def _handle_client(self, sock): 222 | data, r_addr = sock.recvfrom(BUF_SIZE) 223 | if not data: 224 | logging.debug('UDP handle_client: data is empty') 225 | return 226 | if self._stat_callback: 227 | self._stat_callback(self._listen_port, len(data)) 228 | if not self._is_local: 229 | addrlen = len(r_addr[0]) 230 | if addrlen > 255: 231 | # drop 232 | return 233 | data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data 234 | response = encrypt.encrypt_all(self._password, self._method, 1, 235 | data) 236 | if not response: 237 | return 238 | else: 239 | data = encrypt.encrypt_all(self._password, self._method, 0, 240 | data) 241 | if not data: 242 | return 243 | header_result = parse_header(data) 244 | if header_result is None: 245 | return 246 | # addrtype, dest_addr, dest_port, header_length = header_result 247 | response = b'\x00\x00\x00' + data 248 | client_addr = self._client_fd_to_server_addr.get(sock.fileno()) 249 | if client_addr: 250 | self._server_socket.sendto(response, client_addr) 251 | else: 252 | # this packet is from somewhere else we know 253 | # simply drop that packet 254 | pass 255 | 256 | def add_to_loop(self, loop): 257 | if self._eventloop: 258 | raise Exception('already add to loop') 259 | if self._closed: 260 | raise Exception('already closed') 261 | self._eventloop = loop 262 | 263 | server_socket = self._server_socket 264 | self._eventloop.add(server_socket, 265 | eventloop.POLL_IN | eventloop.POLL_ERR, self) 266 | loop.add_periodic(self.handle_periodic) 267 | 268 | def handle_event(self, sock, fd, event): 269 | if sock == self._server_socket: 270 | if event & eventloop.POLL_ERR: 271 | logging.error('UDP server_socket err') 272 | self._handle_server() 273 | elif sock and (fd in self._sockets): 274 | if event & eventloop.POLL_ERR: 275 | logging.error('UDP client_socket err') 276 | self._handle_client(sock) 277 | 278 | def handle_periodic(self): 279 | if self._closed: 280 | if self._server_socket: 281 | self._server_socket.close() 282 | self._server_socket = None 283 | for sock in self._sockets: 284 | sock.close() 285 | logging.info('closed UDP port %d', self._listen_port) 286 | self._cache.sweep() 287 | self._client_fd_to_server_addr.sweep() 288 | 289 | def close(self, next_tick=False): 290 | logging.debug('UDP close') 291 | self._closed = True 292 | if not next_tick: 293 | if self._eventloop: 294 | self._eventloop.remove_periodic(self.handle_periodic) 295 | self._eventloop.remove(self._server_socket) 296 | self._server_socket.close() 297 | for client in list(self._cache.values()): 298 | client.close() 299 | -------------------------------------------------------------------------------- /tests/aes-cfb1.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb1", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/aes-cfb8.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb8", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/aes-ctr.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-ctr", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/aes.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/assert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # assert.sh 1.0 - bash unit testing framework 3 | # Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann 4 | # 5 | # http://github.com/lehmannro/assert.sh 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published 9 | # by the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with this program. If not, see . 19 | 20 | export DISCOVERONLY=${DISCOVERONLY:-} 21 | export DEBUG=${DEBUG:-} 22 | export STOP=${STOP:-} 23 | export INVARIANT=${INVARIANT:-} 24 | export CONTINUE=${CONTINUE:-} 25 | 26 | args="$(getopt -n "$0" -l \ 27 | verbose,help,stop,discover,invariant,continue vhxdic $*)" \ 28 | || exit -1 29 | for arg in $args; do 30 | case "$arg" in 31 | -h) 32 | echo "$0 [-vxidc]" \ 33 | "[--verbose] [--stop] [--invariant] [--discover] [--continue]" 34 | echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" 35 | exit 0;; 36 | --help) 37 | cat < [stdin] 98 | (( tests_ran++ )) || : 99 | [[ -n "$DISCOVERONLY" ]] && return || true 100 | # printf required for formatting 101 | printf -v expected "x${2:-}" # x required to overwrite older results 102 | result="$(eval 2>/dev/null $1 <<< ${3:-})" || true 103 | # Note: $expected is already decorated 104 | if [[ "x$result" == "$expected" ]]; then 105 | [[ -n "$DEBUG" ]] && echo -n . || true 106 | return 107 | fi 108 | result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" 109 | [[ -z "$result" ]] && result="nothing" || result="\"$result\"" 110 | [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" 111 | _assert_fail "expected $expected${_indent}got $result" "$1" "$3" 112 | } 113 | 114 | assert_raises() { 115 | # assert_raises [stdin] 116 | (( tests_ran++ )) || : 117 | [[ -n "$DISCOVERONLY" ]] && return || true 118 | status=0 119 | (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? 120 | expected=${2:-0} 121 | if [[ "$status" -eq "$expected" ]]; then 122 | [[ -n "$DEBUG" ]] && echo -n . || true 123 | return 124 | fi 125 | _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" 126 | } 127 | 128 | _assert_fail() { 129 | # _assert_fail 130 | [[ -n "$DEBUG" ]] && echo -n X 131 | report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" 132 | if [[ -n "$STOP" ]]; then 133 | [[ -n "$DEBUG" ]] && echo 134 | echo "$report" 135 | exit 1 136 | fi 137 | tests_errors[$tests_failed]="$report" 138 | (( tests_failed++ )) || : 139 | } 140 | 141 | _assert_reset 142 | : ${tests_suite_status:=0} # remember if any of the tests failed so far 143 | _assert_cleanup() { 144 | local status=$? 145 | # modify exit code if it's not already non-zero 146 | [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status 147 | } 148 | trap _assert_cleanup EXIT 149 | -------------------------------------------------------------------------------- /tests/chacha20.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"chacha20", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/client-multi-server-ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":["127.0.0.1", "127.0.0.1"], 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/coverage_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | if __name__ == '__main__': 18 | import tornado.ioloop 19 | import tornado.web 20 | import urllib 21 | 22 | class MainHandler(tornado.web.RequestHandler): 23 | def get(self, project): 24 | try: 25 | with open('/tmp/%s-coverage' % project, 'rb') as f: 26 | coverage = f.read().strip() 27 | n = int(coverage.strip('%')) 28 | if n >= 80: 29 | color = 'brightgreen' 30 | else: 31 | color = 'yellow' 32 | self.redirect(('https://img.shields.io/badge/' 33 | 'coverage-%s-%s.svg' 34 | '?style=flat') % 35 | (urllib.quote(coverage), color)) 36 | except IOError: 37 | raise tornado.web.HTTPError(404) 38 | 39 | application = tornado.web.Application([ 40 | (r"/([a-zA-Z0-9\-_]+)", MainHandler), 41 | ]) 42 | 43 | if __name__ == "__main__": 44 | application.listen(8888, address='127.0.0.1') 45 | tornado.ioloop.IOLoop.instance().start() 46 | -------------------------------------------------------------------------------- /tests/fastopen.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"fastopen_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":true 10 | } 11 | -------------------------------------------------------------------------------- /tests/gen_multiple_passwd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | 5 | with open('server-multi-passwd-performance.json', 'wb') as f: 6 | r = { 7 | 'server': '127.0.0.1', 8 | 'local_port': 1081, 9 | 'timeout': 60, 10 | 'method': 'aes-256-cfb' 11 | } 12 | ports = {} 13 | for i in range(7000, 9000): 14 | ports[str(i)] = 'aes_password' 15 | 16 | r['port_password'] = ports 17 | print(r) 18 | f.write(json.dumps(r, indent=4).encode('utf-8')) 19 | -------------------------------------------------------------------------------- /tests/graceful.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":15, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/graceful_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socks 4 | import time 5 | 6 | 7 | SERVER_IP = '127.0.0.1' 8 | SERVER_PORT = 8001 9 | 10 | 11 | if __name__ == '__main__': 12 | s = socks.socksocket() 13 | s.set_proxy(socks.SOCKS5, SERVER_IP, 1081) 14 | s.connect((SERVER_IP, SERVER_PORT)) 15 | s.send(b'test') 16 | time.sleep(30) 17 | s.close() 18 | -------------------------------------------------------------------------------- /tests/graceful_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socket 4 | 5 | 6 | if __name__ == '__main__': 7 | s = socket.socket() 8 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 9 | s.bind(('127.0.0.1', 8001)) 10 | s.listen(1024) 11 | c = None 12 | while True: 13 | c = s.accept() 14 | -------------------------------------------------------------------------------- /tests/ipv6-client-side.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"::1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/ipv6.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"::", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=0 4 | 5 | function run_test { 6 | printf '\e[0;36m' 7 | echo "running test: $command $@" 8 | printf '\e[0m' 9 | 10 | $command "$@" 11 | status=$? 12 | if [ $status -ne 0 ]; then 13 | printf '\e[0;31m' 14 | echo "test failed: $command $@" 15 | printf '\e[0m' 16 | echo 17 | result=1 18 | else 19 | printf '\e[0;32m' 20 | echo OK 21 | printf '\e[0m' 22 | echo 23 | fi 24 | return 0 25 | } 26 | 27 | python --version 28 | coverage erase 29 | mkdir tmp 30 | run_test pep8 --ignore=E402 . 31 | run_test pyflakes . 32 | run_test coverage run tests/nose_plugin.py -v 33 | run_test python setup.py sdist 34 | run_test tests/test_daemon.sh 35 | run_test python tests/test.py --with-coverage -c tests/aes.json 36 | run_test python tests/test.py --with-coverage -c tests/aes-ctr.json 37 | run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json 38 | run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json 39 | run_test python tests/test.py --with-coverage -c tests/rc4-md5.json 40 | run_test python tests/test.py --with-coverage -c tests/salsa20.json 41 | run_test python tests/test.py --with-coverage -c tests/chacha20.json 42 | run_test python tests/test.py --with-coverage -c tests/table.json 43 | run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json 44 | run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json 45 | run_test python tests/test.py --with-coverage -s tests/server-dnsserver.json -c tests/client-multi-server-ip.json 46 | run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json 47 | run_test python tests/test.py --with-coverage -c tests/workers.json 48 | run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json 49 | 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" 50 | 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" 51 | 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" 52 | 53 | # test if DNS works 54 | run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" 55 | 56 | # test localhost is in the forbidden list by default 57 | 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" 58 | 59 | # test localhost is available when forbidden list is empty 60 | 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" 61 | 62 | if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then 63 | if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then 64 | # we have to run it twice: 65 | # the first time there's no syn cookie 66 | # the second time there is syn cookie 67 | run_test python tests/test.py --with-coverage -c tests/fastopen.json 68 | run_test python tests/test.py --with-coverage -c tests/fastopen.json 69 | fi 70 | fi 71 | 72 | run_test tests/test_large_file.sh 73 | 74 | if [ "a$JENKINS" != "a1" ] ; then 75 | # jenkins blocked SIGQUIT with sigprocmask(), we have to skip this test on Jenkins 76 | run_test tests/test_graceful_restart.sh 77 | fi 78 | run_test tests/test_udp_src.sh 79 | run_test tests/test_command.sh 80 | 81 | coverage combine && coverage report --include=shadowsocks/* 82 | rm -rf htmlcov 83 | rm -rf tmp 84 | coverage html --include=shadowsocks/* 85 | 86 | coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage 87 | 88 | exit $result 89 | -------------------------------------------------------------------------------- /tests/libsodium/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d libsodium-1.0.1 ]; then 4 | wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 5 | tar xf libsodium-1.0.1.tar.gz || exit 1 6 | fi 7 | pushd libsodium-1.0.1 8 | ./configure && make -j2 && make install || exit 1 9 | sudo ldconfig 10 | popd 11 | -------------------------------------------------------------------------------- /tests/nose_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import nose 18 | from nose.plugins.base import Plugin 19 | 20 | 21 | class ExtensionPlugin(Plugin): 22 | 23 | name = "ExtensionPlugin" 24 | 25 | def options(self, parser, env): 26 | Plugin.options(self, parser, env) 27 | 28 | def configure(self, options, config): 29 | Plugin.configure(self, options, config) 30 | self.enabled = True 31 | 32 | def wantFile(self, file): 33 | return file.endswith('.py') 34 | 35 | def wantDirectory(self, directory): 36 | return True 37 | 38 | def wantModule(self, file): 39 | return True 40 | 41 | 42 | if __name__ == '__main__': 43 | nose.main(addplugins=[ExtensionPlugin()]) 44 | -------------------------------------------------------------------------------- /tests/rc4-md5.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"rc4-md5", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/salsa20-ctr.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"salsa20-ctr", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/salsa20.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"salsa20", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/server-dnsserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false, 10 | "dns_server": ["8.8.8.8","8.8.4.4"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/server-multi-passwd-client-side.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": "8385", 4 | "local_port": 1081, 5 | "password": "foobar5", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /tests/server-multi-passwd-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "local_port": 1081, 4 | "port_password": { 5 | }, 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /tests/server-multi-passwd-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": 8384, 4 | "local_port": 1081, 5 | "password": "foobar4", 6 | "port_password": { 7 | "8381": "foobar1", 8 | "8382": "foobar2", 9 | "8383": "foobar3", 10 | "8384": "foobar4", 11 | "8385": "foobar5", 12 | "8386": "foobar6", 13 | "8387": "foobar7", 14 | "8388": "foobar8", 15 | "8389": "foobar9" 16 | }, 17 | "timeout": 60, 18 | "method": "table" 19 | } 20 | -------------------------------------------------------------------------------- /tests/server-multi-passwd.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "local_port": 1081, 4 | "port_password": { 5 | "8381": "foobar1", 6 | "8382": "foobar2", 7 | "8383": "foobar3", 8 | "8384": "foobar4", 9 | "8385": "foobar5", 10 | "8386": "foobar6", 11 | "8387": "foobar7", 12 | "8388": "foobar8", 13 | "8389": "foobar9" 14 | }, 15 | "timeout": 60, 16 | "method": "aes-256-cfb" 17 | } 18 | -------------------------------------------------------------------------------- /tests/server-multi-ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": [8384, 8345, 8346, 8347], 4 | "local_port": 1081, 5 | "password": "foobar4", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /tests/setup_tc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEV=lo 4 | PORT=8388 5 | DELAY=100ms 6 | 7 | type tc 2> /dev/null && ( 8 | tc qdisc add dev $DEV root handle 1: htb 9 | tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps 10 | tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0 11 | tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6 12 | 13 | tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6 14 | tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6 15 | 16 | tc qdisc show dev lo 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /tests/socksify/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d dante-1.4.0 ]; then 4 | wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 5 | tar xf dante-1.4.0.tar.gz || exit 1 6 | fi 7 | pushd dante-1.4.0 8 | ./configure && make -j4 && make install || exit 1 9 | popd 10 | cp tests/socksify/socks.conf /etc/ || exit 1 11 | -------------------------------------------------------------------------------- /tests/socksify/socks.conf: -------------------------------------------------------------------------------- 1 | route { 2 | from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1081 3 | proxyprotocol: socks_v5 4 | method: none 5 | } -------------------------------------------------------------------------------- /tests/table.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"table_password", 6 | "timeout":60, 7 | "method":"table", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import sys 22 | import os 23 | import signal 24 | import select 25 | import time 26 | import argparse 27 | from subprocess import Popen, PIPE 28 | 29 | python = ['python'] 30 | 31 | default_url = 'http://localhost/' 32 | 33 | parser = argparse.ArgumentParser(description='test Shadowsocks') 34 | parser.add_argument('-c', '--client-conf', type=str, default=None) 35 | parser.add_argument('-s', '--server-conf', type=str, default=None) 36 | parser.add_argument('-a', '--client-args', type=str, default=None) 37 | parser.add_argument('-b', '--server-args', type=str, default=None) 38 | parser.add_argument('--with-coverage', action='store_true', default=None) 39 | parser.add_argument('--should-fail', action='store_true', default=None) 40 | parser.add_argument('--tcp-only', action='store_true', default=None) 41 | parser.add_argument('--url', type=str, default=default_url) 42 | parser.add_argument('--dns', type=str, default='8.8.8.8') 43 | 44 | config = parser.parse_args() 45 | 46 | if config.with_coverage: 47 | python = ['coverage', 'run', '-p', '-a'] 48 | 49 | client_args = python + ['shadowsocks/local.py', '-v'] 50 | server_args = python + ['shadowsocks/server.py', '-v'] 51 | 52 | if config.client_conf: 53 | client_args.extend(['-c', config.client_conf]) 54 | if config.server_conf: 55 | server_args.extend(['-c', config.server_conf]) 56 | else: 57 | server_args.extend(['-c', config.client_conf]) 58 | if config.client_args: 59 | client_args.extend(config.client_args.split()) 60 | if config.server_args: 61 | server_args.extend(config.server_args.split()) 62 | else: 63 | server_args.extend(config.client_args.split()) 64 | if config.url == default_url: 65 | server_args.extend(['--forbidden-ip', '']) 66 | 67 | p1 = Popen(server_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 68 | p2 = Popen(client_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 69 | p3 = None 70 | p4 = None 71 | p3_fin = False 72 | p4_fin = False 73 | 74 | # 1 shadowsocks started 75 | # 2 curl started 76 | # 3 curl finished 77 | # 4 dig started 78 | # 5 dig finished 79 | stage = 1 80 | 81 | try: 82 | local_ready = False 83 | server_ready = False 84 | fdset = [p1.stdout, p2.stdout, p1.stderr, p2.stderr] 85 | while True: 86 | r, w, e = select.select(fdset, [], fdset) 87 | if e: 88 | break 89 | 90 | for fd in r: 91 | line = fd.readline() 92 | if not line: 93 | if stage == 2 and fd == p3.stdout: 94 | stage = 3 95 | if stage == 4 and fd == p4.stdout: 96 | stage = 5 97 | if bytes != str: 98 | line = str(line, 'utf8') 99 | sys.stderr.write(line) 100 | if line.find('starting local') >= 0: 101 | local_ready = True 102 | if line.find('starting server') >= 0: 103 | server_ready = True 104 | 105 | if stage == 1: 106 | time.sleep(2) 107 | 108 | p3 = Popen(['curl', config.url, '-v', '-L', 109 | '--socks5-hostname', '127.0.0.1:1081', 110 | '-m', '15', '--connect-timeout', '10'], 111 | stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 112 | if p3 is not None: 113 | fdset.append(p3.stdout) 114 | fdset.append(p3.stderr) 115 | stage = 2 116 | else: 117 | sys.exit(1) 118 | 119 | if stage == 3 and p3 is not None: 120 | fdset.remove(p3.stdout) 121 | fdset.remove(p3.stderr) 122 | r = p3.wait() 123 | if config.should_fail: 124 | if r == 0: 125 | sys.exit(1) 126 | else: 127 | if r != 0: 128 | sys.exit(1) 129 | if config.tcp_only: 130 | break 131 | p4 = Popen(['socksify', 'dig', '@%s' % config.dns, 132 | 'www.google.com'], 133 | stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 134 | if p4 is not None: 135 | fdset.append(p4.stdout) 136 | fdset.append(p4.stderr) 137 | stage = 4 138 | else: 139 | sys.exit(1) 140 | 141 | if stage == 5: 142 | r = p4.wait() 143 | if config.should_fail: 144 | if r == 0: 145 | sys.exit(1) 146 | print('test passed (expecting failure)') 147 | else: 148 | if r != 0: 149 | sys.exit(1) 150 | print('test passed') 151 | break 152 | finally: 153 | for p in [p1, p2]: 154 | try: 155 | os.kill(p.pid, signal.SIGINT) 156 | os.waitpid(p.pid, 0) 157 | except OSError: 158 | pass 159 | -------------------------------------------------------------------------------- /tests/test_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . tests/assert.sh 4 | 5 | PYTHON="coverage run -a -p" 6 | LOCAL="$PYTHON shadowsocks/local.py" 7 | SERVER="$PYTHON shadowsocks/server.py" 8 | 9 | assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" 10 | assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" 11 | 12 | assert "$LOCAL 2>&1 | grep ERROR" "ERROR: config not specified" 13 | assert "$LOCAL 2>&1 | grep usage | cut -d: -f1" "usage" 14 | 15 | assert "$SERVER 2>&1 | grep ERROR" "ERROR: config not specified" 16 | assert "$SERVER 2>&1 | grep usage | cut -d: -f1" "usage" 17 | 18 | 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?" 19 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 20 | 21 | 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" 22 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 23 | 24 | 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" 25 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 26 | 27 | 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" 28 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 29 | 30 | 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!" 31 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 32 | 33 | assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified" 34 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 35 | 36 | 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" 37 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 38 | 39 | 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" 40 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 41 | 42 | 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" 43 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 44 | 45 | assert_end command 46 | -------------------------------------------------------------------------------- /tests/test_daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function run_test { 4 | expected=$1 5 | shift 6 | echo "running test: $command $@" 7 | $command $@ 8 | status=$? 9 | if [ $status -ne $expected ]; then 10 | echo "exit $status != $expected" 11 | exit 1 12 | fi 13 | echo "exit status $status == $expected" 14 | echo OK 15 | return 16 | } 17 | 18 | for module in local server 19 | do 20 | 21 | command="coverage run -p -a shadowsocks/$module.py" 22 | 23 | mkdir -p tmp 24 | 25 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 26 | 27 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 28 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 29 | 30 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 31 | run_test 1 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 32 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 33 | 34 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 35 | run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 36 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 37 | 38 | run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 39 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 40 | 41 | run_test 1 -c tests/aes.json -d start --pid-file tmp/not_exist/shadowsocks.pid --log-file tmp/shadowsocks.log 42 | 43 | done 44 | -------------------------------------------------------------------------------- /tests/test_graceful_restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON="coverage run -p -a" 4 | URL=http://127.0.0.1/file 5 | 6 | 7 | # setup processes 8 | $PYTHON shadowsocks/local.py -c tests/graceful.json & 9 | LOCAL=$! 10 | 11 | $PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & 12 | SERVER=$! 13 | 14 | python tests/graceful_server.py & 15 | GSERVER=$! 16 | 17 | sleep 1 18 | 19 | python tests/graceful_cli.py & 20 | GCLI=$! 21 | 22 | sleep 1 23 | 24 | # graceful restart server: send SIGQUIT to old process and start a new one 25 | kill -s SIGQUIT $SERVER 26 | sleep 0.5 27 | $PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & 28 | NEWSERVER=$! 29 | 30 | sleep 1 31 | 32 | # check old server 33 | ps x | grep -v grep | grep $SERVER 34 | OLD_SERVER_RUNNING1=$? 35 | # old server should not quit at this moment 36 | echo old server running: $OLD_SERVER_RUNNING1 37 | 38 | sleep 1 39 | 40 | # close connections on old server 41 | kill -s SIGKILL $GCLI 42 | kill -s SIGKILL $GSERVER 43 | kill -s SIGINT $LOCAL 44 | 45 | sleep 11 46 | 47 | # check old server 48 | ps x | grep -v grep | grep $SERVER 49 | OLD_SERVER_RUNNING2=$? 50 | # old server should quit at this moment 51 | echo old server running: $OLD_SERVER_RUNNING2 52 | 53 | kill -s SIGINT $SERVER 54 | # new server is expected running 55 | kill -s SIGINT $NEWSERVER || exit 1 56 | 57 | if [ $OLD_SERVER_RUNNING1 -ne 0 ]; then 58 | exit 1 59 | fi 60 | 61 | if [ $OLD_SERVER_RUNNING2 -ne 1 ]; then 62 | sleep 1 63 | exit 1 64 | fi 65 | -------------------------------------------------------------------------------- /tests/test_large_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON="coverage run -p -a" 4 | URL=http://127.0.0.1/file 5 | 6 | mkdir -p tmp 7 | 8 | $PYTHON shadowsocks/local.py -c tests/aes.json & 9 | LOCAL=$! 10 | 11 | $PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" & 12 | SERVER=$! 13 | 14 | sleep 3 15 | 16 | time curl -o tmp/expected $URL 17 | time curl -o tmp/result --socks5-hostname 127.0.0.1:1081 $URL 18 | 19 | kill -s SIGINT $LOCAL 20 | kill -s SIGINT $SERVER 21 | 22 | sleep 2 23 | 24 | diff tmp/expected tmp/result || exit 1 25 | -------------------------------------------------------------------------------- /tests/test_udp_src.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socket 4 | import socks 5 | 6 | 7 | SERVER_IP = '127.0.0.1' 8 | SERVER_PORT = 1081 9 | 10 | 11 | if __name__ == '__main__': 12 | # Test 1: same source port IPv4 13 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 14 | socket.SOL_UDP) 15 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 16 | sock_out.bind(('127.0.0.1', 9000)) 17 | 18 | sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 19 | socket.SOL_UDP) 20 | sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 21 | socket.SOL_UDP) 22 | 23 | sock_in1.bind(('127.0.0.1', 9001)) 24 | sock_in2.bind(('127.0.0.1', 9002)) 25 | 26 | sock_out.sendto(b'data', ('127.0.0.1', 9001)) 27 | result1 = sock_in1.recvfrom(8) 28 | 29 | sock_out.sendto(b'data', ('127.0.0.1', 9002)) 30 | result2 = sock_in2.recvfrom(8) 31 | 32 | sock_out.close() 33 | sock_in1.close() 34 | sock_in2.close() 35 | 36 | # make sure they're from the same source port 37 | assert result1 == result2 38 | 39 | # Test 2: same source port IPv6 40 | # try again from the same port but IPv6 41 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 42 | socket.SOL_UDP) 43 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 44 | sock_out.bind(('127.0.0.1', 9000)) 45 | 46 | sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 47 | socket.SOL_UDP) 48 | sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 49 | socket.SOL_UDP) 50 | 51 | sock_in1.bind(('::1', 9001)) 52 | sock_in2.bind(('::1', 9002)) 53 | 54 | sock_out.sendto(b'data', ('::1', 9001)) 55 | result1 = sock_in1.recvfrom(8) 56 | 57 | sock_out.sendto(b'data', ('::1', 9002)) 58 | result2 = sock_in2.recvfrom(8) 59 | 60 | sock_out.close() 61 | sock_in1.close() 62 | sock_in2.close() 63 | 64 | # make sure they're from the same source port 65 | assert result1 == result2 66 | 67 | # Test 3: different source ports IPv6 68 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 69 | socket.SOL_UDP) 70 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 71 | sock_out.bind(('127.0.0.1', 9003)) 72 | 73 | sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 74 | socket.SOL_UDP) 75 | sock_in1.bind(('::1', 9001)) 76 | sock_out.sendto(b'data', ('::1', 9001)) 77 | result3 = sock_in1.recvfrom(8) 78 | 79 | # make sure they're from different source ports 80 | assert result1 != result3 81 | 82 | sock_out.close() 83 | sock_in1.close() 84 | -------------------------------------------------------------------------------- /tests/test_udp_src.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON="coverage run -p -a" 4 | 5 | mkdir -p tmp 6 | 7 | $PYTHON shadowsocks/local.py -c tests/aes.json -v & 8 | LOCAL=$! 9 | 10 | $PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & 11 | SERVER=$! 12 | 13 | sleep 3 14 | 15 | python tests/test_udp_src.py 16 | r=$? 17 | 18 | kill -s SIGINT $LOCAL 19 | kill -s SIGINT $SERVER 20 | 21 | sleep 2 22 | 23 | exit $r 24 | -------------------------------------------------------------------------------- /tests/workers.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"workers_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "workers": 4 10 | } 11 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | Useful Tools 2 | =========== 3 | 4 | autoban.py 5 | ---------- 6 | 7 | Automatically ban IPs that try to brute force crack the server. 8 | 9 | See https://github.com/shadowsocks/shadowsocks/wiki/Ban-Brute-Force-Crackers 10 | -------------------------------------------------------------------------------- /utils/autoban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2015 clowwindy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | from __future__ import absolute_import, division, print_function, \ 25 | with_statement 26 | 27 | import os 28 | import sys 29 | import argparse 30 | 31 | if __name__ == '__main__': 32 | parser = argparse.ArgumentParser(description='See README') 33 | parser.add_argument('-c', '--count', default=3, type=int, 34 | help='with how many failure times it should be ' 35 | 'considered as an attack') 36 | config = parser.parse_args() 37 | ips = {} 38 | banned = set() 39 | for line in sys.stdin: 40 | if 'can not parse header when' in line: 41 | ip = line.split()[-1].split(':')[0] 42 | if ip not in ips: 43 | ips[ip] = 1 44 | print(ip) 45 | sys.stdout.flush() 46 | else: 47 | ips[ip] += 1 48 | if ip not in banned and ips[ip] >= config.count: 49 | banned.add(ip) 50 | cmd = 'iptables -A INPUT -s %s -j DROP' % ip 51 | print(cmd, file=sys.stderr) 52 | sys.stderr.flush() 53 | os.system(cmd) 54 | -------------------------------------------------------------------------------- /utils/fail2ban/shadowsocks.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | 3 | _daemon = shadowsocks 4 | 5 | failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$ 6 | --------------------------------------------------------------------------------