diff --git a/.travis.yml b/.travis.yml index 535bc9a..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 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/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/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/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 1f843fb..3fb017f 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -244,13 +244,13 @@ def __str__(self): return '%s: %s' % (self.hostname, str(self.answers)) -STATUS_IPV4 = 0 -STATUS_IPV6 = 1 +STATUS_FIRST = 0 +STATUS_SECOND = 1 class DNSResolver(object): - def __init__(self, server_list=None): + def __init__(self, server_list=None, prefer_ipv6=False): self._loop = None self._hosts = {} self._hostname_status = {} @@ -263,6 +263,10 @@ def __init__(self, server_list=None): self._parse_resolv() else: self._servers = server_list + if prefer_ipv6: + self._QTYPES = [QTYPE_AAAA, QTYPE_A] + else: + self._QTYPES = [QTYPE_A, QTYPE_AAAA] self._parse_hosts() # TODO monitor hosts change and reload hosts # TODO parse /etc/gai.conf and follow its rules @@ -350,17 +354,18 @@ def _handle_data(self, data): 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 @@ -426,14 +431,14 @@ def resolve(self, hostname, callback): 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: diff --git a/shadowsocks/common.py b/shadowsocks/common.py index ee14995..1a58457 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -146,6 +146,7 @@ def patch_socket(): 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) @@ -160,6 +161,13 @@ 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 diff --git a/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py index 4498876..dd8305c 100644 --- a/shadowsocks/crypto/aead.py +++ b/shadowsocks/crypto/aead.py @@ -71,7 +71,7 @@ sodium_loaded = False -def load_sodium(): +def load_sodium(path=None): """ Load libsodium helpers for nonce increment :return: None @@ -79,12 +79,14 @@ def load_sodium(): global libsodium, sodium_loaded libsodium = util.find_library('sodium', 'sodium_increment', - 'libsodium') + '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 @@ -139,7 +141,7 @@ class AeadCryptoBase(object): +--------+-----------+-----------+ """ - def __init__(self, cipher_name, key, iv, op): + 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] @@ -158,7 +160,9 @@ def __init__(self, cipher_name, key, iv, op): # load libsodium for nonce increment if not sodium_loaded: - load_sodium() + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('sodium', None) + load_sodium(path) def nonce_increment(self): """ @@ -171,6 +175,7 @@ def nonce_increment(self): 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): """ @@ -178,7 +183,6 @@ def cipher_ctx_init(self): :return: None """ self.nonce_increment() - # print("".join("%02x" % ord(b) for b in self._nonce)) def aead_encrypt(self, data): """ diff --git a/shadowsocks/crypto/mbedtls.py b/shadowsocks/crypto/mbedtls.py new file mode 100644 index 0000000..b87c2eb --- /dev/null +++ b/shadowsocks/crypto/mbedtls.py @@ -0,0 +1,481 @@ +#!/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): + from shadowsocks.crypto import openssl + + print(method, ': [stream]', 32) + cipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = openssl.OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def run_aead_method(method, key_len=16): + from shadowsocks.crypto import openssl + + print(method, ': [payload][tag]', key_len) + key_len = int(key_len) + cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = openssl.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): + from shadowsocks.crypto import openssl + + 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 = openssl.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_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 27afd27..f034bd1 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -39,14 +39,16 @@ CIPHER_ENC_UNCHANGED = -1 -def load_openssl(): +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 @@ -89,11 +91,11 @@ class OpenSSLCryptoBase(object): """ OpenSSL crypto base class """ - def __init__(self, cipher_name): + 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: @@ -137,15 +139,16 @@ def clean(self): if 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): - super(OpenSSLAeadCrypto, self).__init__(cipher_name) - AeadCryptoBase.__init__(self, cipher_name, key, iv, op) + 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( @@ -174,7 +177,7 @@ def __init__(self, cipher_name, key, iv, op): def cipher_ctx_init(self): """ Need init cipher context after EVP_CipherFinal_ex to reuse context - :return: void + :return: None """ iv_ptr = c_char_p(self._nonce.raw) r = libcrypto.EVP_CipherInit_ex( @@ -194,7 +197,7 @@ def set_tag(self, tag): """ Set tag before decrypt any data (update) :param tag: authenticated tag - :return: void + :return: None """ tag_len = self._tlen r = libcrypto.EVP_CIPHER_CTX_ctrl( @@ -279,8 +282,8 @@ class OpenSSLStreamCrypto(OpenSSLCryptoBase): """ Crypto for stream modes: cfb, ofb, ctr """ - def __init__(self, cipher_name, key, iv, op): - super(OpenSSLStreamCrypto, self).__init__(cipher_name) + 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, @@ -300,9 +303,6 @@ def decrypt(self, data): 'aes-128-cfb': (16, 16, OpenSSLStreamCrypto), 'aes-192-cfb': (24, 16, OpenSSLStreamCrypto), 'aes-256-cfb': (32, 16, OpenSSLStreamCrypto), - 'aes-128-gcm': (16, 16, OpenSSLAeadCrypto), - 'aes-192-gcm': (24, 24, OpenSSLAeadCrypto), - 'aes-256-gcm': (32, 32, OpenSSLAeadCrypto), 'aes-128-ofb': (16, 16, OpenSSLStreamCrypto), 'aes-192-ofb': (24, 16, OpenSSLStreamCrypto), 'aes-256-ofb': (32, 16, OpenSSLStreamCrypto), @@ -325,6 +325,13 @@ def decrypt(self, data): '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), } @@ -371,10 +378,6 @@ def run_aead_method_chunk(method, key_len=16): util.run_cipher(cipher, decipher) -def test_aes_128_cfb(): - run_method('aes-128-cfb') - - def test_aes_gcm(bits=128): method = "aes-{0}-gcm".format(bits) run_aead_method(method, bits / 8) @@ -395,6 +398,10 @@ def test_aes_ocb_chunk(bits=128): run_aead_method_chunk(method, bits / 8) +def test_aes_128_cfb(): + run_method('aes-128-cfb') + + def test_aes_256_cfb(): run_method('aes-256-cfb') @@ -422,6 +429,7 @@ 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) diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 2f7f22e..014fa3c 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -23,13 +23,14 @@ __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.OpenSSLStreamCrypto(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 764ef1f..ac736c5 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -17,7 +17,7 @@ from __future__ import absolute_import, division, print_function, \ with_statement -from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \ +from ctypes import c_char_p, c_int, c_uint, c_ulonglong, byref, \ create_string_buffer, c_void_p from shadowsocks.crypto import util @@ -36,18 +36,21 @@ BLOCK_SIZE = 64 -def load_libsodium(): +def load_libsodium(crypto_path=None): global loaded, libsodium, buf + crypto_path = dict(crypto_path) if crypto_path else dict() + path = crypto_path.get('sodium', None) + if not aead.sodium_loaded: - aead.load_sodium() + aead.load_sodium(path) if aead.sodium_loaded: libsodium = aead.libsodium else: - print('load libsodium again') + print('load libsodium again with path %s' % path) libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', - 'libsodium') + 'libsodium', path) if libsodium is None: raise Exception('libsodium not found') @@ -55,39 +58,52 @@ def load_libsodium(): 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_ulong, - c_char_p) + 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_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 + 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_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 + c_char_p, c_char_p # npub, k ) # chacha20-ietf-poly1305, same api structure as above @@ -154,9 +170,9 @@ def load_libsodium(): 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) @@ -165,6 +181,11 @@ def __init__(self, cipher_name, key, iv, op): 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: @@ -209,32 +230,34 @@ def clean(self): class SodiumAeadCrypto(AeadCryptoBase): - def __init__(self, cipher_name, key, iv, op): + def __init__(self, cipher_name, key, iv, op, crypto_path=None): if not loaded: - load_libsodium() - AeadCryptoBase.__init__(self, cipher_name, key, iv, op) + 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.\ + self.encryptor = libsodium. \ crypto_aead_chacha20poly1305_ietf_encrypt - self.decryptor = libsodium.\ + 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.\ + self.encryptor = libsodium. \ crypto_aead_xchacha20poly1305_ietf_encrypt - self.decryptor = libsodium.\ + self.decryptor = libsodium. \ crypto_aead_xchacha20poly1305_ietf_decrypt else: - raise Exception('Unknown cipher') + 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') @@ -280,7 +303,7 @@ def aead_decrypt(self, data): raise Exception("Decrypt failed") if cipher_out_len.value != clen - self._tlen: - raise Exception("Encrypt failed") + raise Exception("Decrypt failed") self.cipher_ctx_init() return buf.raw[:cipher_out_len.value] @@ -295,7 +318,9 @@ def decrypt_once(self, 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), @@ -303,26 +328,31 @@ def decrypt_once(self, data): } -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) +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_chacha20(): +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) - 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_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) @@ -407,6 +437,7 @@ def test_aes_256_gcm_chunk(): if __name__ == '__main__': test_chacha20() + test_xchacha20() test_salsa20() test_chacha20_ietf() test_chacha20_poly1305() diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index f44909f..1752be5 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -55,8 +55,7 @@ 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 diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index cf20e24..f8950f6 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -41,9 +41,27 @@ def find_library_nt(name): 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 = [] @@ -81,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 diff --git a/shadowsocks/cryptor.py b/shadowsocks/cryptor.py index 719b1b6..4eae9e8 100644 --- a/shadowsocks/cryptor.py +++ b/shadowsocks/cryptor.py @@ -23,7 +23,7 @@ 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 @@ -36,6 +36,7 @@ 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) @@ -46,8 +47,8 @@ def random_string(length): cached_keys = {} -def try_cipher(key, method=None): - Cryptor(key, method) +def try_cipher(key, method=None, crypto_path=None): + Cryptor(key, method, crypto_path) def EVP_BytesToKey(password, key_len, iv_len): @@ -75,7 +76,14 @@ def EVP_BytesToKey(password, key_len, iv_len): class Cryptor(object): - def __init__(self, password, method): + 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 @@ -83,6 +91,7 @@ def __init__(self, password, method): self.cipher_iv = b'' self.decipher = None self.decipher_iv = None + self.crypto_path = crypto_path method = method.lower() self._method_info = Cryptor.get_method_info(method) if self._method_info: @@ -118,7 +127,7 @@ def get_cipher(self, password, method, op, iv): if op == CIPHER_ENC_ENCRYPTION: # this iv is for cipher not decipher self.cipher_iv = iv - return m[METHOD_INFO_CRYPTO](method, key, iv, op) + return m[METHOD_INFO_CRYPTO](method, key, iv, op, self.crypto_path) def encrypt(self, buf): if len(buf) == 0: @@ -139,7 +148,7 @@ def decrypt(self, buf): self.decipher = self.get_cipher( self.password, self.method, CIPHER_ENC_DECRYPTION, - iv=decipher_iv + decipher_iv ) buf = buf[decipher_iv_len:] if len(buf) == 0: @@ -158,30 +167,30 @@ def gen_key_iv(password, method): return key, iv, m -def encrypt_all_m(key, iv, m, method, data): +def encrypt_all_m(key, iv, m, method, data, crypto_path=None): result = [iv] - cipher = m(method, key, iv, 1) + cipher = m(method, key, iv, 1, crypto_path) result.append(cipher.encrypt_once(data)) return b''.join(result) -def decrypt_all(password, method, data): +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) + 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): +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) + cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION, crypto_path) result.append(cipher.encrypt_once(data)) return b''.join(result) @@ -189,6 +198,7 @@ def encrypt_all(password, method, data): CIPHERS_TO_TEST = [ 'aes-128-cfb', 'aes-256-cfb', + 'aes-256-gcm', 'rc4-md5', 'salsa20', 'chacha20', diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py deleted file mode 100644 index 1d4d066..0000000 --- a/shadowsocks/encrypt.py +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2012-2015 clowwindy -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import absolute_import, division, print_function, \ - with_statement - -import os -import sys -import hashlib -import logging - -from shadowsocks import common -from shadowsocks.crypto import rc4_md5, openssl, sodium, table - - -method_supported = {} -method_supported.update(rc4_md5.ciphers) -method_supported.update(openssl.ciphers) -method_supported.update(sodium.ciphers) -method_supported.update(table.ciphers) - - -def random_string(length): - return os.urandom(length) - - -cached_keys = {} - - -def try_cipher(key, method=None): - Encryptor(key, method) - - -def EVP_BytesToKey(password, key_len, iv_len): - # equivalent to OpenSSL's EVP_BytesToKey() with count 1 - # so that we make the same key and iv as nodejs version - cached_key = '%s-%d-%d' % (password, key_len, iv_len) - r = cached_keys.get(cached_key, None) - if r: - return r - m = [] - i = 0 - while len(b''.join(m)) < (key_len + iv_len): - md5 = hashlib.md5() - data = password - if i > 0: - data = m[i - 1] + password - md5.update(data) - m.append(md5.digest()) - i += 1 - ms = b''.join(m) - key = ms[:key_len] - iv = ms[key_len:key_len + iv_len] - cached_keys[cached_key] = (key, iv) - return key, iv - - -class Encryptor(object): - - def __init__(self, password, method): - self.password = password - self.key = None - self.method = method - self.iv_sent = False - self.cipher_iv = b'' - self.decipher = None - self.decipher_iv = None - method = method.lower() - self._method_info = self.get_method_info(method) - if self._method_info: - self.cipher = self.get_cipher(password, method, 1, - random_string(self._method_info[1])) - else: - logging.error('method %s not supported' % method) - sys.exit(1) - - def get_method_info(self, method): - method = method.lower() - m = method_supported.get(method) - return m - - def iv_len(self): - return len(self.cipher_iv) - - def get_cipher(self, password, method, op, iv): - password = common.to_bytes(password) - m = self._method_info - if m[0] > 0: - key, iv_ = EVP_BytesToKey(password, m[0], m[1]) - else: - # key_length == 0 indicates we should use the key directly - key, iv = password, b'' - self.key = key - iv = iv[:m[1]] - if op == 1: - # this iv is for cipher not decipher - self.cipher_iv = iv[:m[1]] - return m[2](method, key, iv, op) - - def encrypt(self, buf): - if len(buf) == 0: - return buf - if self.iv_sent: - return self.cipher.update(buf) - else: - self.iv_sent = True - return self.cipher_iv + self.cipher.update(buf) - - def decrypt(self, buf): - if len(buf) == 0: - return buf - if self.decipher is None: - decipher_iv_len = self._method_info[1] - decipher_iv = buf[:decipher_iv_len] - self.decipher_iv = decipher_iv - self.decipher = self.get_cipher(self.password, self.method, 0, - iv=decipher_iv) - buf = buf[decipher_iv_len:] - if len(buf) == 0: - return buf - return self.decipher.update(buf) - - -def gen_key_iv(password, method): - method = method.lower() - (key_len, iv_len, m) = method_supported[method] - key = None - if key_len > 0: - key, _ = EVP_BytesToKey(password, key_len, iv_len) - else: - key = password - iv = random_string(iv_len) - return key, iv, m - - -def encrypt_all_m(key, iv, m, method, data): - result = [] - result.append(iv) - cipher = m(method, key, iv, 1) - result.append(cipher.update(data)) - return b''.join(result) - - -def dencrypt_all(password, method, data): - result = [] - method = method.lower() - (key_len, iv_len, m) = method_supported[method] - key = None - if key_len > 0: - key, _ = EVP_BytesToKey(password, key_len, iv_len) - else: - key = password - iv = data[:iv_len] - data = data[iv_len:] - cipher = m(method, key, iv, 0) - result.append(cipher.update(data)) - return b''.join(result), key, iv - - -def encrypt_all(password, method, op, data): - result = [] - method = method.lower() - (key_len, iv_len, m) = method_supported[method] - key = None - if key_len > 0: - key, _ = EVP_BytesToKey(password, key_len, iv_len) - else: - key = password - if op: - iv = random_string(iv_len) - result.append(iv) - else: - iv = data[:iv_len] - data = data[iv_len:] - cipher = m(method, key, iv, op) - result.append(cipher.update(data)) - return b''.join(result) - - -CIPHERS_TO_TEST = [ - 'aes-128-cfb', - 'aes-256-cfb', - 'rc4-md5', - 'salsa20', - 'chacha20', - 'table', -] - - -def test_encryptor(): - from os import urandom - plain = urandom(10240) - for method in CIPHERS_TO_TEST: - logging.warn(method) - encryptor = Encryptor(b'key', method) - decryptor = Encryptor(b'key', method) - cipher = encryptor.encrypt(plain) - plain2 = decryptor.decrypt(cipher) - assert plain == plain2 - - -def test_encrypt_all(): - from os import urandom - plain = urandom(10240) - for method in CIPHERS_TO_TEST: - logging.warn(method) - cipher = encrypt_all(b'key', method, 1, plain) - plain2 = encrypt_all(b'key', method, 0, cipher) - assert plain == plain2 - - -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 = dencrypt_all(b'key', method, cipher) - assert plain == plain2 - - -if __name__ == '__main__': - test_encrypt_all() - test_encryptor() - test_encrypt_all_m() diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index ff4fc7d..55cb346 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -79,18 +79,15 @@ def sweep(self): least = self._last_visits[0] if now - least <= self.timeout: break - if self.close_callback is not None: - for key in self._time_to_keys[least]: - if key in self._store: - if now - self._keys_to_last_time[key] > self.timeout: - value = self._store[key] - if value not in self._closed_values: - self.close_callback(value) - self._closed_values.add(value) 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) del self._store[key] del self._keys_to_last_time[key] c += 1 @@ -140,6 +137,7 @@ def close_cb(t): 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 index d35f9e1..4855aae 100755 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -71,6 +71,7 @@ def __init__(self, config): # 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) @@ -158,6 +159,8 @@ def _parse_command(self, data): 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) @@ -191,22 +194,8 @@ def send_data(data_dict): self._statistics.clear() def _send_control_data(self, data): - if self._control_client_addr: - try: - self._control_socket.sendto(data, self._control_client_addr) - except (socket.error, OSError, IOError) as e: - error_no = eventloop.errno_from_exception(e) - if sys.platform == "win32": - if error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK, errno.WSAEWOULDBLOCK): - return - elif error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK): - return - else: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() + if not self._control_client_addr: + return try: self._control_socket.sendto(data, self._control_client_addr) diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 1be4c0f..4dc5621 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -58,9 +58,10 @@ def main(): udp_servers = [] if 'dns_server' in config: # allow override settings in resolv.conf - dns_resolver = asyncdns.DNSResolver(config['dns_server']) + dns_resolver = asyncdns.DNSResolver(config['dns_server'], + config['prefer_ipv6']) else: - dns_resolver = asyncdns.DNSResolver() + dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6']) port_password = config['port_password'] del config['port_password'] diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 151a534..e115b28 100755 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -129,6 +129,29 @@ 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) @@ -147,6 +170,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']: @@ -172,8 +200,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']) - cryptor.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): @@ -184,11 +223,12 @@ def get_config(is_local): if is_local: 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:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', - 'forbidden-ip=', 'user=', 'manager-address=', '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) @@ -232,10 +272,16 @@ def get_config(is_local): 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'] = value + config['manager_address'] = to_str(value) elif key == '--user': config['user'] = to_str(value) elif key == '--forbidden-ip': @@ -258,6 +304,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) @@ -280,22 +328,16 @@ def get_config(is_local): config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) config['one_time_auth'] = config.get('one_time_auth', False) - 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['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') @@ -342,28 +384,38 @@ def print_local_help(): -m METHOD encryption method, default: aes-256-cfb Sodium: chacha20-poly1305, chacha20-ietf-poly1305, - *xchacha20-ietf-poly1305, + xchacha20-ietf-poly1305, sodium:aes-256-gcm, salsa20, chacha20, chacha20-ietf. - OpenSSL:(* v1.1) - *aes-128-ocb, *aes-192-ocb, *aes-256-ocb, - aes-128-gcm, aes-192-gcm, aes-256-gcm, - aes-128-cfb, aes-192-cfb, aes-256-cfb, - aes-128-ctr, aes-192-ctr, aes-256-ctr, - camellia-128-cfb, camellia-192-cfb, - camellia-256-cfb, + 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 @@ -386,25 +438,35 @@ def print_server_help(): -m METHOD encryption method, default: aes-256-cfb Sodium: chacha20-poly1305, chacha20-ietf-poly1305, - *xchacha20-ietf-poly1305, + xchacha20-ietf-poly1305, sodium:aes-256-gcm, salsa20, chacha20, chacha20-ietf. - OpenSSL:(* v1.1) - *aes-128-ocb, *aes-192-ocb, *aes-256-ocb, - aes-128-gcm, aes-192-gcm, aes-256-gcm, - aes-128-cfb, aes-192-cfb, aes-256-cfb, - aes-128-ctr, aes-192-ctr, aes-256-ctr, - camellia-128-cfb, camellia-192-cfb, - camellia-256-cfb, + 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 - --manager-address ADDR optional server manager UDP address, see wiki + --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 diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index c9e1b4c..8a9c3f3 100755 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -38,6 +38,9 @@ MSG_FASTOPEN = 0x20000000 +# SOCKS METHOD definition +METHOD_NOAUTH = 0 + # SOCKS command definition CMD_CONNECT = 1 CMD_BIND = 2 @@ -54,7 +57,7 @@ # 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 @@ -92,6 +95,18 @@ 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 # helper exceptions for TCPRelayHandler @@ -115,17 +130,19 @@ def __init__(self, server, fd_to_handlers, loop, local_sock, config, 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._cryptor = cryptor.Cryptor(config['password'], - config['method']) - if 'one_time_auth' in config: - self._ota_enable = config['one_time_auth'] - else: - self._ota_enable = False + 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'' @@ -137,10 +154,7 @@ def __init__(self, server, fd_to_handlers, loop, local_sock, config, self._upstream_status = WAIT_STATUS_READING self._downstream_status = WAIT_STATUS_INIT 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 @@ -189,21 +203,23 @@ def _update_stream(self, stream, status): 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 @@ -250,13 +266,8 @@ def _write_to_sock(self, data, sock): return True def _handle_stage_connecting(self, data): - if self._is_local: - if self._ota_enable: - data = self._ota_chunk_data_gen(data) - data = self._cryptor.encrypt(data) - self._data_to_write_to_remote.append(data) - else: - if self._ota_enable: + if not self._is_local: + if self._ota_enable_session: self._ota_chunk_data(data, self._data_to_write_to_remote.append) else: @@ -267,8 +278,7 @@ def _handle_stage_connecting(self, 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: @@ -301,6 +311,7 @@ def _handle_stage_connecting(self, data): shell.print_exception(e) self.destroy() + @shell.exception_handle(self_=True, destroy=True, conn_err=True) def _handle_stage_addr(self, data): try: addr, port = self._local_sock.getpeername()[:2] @@ -360,6 +371,7 @@ def _handle_stage_addr(self, data): 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 or (addrtype & ADDRTYPE_AUTH == ADDRTYPE_AUTH): if not self._ota_enable and self._config['verbose']: logging.info('U[%d] TCP one time auth automatically enabled' % self._config['server_port']) @@ -429,55 +441,57 @@ def _create_remote_socket(self, ip, port): 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 and result[1]: - ip = result[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 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: - 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 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 (socket.error, OSError, IOError) as e: - if eventloop.errno_from_exception(e) == \ - errno.EINPROGRESS: - pass - self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT, - self._server) - self._stage = STAGE_CONNECTING - self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) - self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) - return - except Exception as e: - shell.print_exception(e) - self.destroy() + 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 @@ -500,13 +514,14 @@ def _ota_chunk_data(self, data, data_cb): key = self._cryptor.decipher_iv + index if onetimeauth_verify(_hash, _data, key) is False: logging.warn('U[%d] TCP One time auth fail, chunk is dropped!' % self._config[ - 'server_port']) + 'server_port']) else: - data_cb(self._ota_buff_data) + 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): @@ -519,17 +534,55 @@ def _ota_chunk_data_gen(self, data): def _handle_stage_stream(self, data): if self._is_local: - if self._ota_enable: + 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: + 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 @@ -537,16 +590,15 @@ def _on_local_read(self): 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) - except (socket.error, OSError, IOError) as e: - error_no = eventloop.errno_from_exception(e) - if sys.platform == "win32": - if error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK, errno.WSAEWOULDBLOCK): - return - elif error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK): + data = self._local_sock.recv(buf_size) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in \ + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): return if not data: self.destroy() @@ -560,10 +612,12 @@ def _on_local_read(self): 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 \ @@ -573,8 +627,12 @@ def _on_local_read(self): def _on_remote_read(self): # handle all remote read events 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 socket.error as err: error_no = err.args[0] @@ -597,6 +655,8 @@ def _on_remote_read(self): self._write_to_sock(data, self._local_sock) except Exception as e: shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() # TODO use logging when debug completed self.destroy() @@ -714,6 +774,7 @@ def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._closed = False self._eventloop = None self._fd_to_handlers = {} + self._is_tunnel = False self._timeout = config['timeout'] self._timeouts = [] # a list for all the handlers 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 40c4ceb..3fc8d9a 100755 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -96,14 +96,16 @@ def __init__(self, config, dns_resolver, is_local, stat_callback=None): 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 = common.to_bytes(config['password']) self._method = config['method'] self._timeout = config['timeout'] - if 'one_time_auth' in config: - self._one_time_auth_enable = config['one_time_auth'] - else: - self._one_time_auth_enable = False + 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) @@ -113,10 +115,8 @@ def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._eventloop = None self._closed = False self._sockets = set() - if 'forbidden_ip' in config: - self._forbidden_iplist = config['forbidden_ip'] - else: - self._forbidden_iplist = None + 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) @@ -162,13 +162,19 @@ def _handle_server(self): 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('U[%d] UDP drop a message since frag is not 0' % self._config[ - 'server_port']) - 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: # decrypt data try: @@ -216,10 +222,11 @@ def _handle_server(self): else: server_addr, server_port = dest_addr, dest_port # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._one_time_auth_enable or (addrtype & ADDRTYPE_AUTH == ADDRTYPE_AUTH): - if not self._one_time_auth_enable and self._config['verbose']: - logging.info('U[%d] UDP one time auth automatically enabled' % self._config['server_port']) - self._one_time_auth_enable = True + 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('U[%d] UDP one time auth header is too short' % self._config[ 'server_port']) @@ -263,10 +270,11 @@ def _handle_server(self): if self._is_local: key, iv, m = cryptor.gen_key_iv(self._password, self._method) # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._one_time_auth_enable: + 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) + 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 @@ -308,7 +316,8 @@ def _handle_client(self, sock): data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data try: response = cryptor.encrypt_all(self._password, - self._method, data) + self._method, data, + self._crypto_path) except Exception: logging.debug("UDP handle_client: encrypt data failed") return @@ -317,7 +326,8 @@ def _handle_client(self, sock): else: try: data, key, iv = cryptor.decrypt_all(self._password, - self._method, data) + self._method, data, + self._crypto_path) except Exception: logging.debug('UDP handle_client: decrypt data failed') return @@ -325,9 +335,16 @@ def _handle_client(self, sock): return if parse_header(data) is None: return - 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 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/graceful.json b/tests/graceful.json index 7d94ea5..c00c5df 100644 --- a/tests/graceful.json +++ b/tests/graceful.json @@ -1,10 +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 -} +{ + "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/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 6d0fac8..85dd01a 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -33,12 +33,25 @@ 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 @@ -52,6 +65,15 @@ run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0 run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" +# test 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" @@ -80,11 +102,10 @@ 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..e948bff --- /dev/null +++ b/tests/libopenssl/install.sh @@ -0,0 +1,12 @@ +#!/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 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 index 3566d6d..6c312c7 100644 --- a/tests/rc4-md5-ota.json +++ b/tests/rc4-md5-ota.json @@ -1,11 +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 -} +{ + "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 index 5d55cdc..f8d5902 100644 --- a/tests/server-dnsserver.json +++ b/tests/server-dnsserver.json @@ -1,11 +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"] -} +{ + "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/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_command.sh b/tests/test_command.sh index 8225740..9af91c3 100755 --- a/tests/test_command.sh +++ b/tests/test_command.sh @@ -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/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 49c022d..458e1d4 100755 --- a/utils/autoban.py +++ b/utils/autoban.py @@ -38,7 +38,7 @@ banned = set() for line in sys.stdin: if 'can not parse header when' in line: - ip = line.split()[-1].split(':')[0] + ip = line.split()[-1].split(':')[-2] if ip not in ips: ips[ip] = 1 print(ip)