diff --git a/.gitignore b/.gitignore index 6c1b61e..e6f83bd 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,17 @@ htmlcov .DS_Store .idea +tags + +#Emacs +.#* +venv/ + +# VS-code +.vscode/ + +# Pycharm +.idea + +#ss +config.json diff --git a/.travis.yml b/.travis.yml index f29cb96..f11e179 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,14 @@ cache: 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 dd if=/dev/urandom of=/usr/share/nginx/html/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/libmbedtls/install.sh + - sudo tests/libopenssl/install.sh - sudo tests/setup_tc.sh script: - tests/jenkins.sh diff --git a/CHANGES b/CHANGES index 4db142a..d6fe932 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,18 @@ +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbdb9c1..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 ------------- diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..510d422 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:14.04 + +RUN apt-get update && apt-get install -y \ + python-software-properties \ + software-properties-common \ + && add-apt-repository ppa:chris-lea/libsodium \ + && echo "deb http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \ + && echo "deb-src http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y libsodium-dev python-pip + +RUN pip install shadowsocks + +ENTRYPOINT ["/usr/local/bin/ssserver"] + +# usage: +# docker run -d --restart=always -p 1314:1314 ficapy/shadowsocks -s 0.0.0.0 -p 1314 -k $PD -m chacha20 diff --git a/README.md b/README.md index 76d759a..d87232a 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,16 @@ shadowsocks [![PyPI version]][PyPI] [![Build Status]][Travis CI] -[![Coverage Status]][Coverage] A fast tunnel proxy that helps you bypass firewalls. +Features: +- TCP & UDP support +- User management API +- TCP Fast Open +- Workers and graceful restart +- Destination IP blacklist + Server ------ @@ -15,16 +21,25 @@ Server Debian / Ubuntu: apt-get install python-pip - pip install shadowsocks + pip install git+https://github.com/shadowsocks/shadowsocks.git@master CentOS: yum install python-setuptools && easy_install pip - pip install shadowsocks + pip install git+https://github.com/shadowsocks/shadowsocks.git@master + +For CentOS 7, if you need AEAD ciphers, you need install libsodium +``` +dnf install libsodium python34-pip +pip3 install git+https://github.com/shadowsocks/shadowsocks.git@master +``` +Linux distributions with [snap](http://snapcraft.io/): + + snap install shadowsocks Windows: -See [Install Server on Windows] +See [Install Shadowsocks Server on Windows](https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows). ### Usage @@ -45,62 +60,38 @@ To check the log: Check all the options via `-h`. You can also use a [Configuration] file instead. -Client ------- +If you installed the [snap](http://snapcraft.io/) package, you have to prefix the commands with `shadowsocks.`, +like this: -* [Windows] / [OS X] -* [Android] / [iOS] -* [OpenWRT] + shadowsocks.ssserver -p 443 -k password -m aes-256-cfb + +### Usage with Config File + +[Create configuration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File) + +To start: + + ssserver -c /etc/shadowsocks.json -Use GUI clients on your local PC/phones. Check the README of your client -for more information. Documentation ------------- -You can find all the documentation in the [Wiki]. +You can find all the documentation in the [Wiki](https://github.com/shadowsocks/shadowsocks/wiki). License ------- -Copyright 2015 clowwindy - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. - -Bugs and Issues ----------------- - -* [Troubleshooting] -* [Issue Tracker] -* [Mailing list] +Apache License + + + + -[Android]: https://github.com/shadowsocks/shadowsocks-android [Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat -[Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File -[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks -[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html -[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks -[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help -[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open -[Install Server on Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows -[Mailing list]: https://groups.google.com/group/shadowsocks -[OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks -[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help [PyPI]: https://pypi.python.org/pypi/shadowsocks [PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat [Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks -[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting -[Wiki]: https://github.com/shadowsocks/shadowsocks/wiki -[Windows]: https://github.com/shadowsocks/shadowsocks-csharp + diff --git a/README.rst b/README.rst index bf2a3ec..fe6fd51 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,8 @@ +About shadowsocks-rm +--------------- + +This project is https://github.com/shadowsocks/shadowsocks clone. I JUST fix bug on the original code. Unless it is necessary to have additional features. + shadowsocks =========== diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..a1f9125 --- /dev/null +++ b/config.json.example @@ -0,0 +1,17 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1080, + "password":"password", + "timeout":600, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false, + "tunnel_remote":"8.8.8.8", + "dns_server":["8.8.8.8", "8.8.4.4"], + "tunnel_remote_port":53, + "tunnel_port":53, + "libopenssl":"C:\\Program Files\\Git\\mingw64\\bin\\libeay32.dll", + "libsodium":"/usr/local/lib/libsodium.so", + "libmbedtls":"/usr/local/lib/libmbedcrypto.2.4.0.dylib" +} diff --git a/debian/changelog b/debian/changelog index 4e7ad16..2bf239c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,26 @@ +shadowsocks (2.9.0-2) unstable; urgency=medium + + [ Shell.Xu ] + * Fix compatible issue (Closes: #845016) + + -- Shell.Xu Sun, 20 Nov 2016 14:33:31 +0800 + +shadowsocks (2.9.0-1) unstable; urgency=medium + + [ Shell Xu ] + * Upstream update (Closes: #824640) + * Remove reference not exists (Closes: #810688) + + [ Thomas Goirand ] + * Added lsb-base as Depends:. + * Removed Pre-Depends: dpkg (>= 1.15.6~). + * Ran wrap-and-sort -t -a. + * Fixed VCS URLs to use https. + * Removed useless obsolete version of python-all build-depends. + * Fixed debian/copyright ordering. + + -- Shell.Xu Sat, 01 Oct 2016 16:14:47 +0800 + shadowsocks (2.1.0-1) unstable; urgency=low * Initial release (Closes: #758900) diff --git a/debian/config.json b/debian/config.json index 35cb14a..7846cb2 100644 --- a/debian/config.json +++ b/debian/config.json @@ -7,5 +7,6 @@ "timeout":300, "method":"aes-256-cfb", "fast_open": false, - "workers": 1 + "workers": 1, + "prefer_ipv6": false } \ No newline at end of file diff --git a/debian/control b/debian/control index da00920..fa0b1ec 100644 --- a/debian/control +++ b/debian/control @@ -2,18 +2,23 @@ Source: shadowsocks Section: python Priority: extra Maintainer: Shell.Xu -Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools -Standards-Version: 3.9.5 -Homepage: https://github.com/clowwindy/shadowsocks -Vcs-Git: git://github.com/shell909090/shadowsocks.git -Vcs-Browser: http://github.com/shell909090/shadowsocks +Build-Depends: debhelper (>= 8), + python-all, + python-setuptools, +Standards-Version: 3.9.8 +Homepage: https://github.com/shadowsocks/shadowsocks +Vcs-Git: https://github.com/shell909090/shadowsocks.git +Vcs-Browser: https://github.com/shell909090/shadowsocks Package: shadowsocks Architecture: all -Pre-Depends: dpkg (>= 1.15.6~) -Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto +Depends: lsb-base (>= 3.0-6), + python-m2crypto, + python-pkg-resources, + ${misc:Depends}, + ${python:Depends}, Description: Fast tunnel proxy that helps you bypass firewalls A secure socks5 proxy, designed to protect your Internet traffic. . This package contain local and server part of shadowsocks, a fast, - powerful tunnel proxy to bypass firewalls. \ No newline at end of file + powerful tunnel proxy to bypass firewalls. diff --git a/debian/copyright b/debian/copyright index 7be8162..14f0851 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,30 +1,27 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: shadowsocks -Source: https://github.com/clowwindy/shadowsocks - -Files: debian/* -Copyright: 2014 Shell.Xu -License: Expat +Source: https://github.com/shadowsocks/shadowsocks Files: * Copyright: 2014 clowwindy -License: Expat +License: Apache-2.0 -License: Expat - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. +Files: debian/* +Copyright: 2016 Shell.Xu +License: Apache-2.0 + +License: Apache-2.0 + 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. + . + On Debian systems, the complete text of the Apache License 2.0 can + be found in "/usr/share/common-licenses/Apache-2.0" diff --git a/debian/install b/debian/install index a614864..a16a450 100644 --- a/debian/install +++ b/debian/install @@ -1 +1 @@ -debian/config.json etc/shadowsocks/ \ No newline at end of file +debian/config.json etc/shadowsocks/ diff --git a/debian/shadowsocks.manpages b/debian/shadowsocks.manpages index 3df8a33..98b6847 100644 --- a/debian/shadowsocks.manpages +++ b/debian/shadowsocks.manpages @@ -1,2 +1,2 @@ debian/sslocal.1 -debian/ssserver.1 \ No newline at end of file +debian/ssserver.1 diff --git a/debian/sslocal.1 b/debian/sslocal.1 index 0c2cf51..05ef510 100644 --- a/debian/sslocal.1 +++ b/debian/sslocal.1 @@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors. The programs are documented fully by .IR "Shell Xu " and -.IR "Clowwindy ", -available via the Info system. +.IR "Clowwindy " +. diff --git a/debian/ssserver.1 b/debian/ssserver.1 index 0c2cf51..05ef510 100644 --- a/debian/ssserver.1 +++ b/debian/ssserver.1 @@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors. The programs are documented fully by .IR "Shell Xu " and -.IR "Clowwindy ", -available via the Info system. +.IR "Clowwindy " +. diff --git a/setup.py b/setup.py index 38def84..f41f481 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.6.10", + version="3.0.0", license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 7e4a4ed..fa5be41 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -18,7 +18,6 @@ from __future__ import absolute_import, division, print_function, \ with_statement -import time import os import socket import struct @@ -30,7 +29,7 @@ from shadowsocks import common, lru_cache, eventloop, shell CACHE_SWEEP_INTERVAL = 30 -VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(?= 2: - server = parts[1] - if common.is_ip(server) == socket.AF_INET: - if type(server) != str: - server = server.decode('utf8') - self._servers.append(server) + if not (line and line.startswith(b'nameserver')): + continue + + parts = line.split() + if len(parts) < 2: + continue + + server = parts[1] + if common.is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append(server) except IOError: pass if not self._servers: @@ -294,17 +302,21 @@ class DNSResolver(object): for line in f.readlines(): line = line.strip() parts = line.split() - if len(parts) >= 2: - ip = parts[0] - if common.is_ip(ip): - for i in range(1, len(parts)): - hostname = parts[i] - if hostname: - self._hosts[hostname] = ip + if len(parts) < 2: + continue + + ip = parts[0] + if not common.is_ip(ip): + continue + + for i in range(1, len(parts)): + hostname = parts[i] + if hostname: + self._hosts[hostname] = ip except IOError: self._hosts['localhost'] = '127.0.0.1' - def add_to_loop(self, loop, ref=False): + def add_to_loop(self, loop): if self._loop: raise Exception('already add to loop') self._loop = loop @@ -312,8 +324,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, []) @@ -340,44 +352,42 @@ class DNSResolver(object): answer[2] == QCLASS_IN: ip = answer[0] break - if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ - == STATUS_IPV4: - self._hostname_status[hostname] = STATUS_IPV6 - self._send_req(hostname, QTYPE_AAAA) + if not ip and self._hostname_status.get(hostname, STATUS_SECOND) \ + == STATUS_FIRST: + self._hostname_status[hostname] = STATUS_SECOND + self._send_req(hostname, self._QTYPES[1]) else: if ip: self._cache[hostname] = ip self._call_callback(hostname, ip) - elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + elif self._hostname_status.get(hostname, None) \ + == STATUS_SECOND: for question in response.questions: - if question[1] == QTYPE_AAAA: + if question[1] == self._QTYPES[1]: 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) @@ -419,17 +429,20 @@ class DNSResolver(object): return arr = self._hostname_to_cb.get(hostname, None) if not arr: - self._hostname_status[hostname] = STATUS_IPV4 - self._send_req(hostname, QTYPE_A) + self._hostname_status[hostname] = STATUS_FIRST + self._send_req(hostname, self._QTYPES[0]) self._hostname_to_cb[hostname] = [callback] self._cb_to_hostname[callback] = hostname else: arr.append(callback) # TODO send again only if waited too long - self._send_req(hostname, QTYPE_A) + self._send_req(hostname, self._QTYPES[0]) 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 @@ -437,7 +450,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 @@ -451,8 +464,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 fc03d55..1a58457 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -21,6 +21,25 @@ from __future__ import absolute_import, division, print_function, \ import socket import struct import logging +import hashlib +import hmac + + +ONETIMEAUTH_BYTES = 10 +ONETIMEAUTH_CHUNK_BYTES = 12 +ONETIMEAUTH_CHUNK_DATA_LEN = 2 + + +def sha1_hmac(secret, data): + return hmac.new(secret, data, hashlib.sha1).digest() + + +def onetimeauth_verify(_hash, data, key): + return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] + + +def onetimeauth_gen(data, key): + return sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] def compat_ord(s): @@ -118,13 +137,16 @@ def patch_socket(): patch_socket() -ADDRTYPE_IPV4 = 1 -ADDRTYPE_IPV6 = 4 -ADDRTYPE_HOST = 3 +ADDRTYPE_IPV4 = 0x01 +ADDRTYPE_IPV6 = 0x04 +ADDRTYPE_HOST = 0x03 +ADDRTYPE_AUTH = 0x10 +ADDRTYPE_MASK = 0xF def pack_addr(address): address_str = to_str(address) + address = to_bytes(address) for family in (socket.AF_INET, socket.AF_INET6): try: r = socket.inet_pton(family, address_str) @@ -139,31 +161,38 @@ def pack_addr(address): return b'\x03' + chr(len(address)) + address +# add ss header +def add_header(address, port, data=b''): + _data = b'' + _data = pack_addr(address) + struct.pack('>H', port) + data + return _data + + def parse_header(data): addrtype = ord(data[0]) dest_addr = None dest_port = None header_length = 0 - if addrtype == ADDRTYPE_IPV4: + if addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV4: if len(data) >= 7: dest_addr = socket.inet_ntoa(data[1:5]) dest_port = struct.unpack('>H', data[5:7])[0] header_length = 7 else: logging.warn('header is too short') - elif addrtype == ADDRTYPE_HOST: + elif addrtype & ADDRTYPE_MASK == ADDRTYPE_HOST: if len(data) > 2: addrlen = ord(data[1]) - if len(data) >= 2 + addrlen: + if len(data) >= 4 + 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') else: logging.warn('header is too short') - elif addrtype == ADDRTYPE_IPV6: + elif addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV6: if len(data) >= 19: dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) dest_port = struct.unpack('>H', data[17:19])[0] diff --git a/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py new file mode 100644 index 0000000..c7240b3 --- /dev/null +++ b/shadowsocks/crypto/aead.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Void Copyright NO ONE +# +# Void License +# +# The code belongs to no one. Do whatever you want. +# Forget about boring open source license. +# +# AEAD cipher for shadowsocks +# + +from __future__ import absolute_import, division, print_function, \ + with_statement + +from ctypes import c_int, create_string_buffer, byref, c_void_p + +import hashlib +from struct import pack, unpack + +from shadowsocks.crypto import util +from shadowsocks.crypto import hkdf +from shadowsocks.common import ord, chr + + +EVP_CTRL_GCM_SET_IVLEN = 0x9 +EVP_CTRL_GCM_GET_TAG = 0x10 +EVP_CTRL_GCM_SET_TAG = 0x11 +EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN +EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG +EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG + +EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN +EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG +EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG + +AEAD_MSG_LEN_UNKNOWN = 0 +AEAD_CHUNK_SIZE_LEN = 2 +AEAD_CHUNK_SIZE_MASK = 0x3FFF + +CIPHER_NONCE_LEN = { + 'aes-128-gcm': 12, + 'aes-192-gcm': 12, + 'aes-256-gcm': 12, + 'aes-128-ocb': 12, # requires openssl 1.1 + 'aes-192-ocb': 12, + 'aes-256-ocb': 12, + 'chacha20-poly1305': 12, + 'chacha20-ietf-poly1305': 12, + 'xchacha20-ietf-poly1305': 24, + 'sodium:aes-256-gcm': 12, +} + +CIPHER_TAG_LEN = { + 'aes-128-gcm': 16, + 'aes-192-gcm': 16, + 'aes-256-gcm': 16, + 'aes-128-ocb': 16, # requires openssl 1.1 + 'aes-192-ocb': 16, + 'aes-256-ocb': 16, + 'chacha20-poly1305': 16, + 'chacha20-ietf-poly1305': 16, + 'xchacha20-ietf-poly1305': 16, + 'sodium:aes-256-gcm': 16, +} + +SUBKEY_INFO = b"ss-subkey" + +libsodium = None +sodium_loaded = False + + +def load_sodium(path=None): + """ + Load libsodium helpers for nonce increment + :return: None + """ + global libsodium, sodium_loaded + + libsodium = util.find_library('sodium', 'sodium_increment', + 'libsodium', path) + if libsodium is None: + print('load libsodium failed with path %s' % path) + return + + if libsodium.sodium_init() < 0: + libsodium = None + print('sodium init failed') + return + + libsodium.sodium_increment.restype = c_void_p + libsodium.sodium_increment.argtypes = ( + c_void_p, c_int + ) + + sodium_loaded = True + return + + +def nonce_increment(nonce, nlen): + """ + Increase nonce by 1 in little endian + From libsodium sodium_increment(): + for (; i < nlen; i++) { + c += (uint_fast16_t) n[i]; + n[i] = (unsigned char) c; + c >>= 8; + } + :param nonce: string_buffer nonce + :param nlen: nonce length + :return: nonce plus by 1 + """ + c = 1 + i = 0 + # n = create_string_buffer(nlen) + while i < nlen: + c += ord(nonce[i]) + nonce[i] = chr(c & 0xFF) + c >>= 8 + i += 1 + return # n.raw + + +class AeadCryptoBase(object): + """ + Handles basic aead process of shadowsocks protocol + + TCP Chunk (after encryption, *ciphertext*) + +--------------+---------------+--------------+------------+ + | *DataLen* | DataLen_TAG | *Data* | Data_TAG | + +--------------+---------------+--------------+------------+ + | 2 | Fixed | Variable | Fixed | + +--------------+---------------+--------------+------------+ + + UDP (after encryption, *ciphertext*) + +--------+-----------+-----------+ + | NONCE | *Data* | Data_TAG | + +-------+-----------+-----------+ + | Fixed | Variable | Fixed | + +--------+-----------+-----------+ + """ + + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + self._op = int(op) + self._salt = iv + self._nlen = CIPHER_NONCE_LEN[cipher_name] + self._nonce = create_string_buffer(self._nlen) + self._tlen = CIPHER_TAG_LEN[cipher_name] + + crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1) + self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key)) + # _chunk['mlen']: + # -1, waiting data len header + # n, n > 0, waiting data + self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''} + + # load libsodium for nonce increment + if not sodium_loaded: + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('sodium', None) + load_sodium(path) + + def nonce_increment(self): + """ + AEAD ciphers need nonce to be unique per key + TODO: cache and check unique + :return: None + """ + global libsodium, sodium_loaded + if sodium_loaded: + libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) + else: + nonce_increment(self._nonce, self._nlen) + # print("".join("%02x" % ord(b) for b in self._nonce)) + + def cipher_ctx_init(self): + """ + Increase nonce to make it unique for the same key + :return: None + """ + self.nonce_increment() + + def aead_encrypt(self, data): + """ + Encrypt data with authenticate tag + + :param data: plain text + :return: str [payload][tag] cipher text with tag + """ + raise Exception("Must implement aead_encrypt method") + + def encrypt_chunk(self, data): + """ + Encrypt a chunk for TCP chunks + + :param data: str + :return: str [len][tag][payload][tag] + """ + plen = len(data) + # l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2 + + # network byte order + ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))] + if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen: + self.clean() + raise Exception("size length invalid") + + ctext.append(self.aead_encrypt(data)) + if len(ctext[1]) != plen + self._tlen: + self.clean() + raise Exception("data length invalid") + + return b''.join(ctext) + + def encrypt(self, data): + """ + Encrypt data, for TCP divided into chunks + For UDP data, call aead_encrypt instead + + :param data: str data bytes + :return: str encrypted data + """ + plen = len(data) + if plen <= AEAD_CHUNK_SIZE_MASK: + ctext = self.encrypt_chunk(data) + return ctext + ctext = [] + while plen > 0: + mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \ + else AEAD_CHUNK_SIZE_MASK + c = self.encrypt_chunk(data[:mlen]) + ctext.append(c) + data = data[mlen:] + plen -= mlen + + return b''.join(ctext) + + def aead_decrypt(self, data): + """ + Decrypt data and authenticate tag + + :param data: str [len][tag][payload][tag] cipher text with tag + :return: str plain text + """ + raise Exception("Must implement aead_decrypt method") + + def decrypt_chunk_size(self, data): + """ + Decrypt chunk size + + :param data: str [size][tag] encrypted chunk payload len + :return: (int, str) msg length and remaining encrypted data + """ + if self._chunk['mlen'] > 0: + return self._chunk['mlen'], data + data = self._chunk['data'] + data + self._chunk['data'] = b"" + + hlen = AEAD_CHUNK_SIZE_LEN + self._tlen + if hlen > len(data): + self._chunk['data'] = data + return 0, b"" + plen = self.aead_decrypt(data[:hlen]) + plen, = unpack("!H", plen) + if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0: + self.clean() + raise Exception('Invalid message length') + + return plen, data[hlen:] + + def decrypt_chunk_payload(self, plen, data): + """ + Decrypted encrypted msg payload + + :param plen: int payload length + :param data: str [payload][tag][[len][tag]....] encrypted data + :return: (str, str) plain text and remaining encrypted data + """ + data = self._chunk['data'] + data + if len(data) < plen + self._tlen: + self._chunk['mlen'] = plen + self._chunk['data'] = data + return b"", b"" + self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN + self._chunk['data'] = b"" + + plaintext = self.aead_decrypt(data[:plen + self._tlen]) + + if len(plaintext) != plen: + self.clean() + raise Exception("plaintext length invalid") + + return plaintext, data[plen + self._tlen:] + + def decrypt_chunk(self, data): + """ + Decrypt a TCP chunk + + :param data: str [len][tag][payload][tag][[len][tag]...] encrypted msg + :return: (str, str) decrypted msg and remaining encrypted data + """ + plen, data = self.decrypt_chunk_size(data) + if plen <= 0: + return b"", b"" + return self.decrypt_chunk_payload(plen, data) + + def decrypt(self, data): + """ + Decrypt data for TCP data divided into chunks + For UDP data, call aead_decrypt instead + + :param data: str + :return: str + """ + ptext = [] + pnext, left = self.decrypt_chunk(data) + ptext.append(pnext) + while len(left) > 0: + pnext, left = self.decrypt_chunk(left) + ptext.append(pnext) + return b''.join(ptext) + + +def test_nonce_increment(): + buf = create_string_buffer(12) + print("".join("%02x" % ord(b) for b in buf)) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + print("".join("%02x" % ord(b) for b in buf)) + for i in range(256): + nonce_increment(buf, 12) + print("".join("%02x" % ord(b) for b in buf)) + + +if __name__ == '__main__': + load_sodium() + test_nonce_increment() diff --git a/shadowsocks/crypto/hkdf.py b/shadowsocks/crypto/hkdf.py new file mode 100644 index 0000000..11998e6 --- /dev/null +++ b/shadowsocks/crypto/hkdf.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Void Copyright NO ONE +# +# Void License +# +# The code belongs to no one. Do whatever you want. +# Forget about boring open source license. +# +# HKDF for AEAD ciphers +# + +from __future__ import division + +import hmac +import hashlib +import sys + +if sys.version_info[0] == 3: + def buffer(x): + return x + + +def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256): + """ + Extract a pseudorandom key suitable for use with hkdf_expand + from the input_key_material and a salt using HMAC with the + provided hash (default SHA-256). + + salt should be a random, application-specific byte string. If + salt is None or the empty string, an all-zeros string of the same + length as the hash's block size will be used instead per the RFC. + + See the HKDF draft RFC and paper for usage notes. + """ + hash_len = algorithm().digest_size + if salt is None or len(salt) == 0: + salt = bytearray((0,) * hash_len) + return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\ + .digest() + + +def hkdf_expand(pseudo_random_key, info=b"", length=32, + algorithm=hashlib.sha256): + """ + Expand `pseudo_random_key` and `info` into a key of length `bytes` using + HKDF's expand function based on HMAC with the provided hash (default + SHA-256). See the HKDF draft RFC and paper for usage notes. + """ + hash_len = algorithm().digest_size + length = int(length) + if length > 255 * hash_len: + raise Exception("Cannot expand to more than 255 * %d = %d " + "bytes using the specified hash function" % + (hash_len, 255 * hash_len)) + blocks_needed = length // hash_len \ + + (0 if length % hash_len == 0 else 1) # ceil + okm = b"" + output_block = b"" + for counter in range(blocks_needed): + output_block = hmac.new( + pseudo_random_key, + buffer(output_block + info + bytearray((counter + 1,))), + algorithm + ).digest() + okm += output_block + return okm[:length] + + +class Hkdf(object): + """ + Wrapper class for HKDF extract and expand functions + """ + + def __init__(self, salt, input_key_material, algorithm=hashlib.sha256): + """ + Extract a pseudorandom key from `salt` and `input_key_material` + arguments. + + See the HKDF draft RFC for guidance on setting these values. + The constructor optionally takes a `algorithm` argument defining + the hash function use, defaulting to hashlib.sha256. + """ + self._hash = algorithm + self._prk = hkdf_extract(salt, input_key_material, self._hash) + + def expand(self, info, length=32): + """ + Generate output key material based on an `info` value + + Arguments: + - info - context to generate the OKM + - length - length in bytes of the key to generate + + See the HKDF draft RFC for guidance. + """ + return hkdf_expand(self._prk, info, length, self._hash) diff --git a/shadowsocks/crypto/mbedtls.py b/shadowsocks/crypto/mbedtls.py new file mode 100644 index 0000000..1954a86 --- /dev/null +++ b/shadowsocks/crypto/mbedtls.py @@ -0,0 +1,478 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Void Copyright NO ONE +# +# Void License +# +# The code belongs to no one. Do whatever you want. +# Forget about boring open source license. +# +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + + +from __future__ import absolute_import, division, print_function, \ + with_statement + +from ctypes import c_char_p, c_int, c_size_t, byref,\ + create_string_buffer, c_void_p + +from shadowsocks import common +from shadowsocks.crypto import util +from shadowsocks.crypto.aead import AeadCryptoBase + +__all__ = ['ciphers'] + +libmbedtls = None +loaded = False + +buf = None +buf_size = 2048 + +CIPHER_ENC_UNCHANGED = -1 + +# define MAX_KEY_LENGTH 64 +# define MAX_NONCE_LENGTH 32 +# typedef struct { +# uint32_t init; +# uint64_t counter; +# cipher_evp_t *evp; +# cipher_t *cipher; +# buffer_t *chunk; +# uint8_t salt[MAX_KEY_LENGTH]; +# uint8_t skey[MAX_KEY_LENGTH]; +# uint8_t nonce[MAX_NONCE_LENGTH]; +# } cipher_ctx_t; +# +# sizeof(cipher_ctx_t) = 196 + +CIPHER_CTX_SIZE = 256 + + +def load_mbedtls(crypto_path=None): + global loaded, libmbedtls, buf + + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('mbedtls', None) + libmbedtls = util.find_library('mbedcrypto', + 'mbedtls_cipher_init', + 'libmbedcrypto', path) + if libmbedtls is None: + raise Exception('libmbedcrypto(mbedtls) not found with path %s' + % path) + + libmbedtls.mbedtls_cipher_init.restype = None + libmbedtls.mbedtls_cipher_free.restype = None + + libmbedtls.mbedtls_cipher_info_from_string.restype = c_void_p + libmbedtls.mbedtls_cipher_info_from_string.argtypes = (c_char_p,) + + libmbedtls.mbedtls_cipher_setup.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_setup.argtypes = (c_void_p, c_void_p) + + libmbedtls.mbedtls_cipher_setkey.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_setkey.argtypes = ( + c_void_p, # ctx + c_char_p, # key + c_int, # key_bitlen, not bytes + c_int # op: 1 enc, 0 dec, -1 none + ) + + libmbedtls.mbedtls_cipher_set_iv.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_set_iv.argtypes = ( + c_void_p, # ctx + c_char_p, # iv + c_size_t # iv_len + ) + + libmbedtls.mbedtls_cipher_reset.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_reset.argtypes = (c_void_p,) # ctx + + if hasattr(libmbedtls, 'mbedtls_cipher_update_ad'): + libmbedtls.mbedtls_cipher_update_ad.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_update_ad.argtypes = ( + c_void_p, # ctx + c_char_p, # ad + c_size_t # ad_len + ) + + libmbedtls.mbedtls_cipher_update.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_update.argtypes = ( + c_void_p, # ctx + c_char_p, # input + c_size_t, # ilen, must be multiple of block size except last one + c_void_p, # *output + c_void_p # *olen + ) + + libmbedtls.mbedtls_cipher_finish.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_finish.argtypes = ( + c_void_p, # ctx + c_void_p, # *output + c_void_p # *olen + ) + + if hasattr(libmbedtls, 'mbedtls_cipher_write_tag'): + libmbedtls.mbedtls_cipher_write_tag.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_write_tag.argtypes = ( + c_void_p, # ctx + c_void_p, # *tag + c_size_t # tag_len + ) + libmbedtls.mbedtls_cipher_check_tag.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_check_tag.argtypes = ( + c_void_p, # ctx + c_char_p, # tag + c_size_t # tag_len + ) + + libmbedtls.mbedtls_cipher_crypt.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_crypt.argtypes = ( + c_void_p, # ctx + c_char_p, # iv + c_size_t, # iv_len, = 0 if iv = NULL + c_char_p, # input + c_size_t, # ilen + c_void_p, # *output, no less than ilen + block_size + c_void_p # *olen + ) + + if hasattr(libmbedtls, 'mbedtls_cipher_auth_encrypt'): + libmbedtls.mbedtls_cipher_auth_encrypt.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_auth_encrypt.argtypes = ( + c_void_p, # ctx + c_char_p, # iv + c_size_t, # iv_len + c_char_p, # ad + c_size_t, # ad_len + c_char_p, # input + c_size_t, # ilen + c_void_p, # *output, no less than ilen + block_size + c_void_p, # *olen + c_void_p, # *tag + c_size_t # tag_len + ) + libmbedtls.mbedtls_cipher_auth_decrypt.restype = c_int # 0 on success + libmbedtls.mbedtls_cipher_auth_decrypt.argtypes = ( + c_void_p, # ctx + c_char_p, # iv + c_size_t, # iv_len + c_char_p, # ad + c_size_t, # ad_len + c_char_p, # input + c_size_t, # ilen + c_void_p, # *output, no less than ilen + block_size + c_void_p, # *olen + c_char_p, # tag + c_size_t, # tag_len + ) + + buf = create_string_buffer(buf_size) + loaded = True + + +class MbedTLSCryptoBase(object): + """ + MbedTLS crypto base class + """ + def __init__(self, cipher_name, crypto_path=None): + global loaded + self._ctx = create_string_buffer(b'\0' * CIPHER_CTX_SIZE) + self._cipher = None + if not loaded: + load_mbedtls(crypto_path) + cipher_name = common.to_bytes(cipher_name.upper()) + cipher = libmbedtls.mbedtls_cipher_info_from_string(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libmbedtls' % cipher_name) + libmbedtls.mbedtls_cipher_init(byref(self._ctx)) + if libmbedtls.mbedtls_cipher_setup(byref(self._ctx), cipher): + raise Exception('can not setup cipher') + self._cipher = cipher + + self.encrypt_once = self.update + self.decrypt_once = self.update + + def update(self, data): + """ + Encrypt/decrypt data + :param data: str + :return: str + """ + global buf_size, buf + cipher_out_len = c_size_t(0) + l = len(data) + if buf_size < l: + buf_size = l * 2 + buf = create_string_buffer(buf_size) + libmbedtls.mbedtls_cipher_update( + byref(self._ctx), + c_char_p(data), c_size_t(l), + byref(buf), byref(cipher_out_len) + ) + # buf is copied to a str object when we access buf.raw + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libmbedtls.mbedtls_cipher_free(byref(self._ctx)) + + +class MbedTLSAeadCrypto(MbedTLSCryptoBase, AeadCryptoBase): + """ + Implement mbedtls Aead mode: gcm + """ + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + if cipher_name[:len('mbedtls:')] == 'mbedtls:': + cipher_name = cipher_name[len('mbedtls:'):] + MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path) + AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path) + + key_ptr = c_char_p(self._skey) + r = libmbedtls.mbedtls_cipher_setkey( + byref(self._ctx), + key_ptr, c_int(len(key) * 8), + c_int(op) + ) + if r: + self.clean() + raise Exception('can not initialize cipher context') + + r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx)) + if r: + self.clean() + raise Exception('can not finish preparation of mbed TLS ' + 'cipher context') + + def cipher_ctx_init(self): + """ + Nonce + 1 + :return: None + """ + AeadCryptoBase.nonce_increment(self) + + def set_tag(self, tag): + """ + Set tag before decrypt any data (update) + :param tag: authenticated tag + :return: None + """ + tag_len = self._tlen + r = libmbedtls.mbedtls_cipher_check_tag( + byref(self._ctx), + c_char_p(tag), c_size_t(tag_len) + ) + if not r: + raise Exception('Set tag failed') + + def get_tag(self): + """ + Get authenticated tag, called after EVP_CipherFinal_ex + :return: str + """ + tag_len = self._tlen + tag_buf = create_string_buffer(tag_len) + r = libmbedtls.mbedtls_cipher_write_tag( + byref(self._ctx), + byref(tag_buf), c_size_t(tag_len) + ) + if not r: + raise Exception('Get tag failed') + return tag_buf.raw[:tag_len] + + def final(self): + """ + Finish encrypt/decrypt a chunk (<= 0x3FFF) + :return: str + """ + global buf_size, buf + cipher_out_len = c_size_t(0) + r = libmbedtls.mbedtls_cipher_finish( + byref(self._ctx), + byref(buf), byref(cipher_out_len) + ) + if not r: + # print(self._nonce.raw, r, cipher_out_len) + raise Exception('Finalize cipher failed') + return buf.raw[:cipher_out_len.value] + + def aead_encrypt(self, data): + """ + Encrypt data with authenticate tag + + :param data: plain text + :return: cipher text with tag + """ + global buf_size, buf + plen = len(data) + if buf_size < plen + self._tlen: + buf_size = (plen + self._tlen) * 2 + buf = create_string_buffer(buf_size) + cipher_out_len = c_size_t(0) + tag_buf = create_string_buffer(self._tlen) + + r = libmbedtls.mbedtls_cipher_auth_encrypt( + byref(self._ctx), + c_char_p(self._nonce.raw), c_size_t(self._nlen), + None, c_size_t(0), + c_char_p(data), c_size_t(plen), + byref(buf), byref(cipher_out_len), + byref(tag_buf), c_size_t(self._tlen) + ) + assert cipher_out_len.value == plen + if r: + raise Exception('AEAD encrypt failed {0:#x}'.format(r)) + self.cipher_ctx_init() + return buf.raw[:cipher_out_len.value] + tag_buf.raw[:self._tlen] + + def aead_decrypt(self, data): + """ + Decrypt data and authenticate tag + + :param data: cipher text with tag + :return: plain text + """ + global buf_size, buf + cipher_out_len = c_size_t(0) + plen = len(data) - self._tlen + if buf_size < plen: + buf_size = plen * 2 + buf = create_string_buffer(buf_size) + tag = data[plen:] + r = libmbedtls.mbedtls_cipher_auth_decrypt( + byref(self._ctx), + c_char_p(self._nonce.raw), c_size_t(self._nlen), + None, c_size_t(0), + c_char_p(data), c_size_t(plen), + byref(buf), byref(cipher_out_len), + c_char_p(tag), c_size_t(self._tlen) + ) + if r: + raise Exception('AEAD encrypt failed {0:#x}'.format(r)) + self.cipher_ctx_init() + return buf.raw[:cipher_out_len.value] + + +class MbedTLSStreamCrypto(MbedTLSCryptoBase): + """ + Crypto for stream modes: cfb, ofb, ctr + """ + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + if cipher_name[:len('mbedtls:')] == 'mbedtls:': + cipher_name = cipher_name[len('mbedtls:'):] + MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + r = libmbedtls.mbedtls_cipher_setkey( + byref(self._ctx), + key_ptr, c_int(len(key) * 8), + c_int(op) + ) + if r: + self.clean() + raise Exception('can not set cipher key') + r = libmbedtls.mbedtls_cipher_set_iv( + byref(self._ctx), + iv_ptr, c_size_t(len(iv)) + ) + if r: + self.clean() + raise Exception('can not set cipher iv') + r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx)) + if r: + self.clean() + raise Exception('can not reset cipher') + + self.encrypt = self.update + self.decrypt = self.update + + +ciphers = { + 'mbedtls:aes-128-cfb128': (16, 16, MbedTLSStreamCrypto), + 'mbedtls:aes-192-cfb128': (24, 16, MbedTLSStreamCrypto), + 'mbedtls:aes-256-cfb128': (32, 16, MbedTLSStreamCrypto), + 'mbedtls:aes-128-ctr': (16, 16, MbedTLSStreamCrypto), + 'mbedtls:aes-192-ctr': (24, 16, MbedTLSStreamCrypto), + 'mbedtls:aes-256-ctr': (32, 16, MbedTLSStreamCrypto), + 'mbedtls:camellia-128-cfb128': (16, 16, MbedTLSStreamCrypto), + 'mbedtls:camellia-192-cfb128': (24, 16, MbedTLSStreamCrypto), + 'mbedtls:camellia-256-cfb128': (32, 16, MbedTLSStreamCrypto), + # AEAD: iv_len = salt_len = key_len + 'mbedtls:aes-128-gcm': (16, 16, MbedTLSAeadCrypto), + 'mbedtls:aes-192-gcm': (24, 24, MbedTLSAeadCrypto), + 'mbedtls:aes-256-gcm': (32, 32, MbedTLSAeadCrypto), +} + + +def run_method(method): + + print(method, ': [stream]', 32) + cipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def run_aead_method(method, key_len=16): + + print(method, ': [payload][tag]', key_len) + key_len = int(key_len) + cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = MbedTLSAeadCrypto( + method, + b'k' * key_len, b'i' * key_len, 0 + ) + + util.run_cipher(cipher, decipher) + + +def run_aead_method_chunk(method, key_len=16): + + print(method, ': chunk([size][tag][payload][tag]', key_len) + key_len = int(key_len) + cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = MbedTLSAeadCrypto( + method, + b'k' * key_len, b'i' * key_len, 0 + ) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + util.run_cipher(cipher, decipher) + + +def test_camellia_256_cfb(): + run_method('camellia-256-cfb128') + + +def test_aes_gcm(bits=128): + method = "aes-{0}-gcm".format(bits) + run_aead_method(method, bits / 8) + + +def test_aes_gcm_chunk(bits=128): + method = "aes-{0}-gcm".format(bits) + run_aead_method_chunk(method, bits / 8) + + +def test_aes_256_cfb(): + run_method('aes-256-cfb128') + + +def test_aes_256_ctr(): + run_method('aes-256-ctr') + + +if __name__ == '__main__': + test_aes_256_cfb() + test_camellia_256_cfb() + test_aes_256_ctr() + test_aes_gcm(128) + test_aes_gcm(192) + test_aes_gcm(256) + test_aes_gcm_chunk(128) + test_aes_gcm_chunk(192) + test_aes_gcm_chunk(256) diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index 3775b6c..ff63541 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -22,34 +22,52 @@ from ctypes import c_char_p, c_int, c_long, byref,\ from shadowsocks import common from shadowsocks.crypto import util +from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \ + EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG __all__ = ['ciphers'] libcrypto = None loaded = False +libsodium = None +buf = None buf_size = 2048 +ctx_cleanup = None -def load_openssl(): - global loaded, libcrypto, buf +CIPHER_ENC_UNCHANGED = -1 + +def load_openssl(crypto_path=None): + global loaded, libcrypto, libsodium, buf, ctx_cleanup + + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('openssl', None) libcrypto = util.find_library(('crypto', 'eay32'), 'EVP_get_cipherbyname', - 'libcrypto') + 'libcrypto', path) if libcrypto is None: - raise Exception('libcrypto(OpenSSL) not found') + raise Exception('libcrypto(OpenSSL) not found with path %s' % path) libcrypto.EVP_get_cipherbyname.restype = c_void_p libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, c_char_p, c_char_p, c_int) + libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = (c_void_p, c_int, c_int, c_void_p) libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, c_char_p, c_int) - libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CipherFinal_ex.argtypes = (c_void_p, c_void_p, c_void_p) + + try: + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup + except AttributeError: + libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,) + ctx_cleanup = libcrypto.EVP_CIPHER_CTX_reset libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): libcrypto.OpenSSL_add_all_ciphers() @@ -59,7 +77,7 @@ def load_openssl(): def load_cipher(cipher_name): - func_name = 'EVP_' + cipher_name.replace('-', '_') + func_name = b'EVP_' + cipher_name.replace(b'-', b'_') if bytes != str: func_name = str(func_name, 'utf-8') cipher = getattr(libcrypto, func_name, None) @@ -69,37 +87,48 @@ def load_cipher(cipher_name): return None -class OpenSSLCrypto(object): - def __init__(self, cipher_name, key, iv, op): +class OpenSSLCryptoBase(object): + """ + OpenSSL crypto base class + """ + def __init__(self, cipher_name, crypto_path=None): self._ctx = None + self._cipher = None if not loaded: - load_openssl() + load_openssl(crypto_path) cipher_name = common.to_bytes(cipher_name) cipher = libcrypto.EVP_get_cipherbyname(cipher_name) if not cipher: cipher = load_cipher(cipher_name) if not cipher: raise Exception('cipher %s not found in libcrypto' % cipher_name) - key_ptr = c_char_p(key) - iv_ptr = c_char_p(iv) self._ctx = libcrypto.EVP_CIPHER_CTX_new() + self._cipher = cipher if not self._ctx: raise Exception('can not create cipher context') - r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, - key_ptr, iv_ptr, c_int(op)) - if not r: - self.clean() - raise Exception('can not initialize cipher context') + + def encrypt_once(self, data): + return self.update(data) + + def decrypt_once(self, data): + return self.update(data) def update(self, data): + """ + Encrypt/decrypt data + :param data: str + :return: str + """ global buf_size, buf cipher_out_len = c_long(0) l = len(data) if buf_size < l: buf_size = l * 2 buf = create_string_buffer(buf_size) - libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), - byref(cipher_out_len), c_char_p(data), l) + libcrypto.EVP_CipherUpdate( + self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l + ) # buf is copied to a str object when we access buf.raw return buf.raw[:cipher_out_len.value] @@ -108,47 +137,271 @@ class OpenSSLCrypto(object): def clean(self): if self._ctx: - libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + ctx_cleanup(self._ctx) libcrypto.EVP_CIPHER_CTX_free(self._ctx) + self._ctx = None + + +class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): + """ + Implement OpenSSL Aead mode: gcm, ocb + """ + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path) + AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path) + + key_ptr = c_char_p(self._skey) + r = libcrypto.EVP_CipherInit_ex( + self._ctx, + self._cipher, + None, + key_ptr, None, + c_int(op) + ) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_SET_IVLEN), + c_int(self._nlen), + None + ) + if not r: + self.clean() + raise Exception('Set ivlen failed') + + self.cipher_ctx_init() + + def cipher_ctx_init(self): + """ + Need init cipher context after EVP_CipherFinal_ex to reuse context + :return: None + """ + iv_ptr = c_char_p(self._nonce.raw) + r = libcrypto.EVP_CipherInit_ex( + self._ctx, + None, + None, + None, iv_ptr, + c_int(CIPHER_ENC_UNCHANGED) + ) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + AeadCryptoBase.nonce_increment(self) + + def set_tag(self, tag): + """ + Set tag before decrypt any data (update) + :param tag: authenticated tag + :return: None + """ + tag_len = self._tlen + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_SET_TAG), + c_int(tag_len), c_char_p(tag) + ) + if not r: + self.clean() + raise Exception('Set tag failed') + + def get_tag(self): + """ + Get authenticated tag, called after EVP_CipherFinal_ex + :return: str + """ + tag_len = self._tlen + tag_buf = create_string_buffer(tag_len) + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_GET_TAG), + c_int(tag_len), byref(tag_buf) + ) + if not r: + self.clean() + raise Exception('Get tag failed') + return tag_buf.raw[:tag_len] + + def final(self): + """ + Finish encrypt/decrypt a chunk (<= 0x3FFF) + :return: str + """ + global buf_size, buf + cipher_out_len = c_long(0) + r = libcrypto.EVP_CipherFinal_ex( + self._ctx, + byref(buf), byref(cipher_out_len) + ) + if not r: + self.clean() + # print(self._nonce.raw, r, cipher_out_len) + raise Exception('Finalize cipher failed') + return buf.raw[:cipher_out_len.value] + + def aead_encrypt(self, data): + """ + Encrypt data with authenticate tag + + :param data: plain text + :return: cipher text with tag + """ + ctext = self.update(data) + self.final() + self.get_tag() + self.cipher_ctx_init() + return ctext + + def aead_decrypt(self, data): + """ + Decrypt data and authenticate tag + + :param data: cipher text with tag + :return: plain text + """ + clen = len(data) + if clen < self._tlen: + self.clean() + raise Exception('Data too short') + + self.set_tag(data[clen - self._tlen:]) + plaintext = self.update(data[:clen - self._tlen]) + self.final() + self.cipher_ctx_init() + return plaintext + + def encrypt_once(self, data): + return self.aead_encrypt(data) + + def decrypt_once(self, data): + return self.aead_decrypt(data) + + +class OpenSSLStreamCrypto(OpenSSLCryptoBase): + """ + Crypto for stream modes: cfb, ofb, ctr + """ + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + r = libcrypto.EVP_CipherInit_ex(self._ctx, self._cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def encrypt(self, data): + return self.update(data) + + def decrypt(self, data): + return self.update(data) ciphers = { - 'aes-128-cfb': (16, 16, OpenSSLCrypto), - 'aes-192-cfb': (24, 16, OpenSSLCrypto), - 'aes-256-cfb': (32, 16, OpenSSLCrypto), - 'aes-128-ofb': (16, 16, OpenSSLCrypto), - 'aes-192-ofb': (24, 16, OpenSSLCrypto), - 'aes-256-ofb': (32, 16, OpenSSLCrypto), - 'aes-128-ctr': (16, 16, OpenSSLCrypto), - 'aes-192-ctr': (24, 16, OpenSSLCrypto), - 'aes-256-ctr': (32, 16, OpenSSLCrypto), - 'aes-128-cfb8': (16, 16, OpenSSLCrypto), - 'aes-192-cfb8': (24, 16, OpenSSLCrypto), - 'aes-256-cfb8': (32, 16, OpenSSLCrypto), - 'aes-128-cfb1': (16, 16, OpenSSLCrypto), - 'aes-192-cfb1': (24, 16, OpenSSLCrypto), - 'aes-256-cfb1': (32, 16, OpenSSLCrypto), - 'bf-cfb': (16, 8, OpenSSLCrypto), - 'camellia-128-cfb': (16, 16, OpenSSLCrypto), - 'camellia-192-cfb': (24, 16, OpenSSLCrypto), - 'camellia-256-cfb': (32, 16, OpenSSLCrypto), - 'cast5-cfb': (16, 8, OpenSSLCrypto), - 'des-cfb': (8, 8, OpenSSLCrypto), - 'idea-cfb': (16, 8, OpenSSLCrypto), - 'rc2-cfb': (16, 8, OpenSSLCrypto), - 'rc4': (16, 0, OpenSSLCrypto), - 'seed-cfb': (16, 16, OpenSSLCrypto), + 'aes-128-cfb': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb': (32, 16, OpenSSLStreamCrypto), + 'aes-128-ofb': (16, 16, OpenSSLStreamCrypto), + 'aes-192-ofb': (24, 16, OpenSSLStreamCrypto), + 'aes-256-ofb': (32, 16, OpenSSLStreamCrypto), + 'aes-128-ctr': (16, 16, OpenSSLStreamCrypto), + 'aes-192-ctr': (24, 16, OpenSSLStreamCrypto), + 'aes-256-ctr': (32, 16, OpenSSLStreamCrypto), + 'aes-128-cfb8': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb8': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb8': (32, 16, OpenSSLStreamCrypto), + 'aes-128-cfb1': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb1': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb1': (32, 16, OpenSSLStreamCrypto), + 'bf-cfb': (16, 8, OpenSSLStreamCrypto), + 'camellia-128-cfb': (16, 16, OpenSSLStreamCrypto), + 'camellia-192-cfb': (24, 16, OpenSSLStreamCrypto), + 'camellia-256-cfb': (32, 16, OpenSSLStreamCrypto), + 'cast5-cfb': (16, 8, OpenSSLStreamCrypto), + 'des-cfb': (8, 8, OpenSSLStreamCrypto), + 'idea-cfb': (16, 8, OpenSSLStreamCrypto), + 'rc2-cfb': (16, 8, OpenSSLStreamCrypto), + 'rc4': (16, 0, OpenSSLStreamCrypto), + 'seed-cfb': (16, 16, OpenSSLStreamCrypto), + # AEAD: iv_len = salt_len = key_len + 'aes-128-gcm': (16, 16, OpenSSLAeadCrypto), + 'aes-192-gcm': (24, 24, OpenSSLAeadCrypto), + 'aes-256-gcm': (32, 32, OpenSSLAeadCrypto), + 'aes-128-ocb': (16, 16, OpenSSLAeadCrypto), + 'aes-192-ocb': (24, 24, OpenSSLAeadCrypto), + 'aes-256-ocb': (32, 32, OpenSSLAeadCrypto), } def run_method(method): - cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) - decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0) + print(method, ': [stream]', 32) + cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) +def run_aead_method(method, key_len=16): + + if not loaded: + load_openssl(None) + print(method, ': [payload][tag]', key_len) + cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method)) + if not cipher: + cipher = load_cipher(common.to_bytes(method)) + if not cipher: + print('cipher not avaiable, please upgrade openssl') + return + key_len = int(key_len) + cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0) + + util.run_cipher(cipher, decipher) + + +def run_aead_method_chunk(method, key_len=16): + + if not loaded: + load_openssl(None) + print(method, ': chunk([size][tag][payload][tag]', key_len) + cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method)) + if not cipher: + cipher = load_cipher(common.to_bytes(method)) + if not cipher: + print('cipher not avaiable, please upgrade openssl') + return + key_len = int(key_len) + cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + util.run_cipher(cipher, decipher) + + +def test_aes_gcm(bits=128): + method = "aes-{0}-gcm".format(bits) + run_aead_method(method, bits / 8) + + +def test_aes_ocb(bits=128): + method = "aes-{0}-ocb".format(bits) + run_aead_method(method, bits / 8) + + +def test_aes_gcm_chunk(bits=128): + method = "aes-{0}-gcm".format(bits) + run_aead_method_chunk(method, bits / 8) + + +def test_aes_ocb_chunk(bits=128): + method = "aes-{0}-ocb".format(bits) + run_aead_method_chunk(method, bits / 8) + + def test_aes_128_cfb(): run_method('aes-128-cfb') @@ -179,3 +432,17 @@ def test_rc4(): if __name__ == '__main__': test_aes_128_cfb() + test_aes_256_cfb() + test_aes_256_ofb() + test_aes_gcm(128) + test_aes_gcm(192) + test_aes_gcm(256) + test_aes_gcm_chunk(128) + test_aes_gcm_chunk(192) + test_aes_gcm_chunk(256) + test_aes_ocb(128) + test_aes_ocb(192) + test_aes_ocb(256) + test_aes_ocb_chunk(128) + test_aes_ocb_chunk(192) + test_aes_ocb_chunk(256) diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 1f07a82..014fa3c 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -18,19 +18,19 @@ from __future__ import absolute_import, division, print_function, \ with_statement import hashlib - from shadowsocks.crypto import openssl __all__ = ['ciphers'] -def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, +def create_cipher(alg, key, iv, op, crypto_path=None, + key_as_bytes=0, d=None, salt=None, i=1, padding=1): md5 = hashlib.md5() md5.update(key) md5.update(iv) rc4_key = md5.digest() - return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) + return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op, crypto_path) ciphers = { diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index ae86fef..981321e 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -17,49 +17,162 @@ from __future__ import absolute_import, division, print_function, \ with_statement -from ctypes import c_char_p, c_int, c_ulonglong, byref, \ +from ctypes import c_char_p, c_int, c_uint, c_ulonglong, byref, \ create_string_buffer, c_void_p from shadowsocks.crypto import util +from shadowsocks.crypto import aead +from shadowsocks.crypto.aead import AeadCryptoBase __all__ = ['ciphers'] libsodium = None loaded = False +buf = None buf_size = 2048 -# for salsa20 and chacha20 +# for salsa20 and chacha20 and chacha20-ietf BLOCK_SIZE = 64 -def load_libsodium(): +def load_libsodium(crypto_path=None): global loaded, libsodium, buf - libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', - 'libsodium') - if libsodium is None: - raise Exception('libsodium not found') + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('sodium', None) + + if not aead.sodium_loaded: + aead.load_sodium(path) + + if aead.sodium_loaded: + libsodium = aead.libsodium + else: + print('load libsodium again with path %s' % path) + libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', + 'libsodium', path) + if libsodium is None: + raise Exception('libsodium not found') + + if libsodium.sodium_init() < 0: + raise Exception('libsodium init failed') libsodium.crypto_stream_salsa20_xor_ic.restype = c_int - libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, - c_ulonglong, - c_char_p, c_ulonglong, - c_char_p) + libsodium.crypto_stream_salsa20_xor_ic.argtypes = ( + c_void_p, c_char_p, # cipher output, msg + c_ulonglong, # msg len + c_char_p, c_ulonglong, # nonce, uint64_t initial block counter + c_char_p # key + ) libsodium.crypto_stream_chacha20_xor_ic.restype = c_int - libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, - c_ulonglong, - c_char_p, c_ulonglong, - c_char_p) + libsodium.crypto_stream_chacha20_xor_ic.argtypes = ( + c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p + ) + if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'): + libsodium.crypto_stream_xchacha20_xor_ic.restype = c_int + libsodium.crypto_stream_xchacha20_xor_ic.argtypes = ( + c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p + ) + libsodium.crypto_stream_chacha20_ietf_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = ( + c_void_p, c_char_p, + c_ulonglong, + c_char_p, + c_uint, # uint32_t initial counter + c_char_p + ) + + # chacha20-poly1305 + libsodium.crypto_aead_chacha20poly1305_encrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_encrypt.argtypes = ( + c_void_p, c_void_p, # c, clen + c_char_p, c_ulonglong, # m, mlen + c_char_p, c_ulonglong, # ad, adlen + c_char_p, # nsec, not used + c_char_p, c_char_p # npub, k + ) + libsodium.crypto_aead_chacha20poly1305_decrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_decrypt.argtypes = ( + c_void_p, c_void_p, # m, mlen + c_char_p, # nsec, not used + c_char_p, c_ulonglong, # c, clen + c_char_p, c_ulonglong, # ad, adlen + c_char_p, c_char_p # npub, k + ) + + # chacha20-ietf-poly1305, same api structure as above + libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) + + # xchacha20-ietf-poly1305, same api structure as above + if hasattr(libsodium, 'crypto_aead_xchacha20poly1305_ietf_encrypt'): + libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.restype = c_int + libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + + libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.restype = c_int + libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) + + # aes-256-gcm, same api structure as above + libsodium.crypto_aead_aes256gcm_is_available.restype = c_int + + if libsodium.crypto_aead_aes256gcm_is_available(): + libsodium.crypto_aead_aes256gcm_encrypt.restype = c_int + libsodium.crypto_aead_aes256gcm_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + libsodium.crypto_aead_aes256gcm_decrypt.restype = c_int + libsodium.crypto_aead_aes256gcm_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) buf = create_string_buffer(buf_size) loaded = True class SodiumCrypto(object): - def __init__(self, cipher_name, key, iv, op): + def __init__(self, cipher_name, key, iv, op, crypto_path=None): if not loaded: - load_libsodium() + load_libsodium(crypto_path) self.key = key self.iv = iv self.key_ptr = c_char_p(key) @@ -68,11 +181,30 @@ class SodiumCrypto(object): self.cipher = libsodium.crypto_stream_salsa20_xor_ic elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic + elif cipher_name == 'xchacha20': + if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'): + self.cipher = libsodium.crypto_stream_xchacha20_xor_ic + else: + raise Exception('Unsupported cipher') + elif cipher_name == 'chacha20-ietf': + self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic else: raise Exception('Unknown cipher') # byte counter, not block counter self.counter = 0 + def encrypt(self, data): + return self.update(data) + + def decrypt(self, data): + return self.update(data) + + def encrypt_once(self, data): + return self.update(data) + + def decrypt_once(self, data): + return self.update(data) + def update(self, data): global buf_size, buf l = len(data) @@ -93,28 +225,218 @@ class SodiumCrypto(object): # strip off the padding return buf.raw[padding:padding + l] + def clean(self): + pass + + +class SodiumAeadCrypto(AeadCryptoBase): + def __init__(self, cipher_name, key, iv, op, crypto_path=None): + if not loaded: + load_libsodium(crypto_path) + AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path) + + if cipher_name == 'chacha20-poly1305': + self.encryptor = libsodium.crypto_aead_chacha20poly1305_encrypt + self.decryptor = libsodium.crypto_aead_chacha20poly1305_decrypt + elif cipher_name == 'chacha20-ietf-poly1305': + self.encryptor = libsodium. \ + crypto_aead_chacha20poly1305_ietf_encrypt + self.decryptor = libsodium. \ + crypto_aead_chacha20poly1305_ietf_decrypt + elif cipher_name == 'xchacha20-ietf-poly1305': + if hasattr(libsodium, + 'crypto_aead_xchacha20poly1305_ietf_encrypt'): + self.encryptor = libsodium. \ + crypto_aead_xchacha20poly1305_ietf_encrypt + self.decryptor = libsodium. \ + crypto_aead_xchacha20poly1305_ietf_decrypt + else: + raise Exception('Unsupported cipher') + elif cipher_name == 'sodium:aes-256-gcm': + if hasattr(libsodium, 'crypto_aead_aes256gcm_encrypt'): + self.encryptor = libsodium.crypto_aead_aes256gcm_encrypt + self.decryptor = libsodium.crypto_aead_aes256gcm_decrypt + else: + raise Exception('Unsupported cipher') + else: + raise Exception('Unknown cipher') + + def cipher_ctx_init(self): + global libsodium + libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) + # print("".join("%02x" % ord(b) for b in self._nonce)) + + def aead_encrypt(self, data): + global buf, buf_size + plen = len(data) + if buf_size < plen + self._tlen: + buf_size = (plen + self._tlen) * 2 + buf = create_string_buffer(buf_size) + cipher_out_len = c_ulonglong(0) + self.encryptor( + byref(buf), byref(cipher_out_len), + c_char_p(data), c_ulonglong(plen), + None, c_ulonglong(0), None, + c_char_p(self._nonce.raw), c_char_p(self._skey) + ) + if cipher_out_len.value != plen + self._tlen: + raise Exception("Encrypt failed") + + self.cipher_ctx_init() + return buf.raw[:cipher_out_len.value] + + def aead_decrypt(self, data): + global buf, buf_size + clen = len(data) + if buf_size < clen: + buf_size = clen * 2 + buf = create_string_buffer(buf_size) + cipher_out_len = c_ulonglong(0) + r = self.decryptor( + byref(buf), byref(cipher_out_len), + None, + c_char_p(data), c_ulonglong(clen), + None, c_ulonglong(0), + c_char_p(self._nonce.raw), c_char_p(self._skey) + ) + if r != 0: + raise Exception("Decrypt failed") + + if cipher_out_len.value != clen - self._tlen: + raise Exception("Decrypt failed") + + self.cipher_ctx_init() + return buf.raw[:cipher_out_len.value] + + def encrypt_once(self, data): + return self.aead_encrypt(data) + + def decrypt_once(self, data): + return self.aead_decrypt(data) + ciphers = { 'salsa20': (32, 8, SodiumCrypto), 'chacha20': (32, 8, SodiumCrypto), + 'xchacha20': (32, 24, SodiumCrypto), + 'chacha20-ietf': (32, 12, SodiumCrypto), + # AEAD: iv_len = salt_len = key_len + 'chacha20-poly1305': (32, 32, SodiumAeadCrypto), + 'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), + 'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), + 'sodium:aes-256-gcm': (32, 32, SodiumAeadCrypto), } -def test_salsa20(): - cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) - decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) - - util.run_cipher(cipher, decipher) - - def test_chacha20(): - + print("Test chacha20") cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) util.run_cipher(cipher, decipher) +def test_xchacha20(): + print("Test xchacha20") + cipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 1) + decipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 0) + + util.run_cipher(cipher, decipher) + + +def test_salsa20(): + print("Test salsa20") + cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_ietf(): + print("Test chacha20-ietf") + cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_poly1305(): + print("Test chacha20-poly1305 [payload][tag]") + cipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_poly1305_chunk(): + print("Test chacha20-poly1305 chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + +def test_chacha20_ietf_poly1305(): + print("Test chacha20-ietf-poly1305 [payload][tag]") + cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_ietf_poly1305_chunk(): + print("Test chacha20-ietf-poly1305 chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + +def test_aes_256_gcm(): + print("Test sodium:aes-256-gcm [payload][tag]") + cipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + +def test_aes_256_gcm_chunk(): + print("Test sodium:aes-256-gcm chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + if __name__ == '__main__': test_chacha20() + test_xchacha20() test_salsa20() + test_chacha20_ietf() + test_chacha20_poly1305() + test_chacha20_poly1305_chunk() + test_chacha20_ietf_poly1305() + test_chacha20_ietf_poly1305_chunk() + test_aes_256_gcm() + test_aes_256_gcm_chunk() diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index bc693f5..1752be5 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -55,9 +55,13 @@ def init_table(key): class TableCipher(object): - def __init__(self, cipher_name, key, iv, op): + def __init__(self, cipher_name, key, iv, op, crypto_path=None): self._encrypt_table, self._decrypt_table = init_table(key) self._op = op + self.encrypt = self.update + self.decrypt = self.update + self.encrypt_once = self.update + self.decrypt_once = self.update def update(self, data): if self._op: diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index e579455..f52e7ba 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -26,6 +26,7 @@ def find_library_nt(name): # ctypes.util.find_library just returns first result he found # but we want to try them all # because on Windows, users may have both 32bit and 64bit version installed + import glob results = [] for directory in os.environ['PATH'].split(os.pathsep): fname = os.path.join(directory, name) @@ -33,15 +34,34 @@ def find_library_nt(name): results.append(fname) if fname.lower().endswith(".dll"): continue - fname = fname + ".dll" - if os.path.isfile(fname): - results.append(fname) + fname += "*.dll" + files = glob.glob(fname) + if files: + results.extend(files) return results -def find_library(possible_lib_names, search_symbol, library_name): - import ctypes.util +def load_library(path, search_symbol, library_name): from ctypes import CDLL + try: + lib = CDLL(path) + if hasattr(lib, search_symbol): + logging.info('loading %s from %s', library_name, path) + return lib + else: + logging.warn('can\'t find symbol %s in %s', search_symbol, + path) + except Exception: + pass + return None + + +def find_library(possible_lib_names, search_symbol, library_name, + custom_path=None): + import ctypes.util + + if custom_path: + return load_library(custom_path, search_symbol, library_name) paths = [] @@ -79,16 +99,22 @@ def find_library(possible_lib_names, search_symbol, library_name): if files: paths.extend(files) for path in paths: - try: - lib = CDLL(path) - if hasattr(lib, search_symbol): - logging.info('loading %s from %s', library_name, path) - return lib - else: - logging.warn('can\'t find symbol %s in %s', search_symbol, - path) - except Exception: - pass + lib = load_library(path, search_symbol, library_name) + if lib: + return lib + return None + + +def parse_mode(cipher_nme): + """ + Parse the cipher mode from cipher name + e.g. aes-128-gcm, the mode is gcm + :param cipher_nme: str cipher name, aes-128-cfb, aes-128-gcm ... + :return: str/None The mode, cfb, gcm ... + """ + hyphen = cipher_nme.rfind('-') + if hyphen > 0: + return cipher_nme[hyphen:] return None @@ -97,29 +123,31 @@ def run_cipher(cipher, decipher): import random import time - BLOCK_SIZE = 16384 + block_size = 16384 rounds = 1 * 1024 - plain = urandom(BLOCK_SIZE * rounds) + plain = urandom(block_size * rounds) - results = [] + cipher_results = [] pos = 0 print('test start') start = time.time() while pos < len(plain): l = random.randint(100, 32768) - c = cipher.update(plain[pos:pos + l]) - results.append(c) + # print(pos, l) + c = cipher.encrypt_once(plain[pos:pos + l]) + cipher_results.append(c) pos += l pos = 0 - c = b''.join(results) - results = [] - while pos < len(plain): - l = random.randint(100, 32768) - results.append(decipher.update(c[pos:pos + l])) + # c = b''.join(cipher_results) + plain_results = [] + for c in cipher_results: + # l = random.randint(100, 32768) + l = len(c) + plain_results.append(decipher.decrypt_once(c)) pos += l end = time.time() - print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start))) - assert b''.join(results) == plain + print('speed: %d bytes/s' % (block_size * rounds / (end - start))) + assert b''.join(plain_results) == plain def test_find_library(): diff --git a/shadowsocks/encrypt.py b/shadowsocks/cryptor.py similarity index 52% rename from shadowsocks/encrypt.py rename to shadowsocks/cryptor.py index 4e87f41..4eae9e8 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/cryptor.py @@ -23,12 +23,20 @@ import hashlib import logging from shadowsocks import common -from shadowsocks.crypto import rc4_md5, openssl, sodium, table +from shadowsocks.crypto import rc4_md5, openssl, mbedtls, sodium, table +CIPHER_ENC_ENCRYPTION = 1 +CIPHER_ENC_DECRYPTION = 0 + +METHOD_INFO_KEY_LEN = 0 +METHOD_INFO_IV_LEN = 1 +METHOD_INFO_CRYPTO = 2 + method_supported = {} method_supported.update(rc4_md5.ciphers) method_supported.update(openssl.ciphers) +method_supported.update(mbedtls.ciphers) method_supported.update(sodium.ciphers) method_supported.update(table.ciphers) @@ -36,12 +44,11 @@ method_supported.update(table.ciphers) def random_string(length): return os.urandom(length) - cached_keys = {} -def try_cipher(key, method=None): - Encryptor(key, method) +def try_cipher(key, method=None, crypto_path=None): + Cryptor(key, method, crypto_path) def EVP_BytesToKey(password, key_len, iv_len): @@ -68,24 +75,36 @@ def EVP_BytesToKey(password, key_len, iv_len): return key, iv -class Encryptor(object): - def __init__(self, key, method): - self.key = key +class Cryptor(object): + def __init__(self, password, method, crypto_path=None): + """ + Crypto wrapper + :param password: str cipher password + :param method: str cipher + :param crypto_path: dict or none + {'openssl': path, 'sodium': path, 'mbedtls': path} + """ + self.password = password + self.key = None self.method = method - self.iv = None self.iv_sent = False self.cipher_iv = b'' self.decipher = None + self.decipher_iv = None + self.crypto_path = crypto_path method = method.lower() - self._method_info = self.get_method_info(method) + self._method_info = Cryptor.get_method_info(method) if self._method_info: - self.cipher = self.get_cipher(key, method, 1, - random_string(self._method_info[1])) + self.cipher = self.get_cipher( + password, method, CIPHER_ENC_ENCRYPTION, + random_string(self._method_info[METHOD_INFO_IV_LEN]) + ) else: logging.error('method %s not supported' % method) sys.exit(1) - def get_method_info(self, method): + @staticmethod + def get_method_info(method): method = method.lower() m = method_supported.get(method) return m @@ -96,63 +115,90 @@ class Encryptor(object): def get_cipher(self, password, method, op, iv): password = common.to_bytes(password) m = self._method_info - if m[0] > 0: - key, iv_ = EVP_BytesToKey(password, m[0], m[1]) + if m[METHOD_INFO_KEY_LEN] > 0: + key, _ = EVP_BytesToKey(password, + m[METHOD_INFO_KEY_LEN], + m[METHOD_INFO_IV_LEN]) else: # key_length == 0 indicates we should use the key directly key, iv = password, b'' - - iv = iv[:m[1]] - if op == 1: + self.key = key + iv = iv[:m[METHOD_INFO_IV_LEN]] + if op == CIPHER_ENC_ENCRYPTION: # this iv is for cipher not decipher - self.cipher_iv = iv[:m[1]] - return m[2](method, key, iv, op) + self.cipher_iv = iv + return m[METHOD_INFO_CRYPTO](method, key, iv, op, self.crypto_path) def encrypt(self, buf): if len(buf) == 0: return buf if self.iv_sent: - return self.cipher.update(buf) + return self.cipher.encrypt(buf) else: self.iv_sent = True - return self.cipher_iv + self.cipher.update(buf) + return self.cipher_iv + self.cipher.encrypt(buf) def decrypt(self, buf): if len(buf) == 0: return buf if self.decipher is None: - decipher_iv_len = self._method_info[1] + decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN] decipher_iv = buf[:decipher_iv_len] - self.decipher = self.get_cipher(self.key, self.method, 0, - iv=decipher_iv) + self.decipher_iv = decipher_iv + self.decipher = self.get_cipher( + self.password, self.method, + CIPHER_ENC_DECRYPTION, + decipher_iv + ) buf = buf[decipher_iv_len:] if len(buf) == 0: return buf - return self.decipher.update(buf) + return self.decipher.decrypt(buf) -def encrypt_all(password, method, op, data): - result = [] +def gen_key_iv(password, method): method = method.lower() (key_len, iv_len, m) = method_supported[method] if key_len > 0: key, _ = EVP_BytesToKey(password, key_len, iv_len) else: key = password - if op: - iv = random_string(iv_len) - result.append(iv) - else: - iv = data[:iv_len] - data = data[iv_len:] - cipher = m(method, key, iv, op) - result.append(cipher.update(data)) + iv = random_string(iv_len) + return key, iv, m + + +def encrypt_all_m(key, iv, m, method, data, crypto_path=None): + result = [iv] + cipher = m(method, key, iv, 1, crypto_path) + result.append(cipher.encrypt_once(data)) + return b''.join(result) + + +def decrypt_all(password, method, data, crypto_path=None): + result = [] + method = method.lower() + (key, iv, m) = gen_key_iv(password, method) + iv = data[:len(iv)] + data = data[len(iv):] + cipher = m(method, key, iv, CIPHER_ENC_DECRYPTION, crypto_path) + result.append(cipher.decrypt_once(data)) + return b''.join(result), key, iv + + +def encrypt_all(password, method, data, crypto_path=None): + result = [] + method = method.lower() + (key, iv, m) = gen_key_iv(password, method) + result.append(iv) + cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION, crypto_path) + result.append(cipher.encrypt_once(data)) return b''.join(result) CIPHERS_TO_TEST = [ 'aes-128-cfb', 'aes-256-cfb', + 'aes-256-gcm', 'rc4-md5', 'salsa20', 'chacha20', @@ -165,8 +211,8 @@ def test_encryptor(): plain = urandom(10240) for method in CIPHERS_TO_TEST: logging.warn(method) - encryptor = Encryptor(b'key', method) - decryptor = Encryptor(b'key', method) + encryptor = Cryptor(b'key', method) + decryptor = Cryptor(b'key', method) cipher = encryptor.encrypt(plain) plain2 = decryptor.decrypt(cipher) assert plain == plain2 @@ -177,11 +223,23 @@ def test_encrypt_all(): plain = urandom(10240) for method in CIPHERS_TO_TEST: logging.warn(method) - cipher = encrypt_all(b'key', method, 1, plain) - plain2 = encrypt_all(b'key', method, 0, cipher) + cipher = encrypt_all(b'key', method, plain) + plain2, key, iv = decrypt_all(b'key', method, cipher) + assert plain == plain2 + + +def test_encrypt_all_m(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + key, iv, m = gen_key_iv(b'key', method) + cipher = encrypt_all_m(key, iv, m, method, plain) + plain2, key, iv = decrypt_all(b'key', method, cipher) assert plain == plain2 if __name__ == '__main__': test_encrypt_all() test_encryptor() + test_encrypt_all_m() diff --git a/shadowsocks/daemon.py b/shadowsocks/daemon.py index 8dc5608..77ed323 100644 --- a/shadowsocks/daemon.py +++ b/shadowsocks/daemon.py @@ -117,7 +117,7 @@ def daemon_start(pid_file, log_file): sys.exit(1) os.setsid() - signal.signal(signal.SIG_IGN, signal.SIGHUP) + signal.signal(signal.SIGHUP, signal.SIG_IGN) print('started') os.kill(ppid, signal.SIGTERM) diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 42f9205..ce5da37 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -22,8 +22,10 @@ from __future__ import absolute_import, division, print_function, \ with_statement import os +import time import socket import select +import traceback import errno import logging from collections import defaultdict @@ -51,23 +53,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): @@ -100,17 +87,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): @@ -129,7 +119,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: @@ -137,7 +127,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: @@ -145,16 +135,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() @@ -165,72 +157,73 @@ 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: - shell.print_exception(e) - if self._handlers_to_remove: - for handler in self._handlers_to_remove: - self._handlers.remove(handler) - self._handlers_to_remove = [] - self._iterating = False + + 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 4255a2e..dfc8032 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -27,6 +27,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns +@shell.exception_handle(self_=False, exit_code=1) def main(): shell.check_python() @@ -37,36 +38,31 @@ def main(): os.chdir(p) config = shell.get_config(True) - daemon.daemon_exec(config) - try: - logging.info("starting local at %s:%d" % - (config['local_address'], config['local_port'])) + logging.info("starting local at %s:%d" % + (config['local_address'], config['local_port'])) - dns_resolver = asyncdns.DNSResolver() - tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) - udp_server = udprelay.UDPRelay(config, dns_resolver, True) - loop = eventloop.EventLoop() - dns_resolver.add_to_loop(loop) - tcp_server.add_to_loop(loop) - udp_server.add_to_loop(loop) + dns_resolver = asyncdns.DNSResolver() + tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) + udp_server = udprelay.UDPRelay(config, dns_resolver, True) + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + tcp_server.add_to_loop(loop) + udp_server.add_to_loop(loop) - def handler(signum, _): - logging.warn('received SIGQUIT, doing graceful shutting down..') - tcp_server.close(next_tick=True) - udp_server.close(next_tick=True) - signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tcp_server.close(next_tick=True) + udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) - def int_handler(signum, _): - sys.exit(1) - signal.signal(signal.SIGINT, int_handler) - - daemon.set_user(config.get('user', None)) - loop.run() - except Exception as e: - shell.print_exception(e) + def int_handler(signum, _): sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + daemon.set_user(config.get('user', None)) + loop.run() if __name__ == '__main__': main() diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index 401f19b..55cb346 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -79,18 +79,15 @@ class LRUCache(collections.MutableMapping): least = self._last_visits[0] if now - least <= self.timeout: break - if self.close_callback is not None: - for key in self._time_to_keys[least]: - if key in self._store: - if now - self._keys_to_last_time[key] > self.timeout: + self._last_visits.popleft() + for key in self._time_to_keys[least]: + if key in self._store: + if now - self._keys_to_last_time[key] > self.timeout: + if self.close_callback is not None: value = self._store[key] if value not in self._closed_values: self.close_callback(value) self._closed_values.add(value) - for key in self._time_to_keys[least]: - self._last_visits.popleft() - if key in self._store: - if now - self._keys_to_last_time[key] > self.timeout: del self._store[key] del self._keys_to_last_time[key] c += 1 @@ -140,6 +137,7 @@ def test(): c = LRUCache(timeout=0.1, close_callback=close_cb) c['s'] = 1 + c['t'] = 1 c['s'] time.sleep(0.1) c['s'] diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py new file mode 100644 index 0000000..62f187e --- /dev/null +++ b/shadowsocks/manager.py @@ -0,0 +1,293 @@ +#!/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 = 50 + + +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'] + config['crypto_path'] = config.get('crypto_path', dict()) + 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) + if 'method' in config: + config['method'] = common.to_str(config['method']) + 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() + i = 0 + if len(r) > 0: + send_data(r) + self._statistics.clear() + + def _send_control_data(self, data): + if not self._control_client_addr: + return + + 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 cryptor + + 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 = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', + 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 = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb', + 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 429a20a..4dc5621 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -24,7 +24,8 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ + asyncdns, manager def main(): @@ -48,10 +49,23 @@ def main(): else: config['port_password'][str(server_port)] = config['password'] + if config.get('manager_address', 0): + logging.info('entering manager mode') + manager.run(config) + return + tcp_servers = [] udp_servers = [] - dns_resolver = asyncdns.DNSResolver() - for port, password in config['port_password'].items(): + + if 'dns_server' in config: # allow override settings in resolv.conf + dns_resolver = asyncdns.DNSResolver(config['dns_server'], + config['prefer_ipv6']) + else: + dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6']) + + 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 diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index f8ae81f..d508049 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -23,8 +23,12 @@ import json import sys import getopt import logging +import traceback + +from functools import wraps + from shadowsocks.common import to_bytes, to_str, IPNetwork -from shadowsocks import encrypt +from shadowsocks import cryptor VERBOSE_LEVEL = 5 @@ -53,6 +57,49 @@ def print_exception(e): traceback.print_exc() +def exception_handle(self_, err_msg=None, exit_code=None, + destroy=False, conn_err=False): + # self_: if function passes self as first arg + + def process_exception(e, self=None): + print_exception(e) + if err_msg: + logging.error(err_msg) + if exit_code: + sys.exit(1) + + if not self_: + return + + if conn_err: + addr, port = self._client_address[0], self._client_address[1] + logging.error('%s when handling connection from %s:%d' % + (e, addr, port)) + if self._config['verbose']: + traceback.print_exc() + if destroy: + self.destroy() + + def decorator(func): + if self_: + @wraps(func) + def wrapper(self, *args, **kwargs): + try: + func(self, *args, **kwargs) + except Exception as e: + process_exception(e, self) + else: + @wraps(func) + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception as e: + process_exception(e) + + return wrapper + return decorator + + def print_shadowsocks(): version = '' try: @@ -78,13 +125,37 @@ def check_config(config, is_local): # no need to specify configuration for daemon stop return + 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']) + + if config.get('tunnel_remote', None) is None: + logging.error('tunnel_remote addr not specified') + print_local_help() + sys.exit(2) + else: + config['tunnel_remote'] = to_str(config['tunnel_remote']) + else: + config['server'] = to_str(config.get('server', '0.0.0.0')) + try: + config['forbidden_ip'] = \ + IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128')) + except Exception as e: + logging.error(e) + sys.exit(2) + 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('port_password', None) \ + and not config.get('manager_address'): logging.error('password or port_password not specified') print_help(is_local) sys.exit(2) @@ -95,6 +166,11 @@ def check_config(config, is_local): if 'server_port' in config and type(config['server_port']) != list: config['server_port'] = int(config['server_port']) + if 'tunnel_remote_port' in config: + config['tunnel_remote_port'] = int(config['tunnel_remote_port']) + if 'tunnel_port' in config: + config['tunnel_port'] = int(config['tunnel_port']) + if config.get('local_address', '') in [b'0.0.0.0']: logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') if config.get('server', '') in ['127.0.0.1', 'localhost']: @@ -120,8 +196,19 @@ def check_config(config, is_local): if os.name != 'posix': logging.error('user can be used only on Unix') sys.exit(1) + if config.get('dns_server', None) is not None: + if type(config['dns_server']) != list: + config['dns_server'] = to_str(config['dns_server']) + else: + config['dns_server'] = [to_str(ds) for ds in config['dns_server']] + logging.info('Specified DNS server: %s' % config['dns_server']) - encrypt.try_cipher(config['password'], config['method']) + config['crypto_path'] = {'openssl': config['libopenssl'], + 'mbedtls': config['libmbedtls'], + 'sodium': config['libsodium']} + + cryptor.try_cipher(config['password'], config['method'], + config['crypto_path']) def get_config(is_local): @@ -130,13 +217,14 @@ def get_config(is_local): logging.basicConfig(level=logging.INFO, format='%(levelname)-s: %(message)s') if is_local: - shortopts = 'hd:s:b:p:k:l:m:c:t:vq' + shortopts = 'hd:s:b:p:k:l:m:c:t:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', - 'version'] + 'libopenssl=', 'libmbedtls=', 'libsodium=', 'version'] else: - shortopts = 'hd:s:p:k:m:c:t:vq' + shortopts = 'hd:s:p:k:m:c:t:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', - 'forbidden-ip=', 'user=', 'version'] + 'forbidden-ip=', 'user=', 'manager-address=', 'version', + 'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -148,8 +236,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) @@ -175,12 +262,22 @@ def get_config(is_local): v_count += 1 # '-vv' turns on more verbose mode config['verbose'] = v_count + elif key == '-a': + config['one_time_auth'] = True elif key == '-t': config['timeout'] = int(value) elif key == '--fast-open': config['fast_open'] = True + elif key == '--libopenssl': + config['libopenssl'] = to_str(value) + elif key == '--libmbedtls': + config['libmbedtls'] = to_str(value) + elif key == '--libsodium': + config['libsodium'] = to_str(value) elif key == '--workers': config['workers'] = int(value) + elif key == '--manager-address': + config['manager_address'] = to_str(value) elif key == '--user': config['user'] = to_str(value) elif key == '--forbidden-ip': @@ -203,6 +300,8 @@ def get_config(is_local): elif key == '-q': v_count -= 1 config['verbose'] = v_count + elif key == '--prefer-ipv6': + config['prefer_ipv6'] = True except getopt.GetoptError as e: print(e, file=sys.stderr) print_help(is_local) @@ -224,22 +323,17 @@ def get_config(is_local): config['verbose'] = config.get('verbose', False) config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) - if is_local: - if config.get('server', None) is None: - logging.error('server addr not specified') - print_local_help() - sys.exit(2) - else: - config['server'] = to_str(config['server']) - else: - config['server'] = to_str(config.get('server', '0.0.0.0')) - try: - config['forbidden_ip'] = \ - IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128')) - except Exception as e: - logging.error(e) - sys.exit(2) + config['one_time_auth'] = config.get('one_time_auth', False) + config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) + config['dns_server'] = config.get('dns_server', None) + config['libopenssl'] = config.get('libopenssl', None) + config['libmbedtls'] = config.get('libmbedtls', None) + config['libsodium'] = config.get('libsodium', None) + + config['tunnel_remote'] = to_str(config.get('tunnel_remote', '8.8.8.8')) + config['tunnel_remote_port'] = config.get('tunnel_remote_port', 53) + config['tunnel_port'] = config.get('tunnel_port', 53) logging.getLogger('').handlers = [] logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') @@ -284,15 +378,40 @@ Proxy options: -l LOCAL_PORT local port, default: 1080 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + Sodium: + chacha20-poly1305, chacha20-ietf-poly1305, + xchacha20-ietf-poly1305, + sodium:aes-256-gcm, + salsa20, chacha20, chacha20-ietf. + Sodium 1.0.12: + xchacha20 + OpenSSL: + aes-{128|192|256}-gcm, aes-{128|192|256}-cfb, + aes-{128|192|256}-ofb, aes-{128|192|256}-ctr, + camellia-{128|192|256}-cfb, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. + OpenSSL 1.1: + aes-{128|192|256}-ocb + mbedTLS: + mbedtls:aes-{128|192|256}-cfb128, + mbedtls:aes-{128|192|256}-ctr, + mbedtls:camellia-{128|192|256}-cfb128, + mbedtls:aes-{128|192|256}-gcm -t TIMEOUT timeout in seconds, default: 300 + -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + --libopenssl=PATH custom openssl crypto lib path + --libmbedtls=PATH custom mbedtls crypto lib path + --libsodium=PATH custom sodium crypto lib path 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 + --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 @@ -313,10 +432,37 @@ Proxy options: -p SERVER_PORT server port, default: 8388 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + Sodium: + chacha20-poly1305, chacha20-ietf-poly1305, + xchacha20-ietf-poly1305, + sodium:aes-256-gcm, + salsa20, chacha20, chacha20-ietf. + Sodium 1.0.12: + xchacha20 + OpenSSL: + aes-{128|192|256}-gcm, aes-{128|192|256}-cfb, + aes-{128|192|256}-ofb, aes-{128|192|256}-ctr, + camellia-{128|192|256}-cfb, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. + OpenSSL 1.1: + aes-{128|192|256}-ocb + mbedTLS: + mbedtls:aes-{128|192|256}-cfb128, + mbedtls:aes-{128|192|256}-ctr, + mbedtls:camellia-{128|192|256}-cfb128, + mbedtls:aes-{128|192|256}-gcm -t TIMEOUT timeout in seconds, default: 300 + -a ONE_TIME_AUTH one time auth --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 + --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 + --prefer-ipv6 resolve ipv6 address first + --libopenssl=PATH custom openssl crypto lib path + --libmbedtls=PATH custom mbedtls crypto lib path + --libsodium=PATH custom sodium crypto lib path General options: -h, --help show this help message and exit @@ -356,3 +502,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 4834883..0ef913f 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -26,17 +26,19 @@ import logging import traceback import random -from shadowsocks import encrypt, eventloop, shell, common -from shadowsocks.common import parse_header +from shadowsocks import cryptor, eventloop, shell, common +from shadowsocks.common import parse_header, onetimeauth_verify, \ + onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \ + ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH # 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 METHOD definition +METHOD_NOAUTH = 0 + # SOCKS command definition CMD_CONNECT = 1 CMD_BIND = 2 @@ -53,7 +55,7 @@ CMD_UDP_ASSOCIATE = 3 # for each handler, it could be at one of several stages: # as sslocal: -# stage 0 SOCKS hello received from local, send hello to local +# stage 0 auth METHOD received from local, reply with selection message # stage 1 addr received from local, query DNS for remote # stage 2 UDP assoc # stage 3 DNS resolved, connect to remote @@ -91,9 +93,22 @@ WAIT_STATUS_WRITING = 2 WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING BUF_SIZE = 32 * 1024 +UP_STREAM_BUF_SIZE = 16 * 1024 +DOWN_STREAM_BUF_SIZE = 32 * 1024 + +# helper exceptions for TCPRelayHandler + + +class BadSocksHeader(Exception): + pass + + +class NoAcceptableMethods(Exception): + pass class TCPRelayHandler(object): + def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): self._server = server @@ -103,13 +118,24 @@ class TCPRelayHandler(object): self._remote_sock = None self._config = config self._dns_resolver = dns_resolver + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = server._is_tunnel # TCP Relay works as either sslocal or ssserver # if is_local, this is sslocal self._is_local = is_local self._stage = STAGE_INIT - self._encryptor = encrypt.Encryptor(config['password'], - config['method']) + self._cryptor = cryptor.Cryptor(config['password'], + config['method'], + config['crypto_path']) + self._ota_enable = config.get('one_time_auth', False) + self._ota_enable_session = self._ota_enable + self._ota_buff_head = b'' + self._ota_buff_data = b'' + self._ota_len = 0 + self._ota_chunk_idx = 0 self._fastopen_connected = False self._data_to_write_to_local = [] self._data_to_write_to_remote = [] @@ -117,16 +143,14 @@ class TCPRelayHandler(object): self._downstream_status = WAIT_STATUS_INIT self._client_address = local_sock.getpeername()[:2] self._remote_address = None - if 'forbidden_ip' in config: - self._forbidden_iplist = config['forbidden_ip'] - else: - self._forbidden_iplist = None + self._forbidden_iplist = config.get('forbidden_ip') if is_local: self._chosen_server = self._get_a_server() fd_to_handlers[local_sock.fileno()] = self local_sock.setblocking(False) local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) - loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR) + loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR, + self._server) self.last_activity = 0 self._update_activity() @@ -149,10 +173,10 @@ class TCPRelayHandler(object): logging.debug('chosen server: %s:%d', server, server_port) 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 @@ -168,21 +192,23 @@ class TCPRelayHandler(object): if self._upstream_status != status: self._upstream_status = status dirty = True - if dirty: - if self._local_sock: - event = eventloop.POLL_ERR - if self._downstream_status & WAIT_STATUS_WRITING: - event |= eventloop.POLL_OUT - if self._upstream_status & WAIT_STATUS_READING: - event |= eventloop.POLL_IN - self._loop.modify(self._local_sock, event) - if self._remote_sock: - event = eventloop.POLL_ERR - if self._downstream_status & WAIT_STATUS_READING: - event |= eventloop.POLL_IN - if self._upstream_status & WAIT_STATUS_WRITING: - event |= eventloop.POLL_OUT - self._loop.modify(self._remote_sock, event) + if not dirty: + return + + if self._local_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + if self._upstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + self._loop.modify(self._local_sock, event) + if self._remote_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + if self._upstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + self._loop.modify(self._remote_sock, event) def _write_to_sock(self, data, sock): # write data to sock @@ -225,11 +251,19 @@ class TCPRelayHandler(object): return True def _handle_stage_connecting(self, data): - if self._is_local: - data = self._encryptor.encrypt(data) + if not self._is_local: + if self._ota_enable_session: + self._ota_chunk_data(data, + self._data_to_write_to_remote.append) + else: + self._data_to_write_to_remote.append(data) + return + if self._ota_enable_session: + data = self._ota_chunk_data_gen(data) + data = self._cryptor.encrypt(data) self._data_to_write_to_remote.append(data) - if self._is_local and not self._fastopen_connected and \ - self._config['fast_open']: + + if self._config['fast_open'] and not self._fastopen_connected: # for sslocal and fastopen, we basically wait for data and use # sendto to connect try: @@ -238,10 +272,11 @@ 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) + s = remote_sock.sendto(data, MSG_FASTOPEN, + self._chosen_server) if s < l: data = data[s:] self._data_to_write_to_remote = [data] @@ -262,9 +297,16 @@ class TCPRelayHandler(object): traceback.print_exc() self.destroy() + @shell.exception_handle(self_=True, destroy=True, conn_err=True) def _handle_stage_addr(self, data): - try: - if self._is_local: + if self._is_local: + if self._is_tunnel: + # add ss header to data + tunnel_remote = self.tunnel_remote + tunnel_remote_port = self.tunnel_remote_port + data = common.add_header(tunnel_remote, + tunnel_remote_port, data) + else: cmd = common.ord(data[1]) if cmd == CMD_UDP_ASSOCIATE: logging.debug('UDP associate') @@ -288,39 +330,66 @@ class TCPRelayHandler(object): logging.error('unknown command %d', cmd) self.destroy() return - header_result = parse_header(data) - if header_result is None: - raise Exception('can not parse header') - addrtype, remote_addr, remote_port, header_length = header_result - logging.info('connecting %s:%d from %s:%d' % - (common.to_str(remote_addr), remote_port, - self._client_address[0], self._client_address[1])) - self._remote_address = (common.to_str(remote_addr), remote_port) - # pause reading - self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) - self._stage = STAGE_DNS - if self._is_local: + header_result = parse_header(data) + if header_result is None: + raise Exception('can not parse header') + addrtype, remote_addr, remote_port, header_length = header_result + logging.info('connecting %s:%d from %s:%d' % + (common.to_str(remote_addr), remote_port, + self._client_address[0], self._client_address[1])) + if self._is_local is False: + # spec https://shadowsocks.org/en/spec/one-time-auth.html + self._ota_enable_session = addrtype & ADDRTYPE_AUTH + if self._ota_enable and not self._ota_enable_session: + logging.warn('client one time auth is required') + return + if self._ota_enable_session: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('one time auth header is too short') + return None + offset = header_length + ONETIMEAUTH_BYTES + _hash = data[header_length: offset] + _data = data[:header_length] + key = self._cryptor.decipher_iv + self._cryptor.key + if onetimeauth_verify(_hash, _data, key) is False: + logging.warn('one time auth fail') + self.destroy() + return + header_length += ONETIMEAUTH_BYTES + self._remote_address = (common.to_str(remote_addr), remote_port) + # pause reading + self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) + self._stage = STAGE_DNS + if self._is_local: + # jump over socks5 response + if not self._is_tunnel: # forward address to remote self._write_to_sock((b'\x05\x00\x00\x01' b'\x00\x00\x00\x00\x10\x10'), self._local_sock) - data_to_send = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data_to_send) - # notice here may go into _handle_dns_resolved directly - self._dns_resolver.resolve(self._chosen_server[0], - self._handle_dns_resolved) - else: - if len(data) > header_length: - self._data_to_write_to_remote.append(data[header_length:]) - # notice here may go into _handle_dns_resolved directly - self._dns_resolver.resolve(remote_addr, - self._handle_dns_resolved) - except Exception as e: - self._log_error(e) - if self._config['verbose']: - traceback.print_exc() - # TODO use logging when debug completed - self.destroy() + # spec https://shadowsocks.org/en/spec/one-time-auth.html + # ATYP & 0x10 == 0x10, then OTA is enabled. + if self._ota_enable_session: + data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] + key = self._cryptor.cipher_iv + self._cryptor.key + _header = data[:header_length] + sha110 = onetimeauth_gen(data, key) + data = _header + sha110 + data[header_length:] + data_to_send = self._cryptor.encrypt(data) + self._data_to_write_to_remote.append(data_to_send) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(self._chosen_server[0], + self._handle_dns_resolved) + else: + if self._ota_enable_session: + data = data[header_length:] + self._ota_chunk_data(data, + self._data_to_write_to_remote.append) + elif len(data) > header_length: + self._data_to_write_to_remote.append(data[header_length:]) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(remote_addr, + self._handle_dns_resolved) def _create_remote_socket(self, ip, port): addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, @@ -339,63 +408,160 @@ class TCPRelayHandler(object): remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) return remote_sock + @shell.exception_handle(self_=True) def _handle_dns_resolved(self, result, error): if error: - self._log_error(error) + addr, port = self._client_address[0], self._client_address[1] + logging.error('%s when handling connection from %s:%d' % + (error, addr, port)) + self.destroy() + return + if not (result and result[1]): self.destroy() return - if result: - ip = result[1] - if ip: - try: - self._stage = STAGE_CONNECTING - remote_addr = ip - if self._is_local: - remote_port = self._chosen_server[1] - else: - remote_port = self._remote_address[1] + ip = result[1] + self._stage = STAGE_CONNECTING + remote_addr = ip + if self._is_local: + remote_port = self._chosen_server[1] + else: + remote_port = self._remote_address[1] - if self._is_local and self._config['fast_open']: - # for fastopen: - # wait for more data to arrive and send them in one SYN - self._stage = STAGE_CONNECTING - # we don't have to wait for remote since it's not - # created - self._update_stream(STREAM_UP, WAIT_STATUS_READING) - # TODO when there is already data in this packet - else: - # else do connect - remote_sock = self._create_remote_socket(remote_addr, - remote_port) - try: - remote_sock.connect((remote_addr, remote_port)) - except (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == \ - errno.EINPROGRESS: - pass - self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT) - self._stage = STAGE_CONNECTING - self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) - self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + if self._is_local and self._config['fast_open']: + # for fastopen: + # wait for more data arrive and send them in one SYN + self._stage = STAGE_CONNECTING + # we don't have to wait for remote since it's not + # created + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + # TODO when there is already data in this packet + else: + # else do connect + remote_sock = self._create_remote_socket(remote_addr, + remote_port) + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == \ + errno.EINPROGRESS: + pass + self._loop.add(remote_sock, + eventloop.POLL_ERR | eventloop.POLL_OUT, + self._server) + self._stage = STAGE_CONNECTING + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + + def _write_to_sock_remote(self, data): + self._write_to_sock(data, self._remote_sock) + + def _ota_chunk_data(self, data, data_cb): + # spec https://shadowsocks.org/en/spec/one-time-auth.html + unchunk_data = b'' + while len(data) > 0: + if self._ota_len == 0: + # get DATA.LEN + HMAC-SHA1 + length = ONETIMEAUTH_CHUNK_BYTES - len(self._ota_buff_head) + self._ota_buff_head += data[:length] + data = data[length:] + if len(self._ota_buff_head) < ONETIMEAUTH_CHUNK_BYTES: + # wait more data return - except Exception as e: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() - self.destroy() + data_len = self._ota_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN] + self._ota_len = struct.unpack('>H', data_len)[0] + length = min(self._ota_len - len(self._ota_buff_data), len(data)) + self._ota_buff_data += data[:length] + data = data[length:] + if len(self._ota_buff_data) == self._ota_len: + # get a chunk data + _hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:] + _data = self._ota_buff_data + index = struct.pack('>I', self._ota_chunk_idx) + key = self._cryptor.decipher_iv + index + if onetimeauth_verify(_hash, _data, key) is False: + logging.warn('one time auth fail, drop chunk !') + else: + unchunk_data += _data + self._ota_chunk_idx += 1 + self._ota_buff_head = b'' + self._ota_buff_data = b'' + self._ota_len = 0 + data_cb(unchunk_data) + return + + def _ota_chunk_data_gen(self, data): + data_len = struct.pack(">H", len(data)) + index = struct.pack('>I', self._ota_chunk_idx) + key = self._cryptor.cipher_iv + index + sha110 = onetimeauth_gen(data, key) + self._ota_chunk_idx += 1 + return data_len + sha110 + data + + def _handle_stage_stream(self, data): + if self._is_local: + if self._ota_enable_session: + data = self._ota_chunk_data_gen(data) + data = self._cryptor.encrypt(data) + self._write_to_sock(data, self._remote_sock) + else: + if self._ota_enable_session: + self._ota_chunk_data(data, self._write_to_sock_remote) + else: + self._write_to_sock(data, self._remote_sock) + return + + def _check_auth_method(self, data): + # VER, NMETHODS, and at least 1 METHODS + if len(data) < 3: + logging.warning('method selection header too short') + raise BadSocksHeader + socks_version = common.ord(data[0]) + nmethods = common.ord(data[1]) + if socks_version != 5: + logging.warning('unsupported SOCKS protocol version ' + + str(socks_version)) + raise BadSocksHeader + if nmethods < 1 or len(data) != nmethods + 2: + logging.warning('NMETHODS and number of METHODS mismatch') + raise BadSocksHeader + noauth_exist = False + for method in data[2:]: + if common.ord(method) == METHOD_NOAUTH: + noauth_exist = True + break + if not noauth_exist: + logging.warning('none of SOCKS METHOD\'s ' + 'requested by client is supported') + raise NoAcceptableMethods + + def _handle_stage_init(self, data): + try: + self._check_auth_method(data) + except BadSocksHeader: + self.destroy() + return + except NoAcceptableMethods: + self._write_to_sock(b'\x05\xff', self._local_sock) + self.destroy() + return + + self._write_to_sock(b'\x05\00', self._local_sock) + self._stage = STAGE_ADDR def _on_local_read(self): # handle all local read events and dispatch them to methods for # each stage - self._update_activity() if not self._local_sock: return is_local = self._is_local data = None + if is_local: + buf_size = UP_STREAM_BUF_SIZE + else: + buf_size = DOWN_STREAM_BUF_SIZE try: - data = self._local_sock.recv(BUF_SIZE) + data = self._local_sock.recv(buf_size) except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): @@ -403,20 +569,21 @@ class TCPRelayHandler(object): if not data: self.destroy() return + self._update_activity(len(data)) if not is_local: - data = self._encryptor.decrypt(data) + data = self._cryptor.decrypt(data) if not data: return if self._stage == STAGE_STREAM: - if self._is_local: - data = self._encryptor.encrypt(data) - self._write_to_sock(data, self._remote_sock) + self._handle_stage_stream(data) return elif is_local and self._stage == STAGE_INIT: - # TODO check auth method - self._write_to_sock(b'\x05\00', self._local_sock) - self._stage = STAGE_ADDR - return + # jump over socks5 init + if self._is_tunnel: + self._handle_stage_addr(data) + return + else: + self._handle_stage_init(data) elif self._stage == STAGE_CONNECTING: self._handle_stage_connecting(data) elif (is_local and self._stage == STAGE_ADDR) or \ @@ -425,10 +592,14 @@ class TCPRelayHandler(object): def _on_remote_read(self): # handle all remote read events - self._update_activity() data = None + if self._is_local: + buf_size = UP_STREAM_BUF_SIZE + else: + buf_size = DOWN_STREAM_BUF_SIZE try: - data = self._remote_sock.recv(BUF_SIZE) + 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): @@ -436,10 +607,11 @@ class TCPRelayHandler(object): if not data: self.destroy() return + self._update_activity(len(data)) if self._is_local: - data = self._encryptor.decrypt(data) + data = self._cryptor.decrypt(data) else: - data = self._encryptor.encrypt(data) + data = self._cryptor.encrypt(data) try: self._write_to_sock(data, self._local_sock) except Exception as e: @@ -480,6 +652,7 @@ class TCPRelayHandler(object): logging.error(eventloop.get_sock_error(self._remote_sock)) self.destroy() + @shell.exception_handle(self_=True, destroy=True) def handle_event(self, sock, event): # handle all events in this handler and dispatch them to methods if self._stage == STAGE_DESTROYED: @@ -511,10 +684,6 @@ class TCPRelayHandler(object): else: logging.warn('unknown socket') - def _log_error(self, e): - logging.error('%s when handling connection from %s:%d' % - (e, self._client_address[0], self._client_address[1])) - def destroy(self): # destroy the handler and release any resources # promises: @@ -550,14 +719,15 @@ 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._is_tunnel = False self._timeout = config['timeout'] self._timeouts = [] # a list for all the handlers @@ -591,6 +761,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: @@ -598,10 +769,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) @@ -610,10 +780,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 @@ -659,53 +832,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(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): - continue - else: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() - else: - if sock: - handler = self._fd_to_handlers.get(fd, None) - if handler: - handler.handle_event(sock, event) + 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/tunnel.py b/shadowsocks/tunnel.py new file mode 100755 index 0000000..dbfb438 --- /dev/null +++ b/shadowsocks/tunnel.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2012-2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os +import logging +import signal + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns + + +@shell.exception_handle(self_=False, exit_code=1) +def main(): + shell.check_python() + + # fix py2exe + if hasattr(sys, "frozen") and sys.frozen in \ + ("windows_exe", "console_exe"): + p = os.path.dirname(os.path.abspath(sys.executable)) + os.chdir(p) + + config = shell.get_config(True) + daemon.daemon_exec(config) + dns_resolver = asyncdns.DNSResolver() + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + _config = config.copy() + _config["local_port"] = _config["tunnel_port"] + logging.info("starting tcp tunnel at %s:%d forward to %s:%d" % + (_config['local_address'], _config['local_port'], + _config['tunnel_remote'], _config['tunnel_remote_port'])) + tunnel_tcp_server = tcprelay.TCPRelay(_config, dns_resolver, True) + tunnel_tcp_server._is_tunnel = True + tunnel_tcp_server.add_to_loop(loop) + logging.info("starting udp tunnel at %s:%d forward to %s:%d" % + (_config['local_address'], _config['local_port'], + _config['tunnel_remote'], _config['tunnel_remote_port'])) + tunnel_udp_server = udprelay.UDPRelay(_config, dns_resolver, True) + tunnel_udp_server._is_tunnel = True + tunnel_udp_server.add_to_loop(loop) + + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tunnel_tcp_server.close(next_tick=True) + tunnel_udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + + def int_handler(signum, _): + sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + daemon.set_user(config.get('user', None)) + loop.run() + +if __name__ == '__main__': + main() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 98bfaaa..f726717 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -62,26 +62,28 @@ 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, shell -from shadowsocks.common import parse_header, pack_addr +from shadowsocks import cryptor, eventloop, lru_cache, common, shell +from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \ + onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH 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'] @@ -93,34 +95,39 @@ class UDPRelay(object): self._listen_port = config['server_port'] self._remote_addr = None self._remote_port = None + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = False 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._ota_enable = config.get('one_time_auth', False) + self._ota_enable_session = self._ota_enable self._is_local = is_local self._cache = lru_cache.LRUCache(timeout=config['timeout'], close_callback=self._close_client) self._client_fd_to_server_addr = \ lru_cache.LRUCache(timeout=config['timeout']) + self._dns_cache = lru_cache.LRUCache(timeout=300) self._eventloop = None self._closed = False - self._last_time = time.time() self._sockets = set() - if 'forbidden_ip' in config: - self._forbidden_iplist = config['forbidden_ip'] - else: - self._forbidden_iplist = None + self._forbidden_iplist = config.get('forbidden_ip') + self._crypto_path = config['crypto_path'] addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) if len(addrs) == 0: - raise Exception("can't get addrinfo for %s:%d" % + raise Exception("UDP can't get addrinfo for %s:%d" % (self._listen_addr, self._listen_port)) af, socktype, proto, canonname, sa = addrs[0] server_socket = socket.socket(af, socktype, proto) server_socket.bind((self._listen_addr, self._listen_port)) server_socket.setblocking(False) self._server_socket = server_socket + self._stat_callback = stat_callback def _get_a_server(self): server = self._config['server'] @@ -144,18 +151,35 @@ class UDPRelay(object): def _handle_server(self): server = self._server_socket data, r_addr = server.recvfrom(BUF_SIZE) + key = None + iv = None if not data: logging.debug('UDP handle_server: data is empty') + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) if self._is_local: - frag = common.ord(data[2]) - if frag != 0: - logging.warn('drop a message since frag is not 0') - return + if self._is_tunnel: + # add ss header to data + tunnel_remote = self.tunnel_remote + tunnel_remote_port = self.tunnel_remote_port + data = common.add_header(tunnel_remote, + tunnel_remote_port, data) else: - data = data[3:] + frag = common.ord(data[2]) + if frag != 0: + logging.warn('UDP drop a message since frag is not 0') + return + else: + data = data[3:] else: - data = encrypt.encrypt_all(self._password, self._method, 0, data) # decrypt data + try: + data, key, iv = cryptor.decrypt_all(self._password, + self._method, + data, self._crypto_path) + except Exception: + logging.debug('UDP handle_server: decrypt data failed') + return if not data: logging.debug('UDP handle_server: data is empty after decrypt') return @@ -163,38 +187,67 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - + logging.info("udp data to %s:%d from %s:%d" + % (dest_addr, dest_port, r_addr[0], r_addr[1])) if self._is_local: server_addr, server_port = self._get_a_server() else: server_addr, server_port = dest_addr, dest_port + # spec https://shadowsocks.org/en/spec/one-time-auth.html + self._ota_enable_session = addrtype & ADDRTYPE_AUTH + if self._ota_enable and not self._ota_enable_session: + logging.warn('client one time auth is required') + return + if self._ota_enable_session: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('UDP one time auth header is too short') + return + _hash = data[-ONETIMEAUTH_BYTES:] + data = data[: -ONETIMEAUTH_BYTES] + _key = iv + key + if onetimeauth_verify(_hash, data, _key) is False: + logging.warn('UDP one time auth fail') + return + 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 - key = client_key(r_addr[0], r_addr[1], dest_addr, dest_port) + 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) + key, iv, m = cryptor.gen_key_iv(self._password, self._method) + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._ota_enable_session: + data = self._ota_chunk_data_gen(key, iv, data) + try: + data = cryptor.encrypt_all_m(key, iv, m, self._method, data, + self._crypto_path) + except Exception: + logging.debug("UDP handle_server: encrypt data failed") + return if not data: return else: @@ -215,68 +268,98 @@ class UDPRelay(object): if not data: logging.debug('UDP handle_client: data is empty') return + if self._stat_callback: + self._stat_callback(self._listen_port, len(data)) if not self._is_local: addrlen = len(r_addr[0]) if addrlen > 255: # drop return data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data - response = encrypt.encrypt_all(self._password, self._method, 1, - data) + try: + response = cryptor.encrypt_all(self._password, + self._method, data, + self._crypto_path) + except Exception: + logging.debug("UDP handle_client: encrypt data failed") + return if not response: return else: - data = encrypt.encrypt_all(self._password, self._method, 0, - data) + try: + data, key, iv = cryptor.decrypt_all(self._password, + self._method, data, + self._crypto_path) + except Exception: + logging.debug('UDP handle_client: decrypt data failed') + return if not data: return header_result = parse_header(data) if header_result is None: return - # addrtype, dest_addr, dest_port, header_length = header_result - response = b'\x00\x00\x00' + data + addrtype, dest_addr, dest_port, header_length = header_result + if self._is_tunnel: + # remove ss header + response = data[header_length:] + else: + response = b'\x00\x00\x00' + data client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: + logging.debug("send udp response to %s:%d" + % (client_addr[0], client_addr[1])) self._server_socket.sendto(response, client_addr) else: # this packet is from somewhere else we know # simply drop that packet pass + def _ota_chunk_data_gen(self, key, iv, data): + data = common.chr(common.ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + key = iv + key + return data + onetimeauth_gen(data, key) + def add_to_loop(self, loop): if self._eventloop: raise Exception('already add to loop') if self._closed: raise Exception('already closed') self._eventloop = loop - loop.add_handler(self._handle_events) 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() + self._dns_cache.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/snapcraft.yaml b/snapcraft.yaml new file mode 100644 index 0000000..6860270 --- /dev/null +++ b/snapcraft.yaml @@ -0,0 +1,23 @@ +name: shadowsocks +version: 2.9.1-1 +summary: A fast tunnel proxy that helps you bypass firewalls +description: A fast tunnel proxy that helps you bypass firewalls +confinement: strict +grade: stable + +apps: + sslocal: + command: bin/sslocal + plugs: [network, network-bind] + aliases: [sslocal] + + ssserver: + command: bin/ssserver + plugs: [network, network-bind] + aliases: [ssserver] + +parts: + shadowsocks: + plugin: python + python-version: python2 + source: https://github.com/shadowsocks/shadowsocks/archive/2.9.1.tar.gz diff --git a/tests/aes-cfb1.json b/tests/aes-cfb1.json index 40d0b21..70ae670 100644 --- a/tests/aes-cfb1.json +++ b/tests/aes-cfb1.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-cfb1", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb1", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-cfb8.json b/tests/aes-cfb8.json index fb7014b..fe8b715 100644 --- a/tests/aes-cfb8.json +++ b/tests/aes-cfb8.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-cfb8", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb8", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-ctr.json b/tests/aes-ctr.json index 1fed8a8..1f5a1d7 100644 --- a/tests/aes-ctr.json +++ b/tests/aes-ctr.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-ctr", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-gcm.json b/tests/aes-gcm.json new file mode 100644 index 0000000..271d721 --- /dev/null +++ b/tests/aes-gcm.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-gcm", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes-ocb.json b/tests/aes-ocb.json new file mode 100644 index 0000000..9c1d5b8 --- /dev/null +++ b/tests/aes-ocb.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-ocb", + "local_address":"127.0.0.1", + "fast_open":false, + "libopenssl":"/usr/local/lib/libcrypto.so.1.1" +} diff --git a/tests/aes-ofb.json b/tests/aes-ofb.json new file mode 100644 index 0000000..0e2003f --- /dev/null +++ b/tests/aes-ofb.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-ofb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/aes.json b/tests/aes.json index a3d95b9..2fc29f3 100644 --- a/tests/aes.json +++ b/tests/aes.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-cfb", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/camellia.json b/tests/camellia.json new file mode 100644 index 0000000..d7ffd8c --- /dev/null +++ b/tests/camellia.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"camellia_password", + "timeout":60, + "method":"camellia-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/chacha20-ietf-poly1305.json b/tests/chacha20-ietf-poly1305.json new file mode 100644 index 0000000..074dccb --- /dev/null +++ b/tests/chacha20-ietf-poly1305.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"chacha20-ietf-poly1305", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/chacha20-ietf.json b/tests/chacha20-ietf.json new file mode 100644 index 0000000..a27987c --- /dev/null +++ b/tests/chacha20-ietf.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"chacha20-ietf", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/chacha20-poly1305.json b/tests/chacha20-poly1305.json new file mode 100644 index 0000000..d9f2191 --- /dev/null +++ b/tests/chacha20-poly1305.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"chacha20-poly1305", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/chacha20.json b/tests/chacha20.json index 541a9be..6def51b 100644 --- a/tests/chacha20.json +++ b/tests/chacha20.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"salsa20_password", - "timeout":60, - "method":"chacha20", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"chacha20_password", + "timeout":60, + "method":"chacha20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/client-multi-server-ip.json b/tests/client-multi-server-ip.json index 1823c2a..3050c11 100644 --- a/tests/client-multi-server-ip.json +++ b/tests/client-multi-server-ip.json @@ -1,10 +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 -} +{ + "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/fastopen.json b/tests/fastopen.json index f3980b6..b4f60d4 100644 --- a/tests/fastopen.json +++ b/tests/fastopen.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"fastopen_password", - "timeout":60, - "method":"aes-256-cfb", - "local_address":"127.0.0.1", - "fast_open":true -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"fastopen_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":true +} diff --git a/tests/gen_multiple_passwd.py b/tests/gen_multiple_passwd.py new file mode 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..c00c5df --- /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/tests/ipv6-client-side.json b/tests/ipv6-client-side.json index 6c3cfaf..e14bb07 100644 --- a/tests/ipv6-client-side.json +++ b/tests/ipv6-client-side.json @@ -1,10 +1,10 @@ -{ - "server":"::1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-cfb", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"::1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/ipv6.json b/tests/ipv6.json index d855f9c..18263ee 100644 --- a/tests/ipv6.json +++ b/tests/ipv6.json @@ -1,10 +1,10 @@ -{ - "server":"::", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"aes-256-cfb", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"::", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/jenkins.sh b/tests/jenkins.sh index 71d5b1c..85dd01a 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -33,22 +33,47 @@ run_test coverage run tests/nose_plugin.py -v run_test python setup.py sdist run_test tests/test_daemon.sh run_test python tests/test.py --with-coverage -c tests/aes.json +run_test python tests/test.py --with-coverage -c tests/mbedtls-aes.json +run_test python tests/test.py --with-coverage -c tests/aes-gcm.json +run_test python tests/test.py --with-coverage -c tests/aes-ocb.json +run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-gcm.json run_test python tests/test.py --with-coverage -c tests/aes-ctr.json +run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-ctr.json run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json +run_test python tests/test.py --with-coverage -c tests/aes-ofb.json +run_test python tests/test.py --with-coverage -c tests/camellia.json +run_test python tests/test.py --with-coverage -c tests/mbedtls-camellia.json run_test python tests/test.py --with-coverage -c tests/rc4-md5.json run_test python tests/test.py --with-coverage -c tests/salsa20.json run_test python tests/test.py --with-coverage -c tests/chacha20.json +run_test python tests/test.py --with-coverage -c tests/xchacha20.json +run_test python tests/test.py --with-coverage -c tests/chacha20-ietf.json +run_test python tests/test.py --with-coverage -c tests/chacha20-poly1305.json +run_test python tests/test.py --with-coverage -c tests/xchacha20-ietf-poly1305.json +run_test python tests/test.py --with-coverage -c tests/chacha20-ietf-poly1305.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-dnsserver.json -c tests/client-multi-server-ip.json run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json run_test python tests/test.py --with-coverage -c tests/workers.json -run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json +run_test python tests/test.py --with-coverage -c tests/rc4-md5-ota.json +# travis-ci not support IPv6 +# run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv" run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" +# test custom lib path + +run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libopenssl=/usr/local/lib/libcrypto.so" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=/usr/local/lib/libcrypto.so" +run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=/usr/local/lib/libmbedcrypto.so" -a "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=/usr/local/lib/libmbedcrypto.so" +run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=/usr/local/lib/libsodium.so" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=/usr/local/lib/libsodium.so" +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= --libopenssl=invalid_path" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=invalid_path" +run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=invalid_path" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=invalid_path" +run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=invalid_path" -a "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=invalid_path" + # test if DNS works run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" @@ -70,13 +95,17 @@ fi run_test tests/test_large_file.sh +if [ "a$JENKINS" != "a1" ] ; then + # jenkins blocked SIGQUIT with sigprocmask(), we have to skip this test on Jenkins + run_test tests/test_graceful_restart.sh +fi +run_test tests/test_udp_src.sh run_test tests/test_command.sh -coverage combine && coverage report --include=shadowsocks/* -rm -rf htmlcov -rm -rf tmp -coverage html --include=shadowsocks/* - -coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage +# coverage combine && coverage report --include=shadowsocks/* +# rm -rf htmlcov +# rm -rf tmp +# coverage html --include=shadowsocks/* +# coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage exit $result diff --git a/tests/libmbedtls/install.sh b/tests/libmbedtls/install.sh new file mode 100755 index 0000000..59fb512 --- /dev/null +++ b/tests/libmbedtls/install.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +MBEDTLS_VER=2.4.2 +if [ ! -d mbedtls-$MBEDTLS_VER ]; then + wget https://tls.mbed.org/download/mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1 + tar xf mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1 +fi +pushd mbedtls-$MBEDTLS_VER +make SHARED=1 CFLAGS=-fPIC && sudo make install || exit 1 +sudo ldconfig +popd +rm -rf mbedtls-$MBEDTLS_VER || exit 1 diff --git a/tests/libopenssl/install.sh b/tests/libopenssl/install.sh new file mode 100755 index 0000000..480c772 --- /dev/null +++ b/tests/libopenssl/install.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +OPENSSL_VER=1.1.0e +if [ ! -d openssl-$OPENSSL_VER ]; then + wget https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz || exit 1 + tar xf openssl-$OPENSSL_VER.tar.gz || exit 1 +fi +pushd openssl-$OPENSSL_VER +./config && make && sudo make install || exit 1 +# sudo ldconfig # test multiple libcrypto +popd +rm -rf openssl-$OPENSSL_VER || exit 1 + +rm /usr/bin/openssl || exit 1 +rm -r /usr/include/openssl || exit 1 +ln -s /usr/local/bin/openssl /usr/bin/openssl || exit 1 +ln -s /usr/local/include/openssl /usr/include/openssl || exit 1 +echo /usr/local/lib >> /etc/ld.so.conf || exit 1 +ldconfig -v || exit 1 diff --git a/tests/libsodium/install.sh b/tests/libsodium/install.sh index b0e35fa..215ef72 100755 --- a/tests/libsodium/install.sh +++ b/tests/libsodium/install.sh @@ -1,10 +1,11 @@ #!/bin/bash -if [ ! -d libsodium-1.0.1 ]; then - wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 - tar xf libsodium-1.0.1.tar.gz || exit 1 +if [ ! -d libsodium-1.0.12 ]; then + wget https://github.com/jedisct1/libsodium/releases/download/1.0.12/libsodium-1.0.12.tar.gz || exit 1 + tar xf libsodium-1.0.12.tar.gz || exit 1 fi -pushd libsodium-1.0.1 +pushd libsodium-1.0.12 ./configure && make -j2 && make install || exit 1 sudo ldconfig popd +rm -rf libsodium-1.0.12 || exit 1 diff --git a/tests/mbedtls-aes-ctr.json b/tests/mbedtls-aes-ctr.json new file mode 100644 index 0000000..0740eea --- /dev/null +++ b/tests/mbedtls-aes-ctr.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"mbedtls:aes-256-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/mbedtls-aes-gcm.json b/tests/mbedtls-aes-gcm.json new file mode 100644 index 0000000..c504e1c --- /dev/null +++ b/tests/mbedtls-aes-gcm.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"mbedtls:aes-256-gcm", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/mbedtls-aes.json b/tests/mbedtls-aes.json new file mode 100644 index 0000000..c0b097b --- /dev/null +++ b/tests/mbedtls-aes.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"mbedtls:aes-256-cfb128", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/mbedtls-camellia.json b/tests/mbedtls-camellia.json new file mode 100644 index 0000000..480e2ed --- /dev/null +++ b/tests/mbedtls-camellia.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"camellia_password", + "timeout":60, + "method":"mbedtls:camellia-256-cfb128", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/rc4-md5-ota.json b/tests/rc4-md5-ota.json new file mode 100644 index 0000000..6c312c7 --- /dev/null +++ b/tests/rc4-md5-ota.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"rc4-md5", + "local_address":"127.0.0.1", + "fast_open":false, + "one_time_auth":true +} diff --git a/tests/rc4-md5.json b/tests/rc4-md5.json index 26ba0df..e69b433 100644 --- a/tests/rc4-md5.json +++ b/tests/rc4-md5.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"aes_password", - "timeout":60, - "method":"rc4-md5", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"rc4-md5", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/salsa20-ctr.json b/tests/salsa20-ctr.json index 5ca6c45..8b77d07 100644 --- a/tests/salsa20-ctr.json +++ b/tests/salsa20-ctr.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"salsa20_password", - "timeout":60, - "method":"salsa20-ctr", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"salsa20-ctr", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/salsa20.json b/tests/salsa20.json index 7e30380..a4a664f 100644 --- a/tests/salsa20.json +++ b/tests/salsa20.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"salsa20_password", - "timeout":60, - "method":"salsa20", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"salsa20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/server-dnsserver.json b/tests/server-dnsserver.json new file mode 100644 index 0000000..f8d5902 --- /dev/null +++ b/tests/server-dnsserver.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false, + "dns_server": ["8.8.8.8","8.8.4.4"] +} 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/socksify/install.sh b/tests/socksify/install.sh index 8eff72d..86c4e1d 100755 --- a/tests/socksify/install.sh +++ b/tests/socksify/install.sh @@ -1,8 +1,12 @@ #!/bin/bash -if [ ! -d dante-1.4.0 ]; then - wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 +if [ ! -d dante-1.4.0 ] || [ ! -d dante-1.4.0/configure ]; then + rm dante-1.4.0 -rf + #wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 + wget https://codeload.github.com/notpeter/dante/tar.gz/dante-1.4.0 -O dante-1.4.0.tar.gz || exit 1 tar xf dante-1.4.0.tar.gz || exit 1 + # + mv dante-dante-1.4.0 dante-1.4.0 fi pushd dante-1.4.0 ./configure && make -j4 && make install || exit 1 diff --git a/tests/table.json b/tests/table.json index cca6ac2..49c2c01 100644 --- a/tests/table.json +++ b/tests/table.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"table_password", - "timeout":60, - "method":"table", - "local_address":"127.0.0.1", - "fast_open":false -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"table_password", + "timeout":60, + "method":"table", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/test.py b/tests/test.py index 29b57d4..85bd007 100755 --- a/tests/test.py +++ b/tests/test.py @@ -44,7 +44,7 @@ parser.add_argument('--dns', type=str, default='8.8.8.8') config = parser.parse_args() if config.with_coverage: - python = ['coverage', 'run', '-p', '-a'] + python = ['coverage', 'run', '-a'] client_args = python + ['shadowsocks/local.py', '-v'] server_args = python + ['shadowsocks/server.py', '-v'] diff --git a/tests/test_command.sh b/tests/test_command.sh index be05704..9af91c3 100755 --- a/tests/test_command.sh +++ b/tests/test_command.sh @@ -2,7 +2,7 @@ . tests/assert.sh -PYTHON="coverage run -a -p" +PYTHON="coverage run -a" LOCAL="$PYTHON shadowsocks/local.py" SERVER="$PYTHON shadowsocks/server.py" @@ -30,7 +30,7 @@ $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 -k mypassword -s 0.0.0.0 -p 8388 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " DON'T USE DEFAULT PASSWORD! Please change it in your config.json!" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop -assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified" +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" @@ -39,7 +39,7 @@ $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d sto 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" +assert "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " Not a valid CIDR notation: 127.0.0.1/4a" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop assert_end command diff --git a/tests/test_daemon.sh b/tests/test_daemon.sh index 40f35ef..1acddc0 100755 --- a/tests/test_daemon.sh +++ b/tests/test_daemon.sh @@ -18,7 +18,7 @@ function run_test { for module in local server do -command="coverage run -p -a shadowsocks/$module.py" +command="coverage run -a shadowsocks/$module.py" mkdir -p tmp diff --git a/tests/test_graceful_restart.sh b/tests/test_graceful_restart.sh new file mode 100755 index 0000000..eb4bc0f --- /dev/null +++ b/tests/test_graceful_restart.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +PYTHON="coverage run -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 33bcb59..7a6aec8 100755 --- a/tests/test_large_file.sh +++ b/tests/test_large_file.sh @@ -1,6 +1,6 @@ #!/bin/bash -PYTHON="coverage run -p -a" +PYTHON="coverage run -a" URL=http://127.0.0.1/file mkdir -p tmp diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py new file mode 100644 index 0000000..585606d --- /dev/null +++ b/tests/test_udp_src.py @@ -0,0 +1,85 @@ +#!/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..4a07a23 --- /dev/null +++ b/tests/test_udp_src.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PYTHON="coverage run -a" + +mkdir -p tmp + +$PYTHON shadowsocks/local.py -c tests/aes.json -v & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & +SERVER=$! + +sleep 3 + +python tests/test_udp_src.py +r=$? + +kill -s SIGINT $LOCAL +kill -s SIGINT $SERVER + +sleep 2 + +exit $r diff --git a/tests/workers.json b/tests/workers.json index 2015ff6..8e0943b 100644 --- a/tests/workers.json +++ b/tests/workers.json @@ -1,10 +1,10 @@ -{ - "server":"127.0.0.1", - "server_port":8388, - "local_port":1081, - "password":"workers_password", - "timeout":60, - "method":"aes-256-cfb", - "local_address":"127.0.0.1", - "workers": 4 -} +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"workers_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "workers": 4 +} diff --git a/tests/xchacha20-ietf-poly1305.json b/tests/xchacha20-ietf-poly1305.json new file mode 100644 index 0000000..91d27dd --- /dev/null +++ b/tests/xchacha20-ietf-poly1305.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"salsa20_password", + "timeout":60, + "method":"xchacha20-ietf-poly1305", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/tests/xchacha20.json b/tests/xchacha20.json new file mode 100644 index 0000000..bf6f365 --- /dev/null +++ b/tests/xchacha20.json @@ -0,0 +1,10 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"xchacha20_password", + "timeout":60, + "method":"xchacha20", + "local_address":"127.0.0.1", + "fast_open":false +} diff --git a/utils/autoban.py b/utils/autoban.py index 1bbb65c..52aa163 100755 --- a/utils/autoban.py +++ b/utils/autoban.py @@ -24,9 +24,17 @@ from __future__ import absolute_import, division, print_function, \ with_statement -import os import sys +import socket import argparse +import subprocess + + +def inet_pton(str_ip): + try: + return socket.inet_pton(socket.AF_INET, str_ip) + except socket.error: + return None if __name__ == '__main__': parser = argparse.ArgumentParser(description='See README') @@ -37,17 +45,22 @@ if __name__ == '__main__': ips = {} banned = set() for line in sys.stdin: - if 'can not parse header when' in line: - ip = line.split()[-1].split(':')[0] - if ip not in ips: - ips[ip] = 1 - print(ip) - sys.stdout.flush() - else: - ips[ip] += 1 - if ip not in banned and ips[ip] >= config.count: - banned.add(ip) - cmd = 'iptables -A INPUT -s %s -j DROP' % ip - print(cmd, file=sys.stderr) - sys.stderr.flush() - os.system(cmd) + if 'can not parse header when' not in line: + continue + ip_str = line.split()[-1].rsplit(':', 1)[0] + ip = inet_pton(ip_str) + if ip is None: + continue + if ip not in ips: + ips[ip] = 1 + sys.stdout.flush() + else: + ips[ip] += 1 + if ip not in banned and ips[ip] >= config.count: + banned.add(ip) + print('ban ip %s' % ip_str) + cmd = ['iptables', '-A', 'INPUT', '-s', ip_str, '-j', 'DROP', + '-m', 'comment', '--comment', 'autoban'] + print(' '.join(cmd), file=sys.stderr) + sys.stderr.flush() + subprocess.call(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+$