diff --git a/README.md b/README.md index 007df02..fb1ffca 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 =========== @@ -58,16 +52,6 @@ To check the log: Check all the options via `-h`. You can also use a [Configuration] file instead. -Client ------- - -* [Windows] / [OS X] -* [Android] / [iOS] -* [OpenWRT] - -Use GUI clients on your local PC/phones. Check the README of your client -for more information. - Documentation ------------- @@ -76,44 +60,18 @@ You can find all the documentation in the [Wiki]. License ------- -Copyright 2015 clowwindy - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. - -Bugs and Issues ----------------- - -* [Troubleshooting] -* [Issue Tracker] -* [Mailing list] +Apache License + + + + -[Android]: https://github.com/shadowsocks/shadowsocks-android [Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat -[Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File [Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks [Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html -[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks -[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help -[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open -[Install Server on Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows -[Mailing list]: https://groups.google.com/group/shadowsocks -[OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks -[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help [PyPI]: https://pypi.python.org/pypi/shadowsocks [PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat [Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks -[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting -[Wiki]: https://github.com/shadowsocks/shadowsocks/wiki -[Windows]: https://github.com/shadowsocks/shadowsocks-csharp + diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index c5fc99d..25a0e6b 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -248,7 +248,7 @@ STATUS_IPV6 = 1 class DNSResolver(object): - def __init__(self): + def __init__(self, server_list=None): self._loop = None self._hosts = {} self._hostname_status = {} @@ -256,8 +256,11 @@ class DNSResolver(object): self._cb_to_hostname = {} self._cache = lru_cache.LRUCache(timeout=300) self._sock = None - self._servers = None - self._parse_resolv() + if server_list is None: + self._servers = None + self._parse_resolv() + else: + self._servers = server_list self._parse_hosts() # TODO monitor hosts change and reload hosts # TODO parse /etc/gai.conf and follow its rules diff --git a/shadowsocks/common.py b/shadowsocks/common.py index 857bde6..ee14995 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -21,6 +21,25 @@ from __future__ import absolute_import, division, print_function, \ import socket import struct import logging +import hashlib +import hmac + + +ONETIMEAUTH_BYTES = 10 +ONETIMEAUTH_CHUNK_BYTES = 12 +ONETIMEAUTH_CHUNK_DATA_LEN = 2 + + +def sha1_hmac(secret, data): + return hmac.new(secret, data, hashlib.sha1).digest() + + +def onetimeauth_verify(_hash, data, key): + return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] + + +def onetimeauth_gen(data, key): + return sha1_hmac(key, data)[:ONETIMEAUTH_BYTES] def compat_ord(s): @@ -118,9 +137,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 +165,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 +184,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/daemon.py b/shadowsocks/daemon.py index 8dc5608..77ed323 100644 --- a/shadowsocks/daemon.py +++ b/shadowsocks/daemon.py @@ -117,7 +117,7 @@ def daemon_start(pid_file, log_file): sys.exit(1) os.setsid() - signal.signal(signal.SIG_IGN, signal.SIGHUP) + signal.signal(signal.SIGHUP, signal.SIG_IGN) print('started') os.kill(ppid, signal.SIGTERM) diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 4e87f41..ece72ec 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: @@ -131,10 +133,47 @@ 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() (key_len, iv_len, m) = method_supported[method] + key = None if key_len > 0: key, _ = EVP_BytesToKey(password, key_len, iv_len) else: @@ -182,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/server.py b/shadowsocks/server.py index e25db4c..1be4c0f 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -56,7 +56,12 @@ def main(): tcp_servers = [] udp_servers = [] - dns_resolver = asyncdns.DNSResolver() + + if 'dns_server' in config: # allow override settings in resolv.conf + dns_resolver = asyncdns.DNSResolver(config['dns_server']) + else: + dns_resolver = asyncdns.DNSResolver() + port_password = config['port_password'] del config['port_password'] for port, password in port_password.items(): diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index c91fc22..380bf25 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -84,7 +84,8 @@ def check_config(config, is_local): sys.exit(2) if not is_local and not config.get('password', None) \ - and not config.get('port_password', None): + and not config.get('port_password', None) \ + and not config.get('manager_address'): logging.error('password or port_password not specified') print_help(is_local) sys.exit(2) @@ -130,11 +131,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: @@ -174,6 +175,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': @@ -225,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') @@ -315,6 +319,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..6430f26 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -27,7 +27,9 @@ 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_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 @@ -107,6 +109,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._ota_enable = True + else: + self._ota_enable = False + self._ota_buff_head = b'' + self._ota_buff_data = b'' + self._ota_len = 0 + self._ota_chunk_idx = 0 self._fastopen_connected = False self._data_to_write_to_local = [] self._data_to_write_to_remote = [] @@ -224,8 +234,16 @@ class TCPRelayHandler(object): def _handle_stage_connecting(self, data): if self._is_local: + if self._ota_enable: + data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data) + self._data_to_write_to_remote.append(data) + else: + 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 @@ -239,7 +257,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] @@ -293,6 +312,20 @@ 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])) + if self._is_local is False: + # spec https://shadowsocks.org/en/spec/one-time-auth.html + 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 + 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 self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) @@ -302,13 +335,23 @@ 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._ota_enable: + 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 len(data) > header_length: + if self._ota_enable: + 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, @@ -341,49 +384,103 @@ 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: + 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 - 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() + # 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 _ota_chunk_data(self, data, data_cb): + # spec https://shadowsocks.org/en/spec/one-time-auth.html + while len(data) > 0: + if self._ota_len == 0: + # get DATA.LEN + HMAC-SHA1 + length = ONETIMEAUTH_CHUNK_BYTES - len(self._ota_buff_head) + self._ota_buff_head += data[:length] + data = data[length:] + if len(self._ota_buff_head) < ONETIMEAUTH_CHUNK_BYTES: + # wait more data + return + 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._ota_buff_data) == self._ota_len: + # get a chunk data + _hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:] + _data = self._ota_buff_data + index = struct.pack('>I', self._ota_chunk_idx) + key = self._encryptor.decipher_iv + index + if onetimeauth_verify(_hash, _data, key) is False: + logging.warn('one time auth fail, drop chunk !') + else: + data_cb(self._ota_buff_data) + self._ota_chunk_idx += 1 + self._ota_buff_head = b'' + self._ota_buff_data = b'' + self._ota_len = 0 + return + + def _ota_chunk_data_gen(self, data): + data_len = struct.pack(">H", len(data)) + index = struct.pack('>I', self._ota_chunk_idx) + key = self._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._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._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): # handle all local read events and dispatch them to methods for # each stage @@ -406,9 +503,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..6bf6ce6 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_verify, \ + onetimeauth_gen, ONETIMEAUTH_BYTES, 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) @@ -114,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) @@ -145,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: @@ -152,15 +159,19 @@ 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:] 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('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: @@ -171,7 +182,17 @@ 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 + _hash = data[-ONETIMEAUTH_BYTES:] + data = data[: -ONETIMEAUTH_BYTES] + _key = iv + key + if onetimeauth_verify(_hash, data, _key) is False: + logging.warn('UDP one time auth fail') + return addrs = self._dns_cache.get(server_addr, None) if addrs is None: addrs = socket.getaddrinfo(server_addr, server_port, 0, @@ -202,7 +223,11 @@ class UDPRelay(object): self._eventloop.add(client, eventloop.POLL_IN, self) if self._is_local: - data = encrypt.encrypt_all(self._password, self._method, 1, data) + key, iv, m = 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, self._method, data) if not data: return else: @@ -243,7 +268,7 @@ 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 response = b'\x00\x00\x00' + data client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: @@ -253,6 +278,11 @@ class UDPRelay(object): # simply drop that packet pass + def _ota_chunk_data_gen(self, key, iv, data): + data = common.chr(common.ord(data[0]) | ADDRTYPE_AUTH) + data[1:] + key = iv + key + return data + onetimeauth_gen(data, key) + def add_to_loop(self, loop): if self._eventloop: raise Exception('already add to loop') diff --git a/tests/jenkins.sh b/tests/jenkins.sh index a85c461..6d0fac8 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -42,9 +42,12 @@ run_test python tests/test.py --with-coverage -c tests/chacha20.json run_test python tests/test.py --with-coverage -c tests/table.json run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json +run_test python tests/test.py --with-coverage -s tests/server-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..3566d6d --- /dev/null +++ b/tests/rc4-md5-ota.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"rc4-md5", + "local_address":"127.0.0.1", + "fast_open":false, + "one_time_auth":true +} diff --git a/tests/server-dnsserver.json b/tests/server-dnsserver.json new file mode 100644 index 0000000..5d55cdc --- /dev/null +++ b/tests/server-dnsserver.json @@ -0,0 +1,11 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1081, + "password":"aes_password", + "timeout":60, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false, + "dns_server": ["8.8.8.8","8.8.4.4"] +} diff --git a/tests/server-multi-passwd-empty.json b/tests/server-multi-passwd-empty.json new file mode 100644 index 0000000..de354e5 --- /dev/null +++ b/tests/server-multi-passwd-empty.json @@ -0,0 +1,8 @@ +{ + "server": "127.0.0.1", + "local_port": 1081, + "port_password": { + }, + "timeout": 60, + "method": "aes-256-cfb" +} diff --git a/tests/test.py b/tests/test.py index 29b57d4..85bd007 100755 --- a/tests/test.py +++ b/tests/test.py @@ -44,7 +44,7 @@ parser.add_argument('--dns', type=str, default='8.8.8.8') config = parser.parse_args() if config.with_coverage: - python = ['coverage', 'run', '-p', '-a'] + python = ['coverage', 'run', '-a'] client_args = python + ['shadowsocks/local.py', '-v'] server_args = python + ['shadowsocks/server.py', '-v'] diff --git a/tests/test_command.sh b/tests/test_command.sh index be05704..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" diff --git a/tests/test_daemon.sh b/tests/test_daemon.sh index 40f35ef..1acddc0 100755 --- a/tests/test_daemon.sh +++ b/tests/test_daemon.sh @@ -18,7 +18,7 @@ function run_test { for module in local server do -command="coverage run -p -a shadowsocks/$module.py" +command="coverage run -a shadowsocks/$module.py" mkdir -p tmp diff --git a/tests/test_graceful_restart.sh b/tests/test_graceful_restart.sh 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.py b/tests/test_udp_src.py index e8fa505..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, @@ -81,3 +82,4 @@ if __name__ == '__main__': sock_out.close() sock_in1.close() + """ 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