diff --git a/.gitignore b/.gitignore index fb96264..6c1b61e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ pip-log.txt # Unit test / coverage reports htmlcov -.coverage +.coverage* .tox #Translations diff --git a/.travis.yml b/.travis.yml index 7a222ea..535bc9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,11 @@ before_install: - sudo apt-get update -qq - sudo apt-get install -qq build-essential dnsutils iproute nginx bc - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 + - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" - sudo service nginx restart - - pip install pep8 pyflakes nose coverage + - pip install pep8 pyflakes nose coverage PySocks - sudo tests/socksify/install.sh - sudo tests/libsodium/install.sh - sudo tests/setup_tc.sh script: - - ./.jenkins.sh + - tests/jenkins.sh diff --git a/CHANGES b/CHANGES index 27c8562..d6fe932 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,41 @@ +2.8.2 2015-08-10 +- Fix a encoding problem in manager + +2.8.1 2015-08-06 +- Respond ok to add and remove commands + +2.8 2015-08-06 +- Add Shadowsocks manager + +2.7 2015-08-02 +- Optimize speed for multiple ports + +2.6.11 2015-07-10 +- Fix a compatibility issue in UDP Relay + +2.6.10 2015-06-08 +- Optimize LRU cache +- Refine logging + +2.6.9 2015-05-19 +- Fix a stability issue on Windows + +2.6.8 2015-02-10 +- Support multiple server ip on client side +- Support --version +- Minor fixes + +2.6.7 2015-02-02 +- Support --user +- Support CIDR format in --forbidden-ip +- Minor fixes + +2.6.6 2015-01-23 +- Fix a crash in forbidden list + +2.6.5 2015-01-18 +- Try both 32 bit and 64 bit dll on Windows + 2.6.4 2015-01-14 - Also search lib* when searching libraries diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04aaa02..5d94e0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,8 @@ How to Contribute ================= +Notice this is the repository for Shadowsocks Python version. If you have problems with Android / iOS / Windows etc clients, please post your questions in their issue trackers. + Pull Requests ------------- @@ -21,6 +23,8 @@ a pull request, or ask some of your friends to do so. 3. We don't answer questions of any other types here. Since very few people are watching the issue tracker here, you'll probably get no help from here. Read [Troubleshooting] and get help from forums or [mailing lists]. +4. Issues in languages other than English will be Google translated into English +later. [Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting diff --git a/LICENSE b/LICENSE index 76886fa..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -Shadowsocks -Copyright (c) 2012-2015 clowwindy + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + 1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in index 1dc4c8e..1882dd7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -recursive-include *.py +recursive-include shadowsocks *.py include README.rst include LICENSE diff --git a/README.md b/README.md index 0e45c91..7cda7e7 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ shadowsocks A fast tunnel proxy that helps you bypass firewalls. +Features: +- TCP & UDP support +- User management API +- TCP Fast Open +- Workers and graceful restart +- Destination IP blacklist + Server ------ @@ -28,12 +35,19 @@ See [Install Server on Windows] ### Usage - ssserver -p 8000 -k password -m rc4-md5 + ssserver -p 443 -k password -m aes-256-cfb To run in the background: - ssserver -p 8000 -k password -m rc4-md5 -d start - ssserver -p 8000 -k password -m rc4-md5 -d stop + sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start + +To stop: + + sudo ssserver -d stop + +To check the log: + + sudo less /var/log/shadowsocks.log Check all the options via `-h`. You can also use a [Configuration] file instead. @@ -55,7 +69,20 @@ You can find all the documentation in the [Wiki]. License ------- -MIT + +Copyright 2015 clowwindy + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. Bugs and Issues ---------------- @@ -66,11 +93,11 @@ Bugs and Issues -[Android]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#android +[Android]: https://github.com/shadowsocks/shadowsocks-android [Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat [Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File [Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks -[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/htmlcov/index.html +[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html [Debian sid]: https://packages.debian.org/unstable/python/shadowsocks [iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help [Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open @@ -83,4 +110,4 @@ Bugs and Issues [Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks [Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting [Wiki]: https://github.com/shadowsocks/shadowsocks/wiki -[Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#windows +[Windows]: https://github.com/shadowsocks/shadowsocks-csharp diff --git a/README.rst b/README.rst index 510b233..bf2a3ec 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,6 @@ Install ~~~~~~~ Debian / Ubuntu: -^^^^^^^^^^^^^^^^ :: @@ -20,7 +19,6 @@ Debian / Ubuntu: pip install shadowsocks CentOS: -^^^^^^^ :: @@ -28,7 +26,6 @@ CentOS: pip install shadowsocks Windows: -^^^^^^^^ See `Install Server on Windows `__ @@ -38,14 +35,25 @@ Usage :: - ssserver -p 8000 -k password -m rc4-md5 + ssserver -p 443 -k password -m rc4-md5 To run in the background: :: - ssserver -p 8000 -k password -m rc4-md5 -d start - ssserver -p 8000 -k password -m rc4-md5 -d stop + sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start + +To stop: + +:: + + sudo ssserver -d stop + +To check the log: + +:: + + sudo less /var/log/shadowsocks.log Check all the options via ``-h``. You can also use a `Configuration `__ @@ -73,7 +81,21 @@ You can find all the documentation in the License ------- -MIT +Copyright 2015 clowwindy + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + +:: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. Bugs and Issues --------------- @@ -88,4 +110,4 @@ Bugs and Issues .. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat :target: https://travis-ci.org/shadowsocks/shadowsocks .. |Coverage Status| image:: https://jenkins.shadowvpn.org/result/shadowsocks - :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/htmlcov/index.html + :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html diff --git a/setup.py b/setup.py index c287c5c..e6a7ff7 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.6.4", - license='MIT', + version="2.8.2", + license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', author_email='clowwindy42@gmail.com', @@ -24,7 +24,7 @@ setup( ssserver = shadowsocks.server:main """, classifiers=[ - 'License :: OSI Approved :: MIT License', + 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', diff --git a/shadowsocks/__init__.py b/shadowsocks/__init__.py index 5ba5908..dc3abd4 100644 --- a/shadowsocks/__init__.py +++ b/shadowsocks/__init__.py @@ -1,24 +1,18 @@ #!/usr/bin/python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 6f60dc9..c5fc99d 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -1,37 +1,30 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2014-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement -import time import os import socket import struct import re import logging -from shadowsocks import common, lru_cache, eventloop +from shadowsocks import common, lru_cache, eventloop, shell CACHE_SWEEP_INTERVAL = 30 @@ -227,24 +220,10 @@ def parse_response(data): response.answers.append((an[1], an[2], an[3])) return response except Exception as e: - import traceback - traceback.print_exc() - logging.error(e) + shell.print_exception(e) return None -def is_ip(address): - for family in (socket.AF_INET, socket.AF_INET6): - try: - if type(address) != str: - address = address.decode('utf8') - socket.inet_pton(family, address) - return family - except (TypeError, ValueError, OSError, IOError): - pass - return False - - def is_valid_hostname(hostname): if len(hostname) > 255: return False @@ -276,7 +255,6 @@ class DNSResolver(object): self._hostname_to_cb = {} self._cb_to_hostname = {} self._cache = lru_cache.LRUCache(timeout=300) - self._last_time = time.time() self._sock = None self._servers = None self._parse_resolv() @@ -296,7 +274,7 @@ class DNSResolver(object): parts = line.split() if len(parts) >= 2: server = parts[1] - if is_ip(server) == socket.AF_INET: + if common.is_ip(server) == socket.AF_INET: if type(server) != str: server = server.decode('utf8') self._servers.append(server) @@ -316,7 +294,7 @@ class DNSResolver(object): parts = line.split() if len(parts) >= 2: ip = parts[0] - if is_ip(ip): + if common.is_ip(ip): for i in range(1, len(parts)): hostname = parts[i] if hostname: @@ -324,7 +302,7 @@ class DNSResolver(object): except IOError: self._hosts['localhost'] = '127.0.0.1' - def add_to_loop(self, loop, ref=False): + def add_to_loop(self, loop): if self._loop: raise Exception('already add to loop') self._loop = loop @@ -332,8 +310,8 @@ class DNSResolver(object): self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.SOL_UDP) self._sock.setblocking(False) - loop.add(self._sock, eventloop.POLL_IN) - loop.add_handler(self.handle_events, ref=ref) + loop.add(self._sock, eventloop.POLL_IN, self) + loop.add_periodic(self.handle_periodic) def _call_callback(self, hostname, ip, error=None): callbacks = self._hostname_to_cb.get(hostname, []) @@ -374,30 +352,27 @@ class DNSResolver(object): self._call_callback(hostname, None) break - def handle_events(self, events): - for sock, fd, event in events: - if sock != self._sock: - continue - if event & eventloop.POLL_ERR: - logging.error('dns socket err') - self._loop.remove(self._sock) - self._sock.close() - # TODO when dns server is IPv6 - self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, - socket.SOL_UDP) - self._sock.setblocking(False) - self._loop.add(self._sock, eventloop.POLL_IN) - else: - data, addr = sock.recvfrom(1024) - if addr[0] not in self._servers: - logging.warn('received a packet other than our dns') - break - self._handle_data(data) - break - now = time.time() - if now - self._last_time > CACHE_SWEEP_INTERVAL: - self._cache.sweep() - self._last_time = now + def handle_event(self, sock, fd, event): + if sock != self._sock: + return + if event & eventloop.POLL_ERR: + logging.error('dns socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN, self) + else: + data, addr = sock.recvfrom(1024) + if addr[0] not in self._servers: + logging.warn('received a packet other than our dns') + return + self._handle_data(data) + + def handle_periodic(self): + self._cache.sweep() def remove_callback(self, callback): hostname = self._cb_to_hostname.get(callback) @@ -423,7 +398,7 @@ class DNSResolver(object): hostname = hostname.encode('utf8') if not hostname: callback(None, Exception('empty hostname')) - elif is_ip(hostname): + elif common.is_ip(hostname): callback((hostname, hostname), None) elif hostname in self._hosts: logging.debug('hit hosts: %s', hostname) @@ -450,6 +425,9 @@ class DNSResolver(object): def close(self): if self._sock: + if self._loop: + self._loop.remove_periodic(self.handle_periodic) + self._loop.remove(self._sock) self._sock.close() self._sock = None @@ -457,7 +435,7 @@ class DNSResolver(object): def test(): dns_resolver = DNSResolver() loop = eventloop.EventLoop() - dns_resolver.add_to_loop(loop, ref=True) + dns_resolver.add_to_loop(loop) global counter counter = 0 @@ -471,8 +449,8 @@ def test(): print(result, error) counter += 1 if counter == 9: - loop.remove_handler(dns_resolver.handle_events) dns_resolver.close() + loop.stop() a_callback = callback return a_callback diff --git a/shadowsocks/common.py b/shadowsocks/common.py index e4f698c..db4beea 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2013-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -101,6 +95,18 @@ def inet_pton(family, addr): raise RuntimeError("What family?") +def is_ip(address): + for family in (socket.AF_INET, socket.AF_INET6): + try: + if type(address) != str: + address = address.decode('utf8') + inet_pton(family, address) + return family + except (TypeError, ValueError, OSError, IOError): + pass + return False + + def patch_socket(): if not hasattr(socket, 'inet_pton'): socket.inet_pton = inet_pton @@ -151,7 +157,7 @@ def parse_header(data): if len(data) >= 2 + addrlen: dest_addr = data[2:2 + addrlen] dest_port = struct.unpack('>H', data[2 + addrlen:4 + - addrlen])[0] + addrlen])[0] header_length = 4 + addrlen else: logging.warn('header is too short') @@ -165,13 +171,68 @@ def parse_header(data): else: logging.warn('header is too short') else: - logging.warn('unsupported addrtype %d, maybe wrong password' % - addrtype) + logging.warn('unsupported addrtype %d, maybe wrong password or ' + 'encryption method' % addrtype) if dest_addr is None: return None return addrtype, to_bytes(dest_addr), dest_port, header_length +class IPNetwork(object): + ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0} + + def __init__(self, addrs): + self._network_list_v4 = [] + self._network_list_v6 = [] + if type(addrs) == str: + addrs = addrs.split(',') + list(map(self.add_network, addrs)) + + def add_network(self, addr): + if addr is "": + return + block = addr.split('/') + addr_family = is_ip(block[0]) + addr_len = IPNetwork.ADDRLENGTH[addr_family] + if addr_family is socket.AF_INET: + ip, = struct.unpack("!I", socket.inet_aton(block[0])) + elif addr_family is socket.AF_INET6: + hi, lo = struct.unpack("!QQ", inet_pton(addr_family, block[0])) + ip = (hi << 64) | lo + else: + raise Exception("Not a valid CIDR notation: %s" % addr) + if len(block) is 1: + prefix_size = 0 + while (ip & 1) == 0 and ip is not 0: + ip >>= 1 + prefix_size += 1 + logging.warn("You did't specify CIDR routing prefix size for %s, " + "implicit treated as %s/%d" % (addr, addr, addr_len)) + elif block[1].isdigit() and int(block[1]) <= addr_len: + prefix_size = addr_len - int(block[1]) + ip >>= prefix_size + else: + raise Exception("Not a valid CIDR notation: %s" % addr) + if addr_family is socket.AF_INET: + self._network_list_v4.append((ip, prefix_size)) + else: + self._network_list_v6.append((ip, prefix_size)) + + def __contains__(self, addr): + addr_family = is_ip(addr) + if addr_family is socket.AF_INET: + ip, = struct.unpack("!I", socket.inet_aton(addr)) + return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], + self._network_list_v4)) + elif addr_family is socket.AF_INET6: + hi, lo = struct.unpack("!QQ", inet_pton(addr_family, addr)) + ip = (hi << 64) | lo + return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1], + self._network_list_v6)) + else: + return False + + def test_inet_conv(): ipv4 = b'8.8.4.4' b = inet_pton(socket.AF_INET, ipv4) @@ -198,7 +259,23 @@ def test_pack_header(): assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com' +def test_ip_network(): + ip_network = IPNetwork('127.0.0.0/24,::ff:1/112,::1,192.168.1.1,192.0.2.0') + assert '127.0.0.1' in ip_network + assert '127.0.1.1' not in ip_network + assert ':ff:ffff' in ip_network + assert '::ffff:1' not in ip_network + assert '::1' in ip_network + assert '::2' not in ip_network + assert '192.168.1.1' in ip_network + assert '192.168.1.2' not in ip_network + assert '192.0.2.1' in ip_network + assert '192.0.3.1' in ip_network # 192.0.2.0 is treated as 192.0.2.0/23 + assert 'www.google.com' not in ip_network + + if __name__ == '__main__': test_inet_conv() test_parse_header() test_pack_header() + test_ip_network() diff --git a/shadowsocks/crypto/__init__.py b/shadowsocks/crypto/__init__.py index 6251321..401c7b7 100644 --- a/shadowsocks/crypto/__init__.py +++ b/shadowsocks/crypto/__init__.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index bb11627..3775b6c 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -26,6 +20,7 @@ from __future__ import absolute_import, division, print_function, \ from ctypes import c_char_p, c_int, c_long, byref,\ create_string_buffer, c_void_p +from shadowsocks import common from shadowsocks.crypto import util __all__ = ['ciphers'] @@ -64,7 +59,7 @@ def load_openssl(): def load_cipher(cipher_name): - func_name = b'EVP_' + cipher_name.replace(b'-', b'_') + func_name = 'EVP_' + cipher_name.replace('-', '_') if bytes != str: func_name = str(func_name, 'utf-8') cipher = getattr(libcrypto, func_name, None) @@ -79,6 +74,7 @@ class OpenSSLCrypto(object): self._ctx = None if not loaded: load_openssl() + cipher_name = common.to_bytes(cipher_name) cipher = libcrypto.EVP_get_cipherbyname(cipher_name) if not cipher: cipher = load_cipher(cipher_name) @@ -117,31 +113,31 @@ class OpenSSLCrypto(object): ciphers = { - b'aes-128-cfb': (16, 16, OpenSSLCrypto), - b'aes-192-cfb': (24, 16, OpenSSLCrypto), - b'aes-256-cfb': (32, 16, OpenSSLCrypto), - b'aes-128-ofb': (16, 16, OpenSSLCrypto), - b'aes-192-ofb': (24, 16, OpenSSLCrypto), - b'aes-256-ofb': (32, 16, OpenSSLCrypto), - b'aes-128-ctr': (16, 16, OpenSSLCrypto), - b'aes-192-ctr': (24, 16, OpenSSLCrypto), - b'aes-256-ctr': (32, 16, OpenSSLCrypto), - b'aes-128-cfb8': (16, 16, OpenSSLCrypto), - b'aes-192-cfb8': (24, 16, OpenSSLCrypto), - b'aes-256-cfb8': (32, 16, OpenSSLCrypto), - b'aes-128-cfb1': (16, 16, OpenSSLCrypto), - b'aes-192-cfb1': (24, 16, OpenSSLCrypto), - b'aes-256-cfb1': (32, 16, OpenSSLCrypto), - b'bf-cfb': (16, 8, OpenSSLCrypto), - b'camellia-128-cfb': (16, 16, OpenSSLCrypto), - b'camellia-192-cfb': (24, 16, OpenSSLCrypto), - b'camellia-256-cfb': (32, 16, OpenSSLCrypto), - b'cast5-cfb': (16, 8, OpenSSLCrypto), - b'des-cfb': (8, 8, OpenSSLCrypto), - b'idea-cfb': (16, 8, OpenSSLCrypto), - b'rc2-cfb': (16, 8, OpenSSLCrypto), - b'rc4': (16, 0, OpenSSLCrypto), - b'seed-cfb': (16, 16, OpenSSLCrypto), + 'aes-128-cfb': (16, 16, OpenSSLCrypto), + 'aes-192-cfb': (24, 16, OpenSSLCrypto), + 'aes-256-cfb': (32, 16, OpenSSLCrypto), + 'aes-128-ofb': (16, 16, OpenSSLCrypto), + 'aes-192-ofb': (24, 16, OpenSSLCrypto), + 'aes-256-ofb': (32, 16, OpenSSLCrypto), + 'aes-128-ctr': (16, 16, OpenSSLCrypto), + 'aes-192-ctr': (24, 16, OpenSSLCrypto), + 'aes-256-ctr': (32, 16, OpenSSLCrypto), + 'aes-128-cfb8': (16, 16, OpenSSLCrypto), + 'aes-192-cfb8': (24, 16, OpenSSLCrypto), + 'aes-256-cfb8': (32, 16, OpenSSLCrypto), + 'aes-128-cfb1': (16, 16, OpenSSLCrypto), + 'aes-192-cfb1': (24, 16, OpenSSLCrypto), + 'aes-256-cfb1': (32, 16, OpenSSLCrypto), + 'bf-cfb': (16, 8, OpenSSLCrypto), + 'camellia-128-cfb': (16, 16, OpenSSLCrypto), + 'camellia-192-cfb': (24, 16, OpenSSLCrypto), + 'camellia-256-cfb': (32, 16, OpenSSLCrypto), + 'cast5-cfb': (16, 8, OpenSSLCrypto), + 'des-cfb': (8, 8, OpenSSLCrypto), + 'idea-cfb': (16, 8, OpenSSLCrypto), + 'rc2-cfb': (16, 8, OpenSSLCrypto), + 'rc4': (16, 0, OpenSSLCrypto), + 'seed-cfb': (16, 16, OpenSSLCrypto), } @@ -154,31 +150,31 @@ def run_method(method): def test_aes_128_cfb(): - run_method(b'aes-128-cfb') + run_method('aes-128-cfb') def test_aes_256_cfb(): - run_method(b'aes-256-cfb') + run_method('aes-256-cfb') def test_aes_128_cfb8(): - run_method(b'aes-128-cfb8') + run_method('aes-128-cfb8') def test_aes_256_ofb(): - run_method(b'aes-256-ofb') + run_method('aes-256-ofb') def test_aes_256_ctr(): - run_method(b'aes-256-ctr') + run_method('aes-256-ctr') def test_bf_cfb(): - run_method(b'bf-cfb') + run_method('bf-cfb') def test_rc4(): - run_method(b'rc4') + run_method('rc4') if __name__ == '__main__': diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 33d481d..1f07a82 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -40,15 +34,15 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, ciphers = { - b'rc4-md5': (16, 16, create_cipher), + 'rc4-md5': (16, 16, create_cipher), } def test(): from shadowsocks.crypto import util - cipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 1) - decipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 0) + cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1) + decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index 74fbb33..ae86fef 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -70,9 +64,9 @@ class SodiumCrypto(object): self.iv = iv self.key_ptr = c_char_p(key) self.iv_ptr = c_char_p(iv) - if cipher_name == b'salsa20': + if cipher_name == 'salsa20': self.cipher = libsodium.crypto_stream_salsa20_xor_ic - elif cipher_name == b'chacha20': + elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic else: raise Exception('Unknown cipher') @@ -101,22 +95,22 @@ class SodiumCrypto(object): ciphers = { - b'salsa20': (32, 8, SodiumCrypto), - b'chacha20': (32, 8, SodiumCrypto), + 'salsa20': (32, 8, SodiumCrypto), + 'chacha20': (32, 8, SodiumCrypto), } def test_salsa20(): - cipher = SodiumCrypto(b'salsa20', b'k' * 32, b'i' * 16, 1) - decipher = SodiumCrypto(b'salsa20', b'k' * 32, b'i' * 16, 0) + cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) def test_chacha20(): - cipher = SodiumCrypto(b'chacha20', b'k' * 32, b'i' * 16, 1) - decipher = SodiumCrypto(b'chacha20', b'k' * 32, b'i' * 16, 0) + cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index 08c1205..bc693f5 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -1,24 +1,18 @@ # !/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -73,7 +67,7 @@ class TableCipher(object): ciphers = { - b'table': (0, 0, TableCipher) + 'table': (0, 0, TableCipher) } @@ -169,8 +163,8 @@ def test_table_result(): def test_encryption(): from shadowsocks.crypto import util - cipher = TableCipher(b'table', b'test', b'', 1) - decipher = TableCipher(b'table', b'test', b'', 0) + cipher = TableCipher('table', b'test', b'', 1) + decipher = TableCipher('table', b'test', b'', 0) util.run_cipher(cipher, decipher) diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index 1242f8e..e579455 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement diff --git a/shadowsocks/daemon.py b/shadowsocks/daemon.py index d206ccf..8dc5608 100644 --- a/shadowsocks/daemon.py +++ b/shadowsocks/daemon.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2014-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -29,7 +23,7 @@ import sys import logging import signal import time -from shadowsocks import common +from shadowsocks import common, shell # this module is ported from ShadowVPN daemon.c @@ -43,9 +37,6 @@ def daemon_exec(config): command = 'start' pid_file = config['pid-file'] log_file = config['log-file'] - command = common.to_str(command) - pid_file = common.to_str(pid_file) - log_file = common.to_str(log_file) if command == 'start': daemon_start(pid_file, log_file) elif command == 'stop': @@ -67,7 +58,7 @@ def write_pid_file(pid_file, pid): fd = os.open(pid_file, os.O_RDWR | os.O_CREAT, stat.S_IRUSR | stat.S_IWUSR) except OSError as e: - logging.error(e) + shell.print_exception(e) return -1 flags = fcntl.fcntl(fd, fcntl.F_GETFD) assert flags != -1 @@ -136,7 +127,7 @@ def daemon_start(pid_file, log_file): freopen(log_file, 'a', sys.stdout) freopen(log_file, 'a', sys.stderr) except IOError as e: - logging.error(e) + shell.print_exception(e) sys.exit(1) @@ -149,7 +140,7 @@ def daemon_stop(pid_file): if not buf: logging.error('not running') except IOError as e: - logging.error(e) + shell.print_exception(e) if e.errno == errno.ENOENT: # always exit 0 if we are sure daemon is not running logging.error('not running') @@ -164,7 +155,7 @@ def daemon_stop(pid_file): logging.error('not running') # always exit 0 if we are sure daemon is not running return - logging.error(e) + shell.print_exception(e) sys.exit(1) else: logging.error('pid is not positive: %d', pid) @@ -183,3 +174,35 @@ def daemon_stop(pid_file): sys.exit(1) print('stopped') os.unlink(pid_file) + + +def set_user(username): + if username is None: + return + + import pwd + import grp + + try: + pwrec = pwd.getpwnam(username) + except KeyError: + logging.error('user not found: %s' % username) + raise + user = pwrec[0] + uid = pwrec[2] + gid = pwrec[3] + + cur_uid = os.getuid() + if uid == cur_uid: + return + if cur_uid != 0: + logging.error('can not set user as nonroot user') + # will raise later + + # inspired by supervisor + if hasattr(os, 'setgroups'): + groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] + groups.insert(0, gid) + os.setgroups(groups) + os.setgid(gid) + os.setuid(uid) diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 3dd9264..4e87f41 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -1,24 +1,18 @@ #!/usr/bin/env python - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -28,6 +22,7 @@ import sys import hashlib import logging +from shadowsocks import common from shadowsocks.crypto import rc4_md5, openssl, sodium, table @@ -52,8 +47,6 @@ def try_cipher(key, method=None): def EVP_BytesToKey(password, key_len, iv_len): # equivalent to OpenSSL's EVP_BytesToKey() with count 1 # so that we make the same key and iv as nodejs version - if hasattr(password, 'encode'): - password = password.encode('utf-8') cached_key = '%s-%d-%d' % (password, key_len, iv_len) r = cached_keys.get(cached_key, None) if r: @@ -101,8 +94,7 @@ class Encryptor(object): return len(self.cipher_iv) def get_cipher(self, password, method, op, iv): - if hasattr(password, 'encode'): - password = password.encode('utf-8') + password = common.to_bytes(password) m = self._method_info if m[0] > 0: key, iv_ = EVP_BytesToKey(password, m[0], m[1]) @@ -159,12 +151,12 @@ def encrypt_all(password, method, op, data): CIPHERS_TO_TEST = [ - b'aes-128-cfb', - b'aes-256-cfb', - b'rc4-md5', - b'salsa20', - b'chacha20', - b'table', + 'aes-128-cfb', + 'aes-256-cfb', + 'rc4-md5', + 'salsa20', + 'chacha20', + 'table', ] diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 304b229..78b532c 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2013-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. # from ssloop # https://github.com/clowwindy/ssloop @@ -28,12 +22,15 @@ from __future__ import absolute_import, division, print_function, \ with_statement import os +import time import socket import select import errno import logging from collections import defaultdict +from shadowsocks import shell + __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] @@ -55,23 +52,8 @@ EVENT_NAMES = { POLL_NVAL: 'POLL_NVAL', } - -class EpollLoop(object): - - def __init__(self): - self._epoll = select.epoll() - - def poll(self, timeout): - return self._epoll.poll(timeout) - - def add_fd(self, fd, mode): - self._epoll.register(fd, mode) - - def remove_fd(self, fd): - self._epoll.unregister(fd) - - def modify_fd(self, fd, mode): - self._epoll.modify(fd, mode) +# we check timeouts every TIMEOUT_PRECISION seconds +TIMEOUT_PRECISION = 10 class KqueueLoop(object): @@ -104,17 +86,20 @@ class KqueueLoop(object): results[fd] |= POLL_OUT return results.items() - def add_fd(self, fd, mode): + def register(self, fd, mode): self._fds[fd] = mode self._control(fd, mode, select.KQ_EV_ADD) - def remove_fd(self, fd): + def unregister(self, fd): self._control(fd, self._fds[fd], select.KQ_EV_DELETE) del self._fds[fd] - def modify_fd(self, fd, mode): - self.remove_fd(fd) - self.add_fd(fd, mode) + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + self._kqueue.close() class SelectLoop(object): @@ -133,7 +118,7 @@ class SelectLoop(object): results[fd] |= p[1] return results.items() - def add_fd(self, fd, mode): + def register(self, fd, mode): if mode & POLL_IN: self._r_list.add(fd) if mode & POLL_OUT: @@ -141,7 +126,7 @@ class SelectLoop(object): if mode & POLL_ERR: self._x_list.add(fd) - def remove_fd(self, fd): + def unregister(self, fd): if fd in self._r_list: self._r_list.remove(fd) if fd in self._w_list: @@ -149,16 +134,18 @@ class SelectLoop(object): if fd in self._x_list: self._x_list.remove(fd) - def modify_fd(self, fd, mode): - self.remove_fd(fd) - self.add_fd(fd, mode) + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + pass class EventLoop(object): def __init__(self): - self._iterating = False if hasattr(select, 'epoll'): - self._impl = EpollLoop() + self._impl = select.epoll() model = 'epoll' elif hasattr(select, 'kqueue'): self._impl = KqueueLoop() @@ -169,74 +156,74 @@ class EventLoop(object): else: raise Exception('can not find any available functions in select ' 'package') - self._fd_to_f = {} - self._handlers = [] - self._ref_handlers = [] - self._handlers_to_remove = [] + self._fdmap = {} # (f, handler) + self._last_time = time.time() + self._periodic_callbacks = [] + self._stopping = False logging.debug('using event model: %s', model) def poll(self, timeout=None): events = self._impl.poll(timeout) - return [(self._fd_to_f[fd], fd, event) for fd, event in events] + return [(self._fdmap[fd][0], fd, event) for fd, event in events] - def add(self, f, mode): + def add(self, f, mode, handler): fd = f.fileno() - self._fd_to_f[fd] = f - self._impl.add_fd(fd, mode) + self._fdmap[fd] = (f, handler) + self._impl.register(fd, mode) def remove(self, f): fd = f.fileno() - del self._fd_to_f[fd] - self._impl.remove_fd(fd) + del self._fdmap[fd] + self._impl.unregister(fd) + + def add_periodic(self, callback): + self._periodic_callbacks.append(callback) + + def remove_periodic(self, callback): + self._periodic_callbacks.remove(callback) def modify(self, f, mode): fd = f.fileno() - self._impl.modify_fd(fd, mode) + self._impl.modify(fd, mode) - def add_handler(self, handler, ref=True): - self._handlers.append(handler) - if ref: - # when all ref handlers are removed, loop stops - self._ref_handlers.append(handler) - - def remove_handler(self, handler): - if handler in self._ref_handlers: - self._ref_handlers.remove(handler) - if self._iterating: - self._handlers_to_remove.append(handler) - else: - self._handlers.remove(handler) + def stop(self): + self._stopping = True def run(self): events = [] - while self._ref_handlers: + while not self._stopping: + asap = False try: - events = self.poll(1) + events = self.poll(TIMEOUT_PRECISION) except (OSError, IOError) as e: if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): # EPIPE: Happens when the client closes the connection # EINTR: Happens when received a signal # handles them as soon as possible + asap = True logging.debug('poll:%s', e) else: logging.error('poll:%s', e) import traceback traceback.print_exc() continue - self._iterating = True - for handler in self._handlers: - # TODO when there are a lot of handlers - try: - handler(events) - except (OSError, IOError) as e: - logging.error(e) - import traceback - traceback.print_exc() - if self._handlers_to_remove: - for handler in self._handlers_to_remove: - self._handlers.remove(handler) - self._handlers_to_remove = [] - self._iterating = False + + for sock, fd, event in events: + handler = self._fdmap.get(fd, None) + if handler is not None: + handler = handler[1] + try: + handler.handle_event(sock, fd, event) + except (OSError, IOError) as e: + shell.print_exception(e) + now = time.time() + if asap or now - self._last_time >= TIMEOUT_PRECISION: + for callback in self._periodic_callbacks: + callback() + self._last_time = now + + def __del__(self): + self._impl.close() # from tornado diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 994b6d8..4255a2e 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -1,25 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2012-2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -30,12 +24,11 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ - asyncdns +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns def main(): - utils.check_python() + shell.check_python() # fix py2exe if hasattr(sys, "frozen") and sys.frozen in \ @@ -43,14 +36,10 @@ def main(): p = os.path.dirname(os.path.abspath(sys.executable)) os.chdir(p) - config = utils.get_config(True) + config = shell.get_config(True) daemon.daemon_exec(config) - utils.print_shadowsocks() - - encrypt.try_cipher(config['password'], config['method']) - try: logging.info("starting local at %s:%d" % (config['local_address'], config['local_port'])) @@ -73,13 +62,11 @@ def main(): sys.exit(1) signal.signal(signal.SIGINT, int_handler) + daemon.set_user(config.get('user', None)) loop.run() - except (KeyboardInterrupt, IOError, OSError) as e: - logging.error(e) - if config['verbose']: - import traceback - traceback.print_exc() - os._exit(1) + except Exception as e: + shell.print_exception(e) + sys.exit(1) if __name__ == '__main__': main() diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index 4523399..401f19b 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -47,6 +41,7 @@ class LRUCache(collections.MutableMapping): self._time_to_keys = collections.defaultdict(list) self._keys_to_last_time = {} self._last_visits = collections.deque() + self._closed_values = set() self.update(dict(*args, **kwargs)) # use the free update to set keys def __getitem__(self, key): @@ -89,7 +84,9 @@ class LRUCache(collections.MutableMapping): if key in self._store: if now - self._keys_to_last_time[key] > self.timeout: value = self._store[key] - self.close_callback(value) + if value not in self._closed_values: + self.close_callback(value) + self._closed_values.add(value) for key in self._time_to_keys[least]: self._last_visits.popleft() if key in self._store: @@ -99,6 +96,7 @@ class LRUCache(collections.MutableMapping): c += 1 del self._time_to_keys[least] if c: + self._closed_values.clear() logging.debug('%d keys swept' % c) @@ -132,5 +130,21 @@ def test(): assert 'a' not in c assert 'b' not in c + global close_cb_called + close_cb_called = False + + def close_cb(t): + global close_cb_called + assert not close_cb_called + close_cb_called = True + + c = LRUCache(timeout=0.1, close_callback=close_cb) + c['s'] = 1 + c['s'] + time.sleep(0.1) + c['s'] + time.sleep(0.3) + c.sweep() + if __name__ == '__main__': test() diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py new file mode 100644 index 0000000..e8009b4 --- /dev/null +++ b/shadowsocks/manager.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import errno +import traceback +import socket +import logging +import json +import collections + +from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell + + +BUF_SIZE = 1506 +STAT_SEND_LIMIT = 100 + + +class Manager(object): + + def __init__(self, config): + self._config = config + self._relays = {} # (tcprelay, udprelay) + self._loop = eventloop.EventLoop() + self._dns_resolver = asyncdns.DNSResolver() + self._dns_resolver.add_to_loop(self._loop) + + self._statistics = collections.defaultdict(int) + self._control_client_addr = None + try: + manager_address = config['manager_address'] + if ':' in manager_address: + addr = manager_address.rsplit(':', 1) + addr = addr[0], int(addr[1]) + addrs = socket.getaddrinfo(addr[0], addr[1]) + if addrs: + family = addrs[0][0] + else: + logging.error('invalid address: %s', manager_address) + exit(1) + else: + addr = manager_address + family = socket.AF_UNIX + self._control_socket = socket.socket(family, + socket.SOCK_DGRAM) + self._control_socket.bind(addr) + self._control_socket.setblocking(False) + except (OSError, IOError) as e: + logging.error(e) + logging.error('can not bind to manager address') + exit(1) + self._loop.add(self._control_socket, + eventloop.POLL_IN, self) + self._loop.add_periodic(self.handle_periodic) + + port_password = config['port_password'] + del config['port_password'] + for port, password in port_password.items(): + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password + self.add_port(a_config) + + def add_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.error("server already exists at %s:%d" % (config['server'], + port)) + return + logging.info("adding server at %s:%d" % (config['server'], port)) + t = tcprelay.TCPRelay(config, self._dns_resolver, False, + self.stat_callback) + u = udprelay.UDPRelay(config, self._dns_resolver, False, + self.stat_callback) + t.add_to_loop(self._loop) + u.add_to_loop(self._loop) + self._relays[port] = (t, u) + + def remove_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.info("removing server at %s:%d" % (config['server'], port)) + t, u = servers + t.close(next_tick=False) + u.close(next_tick=False) + del self._relays[port] + else: + logging.error("server not exist at %s:%d" % (config['server'], + port)) + + def handle_event(self, sock, fd, event): + if sock == self._control_socket and event == eventloop.POLL_IN: + data, self._control_client_addr = sock.recvfrom(BUF_SIZE) + parsed = self._parse_command(data) + if parsed: + command, config = parsed + a_config = self._config.copy() + if config: + # let the command override the configuration file + a_config.update(config) + if 'server_port' not in a_config: + logging.error('can not find server_port in config') + else: + if command == 'add': + self.add_port(a_config) + self._send_control_data(b'ok') + elif command == 'remove': + self.remove_port(a_config) + self._send_control_data(b'ok') + elif command == 'ping': + self._send_control_data(b'pong') + else: + logging.error('unknown command %s', command) + + def _parse_command(self, data): + # commands: + # add: {"server_port": 8000, "password": "foobar"} + # remove: {"server_port": 8000"} + data = common.to_str(data) + parts = data.split(':', 1) + if len(parts) < 2: + return data, None + command, config_json = parts + try: + config = shell.parse_json_in_str(config_json) + return command, config + except Exception as e: + logging.error(e) + return None + + def stat_callback(self, port, data_len): + self._statistics[port] += data_len + + def handle_periodic(self): + r = {} + i = 0 + + def send_data(data_dict): + if data_dict: + # use compact JSON format (without space) + data = common.to_bytes(json.dumps(data_dict, + separators=(',', ':'))) + self._send_control_data(b'stat: ' + data) + + for k, v in self._statistics.items(): + r[k] = v + i += 1 + # split the data into segments that fit in UDP packets + if i >= STAT_SEND_LIMIT: + send_data(r) + r.clear() + send_data(r) + self._statistics.clear() + + def _send_control_data(self, data): + if self._control_client_addr: + try: + self._control_socket.sendto(data, self._control_client_addr) + except (socket.error, OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + + def run(self): + self._loop.run() + + +def run(config): + Manager(config).run() + + +def test(): + import time + import threading + import struct + from shadowsocks import encrypt + + logging.basicConfig(level=5, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + enc = [] + eventloop.TIMEOUT_PRECISION = 1 + + def run_server(): + config = { + 'server': '127.0.0.1', + 'local_port': 1081, + 'port_password': { + '8381': 'foobar1', + '8382': 'foobar2' + }, + 'method': 'aes-256-cfb', + 'manager_address': '127.0.0.1:6001', + 'timeout': 60, + 'fast_open': False, + 'verbose': 2 + } + manager = Manager(config) + enc.append(manager) + manager.run() + + t = threading.Thread(target=run_server) + t.start() + time.sleep(1) + manager = enc[0] + cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cli.connect(('127.0.0.1', 6001)) + + # test add and remove + time.sleep(1) + cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') + time.sleep(1) + assert 7001 in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + + cli.send(b'remove: {"server_port":8381}') + time.sleep(1) + assert 8381 not in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + logging.info('add and remove test passed') + + # test statistics for TCP + header = common.pack_addr(b'google.com') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, + header + b'GET /\r\n\r\n') + tcp_cli = socket.socket() + tcp_cli.connect(('127.0.0.1', 7001)) + tcp_cli.send(data) + tcp_cli.recv(4096) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = shell.parse_json_in_str(data) + assert '7001' in stats + logging.info('TCP statistics test passed') + + # test statistics for UDP + header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, + header + b'test') + udp_cli = socket.socket(type=socket.SOCK_DGRAM) + udp_cli.sendto(data, ('127.0.0.1', 8382)) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = json.loads(data) + assert '8382' in stats + logging.info('UDP statistics test passed') + + manager._loop.stop() + t.join() + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 8eed4ad..d4cf961 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -1,25 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -30,19 +24,17 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ - asyncdns +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ + asyncdns, manager def main(): - utils.check_python() + shell.check_python() - config = utils.get_config(False) + config = shell.get_config(False) daemon.daemon_exec(config) - utils.print_shadowsocks() - if config['port_password']: if config['password']: logging.warn('warning: port_password should not be used with ' @@ -50,18 +42,25 @@ def main(): 'will be ignored') else: config['port_password'] = {} - server_port = config['server_port'] - if type(server_port) == list: - for a_server_port in server_port: - config['port_password'][a_server_port] = config['password'] - else: - config['port_password'][str(server_port)] = config['password'] + server_port = config.get('server_port', None) + if server_port: + if type(server_port) == list: + for a_server_port in server_port: + config['port_password'][a_server_port] = config['password'] + else: + config['port_password'][str(server_port)] = config['password'] + + if config.get('manager_address', 0): + logging.info('entering manager mode') + manager.run(config) + return - encrypt.try_cipher(config['password'], config['method']) tcp_servers = [] udp_servers = [] dns_resolver = asyncdns.DNSResolver() - for port, password in config['port_password'].items(): + port_password = config['port_password'] + del config['port_password'] + for port, password in port_password.items(): a_config = config.copy() a_config['server_port'] = int(port) a_config['password'] = password @@ -86,13 +85,12 @@ def main(): loop = eventloop.EventLoop() dns_resolver.add_to_loop(loop) list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) + + daemon.set_user(config.get('user', None)) loop.run() - except (KeyboardInterrupt, IOError, OSError) as e: - logging.error(e) - if config['verbose']: - import traceback - traceback.print_exc() - os._exit(1) + except Exception as e: + shell.print_exception(e) + sys.exit(1) if int(config['workers']) > 1: if os.name == 'posix': diff --git a/shadowsocks/utils.py b/shadowsocks/shell.py similarity index 71% rename from shadowsocks/utils.py rename to shadowsocks/shell.py index a51c965..a78e18f 100644 --- a/shadowsocks/utils.py +++ b/shadowsocks/shell.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -29,11 +23,14 @@ import json import sys import getopt import logging -from shadowsocks.common import to_bytes, to_str +from shadowsocks.common import to_bytes, to_str, IPNetwork +from shadowsocks import encrypt VERBOSE_LEVEL = 5 +verbose = 0 + def check_python(): info = sys.version_info @@ -48,6 +45,14 @@ def check_python(): sys.exit(1) +def print_exception(e): + global verbose + logging.error(e) + if verbose > 0: + import traceback + traceback.print_exc() + + def print_shadowsocks(): version = '' try: @@ -55,7 +60,7 @@ def print_shadowsocks(): version = pkg_resources.get_distribution('shadowsocks').version except Exception: pass - print('shadowsocks %s' % version) + print('Shadowsocks %s' % version) def find_config(): @@ -68,16 +73,38 @@ def find_config(): return None -def check_config(config): +def check_config(config, is_local): + if config.get('daemon', None) == 'stop': + # no need to specify configuration for daemon stop + return + + if is_local and not config.get('password', None): + logging.error('password not specified') + print_help(is_local) + sys.exit(2) + + if not is_local and not config.get('password', None) \ + and not config.get('port_password', None) \ + and not config.get('manager_address'): + logging.error('password or port_password not specified') + print_help(is_local) + sys.exit(2) + + if 'local_port' in config: + config['local_port'] = int(config['local_port']) + + if config.get('server_port', None) and type(config['server_port']) != list: + config['server_port'] = int(config['server_port']) + if config.get('local_address', '') in [b'0.0.0.0']: logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') - if config.get('server', '') in [b'127.0.0.1', b'localhost']: + if config.get('server', '') in ['127.0.0.1', 'localhost']: logging.warn('warning: server set to listen on %s:%s, are you sure?' % (to_str(config['server']), config['server_port'])) - if (config.get('method', '') or '').lower() == b'table': + if (config.get('method', '') or '').lower() == 'table': logging.warn('warning: table is not safe; please use a safer cipher, ' 'like AES-256-CFB') - if (config.get('method', '') or '').lower() == b'rc4': + if (config.get('method', '') or '').lower() == 'rc4': logging.warn('warning: RC4 is not safe; please use a safer cipher, ' 'like AES-256-CFB') if config.get('timeout', 300) < 100: @@ -89,19 +116,28 @@ def check_config(config): if config.get('password') in [b'mypassword']: logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' 'config.json!') - exit(1) + sys.exit(1) + if config.get('user', None) is not None: + if os.name != 'posix': + logging.error('user can be used only on Unix') + sys.exit(1) + + encrypt.try_cipher(config['password'], config['method']) def get_config(is_local): + global verbose + logging.basicConfig(level=logging.INFO, format='%(levelname)-s: %(message)s') if is_local: shortopts = 'hd:s:b:p:k:l:m:c:t:vq' - longopts = ['help', 'fast-open', 'pid-file=', 'log-file='] + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', + 'version'] else: shortopts = 'hd:s:p:k:m:c:t:vq' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', - 'forbidden-ip='] + 'forbidden-ip=', 'user=', 'manager-address=', 'version'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -113,8 +149,7 @@ def get_config(is_local): logging.info('loading config from %s' % config_path) with open(config_path, 'rb') as f: try: - config = json.loads(f.read().decode('utf8'), - object_hook=_decode_dict) + config = parse_json_in_str(f.read().decode('utf8')) except ValueError as e: logging.error('found an error in config.json: %s', e.message) @@ -122,7 +157,6 @@ def get_config(is_local): else: config = {} - optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) v_count = 0 for key, value in optlist: if key == '-p': @@ -132,11 +166,11 @@ def get_config(is_local): elif key == '-l': config['local_port'] = int(value) elif key == '-s': - config['server'] = to_bytes(value) + config['server'] = to_str(value) elif key == '-m': - config['method'] = to_bytes(value) + config['method'] = to_str(value) elif key == '-b': - config['local_address'] = to_bytes(value) + config['local_address'] = to_str(value) elif key == '-v': v_count += 1 # '-vv' turns on more verbose mode @@ -147,6 +181,10 @@ def get_config(is_local): config['fast_open'] = True elif key == '--workers': config['workers'] = int(value) + elif key == '--manager-address': + config['manager_address'] = value + elif key == '--user': + config['user'] = to_str(value) elif key == '--forbidden-ip': config['forbidden_ip'] = to_str(value).split(',') elif key in ('-h', '--help'): @@ -155,12 +193,15 @@ def get_config(is_local): else: print_server_help() sys.exit(0) + elif key == '--version': + print_shadowsocks() + sys.exit(0) elif key == '-d': - config['daemon'] = value + config['daemon'] = to_str(value) elif key == '--pid-file': - config['pid-file'] = value + config['pid-file'] = to_str(value) elif key == '--log-file': - config['log-file'] = value + config['log-file'] = to_str(value) elif key == '-q': v_count -= 1 config['verbose'] = v_count @@ -174,43 +215,33 @@ def get_config(is_local): print_help(is_local) sys.exit(2) - config['password'] = config.get('password', '') - config['method'] = config.get('method', b'aes-256-cfb') + config['password'] = to_bytes(config.get('password', b'')) + config['method'] = to_str(config.get('method', 'aes-256-cfb')) config['port_password'] = config.get('port_password', None) config['timeout'] = int(config.get('timeout', 300)) config['fast_open'] = config.get('fast_open', False) config['workers'] = config.get('workers', 1) config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid') config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log') - config['workers'] = config.get('workers', 1) config['verbose'] = config.get('verbose', False) - config['local_address'] = config.get('local_address', '127.0.0.1') + config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) if is_local: if config.get('server', None) is None: logging.error('server addr not specified') print_local_help() sys.exit(2) + else: + config['server'] = to_str(config['server']) else: - config['server'] = config.get('server', '0.0.0.0') - config['server_port'] = config.get('server_port', 8388) - - if is_local and not config.get('password', None): - logging.error('password not specified') - print_help(is_local) - sys.exit(2) - - if not is_local and not config.get('password', None) \ - and not config.get('port_password', None): - logging.error('password or port_password not specified') - print_help(is_local) - sys.exit(2) - - if 'local_port' in config: - config['local_port'] = int(config['local_port']) - - if 'server_port' in config and type(config['server_port']) != list: - config['server_port'] = int(config['server_port']) + config['server'] = to_str(config.get('server', '0.0.0.0')) + try: + config['forbidden_ip'] = \ + IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128')) + except Exception as e: + logging.error(e) + sys.exit(2) + config['server_port'] = config.get('server_port', None) logging.getLogger('').handlers = [] logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') @@ -224,11 +255,12 @@ def get_config(is_local): level = logging.ERROR else: level = logging.INFO + verbose = config['verbose'] logging.basicConfig(level=level, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - check_config(config) + check_config(config, is_local) return config @@ -241,15 +273,12 @@ def print_help(is_local): def print_local_help(): - print('''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT] - [-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD] - [-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] -[d] [-q] + print('''usage: sslocal [OPTION]... A fast tunnel proxy that helps you bypass firewalls. You can supply configurations via either config file or command line arguments. Proxy options: - -h, --help show this help message and exit -c CONFIG path to config file -s SERVER_ADDR server address -p SERVER_PORT server port, default: 8388 @@ -261,26 +290,26 @@ Proxy options: --fast-open use TCP_FASTOPEN, requires Linux 3.7+ General options: + -h, --help show this help message and exit -d start/stop/restart daemon mode --pid-file PID_FILE pid file for daemon mode --log-file LOG_FILE log file for daemon mode + --user USER username to run as -v, -vv verbose mode -q, -qq quiet mode, only show warnings/errors + --version show version information Online help: ''') def print_server_help(): - print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD - -m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open] - [--workers WORKERS] [-v] [-d start] [-q] + print('''usage: ssserver [OPTION]... A fast tunnel proxy that helps you bypass firewalls. You can supply configurations via either config file or command line arguments. Proxy options: - -h, --help show this help message and exit -c CONFIG path to config file -s SERVER_ADDR server address, default: 0.0.0.0 -p SERVER_PORT server port, default: 8388 @@ -290,13 +319,17 @@ Proxy options: --fast-open use TCP_FASTOPEN, requires Linux 3.7+ --workers WORKERS number of workers, available on Unix/Linux --forbidden-ip IPLIST comma seperated IP list forbidden to connect + --manager-address ADDR optional server manager UDP address, see wiki General options: + -h, --help show this help message and exit -d start/stop/restart daemon mode --pid-file PID_FILE pid file for daemon mode --log-file LOG_FILE log file for daemon mode + --user USER username to run as -v, -vv verbose mode -q, -qq quiet mode, only show warnings/errors + --version show version information Online help: ''') @@ -326,3 +359,8 @@ def _decode_dict(data): value = _decode_dict(value) rv[key] = value return rv + + +def parse_json_in_str(data): + # parse json and convert everything from unicode to str + return json.loads(data, object_hook=_decode_dict) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index c148208..d11af31 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2015 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -32,41 +26,30 @@ import logging import traceback import random -from shadowsocks import encrypt, eventloop, utils, common +from shadowsocks import encrypt, eventloop, shell, common from shadowsocks.common import parse_header # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time TIMEOUTS_CLEAN_SIZE = 512 -# we check timeouts every TIMEOUT_PRECISION seconds -TIMEOUT_PRECISION = 4 - MSG_FASTOPEN = 0x20000000 -# SOCKS CMD defination +# SOCKS command definition CMD_CONNECT = 1 CMD_BIND = 2 CMD_UDP_ASSOCIATE = 3 -# TCP Relay can be either sslocal or ssserver -# for sslocal it is called is_local=True - # for each opening port, we have a TCP Relay + # for each connection, we have a TCP Relay Handler to handle the connection # for each handler, we have 2 sockets: # local: connected to the client # remote: connected to remote server -# for each handler, we have 2 streams: -# upstream: from client to server direction -# read local and write to remote -# downstream: from server to client direction -# read remote and write to local - # for each handler, it could be at one of several stages: -# sslocal: +# as sslocal: # stage 0 SOCKS hello received from local, send hello to local # stage 1 addr received from local, query DNS for remote # stage 2 UDP assoc @@ -74,7 +57,7 @@ CMD_UDP_ASSOCIATE = 3 # stage 4 still connecting, more data from local received # stage 5 remote connected, piping local and remote -# ssserver: +# as ssserver: # stage 0 just jump to stage 1 # stage 1 addr received from local, query DNS for remote # stage 3 DNS resolved, connect to remote @@ -89,11 +72,16 @@ STAGE_CONNECTING = 4 STAGE_STREAM = 5 STAGE_DESTROYED = -1 -# stream direction +# for each handler, we have 2 stream directions: +# upstream: from client to server direction +# read local and write to remote +# downstream: from server to client direction +# read remote and write to local + STREAM_UP = 0 STREAM_DOWN = 1 -# stream wait status, indicating it's waiting for reading, etc +# for each stream, it's waiting for reading, or writing, or both WAIT_STATUS_INIT = 0 WAIT_STATUS_READING = 1 WAIT_STATUS_WRITING = 2 @@ -112,6 +100,9 @@ class TCPRelayHandler(object): self._remote_sock = None self._config = config self._dns_resolver = dns_resolver + + # TCP Relay works as either sslocal or ssserver + # if is_local, this is sslocal self._is_local = is_local self._stage = STAGE_INIT self._encryptor = encrypt.Encryptor(config['password'], @@ -132,7 +123,8 @@ class TCPRelayHandler(object): fd_to_handlers[local_sock.fileno()] = self local_sock.setblocking(False) local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) - loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR) + loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR, + self._server) self.last_activity = 0 self._update_activity() @@ -150,14 +142,15 @@ class TCPRelayHandler(object): server_port = self._config['server_port'] if type(server_port) == list: server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) logging.debug('chosen server: %s:%d', server, server_port) - # TODO support multiple server IP return server, server_port - def _update_activity(self): + def _update_activity(self, data_len=0): # tell the TCP Relay we have activities recently # else it will think we are inactive and timed out - self._server.update_activity(self) + self._server.update_activity(self, data_len) def _update_stream(self, stream, status): # update a stream to a new waiting status @@ -208,9 +201,7 @@ class TCPRelayHandler(object): errno.EWOULDBLOCK): uncomplete = True else: - logging.error(e) - if self._config['verbose']: - traceback.print_exc() + shell.print_exception(e) self.destroy() return False if uncomplete: @@ -245,7 +236,7 @@ class TCPRelayHandler(object): remote_sock = \ self._create_remote_socket(self._chosen_server[0], self._chosen_server[1]) - self._loop.add(remote_sock, eventloop.POLL_ERR) + self._loop.add(remote_sock, eventloop.POLL_ERR, self._server) data = b''.join(self._data_to_write_to_remote) l = len(data) s = remote_sock.sendto(data, MSG_FASTOPEN, self._chosen_server) @@ -264,7 +255,7 @@ class TCPRelayHandler(object): self._config['fast_open'] = False self.destroy() else: - logging.error(e) + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() self.destroy() @@ -302,7 +293,7 @@ class TCPRelayHandler(object): logging.info('connecting %s:%d from %s:%d' % (common.to_str(remote_addr), remote_port, self._client_address[0], self._client_address[1])) - self._remote_address = (remote_addr, remote_port) + self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS @@ -326,14 +317,13 @@ class TCPRelayHandler(object): self._log_error(e) if self._config['verbose']: traceback.print_exc() - # TODO use logging when debug completed self.destroy() def _create_remote_socket(self, ip, port): addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, socket.SOL_TCP) if len(addrs) == 0: - raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) + raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) af, socktype, proto, canonname, sa = addrs[0] if self._forbidden_iplist: if common.to_str(sa[0]) in self._forbidden_iplist: @@ -382,13 +372,14 @@ class TCPRelayHandler(object): errno.EINPROGRESS: pass self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT) + eventloop.POLL_ERR | eventloop.POLL_OUT, + self._server) self._stage = STAGE_CONNECTING self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) return - except (OSError, IOError) as e: - logging.error(e) + except Exception as e: + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() self.destroy() @@ -396,7 +387,6 @@ class TCPRelayHandler(object): def _on_local_read(self): # handle all local read events and dispatch them to methods for # each stage - self._update_activity() if not self._local_sock: return is_local = self._is_local @@ -410,6 +400,7 @@ class TCPRelayHandler(object): if not data: self.destroy() return + self._update_activity(len(data)) if not is_local: data = self._encryptor.decrypt(data) if not data: @@ -432,10 +423,10 @@ class TCPRelayHandler(object): def _on_remote_read(self): # handle all remote read events - self._update_activity() data = None try: data = self._remote_sock.recv(BUF_SIZE) + except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): @@ -443,6 +434,7 @@ class TCPRelayHandler(object): if not data: self.destroy() return + self._update_activity(len(data)) if self._is_local: data = self._encryptor.decrypt(data) else: @@ -450,7 +442,7 @@ class TCPRelayHandler(object): try: self._write_to_sock(data, self._local_sock) except Exception as e: - logging.error(e) + shell.print_exception(e) if self._config['verbose']: traceback.print_exc() # TODO use logging when debug completed @@ -557,14 +549,13 @@ class TCPRelayHandler(object): class TCPRelay(object): - def __init__(self, config, dns_resolver, is_local): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config self._is_local = is_local self._dns_resolver = dns_resolver self._closed = False self._eventloop = None self._fd_to_handlers = {} - self._last_time = time.time() self._timeout = config['timeout'] self._timeouts = [] # a list for all the handlers @@ -598,6 +589,7 @@ class TCPRelay(object): self._config['fast_open'] = False server_socket.listen(1024) self._server_socket = server_socket + self._stat_callback = stat_callback def add_to_loop(self, loop): if self._eventloop: @@ -605,10 +597,9 @@ class TCPRelay(object): if self._closed: raise Exception('already closed') self._eventloop = loop - loop.add_handler(self._handle_events) - self._eventloop.add(self._server_socket, - eventloop.POLL_IN | eventloop.POLL_ERR) + eventloop.POLL_IN | eventloop.POLL_ERR, self) + self._eventloop.add_periodic(self.handle_periodic) def remove_handler(self, handler): index = self._handler_to_timeouts.get(hash(handler), -1) @@ -617,10 +608,13 @@ class TCPRelay(object): self._timeouts[index] = None del self._handler_to_timeouts[hash(handler)] - def update_activity(self, handler): + def update_activity(self, handler, data_len): + if data_len and self._stat_callback: + self._stat_callback(self._listen_port, data_len) + # set handler to active now = int(time.time()) - if now - handler.last_activity < TIMEOUT_PRECISION: + if now - handler.last_activity < eventloop.TIMEOUT_PRECISION: # thus we can lower timeout modification frequency return handler.last_activity = now @@ -637,7 +631,7 @@ class TCPRelay(object): # we just need a sorted last_activity queue and it's faster than heapq # in fact we can do O(1) insertion/remove so we invent our own if self._timeouts: - logging.log(utils.VERBOSE_LEVEL, 'sweeping timeouts') + logging.log(shell.VERBOSE_LEVEL, 'sweeping timeouts') now = time.time() length = len(self._timeouts) pos = self._timeout_offset @@ -666,53 +660,57 @@ class TCPRelay(object): pos = 0 self._timeout_offset = pos - def _handle_events(self, events): + def handle_event(self, sock, fd, event): # handle events and dispatch to handlers - for sock, fd, event in events: - if sock: - logging.log(utils.VERBOSE_LEVEL, 'fd %d %s', fd, - eventloop.EVENT_NAMES.get(event, event)) - if sock == self._server_socket: - if event & eventloop.POLL_ERR: - # TODO - raise Exception('server_socket error') - try: - logging.debug('accept') - conn = self._server_socket.accept() - TCPRelayHandler(self, self._fd_to_handlers, - self._eventloop, conn[0], self._config, - self._dns_resolver, self._is_local) - except (OSError, IOError) as e: - error_no = eventloop.errno_from_exception(e) - if error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK): - continue - else: - logging.error(e) - if self._config['verbose']: - traceback.print_exc() - else: - if sock: - handler = self._fd_to_handlers.get(fd, None) - if handler: - handler.handle_event(sock, event) + if sock: + logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd, + eventloop.EVENT_NAMES.get(event, event)) + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + # TODO + raise Exception('server_socket error') + try: + logging.debug('accept') + conn = self._server_socket.accept() + TCPRelayHandler(self, self._fd_to_handlers, + self._eventloop, conn[0], self._config, + self._dns_resolver, self._is_local) + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return else: - logging.warn('poll removed fd') + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + else: + if sock: + handler = self._fd_to_handlers.get(fd, None) + if handler: + handler.handle_event(sock, event) + else: + logging.warn('poll removed fd') - now = time.time() - if now - self._last_time > TIMEOUT_PRECISION: - self._sweep_timeout() - self._last_time = now + def handle_periodic(self): if self._closed: if self._server_socket: self._eventloop.remove(self._server_socket) self._server_socket.close() self._server_socket = None - logging.info('closed listen port %d', self._listen_port) + logging.info('closed TCP port %d', self._listen_port) if not self._fd_to_handlers: - self._eventloop.remove_handler(self._handle_events) + logging.info('stopping') + self._eventloop.stop() + self._sweep_timeout() def close(self, next_tick=False): + logging.debug('TCP close') self._closed = True if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) self._server_socket.close() + for handler in list(self._fd_to_handlers.values()): + handler.destroy() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index ff6391c..96d1fbd 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. # SOCKS5 UDP Request # +----+------+------+----------+----------+----------+ @@ -68,26 +62,26 @@ from __future__ import absolute_import, division, print_function, \ with_statement -import time import socket import logging import struct import errno import random -from shadowsocks import encrypt, eventloop, lru_cache, common +from shadowsocks import encrypt, eventloop, lru_cache, common, shell from shadowsocks.common import parse_header, pack_addr BUF_SIZE = 65536 -def client_key(a, b, c, d): - return '%s:%s:%s:%s' % (a, b, c, d) +def client_key(source_addr, server_af): + # notice this is server af, not dest af + return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af) class UDPRelay(object): - def __init__(self, config, dns_resolver, is_local): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config if is_local: self._listen_addr = config['local_address'] @@ -100,7 +94,7 @@ class UDPRelay(object): self._remote_addr = None self._remote_port = None self._dns_resolver = dns_resolver - self._password = config['password'] + self._password = common.to_bytes(config['password']) self._method = config['method'] self._timeout = config['timeout'] self._is_local = is_local @@ -108,9 +102,9 @@ class UDPRelay(object): close_callback=self._close_client) self._client_fd_to_server_addr = \ lru_cache.LRUCache(timeout=config['timeout']) + self._dns_cache = lru_cache.LRUCache(timeout=300) self._eventloop = None self._closed = False - self._last_time = time.time() self._sockets = set() if 'forbidden_ip' in config: self._forbidden_iplist = config['forbidden_ip'] @@ -127,14 +121,16 @@ class UDPRelay(object): server_socket.bind((self._listen_addr, self._listen_port)) server_socket.setblocking(False) self._server_socket = server_socket + self._stat_callback = stat_callback def _get_a_server(self): server = self._config['server'] server_port = self._config['server_port'] if type(server_port) == list: server_port = random.choice(server_port) + if type(server) == list: + server = random.choice(server) logging.debug('chosen server: %s:%d', server, server_port) - # TODO support multiple server IP return server, server_port def _close_client(self, client): @@ -151,6 +147,8 @@ class UDPRelay(object): data, r_addr = server.recvfrom(BUF_SIZE) if not data: logging.debug('UDP handle_server: data is empty') + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) if self._is_local: frag = common.ord(data[2]) if frag != 0: @@ -174,29 +172,34 @@ class UDPRelay(object): else: server_addr, server_port = dest_addr, dest_port - key = client_key(r_addr[0], r_addr[1], dest_addr, dest_port) + addrs = self._dns_cache.get(server_addr, None) + if addrs is None: + addrs = socket.getaddrinfo(server_addr, server_port, 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if not addrs: + # drop + return + else: + self._dns_cache[server_addr] = addrs + + af, socktype, proto, canonname, sa = addrs[0] + key = client_key(r_addr, af) client = self._cache.get(key, None) if not client: # TODO async getaddrinfo - addrs = socket.getaddrinfo(server_addr, server_port, 0, - socket.SOCK_DGRAM, socket.SOL_UDP) - if addrs: - af, socktype, proto, canonname, sa = addrs[0] - if self._forbidden_iplist: - if common.to_str(sa[0]) in self._forbidden_iplist: - logging.debug('IP %s is in forbidden list, drop' % - common.to_str(sa[0])) - # drop - return - client = socket.socket(af, socktype, proto) - client.setblocking(False) - self._cache[key] = client - self._client_fd_to_server_addr[client.fileno()] = r_addr - else: - # drop - return + if self._forbidden_iplist: + if common.to_str(sa[0]) in self._forbidden_iplist: + logging.debug('IP %s is in forbidden list, drop' % + common.to_str(sa[0])) + # drop + return + client = socket.socket(af, socktype, proto) + client.setblocking(False) + self._cache[key] = client + self._client_fd_to_server_addr[client.fileno()] = r_addr + self._sockets.add(client.fileno()) - self._eventloop.add(client, eventloop.POLL_IN) + self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: data = encrypt.encrypt_all(self._password, self._method, 1, data) @@ -213,13 +216,15 @@ class UDPRelay(object): if err in (errno.EINPROGRESS, errno.EAGAIN): pass else: - logging.error(e) + shell.print_exception(e) def _handle_client(self, sock): data, r_addr = sock.recvfrom(BUF_SIZE) if not data: logging.debug('UDP handle_client: data is empty') return + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) if not self._is_local: addrlen = len(r_addr[0]) if addrlen > 255: @@ -254,34 +259,40 @@ class UDPRelay(object): if self._closed: raise Exception('already closed') self._eventloop = loop - loop.add_handler(self._handle_events) server_socket = self._server_socket self._eventloop.add(server_socket, - eventloop.POLL_IN | eventloop.POLL_ERR) + eventloop.POLL_IN | eventloop.POLL_ERR, self) + loop.add_periodic(self.handle_periodic) - def _handle_events(self, events): - for sock, fd, event in events: - if sock == self._server_socket: - if event & eventloop.POLL_ERR: - logging.error('UDP server_socket err') - self._handle_server() - elif sock and (fd in self._sockets): - if event & eventloop.POLL_ERR: - logging.error('UDP client_socket err') - self._handle_client(sock) - now = time.time() - if now - self._last_time > 3: - self._cache.sweep() - self._client_fd_to_server_addr.sweep() - self._last_time = now + def handle_event(self, sock, fd, event): + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + logging.error('UDP server_socket err') + self._handle_server() + elif sock and (fd in self._sockets): + if event & eventloop.POLL_ERR: + logging.error('UDP client_socket err') + self._handle_client(sock) + + def handle_periodic(self): if self._closed: - self._server_socket.close() - for sock in self._sockets: - sock.close() - self._eventloop.remove_handler(self._handle_events) + if self._server_socket: + self._server_socket.close() + self._server_socket = None + for sock in self._sockets: + sock.close() + logging.info('closed UDP port %d', self._listen_port) + self._cache.sweep() + self._client_fd_to_server_addr.sweep() def close(self, next_tick=False): + logging.debug('UDP close') self._closed = True if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) self._server_socket.close() + for client in list(self._cache.values()): + client.close() diff --git a/tests/client-multi-server-ip.json b/tests/client-multi-server-ip.json new file mode 100644 index 0000000..1823c2a --- /dev/null +++ b/tests/client-multi-server-ip.json @@ -0,0 +1,10 @@ +{ + "server":["127.0.0.1", "127.0.0.1"], + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/coverage_server.py b/tests/coverage_server.py index 2df55e3..23cc8cd 100644 --- a/tests/coverage_server.py +++ b/tests/coverage_server.py @@ -1,4 +1,18 @@ #!/usr/bin/env python +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. if __name__ == '__main__': import tornado.ioloop @@ -11,7 +25,7 @@ if __name__ == '__main__': with open('/tmp/%s-coverage' % project, 'rb') as f: coverage = f.read().strip() n = int(coverage.strip('%')) - if n > 80: + if n >= 80: color = 'brightgreen' else: color = 'yellow' diff --git a/tests/gen_multiple_passwd.py b/tests/gen_multiple_passwd.py new file mode 100644 index 0000000..62586c2 --- /dev/null +++ b/tests/gen_multiple_passwd.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +import json + +with open('server-multi-passwd-performance.json', 'wb') as f: + r = { + 'server': '127.0.0.1', + 'local_port': 1081, + 'timeout': 60, + 'method': 'aes-256-cfb' + } + ports = {} + for i in range(7000, 9000): + ports[str(i)] = 'aes_password' + + r['port_password'] = ports + print(r) + f.write(json.dumps(r, indent=4).encode('utf-8')) diff --git a/tests/graceful.json b/tests/graceful.json new file mode 100644 index 0000000..7d94ea5 --- /dev/null +++ b/tests/graceful.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":15, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/graceful_cli.py b/tests/graceful_cli.py new file mode 100644 index 0000000..e58674b --- /dev/null +++ b/tests/graceful_cli.py @@ -0,0 +1,17 @@ +#!/usr/bin/python + +import socks +import time + + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 8001 + + +if __name__ == '__main__': + s = socks.socksocket() + s.set_proxy(socks.SOCKS5, SERVER_IP, 1081) + s.connect((SERVER_IP, SERVER_PORT)) + s.send(b'test') + time.sleep(30) + s.close() diff --git a/tests/graceful_server.py b/tests/graceful_server.py new file mode 100644 index 0000000..d7038f1 --- /dev/null +++ b/tests/graceful_server.py @@ -0,0 +1,13 @@ +#!/usr/bin/python + +import socket + + +if __name__ == '__main__': + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('127.0.0.1', 8001)) + s.listen(1024) + c = None + while True: + c = s.accept() diff --git a/.jenkins.sh b/tests/jenkins.sh similarity index 72% rename from .jenkins.sh rename to tests/jenkins.sh index bb58e04..a85c461 100755 --- a/.jenkins.sh +++ b/tests/jenkins.sh @@ -24,9 +24,10 @@ function run_test { return 0 } +python --version coverage erase mkdir tmp -run_test pep8 . +run_test pep8 --ignore=E402 . run_test pyflakes . run_test coverage run tests/nose_plugin.py -v run_test python setup.py sdist @@ -40,6 +41,7 @@ run_test python tests/test.py --with-coverage -c tests/salsa20.json run_test python tests/test.py --with-coverage -c tests/chacha20.json run_test python tests/test.py --with-coverage -c tests/table.json run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json +run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json run_test python tests/test.py --with-coverage -c tests/workers.json run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json @@ -47,6 +49,15 @@ run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0 run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" +# test if DNS works +run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" + +# test localhost is in the forbidden list by default +run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" + +# test localhost is available when forbidden list is empty +run_test python tests/test.py --with-coverage --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" + if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then # we have to run it twice: @@ -59,6 +70,11 @@ fi run_test tests/test_large_file.sh +if [ "a$JENKINS" != "a1" ] ; then + # jenkins blocked SIGQUIT with sigprocmask(), we have to skip this test on Jenkins + run_test tests/test_graceful_restart.sh +fi +run_test tests/test_udp_src.sh run_test tests/test_command.sh coverage combine && coverage report --include=shadowsocks/* diff --git a/tests/nose_plugin.py b/tests/nose_plugin.py index ad32cf0..86b1a86 100644 --- a/tests/nose_plugin.py +++ b/tests/nose_plugin.py @@ -1,3 +1,19 @@ +#!/usr/bin/env python +# +# Copyright 2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import nose from nose.plugins.base import Plugin diff --git a/tests/server-multi-passwd-empty.json b/tests/server-multi-passwd-empty.json new file mode 100644 index 0000000..de354e5 --- /dev/null +++ b/tests/server-multi-passwd-empty.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + }, + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/server-multi-passwd-performance.json b/tests/server-multi-passwd-performance.json new file mode 100644 index 0000000..c9fbc37 --- /dev/null +++ b/tests/server-multi-passwd-performance.json @@ -0,0 +1,2008 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + "7582": "aes_password", + "7672": "aes_password", + "8923": "aes_password", + "8502": "aes_password", + "8282": "aes_password", + "8871": "aes_password", + "7732": "aes_password", + "8671": "aes_password", + "7018": "aes_password", + "8492": "aes_password", + "7748": "aes_password", + "8992": "aes_password", + "8246": "aes_password", + "7127": "aes_password", + "7775": "aes_password", + "8542": "aes_password", + "8488": "aes_password", + "7515": "aes_password", + "7659": "aes_password", + "8892": "aes_password", + "8028": "aes_password", + "7276": "aes_password", + "7959": "aes_password", + "7457": "aes_password", + "8635": "aes_password", + "7592": "aes_password", + "8764": "aes_password", + "8861": "aes_password", + "8842": "aes_password", + "8135": "aes_password", + "8140": "aes_password", + "8376": "aes_password", + "7733": "aes_password", + "8174": "aes_password", + "7265": "aes_password", + "8314": "aes_password", + "8772": "aes_password", + "8991": "aes_password", + "7183": "aes_password", + "7067": "aes_password", + "7730": "aes_password", + "8694": "aes_password", + "7629": "aes_password", + "7041": "aes_password", + "8507": "aes_password", + "8112": "aes_password", + "8491": "aes_password", + "7273": "aes_password", + "8811": "aes_password", + "8947": "aes_password", + "8612": "aes_password", + "8134": "aes_password", + "8422": "aes_password", + "8970": "aes_password", + "7051": "aes_password", + "8158": "aes_password", + "8934": "aes_password", + "7579": "aes_password", + "7140": "aes_password", + "8448": "aes_password", + "8536": "aes_password", + "7554": "aes_password", + "8168": "aes_password", + "8307": "aes_password", + "8946": "aes_password", + "7872": "aes_password", + "7330": "aes_password", + "8208": "aes_password", + "7955": "aes_password", + "8597": "aes_password", + "7025": "aes_password", + "7086": "aes_password", + "7534": "aes_password", + "7311": "aes_password", + "7758": "aes_password", + "7103": "aes_password", + "8408": "aes_password", + "7688": "aes_password", + "7073": "aes_password", + "8963": "aes_password", + "8578": "aes_password", + "7735": "aes_password", + "7657": "aes_password", + "7763": "aes_password", + "7680": "aes_password", + "8627": "aes_password", + "8205": "aes_password", + "7188": "aes_password", + "8743": "aes_password", + "8472": "aes_password", + "8823": "aes_password", + "7167": "aes_password", + "7008": "aes_password", + "7601": "aes_password", + "8603": "aes_password", + "8467": "aes_password", + "8803": "aes_password", + "7014": "aes_password", + "7233": "aes_password", + "7199": "aes_password", + "7192": "aes_password", + "7329": "aes_password", + "8031": "aes_password", + "8584": "aes_password", + "8041": "aes_password", + "8962": "aes_password", + "8824": "aes_password", + "8079": "aes_password", + "8049": "aes_password", + "7743": "aes_password", + "8035": "aes_password", + "8212": "aes_password", + "8452": "aes_password", + "8484": "aes_password", + "8232": "aes_password", + "8444": "aes_password", + "8410": "aes_password", + "7110": "aes_password", + "7505": "aes_password", + "8856": "aes_password", + "8293": "aes_password", + "7967": "aes_password", + "8267": "aes_password", + "7772": "aes_password", + "8864": "aes_password", + "8518": "aes_password", + "7520": "aes_password", + "7976": "aes_password", + "8407": "aes_password", + "8971": "aes_password", + "7389": "aes_password", + "7510": "aes_password", + "7373": "aes_password", + "8013": "aes_password", + "8310": "aes_password", + "7028": "aes_password", + "7874": "aes_password", + "7356": "aes_password", + "7729": "aes_password", + "7427": "aes_password", + "8312": "aes_password", + "7721": "aes_password", + "7020": "aes_password", + "8231": "aes_password", + "8188": "aes_password", + "8869": "aes_password", + "8595": "aes_password", + "8022": "aes_password", + "8911": "aes_password", + "7957": "aes_password", + "7141": "aes_password", + "7157": "aes_password", + "8471": "aes_password", + "8157": "aes_password", + "8795": "aes_password", + "7087": "aes_password", + "7470": "aes_password", + "7266": "aes_password", + "8072": "aes_password", + "8346": "aes_password", + "7163": "aes_password", + "8954": "aes_password", + "7046": "aes_password", + "7856": "aes_password", + "7883": "aes_password", + "8198": "aes_password", + "8443": "aes_password", + "8496": "aes_password", + "8900": "aes_password", + "8354": "aes_password", + "8758": "aes_password", + "8287": "aes_password", + "7574": "aes_password", + "8316": "aes_password", + "7539": "aes_password", + "8460": "aes_password", + "7616": "aes_password", + "8599": "aes_password", + "7795": "aes_password", + "7079": "aes_password", + "8468": "aes_password", + "8462": "aes_password", + "8645": "aes_password", + "8347": "aes_password", + "8776": "aes_password", + "7072": "aes_password", + "8781": "aes_password", + "7765": "aes_password", + "8048": "aes_password", + "7401": "aes_password", + "8718": "aes_password", + "8712": "aes_password", + "7801": "aes_password", + "8673": "aes_password", + "8791": "aes_password", + "7567": "aes_password", + "7003": "aes_password", + "7358": "aes_password", + "8916": "aes_password", + "7021": "aes_password", + "7487": "aes_password", + "7499": "aes_password", + "7108": "aes_password", + "7501": "aes_password", + "7313": "aes_password", + "8887": "aes_password", + "8724": "aes_password", + "7376": "aes_password", + "7153": "aes_password", + "7377": "aes_password", + "8426": "aes_password", + "8831": "aes_password", + "7380": "aes_password", + "7958": "aes_password", + "8250": "aes_password", + "8155": "aes_password", + "8435": "aes_password", + "7630": "aes_password", + "8026": "aes_password", + "7533": "aes_password", + "8704": "aes_password", + "8411": "aes_password", + "7645": "aes_password", + "7937": "aes_password", + "7488": "aes_password", + "8750": "aes_password", + "7196": "aes_password", + "8714": "aes_password", + "8677": "aes_password", + "7475": "aes_password", + "7625": "aes_password", + "8234": "aes_password", + "8870": "aes_password", + "7147": "aes_password", + "8417": "aes_password", + "7362": "aes_password", + "7341": "aes_password", + "8896": "aes_password", + "8423": "aes_password", + "8884": "aes_password", + "7220": "aes_password", + "8615": "aes_password", + "8719": "aes_password", + "8575": "aes_password", + "8891": "aes_password", + "8210": "aes_password", + "8289": "aes_password", + "7406": "aes_password", + "7692": "aes_password", + "7518": "aes_password", + "7244": "aes_password", + "8561": "aes_password", + "7325": "aes_password", + "7306": "aes_password", + "8266": "aes_password", + "8136": "aes_password", + "7991": "aes_password", + "8844": "aes_password", + "8259": "aes_password", + "7749": "aes_password", + "7238": "aes_password", + "7952": "aes_password", + "8528": "aes_password", + "8477": "aes_password", + "7555": "aes_password", + "7544": "aes_password", + "7478": "aes_password", + "7112": "aes_password", + "8931": "aes_password", + "8082": "aes_password", + "8189": "aes_password", + "8461": "aes_password", + "7740": "aes_password", + "8633": "aes_password", + "8322": "aes_password", + "7093": "aes_password", + "8415": "aes_password", + "8093": "aes_password", + "8682": "aes_password", + "7860": "aes_password", + "8580": "aes_password", + "8503": "aes_password", + "7794": "aes_password", + "8394": "aes_password", + "8487": "aes_password", + "8053": "aes_password", + "7277": "aes_password", + "7241": "aes_password", + "8430": "aes_password", + "8428": "aes_password", + "7805": "aes_password", + "8393": "aes_password", + "7711": "aes_password", + "7807": "aes_password", + "7600": "aes_password", + "8403": "aes_password", + "8141": "aes_password", + "8937": "aes_password", + "7559": "aes_password", + "7834": "aes_password", + "8837": "aes_password", + "7823": "aes_password", + "8928": "aes_password", + "7083": "aes_password", + "7590": "aes_password", + "8806": "aes_password", + "7130": "aes_password", + "7929": "aes_password", + "8684": "aes_password", + "8195": "aes_password", + "8706": "aes_password", + "7044": "aes_password", + "7403": "aes_password", + "8890": "aes_password", + "8364": "aes_password", + "8206": "aes_password", + "7882": "aes_password", + "8522": "aes_password", + "8958": "aes_password", + "7429": "aes_password", + "8586": "aes_password", + "8330": "aes_password", + "8922": "aes_password", + "7940": "aes_password", + "7379": "aes_password", + "8955": "aes_password", + "7168": "aes_password", + "7294": "aes_password", + "7949": "aes_password", + "7384": "aes_password", + "8832": "aes_password", + "7423": "aes_password", + "8763": "aes_password", + "7148": "aes_password", + "7029": "aes_password", + "7969": "aes_password", + "8190": "aes_password", + "8807": "aes_password", + "8889": "aes_password", + "7750": "aes_password", + "7348": "aes_password", + "7193": "aes_password", + "7459": "aes_password", + "7507": "aes_password", + "7536": "aes_password", + "8734": "aes_password", + "7174": "aes_password", + "8400": "aes_password", + "8630": "aes_password", + "7128": "aes_password", + "7261": "aes_password", + "7527": "aes_password", + "7232": "aes_password", + "7843": "aes_password", + "7326": "aes_password", + "8639": "aes_password", + "7830": "aes_password", + "7981": "aes_password", + "8404": "aes_password", + "8888": "aes_password", + "7920": "aes_password", + "7410": "aes_password", + "7204": "aes_password", + "8382": "aes_password", + "8355": "aes_password", + "7700": "aes_password", + "7606": "aes_password", + "7372": "aes_password", + "8106": "aes_password", + "8160": "aes_password", + "7511": "aes_password", + "8204": "aes_password", + "8732": "aes_password", + "8751": "aes_password", + "7727": "aes_password", + "7137": "aes_password", + "8311": "aes_password", + "8587": "aes_password", + "7336": "aes_password", + "7674": "aes_password", + "8009": "aes_password", + "7230": "aes_password", + "7383": "aes_password", + "8867": "aes_password", + "7260": "aes_password", + "7497": "aes_password", + "7390": "aes_password", + "8821": "aes_password", + "7274": "aes_password", + "7285": "aes_password", + "7857": "aes_password", + "8137": "aes_password", + "7114": "aes_password", + "7979": "aes_password", + "8726": "aes_password", + "7227": "aes_password", + "7714": "aes_password", + "8012": "aes_password", + "7613": "aes_password", + "8876": "aes_password", + "7622": "aes_password", + "8582": "aes_password", + "7120": "aes_password", + "7104": "aes_password", + "8785": "aes_password", + "8096": "aes_password", + "8129": "aes_password", + "8481": "aes_password", + "8695": "aes_password", + "7473": "aes_password", + "8163": "aes_password", + "8357": "aes_password", + "8501": "aes_password", + "7177": "aes_password", + "7931": "aes_password", + "8220": "aes_password", + "7399": "aes_password", + "7956": "aes_password", + "8801": "aes_password", + "7719": "aes_password", + "8042": "aes_password", + "7433": "aes_password", + "7827": "aes_password", + "8377": "aes_password", + "7745": "aes_password", + "7302": "aes_password", + "8399": "aes_password", + "7766": "aes_password", + "8720": "aes_password", + "8685": "aes_password", + "8558": "aes_password", + "7796": "aes_password", + "7319": "aes_password", + "7170": "aes_password", + "7342": "aes_password", + "7191": "aes_password", + "8747": "aes_password", + "7231": "aes_password", + "7817": "aes_password", + "7352": "aes_password", + "7057": "aes_password", + "8177": "aes_password", + "7221": "aes_password", + "7297": "aes_password", + "7686": "aes_password", + "7082": "aes_password", + "8414": "aes_password", + "8529": "aes_password", + "7257": "aes_password", + "7300": "aes_password", + "7159": "aes_password", + "8901": "aes_password", + "7578": "aes_password", + "8479": "aes_password", + "7225": "aes_password", + "8286": "aes_password", + "7182": "aes_password", + "8194": "aes_password", + "8850": "aes_password", + "7847": "aes_password", + "7665": "aes_password", + "8011": "aes_password", + "7702": "aes_password", + "8638": "aes_password", + "7116": "aes_password", + "7301": "aes_password", + "8936": "aes_password", + "8661": "aes_password", + "8333": "aes_password", + "8025": "aes_password", + "7368": "aes_password", + "8634": "aes_password", + "7154": "aes_password", + "8365": "aes_password", + "8736": "aes_password", + "8478": "aes_password", + "7436": "aes_password", + "7411": "aes_password", + "7913": "aes_password", + "8236": "aes_password", + "8854": "aes_password", + "8722": "aes_password", + "8227": "aes_password", + "7757": "aes_password", + "8835": "aes_password", + "8651": "aes_password", + "7417": "aes_password", + "7877": "aes_password", + "7200": "aes_password", + "8622": "aes_password", + "7004": "aes_password", + "8845": "aes_password", + "8159": "aes_password", + "8741": "aes_password", + "7106": "aes_password", + "8897": "aes_password", + "7968": "aes_password", + "7047": "aes_password", + "8860": "aes_password", + "8777": "aes_password", + "7597": "aes_password", + "8859": "aes_password", + "7117": "aes_password", + "8178": "aes_password", + "8642": "aes_password", + "7246": "aes_password", + "7557": "aes_password", + "7965": "aes_password", + "7699": "aes_password", + "8658": "aes_password", + "7442": "aes_password", + "8272": "aes_password", + "7821": "aes_password", + "7893": "aes_password", + "8665": "aes_password", + "8499": "aes_password", + "7897": "aes_password", + "7173": "aes_password", + "7007": "aes_password", + "8219": "aes_password", + "8040": "aes_password", + "7571": "aes_password", + "7526": "aes_password", + "8203": "aes_password", + "7810": "aes_password", + "8974": "aes_password", + "8200": "aes_password", + "7778": "aes_password", + "7987": "aes_password", + "7701": "aes_password", + "7443": "aes_password", + "7798": "aes_password", + "8995": "aes_password", + "8473": "aes_password", + "7132": "aes_password", + "7262": "aes_password", + "7720": "aes_password", + "7282": "aes_password", + "8066": "aes_password", + "7006": "aes_password", + "7197": "aes_password", + "7815": "aes_password", + "7933": "aes_password", + "8138": "aes_password", + "8418": "aes_password", + "7365": "aes_password", + "7786": "aes_password", + "7891": "aes_password", + "8317": "aes_password", + "8207": "aes_password", + "8416": "aes_password", + "7448": "aes_password", + "8843": "aes_password", + "7371": "aes_password", + "8780": "aes_password", + "7989": "aes_password", + "8043": "aes_password", + "7363": "aes_password", + "7550": "aes_password", + "8678": "aes_password", + "7837": "aes_password", + "8302": "aes_password", + "7907": "aes_password", + "8865": "aes_password", + "8153": "aes_password", + "8090": "aes_password", + "7268": "aes_password", + "8292": "aes_password", + "7919": "aes_password", + "8131": "aes_password", + "8815": "aes_password", + "8154": "aes_password", + "7777": "aes_password", + "8369": "aes_password", + "8929": "aes_password", + "7670": "aes_password", + "7484": "aes_password", + "8353": "aes_password", + "8017": "aes_password", + "8833": "aes_password", + "8001": "aes_password", + "8058": "aes_password", + "7918": "aes_password", + "7694": "aes_password", + "7485": "aes_password", + "8592": "aes_password", + "7584": "aes_password", + "8527": "aes_password", + "8285": "aes_password", + "8264": "aes_password", + "8879": "aes_password", + "7944": "aes_password", + "7000": "aes_password", + "7187": "aes_password", + "7039": "aes_password", + "7769": "aes_password", + "8063": "aes_password", + "8701": "aes_password", + "7432": "aes_password", + "8594": "aes_password", + "7052": "aes_password", + "7369": "aes_password", + "8474": "aes_password", + "7709": "aes_password", + "8296": "aes_password", + "8278": "aes_password", + "8395": "aes_password", + "8674": "aes_password", + "8733": "aes_password", + "7890": "aes_password", + "7080": "aes_password", + "7528": "aes_password", + "7782": "aes_password", + "7466": "aes_password", + "7903": "aes_password", + "8105": "aes_password", + "7144": "aes_password", + "8069": "aes_password", + "8254": "aes_password", + "8573": "aes_password", + "7787": "aes_password", + "7577": "aes_password", + "8918": "aes_password", + "8767": "aes_password", + "7611": "aes_password", + "8233": "aes_password", + "7802": "aes_password", + "7129": "aes_password", + "8968": "aes_password", + "8217": "aes_password", + "8176": "aes_password", + "8027": "aes_password", + "7941": "aes_password", + "8070": "aes_password", + "8697": "aes_password", + "8798": "aes_password", + "8553": "aes_password", + "8510": "aes_password", + "7254": "aes_password", + "7386": "aes_password", + "8800": "aes_password", + "7712": "aes_password", + "7844": "aes_password", + "7535": "aes_password", + "8273": "aes_password", + "8875": "aes_password", + "8675": "aes_password", + "7396": "aes_password", + "7649": "aes_password", + "8623": "aes_password", + "7186": "aes_password", + "8125": "aes_password", + "7734": "aes_password", + "8275": "aes_password", + "7521": "aes_password", + "7135": "aes_password", + "8546": "aes_password", + "7867": "aes_password", + "8989": "aes_password", + "8787": "aes_password", + "8225": "aes_password", + "8457": "aes_password", + "7405": "aes_password", + "7588": "aes_password", + "7854": "aes_password", + "7789": "aes_password", + "8133": "aes_password", + "7641": "aes_password", + "8535": "aes_password", + "7849": "aes_password", + "7594": "aes_password", + "8313": "aes_password", + "8097": "aes_password", + "8030": "aes_password", + "8737": "aes_password", + "8260": "aes_password", + "8950": "aes_password", + "7249": "aes_password", + "7644": "aes_password", + "7912": "aes_password", + "8979": "aes_password", + "8998": "aes_password", + "8731": "aes_password", + "8662": "aes_password", + "7983": "aes_password", + "7035": "aes_password", + "7841": "aes_password", + "8600": "aes_password", + "7228": "aes_password", + "7071": "aes_password", + "8080": "aes_password", + "8713": "aes_password", + "7210": "aes_password", + "7935": "aes_password", + "8057": "aes_password", + "8242": "aes_password", + "7084": "aes_password", + "7070": "aes_password", + "7494": "aes_password", + "8451": "aes_password", + "8626": "aes_password", + "8618": "aes_password", + "7741": "aes_password", + "7118": "aes_password", + "7387": "aes_password", + "7715": "aes_password", + "8143": "aes_password", + "7668": "aes_password", + "7716": "aes_password", + "8101": "aes_password", + "7234": "aes_password", + "8021": "aes_password", + "7156": "aes_password", + "8392": "aes_password", + "7900": "aes_password", + "7055": "aes_password", + "8566": "aes_password", + "7869": "aes_password", + "7864": "aes_password", + "8265": "aes_password", + "8216": "aes_password", + "7738": "aes_password", + "7467": "aes_password", + "8447": "aes_password", + "8564": "aes_password", + "7767": "aes_password", + "7811": "aes_password", + "7898": "aes_password", + "8182": "aes_password", + "8065": "aes_password", + "7561": "aes_password", + "8545": "aes_password", + "7253": "aes_password", + "8173": "aes_password", + "7922": "aes_password", + "7951": "aes_password", + "7612": "aes_password", + "7324": "aes_password", + "7549": "aes_password", + "7858": "aes_password", + "8655": "aes_password", + "8211": "aes_password", + "8469": "aes_password", + "7298": "aes_password", + "8380": "aes_password", + "7394": "aes_password", + "7089": "aes_password", + "8060": "aes_password", + "7591": "aes_password", + "7542": "aes_password", + "7540": "aes_password", + "7456": "aes_password", + "7768": "aes_password", + "8489": "aes_password", + "8089": "aes_password", + "7838": "aes_password", + "8644": "aes_password", + "8344": "aes_password", + "7739": "aes_password", + "7984": "aes_password", + "7909": "aes_password", + "8517": "aes_password", + "8056": "aes_password", + "8297": "aes_password", + "8647": "aes_password", + "8334": "aes_password", + "7056": "aes_password", + "7164": "aes_password", + "8878": "aes_password", + "7816": "aes_password", + "7444": "aes_password", + "8996": "aes_password", + "8359": "aes_password", + "7901": "aes_password", + "8127": "aes_password", + "7424": "aes_password", + "8116": "aes_password", + "7354": "aes_password", + "8919": "aes_password", + "7214": "aes_password", + "7589": "aes_password", + "8982": "aes_password", + "8244": "aes_password", + "8295": "aes_password", + "7669": "aes_password", + "7562": "aes_password", + "8470": "aes_password", + "7474": "aes_password", + "7345": "aes_password", + "7799": "aes_password", + "8269": "aes_password", + "8213": "aes_password", + "8786": "aes_password", + "7482": "aes_password", + "8261": "aes_password", + "8755": "aes_password", + "8882": "aes_password", + "7166": "aes_password", + "7428": "aes_password", + "8766": "aes_password", + "7458": "aes_password", + "8372": "aes_password", + "8045": "aes_password", + "8185": "aes_password", + "7602": "aes_password", + "8373": "aes_password", + "7826": "aes_password", + "8249": "aes_password", + "8881": "aes_password", + "8830": "aes_password", + "8044": "aes_password", + "7563": "aes_password", + "7509": "aes_password", + "7290": "aes_password", + "7019": "aes_password", + "8454": "aes_password", + "8637": "aes_password", + "7876": "aes_password", + "8500": "aes_password", + "8226": "aes_password", + "7744": "aes_password", + "7753": "aes_password", + "7017": "aes_password", + "8362": "aes_password", + "7642": "aes_password", + "8091": "aes_password", + "7512": "aes_password", + "7708": "aes_password", + "8335": "aes_password", + "7292": "aes_password", + "8281": "aes_password", + "8132": "aes_password", + "7683": "aes_password", + "7048": "aes_password", + "7316": "aes_password", + "7011": "aes_password", + "8299": "aes_password", + "8440": "aes_password", + "8147": "aes_password", + "8280": "aes_password", + "8130": "aes_password", + "8303": "aes_password", + "8610": "aes_password", + "8361": "aes_password", + "8339": "aes_password", + "8037": "aes_password", + "8102": "aes_password", + "7845": "aes_password", + "7307": "aes_password", + "8607": "aes_password", + "8523": "aes_password", + "7839": "aes_password", + "7279": "aes_password", + "7321": "aes_password", + "8032": "aes_password", + "8894": "aes_password", + "8166": "aes_password", + "7381": "aes_password", + "8113": "aes_password", + "8139": "aes_password", + "8290": "aes_password", + "7990": "aes_password", + "7388": "aes_password", + "8571": "aes_password", + "8730": "aes_password", + "8441": "aes_password", + "8074": "aes_password", + "7813": "aes_password", + "8555": "aes_password", + "8978": "aes_password", + "7835": "aes_password", + "7323": "aes_password", + "7293": "aes_password", + "8550": "aes_password", + "7617": "aes_password", + "8071": "aes_password", + "7998": "aes_password", + "8115": "aes_password", + "7419": "aes_password", + "8825": "aes_password", + "8412": "aes_password", + "8019": "aes_password", + "8142": "aes_password", + "8186": "aes_password", + "8909": "aes_password", + "8078": "aes_password", + "8952": "aes_password", + "8360": "aes_password", + "8336": "aes_password", + "7953": "aes_password", + "7005": "aes_password", + "8663": "aes_password", + "8866": "aes_password", + "7950": "aes_password", + "7248": "aes_password", + "8519": "aes_password", + "8099": "aes_password", + "8151": "aes_password", + "8959": "aes_password", + "7042": "aes_password", + "7939": "aes_password", + "8064": "aes_password", + "8165": "aes_password", + "8836": "aes_password", + "8965": "aes_password", + "7431": "aes_password", + "7223": "aes_password", + "7999": "aes_password", + "8913": "aes_password", + "7921": "aes_password", + "8883": "aes_password", + "8169": "aes_password", + "7441": "aes_password", + "7469": "aes_password", + "7666": "aes_password", + "8547": "aes_password", + "7993": "aes_password", + "7705": "aes_password", + "8103": "aes_password", + "8524": "aes_password", + "8240": "aes_password", + "7779": "aes_password", + "7344": "aes_password", + "7395": "aes_password", + "8420": "aes_password", + "7287": "aes_password", + "7926": "aes_password", + "8100": "aes_password", + "8874": "aes_password", + "7496": "aes_password", + "7626": "aes_password", + "7784": "aes_password", + "7880": "aes_password", + "7226": "aes_password", + "8405": "aes_password", + "7910": "aes_password", + "8693": "aes_password", + "7997": "aes_password", + "8585": "aes_password", + "8383": "aes_password", + "8429": "aes_password", + "7878": "aes_password", + "8098": "aes_password", + "7288": "aes_password", + "8509": "aes_password", + "8809": "aes_password", + "7973": "aes_password", + "7620": "aes_password", + "7115": "aes_password", + "8445": "aes_password", + "8977": "aes_password", + "8341": "aes_password", + "7859": "aes_password", + "8256": "aes_password", + "7119": "aes_password", + "8442": "aes_password", + "8606": "aes_password", + "7992": "aes_password", + "7270": "aes_password", + "8988": "aes_password", + "7375": "aes_password", + "7747": "aes_password", + "7100": "aes_password", + "7639": "aes_password", + "7296": "aes_password", + "7435": "aes_password", + "7889": "aes_password", + "7636": "aes_password", + "7946": "aes_password", + "7819": "aes_password", + "7978": "aes_password", + "7728": "aes_password", + "8152": "aes_password", + "7660": "aes_password", + "7464": "aes_password", + "8398": "aes_password", + "8804": "aes_password", + "7450": "aes_password", + "8020": "aes_password", + "7988": "aes_password", + "7398": "aes_password", + "7171": "aes_password", + "8315": "aes_password", + "7871": "aes_password", + "8513": "aes_password", + "7250": "aes_password", + "8181": "aes_password", + "7793": "aes_password", + "7414": "aes_password", + "7179": "aes_password", + "7445": "aes_password", + "7259": "aes_password", + "8779": "aes_password", + "7695": "aes_password", + "7149": "aes_password", + "8973": "aes_password", + "8258": "aes_password", + "7291": "aes_password", + "8301": "aes_password", + "7447": "aes_password", + "8276": "aes_password", + "8279": "aes_password", + "7572": "aes_password", + "7146": "aes_password", + "7764": "aes_password", + "7504": "aes_password", + "7604": "aes_password", + "7465": "aes_password", + "7565": "aes_password", + "8976": "aes_password", + "8016": "aes_password", + "7707": "aes_password", + "7627": "aes_password", + "8379": "aes_password", + "7523": "aes_password", + "7145": "aes_password", + "7971": "aes_password", + "8790": "aes_password", + "8475": "aes_password", + "7718": "aes_password", + "7481": "aes_password", + "7650": "aes_password", + "7808": "aes_password", + "7142": "aes_password", + "8816": "aes_password", + "7676": "aes_password", + "7873": "aes_password", + "7885": "aes_password", + "7049": "aes_password", + "8994": "aes_password", + "8863": "aes_password", + "7178": "aes_password", + "8625": "aes_password", + "7237": "aes_password", + "7936": "aes_password", + "7575": "aes_password", + "7673": "aes_password", + "7689": "aes_password", + "7455": "aes_password", + "7780": "aes_password", + "8687": "aes_password", + "7675": "aes_password", + "8431": "aes_password", + "7030": "aes_password", + "8756": "aes_password", + "8318": "aes_password", + "7088": "aes_password", + "8792": "aes_password", + "7454": "aes_password", + "7776": "aes_password", + "8531": "aes_password", + "8829": "aes_password", + "8350": "aes_password", + "7198": "aes_password", + "7438": "aes_password", + "8543": "aes_password", + "8981": "aes_password", + "8915": "aes_password", + "8446": "aes_password", + "7812": "aes_password", + "7570": "aes_password", + "7054": "aes_password", + "8903": "aes_password", + "7213": "aes_password", + "8670": "aes_password", + "7211": "aes_password", + "7327": "aes_password", + "7121": "aes_password", + "8926": "aes_password", + "7430": "aes_password", + "8413": "aes_password", + "7337": "aes_password", + "7771": "aes_password", + "7350": "aes_password", + "7284": "aes_password", + "8252": "aes_password", + "7138": "aes_password", + "7491": "aes_password", + "8774": "aes_password", + "8828": "aes_password", + "7069": "aes_password", + "7545": "aes_password", + "7360": "aes_password", + "8771": "aes_password", + "7915": "aes_password", + "8485": "aes_password", + "7002": "aes_password", + "8838": "aes_password", + "7040": "aes_password", + "8820": "aes_password", + "8033": "aes_password", + "7053": "aes_password", + "8640": "aes_password", + "8552": "aes_password", + "8180": "aes_password", + "7361": "aes_password", + "7560": "aes_password", + "8277": "aes_password", + "7243": "aes_password", + "8999": "aes_password", + "7391": "aes_password", + "8930": "aes_password", + "8957": "aes_password", + "8504": "aes_password", + "7122": "aes_password", + "7139": "aes_password", + "8789": "aes_password", + "8245": "aes_password", + "7493": "aes_password", + "8810": "aes_password", + "8521": "aes_password", + "7317": "aes_password", + "7476": "aes_password", + "8562": "aes_password", + "7724": "aes_password", + "8652": "aes_password", + "7434": "aes_password", + "8110": "aes_password", + "8533": "aes_password", + "7970": "aes_password", + "8745": "aes_password", + "7585": "aes_password", + "7085": "aes_password", + "8421": "aes_password", + "8370": "aes_password", + "7289": "aes_password", + "7996": "aes_password", + "8840": "aes_password", + "7194": "aes_password", + "7256": "aes_password", + "8609": "aes_password", + "8604": "aes_password", + "7062": "aes_password", + "7252": "aes_password", + "8076": "aes_password", + "7647": "aes_password", + "8271": "aes_password", + "7684": "aes_password", + "8539": "aes_password", + "8588": "aes_password", + "8611": "aes_password", + "8654": "aes_password", + "7836": "aes_password", + "8351": "aes_password", + "7962": "aes_password", + "8230": "aes_password", + "7332": "aes_password", + "7573": "aes_password", + "7825": "aes_password", + "7934": "aes_password", + "8572": "aes_password", + "8700": "aes_password", + "8328": "aes_password", + "8046": "aes_password", + "7425": "aes_password", + "8729": "aes_password", + "8886": "aes_password", + "7806": "aes_password", + "7239": "aes_password", + "8601": "aes_password", + "8717": "aes_password", + "7697": "aes_password", + "8352": "aes_password", + "8123": "aes_password", + "7205": "aes_password", + "8298": "aes_password", + "7490": "aes_password", + "7023": "aes_password", + "7964": "aes_password", + "8986": "aes_password", + "7101": "aes_password", + "7634": "aes_password", + "7269": "aes_password", + "8000": "aes_password", + "8172": "aes_password", + "7172": "aes_password", + "8107": "aes_password", + "8486": "aes_password", + "8716": "aes_password", + "8961": "aes_password", + "7685": "aes_password", + "7667": "aes_password", + "7359": "aes_password", + "7804": "aes_password", + "7012": "aes_password", + "8975": "aes_password", + "7598": "aes_password", + "7662": "aes_password", + "7043": "aes_password", + "8331": "aes_password", + "8617": "aes_password", + "7015": "aes_password", + "8390": "aes_password", + "8608": "aes_password", + "7346": "aes_password", + "7331": "aes_password", + "8984": "aes_password", + "7339": "aes_password", + "8465": "aes_password", + "7184": "aes_password", + "7851": "aes_password", + "7868": "aes_password", + "7663": "aes_password", + "8849": "aes_password", + "8667": "aes_password", + "7506": "aes_password", + "7351": "aes_password", + "7109": "aes_password", + "7426": "aes_password", + "7453": "aes_password", + "7737": "aes_password", + "7870": "aes_password", + "8308": "aes_password", + "7543": "aes_password", + "7299": "aes_password", + "8340": "aes_password", + "8243": "aes_password", + "7357": "aes_password", + "8641": "aes_password", + "7553": "aes_password", + "8464": "aes_password", + "7966": "aes_password", + "8581": "aes_password", + "7343": "aes_password", + "8759": "aes_password", + "7631": "aes_password", + "7181": "aes_password", + "7415": "aes_password", + "8187": "aes_password", + "8924": "aes_password", + "7452": "aes_password", + "8797": "aes_password", + "8525": "aes_password", + "7026": "aes_password", + "7322": "aes_password", + "7982": "aes_password", + "8906": "aes_password", + "8342": "aes_password", + "7977": "aes_password", + "7059": "aes_password", + "7986": "aes_password", + "7513": "aes_password", + "7134": "aes_password", + "7462": "aes_password", + "7800": "aes_password", + "8966": "aes_password", + "8453": "aes_password", + "7760": "aes_password", + "7155": "aes_password", + "8632": "aes_password", + "7679": "aes_password", + "8808": "aes_password", + "8508": "aes_password", + "7479": "aes_password", + "7263": "aes_password", + "8648": "aes_password", + "7972": "aes_password", + "8215": "aes_password", + "8948": "aes_password", + "7894": "aes_password", + "7364": "aes_password", + "8520": "aes_password", + "8754": "aes_password", + "7773": "aes_password", + "7255": "aes_password", + "7413": "aes_password", + "7524": "aes_password", + "8511": "aes_password", + "8914": "aes_password", + "8557": "aes_password", + "8085": "aes_password", + "7938": "aes_password", + "8549": "aes_password", + "8839": "aes_password", + "7628": "aes_password", + "7852": "aes_password", + "8788": "aes_password", + "8680": "aes_password", + "8371": "aes_password", + "8378": "aes_password", + "7525": "aes_password", + "7338": "aes_password", + "8969": "aes_password", + "8895": "aes_password", + "7280": "aes_password", + "8827": "aes_password", + "7502": "aes_password", + "8126": "aes_password", + "7208": "aes_password", + "8643": "aes_password", + "7175": "aes_password", + "8183": "aes_password", + "7314": "aes_password", + "8818": "aes_password", + "8039": "aes_password", + "7392": "aes_password", + "7902": "aes_password", + "7576": "aes_password", + "8537": "aes_password", + "7618": "aes_password", + "7203": "aes_password", + "8306": "aes_password", + "7855": "aes_password", + "8778": "aes_password", + "7928": "aes_password", + "7866": "aes_password", + "8483": "aes_password", + "7152": "aes_password", + "7309": "aes_password", + "7222": "aes_password", + "8023": "aes_password", + "7619": "aes_password", + "8985": "aes_password", + "7643": "aes_password", + "7440": "aes_password", + "7646": "aes_password", + "7892": "aes_password", + "7061": "aes_password", + "8941": "aes_password", + "7846": "aes_password", + "8683": "aes_password", + "7829": "aes_password", + "8711": "aes_password", + "8235": "aes_password", + "8463": "aes_password", + "7899": "aes_password", + "8602": "aes_password", + "8114": "aes_password", + "7593": "aes_password", + "7385": "aes_password", + "7624": "aes_password", + "8659": "aes_password", + "7460": "aes_password", + "7917": "aes_password", + "8705": "aes_password", + "7713": "aes_password", + "7875": "aes_password", + "7788": "aes_password", + "7548": "aes_password", + "7076": "aes_password", + "8770": "aes_password", + "8506": "aes_password", + "8769": "aes_password", + "8197": "aes_password", + "7655": "aes_password", + "7247": "aes_password", + "8432": "aes_password", + "7783": "aes_password", + "8327": "aes_password", + "8120": "aes_password", + "7131": "aes_password", + "8268": "aes_password", + "7696": "aes_password", + "8783": "aes_password", + "8406": "aes_password", + "8433": "aes_password", + "8024": "aes_password", + "8005": "aes_password", + "7097": "aes_password", + "8554": "aes_password", + "8967": "aes_password", + "8943": "aes_password", + "7792": "aes_password", + "8003": "aes_password", + "8698": "aes_password", + "7229": "aes_password", + "7687": "aes_password", + "8532": "aes_password", + "7304": "aes_password", + "8122": "aes_password", + "8202": "aes_password", + "7833": "aes_password", + "8679": "aes_password", + "7580": "aes_password", + "8498": "aes_password", + "8345": "aes_password", + "8696": "aes_password", + "7422": "aes_password", + "8650": "aes_password", + "8905": "aes_password", + "7495": "aes_password", + "7075": "aes_password", + "8912": "aes_password", + "8570": "aes_password", + "8847": "aes_password", + "8613": "aes_password", + "7551": "aes_password", + "7150": "aes_password", + "7218": "aes_password", + "7514": "aes_password", + "7195": "aes_password", + "8775": "aes_password", + "8757": "aes_password", + "8251": "aes_password", + "7209": "aes_password", + "7162": "aes_password", + "7564": "aes_password", + "8121": "aes_password", + "7960": "aes_password", + "7656": "aes_password", + "7397": "aes_password", + "8699": "aes_password", + "7421": "aes_password", + "8263": "aes_password", + "8598": "aes_password", + "7009": "aes_password", + "7861": "aes_password", + "7001": "aes_password", + "8744": "aes_password", + "8368": "aes_password", + "7975": "aes_password", + "7165": "aes_password", + "8530": "aes_password", + "8614": "aes_password", + "7258": "aes_password", + "8034": "aes_password", + "7449": "aes_password", + "7932": "aes_password", + "7906": "aes_password", + "8124": "aes_password", + "8526": "aes_password", + "8568": "aes_password", + "7754": "aes_password", + "8228": "aes_password", + "8904": "aes_password", + "8161": "aes_password", + "7547": "aes_password", + "7310": "aes_password", + "7558": "aes_password", + "8338": "aes_password", + "8799": "aes_password", + "8224": "aes_password", + "8563": "aes_password", + "7863": "aes_password", + "7682": "aes_password", + "8944": "aes_password", + "7914": "aes_password", + "7541": "aes_password", + "8953": "aes_password", + "7318": "aes_password", + "8560": "aes_password", + "8972": "aes_password", + "7608": "aes_password", + "8455": "aes_password", + "7468": "aes_password", + "7037": "aes_password", + "7690": "aes_password", + "8814": "aes_password", + "8664": "aes_password", + "7756": "aes_password", + "7569": "aes_password", + "7102": "aes_password", + "8119": "aes_password", + "7161": "aes_password", + "7378": "aes_password", + "7790": "aes_password", + "7638": "aes_password", + "7678": "aes_password", + "7483": "aes_password", + "7063": "aes_password", + "8348": "aes_password", + "7974": "aes_password", + "8596": "aes_password", + "8653": "aes_password", + "8319": "aes_password", + "8933": "aes_password", + "8877": "aes_password", + "7886": "aes_password", + "8459": "aes_password", + "8008": "aes_password", + "8631": "aes_password", + "8482": "aes_password", + "8868": "aes_password", + "7031": "aes_password", + "8036": "aes_password", + "7530": "aes_password", + "7498": "aes_password", + "8497": "aes_password", + "8505": "aes_password", + "8323": "aes_password", + "7566": "aes_password", + "7038": "aes_password", + "7640": "aes_password", + "8690": "aes_password", + "8150": "aes_password", + "7235": "aes_password", + "8921": "aes_password", + "8196": "aes_password", + "8938": "aes_password", + "7531": "aes_password", + "8002": "aes_password", + "7439": "aes_password", + "7451": "aes_password", + "7583": "aes_password", + "7400": "aes_password", + "7242": "aes_password", + "8589": "aes_password", + "8826": "aes_password", + "8050": "aes_password", + "8960": "aes_password", + "7409": "aes_password", + "8873": "aes_password", + "8710": "aes_password", + "8932": "aes_password", + "7751": "aes_password", + "8846": "aes_password", + "7420": "aes_password", + "8817": "aes_password", + "7884": "aes_password", + "7124": "aes_password", + "8047": "aes_password", + "8081": "aes_password", + "8144": "aes_password", + "7477": "aes_password", + "7066": "aes_password", + "7862": "aes_password", + "8389": "aes_password", + "8321": "aes_password", + "8239": "aes_password", + "8987": "aes_password", + "7032": "aes_password", + "7522": "aes_password", + "7691": "aes_password", + "8567": "aes_password", + "8425": "aes_password", + "8128": "aes_password", + "8853": "aes_password", + "7092": "aes_password", + "8624": "aes_password", + "7416": "aes_password", + "7461": "aes_password", + "7283": "aes_password", + "7463": "aes_password", + "8956": "aes_password", + "8574": "aes_password", + "8872": "aes_password", + "8480": "aes_password", + "8384": "aes_password", + "8540": "aes_password", + "8381": "aes_password", + "7064": "aes_password", + "8095": "aes_password", + "8349": "aes_password", + "8855": "aes_password", + "7881": "aes_password", + "7519": "aes_password", + "7961": "aes_password", + "8728": "aes_password", + "8222": "aes_password", + "8326": "aes_password", + "8218": "aes_password", + "8723": "aes_password", + "7068": "aes_password", + "7202": "aes_password", + "7923": "aes_password", + "7347": "aes_password", + "8794": "aes_password", + "7264": "aes_password", + "7437": "aes_password", + "8494": "aes_password", + "7335": "aes_password", + "7717": "aes_password", + "7295": "aes_password", + "8061": "aes_password", + "8702": "aes_password", + "7824": "aes_password", + "8111": "aes_password", + "7176": "aes_password", + "8332": "aes_password", + "8556": "aes_password", + "7818": "aes_password", + "8583": "aes_password", + "8920": "aes_password", + "8006": "aes_password", + "8108": "aes_password", + "7308": "aes_password", + "8708": "aes_password", + "7587": "aes_password", + "7219": "aes_password", + "7367": "aes_password", + "8681": "aes_password", + "8945": "aes_password", + "8337": "aes_password", + "7586": "aes_password", + "7095": "aes_password", + "8880": "aes_password", + "8388": "aes_password", + "7065": "aes_password", + "7045": "aes_password", + "7111": "aes_password", + "8762": "aes_password", + "7703": "aes_password", + "8686": "aes_password", + "8367": "aes_password", + "8796": "aes_password", + "7840": "aes_password", + "7945": "aes_password", + "8329": "aes_password", + "7107": "aes_password", + "8167": "aes_password", + "8862": "aes_password", + "8951": "aes_password", + "7963": "aes_password", + "8073": "aes_password", + "8068": "aes_password", + "7759": "aes_password", + "8175": "aes_password", + "7704": "aes_password", + "8083": "aes_password", + "7206": "aes_password", + "8054": "aes_password", + "7160": "aes_password", + "8727": "aes_password", + "7333": "aes_password", + "7094": "aes_password", + "8753": "aes_password", + "7755": "aes_password", + "8577": "aes_password", + "7099": "aes_password", + "7334": "aes_password", + "8055": "aes_password", + "8397": "aes_password", + "8214": "aes_password", + "8366": "aes_password", + "8541": "aes_password", + "7312": "aes_password", + "8283": "aes_password", + "8052": "aes_password", + "7653": "aes_password", + "7402": "aes_password", + "7224": "aes_password", + "7217": "aes_password", + "7126": "aes_password", + "7710": "aes_password", + "8668": "aes_password", + "8401": "aes_password", + "8628": "aes_password", + "8424": "aes_password", + "7658": "aes_password", + "7471": "aes_password", + "8813": "aes_password", + "7374": "aes_password", + "8067": "aes_password", + "8703": "aes_password", + "7681": "aes_password", + "7382": "aes_password", + "8538": "aes_password", + "8676": "aes_password", + "7033": "aes_password", + "7480": "aes_password", + "8841": "aes_password", + "8834": "aes_password", + "8666": "aes_password", + "8084": "aes_password", + "7925": "aes_password", + "7742": "aes_password", + "8669": "aes_password", + "7532": "aes_password", + "8657": "aes_password", + "8358": "aes_password", + "8590": "aes_password", + "7746": "aes_password", + "7605": "aes_password", + "7905": "aes_password", + "8294": "aes_password", + "8980": "aes_password", + "8735": "aes_password", + "8179": "aes_password", + "8760": "aes_password", + "7036": "aes_password", + "7404": "aes_password", + "8062": "aes_password", + "8201": "aes_password", + "7320": "aes_password", + "8964": "aes_password", + "8402": "aes_password", + "7303": "aes_password", + "8765": "aes_password", + "7803": "aes_password", + "8247": "aes_password", + "8449": "aes_password", + "7278": "aes_password", + "7189": "aes_password", + "8284": "aes_password", + "8848": "aes_password", + "7809": "aes_password", + "7503": "aes_password", + "8419": "aes_password", + "7723": "aes_password", + "7446": "aes_password", + "7556": "aes_password", + "8739": "aes_password", + "8514": "aes_password", + "7486": "aes_password", + "7143": "aes_password", + "8709": "aes_password", + "7105": "aes_password", + "7654": "aes_password", + "8436": "aes_password", + "8761": "aes_password", + "7595": "aes_password", + "8209": "aes_password", + "8029": "aes_password", + "7603": "aes_password", + "8257": "aes_password", + "8649": "aes_password", + "7370": "aes_password", + "8534": "aes_password", + "7267": "aes_password", + "8565": "aes_password", + "7664": "aes_password", + "8551": "aes_password", + "7888": "aes_password", + "8576": "aes_password", + "8893": "aes_password", + "8857": "aes_password", + "7599": "aes_password", + "7568": "aes_password", + "7791": "aes_password", + "7916": "aes_password", + "8885": "aes_password", + "7418": "aes_password", + "7245": "aes_password", + "8170": "aes_password", + "8466": "aes_password", + "7098": "aes_password", + "7489": "aes_password", + "8997": "aes_password", + "8014": "aes_password", + "8805": "aes_password", + "7774": "aes_password", + "8325": "aes_password", + "8262": "aes_password", + "8038": "aes_password", + "8512": "aes_password", + "7546": "aes_password", + "7614": "aes_password", + "8309": "aes_password", + "7123": "aes_password", + "8324": "aes_password", + "8591": "aes_password", + "8255": "aes_password", + "7623": "aes_password", + "8270": "aes_password", + "8715": "aes_password", + "8515": "aes_password", + "8088": "aes_password", + "8375": "aes_password", + "7693": "aes_password", + "8752": "aes_password", + "8059": "aes_password", + "7271": "aes_password", + "8917": "aes_password", + "7500": "aes_password", + "8656": "aes_password", + "7022": "aes_password", + "8812": "aes_password", + "8858": "aes_password", + "7980": "aes_password", + "7090": "aes_password", + "7637": "aes_password", + "7671": "aes_password", + "7904": "aes_password", + "8629": "aes_password", + "7034": "aes_password", + "7207": "aes_password", + "7096": "aes_password", + "7621": "aes_password", + "7770": "aes_password", + "8221": "aes_password", + "7077": "aes_password", + "7661": "aes_password", + "7994": "aes_password", + "7632": "aes_password", + "7848": "aes_password", + "8822": "aes_password", + "7328": "aes_password", + "8438": "aes_password", + "8939": "aes_password", + "8636": "aes_password", + "7995": "aes_password", + "8363": "aes_password", + "7010": "aes_password", + "7212": "aes_password", + "8559": "aes_password", + "8548": "aes_password", + "7275": "aes_password", + "8819": "aes_password", + "7927": "aes_password", + "8291": "aes_password", + "7652": "aes_password", + "7954": "aes_password", + "7832": "aes_password", + "8087": "aes_password", + "8646": "aes_password", + "7736": "aes_password", + "7581": "aes_password", + "7216": "aes_password", + "7190": "aes_password", + "7180": "aes_password", + "7412": "aes_password", + "8569": "aes_password", + "8010": "aes_password", + "7529": "aes_password", + "8118": "aes_password", + "8162": "aes_password", + "8925": "aes_password", + "8490": "aes_password", + "7762": "aes_password", + "7610": "aes_password", + "8439": "aes_password", + "7698": "aes_password", + "7648": "aes_password", + "8742": "aes_password", + "8993": "aes_password", + "7785": "aes_password", + "7609": "aes_password", + "7896": "aes_password", + "8851": "aes_password", + "7731": "aes_password", + "8248": "aes_password", + "8386": "aes_password", + "8621": "aes_password", + "7349": "aes_password", + "7366": "aes_password", + "7169": "aes_password", + "7942": "aes_password", + "7633": "aes_password", + "7781": "aes_password", + "8387": "aes_password", + "8117": "aes_password", + "8782": "aes_password", + "8192": "aes_password", + "8104": "aes_password", + "7615": "aes_password", + "7050": "aes_password", + "7315": "aes_password", + "7725": "aes_password", + "7058": "aes_password", + "7865": "aes_password", + "7133": "aes_password", + "8458": "aes_password", + "7552": "aes_password", + "7761": "aes_password", + "8229": "aes_password", + "7677": "aes_password", + "8619": "aes_password", + "8193": "aes_password", + "8164": "aes_password", + "8493": "aes_password", + "7943": "aes_password", + "8476": "aes_password", + "8427": "aes_password", + "7240": "aes_password", + "8773": "aes_password", + "8749": "aes_password", + "8983": "aes_password", + "8910": "aes_password", + "7722": "aes_password", + "8092": "aes_password", + "8086": "aes_password", + "7911": "aes_password", + "8793": "aes_password", + "8725": "aes_password", + "7201": "aes_password", + "7538": "aes_password", + "7016": "aes_password", + "8396": "aes_password", + "7726": "aes_password", + "8495": "aes_password", + "8300": "aes_password", + "7948": "aes_password", + "8237": "aes_password", + "8802": "aes_password", + "7074": "aes_password", + "8077": "aes_password", + "7516": "aes_password", + "8908": "aes_password", + "8616": "aes_password", + "7814": "aes_password", + "8688": "aes_password", + "7236": "aes_password", + "7537": "aes_password", + "8691": "aes_password", + "8109": "aes_password", + "8223": "aes_password", + "7895": "aes_password", + "8018": "aes_password", + "7820": "aes_password", + "7407": "aes_password", + "8544": "aes_password", + "7651": "aes_password", + "8288": "aes_password", + "7930": "aes_password", + "8004": "aes_password", + "8935": "aes_password", + "7272": "aes_password", + "8707": "aes_password", + "8907": "aes_password", + "8146": "aes_password", + "7850": "aes_password", + "8579": "aes_password", + "7125": "aes_password", + "8692": "aes_password", + "7828": "aes_password", + "7113": "aes_password", + "7027": "aes_password", + "8148": "aes_password", + "7078": "aes_password", + "7508": "aes_password", + "8434": "aes_password", + "8374": "aes_password", + "8356": "aes_password", + "8942": "aes_password", + "8437": "aes_password", + "8902": "aes_password", + "7887": "aes_password", + "7281": "aes_password", + "8593": "aes_password", + "7853": "aes_password", + "8898": "aes_password", + "8927": "aes_password", + "8184": "aes_password", + "7879": "aes_password", + "8409": "aes_password", + "8241": "aes_password", + "7013": "aes_password", + "8145": "aes_password", + "8094": "aes_password", + "8450": "aes_password", + "7353": "aes_password", + "7286": "aes_password", + "7393": "aes_password", + "8852": "aes_password", + "8456": "aes_password", + "8660": "aes_password", + "7908": "aes_password", + "7408": "aes_password", + "8784": "aes_password", + "8990": "aes_password", + "8899": "aes_password", + "8748": "aes_password", + "8385": "aes_password", + "7151": "aes_password", + "7355": "aes_password", + "8051": "aes_password", + "7842": "aes_password", + "7985": "aes_password", + "7081": "aes_password", + "8620": "aes_password", + "7924": "aes_password", + "7305": "aes_password", + "8949": "aes_password", + "7091": "aes_password", + "7947": "aes_password", + "8075": "aes_password", + "8721": "aes_password", + "8605": "aes_password", + "8305": "aes_password", + "7185": "aes_password", + "7831": "aes_password", + "8672": "aes_password", + "8253": "aes_password", + "8274": "aes_password", + "8746": "aes_password", + "8738": "aes_password", + "8015": "aes_password", + "8007": "aes_password", + "8768": "aes_password", + "7492": "aes_password", + "7752": "aes_password", + "8391": "aes_password", + "7596": "aes_password", + "7797": "aes_password", + "8940": "aes_password", + "8304": "aes_password", + "7706": "aes_password", + "7060": "aes_password", + "7251": "aes_password", + "7472": "aes_password", + "7340": "aes_password", + "7607": "aes_password", + "7215": "aes_password", + "7136": "aes_password", + "8171": "aes_password", + "8740": "aes_password", + "8343": "aes_password", + "7024": "aes_password", + "7635": "aes_password", + "8689": "aes_password", + "7158": "aes_password", + "8320": "aes_password", + "8516": "aes_password", + "8238": "aes_password", + "8156": "aes_password", + "8149": "aes_password", + "8199": "aes_password", + "7822": "aes_password", + "7517": "aes_password", + "8191": "aes_password" + }, + "method": "aes-256-cfb", + "timeout": 60 +} \ No newline at end of file diff --git a/tests/test.py b/tests/test.py index 933027b..29b57d4 100755 --- a/tests/test.py +++ b/tests/test.py @@ -1,25 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright (c) 2014 clowwindy # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright 2015 clowwindy # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from __future__ import absolute_import, division, print_function, \ with_statement @@ -34,6 +28,8 @@ from subprocess import Popen, PIPE python = ['python'] +default_url = 'http://localhost/' + parser = argparse.ArgumentParser(description='test Shadowsocks') parser.add_argument('-c', '--client-conf', type=str, default=None) parser.add_argument('-s', '--server-conf', type=str, default=None) @@ -41,7 +37,8 @@ parser.add_argument('-a', '--client-args', type=str, default=None) parser.add_argument('-b', '--server-args', type=str, default=None) parser.add_argument('--with-coverage', action='store_true', default=None) parser.add_argument('--should-fail', action='store_true', default=None) -parser.add_argument('--url', type=str, default='http://www.example.com/') +parser.add_argument('--tcp-only', action='store_true', default=None) +parser.add_argument('--url', type=str, default=default_url) parser.add_argument('--dns', type=str, default='8.8.8.8') config = parser.parse_args() @@ -64,6 +61,8 @@ if config.client_args: server_args.extend(config.server_args.split()) else: server_args.extend(config.client_args.split()) +if config.url == default_url: + server_args.extend(['--forbidden-ip', '']) p1 = Popen(server_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) p2 = Popen(client_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) @@ -127,6 +126,8 @@ try: else: if r != 0: sys.exit(1) + if config.tcp_only: + break p4 = Popen(['socksify', 'dig', '@%s' % config.dns, 'www.google.com'], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) diff --git a/tests/test_command.sh b/tests/test_command.sh index eba4c8c..be05704 100755 --- a/tests/test_command.sh +++ b/tests/test_command.sh @@ -6,6 +6,9 @@ PYTHON="coverage run -a -p" LOCAL="$PYTHON shadowsocks/local.py" SERVER="$PYTHON shadowsocks/server.py" +assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" +assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" + assert "$LOCAL 2>&1 | grep ERROR" "ERROR: config not specified" assert "$LOCAL 2>&1 | grep usage | cut -d: -f1" "usage" @@ -30,11 +33,13 @@ $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d sto assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop -assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": password not specified" +assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password not specified" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop -assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": password or port_password not specified" +assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password or port_password not specified" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop +assert "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": Not a valid CIDR notation: 127.0.0.1/4a" +$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop assert_end command diff --git a/tests/test_graceful_restart.sh b/tests/test_graceful_restart.sh new file mode 100755 index 0000000..d91ba92 --- /dev/null +++ b/tests/test_graceful_restart.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +PYTHON="coverage run -p -a" +URL=http://127.0.0.1/file + + +# setup processes +$PYTHON shadowsocks/local.py -c tests/graceful.json & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & +SERVER=$! + +python tests/graceful_server.py & +GSERVER=$! + +sleep 1 + +python tests/graceful_cli.py & +GCLI=$! + +sleep 1 + +# graceful restart server: send SIGQUIT to old process and start a new one +kill -s SIGQUIT $SERVER +sleep 0.5 +$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" & +NEWSERVER=$! + +sleep 1 + +# check old server +ps x | grep -v grep | grep $SERVER +OLD_SERVER_RUNNING1=$? +# old server should not quit at this moment +echo old server running: $OLD_SERVER_RUNNING1 + +sleep 1 + +# close connections on old server +kill -s SIGKILL $GCLI +kill -s SIGKILL $GSERVER +kill -s SIGINT $LOCAL + +sleep 11 + +# check old server +ps x | grep -v grep | grep $SERVER +OLD_SERVER_RUNNING2=$? +# old server should quit at this moment +echo old server running: $OLD_SERVER_RUNNING2 + +kill -s SIGINT $SERVER +# new server is expected running +kill -s SIGINT $NEWSERVER || exit 1 + +if [ $OLD_SERVER_RUNNING1 -ne 0 ]; then + exit 1 +fi + +if [ $OLD_SERVER_RUNNING2 -ne 1 ]; then + sleep 1 + exit 1 +fi diff --git a/tests/test_large_file.sh b/tests/test_large_file.sh index e8acd79..33bcb59 100755 --- a/tests/test_large_file.sh +++ b/tests/test_large_file.sh @@ -8,7 +8,7 @@ mkdir -p tmp $PYTHON shadowsocks/local.py -c tests/aes.json & LOCAL=$! -$PYTHON shadowsocks/server.py -c tests/aes.json & +$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" & SERVER=$! sleep 3 diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py new file mode 100644 index 0000000..e8fa505 --- /dev/null +++ b/tests/test_udp_src.py @@ -0,0 +1,83 @@ +#!/usr/bin/python + +import socket +import socks + + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 1081 + + +if __name__ == '__main__': + # Test 1: same source port IPv4 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('127.0.0.1', 9001)) + sock_in2.bind(('127.0.0.1', 9002)) + + sock_out.sendto(b'data', ('127.0.0.1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('127.0.0.1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # make sure they're from the same source port + assert result1 == result2 + + # Test 2: same source port IPv6 + # try again from the same port but IPv6 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('::1', 9001)) + sock_in2.bind(('::1', 9002)) + + sock_out.sendto(b'data', ('::1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('::1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # make sure they're from the same source port + assert result1 == result2 + + # Test 3: different source ports IPv6 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9003)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in1.bind(('::1', 9001)) + sock_out.sendto(b'data', ('::1', 9001)) + result3 = sock_in1.recvfrom(8) + + # make sure they're from different source ports + assert result1 != result3 + + sock_out.close() + sock_in1.close() diff --git a/tests/test_udp_src.sh b/tests/test_udp_src.sh new file mode 100755 index 0000000..d356581 --- /dev/null +++ b/tests/test_udp_src.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PYTHON="coverage run -p -a" + +mkdir -p tmp + +$PYTHON shadowsocks/local.py -c tests/aes.json -v & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & +SERVER=$! + +sleep 3 + +python tests/test_udp_src.py +r=$? + +kill -s SIGINT $LOCAL +kill -s SIGINT $SERVER + +sleep 2 + +exit $r diff --git a/utils/autoban.py b/utils/autoban.py index d04ebbe..1bbb65c 100755 --- a/utils/autoban.py +++ b/utils/autoban.py @@ -42,10 +42,12 @@ if __name__ == '__main__': if ip not in ips: ips[ip] = 1 print(ip) + sys.stdout.flush() else: ips[ip] += 1 if ip not in banned and ips[ip] >= config.count: banned.add(ip) cmd = 'iptables -A INPUT -s %s -j DROP' % ip print(cmd, file=sys.stderr) + sys.stderr.flush() os.system(cmd) diff --git a/utils/fail2ban/shadowsocks.conf b/utils/fail2ban/shadowsocks.conf new file mode 100644 index 0000000..9b1c7ec --- /dev/null +++ b/utils/fail2ban/shadowsocks.conf @@ -0,0 +1,5 @@ +[Definition] + +_daemon = shadowsocks + +failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$