From 6683dfd3bb61fac52f77d35055c3956bb8f7c43f Mon Sep 17 00:00:00 2001 From: mengskysama Date: Fri, 9 Oct 2015 14:49:09 +0800 Subject: [PATCH 01/68] fix manger bug --- shadowsocks/manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py index e8009b4..b42ffa9 100644 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -29,7 +29,7 @@ from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell BUF_SIZE = 1506 -STAT_SEND_LIMIT = 100 +STAT_SEND_LIMIT = 50 class Manager(object): @@ -167,7 +167,9 @@ class Manager(object): if i >= STAT_SEND_LIMIT: send_data(r) r.clear() - send_data(r) + i = 0 + if len(r) > 0: + send_data(r) self._statistics.clear() def _send_control_data(self, data): From 4d24b7477a9b969d9b3f13c8e55fb586cdbf2e7a Mon Sep 17 00:00:00 2001 From: mengskysama Date: Mon, 26 Oct 2015 09:43:48 +0800 Subject: [PATCH 02/68] fix bug! if _last_visits[index] len > 1 --- shadowsocks/lru_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index 401f19b..ff4fc7d 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -87,8 +87,8 @@ class LRUCache(collections.MutableMapping): 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]: - self._last_visits.popleft() if key in self._store: if now - self._keys_to_last_time[key] > self.timeout: del self._store[key] From 0aba0aa40c0a8249d5b7f9488ee7cf9314a05554 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Mon, 26 Oct 2015 09:45:02 +0800 Subject: [PATCH 03/68] fix memory leak! _dns_cache bommmm --- shadowsocks/udprelay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 96d1fbd..ab12c54 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -285,6 +285,7 @@ class UDPRelay(object): logging.info('closed UDP port %d', self._listen_port) self._cache.sweep() self._client_fd_to_server_addr.sweep() + self._dns_cache.sweep() def close(self, next_tick=False): logging.debug('UDP close') From 34851fbcb312d7d44b9b6ed0ad6f409c3abe5c7c Mon Sep 17 00:00:00 2001 From: mengskysama Date: Mon, 26 Oct 2015 10:48:52 +0800 Subject: [PATCH 04/68] readme --- README.md | 6 ++++++ README.rst | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index 7cda7e7..007df02 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +About shadowsocks-rm +---------------- + +This project is https://github.com/shadowsocks/shadowsocks clone. I JUST fix bug on the original code. Unless it is necessary to have additional features. + + shadowsocks =========== diff --git a/README.rst b/README.rst index bf2a3ec..fe6fd51 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,8 @@ +About shadowsocks-rm +--------------- + +This project is https://github.com/shadowsocks/shadowsocks clone. I JUST fix bug on the original code. Unless it is necessary to have additional features. + shadowsocks =========== From 173323c0f867d2e7e821966536a13879befa3ca9 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Fri, 13 Nov 2015 14:29:27 +0800 Subject: [PATCH 05/68] Update common.py https://github.com/mengskysama/shadowsocks-rm/issues/4 --- shadowsocks/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks/common.py b/shadowsocks/common.py index db4beea..857bde6 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -154,7 +154,7 @@ def parse_header(data): elif addrtype == ADDRTYPE_HOST: if len(data) > 2: addrlen = ord(data[1]) - if len(data) >= 2 + addrlen: + if len(data) >= 4 + addrlen: dest_addr = data[2:2 + addrlen] dest_port = struct.unpack('>H', data[2 + addrlen:4 + addrlen])[0] From 767b9217f874db37e947f5883f06e11e64ba9861 Mon Sep 17 00:00:00 2001 From: jsy Date: Sun, 6 Dec 2015 02:02:25 +0800 Subject: [PATCH 06/68] one time auth server side --- shadowsocks/common.py | 29 +++++++++++--- shadowsocks/encrypt.py | 15 +++++--- shadowsocks/shell.py | 3 ++ shadowsocks/tcprelay.py | 83 +++++++++++++++++++++++++++++++++++++---- shadowsocks/udprelay.py | 21 ++++++++++- 5 files changed, 130 insertions(+), 21 deletions(-) diff --git a/shadowsocks/common.py b/shadowsocks/common.py index 857bde6..7ec04ba 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -21,6 +21,21 @@ from __future__ import absolute_import, division, print_function, \ import socket import struct import logging +import hashlib +import hmac + + +ONETIMEAUTH_BYTES = 10 +ONETIMEAUTH_CHUNK_BYTES = 12 +ONETIMEAUTH_CHUNK_DATA_LEN = 2 + + +def sha1_hmac(secret, data): + return hmac.new(secret, data, hashlib.sha1).digest() + + +def onetimeauth_verify(_hash, data, key): + return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] def compat_ord(s): @@ -118,9 +133,11 @@ def patch_socket(): patch_socket() -ADDRTYPE_IPV4 = 1 -ADDRTYPE_IPV6 = 4 -ADDRTYPE_HOST = 3 +ADDRTYPE_IPV4 = 0x01 +ADDRTYPE_IPV6 = 0x04 +ADDRTYPE_HOST = 0x03 +ADDRTYPE_AUTH = 0x10 +ADDRTYPE_MASK = 0xF def pack_addr(address): @@ -144,14 +161,14 @@ def parse_header(data): dest_addr = None dest_port = None header_length = 0 - if addrtype == ADDRTYPE_IPV4: + if addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV4: if len(data) >= 7: dest_addr = socket.inet_ntoa(data[1:5]) dest_port = struct.unpack('>H', data[5:7])[0] header_length = 7 else: logging.warn('header is too short') - elif addrtype == ADDRTYPE_HOST: + elif addrtype & ADDRTYPE_MASK == ADDRTYPE_HOST: if len(data) > 2: addrlen = ord(data[1]) if len(data) >= 4 + addrlen: @@ -163,7 +180,7 @@ def parse_header(data): logging.warn('header is too short') else: logging.warn('header is too short') - elif addrtype == ADDRTYPE_IPV6: + elif addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV6: if len(data) >= 19: dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) dest_port = struct.unpack('>H', data[17:19])[0] diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 4e87f41..54dbb42 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -69,17 +69,18 @@ def EVP_BytesToKey(password, key_len, iv_len): class Encryptor(object): - def __init__(self, key, method): - self.key = key + def __init__(self, password, method): + self.password = password + self.key = None self.method = method - self.iv = None self.iv_sent = False self.cipher_iv = b'' self.decipher = None + self.decipher_iv = None method = method.lower() self._method_info = self.get_method_info(method) if self._method_info: - self.cipher = self.get_cipher(key, method, 1, + self.cipher = self.get_cipher(password, method, 1, random_string(self._method_info[1])) else: logging.error('method %s not supported' % method) @@ -101,7 +102,7 @@ class Encryptor(object): 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 @@ -123,7 +124,8 @@ class Encryptor(object): if self.decipher is None: decipher_iv_len = self._method_info[1] decipher_iv = buf[:decipher_iv_len] - self.decipher = self.get_cipher(self.key, self.method, 0, + 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: @@ -135,6 +137,7 @@ 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: diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index c91fc22..e2284da 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -174,6 +174,8 @@ def get_config(is_local): v_count += 1 # '-vv' turns on more verbose mode config['verbose'] = v_count + elif key == '-a': + config['one_time_auth'] = True elif key == '-t': config['timeout'] = int(value) elif key == '--fast-open': @@ -315,6 +317,7 @@ Proxy options: -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb -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 diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index d11af31..22ba104 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -27,7 +27,8 @@ import traceback import random from shadowsocks import encrypt, eventloop, shell, common -from shadowsocks.common import parse_header +from shadowsocks.common import parse_header, onetimeauth_verify, \ + ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time TIMEOUTS_CLEAN_SIZE = 512 @@ -107,6 +108,14 @@ class TCPRelayHandler(object): self._stage = STAGE_INIT self._encryptor = encrypt.Encryptor(config['password'], config['method']) + if 'one_time_auth' in config and config['one_time_auth']: + self._one_time_auth_enable = True + else: + self._one_time_auth_enable = False + self._one_time_auth_buff_head = '' + self._one_time_auth_buff_data = '' + self._one_time_auth_len = 0 + self._one_time_auth_chunk_idx = 0 self._fastopen_connected = False self._data_to_write_to_local = [] self._data_to_write_to_remote = [] @@ -225,7 +234,11 @@ class TCPRelayHandler(object): def _handle_stage_connecting(self, data): if self._is_local: data = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data) + if self._one_time_auth_enable: + self._one_time_auth_chunk_data(data, + self._data_to_write_to_remote.append) + else: + self._data_to_write_to_remote.append(data) if self._is_local and not self._fastopen_connected and \ self._config['fast_open']: # for sslocal and fastopen, we basically wait for data and use @@ -293,6 +306,17 @@ class TCPRelayHandler(object): logging.info('connecting %s:%d from %s:%d' % (common.to_str(remote_addr), remote_port, self._client_address[0], self._client_address[1])) + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('one time auth header is too short') + return None + if onetimeauth_verify(data[header_length: header_length+ONETIMEAUTH_BYTES], + data[:header_length], + self._encryptor.decipher_iv + self._encryptor.key) is False: + logging.warn('one time auth fail') + self.destroy() + header_length += ONETIMEAUTH_BYTES self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) @@ -308,7 +332,11 @@ class TCPRelayHandler(object): self._dns_resolver.resolve(self._chosen_server[0], self._handle_dns_resolved) else: - if len(data) > header_length: + if self._one_time_auth_enable: + data = data[header_length:] + self._one_time_auth_chunk_data(data, + self._data_to_write_to_remote.append) + elif len(data) > header_length: self._data_to_write_to_remote.append(data[header_length:]) # notice here may go into _handle_dns_resolved directly self._dns_resolver.resolve(remote_addr, @@ -344,7 +372,6 @@ class TCPRelayHandler(object): if result: ip = result[1] if ip: - try: self._stage = STAGE_CONNECTING remote_addr = ip @@ -384,6 +411,50 @@ class TCPRelayHandler(object): traceback.print_exc() self.destroy() + def _write_to_sock_remote(self, data): + self._write_to_sock(data, self._remote_sock) + + def _one_time_auth_chunk_data(self, data, data_cb): + # spec https://shadowsocks.org/en/spec/one-time-auth.html + while len(data) > 0: + if self._one_time_auth_len == 0: + # get DATA.LEN + HMAC-SHA1 + length = ONETIMEAUTH_CHUNK_BYTES - len(self._one_time_auth_buff_head) + self._one_time_auth_buff_head += data[:length] + data = data[length:] + if len(self._one_time_auth_buff_head) < ONETIMEAUTH_CHUNK_BYTES: + # wait more data + return + self._one_time_auth_len = struct.unpack('>H', + self._one_time_auth_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN])[0] + length = min(self._one_time_auth_len, len(data)) + self._one_time_auth_buff_data += data[:length] + data = data[length:] + if len(self._one_time_auth_buff_data) == self._one_time_auth_len: + # get a chunk data + if onetimeauth_verify(self._one_time_auth_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:], + self._one_time_auth_buff_data, + self._encryptor.decipher_iv + struct.pack('>I', self._one_time_auth_chunk_idx)) \ + is False: + # + logging.warn('one time auth fail, drop chunk !') + else: + data_cb(self._one_time_auth_buff_data) + self._one_time_auth_buff_head = '' + self._one_time_auth_buff_data = '' + self._one_time_auth_chunk_idx += 1 + self._one_time_auth_len = 0 + return + + def _handle_stage_stream(self, data): + if self._is_local: + data = self._encryptor.encrypt(data) + if self._one_time_auth_enable: + self._one_time_auth_chunk_data(data, self._write_to_sock_remote) + else: + self._write_to_sock(data, self._remote_sock) + return + def _on_local_read(self): # handle all local read events and dispatch them to methods for # each stage @@ -406,9 +477,7 @@ class TCPRelayHandler(object): if not data: return if self._stage == STAGE_STREAM: - if self._is_local: - data = self._encryptor.encrypt(data) - self._write_to_sock(data, self._remote_sock) + self._handle_stage_stream(data) return elif is_local and self._stage == STAGE_INIT: # TODO check auth method diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index ab12c54..bef46af 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -69,7 +69,8 @@ import errno import random from shadowsocks import encrypt, eventloop, lru_cache, common, shell -from shadowsocks.common import parse_header, pack_addr +from shadowsocks.common import parse_header, pack_addr, \ + ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH BUF_SIZE = 65536 @@ -97,6 +98,10 @@ class UDPRelay(object): self._password = common.to_bytes(config['password']) self._method = config['method'] self._timeout = config['timeout'] + if 'one_time_auth' in config and config['one_time_auth']: + self._one_time_auth_enable = True + else: + self._one_time_auth_enable = False self._is_local = is_local self._cache = lru_cache.LRUCache(timeout=config['timeout'], close_callback=self._close_client) @@ -243,7 +248,19 @@ class UDPRelay(object): header_result = parse_header(data) if header_result is None: return - # addrtype, dest_addr, dest_port, header_length = header_result + addrtype, dest_addr, dest_port, header_length = header_result + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('one time auth header is too short') + return None + if onetimeauth_verify(data[-ONETIMEAUTH_BYTES:], + data[header_length: -ONETIMEAUTH_BYTES], + self._encryptor.decipher_iv + self._encryptor.key) is False: + logging.warn('one time auth fail') + return None + self._one_time_authed = True + header_length += ONETIMEAUTH_BYTES response = b'\x00\x00\x00' + data client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: From 77b2a2205597ba80f016b2c0bdc03fed3ee5bd34 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 10:11:31 +0800 Subject: [PATCH 07/68] ota completed! --- shadowsocks/common.py | 2 ++ shadowsocks/shell.py | 4 +-- shadowsocks/tcprelay.py | 57 ++++++++++++++++++++++++++--------------- shadowsocks/udprelay.py | 36 +++++++++++++++----------- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/shadowsocks/common.py b/shadowsocks/common.py index 7ec04ba..9dd5f7c 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -37,6 +37,8 @@ def sha1_hmac(secret, data): def onetimeauth_verify(_hash, data, key): return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] +def onetimeauth_gen(data, key): + return sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] def compat_ord(s): if type(s) == int: diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index e2284da..336d918 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -130,11 +130,11 @@ def get_config(is_local): logging.basicConfig(level=logging.INFO, format='%(levelname)-s: %(message)s') if is_local: - shortopts = 'hd:s:b:p:k:l:m:c:t:vq' + shortopts = 'hd:s:b:p:k:l:m:c:t:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', 'version'] else: - shortopts = 'hd:s:p:k:m:c:t:vq' + shortopts = 'hd:s:p:k:m:c:t:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', 'forbidden-ip=', 'user=', 'manager-address=', 'version'] try: diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 22ba104..c61f8c0 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -27,7 +27,7 @@ import traceback import random from shadowsocks import encrypt, eventloop, shell, common -from shadowsocks.common import parse_header, onetimeauth_verify, \ +from shadowsocks.common import parse_header, onetimeauth_verify, onetimeauth_gen, \ ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time @@ -233,12 +233,14 @@ class TCPRelayHandler(object): def _handle_stage_connecting(self, data): if self._is_local: + if self._one_time_auth_enable: + data = self._one_time_auth_chunk_data_gen(data) data = self._encryptor.encrypt(data) - if self._one_time_auth_enable: - self._one_time_auth_chunk_data(data, - self._data_to_write_to_remote.append) - else: self._data_to_write_to_remote.append(data) + else: + if self._one_time_auth_enable: + self._one_time_auth_chunk_data(data, + self._data_to_write_to_remote.append) if self._is_local and not self._fastopen_connected and \ self._config['fast_open']: # for sslocal and fastopen, we basically wait for data and use @@ -306,17 +308,18 @@ class TCPRelayHandler(object): logging.info('connecting %s:%d from %s:%d' % (common.to_str(remote_addr), remote_port, self._client_address[0], self._client_address[1])) - # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: - if len(data) < header_length + ONETIMEAUTH_BYTES: - logging.warn('one time auth header is too short') - return None - if onetimeauth_verify(data[header_length: header_length+ONETIMEAUTH_BYTES], - data[:header_length], - self._encryptor.decipher_iv + self._encryptor.key) is False: - logging.warn('one time auth fail') - self.destroy() - header_length += ONETIMEAUTH_BYTES + if self._is_local is False: + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('one time auth header is too short') + return None + if onetimeauth_verify(data[header_length: header_length+ONETIMEAUTH_BYTES], + data[:header_length], + self._encryptor.decipher_iv + self._encryptor.key) is False: + logging.warn('one time auth fail') + self.destroy() + header_length += ONETIMEAUTH_BYTES self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) @@ -326,6 +329,11 @@ class TCPRelayHandler(object): self._write_to_sock((b'\x05\x00\x00\x01' b'\x00\x00\x00\x00\x10\x10'), self._local_sock) + # spec https://shadowsocks.org/en/spec/one-time-auth.html + # ATYP & 0x10 == 1, then OTA is enabled. + if self._one_time_auth_enable: + data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + data += onetimeauth_gen(data, self._encryptor.cipher_iv + self._encryptor.key) data_to_send = self._encryptor.encrypt(data) self._data_to_write_to_remote.append(data_to_send) # notice here may go into _handle_dns_resolved directly @@ -436,23 +444,30 @@ class TCPRelayHandler(object): self._one_time_auth_buff_data, self._encryptor.decipher_iv + struct.pack('>I', self._one_time_auth_chunk_idx)) \ is False: - # logging.warn('one time auth fail, drop chunk !') else: data_cb(self._one_time_auth_buff_data) + self._one_time_auth_chunk_idx += 1 self._one_time_auth_buff_head = '' self._one_time_auth_buff_data = '' - self._one_time_auth_chunk_idx += 1 self._one_time_auth_len = 0 return + def _one_time_auth_chunk_data_gen(self, data): + data_len = struct.pack(">H", len(data)) + sha110 = onetimeauth_gen(data, self._encryptor.cipher_iv + struct.pack('>I', self._one_time_auth_chunk_idx)) + self._one_time_auth_chunk_idx += 1 + return data_len + sha110 + data + def _handle_stage_stream(self, data): if self._is_local: + if self._one_time_auth_enable: + data = self._one_time_auth_chunk_data_gen(data) data = self._encryptor.encrypt(data) - if self._one_time_auth_enable: - self._one_time_auth_chunk_data(data, self._write_to_sock_remote) - else: self._write_to_sock(data, self._remote_sock) + else: + if self._one_time_auth_enable: + self._one_time_auth_chunk_data(data, self._write_to_sock_remote) return def _on_local_read(self): diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index bef46af..0a92102 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -119,7 +119,7 @@ class UDPRelay(object): addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) if len(addrs) == 0: - raise Exception("can't get addrinfo for %s:%d" % + raise Exception("UDP can't get addrinfo for %s:%d" % (self._listen_addr, self._listen_port)) af, socktype, proto, canonname, sa = addrs[0] server_socket = socket.socket(af, socktype, proto) @@ -157,7 +157,7 @@ class UDPRelay(object): if self._is_local: frag = common.ord(data[2]) if frag != 0: - logging.warn('drop a message since frag is not 0') + logging.warn('UDP drop a message since frag is not 0') return else: data = data[3:] @@ -176,7 +176,18 @@ class UDPRelay(object): server_addr, server_port = self._get_a_server() else: server_addr, server_port = dest_addr, dest_port - + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('UDP one time auth header is too short') + return None + if onetimeauth_verify(data[-ONETIMEAUTH_BYTES:], + data[header_length: -ONETIMEAUTH_BYTES], + self._encryptor.decipher_iv + self._encryptor.key) is False: + logging.warn('UDP one time auth fail') + return None + self._one_time_authed = True + header_length += ONETIMEAUTH_BYTES addrs = self._dns_cache.get(server_addr, None) if addrs is None: addrs = socket.getaddrinfo(server_addr, server_port, 0, @@ -207,6 +218,9 @@ class UDPRelay(object): self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: + # spec https://shadowsocks.org/en/spec/one-time-auth.html + if self._one_time_auth_enable: + data = _one_time_auth_chunk_data_gen(data) data = encrypt.encrypt_all(self._password, self._method, 1, data) if not data: return @@ -249,18 +263,6 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: - if len(data) < header_length + ONETIMEAUTH_BYTES: - logging.warn('one time auth header is too short') - return None - if onetimeauth_verify(data[-ONETIMEAUTH_BYTES:], - data[header_length: -ONETIMEAUTH_BYTES], - self._encryptor.decipher_iv + self._encryptor.key) is False: - logging.warn('one time auth fail') - return None - self._one_time_authed = True - header_length += ONETIMEAUTH_BYTES response = b'\x00\x00\x00' + data client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: @@ -270,6 +272,10 @@ class UDPRelay(object): # simply drop that packet pass + def _one_time_auth_chunk_data_gen(self, data): + data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + return data + onetimeauth_gen(data, self._encryptor.cipher_iv + self._encryptor.key) + def add_to_loop(self, loop): if self._eventloop: raise Exception('already add to loop') From ca99abff899451dbc45a45ce0e3b7c6d842b1ec2 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 10:26:51 +0800 Subject: [PATCH 08/68] fix udp ota --- shadowsocks/udprelay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 0a92102..d4b922f 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -69,7 +69,7 @@ import errno import random from shadowsocks import encrypt, eventloop, lru_cache, common, shell -from shadowsocks.common import parse_header, pack_addr, \ +from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, onetimeauth_gen, \ ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH @@ -180,12 +180,12 @@ class UDPRelay(object): if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('UDP one time auth header is too short') - return None + return if onetimeauth_verify(data[-ONETIMEAUTH_BYTES:], data[header_length: -ONETIMEAUTH_BYTES], self._encryptor.decipher_iv + self._encryptor.key) is False: logging.warn('UDP one time auth fail') - return None + return self._one_time_authed = True header_length += ONETIMEAUTH_BYTES addrs = self._dns_cache.get(server_addr, None) From 569ca0d0a43839d6ae521ae0aeb275fe7599b8bb Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 18:30:45 +0800 Subject: [PATCH 09/68] reverse config --- shadowsocks/server.py | 13 ++++++------- shadowsocks/shell.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 5896c7d..1be4c0f 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -42,13 +42,12 @@ def main(): 'will be ignored') else: config['port_password'] = {} - server_port = config.get('server_port', None) - if server_port: - if type(server_port) == list: - for a_server_port in server_port: - config['port_password'][a_server_port] = config['password'] - else: - config['port_password'][str(server_port)] = config['password'] + server_port = config['server_port'] + if type(server_port) == list: + for a_server_port in server_port: + config['port_password'][a_server_port] = config['password'] + else: + config['port_password'][str(server_port)] = config['password'] if config.get('manager_address', 0): logging.info('entering manager mode') diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index eb1dc04..67bfd92 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -93,7 +93,7 @@ def check_config(config, is_local): if 'local_port' in config: config['local_port'] = int(config['local_port']) - if config.get('server_port', None) and type(config['server_port']) != list: + if 'server_port' in config and type(config['server_port']) != list: config['server_port'] = int(config['server_port']) if config.get('local_address', '') in [b'0.0.0.0']: @@ -243,7 +243,7 @@ def get_config(is_local): except Exception as e: logging.error(e) sys.exit(2) - config['server_port'] = config.get('server_port', None) + config['server_port'] = config.get('server_port', 8388) logging.getLogger('').handlers = [] logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') From 7efc3e2640342f2e662fba71d5c45ced9276ae17 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 18:32:39 +0800 Subject: [PATCH 10/68] README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index f621dfd..b61c0db 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,3 @@ -About shadowsocks-rm ----------------- - -This project is https://github.com/shadowsocks/shadowsocks clone. I JUST fix bug on the original code. Unless it is necessary to have additional features. - - shadowsocks =========== From 2d40a361db004fedb78edbb60054e9ab721f4827 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 18:42:23 +0800 Subject: [PATCH 11/68] ota config --- shadowsocks/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 67bfd92..380bf25 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -228,6 +228,7 @@ def get_config(is_local): config['verbose'] = config.get('verbose', False) config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) + 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') From ee391c777340cfc0e653e7a0a6647e7a32ce9bb8 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 22:58:53 +0800 Subject: [PATCH 12/68] udp ota bug --- shadowsocks/udprelay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index d4b922f..6f0341f 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -69,8 +69,8 @@ import errno import random from shadowsocks import encrypt, eventloop, lru_cache, common, shell -from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, onetimeauth_gen, \ - ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH +from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \ + onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH BUF_SIZE = 65536 @@ -220,7 +220,7 @@ class UDPRelay(object): if self._is_local: # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._one_time_auth_enable: - data = _one_time_auth_chunk_data_gen(data) + data = self._one_time_auth_chunk_data_gen(data) data = encrypt.encrypt_all(self._password, self._method, 1, data) if not data: return From a4a87eb12703fa1e8f1668482f178861aa0065af Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 23:18:16 +0800 Subject: [PATCH 13/68] PEP8 --- shadowsocks/tcprelay.py | 176 +++++++++++++++++++++------------------- shadowsocks/udprelay.py | 14 ++-- 2 files changed, 100 insertions(+), 90 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index c61f8c0..f415b22 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -109,13 +109,13 @@ class TCPRelayHandler(object): self._encryptor = encrypt.Encryptor(config['password'], config['method']) if 'one_time_auth' in config and config['one_time_auth']: - self._one_time_auth_enable = True + self._ota_enable = True else: - self._one_time_auth_enable = False - self._one_time_auth_buff_head = '' - self._one_time_auth_buff_data = '' - self._one_time_auth_len = 0 - self._one_time_auth_chunk_idx = 0 + self._ota_enable = False + self._ota_buff_head = '' + self._ota_buff_data = '' + self._ota_len = 0 + self._ota_chunk_idx = 0 self._fastopen_connected = False self._data_to_write_to_local = [] self._data_to_write_to_remote = [] @@ -233,14 +233,14 @@ class TCPRelayHandler(object): def _handle_stage_connecting(self, data): if self._is_local: - if self._one_time_auth_enable: - data = self._one_time_auth_chunk_data_gen(data) + if self._ota_enable: + data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) self._data_to_write_to_remote.append(data) else: - if self._one_time_auth_enable: - self._one_time_auth_chunk_data(data, - self._data_to_write_to_remote.append) + if self._ota_enable: + self._ota_chunk_data(data, + self._data_to_write_to_remote.append) if self._is_local and not self._fastopen_connected and \ self._config['fast_open']: # for sslocal and fastopen, we basically wait for data and use @@ -254,7 +254,8 @@ class TCPRelayHandler(object): self._loop.add(remote_sock, eventloop.POLL_ERR, self._server) data = b''.join(self._data_to_write_to_remote) l = len(data) - s = remote_sock.sendto(data, MSG_FASTOPEN, self._chosen_server) + s = remote_sock.sendto(data, MSG_FASTOPEN, + self._chosen_server) if s < l: data = data[s:] self._data_to_write_to_remote = [data] @@ -310,13 +311,15 @@ class TCPRelayHandler(object): self._client_address[0], self._client_address[1])) if self._is_local is False: # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + if self._ota_enable or addrtype & ADDRTYPE_AUTH: if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('one time auth header is too short') return None - if onetimeauth_verify(data[header_length: header_length+ONETIMEAUTH_BYTES], - data[:header_length], - self._encryptor.decipher_iv + self._encryptor.key) is False: + offset = header_length + ONETIMEAUTH_BYTES + _hash = data[header_length: offset] + _data = data[:header_length] + key = self._encryptor.decipher_iv + self._encryptor.key + if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail') self.destroy() header_length += ONETIMEAUTH_BYTES @@ -331,19 +334,20 @@ class TCPRelayHandler(object): self._local_sock) # spec https://shadowsocks.org/en/spec/one-time-auth.html # ATYP & 0x10 == 1, then OTA is enabled. - if self._one_time_auth_enable: + if self._ota_enable: data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] - data += onetimeauth_gen(data, self._encryptor.cipher_iv + self._encryptor.key) + key = self._encryptor.cipher_iv + self._encryptor.key + data += onetimeauth_gen(data, key) data_to_send = self._encryptor.encrypt(data) self._data_to_write_to_remote.append(data_to_send) # notice here may go into _handle_dns_resolved directly self._dns_resolver.resolve(self._chosen_server[0], self._handle_dns_resolved) else: - if self._one_time_auth_enable: + if self._ota_enable: data = data[header_length:] - self._one_time_auth_chunk_data(data, - self._data_to_write_to_remote.append) + self._ota_chunk_data(data, + self._data_to_write_to_remote.append) elif len(data) > header_length: self._data_to_write_to_remote.append(data[header_length:]) # notice here may go into _handle_dns_resolved directly @@ -377,97 +381,99 @@ class TCPRelayHandler(object): self._log_error(error) self.destroy() return - if result: + if result and result[1]: ip = result[1] - if ip: - try: - self._stage = STAGE_CONNECTING - remote_addr = ip - if self._is_local: - remote_port = self._chosen_server[1] - else: - remote_port = self._remote_address[1] + try: + self._stage = STAGE_CONNECTING + remote_addr = ip + if self._is_local: + remote_port = self._chosen_server[1] + else: + remote_port = self._remote_address[1] - if self._is_local and self._config['fast_open']: - # for fastopen: - # wait for more data to arrive and send them in one SYN - self._stage = STAGE_CONNECTING - # we don't have to wait for remote since it's not - # created - self._update_stream(STREAM_UP, WAIT_STATUS_READING) - # TODO when there is already data in this packet - else: - # else do connect - remote_sock = self._create_remote_socket(remote_addr, - remote_port) - try: - remote_sock.connect((remote_addr, remote_port)) - except (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == \ - errno.EINPROGRESS: - pass - self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT, - self._server) - self._stage = STAGE_CONNECTING - self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) - self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) - return - except Exception as e: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() + if self._is_local and self._config['fast_open']: + # for fastopen: + # wait for more data arrive and send them in one SYN + self._stage = STAGE_CONNECTING + # we don't have to wait for remote since it's not + # created + self._update_stream(STREAM_UP, WAIT_STATUS_READING) + # TODO when there is already data in this packet + else: + # else do connect + remote_sock = self._create_remote_socket(remote_addr, + remote_port) + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == \ + errno.EINPROGRESS: + pass + self._loop.add(remote_sock, + eventloop.POLL_ERR | eventloop.POLL_OUT, + self._server) + self._stage = STAGE_CONNECTING + self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) + self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) + return + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() self.destroy() def _write_to_sock_remote(self, data): self._write_to_sock(data, self._remote_sock) - def _one_time_auth_chunk_data(self, data, data_cb): + def _ota_chunk_data(self, data, data_cb): # spec https://shadowsocks.org/en/spec/one-time-auth.html while len(data) > 0: - if self._one_time_auth_len == 0: + if self._ota_len == 0: # get DATA.LEN + HMAC-SHA1 - length = ONETIMEAUTH_CHUNK_BYTES - len(self._one_time_auth_buff_head) - self._one_time_auth_buff_head += data[:length] + length = ONETIMEAUTH_CHUNK_BYTES - len(self._ota_buff_head) + self._ota_buff_head += data[:length] data = data[length:] - if len(self._one_time_auth_buff_head) < ONETIMEAUTH_CHUNK_BYTES: + if len(self._ota_buff_head) < ONETIMEAUTH_CHUNK_BYTES: # wait more data return - self._one_time_auth_len = struct.unpack('>H', - self._one_time_auth_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN])[0] - length = min(self._one_time_auth_len, len(data)) - self._one_time_auth_buff_data += data[:length] + data_len = self._ota_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN] + self._ota_len = struct.unpack('>H', data_len)[0] + length = min(self._ota_len, len(data)) + self._ota_buff_data += data[:length] data = data[length:] - if len(self._one_time_auth_buff_data) == self._one_time_auth_len: + if len(self._ota_buff_data) == self._ota_len: # get a chunk data - if onetimeauth_verify(self._one_time_auth_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:], - self._one_time_auth_buff_data, - self._encryptor.decipher_iv + struct.pack('>I', self._one_time_auth_chunk_idx)) \ - is False: + _hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:] + _data = self._ota_buff_data + index = struct.pack('>I', self._ota_chunk_idx) + key = self._encryptor.decipher_iv + index + if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail, drop chunk !') else: - data_cb(self._one_time_auth_buff_data) - self._one_time_auth_chunk_idx += 1 - self._one_time_auth_buff_head = '' - self._one_time_auth_buff_data = '' - self._one_time_auth_len = 0 + data_cb(self._ota_buff_data) + self._ota_chunk_idx += 1 + self._ota_buff_head = '' + self._ota_buff_data = '' + self._ota_len = 0 return - def _one_time_auth_chunk_data_gen(self, data): + def _ota_chunk_data_gen(self, data): data_len = struct.pack(">H", len(data)) - sha110 = onetimeauth_gen(data, self._encryptor.cipher_iv + struct.pack('>I', self._one_time_auth_chunk_idx)) - self._one_time_auth_chunk_idx += 1 + index = struct.pack('>I', self._ota_chunk_idx) + key = self._encryptor.cipher_iv + index + sha110 = onetimeauth_gen(data, key) + self._ota_chunk_idx += 1 return data_len + sha110 + data def _handle_stage_stream(self, data): if self._is_local: - if self._one_time_auth_enable: - data = self._one_time_auth_chunk_data_gen(data) + if self._ota_enable: + data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) self._write_to_sock(data, self._remote_sock) else: - if self._one_time_auth_enable: - self._one_time_auth_chunk_data(data, self._write_to_sock_remote) + if self._ota_enable: + self._ota_chunk_data(data, self._write_to_sock_remote) return def _on_local_read(self): diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 6f0341f..cbfaa31 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -165,7 +165,9 @@ class UDPRelay(object): data = encrypt.encrypt_all(self._password, self._method, 0, data) # decrypt data if not data: - logging.debug('UDP handle_server: data is empty after decrypt') + logging.debug( + 'UDP handle_server: data is empty after decrypt' + ) return header_result = parse_header(data) if header_result is None: @@ -181,9 +183,10 @@ class UDPRelay(object): if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('UDP one time auth header is too short') return - if onetimeauth_verify(data[-ONETIMEAUTH_BYTES:], - data[header_length: -ONETIMEAUTH_BYTES], - self._encryptor.decipher_iv + self._encryptor.key) is False: + _hash = data[-ONETIMEAUTH_BYTES:] + _data = data[header_length: -ONETIMEAUTH_BYTES] + _key = self._encryptor.decipher_iv + self._encryptor.key + if onetimeauth_verify(_hash, _data, _key) is False: logging.warn('UDP one time auth fail') return self._one_time_authed = True @@ -274,7 +277,8 @@ class UDPRelay(object): def _one_time_auth_chunk_data_gen(self, data): data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] - return data + onetimeauth_gen(data, self._encryptor.cipher_iv + self._encryptor.key) + key = self._encryptor.cipher_iv + self._encryptor.key + return data + onetimeauth_gen(data, key) def add_to_loop(self, loop): if self._eventloop: From 7c8ecb811ca7a4bce4932045e6a9b9811cf48d54 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 23:20:12 +0800 Subject: [PATCH 14/68] PEP8 --- shadowsocks/tcprelay.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index f415b22..cb3c413 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -27,8 +27,9 @@ import traceback import random from shadowsocks import encrypt, eventloop, shell, common -from shadowsocks.common import parse_header, onetimeauth_verify, onetimeauth_gen, \ - ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH +from shadowsocks.common import parse_header, onetimeauth_verify, \ + onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \ + ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time TIMEOUTS_CLEAN_SIZE = 512 From 614eed2bf0c8bf1c8e8e0c506331e8dd98be826b Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 23:27:21 +0800 Subject: [PATCH 15/68] PEP8 --- shadowsocks/common.py | 2 ++ shadowsocks/udprelay.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/shadowsocks/common.py b/shadowsocks/common.py index 9dd5f7c..ee14995 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -37,9 +37,11 @@ def sha1_hmac(secret, data): def onetimeauth_verify(_hash, data, key): return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] + def onetimeauth_gen(data, key): return sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] + def compat_ord(s): if type(s) == int: return s diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index cbfaa31..6d29f63 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -184,7 +184,7 @@ class UDPRelay(object): logging.warn('UDP one time auth header is too short') return _hash = data[-ONETIMEAUTH_BYTES:] - _data = data[header_length: -ONETIMEAUTH_BYTES] + _data = data[header_length: -ONETIMEAUTH_BYTES] _key = self._encryptor.decipher_iv + self._encryptor.key if onetimeauth_verify(_hash, _data, _key) is False: logging.warn('UDP one time auth fail') From 285dc407294adfe1d3ce4107530f4259869b0701 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 23:47:47 +0800 Subject: [PATCH 16/68] here? --- tests/test_daemon.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_daemon.sh b/tests/test_daemon.sh index 40f35ef..1acddc0 100755 --- a/tests/test_daemon.sh +++ b/tests/test_daemon.sh @@ -18,7 +18,7 @@ function run_test { for module in local server do -command="coverage run -p -a shadowsocks/$module.py" +command="coverage run -a shadowsocks/$module.py" mkdir -p tmp From ab4206258775d494e86aba03a2aa5c990c96a625 Mon Sep 17 00:00:00 2001 From: jsy Date: Mon, 11 Jan 2016 23:57:18 +0800 Subject: [PATCH 17/68] Can't append to data files in parallel mode. --- tests/test_command.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_command.sh b/tests/test_command.sh index be05704..8225740 100755 --- a/tests/test_command.sh +++ b/tests/test_command.sh @@ -2,7 +2,7 @@ . tests/assert.sh -PYTHON="coverage run -a -p" +PYTHON="coverage run -a" LOCAL="$PYTHON shadowsocks/local.py" SERVER="$PYTHON shadowsocks/server.py" From 4786b9c3addd2b16664af20d53f8e08b498d013b Mon Sep 17 00:00:00 2001 From: jsy Date: Tue, 12 Jan 2016 00:03:13 +0800 Subject: [PATCH 18/68] Can't append to data files in parallel mode. --- tests/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index 29b57d4..85bd007 100755 --- a/tests/test.py +++ b/tests/test.py @@ -44,7 +44,7 @@ parser.add_argument('--dns', type=str, default='8.8.8.8') config = parser.parse_args() if config.with_coverage: - python = ['coverage', 'run', '-p', '-a'] + python = ['coverage', 'run', '-a'] client_args = python + ['shadowsocks/local.py', '-v'] server_args = python + ['shadowsocks/server.py', '-v'] From 3788df59bbd629fd4f38e219e6581678d6df276a Mon Sep 17 00:00:00 2001 From: jsy Date: Tue, 12 Jan 2016 00:04:40 +0800 Subject: [PATCH 19/68] Can't append to data files in parallel mode. --- tests/test_graceful_restart.sh | 2 +- tests/test_large_file.sh | 2 +- tests/test_udp_src.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_graceful_restart.sh b/tests/test_graceful_restart.sh index d91ba92..eb4bc0f 100755 --- a/tests/test_graceful_restart.sh +++ b/tests/test_graceful_restart.sh @@ -1,6 +1,6 @@ #!/bin/bash -PYTHON="coverage run -p -a" +PYTHON="coverage run -a" URL=http://127.0.0.1/file diff --git a/tests/test_large_file.sh b/tests/test_large_file.sh index 33bcb59..7a6aec8 100755 --- a/tests/test_large_file.sh +++ b/tests/test_large_file.sh @@ -1,6 +1,6 @@ #!/bin/bash -PYTHON="coverage run -p -a" +PYTHON="coverage run -a" URL=http://127.0.0.1/file mkdir -p tmp diff --git a/tests/test_udp_src.sh b/tests/test_udp_src.sh index d356581..4a07a23 100755 --- a/tests/test_udp_src.sh +++ b/tests/test_udp_src.sh @@ -1,6 +1,6 @@ #!/bin/bash -PYTHON="coverage run -p -a" +PYTHON="coverage run -a" mkdir -p tmp From 9f3641db546121b6e120320871a5e6b9f1f99566 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 00:27:46 +0800 Subject: [PATCH 20/68] url ssl timeout --- tests/jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/jenkins.sh b/tests/jenkins.sh index 5b53e93..dc583ba 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -51,7 +51,7 @@ run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 1 run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" # test if DNS works -run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" +run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://www.google.com/" # test localhost is in the forbidden list by default run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" From d2f1caeb5d3458efe602878a6a94be5792b80629 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 01:03:25 +0800 Subject: [PATCH 21/68] fixbug: when ota disable data not write --- shadowsocks/tcprelay.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index cb3c413..20517c2 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -242,6 +242,8 @@ class TCPRelayHandler(object): if self._ota_enable: self._ota_chunk_data(data, self._data_to_write_to_remote.append) + else: + self._data_to_write_to_remote.append(data) if self._is_local and not self._fastopen_connected and \ self._config['fast_open']: # for sslocal and fastopen, we basically wait for data and use @@ -475,6 +477,8 @@ class TCPRelayHandler(object): else: if self._ota_enable: self._ota_chunk_data(data, self._write_to_sock_remote) + else: + self._write_to_sock(data, self._remote_sock) return def _on_local_read(self): From efad7578375cad323b5704c6827ad482fcc0f437 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 01:44:18 +0800 Subject: [PATCH 22/68] 9001 not idle? --- tests/test_udp_src.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py index e8fa505..50af385 100644 --- a/tests/test_udp_src.py +++ b/tests/test_udp_src.py @@ -72,8 +72,8 @@ if __name__ == '__main__': sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.SOL_UDP) - sock_in1.bind(('::1', 9001)) - sock_out.sendto(b'data', ('::1', 9001)) + sock_in1.bind(('::1', 9004)) + sock_out.sendto(b'data', ('::1', 9004)) result3 = sock_in1.recvfrom(8) # make sure they're from different source ports From 288c11c9d9fe5a1389bb1f9e26727be3830e8308 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 01:55:42 +0800 Subject: [PATCH 23/68] CI Try again! --- tests/jenkins.sh | 2 +- tests/test_udp_src.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/jenkins.sh b/tests/jenkins.sh index dc583ba..5b53e93 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -51,7 +51,7 @@ run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 1 run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" # test if DNS works -run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://www.google.com/" +run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" # test localhost is in the forbidden list by default run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py index 50af385..dd78ebf 100644 --- a/tests/test_udp_src.py +++ b/tests/test_udp_src.py @@ -48,13 +48,13 @@ if __name__ == '__main__': sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.SOL_UDP) - sock_in1.bind(('::1', 9001)) - sock_in2.bind(('::1', 9002)) + sock_in1.bind(('::1', 9005)) + sock_in2.bind(('::1', 9006)) - sock_out.sendto(b'data', ('::1', 9001)) + sock_out.sendto(b'data', ('::1', 9005)) result1 = sock_in1.recvfrom(8) - sock_out.sendto(b'data', ('::1', 9002)) + sock_out.sendto(b'data', ('::1', 9006)) result2 = sock_in2.recvfrom(8) sock_out.close() From 248582c9328b9ec23bbcfada9000ac6131c5bce7 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 10:34:48 +0800 Subject: [PATCH 24/68] remove IPv6 testcase and add ota test --- shadowsocks/rc4-md5-ota.json | 11 +++++++++++ tests/jenkins.sh | 4 +++- tests/rc4-md5-ota.json | 12 ++++++++++++ tests/test_udp_src.py | 14 ++++++++------ 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 shadowsocks/rc4-md5-ota.json create mode 100644 tests/rc4-md5-ota.json diff --git a/shadowsocks/rc4-md5-ota.json b/shadowsocks/rc4-md5-ota.json new file mode 100644 index 0000000..164ee5a --- /dev/null +++ b/shadowsocks/rc4-md5-ota.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8899, + "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/jenkins.sh b/tests/jenkins.sh index 5b53e93..6d0fac8 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -45,7 +45,9 @@ run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client- run_test python tests/test.py --with-coverage -s tests/server-dnsserver.json -c tests/client-multi-server-ip.json run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json run_test python tests/test.py --with-coverage -c tests/workers.json -run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json +run_test python tests/test.py --with-coverage -c tests/rc4-md5-ota.json +# travis-ci not support IPv6 +# run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv" run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" diff --git a/tests/rc4-md5-ota.json b/tests/rc4-md5-ota.json new file mode 100644 index 0000000..ea93226 --- /dev/null +++ b/tests/rc4-md5-ota.json @@ -0,0 +1,12 @@ +{ + "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, + "verbose":1 +} diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py index dd78ebf..585606d 100644 --- a/tests/test_udp_src.py +++ b/tests/test_udp_src.py @@ -36,6 +36,7 @@ if __name__ == '__main__': # make sure they're from the same source port assert result1 == result2 + """ # Test 2: same source port IPv6 # try again from the same port but IPv6 sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, @@ -48,13 +49,13 @@ if __name__ == '__main__': sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.SOL_UDP) - sock_in1.bind(('::1', 9005)) - sock_in2.bind(('::1', 9006)) + sock_in1.bind(('::1', 9001)) + sock_in2.bind(('::1', 9002)) - sock_out.sendto(b'data', ('::1', 9005)) + sock_out.sendto(b'data', ('::1', 9001)) result1 = sock_in1.recvfrom(8) - sock_out.sendto(b'data', ('::1', 9006)) + sock_out.sendto(b'data', ('::1', 9002)) result2 = sock_in2.recvfrom(8) sock_out.close() @@ -72,8 +73,8 @@ if __name__ == '__main__': sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.SOL_UDP) - sock_in1.bind(('::1', 9004)) - sock_out.sendto(b'data', ('::1', 9004)) + sock_in1.bind(('::1', 9001)) + sock_out.sendto(b'data', ('::1', 9001)) result3 = sock_in1.recvfrom(8) # make sure they're from different source ports @@ -81,3 +82,4 @@ if __name__ == '__main__': sock_out.close() sock_in1.close() + """ From 269e3dd82dd2c670624de83afa6fdd60bfd6b040 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 12:10:40 +0800 Subject: [PATCH 25/68] test pass ota in udp --- shadowsocks/encrypt.py | 48 ++++++++++++++++++++++++++++++++++++ shadowsocks/rc4-md5-ota.json | 11 --------- shadowsocks/udprelay.py | 21 ++++++++-------- tests/rc4-md5-ota.json | 3 +-- 4 files changed, 60 insertions(+), 23 deletions(-) delete mode 100644 shadowsocks/rc4-md5-ota.json diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 54dbb42..ece72ec 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -133,6 +133,42 @@ class Encryptor(object): 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() @@ -185,6 +221,18 @@ def test_encrypt_all(): 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/rc4-md5-ota.json b/shadowsocks/rc4-md5-ota.json deleted file mode 100644 index 164ee5a..0000000 --- a/shadowsocks/rc4-md5-ota.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "server":"127.0.0.1", - "server_port":8899, - "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/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 6d29f63..975906e 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -150,6 +150,8 @@ class UDPRelay(object): def _handle_server(self): server = self._server_socket data, r_addr = server.recvfrom(BUF_SIZE) + key = None + iv = None if not data: logging.debug('UDP handle_server: data is empty') if self._stat_callback: @@ -162,7 +164,7 @@ class UDPRelay(object): else: data = data[3:] else: - data = encrypt.encrypt_all(self._password, self._method, 0, data) + data, key, iv = encrypt.dencrypt_all(self._password, self._method, data) # decrypt data if not data: logging.debug( @@ -184,13 +186,11 @@ class UDPRelay(object): logging.warn('UDP one time auth header is too short') return _hash = data[-ONETIMEAUTH_BYTES:] - _data = data[header_length: -ONETIMEAUTH_BYTES] - _key = self._encryptor.decipher_iv + self._encryptor.key - if onetimeauth_verify(_hash, _data, _key) is False: + data = data[: -ONETIMEAUTH_BYTES] + _key = iv + key + if onetimeauth_verify(_hash, data, _key) is False: logging.warn('UDP one time auth fail') return - self._one_time_authed = True - header_length += ONETIMEAUTH_BYTES addrs = self._dns_cache.get(server_addr, None) if addrs is None: addrs = socket.getaddrinfo(server_addr, server_port, 0, @@ -221,10 +221,11 @@ class UDPRelay(object): self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: + key, iv, m = gen_key_iv(self._password, self._method) # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._one_time_auth_enable: - data = self._one_time_auth_chunk_data_gen(data) - data = encrypt.encrypt_all(self._password, self._method, 1, data) + data = self._ota_chunk_data_gen(key, iv, data) + data = encrypt.encrypt_all_m(key, iv, m, method, data) if not data: return else: @@ -275,9 +276,9 @@ class UDPRelay(object): # simply drop that packet pass - def _one_time_auth_chunk_data_gen(self, data): + def _ota_chunk_data_gen(self, key, iv, data): data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] - key = self._encryptor.cipher_iv + self._encryptor.key + key = iv + key return data + onetimeauth_gen(data, key) def add_to_loop(self, loop): diff --git a/tests/rc4-md5-ota.json b/tests/rc4-md5-ota.json index ea93226..3566d6d 100644 --- a/tests/rc4-md5-ota.json +++ b/tests/rc4-md5-ota.json @@ -7,6 +7,5 @@ "method":"rc4-md5", "local_address":"127.0.0.1", "fast_open":false, - "one_time_auth":true, - "verbose":1 + "one_time_auth":true } From f163a7b094d4fa6f98f9d595fdd47f6f67f58769 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 12:17:11 +0800 Subject: [PATCH 26/68] add CI --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b61c0db..608a1fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ shadowsocks =========== +|PyPI version| |Build Status| |Coverage Status| + A fast tunnel proxy that helps you bypass firewalls. Features: From 811a0e6eb8f67a4f97a3a6f610df146385fd5786 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 12:17:46 +0800 Subject: [PATCH 27/68] PEP8 and undefined name 'gen_key_iv' --- shadowsocks/udprelay.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 975906e..3fbd9f3 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -164,7 +164,9 @@ class UDPRelay(object): else: data = data[3:] else: - data, key, iv = encrypt.dencrypt_all(self._password, self._method, data) + data, key, iv = encrypt.dencrypt_all(self._password, + self._method, + data) # decrypt data if not data: logging.debug( @@ -221,11 +223,11 @@ class UDPRelay(object): self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: - key, iv, m = gen_key_iv(self._password, self._method) + key, iv, m = encrypt.gen_key_iv(self._password, self._method) # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._one_time_auth_enable: data = self._ota_chunk_data_gen(key, iv, data) - data = encrypt.encrypt_all_m(key, iv, m, method, data) + data = encrypt.encrypt_all_m(key, iv, m, self._method, data) if not data: return else: From a8d116d70a40c32fb234e813943bc09145512893 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 14:20:46 +0800 Subject: [PATCH 28/68] fix bug with ord in Py3 --- shadowsocks/tcprelay.py | 10 +++++----- shadowsocks/udprelay.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 20517c2..6430f26 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -113,8 +113,8 @@ class TCPRelayHandler(object): self._ota_enable = True else: self._ota_enable = False - self._ota_buff_head = '' - self._ota_buff_data = '' + self._ota_buff_head = b'' + self._ota_buff_data = b'' self._ota_len = 0 self._ota_chunk_idx = 0 self._fastopen_connected = False @@ -338,7 +338,7 @@ class TCPRelayHandler(object): # spec https://shadowsocks.org/en/spec/one-time-auth.html # ATYP & 0x10 == 1, then OTA is enabled. if self._ota_enable: - data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] key = self._encryptor.cipher_iv + self._encryptor.key data += onetimeauth_gen(data, key) data_to_send = self._encryptor.encrypt(data) @@ -455,8 +455,8 @@ class TCPRelayHandler(object): else: data_cb(self._ota_buff_data) self._ota_chunk_idx += 1 - self._ota_buff_head = '' - self._ota_buff_data = '' + self._ota_buff_head = b'' + self._ota_buff_data = b'' self._ota_len = 0 return diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 3fbd9f3..6bf6ce6 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -279,7 +279,7 @@ class UDPRelay(object): pass def _ota_chunk_data_gen(self, key, iv, data): - data = chr(ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + data = common.chr(common.ord(data[0]) | ADDRTYPE_AUTH) + data[1:] key = iv + key return data + onetimeauth_gen(data, key) From efe39a75f9cfead70f6925b01d447fd9246fbb0b Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 14:22:10 +0800 Subject: [PATCH 29/68] README CI --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 608a1fd..ed06dba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ shadowsocks =========== -|PyPI version| |Build Status| |Coverage Status| +[![PyPI version]][PyPI] +[![Build Status]][Travis CI] +[![Coverage Status]][Coverage] A fast tunnel proxy that helps you bypass firewalls. From b9766ce5df1af306f690279d8eeb09bb99751ec9 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Tue, 12 Jan 2016 14:38:10 +0800 Subject: [PATCH 30/68] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index ed06dba..fb1ffca 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,17 @@ License ------- Apache License + + + + + + + +[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat +[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks +[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html +[PyPI]: https://pypi.python.org/pypi/shadowsocks +[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat +[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks + From b276d52735ce848202b8d1ea82e1f088216c0655 Mon Sep 17 00:00:00 2001 From: htc Date: Sat, 20 Feb 2016 10:37:25 +0800 Subject: [PATCH 31/68] Add support for option 'prefer-ipv6' --- debian/config.json | 3 ++- shadowsocks/asyncdns.py | 29 +++++++++++++++++------------ shadowsocks/server.py | 5 +++-- shadowsocks/shell.py | 7 ++++++- 4 files changed, 28 insertions(+), 16 deletions(-) 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 25a0e6b..44528d7 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -242,13 +242,13 @@ class DNSResponse(object): 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 = {} @@ -261,6 +261,10 @@ class DNSResolver(object): 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 @@ -341,17 +345,18 @@ class DNSResolver(object): answer[2] == QCLASS_IN: ip = answer[0] break - if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ - == STATUS_IPV4: - self._hostname_status[hostname] = STATUS_IPV6 - self._send_req(hostname, QTYPE_AAAA) + if not ip and self._hostname_status.get(hostname, STATUS_SECOND) \ + == STATUS_FIRST: + self._hostname_status[hostname] = STATUS_SECOND + self._send_req(hostname, self._QTYPES[1]) else: if ip: self._cache[hostname] = ip self._call_callback(hostname, ip) - elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + elif self._hostname_status.get(hostname, None) \ + == STATUS_SECOND: for question in response.questions: - if question[1] == QTYPE_AAAA: + if question[1] == self._QTYPES[1]: self._call_callback(hostname, None) break @@ -417,14 +422,14 @@ class DNSResolver(object): return arr = self._hostname_to_cb.get(hostname, None) if not arr: - self._hostname_status[hostname] = STATUS_IPV4 - self._send_req(hostname, QTYPE_A) + self._hostname_status[hostname] = STATUS_FIRST + self._send_req(hostname, self._QTYPES[0]) self._hostname_to_cb[hostname] = [callback] self._cb_to_hostname[callback] = hostname else: arr.append(callback) # TODO send again only if waited too long - self._send_req(hostname, QTYPE_A) + self._send_req(hostname, self._QTYPES[0]) def close(self): if self._sock: 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 380bf25..42efbc0 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -137,7 +137,8 @@ def get_config(is_local): 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', + 'prefer-ipv6'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -207,6 +208,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) @@ -229,6 +232,7 @@ 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) + config['prefer_ipv6'] = config.get('prefer_ipv6', False) if is_local: if config.get('server', None) is None: logging.error('server addr not specified') @@ -324,6 +328,7 @@ Proxy options: --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 General options: -h, --help show this help message and exit From 9d6b9fcde0f1e6511a7ef067868fafe9c0c672a3 Mon Sep 17 00:00:00 2001 From: htc550605125 Date: Tue, 23 Feb 2016 20:47:04 -0500 Subject: [PATCH 32/68] add dockerfile --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36ddaa0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM stackbrew/debian:jessie +RUN apt-get update +RUN apt-get install -y python python-setuptools + +ADD . /shadowsocks + +WORKDIR /shadowsocks +RUN python setup.py install +CMD ssserver From 1f24b31f891e237cf17949dfc42e280bc077c8cc Mon Sep 17 00:00:00 2001 From: mengskysama Date: Sat, 16 Apr 2016 11:07:22 +0800 Subject: [PATCH 33/68] Update install.sh replace dante corrupt src --- tests/socksify/install.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/socksify/install.sh b/tests/socksify/install.sh index 8eff72d..9de6458 100755 --- a/tests/socksify/install.sh +++ b/tests/socksify/install.sh @@ -1,8 +1,11 @@ #!/bin/bash if [ ! -d dante-1.4.0 ]; then - wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 + #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 From bb53b0cb901d87daafae713bba902ecb6e713506 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Sat, 16 Apr 2016 11:46:06 +0800 Subject: [PATCH 34/68] fix build dante cache and old src corrupt and slow --- tests/socksify/install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/socksify/install.sh b/tests/socksify/install.sh index 9de6458..86c4e1d 100755 --- a/tests/socksify/install.sh +++ b/tests/socksify/install.sh @@ -1,6 +1,7 @@ #!/bin/bash -if [ ! -d dante-1.4.0 ]; then +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 From aae990a2fc6ea4c5709e1a44f1e02ca1a2be29fe Mon Sep 17 00:00:00 2001 From: watermeter Date: Sat, 16 Apr 2016 11:54:12 +0800 Subject: [PATCH 35/68] Add auth method check for TCP socks connections (#528) * Add auth method check for TCP socks connections * remove meaningless return at end of function * remove extra blank lines --- shadowsocks/tcprelay.py | 54 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 6430f26..cf45351 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -36,6 +36,9 @@ TIMEOUTS_CLEAN_SIZE = 512 MSG_FASTOPEN = 0x20000000 +# SOCKS METHOD definition +METHOD_NOAUTH = 0 + # SOCKS command definition CMD_CONNECT = 1 CMD_BIND = 2 @@ -52,7 +55,7 @@ CMD_UDP_ASSOCIATE = 3 # for each handler, it could be at one of several stages: # as sslocal: -# stage 0 SOCKS hello received from local, send hello to local +# stage 0 auth METHOD received from local, reply with selection message # stage 1 addr received from local, query DNS for remote # stage 2 UDP assoc # stage 3 DNS resolved, connect to remote @@ -92,6 +95,14 @@ WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING BUF_SIZE = 32 * 1024 +# helper exceptions for TCPRelayHandler + +class BadSocksHeader(Exception): + pass + +class NoAcceptableMethods(Exception): + pass + class TCPRelayHandler(object): def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): @@ -481,6 +492,42 @@ class TCPRelayHandler(object): 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 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 @@ -506,10 +553,7 @@ class TCPRelayHandler(object): 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 + self._handle_stage_init(data) elif self._stage == STAGE_CONNECTING: self._handle_stage_connecting(data) elif (is_local and self._stage == STAGE_ADDR) or \ From f62a550e9f8351ab3dc9d57e0fb554d5022b3d81 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Sat, 16 Apr 2016 12:13:59 +0800 Subject: [PATCH 36/68] pep8 --- shadowsocks/tcprelay.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index cf45351..5100f1c 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -100,9 +100,11 @@ BUF_SIZE = 32 * 1024 class BadSocksHeader(Exception): pass + class NoAcceptableMethods(Exception): pass + class TCPRelayHandler(object): def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): @@ -500,7 +502,8 @@ class TCPRelayHandler(object): socks_version = common.ord(data[0]) nmethods = common.ord(data[1]) if socks_version != 5: - logging.warning('unsupported SOCKS protocol version ' + str(socks_version)) + 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') @@ -511,7 +514,8 @@ class TCPRelayHandler(object): noauth_exist = True break if not noauth_exist: - logging.warning('none of METHOD\'s requested by client is supported') + logging.warning('none of SOCKS METHOD\'s ' + 'requested by client is supported') raise NoAcceptableMethods def _handle_stage_init(self, data): From 9844ba9dc75028e1bbe7ea02e7ba7f4b181bf178 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Sat, 16 Apr 2016 12:19:32 +0800 Subject: [PATCH 37/68] pep8 --- shadowsocks/tcprelay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 5100f1c..1387ac7 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -502,7 +502,7 @@ class TCPRelayHandler(object): socks_version = common.ord(data[0]) nmethods = common.ord(data[1]) if socks_version != 5: - logging.warning('unsupported SOCKS protocol version ' + + logging.warning('unsupported SOCKS protocol version ' + str(socks_version)) raise BadSocksHeader if nmethods < 1 or len(data) != nmethods + 2: From 677e6c4c563328191ca513d2f766574325d8a6eb Mon Sep 17 00:00:00 2001 From: v3aqb Date: Sun, 15 May 2016 01:27:21 +0800 Subject: [PATCH 38/68] fix ota --- shadowsocks/tcprelay.py | 1 + shadowsocks/udprelay.py | 1 + 2 files changed, 2 insertions(+) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 1387ac7..cc49d67 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -328,6 +328,7 @@ class TCPRelayHandler(object): if self._is_local is False: # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._ota_enable or addrtype & ADDRTYPE_AUTH: + self._ota_enable = True if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('one time auth header is too short') return None diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 6bf6ce6..849f611 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -184,6 +184,7 @@ class UDPRelay(object): 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: + self._one_time_auth_enable = True if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('UDP one time auth header is too short') return From f7afcd4df067a4fb5e31f2fedbd475081aba92a3 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Mon, 18 Jul 2016 23:00:00 +0800 Subject: [PATCH 39/68] add ota help in local --- shadowsocks/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 42efbc0..6cca837 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -294,6 +294,7 @@ Proxy options: -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb -t TIMEOUT timeout in seconds, default: 300 + -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ General options: From 8e8ee5d490ce319b8db9b61001dac51a7da4be63 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Mon, 18 Jul 2016 23:31:51 +0800 Subject: [PATCH 40/68] Update tcprelay.py one time auth fail should be return --- shadowsocks/tcprelay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index cc49d67..075a50b 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -339,6 +339,7 @@ class TCPRelayHandler(object): if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail') self.destroy() + return header_length += ONETIMEAUTH_BYTES self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading From 6ef14e56db71757721e452da5ece0084a092e08a Mon Sep 17 00:00:00 2001 From: smounives Date: Wed, 17 Aug 2016 17:06:54 +0800 Subject: [PATCH 41/68] Add chacha20-ietf crypto (#590) * Add chacha20-ietf crypto * fix chacha20-ietf * PEP8 * Update sodium.py (#1) pep8 --- setup.py | 2 +- shadowsocks/crypto/sodium.py | 23 +++++++++++++++++++++-- tests/chacha20-ietf.json | 10 ++++++++++ tests/libsodium/install.sh | 8 ++++---- 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 tests/chacha20-ietf.json diff --git a/setup.py b/setup.py index e6a7ff7..ba61ad3 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.8.2", + version="2.9.0", license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index ae86fef..b744e2c 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, \ +from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \ create_string_buffer, c_void_p from shadowsocks.crypto import util @@ -29,7 +29,7 @@ loaded = False buf_size = 2048 -# for salsa20 and chacha20 +# for salsa20 and chacha20 and chacha20-ietf BLOCK_SIZE = 64 @@ -51,6 +51,13 @@ def load_libsodium(): 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) buf = create_string_buffer(buf_size) loaded = True @@ -68,6 +75,8 @@ class SodiumCrypto(object): self.cipher = libsodium.crypto_stream_salsa20_xor_ic elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic + elif cipher_name == 'chacha20-ietf': + self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic else: raise Exception('Unknown cipher') # byte counter, not block counter @@ -97,6 +106,7 @@ class SodiumCrypto(object): ciphers = { 'salsa20': (32, 8, SodiumCrypto), 'chacha20': (32, 8, SodiumCrypto), + 'chacha20-ietf': (32, 12, SodiumCrypto), } @@ -115,6 +125,15 @@ def test_chacha20(): util.run_cipher(cipher, decipher) +def test_chacha20_ietf(): + + cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + if __name__ == '__main__': test_chacha20() test_salsa20() + test_chacha20_ietf() diff --git a/tests/chacha20-ietf.json b/tests/chacha20-ietf.json new file mode 100644 index 0000000..bfe3e1f --- /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/libsodium/install.sh b/tests/libsodium/install.sh index b0e35fa..790e471 100755 --- a/tests/libsodium/install.sh +++ b/tests/libsodium/install.sh @@ -1,10 +1,10 @@ #!/bin/bash -if [ ! -d libsodium-1.0.1 ]; then - wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 - tar xf libsodium-1.0.1.tar.gz || exit 1 +if [ ! -d libsodium-1.0.11 ]; then + wget https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz || exit 1 + tar xf libsodium-1.0.11.tar.gz || exit 1 fi -pushd libsodium-1.0.1 +pushd libsodium-1.0.11 ./configure && make -j2 && make install || exit 1 sudo ldconfig popd From f35590b2e205852be5e3ccdd6eac5df79fb0b5eb Mon Sep 17 00:00:00 2001 From: ficapy Date: Tue, 23 Aug 2016 22:00:32 +0800 Subject: [PATCH 42/68] Dockfile add libsodium (#601) --- Dockerfile | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36ddaa0..510d422 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,17 @@ -FROM stackbrew/debian:jessie -RUN apt-get update -RUN apt-get install -y python python-setuptools +FROM ubuntu:14.04 -ADD . /shadowsocks +RUN apt-get update && apt-get install -y \ + python-software-properties \ + software-properties-common \ + && add-apt-repository ppa:chris-lea/libsodium \ + && echo "deb http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \ + && echo "deb-src http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y libsodium-dev python-pip -WORKDIR /shadowsocks -RUN python setup.py install -CMD ssserver +RUN pip install shadowsocks + +ENTRYPOINT ["/usr/local/bin/ssserver"] + +# usage: +# docker run -d --restart=always -p 1314:1314 ficapy/shadowsocks -s 0.0.0.0 -p 1314 -k $PD -m chacha20 From 56bf81f58a09944c4a68a53711329ebe8df2dca3 Mon Sep 17 00:00:00 2001 From: v3aqb Date: Sun, 4 Sep 2016 15:05:15 +0800 Subject: [PATCH 43/68] fix ota (#609)(#610) (#610) --- shadowsocks/tcprelay.py | 20 ++++++++++++-------- shadowsocks/udprelay.py | 14 +++++++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 075a50b..54c364f 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -126,6 +126,7 @@ class TCPRelayHandler(object): self._ota_enable = True else: self._ota_enable = False + self._ota_enable_session = self._ota_enable self._ota_buff_head = b'' self._ota_buff_data = b'' self._ota_len = 0 @@ -247,12 +248,12 @@ class TCPRelayHandler(object): def _handle_stage_connecting(self, data): if self._is_local: - if self._ota_enable: + if self._ota_enable_session: data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) self._data_to_write_to_remote.append(data) else: - if self._ota_enable: + if self._ota_enable_session: self._ota_chunk_data(data, self._data_to_write_to_remote.append) else: @@ -327,8 +328,11 @@ class TCPRelayHandler(object): self._client_address[0], self._client_address[1])) if self._is_local is False: # spec https://shadowsocks.org/en/spec/one-time-auth.html - if self._ota_enable or addrtype & ADDRTYPE_AUTH: - self._ota_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('one time auth header is too short') return None @@ -352,7 +356,7 @@ class TCPRelayHandler(object): self._local_sock) # spec https://shadowsocks.org/en/spec/one-time-auth.html # ATYP & 0x10 == 1, then OTA is enabled. - if self._ota_enable: + if self._ota_enable_session: data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] key = self._encryptor.cipher_iv + self._encryptor.key data += onetimeauth_gen(data, key) @@ -362,7 +366,7 @@ class TCPRelayHandler(object): self._dns_resolver.resolve(self._chosen_server[0], self._handle_dns_resolved) else: - if self._ota_enable: + if self._ota_enable_session: data = data[header_length:] self._ota_chunk_data(data, self._data_to_write_to_remote.append) @@ -485,12 +489,12 @@ class TCPRelayHandler(object): 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._encryptor.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) diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 849f611..41ae5d6 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -99,9 +99,10 @@ class UDPRelay(object): self._method = config['method'] self._timeout = config['timeout'] if 'one_time_auth' in config and config['one_time_auth']: - self._one_time_auth_enable = True + self._ota_enable = True else: - self._one_time_auth_enable = False + self._ota_enable = 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) @@ -183,8 +184,11 @@ class UDPRelay(object): 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: - 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('UDP one time auth header is too short') return @@ -226,7 +230,7 @@ class UDPRelay(object): if self._is_local: key, iv, m = encrypt.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) data = encrypt.encrypt_all_m(key, iv, m, self._method, data) if not data: From 5c11527e1b1294d6e5195e3b0bec34276d4bb950 Mon Sep 17 00:00:00 2001 From: mengskysama Date: Sun, 4 Sep 2016 01:18:13 -0700 Subject: [PATCH 44/68] fix ota call write_to_sock multi --- shadowsocks/tcprelay.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 54c364f..810e713 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -449,6 +449,7 @@ class TCPRelayHandler(object): 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 @@ -472,11 +473,12 @@ class TCPRelayHandler(object): if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail, drop chunk !') 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): From 5cd9f04948c6a46bf3af75577a2225bff9ebc9f1 Mon Sep 17 00:00:00 2001 From: ahxxm Date: Fri, 7 Oct 2016 12:30:17 +0800 Subject: [PATCH 45/68] Refactor (#615) * make tcprelay.py less nested * import traceback module at the top * make loops in DNSResolver less nested * make manager.py less nested * introduce exception_handle decorator make try/except block more clean * apply exception_handle decorator to tcprelay * quote condition judgement * pep8 fix --- shadowsocks/asyncdns.py | 39 +++--- shadowsocks/eventloop.py | 2 +- shadowsocks/local.py | 44 +++--- shadowsocks/manager.py | 26 ++-- shadowsocks/shell.py | 47 +++++++ shadowsocks/tcprelay.py | 293 +++++++++++++++++++-------------------- 6 files changed, 248 insertions(+), 203 deletions(-) diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 44528d7..91601ea 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -276,15 +276,18 @@ class DNSResolver(object): content = f.readlines() for line in content: line = line.strip() - if line: - if line.startswith(b'nameserver'): - parts = line.split() - if len(parts) >= 2: - server = parts[1] - if common.is_ip(server) == socket.AF_INET: - if type(server) != str: - server = server.decode('utf8') - self._servers.append(server) + if not (line and line.startswith(b'nameserver')): + continue + + parts = line.split() + if len(parts) < 2: + continue + + server = parts[1] + if common.is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append(server) except IOError: pass if not self._servers: @@ -299,13 +302,17 @@ class DNSResolver(object): for line in f.readlines(): line = line.strip() parts = line.split() - if len(parts) >= 2: - ip = parts[0] - if common.is_ip(ip): - for i in range(1, len(parts)): - hostname = parts[i] - if hostname: - self._hosts[hostname] = ip + if len(parts) < 2: + continue + + ip = parts[0] + if not common.is_ip(ip): + continue + + for i in range(1, len(parts)): + hostname = parts[i] + if hostname: + self._hosts[hostname] = ip except IOError: self._hosts['localhost'] = '127.0.0.1' diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 78b532c..ce5da37 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -25,6 +25,7 @@ import os import time import socket import select +import traceback import errno import logging from collections import defaultdict @@ -204,7 +205,6 @@ class EventLoop(object): logging.debug('poll:%s', e) else: logging.error('poll:%s', e) - import traceback traceback.print_exc() continue diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 4255a2e..dfc8032 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -27,6 +27,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns +@shell.exception_handle(self_=False, exit_code=1) def main(): shell.check_python() @@ -37,36 +38,31 @@ def main(): os.chdir(p) config = shell.get_config(True) - daemon.daemon_exec(config) - try: - logging.info("starting local at %s:%d" % - (config['local_address'], config['local_port'])) + logging.info("starting local at %s:%d" % + (config['local_address'], config['local_port'])) - dns_resolver = asyncdns.DNSResolver() - tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) - udp_server = udprelay.UDPRelay(config, dns_resolver, True) - loop = eventloop.EventLoop() - dns_resolver.add_to_loop(loop) - tcp_server.add_to_loop(loop) - udp_server.add_to_loop(loop) + dns_resolver = asyncdns.DNSResolver() + tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) + udp_server = udprelay.UDPRelay(config, dns_resolver, True) + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + tcp_server.add_to_loop(loop) + udp_server.add_to_loop(loop) - def handler(signum, _): - logging.warn('received SIGQUIT, doing graceful shutting down..') - tcp_server.close(next_tick=True) - udp_server.close(next_tick=True) - signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tcp_server.close(next_tick=True) + udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) - def int_handler(signum, _): - sys.exit(1) - signal.signal(signal.SIGINT, int_handler) - - daemon.set_user(config.get('user', None)) - loop.run() - except Exception as e: - shell.print_exception(e) + def int_handler(signum, _): sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + daemon.set_user(config.get('user', None)) + loop.run() if __name__ == '__main__': main() diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py index b42ffa9..bba542e 100644 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -173,18 +173,20 @@ class Manager(object): self._statistics.clear() def _send_control_data(self, data): - if self._control_client_addr: - try: - self._control_socket.sendto(data, self._control_client_addr) - except (socket.error, OSError, IOError) as e: - error_no = eventloop.errno_from_exception(e) - if error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK): - return - else: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() + if not self._control_client_addr: + return + + try: + self._control_socket.sendto(data, self._control_client_addr) + except (socket.error, OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() def run(self): self._loop.run() diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 6cca837..041086a 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -23,6 +23,10 @@ import json import sys import getopt import logging +import traceback + +from functools import wraps + from shadowsocks.common import to_bytes, to_str, IPNetwork from shadowsocks import encrypt @@ -53,6 +57,49 @@ def print_exception(e): traceback.print_exc() +def exception_handle(self_, err_msg=None, exit_code=None, + destroy=False, conn_err=False): + # self_: if function passes self as first arg + + def process_exception(e, self=None): + print_exception(e) + if err_msg: + logging.error(err_msg) + if exit_code: + sys.exit(1) + + if not self_: + return + + if conn_err: + addr, port = self._client_address[0], self._client_address[1] + logging.error('%s when handling connection from %s:%d' % + (e, addr, port)) + if self._config['verbose']: + traceback.print_exc() + if destroy: + self.destroy() + + def decorator(func): + if self_: + @wraps(func) + def wrapper(self, *args, **kwargs): + try: + func(self, *args, **kwargs) + except Exception as e: + process_exception(e, self) + else: + @wraps(func) + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception as e: + process_exception(e) + + return wrapper + return decorator + + def print_shadowsocks(): version = '' try: diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 810e713..2e4772d 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -190,21 +190,23 @@ class TCPRelayHandler(object): if self._upstream_status != status: self._upstream_status = status dirty = True - if dirty: - if self._local_sock: - event = eventloop.POLL_ERR - if self._downstream_status & WAIT_STATUS_WRITING: - event |= eventloop.POLL_OUT - if self._upstream_status & WAIT_STATUS_READING: - event |= eventloop.POLL_IN - self._loop.modify(self._local_sock, event) - if self._remote_sock: - event = eventloop.POLL_ERR - if self._downstream_status & WAIT_STATUS_READING: - event |= eventloop.POLL_IN - if self._upstream_status & WAIT_STATUS_WRITING: - event |= eventloop.POLL_OUT - self._loop.modify(self._remote_sock, event) + if not dirty: + return + + if self._local_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + if self._upstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + self._loop.modify(self._local_sock, event) + if self._remote_sock: + event = eventloop.POLL_ERR + if self._downstream_status & WAIT_STATUS_READING: + event |= eventloop.POLL_IN + if self._upstream_status & WAIT_STATUS_WRITING: + event |= eventloop.POLL_OUT + self._loop.modify(self._remote_sock, event) def _write_to_sock(self, data, sock): # write data to sock @@ -247,19 +249,20 @@ class TCPRelayHandler(object): return True def _handle_stage_connecting(self, data): - if self._is_local: - if self._ota_enable_session: - data = self._ota_chunk_data_gen(data) - data = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data) - else: + if not self._is_local: if self._ota_enable_session: self._ota_chunk_data(data, self._data_to_write_to_remote.append) else: self._data_to_write_to_remote.append(data) - if self._is_local and not self._fastopen_connected and \ - self._config['fast_open']: + return + + if self._ota_enable_session: + data = self._ota_chunk_data_gen(data) + data = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data) + + 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: @@ -293,93 +296,88 @@ class TCPRelayHandler(object): traceback.print_exc() self.destroy() + @shell.exception_handle(self_=True, destroy=True, conn_err=True) def _handle_stage_addr(self, data): - try: - if self._is_local: - cmd = common.ord(data[1]) - if cmd == CMD_UDP_ASSOCIATE: - logging.debug('UDP associate') - if self._local_sock.family == socket.AF_INET6: - header = b'\x05\x00\x00\x04' - else: - header = b'\x05\x00\x00\x01' - addr, port = self._local_sock.getsockname()[:2] - addr_to_send = socket.inet_pton(self._local_sock.family, - addr) - port_to_send = struct.pack('>H', port) - self._write_to_sock(header + addr_to_send + port_to_send, - self._local_sock) - self._stage = STAGE_UDP_ASSOC - # just wait for the client to disconnect - return - elif cmd == CMD_CONNECT: - # just trim VER CMD RSV - data = data[3:] + if self._is_local: + cmd = common.ord(data[1]) + if cmd == CMD_UDP_ASSOCIATE: + logging.debug('UDP associate') + if self._local_sock.family == socket.AF_INET6: + header = b'\x05\x00\x00\x04' else: - logging.error('unknown command %d', cmd) + header = b'\x05\x00\x00\x01' + addr, port = self._local_sock.getsockname()[:2] + addr_to_send = socket.inet_pton(self._local_sock.family, + addr) + port_to_send = struct.pack('>H', port) + self._write_to_sock(header + addr_to_send + port_to_send, + self._local_sock) + self._stage = STAGE_UDP_ASSOC + # just wait for the client to disconnect + return + elif cmd == CMD_CONNECT: + # just trim VER CMD RSV + data = data[3:] + else: + logging.error('unknown command %d', cmd) + self.destroy() + return + header_result = parse_header(data) + if header_result is None: + raise Exception('can not parse header') + addrtype, remote_addr, remote_port, header_length = header_result + logging.info('connecting %s:%d from %s:%d' % + (common.to_str(remote_addr), remote_port, + self._client_address[0], self._client_address[1])) + if self._is_local is False: + # spec https://shadowsocks.org/en/spec/one-time-auth.html + self._ota_enable_session = addrtype & ADDRTYPE_AUTH + if self._ota_enable and not self._ota_enable_session: + logging.warn('client one time auth is required') + return + if self._ota_enable_session: + if len(data) < header_length + ONETIMEAUTH_BYTES: + logging.warn('one time auth header is too short') + return None + offset = header_length + ONETIMEAUTH_BYTES + _hash = data[header_length: offset] + _data = data[:header_length] + key = self._encryptor.decipher_iv + self._encryptor.key + if onetimeauth_verify(_hash, _data, key) is False: + logging.warn('one time auth fail') self.destroy() return - header_result = parse_header(data) - if header_result is None: - raise Exception('can not parse header') - addrtype, remote_addr, remote_port, header_length = header_result - logging.info('connecting %s:%d from %s:%d' % - (common.to_str(remote_addr), remote_port, - self._client_address[0], self._client_address[1])) - if self._is_local is False: - # spec https://shadowsocks.org/en/spec/one-time-auth.html - self._ota_enable_session = addrtype & ADDRTYPE_AUTH - if self._ota_enable and not self._ota_enable_session: - logging.warn('client one time auth is required') - return - if self._ota_enable_session: - if len(data) < header_length + ONETIMEAUTH_BYTES: - logging.warn('one time auth header is too short') - return None - offset = header_length + ONETIMEAUTH_BYTES - _hash = data[header_length: offset] - _data = data[:header_length] - key = self._encryptor.decipher_iv + self._encryptor.key - if onetimeauth_verify(_hash, _data, key) is False: - logging.warn('one time auth fail') - self.destroy() - return - header_length += ONETIMEAUTH_BYTES - self._remote_address = (common.to_str(remote_addr), remote_port) - # pause reading - self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) - self._stage = STAGE_DNS - if self._is_local: - # forward address to remote - self._write_to_sock((b'\x05\x00\x00\x01' - b'\x00\x00\x00\x00\x10\x10'), - self._local_sock) - # spec https://shadowsocks.org/en/spec/one-time-auth.html - # ATYP & 0x10 == 1, then OTA is enabled. - if self._ota_enable_session: - data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] - key = self._encryptor.cipher_iv + self._encryptor.key - data += onetimeauth_gen(data, key) - data_to_send = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data_to_send) - # notice here may go into _handle_dns_resolved directly - self._dns_resolver.resolve(self._chosen_server[0], - self._handle_dns_resolved) - else: - if self._ota_enable_session: - data = data[header_length:] - self._ota_chunk_data(data, - self._data_to_write_to_remote.append) - elif len(data) > header_length: - self._data_to_write_to_remote.append(data[header_length:]) - # notice here may go into _handle_dns_resolved directly - self._dns_resolver.resolve(remote_addr, - self._handle_dns_resolved) - except Exception as e: - self._log_error(e) - if self._config['verbose']: - traceback.print_exc() - self.destroy() + header_length += ONETIMEAUTH_BYTES + self._remote_address = (common.to_str(remote_addr), remote_port) + # pause reading + self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) + self._stage = STAGE_DNS + if self._is_local: + # forward address to remote + self._write_to_sock((b'\x05\x00\x00\x01' + b'\x00\x00\x00\x00\x10\x10'), + self._local_sock) + # spec https://shadowsocks.org/en/spec/one-time-auth.html + # ATYP & 0x10 == 1, then OTA is enabled. + if self._ota_enable_session: + data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] + key = self._encryptor.cipher_iv + self._encryptor.key + data += onetimeauth_gen(data, key) + data_to_send = self._encryptor.encrypt(data) + self._data_to_write_to_remote.append(data_to_send) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(self._chosen_server[0], + self._handle_dns_resolved) + else: + if self._ota_enable_session: + data = data[header_length:] + self._ota_chunk_data(data, + self._data_to_write_to_remote.append) + elif len(data) > header_length: + self._data_to_write_to_remote.append(data[header_length:]) + # notice here may go into _handle_dns_resolved directly + self._dns_resolver.resolve(remote_addr, + self._handle_dns_resolved) def _create_remote_socket(self, ip, port): addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, @@ -398,51 +396,50 @@ class TCPRelayHandler(object): remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) return remote_sock + @shell.exception_handle(self_=True) def _handle_dns_resolved(self, result, error): if error: - self._log_error(error) + addr, port = self._client_address[0], self._client_address[1] + logging.error('%s when handling connection from %s:%d' % + (error, addr, port)) + self.destroy() + return + if not (result and result[1]): self.destroy() return - if result and result[1]: - ip = result[1] - 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 (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == \ - errno.EINPROGRESS: - pass - self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT, - self._server) - self._stage = STAGE_CONNECTING - self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) - self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) - return - except Exception as e: - shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() - self.destroy() + 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: + 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) @@ -661,10 +658,6 @@ class TCPRelayHandler(object): else: logging.warn('unknown socket') - def _log_error(self, e): - logging.error('%s when handling connection from %s:%d' % - (e, self._client_address[0], self._client_address[1])) - def destroy(self): # destroy the handler and release any resources # promises: From 4f28f7c8e409a06709bca4d8a805f6e6726fc089 Mon Sep 17 00:00:00 2001 From: loggerhead Date: Mon, 10 Oct 2016 23:05:20 +0800 Subject: [PATCH 46/68] Fix a OTA bug on `_ota_chunk_data` (#642) * fix a OTA bug * correct a wrong comment --- shadowsocks/tcprelay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 2e4772d..2ff7b21 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -358,7 +358,7 @@ class TCPRelayHandler(object): b'\x00\x00\x00\x00\x10\x10'), self._local_sock) # spec https://shadowsocks.org/en/spec/one-time-auth.html - # ATYP & 0x10 == 1, then OTA is enabled. + # ATYP & 0x10 == 0x10, then OTA is enabled. if self._ota_enable_session: data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] key = self._encryptor.cipher_iv + self._encryptor.key @@ -458,7 +458,7 @@ class TCPRelayHandler(object): return data_len = self._ota_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN] self._ota_len = struct.unpack('>H', data_len)[0] - length = min(self._ota_len, len(data)) + length = min(self._ota_len - len(self._ota_buff_data), len(data)) self._ota_buff_data += data[:length] data = data[length:] if len(self._ota_buff_data) == self._ota_len: From 5a05312189a4e94b454d225895b5e1709c1b9fc3 Mon Sep 17 00:00:00 2001 From: Shell Chen Date: Wed, 12 Oct 2016 00:31:41 +1100 Subject: [PATCH 47/68] Fix method on manager's add command (#578) (#614) --- shadowsocks/manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py index bba542e..f3a7b77 100644 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -141,6 +141,8 @@ class Manager(object): 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) From 6dae6e2c514134c139abbaee54ca74033738f8de Mon Sep 17 00:00:00 2001 From: zz Date: Sun, 20 Nov 2016 14:57:52 +0800 Subject: [PATCH 48/68] fix that autoban can not get ip when use ipv6 (#674) --- utils/autoban.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/autoban.py b/utils/autoban.py index 1bbb65c..c7af0a5 100755 --- a/utils/autoban.py +++ b/utils/autoban.py @@ -38,7 +38,7 @@ if __name__ == '__main__': 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) From 72f1d68a05a43024d678f7d3d71b64563a4473d6 Mon Sep 17 00:00:00 2001 From: loggerhead Date: Sun, 20 Nov 2016 14:59:32 +0800 Subject: [PATCH 49/68] Fixed #675 (#676) * fix a OTA bug * correct a wrong comment * ignore emacs autosave files * keep consistence with the defensive style * a little refractor * fix daemon stop failed (#675) * fix test failed --- .gitignore | 3 +++ shadowsocks/shell.py | 31 ++++++++++++++++--------------- shadowsocks/tcprelay.py | 14 +++++--------- shadowsocks/udprelay.py | 11 ++--------- tests/test_command.sh | 4 ++-- 5 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 6c1b61e..4907974 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ htmlcov .DS_Store .idea + +#Emacs +.#* diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 041086a..3c6676f 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -125,6 +125,22 @@ 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']) + 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) @@ -280,21 +296,6 @@ def get_config(is_local): config['local_port'] = config.get('local_port', 1080) config['one_time_auth'] = config.get('one_time_auth', False) config['prefer_ipv6'] = config.get('prefer_ipv6', 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['server_port'] = config.get('server_port', 8388) logging.getLogger('').handlers = [] diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 2ff7b21..207407a 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -122,10 +122,7 @@ class TCPRelayHandler(object): self._stage = STAGE_INIT self._encryptor = encrypt.Encryptor(config['password'], config['method']) - if 'one_time_auth' in config and config['one_time_auth']: - self._ota_enable = True - else: - self._ota_enable = False + 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'' @@ -138,10 +135,7 @@ class TCPRelayHandler(object): self._downstream_status = WAIT_STATUS_INIT self._client_address = local_sock.getpeername()[:2] self._remote_address = None - if 'forbidden_ip' in config: - self._forbidden_iplist = config['forbidden_ip'] - else: - self._forbidden_iplist = None + self._forbidden_iplist = config.get('forbidden_ip') if is_local: self._chosen_server = self._get_a_server() fd_to_handlers[local_sock.fileno()] = self @@ -362,7 +356,9 @@ class TCPRelayHandler(object): if self._ota_enable_session: data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] key = self._encryptor.cipher_iv + self._encryptor.key - data += onetimeauth_gen(data, key) + _header = data[:header_length] + sha110 = onetimeauth_gen(data, key) + data = _header + sha110 + data[header_length:] data_to_send = self._encryptor.encrypt(data) self._data_to_write_to_remote.append(data_to_send) # notice here may go into _handle_dns_resolved directly diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 41ae5d6..3a36cff 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -98,10 +98,7 @@ class UDPRelay(object): self._password = common.to_bytes(config['password']) self._method = config['method'] self._timeout = config['timeout'] - if 'one_time_auth' in config and config['one_time_auth']: - self._ota_enable = True - else: - self._ota_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'], @@ -112,11 +109,7 @@ class UDPRelay(object): 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') addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) if len(addrs) == 0: 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 From 229ec75e4d091050b54bab9bea57fe7b4ad1cec5 Mon Sep 17 00:00:00 2001 From: Yuan Alvin Fu Date: Sun, 20 Nov 2016 02:22:17 -0500 Subject: [PATCH 50/68] Update README.md (#643) --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index fb1ffca..ee4b880 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,15 @@ To check the log: Check all the options via `-h`. You can also use a [Configuration] file instead. +### Usage with Config File + +[Create configeration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File) + +To start: + + ssserver -c /etc/shadowsocks.json + + Documentation ------------- From d31003e9bf031dedaf8caef1daaee29be41f10a2 Mon Sep 17 00:00:00 2001 From: "Shell.Xu" Date: Sun, 20 Nov 2016 15:29:11 +0800 Subject: [PATCH 51/68] new debian package (#659) * * new upstream version * change repository. * change license. * standards version change to 3.9.8 * fix #810688: man page references texinfo documents which don't exist * * merge back thomas's change * * Fix compatible issue (Closes: #845016) --- debian/changelog | 23 +++++++++++++++++++ debian/control | 21 ++++++++++------- debian/copyright | 43 ++++++++++++++++------------------- debian/install | 2 +- debian/shadowsocks.manpages | 2 +- debian/sslocal.1 | 4 ++-- debian/ssserver.1 | 4 ++-- shadowsocks/crypto/openssl.py | 11 ++++++--- 8 files changed, 70 insertions(+), 40 deletions(-) diff --git a/debian/changelog b/debian/changelog index 4e7ad16..2bf239c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,26 @@ +shadowsocks (2.9.0-2) unstable; urgency=medium + + [ Shell.Xu ] + * Fix compatible issue (Closes: #845016) + + -- Shell.Xu Sun, 20 Nov 2016 14:33:31 +0800 + +shadowsocks (2.9.0-1) unstable; urgency=medium + + [ Shell Xu ] + * Upstream update (Closes: #824640) + * Remove reference not exists (Closes: #810688) + + [ Thomas Goirand ] + * Added lsb-base as Depends:. + * Removed Pre-Depends: dpkg (>= 1.15.6~). + * Ran wrap-and-sort -t -a. + * Fixed VCS URLs to use https. + * Removed useless obsolete version of python-all build-depends. + * Fixed debian/copyright ordering. + + -- Shell.Xu Sat, 01 Oct 2016 16:14:47 +0800 + shadowsocks (2.1.0-1) unstable; urgency=low * Initial release (Closes: #758900) diff --git a/debian/control b/debian/control index da00920..fa0b1ec 100644 --- a/debian/control +++ b/debian/control @@ -2,18 +2,23 @@ Source: shadowsocks Section: python Priority: extra Maintainer: Shell.Xu -Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools -Standards-Version: 3.9.5 -Homepage: https://github.com/clowwindy/shadowsocks -Vcs-Git: git://github.com/shell909090/shadowsocks.git -Vcs-Browser: http://github.com/shell909090/shadowsocks +Build-Depends: debhelper (>= 8), + python-all, + python-setuptools, +Standards-Version: 3.9.8 +Homepage: https://github.com/shadowsocks/shadowsocks +Vcs-Git: https://github.com/shell909090/shadowsocks.git +Vcs-Browser: https://github.com/shell909090/shadowsocks Package: shadowsocks Architecture: all -Pre-Depends: dpkg (>= 1.15.6~) -Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto +Depends: lsb-base (>= 3.0-6), + python-m2crypto, + python-pkg-resources, + ${misc:Depends}, + ${python:Depends}, Description: Fast tunnel proxy that helps you bypass firewalls A secure socks5 proxy, designed to protect your Internet traffic. . This package contain local and server part of shadowsocks, a fast, - powerful tunnel proxy to bypass firewalls. \ No newline at end of file + powerful tunnel proxy to bypass firewalls. diff --git a/debian/copyright b/debian/copyright index 7be8162..14f0851 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,30 +1,27 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: shadowsocks -Source: https://github.com/clowwindy/shadowsocks - -Files: debian/* -Copyright: 2014 Shell.Xu -License: Expat +Source: https://github.com/shadowsocks/shadowsocks Files: * Copyright: 2014 clowwindy -License: Expat +License: Apache-2.0 -License: Expat - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. +Files: debian/* +Copyright: 2016 Shell.Xu +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache License 2.0 can + be found in "/usr/share/common-licenses/Apache-2.0" diff --git a/debian/install b/debian/install index a614864..a16a450 100644 --- a/debian/install +++ b/debian/install @@ -1 +1 @@ -debian/config.json etc/shadowsocks/ \ No newline at end of file +debian/config.json etc/shadowsocks/ diff --git a/debian/shadowsocks.manpages b/debian/shadowsocks.manpages index 3df8a33..98b6847 100644 --- a/debian/shadowsocks.manpages +++ b/debian/shadowsocks.manpages @@ -1,2 +1,2 @@ debian/sslocal.1 -debian/ssserver.1 \ No newline at end of file +debian/ssserver.1 diff --git a/debian/sslocal.1 b/debian/sslocal.1 index 0c2cf51..05ef510 100644 --- a/debian/sslocal.1 +++ b/debian/sslocal.1 @@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors. The programs are documented fully by .IR "Shell Xu " and -.IR "Clowwindy ", -available via the Info system. +.IR "Clowwindy " +. diff --git a/debian/ssserver.1 b/debian/ssserver.1 index 0c2cf51..05ef510 100644 --- a/debian/ssserver.1 +++ b/debian/ssserver.1 @@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors. The programs are documented fully by .IR "Shell Xu " and -.IR "Clowwindy ", -available via the Info system. +.IR "Clowwindy " +. diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index 3775b6c..da7f177 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -32,7 +32,7 @@ buf_size = 2048 def load_openssl(): - global loaded, libcrypto, buf + global loaded, libcrypto, buf, ctx_cleanup libcrypto = util.find_library(('crypto', 'eay32'), 'EVP_get_cipherbyname', @@ -49,7 +49,12 @@ def load_openssl(): libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, c_char_p, c_int) - libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + try: + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup + except AttributeError: + libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,) + ctx_cleanup = libcrypto.EVP_CIPHER_CTX_reset libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): libcrypto.OpenSSL_add_all_ciphers() @@ -108,7 +113,7 @@ class OpenSSLCrypto(object): def clean(self): if self._ctx: - libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + ctx_cleanup(self._ctx) libcrypto.EVP_CIPHER_CTX_free(self._ctx) From bdefeb523f247df512a33088203fcd2804a0f641 Mon Sep 17 00:00:00 2001 From: Anthony Wong Date: Mon, 2 Jan 2017 14:56:25 +0800 Subject: [PATCH 52/68] Update README.md (#697) Add missing links. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee4b880..5c7fa39 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ CentOS: Windows: -See [Install Server on Windows] +See [Install Shadowsocks Server on Windows](https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows). ### Usage @@ -64,7 +64,7 @@ To start: Documentation ------------- -You can find all the documentation in the [Wiki]. +You can find all the documentation in the [Wiki](https://github.com/shadowsocks/shadowsocks/wiki). License ------- From 392e54e5086ce8b3617f6aab8b7e54fe516a0c41 Mon Sep 17 00:00:00 2001 From: Lution Date: Mon, 2 Jan 2017 14:56:57 +0800 Subject: [PATCH 53/68] update regex to support hostnames containing underscore (#694) --- shadowsocks/asyncdns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index 91601ea..fa5be41 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -29,7 +29,7 @@ from shadowsocks import common, lru_cache, eventloop, shell CACHE_SWEEP_INTERVAL = 30 -VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(? Date: Sun, 1 Jan 2017 23:08:59 -0800 Subject: [PATCH 54/68] 2.9.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ba61ad3..b27c8eb 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.9.0", + version="2.9.1", license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', From 8ac72b0b25920b72042e8ea7a769c5150c463765 Mon Sep 17 00:00:00 2001 From: hdl Date: Tue, 28 Feb 2017 06:15:47 -0800 Subject: [PATCH 55/68] parse dns_servre in config (#739) thx --- shadowsocks/shell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 3c6676f..1b65594 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -184,6 +184,8 @@ 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: + logging.info('Specified DNS server: %s' % config['dns_server']) encrypt.try_cipher(config['password'], config['method']) @@ -297,6 +299,7 @@ def get_config(is_local): config['one_time_auth'] = config.get('one_time_auth', False) config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) + config['dns_server'] = config.get('dns_server', None) logging.getLogger('').handlers = [] logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') From fe13c20dc1d1e5ce3e97aeecdce17855def39612 Mon Sep 17 00:00:00 2001 From: ImPerat0R_ Date: Tue, 28 Feb 2017 22:19:36 +0800 Subject: [PATCH 56/68] Correct spelling. (#755) thx --- shadowsocks/encrypt.py | 4 ++-- shadowsocks/udprelay.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index ece72ec..a86058b 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -153,7 +153,7 @@ def encrypt_all_m(key, iv, m, method, data): return b''.join(result) -def dencrypt_all(password, method, data): +def decrypt_all(password, method, data): result = [] method = method.lower() (key_len, iv_len, m) = method_supported[method] @@ -228,7 +228,7 @@ def test_encrypt_all_m(): 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) + plain2, key, iv = decrypt_all(b'key', method, cipher) assert plain == plain2 diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 3a36cff..ba8299b 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -158,9 +158,9 @@ class UDPRelay(object): else: data = data[3:] else: - data, key, iv = encrypt.dencrypt_all(self._password, - self._method, - data) + data, key, iv = encrypt.decrypt_all(self._password, + self._method, + data) # decrypt data if not data: logging.debug( From e06819c124d025c1a63d61c20ad37dbbf63cc534 Mon Sep 17 00:00:00 2001 From: Falseen Date: Thu, 2 Mar 2017 21:20:04 +0800 Subject: [PATCH 57/68] add simple ss-tunnel to shadowsocks for dns forward (#759) * add vscode to .gitignore * add config.json to gitignore * add simple ss-tunnel to shadowsocks for dns forward 1.add tunnel.py file to shadowoscks for dns forward (seem ss-tunnel of ss-libev) 2.add add_header to common.py for add socks5 request header 3.add dns_service dns_server dns_server_port dns_server_port dns_local_port to shell.py and config.json 4.update to udprelay for ss-tunnel (dns forward) 5.update to local.py for ss-tunnel * add config.json.example to shadowsocks * add tunnel_udp_server.close to local.py and tunnel.py * fix error about pep8 and pyflakes for travis * update rename rename add "tunnel_" to dns_service dns_server dns_server_port dns_local_port * fix tunnel for run tunnel alone * fix pep8 for travis * update the config name for tunnel ord name: tunnel_service tunnel_dns_server tunnel_dns_server_port tunnel_dns_local_port new name: both_tunnel_local tunnel_remote tunnel_remote_port tunnel_port * update for tunnel update to local.py shell.py tunnel.py for tunnel * update pep8 for travis * update config to _config for tunnel and fix pep8 * changed "add socks5 header to data" to "add ss header to data" and changed "remove socks5 header" to "remove ss header" * add tcp forward to tunnel and fix a bug for pack_addr from common.py 1. update tunnel to tcprelay.py 2. add tunnel_tcp_server to tunnel.py 3. add tunnel_tcp_server to local.py 4. add `address = to_bytes(address)` to `pack_addr` from common.py (fix a error when address is a domain) * fix pep8 for travis again * remove ss_header from tcprelay.py and update the "header_length" to udprelay.py 1. Remove unnecessary "add ss_header" from tcprelay.py 2. update "data[7:]" to "data[header_length:]" * remove "both_tunnel_local" and fix some error for tunnel * update * update add_header * rename is_tunnel to _is_tunnel https://github.com/shadowsocks/shadowsocks/pull/759 --- .gitignore | 7 ++++ config.json.example | 13 +++++++ shadowsocks/common.py | 8 +++++ shadowsocks/shell.py | 18 ++++++++++ shadowsocks/tcprelay.py | 76 ++++++++++++++++++++++++++--------------- shadowsocks/tunnel.py | 74 +++++++++++++++++++++++++++++++++++++++ shadowsocks/udprelay.py | 33 ++++++++++++++---- 7 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 config.json.example create mode 100644 shadowsocks/tunnel.py diff --git a/.gitignore b/.gitignore index 4907974..0b36d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,10 @@ htmlcov #Emacs .#* + +#vscode +.idea +.vscode + +#ss +config.json diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..4006656 --- /dev/null +++ b/config.json.example @@ -0,0 +1,13 @@ +{ + "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", + "tunnel_remote_port":53, + "tunnel_port":53 +} 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 @@ ADDRTYPE_MASK = 0xF def pack_addr(address): address_str = to_str(address) + address = to_bytes(address) for family in (socket.AF_INET, socket.AF_INET6): try: r = socket.inet_pton(family, address_str) @@ -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/shell.py b/shadowsocks/shell.py index 1b65594..b46510c 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -132,6 +132,13 @@ def check_config(config, is_local): 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: @@ -159,6 +166,12 @@ 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']: @@ -299,6 +312,11 @@ def get_config(is_local): config['one_time_auth'] = config.get('one_time_auth', False) config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) + + config['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) config['dns_server'] = config.get('dns_server', None) logging.getLogger('').handlers = [] diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 207407a..a61cba7 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -106,6 +106,7 @@ class NoAcceptableMethods(Exception): class TCPRelayHandler(object): + def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): self._server = server @@ -115,6 +116,10 @@ class TCPRelayHandler(object): self._remote_sock = None self._config = config self._dns_resolver = dns_resolver + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = server._is_tunnel # TCP Relay works as either sslocal or ssserver # if is_local, this is sslocal @@ -250,7 +255,6 @@ class TCPRelayHandler(object): else: self._data_to_write_to_remote.append(data) return - if self._ota_enable_session: data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) @@ -293,29 +297,36 @@ class TCPRelayHandler(object): @shell.exception_handle(self_=True, destroy=True, conn_err=True) def _handle_stage_addr(self, data): if self._is_local: - cmd = common.ord(data[1]) - if cmd == CMD_UDP_ASSOCIATE: - logging.debug('UDP associate') - if self._local_sock.family == socket.AF_INET6: - header = b'\x05\x00\x00\x04' - else: - header = b'\x05\x00\x00\x01' - addr, port = self._local_sock.getsockname()[:2] - addr_to_send = socket.inet_pton(self._local_sock.family, - addr) - port_to_send = struct.pack('>H', port) - self._write_to_sock(header + addr_to_send + port_to_send, - self._local_sock) - self._stage = STAGE_UDP_ASSOC - # just wait for the client to disconnect - return - elif cmd == CMD_CONNECT: - # just trim VER CMD RSV - data = data[3:] + 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: - logging.error('unknown command %d', cmd) - self.destroy() - return + cmd = common.ord(data[1]) + if cmd == CMD_UDP_ASSOCIATE: + logging.debug('UDP associate') + if self._local_sock.family == socket.AF_INET6: + header = b'\x05\x00\x00\x04' + else: + header = b'\x05\x00\x00\x01' + addr, port = self._local_sock.getsockname()[:2] + addr_to_send = socket.inet_pton(self._local_sock.family, + addr) + port_to_send = struct.pack('>H', port) + self._write_to_sock(header + addr_to_send + port_to_send, + self._local_sock) + self._stage = STAGE_UDP_ASSOC + # just wait for the client to disconnect + return + elif cmd == CMD_CONNECT: + # just trim VER CMD RSV + data = data[3:] + else: + logging.error('unknown command %d', cmd) + self.destroy() + return header_result = parse_header(data) if header_result is None: raise Exception('can not parse header') @@ -347,10 +358,12 @@ class TCPRelayHandler(object): self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS if self._is_local: - # forward address to remote - self._write_to_sock((b'\x05\x00\x00\x01' - b'\x00\x00\x00\x00\x10\x10'), - self._local_sock) + # jump over socks5 response + if not self._is_tunnel: + # forward address to remote + self._write_to_sock((b'\x05\x00\x00\x01' + b'\x00\x00\x00\x00\x10\x10'), + self._local_sock) # spec https://shadowsocks.org/en/spec/one-time-auth.html # ATYP & 0x10 == 0x10, then OTA is enabled. if self._ota_enable_session: @@ -558,7 +571,12 @@ class TCPRelayHandler(object): self._handle_stage_stream(data) return elif is_local and self._stage == STAGE_INIT: - self._handle_stage_init(data) + # 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 \ @@ -689,6 +707,7 @@ class TCPRelayHandler(object): class TCPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config self._is_local = is_local @@ -696,6 +715,7 @@ class TCPRelay(object): 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 100644 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 ba8299b..68e7c55 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -82,6 +82,7 @@ def client_key(source_addr, server_af): class UDPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config if is_local: @@ -94,6 +95,10 @@ class UDPRelay(object): self._listen_port = config['server_port'] self._remote_addr = None self._remote_port = None + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = False self._dns_resolver = dns_resolver self._password = common.to_bytes(config['password']) self._method = config['method'] @@ -151,12 +156,19 @@ class UDPRelay(object): 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('UDP drop a message since frag is not 0') - return + if self._is_tunnel: + # add ss header to data + tunnel_remote = self.tunnel_remote + tunnel_remote_port = self.tunnel_remote_port + data = common.add_header(tunnel_remote, + tunnel_remote_port, data) else: - data = data[3:] + frag = common.ord(data[2]) + if frag != 0: + logging.warn('UDP drop a message since frag is not 0') + return + else: + data = data[3:] else: data, key, iv = encrypt.decrypt_all(self._password, self._method, @@ -171,7 +183,8 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - + logging.info("udp data to %s:%d from %s:%d" + % (dest_addr, dest_port, r_addr[0], r_addr[1])) if self._is_local: server_addr, server_port = self._get_a_server() else: @@ -267,9 +280,15 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - response = b'\x00\x00\x00' + data + 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 From c4731de5328fd929e3f0abd58a5c02535f1f0ee5 Mon Sep 17 00:00:00 2001 From: Elvis Wang Date: Thu, 2 Mar 2017 21:28:16 +0800 Subject: [PATCH 58/68] Update README.md (#575) The shadowsocks in pypi is out of date, may install from github@master --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c7fa39..346e53e 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,12 @@ Server Debian / Ubuntu: apt-get install python-pip - pip install shadowsocks + pip install git+https://github.com/shadowsocks/shadowsocks.git@master CentOS: yum install python-setuptools && easy_install pip - pip install shadowsocks + pip install git+https://github.com/shadowsocks/shadowsocks.git@master Windows: From 445a3c9c7e34116c47718ebbcb6e07820e8963f0 Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Sat, 4 Mar 2017 14:37:29 +0800 Subject: [PATCH 59/68] Add AEAD ciphers support (#775) * Add AEAD ciphers support, add manger api * fix test_encrypt_all * Add manager api requirements * #775 fix UDP decrypt_all issue * fix udp replay: decrypt_all return a list remove manager api * fix indent according pep8 * remove abc requirement * remove unused import * fix pep8 format * fix test_aes_256_gcm() --- .gitignore | 8 +- setup.py | 2 +- shadowsocks/crypto/aead.py | 285 ++++++++++++++++++++++++++++++++++ shadowsocks/crypto/hkdf.py | 98 ++++++++++++ shadowsocks/crypto/openssl.py | 258 +++++++++++++++++++++++++----- shadowsocks/crypto/rc4_md5.py | 3 +- shadowsocks/crypto/sodium.py | 165 ++++++++++++++++++++ shadowsocks/crypto/table.py | 4 + shadowsocks/crypto/util.py | 27 +++- shadowsocks/cryptor.py | 235 ++++++++++++++++++++++++++++ shadowsocks/encrypt.py | 4 +- shadowsocks/manager.py | 6 +- shadowsocks/shell.py | 28 +++- shadowsocks/tcprelay.py | 28 ++-- shadowsocks/udprelay.py | 15 +- 15 files changed, 1086 insertions(+), 80 deletions(-) create mode 100644 shadowsocks/crypto/aead.py create mode 100644 shadowsocks/crypto/hkdf.py create mode 100644 shadowsocks/cryptor.py diff --git a/.gitignore b/.gitignore index 0b36d7d..e6f83bd 100644 --- a/.gitignore +++ b/.gitignore @@ -29,13 +29,17 @@ htmlcov .DS_Store .idea +tags #Emacs .#* +venv/ -#vscode +# VS-code +.vscode/ + +# Pycharm .idea -.vscode #ss config.json diff --git a/setup.py b/setup.py index b27c8eb..f41f481 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f: setup( name="shadowsocks", - version="2.9.1", + version="3.0.0", license='http://www.apache.org/licenses/LICENSE-2.0', description="A fast tunnel proxy that help you get through firewalls", author='clowwindy', diff --git a/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py new file mode 100644 index 0000000..1ca06f2 --- /dev/null +++ b/shadowsocks/crypto/aead.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Void Copyright NO ONE +# +# Void License +# +# The code belongs to no one. Do whatever you want. +# Forget about boring open source license. +# +# AEAD cipher for shadowsocks +# + +from __future__ import absolute_import, division, print_function, \ + with_statement + +from ctypes import create_string_buffer + +import hashlib +from struct import pack, unpack + +from shadowsocks.crypto import hkdf +from shadowsocks.common import ord, chr + + +EVP_CTRL_GCM_SET_IVLEN = 0x9 +EVP_CTRL_GCM_GET_TAG = 0x10 +EVP_CTRL_GCM_SET_TAG = 0x11 +EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN +EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG +EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG + +EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN +EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG +EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG + +AEAD_MSG_LEN_UNKNOWN = 0 +AEAD_CHUNK_SIZE_LEN = 2 +AEAD_CHUNK_SIZE_MASK = 0x3FFF + +CIPHER_NONCE_LEN = { + 'aes-128-gcm': 12, + 'aes-192-gcm': 12, + 'aes-256-gcm': 12, + 'chacha20-poly1305': 12, + 'chacha20-ietf-poly1305': 12, + 'xchacha20-ietf-poly1305': 24, +} + +CIPHER_TAG_LEN = { + 'aes-128-gcm': 16, + 'aes-192-gcm': 16, + 'aes-256-gcm': 16, + 'chacha20-poly1305': 16, + 'chacha20-ietf-poly1305': 16, + 'xchacha20-ietf-poly1305': 16, +} + +SUBKEY_INFO = b"ss-subkey" + + +def nonce_increment(nonce, nlen): + """ + Increase nonce by 1 in little endian + From libsodium sodium_increment(): + for (; i < nlen; i++) { + c += (uint_fast16_t) n[i]; + n[i] = (unsigned char) c; + c >>= 8; + } + :param nonce: string_buffer nonce + :param nlen: nonce length + :return: nonce plus by 1 + """ + c = 1 + i = 0 + # n = create_string_buffer(nlen) + while i < nlen: + c += ord(nonce[i]) + nonce[i] = chr(c & 0xFF) + c >>= 8 + i += 1 + return # n.raw + + +class AeadCryptoBase(object): + """ + Handles basic aead process of shadowsocks protocol + + TCP Chunk (after encryption, *ciphertext*) + +--------------+---------------+--------------+------------+ + | *DataLen* | DataLen_TAG | *Data* | Data_TAG | + +--------------+---------------+--------------+------------+ + | 2 | Fixed | Variable | Fixed | + +--------------+---------------+--------------+------------+ + + UDP (after encryption, *ciphertext*) + +--------+-----------+-----------+ + | NONCE | *Data* | Data_TAG | + +-------+-----------+-----------+ + | Fixed | Variable | Fixed | + +--------+-----------+-----------+ + """ + + def __init__(self, cipher_name, key, iv, op): + self._op = int(op) + self._salt = iv + self._nlen = CIPHER_NONCE_LEN[cipher_name] + self._nonce = create_string_buffer(self._nlen) + self._tlen = CIPHER_TAG_LEN[cipher_name] + + crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1) + self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key)) + # _chunk['mlen']: + # -1, waiting data len header + # n, n > 0, waiting data + self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''} + + self.encrypt_once = self.aead_encrypt + self.decrypt_once = self.aead_decrypt + + def cipher_ctx_init(self): + """ + Increase nonce to make it unique for the same key + :return: void + """ + nonce_increment(self._nonce, self._nlen) + # print("".join("%02x" % ord(b) for b in self._nonce)) + + def aead_encrypt(self, data): + """ + Encrypt data with authenticate tag + + :param data: plain text + :return: cipher text with tag + """ + raise Exception("Must implement aead_encrypt method") + + def encrypt_chunk(self, data): + """ + Encrypt a chunk for TCP chunks + + :param data: str + :return: (str, int) + """ + plen = len(data) + l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2 + + # network byte order + ctext = self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK)) + if len(ctext) != AEAD_CHUNK_SIZE_LEN + self._tlen: + raise Exception("data length invalid") + + self.cipher_ctx_init() + ctext += self.aead_encrypt(data) + if len(ctext) != l: + raise Exception("data length invalid") + + self.cipher_ctx_init() + return ctext, l + + def encrypt(self, data): + """ + Encrypt data, for TCP divided into chunks + For UDP data, call aead_encrypt instead + + :param data: str data bytes + :return: str encrypted data + """ + plen = len(data) + if plen <= AEAD_CHUNK_SIZE_MASK: + ctext, _ = self.encrypt_chunk(data) + return ctext + ctext, clen = b"", 0 + while plen > 0: + mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \ + else AEAD_CHUNK_SIZE_MASK + r, l = self.encrypt_chunk(data[:mlen]) + ctext += r + clen += l + data = data[mlen:] + plen -= mlen + + return ctext + + def aead_decrypt(self, data): + """ + Decrypt data and authenticate tag + + :param data: str cipher text with tag + :return: str plain text + """ + raise Exception("Must implement aead_decrypt method") + + def decrypt_chunk_size(self, data): + """ + Decrypt chunk size + + :param data: str encrypted msg + :return: (int, str) msg length and remaining encrypted data + """ + if self._chunk['mlen'] > 0: + return self._chunk['mlen'], data + data = self._chunk['data'] + data + self._chunk['data'] = b"" + + hlen = AEAD_CHUNK_SIZE_LEN + self._tlen + if hlen > len(data): + self._chunk['data'] = data + return 0, b"" + plen = self.aead_decrypt(data[:hlen]) + plen, = unpack("!H", plen) + if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0: + raise Exception('Invalid message length') + + self.cipher_ctx_init() + return plen, data[hlen:] + + def decrypt_chunk_payload(self, plen, data): + """ + Decrypted encrypted msg payload + + :param plen: int payload length + :param data: str encrypted data + :return: (str, str) plain text and remaining encrypted data + """ + data = self._chunk['data'] + data + if len(data) < plen + self._tlen: + self._chunk['mlen'] = plen + self._chunk['data'] = data + return b"", b"" + self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN + self._chunk['data'] = b"" + + plaintext = self.aead_decrypt(data[:plen + self._tlen]) + + if len(plaintext) != plen: + raise Exception("plaintext length invalid") + + self.cipher_ctx_init() + + return plaintext, data[plen + self._tlen:] + + def decrypt_chunk(self, data): + """ + Decrypt a TCP chunk + + :param data: str encrypted msg + :return: (str, str) decrypted msg and remaining encrypted data + """ + plen, data = self.decrypt_chunk_size(data) + if plen <= 0: + return b"", b"" + return self.decrypt_chunk_payload(plen, data) + + def decrypt(self, data): + """ + Decrypt data for TCP data divided into chunks + For UDP data, call aead_decrypt instead + + :param data: str + :return: str + """ + ptext, left = self.decrypt_chunk(data) + while len(left) > 0: + pnext, left = self.decrypt_chunk(left) + ptext += pnext + return ptext + + +def test_nonce_increment(): + buf = create_string_buffer(12) + print("".join("%02x" % ord(b) for b in buf)) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + nonce_increment(buf, 12) + print("".join("%02x" % ord(b) for b in buf)) + for i in range(256): + nonce_increment(buf, 12) + print("".join("%02x" % ord(b) for b in buf)) + + +if __name__ == '__main__': + test_nonce_increment() diff --git a/shadowsocks/crypto/hkdf.py b/shadowsocks/crypto/hkdf.py new file mode 100644 index 0000000..11998e6 --- /dev/null +++ b/shadowsocks/crypto/hkdf.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Void Copyright NO ONE +# +# Void License +# +# The code belongs to no one. Do whatever you want. +# Forget about boring open source license. +# +# HKDF for AEAD ciphers +# + +from __future__ import division + +import hmac +import hashlib +import sys + +if sys.version_info[0] == 3: + def buffer(x): + return x + + +def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256): + """ + Extract a pseudorandom key suitable for use with hkdf_expand + from the input_key_material and a salt using HMAC with the + provided hash (default SHA-256). + + salt should be a random, application-specific byte string. If + salt is None or the empty string, an all-zeros string of the same + length as the hash's block size will be used instead per the RFC. + + See the HKDF draft RFC and paper for usage notes. + """ + hash_len = algorithm().digest_size + if salt is None or len(salt) == 0: + salt = bytearray((0,) * hash_len) + return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\ + .digest() + + +def hkdf_expand(pseudo_random_key, info=b"", length=32, + algorithm=hashlib.sha256): + """ + Expand `pseudo_random_key` and `info` into a key of length `bytes` using + HKDF's expand function based on HMAC with the provided hash (default + SHA-256). See the HKDF draft RFC and paper for usage notes. + """ + hash_len = algorithm().digest_size + length = int(length) + if length > 255 * hash_len: + raise Exception("Cannot expand to more than 255 * %d = %d " + "bytes using the specified hash function" % + (hash_len, 255 * hash_len)) + blocks_needed = length // hash_len \ + + (0 if length % hash_len == 0 else 1) # ceil + okm = b"" + output_block = b"" + for counter in range(blocks_needed): + output_block = hmac.new( + pseudo_random_key, + buffer(output_block + info + bytearray((counter + 1,))), + algorithm + ).digest() + okm += output_block + return okm[:length] + + +class Hkdf(object): + """ + Wrapper class for HKDF extract and expand functions + """ + + def __init__(self, salt, input_key_material, algorithm=hashlib.sha256): + """ + Extract a pseudorandom key from `salt` and `input_key_material` + arguments. + + See the HKDF draft RFC for guidance on setting these values. + The constructor optionally takes a `algorithm` argument defining + the hash function use, defaulting to hashlib.sha256. + """ + self._hash = algorithm + self._prk = hkdf_extract(salt, input_key_material, self._hash) + + def expand(self, info, length=32): + """ + Generate output key material based on an `info` value + + Arguments: + - info - context to generate the OKM + - length - length in bytes of the key to generate + + See the HKDF draft RFC for guidance. + """ + return hkdf_expand(self._prk, info, length, self._hash) diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index da7f177..50cdc22 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -22,6 +22,8 @@ from ctypes import c_char_p, c_int, c_long, byref,\ from shadowsocks import common from shadowsocks.crypto import util +from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \ + nonce_increment, EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG __all__ = ['ciphers'] @@ -30,6 +32,8 @@ loaded = False buf_size = 2048 +CIPHER_ENC_UNCHANGED = -1 + def load_openssl(): global loaded, libcrypto, buf, ctx_cleanup @@ -45,10 +49,13 @@ def load_openssl(): libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, c_char_p, c_char_p, c_int) + libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = (c_void_p, c_int, c_int, c_void_p) libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, c_char_p, c_int) + libcrypto.EVP_CipherFinal_ex.argtypes = (c_void_p, c_void_p, c_void_p) + try: libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup @@ -64,7 +71,7 @@ def load_openssl(): def load_cipher(cipher_name): - func_name = 'EVP_' + cipher_name.replace('-', '_') + func_name = b'EVP_' + cipher_name.replace(b'-', b'_') if bytes != str: func_name = str(func_name, 'utf-8') cipher = getattr(libcrypto, func_name, None) @@ -74,9 +81,13 @@ def load_cipher(cipher_name): return None -class OpenSSLCrypto(object): - def __init__(self, cipher_name, key, iv, op): +class OpenSSLCryptoBase(object): + """ + OpenSSL crypto base class + """ + def __init__(self, cipher_name): self._ctx = None + self._cipher = None if not loaded: load_openssl() cipher_name = common.to_bytes(cipher_name) @@ -85,26 +96,30 @@ class OpenSSLCrypto(object): cipher = load_cipher(cipher_name) if not cipher: raise Exception('cipher %s not found in libcrypto' % cipher_name) - key_ptr = c_char_p(key) - iv_ptr = c_char_p(iv) self._ctx = libcrypto.EVP_CIPHER_CTX_new() + self._cipher = cipher if not self._ctx: raise Exception('can not create cipher context') - r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, - key_ptr, iv_ptr, c_int(op)) - if not r: - self.clean() - raise Exception('can not initialize cipher context') + + 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_long(0) l = len(data) if buf_size < l: buf_size = l * 2 buf = create_string_buffer(buf_size) - libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), - byref(cipher_out_len), c_char_p(data), l) + libcrypto.EVP_CipherUpdate( + self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l + ) # buf is copied to a str object when we access buf.raw return buf.raw[:cipher_out_len.value] @@ -117,39 +132,190 @@ class OpenSSLCrypto(object): libcrypto.EVP_CIPHER_CTX_free(self._ctx) +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) + + r = libcrypto.EVP_CipherInit_ex( + self._ctx, + self._cipher, None, + None, None, c_int(op) + ) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_SET_IVLEN), + c_int(self._nlen), + None + ) + if not r: + raise Exception('Set ivlen failed') + + self.cipher_ctx_init() + + def cipher_ctx_init(self): + """ + Need init cipher context after EVP_CipherFinal_ex to reuse context + :return: void + """ + key_ptr = c_char_p(self._skey) + iv_ptr = c_char_p(self._nonce.raw) + + r = libcrypto.EVP_CipherInit_ex( + self._ctx, + None, None, + key_ptr, iv_ptr, + c_int(CIPHER_ENC_UNCHANGED) + ) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + nonce_increment(self._nonce, self._nlen) + # print("".join("%02x" % ord(b) for b in self._nonce)) + + def set_tag(self, tag): + """ + Set tag before decrypt any data (update) + :param tag: authenticated tag + :return: void + """ + tag_len = self._tlen + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_SET_TAG), + c_int(tag_len), c_char_p(tag) + ) + if not r: + raise Exception('Set tag failed') + + def get_tag(self): + """ + Get authenticated tag, called after EVP_CipherFinal_ex + :return: str + """ + tag_len = self._tlen + tag_buf = create_string_buffer(tag_len) + r = libcrypto.EVP_CIPHER_CTX_ctrl( + self._ctx, + c_int(EVP_CTRL_AEAD_GET_TAG), + c_int(tag_len), byref(tag_buf) + ) + if not r: + raise Exception('Get tag failed') + return tag_buf.raw[:tag_len] + + def final(self): + """ + Finish encrypt/decrypt a chunk (<= 0x3FFF) + :return: str + """ + global buf_size, buf + cipher_out_len = c_long(0) + r = libcrypto.EVP_CipherFinal_ex( + self._ctx, + byref(buf), byref(cipher_out_len) + ) + if not r: + # print(self._nonce.raw, r, cipher_out_len) + raise Exception('Verify data failed') + return buf.raw[:cipher_out_len.value] + + def aead_encrypt(self, data): + """ + Encrypt data with authenticate tag + + :param data: plain text + :return: cipher text with tag + """ + ctext = self.update(data) + self.final() + self.get_tag() + return ctext + + def aead_decrypt(self, data): + """ + Decrypt data and authenticate tag + + :param data: cipher text with tag + :return: plain text + """ + clen = len(data) + if clen < self._tlen: + raise Exception('Data too short') + + self.set_tag(data[clen - self._tlen:]) + plaintext = self.update(data[:clen - self._tlen]) + self.final() + return plaintext + + +class OpenSSLStreamCrypto(OpenSSLCryptoBase): + """ + Crypto for stream modes: cfb, ofb, ctr + """ + def __init__(self, cipher_name, key, iv, op): + super(OpenSSLStreamCrypto, self).__init__(cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + r = libcrypto.EVP_CipherInit_ex(self._ctx, self._cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + self.encrypt = self.update + self.decrypt = self.update + + ciphers = { - 'aes-128-cfb': (16, 16, OpenSSLCrypto), - 'aes-192-cfb': (24, 16, OpenSSLCrypto), - 'aes-256-cfb': (32, 16, OpenSSLCrypto), - 'aes-128-ofb': (16, 16, OpenSSLCrypto), - 'aes-192-ofb': (24, 16, OpenSSLCrypto), - 'aes-256-ofb': (32, 16, OpenSSLCrypto), - 'aes-128-ctr': (16, 16, OpenSSLCrypto), - 'aes-192-ctr': (24, 16, OpenSSLCrypto), - 'aes-256-ctr': (32, 16, OpenSSLCrypto), - 'aes-128-cfb8': (16, 16, OpenSSLCrypto), - 'aes-192-cfb8': (24, 16, OpenSSLCrypto), - 'aes-256-cfb8': (32, 16, OpenSSLCrypto), - 'aes-128-cfb1': (16, 16, OpenSSLCrypto), - 'aes-192-cfb1': (24, 16, OpenSSLCrypto), - 'aes-256-cfb1': (32, 16, OpenSSLCrypto), - 'bf-cfb': (16, 8, OpenSSLCrypto), - 'camellia-128-cfb': (16, 16, OpenSSLCrypto), - 'camellia-192-cfb': (24, 16, OpenSSLCrypto), - 'camellia-256-cfb': (32, 16, OpenSSLCrypto), - 'cast5-cfb': (16, 8, OpenSSLCrypto), - 'des-cfb': (8, 8, OpenSSLCrypto), - 'idea-cfb': (16, 8, OpenSSLCrypto), - 'rc2-cfb': (16, 8, OpenSSLCrypto), - 'rc4': (16, 0, OpenSSLCrypto), - 'seed-cfb': (16, 16, OpenSSLCrypto), + 'aes-128-cfb': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb': (32, 16, OpenSSLStreamCrypto), + 'aes-128-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), + 'aes-128-ctr': (16, 16, OpenSSLStreamCrypto), + 'aes-192-ctr': (24, 16, OpenSSLStreamCrypto), + 'aes-256-ctr': (32, 16, OpenSSLStreamCrypto), + 'aes-128-cfb8': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb8': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb8': (32, 16, OpenSSLStreamCrypto), + 'aes-128-cfb1': (16, 16, OpenSSLStreamCrypto), + 'aes-192-cfb1': (24, 16, OpenSSLStreamCrypto), + 'aes-256-cfb1': (32, 16, OpenSSLStreamCrypto), + 'bf-cfb': (16, 8, OpenSSLStreamCrypto), + 'camellia-128-cfb': (16, 16, OpenSSLStreamCrypto), + 'camellia-192-cfb': (24, 16, OpenSSLStreamCrypto), + 'camellia-256-cfb': (32, 16, OpenSSLStreamCrypto), + 'cast5-cfb': (16, 8, OpenSSLStreamCrypto), + 'des-cfb': (8, 8, OpenSSLStreamCrypto), + 'idea-cfb': (16, 8, OpenSSLStreamCrypto), + 'rc2-cfb': (16, 8, OpenSSLStreamCrypto), + 'rc4': (16, 0, OpenSSLStreamCrypto), + 'seed-cfb': (16, 16, OpenSSLStreamCrypto), } def run_method(method): - cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) - decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0) + cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def run_aead_method(method, key_len=16): + + key_len = int(key_len) + cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0) util.run_cipher(cipher, decipher) @@ -158,6 +324,16 @@ def test_aes_128_cfb(): run_method('aes-128-cfb') +def test_aes_gcm(bits=128): + method = "aes-{0}-gcm".format(bits) + print(method, int(bits / 8)) + run_aead_method(method, bits / 8) + + +def test_aes_256_gcm(): + test_aes_gcm(256) + + def test_aes_256_cfb(): run_method('aes-256-cfb') @@ -184,3 +360,7 @@ def test_rc4(): if __name__ == '__main__': test_aes_128_cfb() + test_aes_gcm(128) + test_aes_gcm(192) + test_aes_gcm(256) + test_aes_256_gcm() diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 1f07a82..2f7f22e 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -18,7 +18,6 @@ from __future__ import absolute_import, division, print_function, \ with_statement import hashlib - from shadowsocks.crypto import openssl __all__ = ['ciphers'] @@ -30,7 +29,7 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, md5.update(key) md5.update(iv) rc4_key = md5.digest() - return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) + return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op) ciphers = { diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index b744e2c..48a459f 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -21,6 +21,7 @@ from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \ create_string_buffer, c_void_p from shadowsocks.crypto import util +from shadowsocks.crypto.aead import AeadCryptoBase __all__ = ['ciphers'] @@ -59,6 +60,67 @@ def load_libsodium(): c_ulong, c_char_p) + # chacha20-poly1305 + libsodium.crypto_aead_chacha20poly1305_encrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_encrypt.argtypes = ( + c_void_p, c_void_p, # c, clen + c_char_p, c_ulonglong, # m, mlen + c_char_p, c_ulonglong, # ad, adlen + c_char_p, # nsec, not used + c_char_p, c_char_p # npub, k + ) + libsodium.crypto_aead_chacha20poly1305_decrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_decrypt.argtypes = ( + c_void_p, c_void_p, # m, mlen + c_char_p, # nsec, not used + c_char_p, c_ulonglong, # c, clen + c_char_p, c_ulonglong, # ad, adlen + c_char_p, c_char_p # npub, k + ) + + # chacha20-ietf-poly1305, same api structure as above + libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.restype = c_int + libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) + + # xchacha20-ietf-poly1305, same api structure as above + if hasattr(libsodium, 'crypto_aead_xchacha20poly1305_ietf_encrypt'): + libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.restype = c_int + libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + + libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.restype = c_int + libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) + + libsodium.sodium_increment.restype = c_void_p + libsodium.sodium_increment.argtypes = ( + c_void_p, c_int + ) + buf = create_string_buffer(buf_size) loaded = True @@ -81,6 +143,10 @@ class SodiumCrypto(object): raise Exception('Unknown cipher') # byte counter, not block counter self.counter = 0 + self.encrypt = self.update + self.decrypt = self.update + self.encrypt_once = self.update + self.decrypt_once = self.update def update(self, data): global buf_size, buf @@ -103,10 +169,85 @@ class SodiumCrypto(object): return buf.raw[padding:padding + l] +class SodiumAeadCrypto(AeadCryptoBase): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_libsodium() + AeadCryptoBase.__init__(self, cipher_name, key, iv, op) + + if cipher_name == 'chacha20-poly1305': + self.encryptor = libsodium.crypto_aead_chacha20poly1305_encrypt + self.decryptor = libsodium.crypto_aead_chacha20poly1305_decrypt + elif cipher_name == 'chacha20-ietf-poly1305': + self.encryptor = libsodium.\ + crypto_aead_chacha20poly1305_ietf_encrypt + self.decryptor = libsodium.\ + crypto_aead_chacha20poly1305_ietf_decrypt + elif cipher_name == 'xchacha20-ietf-poly1305': + if hasattr(libsodium, + 'crypto_aead_xchacha20poly1305_ietf_encrypt'): + self.encryptor = libsodium.\ + crypto_aead_xchacha20poly1305_ietf_encrypt + self.decryptor = libsodium.\ + crypto_aead_xchacha20poly1305_ietf_decrypt + else: + raise Exception('Unknown cipher') + else: + raise Exception('Unknown cipher') + + def cipher_ctx_init(self): + + libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) + # print("".join("%02x" % ord(b) for b in self._nonce)) + + def aead_encrypt(self, data): + global buf, buf_size + plen = len(data) + if buf_size < plen + self._tlen: + buf_size = (plen + self._tlen) * 2 + buf = create_string_buffer(buf_size) + cipher_out_len = c_ulonglong(0) + self.encryptor( + byref(buf), byref(cipher_out_len), + c_char_p(data), c_ulonglong(plen), + None, c_ulonglong(0), None, + c_char_p(self._nonce.raw), c_char_p(self._skey) + ) + if cipher_out_len.value != plen + self._tlen: + raise Exception("Encrypt failed") + + return buf.raw[:cipher_out_len.value] + + def aead_decrypt(self, data): + global buf, buf_size + clen = len(data) + if buf_size < clen: + buf_size = clen * 2 + buf = create_string_buffer(buf_size) + cipher_out_len = c_ulonglong(0) + r = self.decryptor( + byref(buf), byref(cipher_out_len), + None, + c_char_p(data), c_ulonglong(clen), + None, c_ulonglong(0), + c_char_p(self._nonce.raw), c_char_p(self._skey) + ) + if r != 0: + raise Exception("Decrypt failed") + + if cipher_out_len.value != clen - self._tlen: + raise Exception("Encrypt failed") + + return buf.raw[:cipher_out_len.value] + + ciphers = { 'salsa20': (32, 8, SodiumCrypto), 'chacha20': (32, 8, SodiumCrypto), 'chacha20-ietf': (32, 12, SodiumCrypto), + 'chacha20-poly1305': (32, 32, SodiumAeadCrypto), + 'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), + 'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), } @@ -133,7 +274,31 @@ def test_chacha20_ietf(): util.run_cipher(cipher, decipher) +def test_chacha20_poly1305(): + + print("Test chacha20-poly1305") + cipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_ietf_poly1305(): + + print("Test chacha20-ietf-poly1305") + cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + if __name__ == '__main__': test_chacha20() test_salsa20() test_chacha20_ietf() + test_chacha20_poly1305() + test_chacha20_ietf_poly1305() diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index bc693f5..9e11002 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -58,6 +58,10 @@ class TableCipher(object): def __init__(self, cipher_name, key, iv, op): self._encrypt_table, self._decrypt_table = init_table(key) self._op = op + self.encrypt = self.update + self.decrypt = self.update + self.encrypt_once = self.update + self.decrypt_once = self.update def update(self, data): if self._op: diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index e579455..bb23197 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -33,7 +33,7 @@ def find_library_nt(name): results.append(fname) if fname.lower().endswith(".dll"): continue - fname = fname + ".dll" + fname += ".dll" if os.path.isfile(fname): results.append(fname) return results @@ -92,14 +92,27 @@ def find_library(possible_lib_names, search_symbol, library_name): 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 + + def run_cipher(cipher, decipher): from os import urandom import random import time - BLOCK_SIZE = 16384 + block_size = 16384 rounds = 1 * 1024 - plain = urandom(BLOCK_SIZE * rounds) + plain = urandom(block_size * rounds) results = [] pos = 0 @@ -107,18 +120,18 @@ def run_cipher(cipher, decipher): start = time.time() while pos < len(plain): l = random.randint(100, 32768) - c = cipher.update(plain[pos:pos + l]) + c = cipher.encrypt(plain[pos:pos + l]) results.append(c) pos += l pos = 0 c = b''.join(results) results = [] - while pos < len(plain): + while pos < len(c): l = random.randint(100, 32768) - results.append(decipher.update(c[pos:pos + l])) + results.append(decipher.decrypt(c[pos:pos + l])) pos += l end = time.time() - print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start))) + print('speed: %d bytes/s' % (block_size * rounds / (end - start))) assert b''.join(results) == plain diff --git a/shadowsocks/cryptor.py b/shadowsocks/cryptor.py new file mode 100644 index 0000000..719b1b6 --- /dev/null +++ b/shadowsocks/cryptor.py @@ -0,0 +1,235 @@ +#!/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 + + +CIPHER_ENC_ENCRYPTION = 1 +CIPHER_ENC_DECRYPTION = 0 + +METHOD_INFO_KEY_LEN = 0 +METHOD_INFO_IV_LEN = 1 +METHOD_INFO_CRYPTO = 2 + +method_supported = {} +method_supported.update(rc4_md5.ciphers) +method_supported.update(openssl.ciphers) +method_supported.update(sodium.ciphers) +method_supported.update(table.ciphers) + + +def random_string(length): + return os.urandom(length) + +cached_keys = {} + + +def try_cipher(key, method=None): + Cryptor(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 Cryptor(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 = Cryptor.get_method_info(method) + if self._method_info: + self.cipher = self.get_cipher( + password, method, CIPHER_ENC_ENCRYPTION, + random_string(self._method_info[METHOD_INFO_IV_LEN]) + ) + else: + logging.error('method %s not supported' % method) + sys.exit(1) + + @staticmethod + def get_method_info(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[METHOD_INFO_KEY_LEN] > 0: + key, _ = EVP_BytesToKey(password, + m[METHOD_INFO_KEY_LEN], + m[METHOD_INFO_IV_LEN]) + else: + # key_length == 0 indicates we should use the key directly + key, iv = password, b'' + self.key = key + iv = iv[:m[METHOD_INFO_IV_LEN]] + if op == CIPHER_ENC_ENCRYPTION: + # this iv is for cipher not decipher + self.cipher_iv = iv + return m[METHOD_INFO_CRYPTO](method, key, iv, op) + + def encrypt(self, buf): + if len(buf) == 0: + return buf + if self.iv_sent: + return self.cipher.encrypt(buf) + else: + self.iv_sent = True + return self.cipher_iv + self.cipher.encrypt(buf) + + def decrypt(self, buf): + if len(buf) == 0: + return buf + if self.decipher is None: + decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN] + decipher_iv = buf[:decipher_iv_len] + self.decipher_iv = decipher_iv + self.decipher = self.get_cipher( + self.password, self.method, + CIPHER_ENC_DECRYPTION, + iv=decipher_iv + ) + buf = buf[decipher_iv_len:] + if len(buf) == 0: + return buf + return self.decipher.decrypt(buf) + + +def gen_key_iv(password, method): + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + if key_len > 0: + key, _ = EVP_BytesToKey(password, key_len, iv_len) + else: + key = password + iv = random_string(iv_len) + return key, iv, m + + +def encrypt_all_m(key, iv, m, method, data): + result = [iv] + cipher = m(method, key, iv, 1) + result.append(cipher.encrypt_once(data)) + return b''.join(result) + + +def decrypt_all(password, method, data): + 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) + result.append(cipher.decrypt_once(data)) + return b''.join(result), key, iv + + +def encrypt_all(password, method, data): + result = [] + method = method.lower() + (key, iv, m) = gen_key_iv(password, method) + result.append(iv) + cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION) + result.append(cipher.encrypt_once(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 = Cryptor(b'key', method) + decryptor = Cryptor(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, plain) + plain2, key, iv = decrypt_all(b'key', method, cipher) + assert plain == plain2 + + +def test_encrypt_all_m(): + from os import urandom + plain = urandom(10240) + for method in CIPHERS_TO_TEST: + logging.warn(method) + key, iv, m = gen_key_iv(b'key', method) + cipher = encrypt_all_m(key, iv, m, method, plain) + plain2, key, iv = decrypt_all(b'key', method, cipher) + assert plain == plain2 + + +if __name__ == '__main__': + test_encrypt_all() + test_encryptor() + test_encrypt_all_m() diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index a86058b..ece72ec 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -153,7 +153,7 @@ def encrypt_all_m(key, iv, m, method, data): return b''.join(result) -def decrypt_all(password, method, data): +def dencrypt_all(password, method, data): result = [] method = method.lower() (key_len, iv_len, m) = method_supported[method] @@ -228,7 +228,7 @@ def test_encrypt_all_m(): logging.warn(method) key, iv, m = gen_key_iv(b'key', method) cipher = encrypt_all_m(key, iv, m, method, plain) - plain2, key, iv = decrypt_all(b'key', method, cipher) + plain2, key, iv = dencrypt_all(b'key', method, cipher) assert plain == plain2 diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py index f3a7b77..55aea86 100644 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -202,7 +202,7 @@ def test(): import time import threading import struct - from shadowsocks import encrypt + from shadowsocks import cryptor logging.basicConfig(level=5, format='%(asctime)s %(levelname)-8s %(message)s', @@ -252,7 +252,7 @@ def test(): # test statistics for TCP header = common.pack_addr(b'google.com') + struct.pack('>H', 80) - data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, + data = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', header + b'GET /\r\n\r\n') tcp_cli = socket.socket() tcp_cli.connect(('127.0.0.1', 7001)) @@ -270,7 +270,7 @@ def test(): # test statistics for UDP header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) - data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, + data = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb', header + b'test') udp_cli = socket.socket(type=socket.SOCK_DGRAM) udp_cli.sendto(data, ('127.0.0.1', 8382)) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index b46510c..ce04c1d 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -28,7 +28,7 @@ import traceback from functools import wraps from shadowsocks.common import to_bytes, to_str, IPNetwork -from shadowsocks import encrypt +from shadowsocks import cryptor VERBOSE_LEVEL = 5 @@ -200,7 +200,7 @@ def check_config(config, is_local): if config.get('dns_server', None) is not None: logging.info('Specified DNS server: %s' % config['dns_server']) - encrypt.try_cipher(config['password'], config['method']) + cryptor.try_cipher(config['password'], config['method']) def get_config(is_local): @@ -362,6 +362,18 @@ Proxy options: -l LOCAL_PORT local port, default: 1080 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + supported method: + chacha20-poly1305, chacha20-ietf-poly1305, + *xchacha20-ietf-poly1305, + aes-128-gcm, aes-192-gcm, aes-256-gcm, + aes-128-cfb, aes-192-cfb, aes-256-cfb, + es-128-ctr, aes-192-ctr, aes-256-ctr, + camellia-128-cfb, camellia-192-cfb, + camellia-256-cfb, + salsa20, chacha20, chacha20-ietf, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. -t TIMEOUT timeout in seconds, default: 300 -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ @@ -392,6 +404,18 @@ Proxy options: -p SERVER_PORT server port, default: 8388 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + supported method: + chacha20-poly1305, chacha20-ietf-poly1305, + *xchacha20-ietf-poly1305, + aes-128-gcm, aes-192-gcm, aes-256-gcm, + aes-128-cfb, aes-192-cfb, aes-256-cfb, + es-128-ctr, aes-192-ctr, aes-256-ctr, + camellia-128-cfb, camellia-192-cfb, + camellia-256-cfb, + salsa20, chacha20, chacha20-ietf, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. -t TIMEOUT timeout in seconds, default: 300 -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index a61cba7..c57406d 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -26,7 +26,7 @@ import logging import traceback import random -from shadowsocks import encrypt, eventloop, shell, common +from shadowsocks import cryptor, eventloop, shell, common from shadowsocks.common import parse_header, onetimeauth_verify, \ onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \ ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH @@ -94,9 +94,9 @@ WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING BUF_SIZE = 32 * 1024 - # helper exceptions for TCPRelayHandler + class BadSocksHeader(Exception): pass @@ -125,8 +125,8 @@ class TCPRelayHandler(object): # if is_local, this is sslocal self._is_local = is_local self._stage = STAGE_INIT - self._encryptor = encrypt.Encryptor(config['password'], - config['method']) + self._cryptor = cryptor.Cryptor(config['password'], + config['method']) self._ota_enable = config.get('one_time_auth', False) self._ota_enable_session = self._ota_enable self._ota_buff_head = b'' @@ -257,7 +257,7 @@ class TCPRelayHandler(object): return if self._ota_enable_session: data = self._ota_chunk_data_gen(data) - data = self._encryptor.encrypt(data) + data = self._cryptor.encrypt(data) self._data_to_write_to_remote.append(data) if self._config['fast_open'] and not self._fastopen_connected: @@ -347,7 +347,7 @@ class TCPRelayHandler(object): offset = header_length + ONETIMEAUTH_BYTES _hash = data[header_length: offset] _data = data[:header_length] - key = self._encryptor.decipher_iv + self._encryptor.key + key = self._cryptor.decipher_iv + self._cryptor.key if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail') self.destroy() @@ -368,11 +368,11 @@ class TCPRelayHandler(object): # ATYP & 0x10 == 0x10, then OTA is enabled. if self._ota_enable_session: data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] - key = self._encryptor.cipher_iv + self._encryptor.key + key = self._cryptor.cipher_iv + self._cryptor.key _header = data[:header_length] sha110 = onetimeauth_gen(data, key) data = _header + sha110 + data[header_length:] - data_to_send = self._encryptor.encrypt(data) + data_to_send = self._cryptor.encrypt(data) self._data_to_write_to_remote.append(data_to_send) # notice here may go into _handle_dns_resolved directly self._dns_resolver.resolve(self._chosen_server[0], @@ -475,7 +475,7 @@ class TCPRelayHandler(object): _hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:] _data = self._ota_buff_data index = struct.pack('>I', self._ota_chunk_idx) - key = self._encryptor.decipher_iv + index + key = self._cryptor.decipher_iv + index if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail, drop chunk !') else: @@ -490,7 +490,7 @@ class TCPRelayHandler(object): def _ota_chunk_data_gen(self, data): data_len = struct.pack(">H", len(data)) index = struct.pack('>I', self._ota_chunk_idx) - key = self._encryptor.cipher_iv + index + key = self._cryptor.cipher_iv + index sha110 = onetimeauth_gen(data, key) self._ota_chunk_idx += 1 return data_len + sha110 + data @@ -499,7 +499,7 @@ class TCPRelayHandler(object): if self._is_local: if self._ota_enable_session: data = self._ota_chunk_data_gen(data) - data = self._encryptor.encrypt(data) + data = self._cryptor.encrypt(data) self._write_to_sock(data, self._remote_sock) else: if self._ota_enable_session: @@ -564,7 +564,7 @@ class TCPRelayHandler(object): return self._update_activity(len(data)) if not is_local: - data = self._encryptor.decrypt(data) + data = self._cryptor.decrypt(data) if not data: return if self._stage == STAGE_STREAM: @@ -598,9 +598,9 @@ class TCPRelayHandler(object): return self._update_activity(len(data)) if self._is_local: - data = self._encryptor.decrypt(data) + data = self._cryptor.decrypt(data) else: - data = self._encryptor.encrypt(data) + data = self._cryptor.encrypt(data) try: self._write_to_sock(data, self._local_sock) except Exception as e: diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 68e7c55..e966541 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -68,7 +68,7 @@ import struct import errno import random -from shadowsocks import encrypt, eventloop, lru_cache, common, shell +from shadowsocks import cryptor, eventloop, lru_cache, common, shell from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \ onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH @@ -170,7 +170,7 @@ class UDPRelay(object): else: data = data[3:] else: - data, key, iv = encrypt.decrypt_all(self._password, + data, key, iv = cryptor.decrypt_all(self._password, self._method, data) # decrypt data @@ -234,11 +234,11 @@ class UDPRelay(object): self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: - key, iv, m = encrypt.gen_key_iv(self._password, self._method) + key, iv, m = cryptor.gen_key_iv(self._password, self._method) # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._ota_enable_session: data = self._ota_chunk_data_gen(key, iv, data) - data = encrypt.encrypt_all_m(key, iv, m, self._method, data) + data = cryptor.encrypt_all_m(key, iv, m, self._method, data) if not data: return else: @@ -267,13 +267,12 @@ class UDPRelay(object): # drop return data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data - response = encrypt.encrypt_all(self._password, self._method, 1, - data) + response = cryptor.encrypt_all(self._password, self._method, data) if not response: return else: - data = encrypt.encrypt_all(self._password, self._method, 0, - data) + data, key, iv = cryptor.decrypt_all(self._password, + self._method, data) if not data: return header_result = parse_header(data) From d6b40efa5d9522406a710ae68244802160e8aba2 Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Sun, 5 Mar 2017 01:09:40 +0800 Subject: [PATCH 60/68] encrypt.py is renamed to cryptor.py (#780) --- shadowsocks/encrypt.py | 238 ----------------------------------------- 1 file changed, 238 deletions(-) delete mode 100644 shadowsocks/encrypt.py diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py deleted file mode 100644 index ece72ec..0000000 --- a/shadowsocks/encrypt.py +++ /dev/null @@ -1,238 +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() From 74f8f8cb852201b1f18272e2db2431e7179d32a0 Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Fri, 10 Mar 2017 17:44:42 +0800 Subject: [PATCH 61/68] Add sodium:aes-256-gcm and openssl:ocb AEAD mode (#784) * Add sodium:aes-256-gcm and openssl:ocb AEAD mode Use sodium_increment for nonce_increment if avaiable * Fix pep8 * Fix python3 test code --- shadowsocks/crypto/aead.py | 111 +++++++++++++++++++++++--------- shadowsocks/crypto/openssl.py | 82 ++++++++++++++++++----- shadowsocks/crypto/sodium.py | 118 ++++++++++++++++++++++++++++++---- shadowsocks/crypto/util.py | 20 +++--- shadowsocks/shell.py | 54 +++++++++------- 5 files changed, 295 insertions(+), 90 deletions(-) diff --git a/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py index 1ca06f2..23c0b36 100644 --- a/shadowsocks/crypto/aead.py +++ b/shadowsocks/crypto/aead.py @@ -14,11 +14,12 @@ from __future__ import absolute_import, division, print_function, \ with_statement -from ctypes import create_string_buffer +from ctypes import c_int, create_string_buffer, byref, c_void_p import hashlib from struct import pack, unpack +from shadowsocks.crypto import util from shadowsocks.crypto import hkdf from shadowsocks.common import ord, chr @@ -42,22 +43,58 @@ CIPHER_NONCE_LEN = { 'aes-128-gcm': 12, 'aes-192-gcm': 12, 'aes-256-gcm': 12, + 'aes-128-ocb': 12, # requires openssl 1.1 + 'aes-192-ocb': 12, + 'aes-256-ocb': 12, 'chacha20-poly1305': 12, 'chacha20-ietf-poly1305': 12, 'xchacha20-ietf-poly1305': 24, + 'sodium:aes-256-gcm': 12, } CIPHER_TAG_LEN = { 'aes-128-gcm': 16, 'aes-192-gcm': 16, 'aes-256-gcm': 16, + 'aes-128-ocb': 16, # requires openssl 1.1 + 'aes-192-ocb': 16, + 'aes-256-ocb': 16, 'chacha20-poly1305': 16, 'chacha20-ietf-poly1305': 16, 'xchacha20-ietf-poly1305': 16, + 'sodium:aes-256-gcm': 16, } SUBKEY_INFO = b"ss-subkey" +libsodium = None +sodium_loaded = False + + +def load_sodium(): + """ + Load libsodium helpers for nonce increment + :return: None + """ + global libsodium, sodium_loaded + + libsodium = util.find_library('sodium', 'sodium_increment', + 'libsodium') + if libsodium is None: + return + + if libsodium.sodium_init() < 0: + libsodium = None + return + + libsodium.sodium_increment.restype = c_void_p + libsodium.sodium_increment.argtypes = ( + c_void_p, c_int + ) + + sodium_loaded = True + return + def nonce_increment(nonce, nlen): """ @@ -119,12 +156,28 @@ class AeadCryptoBase(object): self.encrypt_once = self.aead_encrypt self.decrypt_once = self.aead_decrypt + # load libsodium for nonce increment + if not sodium_loaded: + load_sodium() + + def nonce_increment(self): + """ + AEAD ciphers need nonce to be unique per key + TODO: cache and check unique + :return: None + """ + global libsodium, sodium_loaded + if sodium_loaded: + libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) + else: + nonce_increment(self._nonce, self._nlen) + def cipher_ctx_init(self): """ Increase nonce to make it unique for the same key - :return: void + :return: None """ - nonce_increment(self._nonce, self._nlen) + self.nonce_increment() # print("".join("%02x" % ord(b) for b in self._nonce)) def aead_encrypt(self, data): @@ -132,7 +185,7 @@ class AeadCryptoBase(object): Encrypt data with authenticate tag :param data: plain text - :return: cipher text with tag + :return: str [payload][tag] cipher text with tag """ raise Exception("Must implement aead_encrypt method") @@ -141,23 +194,21 @@ class AeadCryptoBase(object): Encrypt a chunk for TCP chunks :param data: str - :return: (str, int) + :return: str [len][tag][payload][tag] """ plen = len(data) - l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2 + # l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2 # network byte order - ctext = self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK)) - if len(ctext) != AEAD_CHUNK_SIZE_LEN + self._tlen: + ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))] + if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen: + raise Exception("size length invalid") + + ctext.append(self.aead_encrypt(data)) + if len(ctext[1]) != plen + self._tlen: raise Exception("data length invalid") - self.cipher_ctx_init() - ctext += self.aead_encrypt(data) - if len(ctext) != l: - raise Exception("data length invalid") - - self.cipher_ctx_init() - return ctext, l + return b''.join(ctext) def encrypt(self, data): """ @@ -169,25 +220,24 @@ class AeadCryptoBase(object): """ plen = len(data) if plen <= AEAD_CHUNK_SIZE_MASK: - ctext, _ = self.encrypt_chunk(data) + ctext = self.encrypt_chunk(data) return ctext - ctext, clen = b"", 0 + ctext = [] while plen > 0: mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \ else AEAD_CHUNK_SIZE_MASK - r, l = self.encrypt_chunk(data[:mlen]) - ctext += r - clen += l + c = self.encrypt_chunk(data[:mlen]) + ctext.append(c) data = data[mlen:] plen -= mlen - return ctext + return b''.join(ctext) def aead_decrypt(self, data): """ Decrypt data and authenticate tag - :param data: str cipher text with tag + :param data: str [len][tag][payload][tag] cipher text with tag :return: str plain text """ raise Exception("Must implement aead_decrypt method") @@ -196,7 +246,7 @@ class AeadCryptoBase(object): """ Decrypt chunk size - :param data: str encrypted msg + :param data: str [size][tag] encrypted chunk payload len :return: (int, str) msg length and remaining encrypted data """ if self._chunk['mlen'] > 0: @@ -213,7 +263,6 @@ class AeadCryptoBase(object): if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0: raise Exception('Invalid message length') - self.cipher_ctx_init() return plen, data[hlen:] def decrypt_chunk_payload(self, plen, data): @@ -221,7 +270,7 @@ class AeadCryptoBase(object): Decrypted encrypted msg payload :param plen: int payload length - :param data: str encrypted data + :param data: str [payload][tag][[len][tag]....] encrypted data :return: (str, str) plain text and remaining encrypted data """ data = self._chunk['data'] + data @@ -237,15 +286,13 @@ class AeadCryptoBase(object): if len(plaintext) != plen: raise Exception("plaintext length invalid") - self.cipher_ctx_init() - return plaintext, data[plen + self._tlen:] def decrypt_chunk(self, data): """ Decrypt a TCP chunk - :param data: str encrypted msg + :param data: str [len][tag][payload][tag][[len][tag]...] encrypted msg :return: (str, str) decrypted msg and remaining encrypted data """ plen, data = self.decrypt_chunk_size(data) @@ -261,11 +308,13 @@ class AeadCryptoBase(object): :param data: str :return: str """ - ptext, left = self.decrypt_chunk(data) + ptext = [] + pnext, left = self.decrypt_chunk(data) + ptext.append(pnext) while len(left) > 0: pnext, left = self.decrypt_chunk(left) - ptext += pnext - return ptext + ptext.append(pnext) + return b''.join(ptext) def test_nonce_increment(): diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index 50cdc22..6d4d154 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -23,20 +23,24 @@ from ctypes import c_char_p, c_int, c_long, byref,\ from shadowsocks import common from shadowsocks.crypto import util from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \ - nonce_increment, EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG + EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG __all__ = ['ciphers'] libcrypto = None loaded = False +libsodium = None +buf = None buf_size = 2048 +ctx_cleanup = None + CIPHER_ENC_UNCHANGED = -1 def load_openssl(): - global loaded, libcrypto, buf, ctx_cleanup + global loaded, libcrypto, libsodium, buf, ctx_cleanup libcrypto = util.find_library(('crypto', 'eay32'), 'EVP_get_cipherbyname', @@ -140,10 +144,13 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): super(OpenSSLAeadCrypto, self).__init__(cipher_name) AeadCryptoBase.__init__(self, cipher_name, key, iv, op) + key_ptr = c_char_p(self._skey) r = libcrypto.EVP_CipherInit_ex( self._ctx, - self._cipher, None, - None, None, c_int(op) + self._cipher, + None, + key_ptr, None, + c_int(op) ) if not r: self.clean() @@ -165,21 +172,19 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): Need init cipher context after EVP_CipherFinal_ex to reuse context :return: void """ - key_ptr = c_char_p(self._skey) iv_ptr = c_char_p(self._nonce.raw) - r = libcrypto.EVP_CipherInit_ex( self._ctx, - None, None, - key_ptr, iv_ptr, + None, + None, + None, iv_ptr, c_int(CIPHER_ENC_UNCHANGED) ) if not r: self.clean() raise Exception('can not initialize cipher context') - nonce_increment(self._nonce, self._nlen) - # print("".join("%02x" % ord(b) for b in self._nonce)) + AeadCryptoBase.nonce_increment(self) def set_tag(self, tag): """ @@ -225,7 +230,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): ) if not r: # print(self._nonce.raw, r, cipher_out_len) - raise Exception('Verify data failed') + raise Exception('Finalize cipher failed') return buf.raw[:cipher_out_len.value] def aead_encrypt(self, data): @@ -236,6 +241,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): :return: cipher text with tag """ ctext = self.update(data) + self.final() + self.get_tag() + self.cipher_ctx_init() return ctext def aead_decrypt(self, data): @@ -251,6 +257,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): self.set_tag(data[clen - self._tlen:]) plaintext = self.update(data[:clen - self._tlen]) + self.final() + self.cipher_ctx_init() return plaintext @@ -305,6 +312,7 @@ ciphers = { def run_method(method): + print(method, ': [stream]', 32) cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1) decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0) @@ -313,6 +321,13 @@ def run_method(method): def run_aead_method(method, key_len=16): + print(method, ': [payload][tag]', key_len) + cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method)) + if not cipher: + cipher = load_cipher(common.to_bytes(method)) + if not cipher: + print('cipher not avaiable, please upgrade openssl') + return key_len = int(key_len) cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0) @@ -320,18 +335,46 @@ def run_aead_method(method, key_len=16): util.run_cipher(cipher, decipher) +def run_aead_method_chunk(method, key_len=16): + + print(method, ': chunk([size][tag][payload][tag]', key_len) + cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method)) + if not cipher: + cipher = load_cipher(common.to_bytes(method)) + if not cipher: + print('cipher not avaiable, please upgrade openssl') + return + key_len = int(key_len) + cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1) + decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + util.run_cipher(cipher, decipher) + + def test_aes_128_cfb(): run_method('aes-128-cfb') def test_aes_gcm(bits=128): method = "aes-{0}-gcm".format(bits) - print(method, int(bits / 8)) run_aead_method(method, bits / 8) -def test_aes_256_gcm(): - test_aes_gcm(256) +def test_aes_ocb(bits=128): + method = "aes-{0}-ocb".format(bits) + run_aead_method(method, bits / 8) + + +def test_aes_gcm_chunk(bits=128): + method = "aes-{0}-gcm".format(bits) + run_aead_method_chunk(method, bits / 8) + + +def test_aes_ocb_chunk(bits=128): + method = "aes-{0}-ocb".format(bits) + run_aead_method_chunk(method, bits / 8) def test_aes_256_cfb(): @@ -360,7 +403,16 @@ def test_rc4(): if __name__ == '__main__': test_aes_128_cfb() + test_aes_256_cfb() test_aes_gcm(128) test_aes_gcm(192) test_aes_gcm(256) - test_aes_256_gcm() + test_aes_gcm_chunk(128) + test_aes_gcm_chunk(192) + test_aes_gcm_chunk(256) + test_aes_ocb(128) + test_aes_ocb(192) + test_aes_ocb(256) + test_aes_ocb_chunk(128) + test_aes_ocb_chunk(192) + test_aes_ocb_chunk(256) diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index 48a459f..69196a3 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -21,6 +21,7 @@ from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \ create_string_buffer, c_void_p from shadowsocks.crypto import util +from shadowsocks.crypto import aead from shadowsocks.crypto.aead import AeadCryptoBase __all__ = ['ciphers'] @@ -28,6 +29,7 @@ __all__ = ['ciphers'] libsodium = None loaded = False +buf = None buf_size = 2048 # for salsa20 and chacha20 and chacha20-ietf @@ -37,10 +39,20 @@ BLOCK_SIZE = 64 def load_libsodium(): global loaded, libsodium, buf - libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', - 'libsodium') - if libsodium is None: - raise Exception('libsodium not found') + if not aead.sodium_loaded: + aead.load_sodium() + + if aead.sodium_loaded: + libsodium = aead.libsodium + else: + print('load libsodium again') + libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', + 'libsodium') + if libsodium is None: + raise Exception('libsodium not found') + + if libsodium.sodium_init() < 0: + raise Exception('libsodium init failed') libsodium.crypto_stream_salsa20_xor_ic.restype = c_int libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, @@ -116,10 +128,26 @@ def load_libsodium(): c_char_p, c_char_p ) - libsodium.sodium_increment.restype = c_void_p - libsodium.sodium_increment.argtypes = ( - c_void_p, c_int - ) + # aes-256-gcm, same api structure as above + libsodium.crypto_aead_aes256gcm_is_available.restype = c_int + + if libsodium.crypto_aead_aes256gcm_is_available(): + libsodium.crypto_aead_aes256gcm_encrypt.restype = c_int + libsodium.crypto_aead_aes256gcm_encrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, + c_char_p, c_char_p + ) + libsodium.crypto_aead_aes256gcm_decrypt.restype = c_int + libsodium.crypto_aead_aes256gcm_decrypt.argtypes = ( + c_void_p, c_void_p, + c_char_p, + c_char_p, c_ulonglong, + c_char_p, c_ulonglong, + c_char_p, c_char_p + ) buf = create_string_buffer(buf_size) loaded = True @@ -192,11 +220,15 @@ class SodiumAeadCrypto(AeadCryptoBase): crypto_aead_xchacha20poly1305_ietf_decrypt else: raise Exception('Unknown 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('Unknown cipher') def cipher_ctx_init(self): - + global libsodium libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) # print("".join("%02x" % ord(b) for b in self._nonce)) @@ -216,6 +248,7 @@ class SodiumAeadCrypto(AeadCryptoBase): if cipher_out_len.value != plen + self._tlen: raise Exception("Encrypt failed") + self.cipher_ctx_init() return buf.raw[:cipher_out_len.value] def aead_decrypt(self, data): @@ -238,6 +271,7 @@ class SodiumAeadCrypto(AeadCryptoBase): if cipher_out_len.value != clen - self._tlen: raise Exception("Encrypt failed") + self.cipher_ctx_init() return buf.raw[:cipher_out_len.value] @@ -248,10 +282,13 @@ ciphers = { 'chacha20-poly1305': (32, 32, SodiumAeadCrypto), 'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), 'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto), + 'sodium:aes-256-gcm': (32, 32, SodiumAeadCrypto), } def test_salsa20(): + + print("Test salsa20") cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) @@ -260,6 +297,7 @@ def test_salsa20(): 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) @@ -268,6 +306,7 @@ def test_chacha20(): 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) @@ -276,7 +315,7 @@ def test_chacha20_ietf(): def test_chacha20_poly1305(): - print("Test chacha20-poly1305") + print("Test chacha20-poly1305 [payload][tag]") cipher = SodiumAeadCrypto('chacha20-poly1305', b'k' * 32, b'i' * 32, 1) decipher = SodiumAeadCrypto('chacha20-poly1305', @@ -285,9 +324,23 @@ def test_chacha20_poly1305(): util.run_cipher(cipher, decipher) +def test_chacha20_poly1305_chunk(): + + print("Test chacha20-poly1305 chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-poly1305', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + def test_chacha20_ietf_poly1305(): - print("Test chacha20-ietf-poly1305") + print("Test chacha20-ietf-poly1305 [payload][tag]") cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', b'k' * 32, b'i' * 32, 1) decipher = SodiumAeadCrypto('chacha20-ietf-poly1305', @@ -296,9 +349,52 @@ def test_chacha20_ietf_poly1305(): util.run_cipher(cipher, decipher) +def test_chacha20_ietf_poly1305_chunk(): + + print("Test chacha20-ietf-poly1305 chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('chacha20-ietf-poly1305', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + +def test_aes_256_gcm(): + + print("Test sodium:aes-256-gcm [payload][tag]") + cipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 0) + + util.run_cipher(cipher, decipher) + + +def test_aes_256_gcm_chunk(): + + print("Test sodium:aes-256-gcm chunk [size][tag][payload][tag]") + cipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 1) + decipher = SodiumAeadCrypto('sodium:aes-256-gcm', + b'k' * 32, b'i' * 32, 0) + + cipher.encrypt_once = cipher.encrypt + decipher.decrypt_once = decipher.decrypt + + util.run_cipher(cipher, decipher) + + if __name__ == '__main__': test_chacha20() test_salsa20() test_chacha20_ietf() test_chacha20_poly1305() + test_chacha20_poly1305_chunk() test_chacha20_ietf_poly1305() + test_chacha20_ietf_poly1305_chunk() + test_aes_256_gcm() + test_aes_256_gcm_chunk() diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index bb23197..de389b9 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -114,25 +114,27 @@ def run_cipher(cipher, decipher): rounds = 1 * 1024 plain = urandom(block_size * rounds) - results = [] + cipher_results = [] pos = 0 print('test start') start = time.time() while pos < len(plain): l = random.randint(100, 32768) - c = cipher.encrypt(plain[pos:pos + l]) - results.append(c) + # print(pos, l) + c = cipher.encrypt_once(plain[pos:pos + l]) + cipher_results.append(c) pos += l pos = 0 - c = b''.join(results) - results = [] - while pos < len(c): - l = random.randint(100, 32768) - results.append(decipher.decrypt(c[pos:pos + l])) + # c = b''.join(cipher_results) + plain_results = [] + for c in cipher_results: + # l = random.randint(100, 32768) + l = len(c) + plain_results.append(decipher.decrypt_once(c)) pos += l end = time.time() print('speed: %d bytes/s' % (block_size * rounds / (end - start))) - assert b''.join(results) == plain + assert b''.join(plain_results) == plain def test_find_library(): diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index ce04c1d..1a7322c 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -362,18 +362,21 @@ Proxy options: -l LOCAL_PORT local port, default: 1080 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb - supported method: - chacha20-poly1305, chacha20-ietf-poly1305, - *xchacha20-ietf-poly1305, - aes-128-gcm, aes-192-gcm, aes-256-gcm, - aes-128-cfb, aes-192-cfb, aes-256-cfb, - es-128-ctr, aes-192-ctr, aes-256-ctr, - camellia-128-cfb, camellia-192-cfb, - camellia-256-cfb, - salsa20, chacha20, chacha20-ietf, - bf-cfb, cast5-cfb, des-cfb, idea-cfb, - rc2-cfb, seed-cfb, - rc4, rc4-md5, table. + Sodium: + chacha20-poly1305, chacha20-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, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. -t TIMEOUT timeout in seconds, default: 300 -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ @@ -404,18 +407,21 @@ Proxy options: -p SERVER_PORT server port, default: 8388 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb - supported method: - chacha20-poly1305, chacha20-ietf-poly1305, - *xchacha20-ietf-poly1305, - aes-128-gcm, aes-192-gcm, aes-256-gcm, - aes-128-cfb, aes-192-cfb, aes-256-cfb, - es-128-ctr, aes-192-ctr, aes-256-ctr, - camellia-128-cfb, camellia-192-cfb, - camellia-256-cfb, - salsa20, chacha20, chacha20-ietf, - bf-cfb, cast5-cfb, des-cfb, idea-cfb, - rc2-cfb, seed-cfb, - rc4, rc4-md5, table. + Sodium: + chacha20-poly1305, chacha20-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, + bf-cfb, cast5-cfb, des-cfb, idea-cfb, + rc2-cfb, seed-cfb, + rc4, rc4-md5, table. -t TIMEOUT timeout in seconds, default: 300 -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ From 4e21f83bd66287c54d15c08d5a85d47d68777c92 Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Wed, 15 Mar 2017 00:35:17 +0800 Subject: [PATCH 62/68] Handle cipher exceptions #783 (#791) --- shadowsocks/tcprelay.py | 1 + shadowsocks/tunnel.py | 0 shadowsocks/udprelay.py | 35 +++++++++++++++++++++++++---------- 3 files changed, 26 insertions(+), 10 deletions(-) mode change 100644 => 100755 shadowsocks/tunnel.py diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index c57406d..ea85e4d 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -641,6 +641,7 @@ class TCPRelayHandler(object): logging.error(eventloop.get_sock_error(self._remote_sock)) self.destroy() + @shell.exception_handle(self_=True, destroy=True) def handle_event(self, sock, event): # handle all events in this handler and dispatch them to methods if self._stage == STAGE_DESTROYED: diff --git a/shadowsocks/tunnel.py b/shadowsocks/tunnel.py old mode 100644 new mode 100755 diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index e966541..38af1b6 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -170,14 +170,16 @@ class UDPRelay(object): else: data = data[3:] else: - data, key, iv = cryptor.decrypt_all(self._password, - self._method, - data) # decrypt data + try: + data, key, iv = cryptor.decrypt_all(self._password, + self._method, + data) + except Exception: + logging.debug('UDP handle_server: decrypt data failed') + return if not data: - logging.debug( - 'UDP handle_server: data is empty after decrypt' - ) + logging.debug('UDP handle_server: data is empty after decrypt') return header_result = parse_header(data) if header_result is None: @@ -238,7 +240,11 @@ class UDPRelay(object): # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._ota_enable_session: data = self._ota_chunk_data_gen(key, iv, data) - data = cryptor.encrypt_all_m(key, iv, m, self._method, data) + try: + data = cryptor.encrypt_all_m(key, iv, m, self._method, data) + except Exception: + logging.debug("UDP handle_server: encrypt data failed") + return if not data: return else: @@ -267,12 +273,21 @@ class UDPRelay(object): # drop return data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data - response = cryptor.encrypt_all(self._password, self._method, data) + try: + response = cryptor.encrypt_all(self._password, + self._method, data) + except Exception: + logging.debug("UDP handle_client: encrypt data failed") + return if not response: return else: - data, key, iv = cryptor.decrypt_all(self._password, - self._method, data) + try: + data, key, iv = cryptor.decrypt_all(self._password, + self._method, data) + except Exception: + logging.debug('UDP handle_client: decrypt data failed') + return if not data: return header_result = parse_header(data) From 9e25cc6bb44836f39eabd25e677b066a8551972f Mon Sep 17 00:00:00 2001 From: Falseen Date: Sun, 19 Mar 2017 12:37:57 +0800 Subject: [PATCH 63/68] fix some error for parse dns_servre in config (#798) and fix pep8 https://github.com/shadowsocks/shadowsocks/pull/739 --- config.json.example | 1 + shadowsocks/shell.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config.json.example b/config.json.example index 4006656..2cee3b8 100644 --- a/config.json.example +++ b/config.json.example @@ -7,6 +7,7 @@ "method":"aes-256-cfb", "local_address":"127.0.0.1", "fast_open":false, + "dns_server":["8.8.8.8", 8.8.4.4], "tunnel_remote":"8.8.8.8", "tunnel_remote_port":53, "tunnel_port":53 diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 1a7322c..af33456 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -167,8 +167,7 @@ def check_config(config, is_local): config['server_port'] = int(config['server_port']) if 'tunnel_remote_port' in config: - config['tunnel_remote_port'] = \ - int(config['tunnel_remote_port']) + config['tunnel_remote_port'] = int(config['tunnel_remote_port']) if 'tunnel_port' in config: config['tunnel_port'] = int(config['tunnel_port']) @@ -198,6 +197,8 @@ def check_config(config, is_local): 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']) logging.info('Specified DNS server: %s' % config['dns_server']) cryptor.try_cipher(config['password'], config['method']) @@ -313,8 +314,7 @@ def get_config(is_local): config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) - config['tunnel_remote'] = \ - to_str(config.get('tunnel_remote', '8.8.8.8')) + 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) config['dns_server'] = config.get('dns_server', None) From 1222fb19a6b8712c142ad38840c68a1b237faae2 Mon Sep 17 00:00:00 2001 From: Falseen Date: Tue, 21 Mar 2017 20:15:38 +0800 Subject: [PATCH 64/68] update find_lib's name for the new openssl-1.1 dll (just for windows) (#796) the new openssl's dll(1.1) is rename to libcrypto-1_1-x64.dll and libcrypto-1_1.dll --- shadowsocks/crypto/util.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index de389b9..cf20e24 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -26,6 +26,7 @@ def find_library_nt(name): # ctypes.util.find_library just returns first result he found # but we want to try them all # because on Windows, users may have both 32bit and 64bit version installed + import glob results = [] for directory in os.environ['PATH'].split(os.pathsep): fname = os.path.join(directory, name) @@ -33,9 +34,10 @@ def find_library_nt(name): results.append(fname) if fname.lower().endswith(".dll"): continue - fname += ".dll" - if os.path.isfile(fname): - results.append(fname) + fname += "*.dll" + files = glob.glob(fname) + if files: + results.extend(files) return results From 0f4e3fa00cad2c6419be1a3b606b8fc7969eb38a Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Sat, 29 Apr 2017 14:27:55 +0800 Subject: [PATCH 65/68] Add mbedtls wrapper, custom crypto lib path, test files (#803) * add mbedtls crypto wrapper. add tests files for new aead ciphers add custom lib path support fix some typo * fix forbidden ip list * rm crypto lib build files * remove crypto source * add xchacha20 test config * convert dos new line format to unix format * Fix help msg --- .travis.yml | 2 + config.json.example | 7 +- shadowsocks/crypto/aead.py | 15 +- shadowsocks/crypto/mbedtls.py | 481 +++++++++++++++++++++++++++++ shadowsocks/crypto/openssl.py | 45 +-- shadowsocks/crypto/rc4_md5.py | 5 +- shadowsocks/crypto/sodium.py | 131 ++++---- shadowsocks/crypto/table.py | 2 +- shadowsocks/crypto/util.py | 35 ++- shadowsocks/cryptor.py | 34 +- shadowsocks/manager.py | 1 + shadowsocks/shell.py | 86 ++++-- shadowsocks/tcprelay.py | 17 +- shadowsocks/udprelay.py | 13 +- tests/aes-cfb1.json | 20 +- tests/aes-cfb8.json | 20 +- tests/aes-ctr.json | 20 +- tests/aes-gcm.json | 10 + tests/aes-ocb.json | 11 + tests/aes-ofb.json | 10 + tests/aes.json | 20 +- tests/camellia.json | 10 + tests/chacha20-ietf-poly1305.json | 10 + tests/chacha20-ietf.json | 20 +- tests/chacha20-poly1305.json | 10 + tests/chacha20.json | 20 +- tests/client-multi-server-ip.json | 20 +- tests/fastopen.json | 20 +- tests/graceful.json | 20 +- tests/ipv6-client-side.json | 20 +- tests/ipv6.json | 20 +- tests/jenkins.sh | 22 ++ tests/libmbedtls/install.sh | 12 + tests/libopenssl/install.sh | 12 + tests/libsodium/install.sh | 9 +- tests/mbedtls-aes-ctr.json | 10 + tests/mbedtls-aes-gcm.json | 10 + tests/mbedtls-aes.json | 10 + tests/mbedtls-camellia.json | 10 + tests/rc4-md5-ota.json | 22 +- tests/rc4-md5.json | 20 +- tests/salsa20-ctr.json | 20 +- tests/salsa20.json | 20 +- tests/server-dnsserver.json | 22 +- tests/table.json | 20 +- tests/workers.json | 20 +- tests/xchacha20-ietf-poly1305.json | 10 + tests/xchacha20.json | 10 + 48 files changed, 1088 insertions(+), 326 deletions(-) create mode 100644 shadowsocks/crypto/mbedtls.py create mode 100644 tests/aes-gcm.json create mode 100644 tests/aes-ocb.json create mode 100644 tests/aes-ofb.json create mode 100644 tests/camellia.json create mode 100644 tests/chacha20-ietf-poly1305.json create mode 100644 tests/chacha20-poly1305.json create mode 100755 tests/libmbedtls/install.sh create mode 100755 tests/libopenssl/install.sh create mode 100644 tests/mbedtls-aes-ctr.json create mode 100644 tests/mbedtls-aes-gcm.json create mode 100644 tests/mbedtls-aes.json create mode 100644 tests/mbedtls-camellia.json create mode 100644 tests/xchacha20-ietf-poly1305.json create mode 100644 tests/xchacha20.json diff --git a/.travis.yml b/.travis.yml index 535bc9a..dda0661 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ before_install: - 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 index 2cee3b8..a1f9125 100644 --- a/config.json.example +++ b/config.json.example @@ -7,8 +7,11 @@ "method":"aes-256-cfb", "local_address":"127.0.0.1", "fast_open":false, - "dns_server":["8.8.8.8", 8.8.4.4], "tunnel_remote":"8.8.8.8", + "dns_server":["8.8.8.8", "8.8.4.4"], "tunnel_remote_port":53, - "tunnel_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/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py index 23c0b36..d800f36 100644 --- a/shadowsocks/crypto/aead.py +++ b/shadowsocks/crypto/aead.py @@ -71,7 +71,7 @@ libsodium = None 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 @@ class AeadCryptoBase(object): # 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 @@ class AeadCryptoBase(object): 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 @@ class AeadCryptoBase(object): :return: None """ self.nonce_increment() - # print("".join("%02x" % ord(b) for b in self._nonce)) def aead_encrypt(self, data): """ @@ -331,4 +335,5 @@ def test_nonce_increment(): if __name__ == '__main__': + load_sodium() test_nonce_increment() 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 6d4d154..7f7eef2 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -39,14 +39,16 @@ ctx_cleanup = None 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: @@ -140,9 +142,9 @@ 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( @@ -170,7 +172,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): 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( @@ -190,7 +192,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): """ 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( @@ -265,8 +267,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, @@ -282,9 +284,6 @@ ciphers = { '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), @@ -307,6 +306,13 @@ ciphers = { '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), } @@ -353,10 +359,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) @@ -377,6 +379,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') @@ -404,6 +410,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 @@ from shadowsocks.crypto import openssl __all__ = ['ciphers'] -def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, +def create_cipher(alg, key, iv, op, crypto_path=None, + key_as_bytes=0, d=None, salt=None, i=1, padding=1): md5 = hashlib.md5() md5.update(key) md5.update(iv) rc4_key = md5.digest() - return openssl.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 69196a3..c5a6c5c 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 @@ buf_size = 2048 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 @@ class SodiumCrypto(object): self.cipher = libsodium.crypto_stream_salsa20_xor_ic elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic + elif cipher_name == 'xchacha20': + if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'): + self.cipher = libsodium.crypto_stream_xchacha20_xor_ic + else: + raise Exception('Unsupported cipher') elif cipher_name == 'chacha20-ietf': self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic else: @@ -198,32 +219,34 @@ class SodiumCrypto(object): 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') @@ -269,7 +292,7 @@ class SodiumAeadCrypto(AeadCryptoBase): 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] @@ -278,7 +301,9 @@ class SodiumAeadCrypto(AeadCryptoBase): 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), @@ -286,17 +311,7 @@ ciphers = { } -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(): - print("Test chacha20") cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) @@ -304,8 +319,23 @@ def test_chacha20(): util.run_cipher(cipher, decipher) -def test_chacha20_ietf(): +def test_xchacha20(): + print("Test xchacha20") + cipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 1) + decipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 0) + util.run_cipher(cipher, decipher) + + +def test_salsa20(): + print("Test salsa20") + cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20_ietf(): print("Test chacha20-ietf") cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1) decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0) @@ -314,7 +344,6 @@ def test_chacha20_ietf(): def test_chacha20_poly1305(): - print("Test chacha20-poly1305 [payload][tag]") cipher = SodiumAeadCrypto('chacha20-poly1305', b'k' * 32, b'i' * 32, 1) @@ -325,7 +354,6 @@ def test_chacha20_poly1305(): def test_chacha20_poly1305_chunk(): - print("Test chacha20-poly1305 chunk [size][tag][payload][tag]") cipher = SodiumAeadCrypto('chacha20-poly1305', b'k' * 32, b'i' * 32, 1) @@ -339,7 +367,6 @@ def test_chacha20_poly1305_chunk(): def test_chacha20_ietf_poly1305(): - print("Test chacha20-ietf-poly1305 [payload][tag]") cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', b'k' * 32, b'i' * 32, 1) @@ -350,7 +377,6 @@ def test_chacha20_ietf_poly1305(): def test_chacha20_ietf_poly1305_chunk(): - print("Test chacha20-ietf-poly1305 chunk [size][tag][payload][tag]") cipher = SodiumAeadCrypto('chacha20-ietf-poly1305', b'k' * 32, b'i' * 32, 1) @@ -364,7 +390,6 @@ def test_chacha20_ietf_poly1305_chunk(): def test_aes_256_gcm(): - print("Test sodium:aes-256-gcm [payload][tag]") cipher = SodiumAeadCrypto('sodium:aes-256-gcm', b'k' * 32, b'i' * 32, 1) @@ -375,7 +400,6 @@ def test_aes_256_gcm(): def test_aes_256_gcm_chunk(): - print("Test sodium:aes-256-gcm chunk [size][tag][payload][tag]") cipher = SodiumAeadCrypto('sodium:aes-256-gcm', b'k' * 32, b'i' * 32, 1) @@ -390,6 +414,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 9e11002..1752be5 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -55,7 +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..f52e7ba 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,9 @@ 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 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 hashlib import logging from shadowsocks import common -from shadowsocks.crypto import rc4_md5, openssl, sodium, table +from shadowsocks.crypto import rc4_md5, openssl, mbedtls, sodium, table CIPHER_ENC_ENCRYPTION = 1 @@ -36,6 +36,7 @@ METHOD_INFO_CRYPTO = 2 method_supported = {} method_supported.update(rc4_md5.ciphers) method_supported.update(openssl.ciphers) +method_supported.update(mbedtls.ciphers) method_supported.update(sodium.ciphers) method_supported.update(table.ciphers) @@ -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 @@ class Cryptor(object): 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 @@ class Cryptor(object): 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 @@ class Cryptor(object): 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/manager.py b/shadowsocks/manager.py index 55aea86..62f187e 100644 --- a/shadowsocks/manager.py +++ b/shadowsocks/manager.py @@ -71,6 +71,7 @@ class Manager(object): 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) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index af33456..df04cee 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -201,7 +201,12 @@ def check_config(config, is_local): config['dns_server'] = to_str(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): @@ -212,12 +217,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', - 'prefer-ipv6'] + 'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -261,10 +266,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': @@ -313,11 +324,14 @@ def get_config(is_local): config['one_time_auth'] = config.get('one_time_auth', False) config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) + config['dns_server'] = config.get('dns_server', None) + config['libopenssl'] = config.get('libopenssl', None) + config['libmbedtls'] = config.get('libmbedtls', None) + config['libsodium'] = config.get('libsodium', None) config['tunnel_remote'] = to_str(config.get('tunnel_remote', '8.8.8.8')) config['tunnel_remote_port'] = config.get('tunnel_remote_port', 53) config['tunnel_port'] = config.get('tunnel_port', 53) - config['dns_server'] = config.get('dns_server', None) logging.getLogger('').handlers = [] logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') @@ -364,29 +378,38 @@ Proxy options: -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 @@ -409,26 +432,35 @@ Proxy options: -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 ea85e4d..0ef913f 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -93,6 +93,8 @@ WAIT_STATUS_WRITING = 2 WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING BUF_SIZE = 32 * 1024 +UP_STREAM_BUF_SIZE = 16 * 1024 +DOWN_STREAM_BUF_SIZE = 32 * 1024 # helper exceptions for TCPRelayHandler @@ -126,7 +128,8 @@ class TCPRelayHandler(object): self._is_local = is_local self._stage = STAGE_INIT self._cryptor = cryptor.Cryptor(config['password'], - config['method']) + 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'' @@ -553,8 +556,12 @@ class TCPRelayHandler(object): return is_local = self._is_local data = None + if is_local: + buf_size = UP_STREAM_BUF_SIZE + else: + buf_size = DOWN_STREAM_BUF_SIZE try: - data = self._local_sock.recv(BUF_SIZE) + data = self._local_sock.recv(buf_size) except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): @@ -586,8 +593,12 @@ class TCPRelayHandler(object): 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 (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 38af1b6..f726717 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -115,6 +115,8 @@ class UDPRelay(object): self._closed = False self._sockets = set() self._forbidden_iplist = config.get('forbidden_ip') + self._crypto_path = config['crypto_path'] + addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) if len(addrs) == 0: @@ -174,7 +176,7 @@ class UDPRelay(object): try: data, key, iv = cryptor.decrypt_all(self._password, self._method, - data) + data, self._crypto_path) except Exception: logging.debug('UDP handle_server: decrypt data failed') return @@ -241,7 +243,8 @@ class UDPRelay(object): 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 @@ -275,7 +278,8 @@ class UDPRelay(object): 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 @@ -284,7 +288,8 @@ class UDPRelay(object): 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 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 index bfe3e1f..a27987c 100644 --- a/tests/chacha20-ietf.json +++ b/tests/chacha20-ietf.json @@ -1,10 +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 -} +{ + "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..3a071c8 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" 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 790e471..215ef72 100755 --- a/tests/libsodium/install.sh +++ b/tests/libsodium/install.sh @@ -1,10 +1,11 @@ #!/bin/bash -if [ ! -d libsodium-1.0.11 ]; then - wget https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz || exit 1 - tar xf libsodium-1.0.11.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.11 +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/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/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 +} From 06b028b5c08c80931482327eccfb6abf1b2ff15c Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Sat, 29 Apr 2017 14:29:53 +0800 Subject: [PATCH 66/68] Fix memory leak when excpetions occur (#814) --- shadowsocks/crypto/aead.py | 4 ++++ shadowsocks/crypto/openssl.py | 5 +++++ shadowsocks/crypto/sodium.py | 3 +++ 3 files changed, 12 insertions(+) diff --git a/shadowsocks/crypto/aead.py b/shadowsocks/crypto/aead.py index d800f36..7c52843 100644 --- a/shadowsocks/crypto/aead.py +++ b/shadowsocks/crypto/aead.py @@ -206,10 +206,12 @@ class AeadCryptoBase(object): # network byte order ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))] if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen: + self.clean() raise Exception("size length invalid") ctext.append(self.aead_encrypt(data)) if len(ctext[1]) != plen + self._tlen: + self.clean() raise Exception("data length invalid") return b''.join(ctext) @@ -265,6 +267,7 @@ class AeadCryptoBase(object): plen = self.aead_decrypt(data[:hlen]) plen, = unpack("!H", plen) if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0: + self.clean() raise Exception('Invalid message length') return plen, data[hlen:] @@ -288,6 +291,7 @@ class AeadCryptoBase(object): plaintext = self.aead_decrypt(data[:plen + self._tlen]) if len(plaintext) != plen: + self.clean() raise Exception("plaintext length invalid") return plaintext, data[plen + self._tlen:] diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index 7f7eef2..f2948ae 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -165,6 +165,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): None ) if not r: + self.clean() raise Exception('Set ivlen failed') self.cipher_ctx_init() @@ -201,6 +202,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): c_int(tag_len), c_char_p(tag) ) if not r: + self.clean() raise Exception('Set tag failed') def get_tag(self): @@ -216,6 +218,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): c_int(tag_len), byref(tag_buf) ) if not r: + self.clean() raise Exception('Get tag failed') return tag_buf.raw[:tag_len] @@ -231,6 +234,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): byref(buf), byref(cipher_out_len) ) if not r: + self.clean() # print(self._nonce.raw, r, cipher_out_len) raise Exception('Finalize cipher failed') return buf.raw[:cipher_out_len.value] @@ -255,6 +259,7 @@ class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase): """ clen = len(data) if clen < self._tlen: + self.clean() raise Exception('Data too short') self.set_tag(data[clen - self._tlen:]) diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index c5a6c5c..fce65c0 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -217,6 +217,9 @@ class SodiumCrypto(object): # strip off the padding return buf.raw[padding:padding + l] + def clean(self): + pass + class SodiumAeadCrypto(AeadCryptoBase): def __init__(self, cipher_name, key, iv, op, crypto_path=None): From 13b5d6e5ecebef7ee88a48e4f4314dc17dcb7b2d Mon Sep 17 00:00:00 2001 From: Zou Yong Date: Fri, 12 May 2017 03:06:38 -0500 Subject: [PATCH 67/68] Fix udp dns issue under python3 (#849) str is not bytes under python3, if dns config is a list, will cause asyncdns.py failed: asyncdns.py: .......... def handle_event(self, sock, fd, event): if sock != self._sock: return if event & eventloop.POLL_ERR: ........... else: data, addr = sock.recvfrom(1024) if addr[0] not in self._servers: logging.warn('received a packet other than our dns') return self._handle_data(data) --- shadowsocks/shell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index df04cee..d508049 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -199,6 +199,8 @@ def check_config(config, is_local): 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']) config['crypto_path'] = {'openssl': config['libopenssl'], From d5026cf5ef05ef5daadd2ea1fec9f50e7347e856 Mon Sep 17 00:00:00 2001 From: Ted George Date: Fri, 12 May 2017 09:07:36 +0100 Subject: [PATCH 68/68] Update README.md (#841) thanks --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 346e53e..4631576 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ shadowsocks [![PyPI version]][PyPI] [![Build Status]][Travis CI] -[![Coverage Status]][Coverage] A fast tunnel proxy that helps you bypass firewalls. @@ -78,8 +77,6 @@ Apache License [Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat -[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks -[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html [PyPI]: https://pypi.python.org/pypi/shadowsocks [PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat [Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks