Merge remote-tracking branch 'shadowsocks/master'

This commit is contained in:
Sheng Lin 2016-02-03 15:19:08 +08:00
commit 52179f472d
20 changed files with 331 additions and 128 deletions

View file

@ -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 shadowsocks
=========== ===========
@ -58,16 +52,6 @@ To check the log:
Check all the options via `-h`. You can also use a [Configuration] file Check all the options via `-h`. You can also use a [Configuration] file
instead. 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 Documentation
------------- -------------
@ -76,44 +60,18 @@ You can find all the documentation in the [Wiki].
License License
------- -------
Copyright 2015 clowwindy Apache License
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]
[Android]: https://github.com/shadowsocks/shadowsocks-android
[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat [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 Status]: https://jenkins.shadowvpn.org/result/shadowsocks
[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html [Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html
[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks
[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help
[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open
[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]: https://pypi.python.org/pypi/shadowsocks
[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat [PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks [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

View file

@ -248,7 +248,7 @@ STATUS_IPV6 = 1
class DNSResolver(object): class DNSResolver(object):
def __init__(self): def __init__(self, server_list=None):
self._loop = None self._loop = None
self._hosts = {} self._hosts = {}
self._hostname_status = {} self._hostname_status = {}
@ -256,8 +256,11 @@ class DNSResolver(object):
self._cb_to_hostname = {} self._cb_to_hostname = {}
self._cache = lru_cache.LRUCache(timeout=300) self._cache = lru_cache.LRUCache(timeout=300)
self._sock = None self._sock = None
if server_list is None:
self._servers = None self._servers = None
self._parse_resolv() self._parse_resolv()
else:
self._servers = server_list
self._parse_hosts() self._parse_hosts()
# TODO monitor hosts change and reload hosts # TODO monitor hosts change and reload hosts
# TODO parse /etc/gai.conf and follow its rules # TODO parse /etc/gai.conf and follow its rules

View file

@ -21,6 +21,25 @@ from __future__ import absolute_import, division, print_function, \
import socket import socket
import struct import struct
import logging 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): def compat_ord(s):
@ -118,9 +137,11 @@ def patch_socket():
patch_socket() patch_socket()
ADDRTYPE_IPV4 = 1 ADDRTYPE_IPV4 = 0x01
ADDRTYPE_IPV6 = 4 ADDRTYPE_IPV6 = 0x04
ADDRTYPE_HOST = 3 ADDRTYPE_HOST = 0x03
ADDRTYPE_AUTH = 0x10
ADDRTYPE_MASK = 0xF
def pack_addr(address): def pack_addr(address):
@ -144,14 +165,14 @@ def parse_header(data):
dest_addr = None dest_addr = None
dest_port = None dest_port = None
header_length = 0 header_length = 0
if addrtype == ADDRTYPE_IPV4: if addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV4:
if len(data) >= 7: if len(data) >= 7:
dest_addr = socket.inet_ntoa(data[1:5]) dest_addr = socket.inet_ntoa(data[1:5])
dest_port = struct.unpack('>H', data[5:7])[0] dest_port = struct.unpack('>H', data[5:7])[0]
header_length = 7 header_length = 7
else: else:
logging.warn('header is too short') logging.warn('header is too short')
elif addrtype == ADDRTYPE_HOST: elif addrtype & ADDRTYPE_MASK == ADDRTYPE_HOST:
if len(data) > 2: if len(data) > 2:
addrlen = ord(data[1]) addrlen = ord(data[1])
if len(data) >= 4 + addrlen: if len(data) >= 4 + addrlen:
@ -163,7 +184,7 @@ def parse_header(data):
logging.warn('header is too short') logging.warn('header is too short')
else: else:
logging.warn('header is too short') logging.warn('header is too short')
elif addrtype == ADDRTYPE_IPV6: elif addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV6:
if len(data) >= 19: if len(data) >= 19:
dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17])
dest_port = struct.unpack('>H', data[17:19])[0] dest_port = struct.unpack('>H', data[17:19])[0]

View file

@ -117,7 +117,7 @@ def daemon_start(pid_file, log_file):
sys.exit(1) sys.exit(1)
os.setsid() os.setsid()
signal.signal(signal.SIG_IGN, signal.SIGHUP) signal.signal(signal.SIGHUP, signal.SIG_IGN)
print('started') print('started')
os.kill(ppid, signal.SIGTERM) os.kill(ppid, signal.SIGTERM)

View file

@ -69,17 +69,18 @@ def EVP_BytesToKey(password, key_len, iv_len):
class Encryptor(object): class Encryptor(object):
def __init__(self, key, method): def __init__(self, password, method):
self.key = key self.password = password
self.key = None
self.method = method self.method = method
self.iv = None
self.iv_sent = False self.iv_sent = False
self.cipher_iv = b'' self.cipher_iv = b''
self.decipher = None self.decipher = None
self.decipher_iv = None
method = method.lower() method = method.lower()
self._method_info = self.get_method_info(method) self._method_info = self.get_method_info(method)
if self._method_info: 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])) random_string(self._method_info[1]))
else: else:
logging.error('method %s not supported' % method) logging.error('method %s not supported' % method)
@ -101,7 +102,7 @@ class Encryptor(object):
else: else:
# key_length == 0 indicates we should use the key directly # key_length == 0 indicates we should use the key directly
key, iv = password, b'' key, iv = password, b''
self.key = key
iv = iv[:m[1]] iv = iv[:m[1]]
if op == 1: if op == 1:
# this iv is for cipher not decipher # this iv is for cipher not decipher
@ -123,7 +124,8 @@ class Encryptor(object):
if self.decipher is None: if self.decipher is None:
decipher_iv_len = self._method_info[1] decipher_iv_len = self._method_info[1]
decipher_iv = buf[:decipher_iv_len] 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) iv=decipher_iv)
buf = buf[decipher_iv_len:] buf = buf[decipher_iv_len:]
if len(buf) == 0: if len(buf) == 0:
@ -131,10 +133,47 @@ class Encryptor(object):
return self.decipher.update(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): def encrypt_all(password, method, op, data):
result = [] result = []
method = method.lower() method = method.lower()
(key_len, iv_len, m) = method_supported[method] (key_len, iv_len, m) = method_supported[method]
key = None
if key_len > 0: if key_len > 0:
key, _ = EVP_BytesToKey(password, key_len, iv_len) key, _ = EVP_BytesToKey(password, key_len, iv_len)
else: else:
@ -182,6 +221,18 @@ def test_encrypt_all():
assert plain == plain2 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__': if __name__ == '__main__':
test_encrypt_all() test_encrypt_all()
test_encryptor() test_encryptor()
test_encrypt_all_m()

View file

@ -56,7 +56,12 @@ def main():
tcp_servers = [] tcp_servers = []
udp_servers = [] udp_servers = []
if 'dns_server' in config: # allow override settings in resolv.conf
dns_resolver = asyncdns.DNSResolver(config['dns_server'])
else:
dns_resolver = asyncdns.DNSResolver() dns_resolver = asyncdns.DNSResolver()
port_password = config['port_password'] port_password = config['port_password']
del config['port_password'] del config['port_password']
for port, password in port_password.items(): for port, password in port_password.items():

View file

@ -84,7 +84,8 @@ def check_config(config, is_local):
sys.exit(2) sys.exit(2)
if not is_local and not config.get('password', None) \ 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') logging.error('password or port_password not specified')
print_help(is_local) print_help(is_local)
sys.exit(2) sys.exit(2)
@ -130,11 +131,11 @@ def get_config(is_local):
logging.basicConfig(level=logging.INFO, logging.basicConfig(level=logging.INFO,
format='%(levelname)-s: %(message)s') format='%(levelname)-s: %(message)s')
if is_local: 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=', longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=',
'version'] 'version']
else: 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=', longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
'forbidden-ip=', 'user=', 'manager-address=', 'version'] 'forbidden-ip=', 'user=', 'manager-address=', 'version']
try: try:
@ -174,6 +175,8 @@ def get_config(is_local):
v_count += 1 v_count += 1
# '-vv' turns on more verbose mode # '-vv' turns on more verbose mode
config['verbose'] = v_count config['verbose'] = v_count
elif key == '-a':
config['one_time_auth'] = True
elif key == '-t': elif key == '-t':
config['timeout'] = int(value) config['timeout'] = int(value)
elif key == '--fast-open': elif key == '--fast-open':
@ -225,6 +228,7 @@ def get_config(is_local):
config['verbose'] = config.get('verbose', False) config['verbose'] = config.get('verbose', False)
config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_address'] = to_str(config.get('local_address', '127.0.0.1'))
config['local_port'] = config.get('local_port', 1080) config['local_port'] = config.get('local_port', 1080)
config['one_time_auth'] = config.get('one_time_auth', False)
if is_local: if is_local:
if config.get('server', None) is None: if config.get('server', None) is None:
logging.error('server addr not specified') logging.error('server addr not specified')
@ -315,6 +319,7 @@ Proxy options:
-k PASSWORD password -k PASSWORD password
-m METHOD encryption method, default: aes-256-cfb -m METHOD encryption method, default: aes-256-cfb
-t TIMEOUT timeout in seconds, default: 300 -t TIMEOUT timeout in seconds, default: 300
-a ONE_TIME_AUTH one time auth
--fast-open use TCP_FASTOPEN, requires Linux 3.7+ --fast-open use TCP_FASTOPEN, requires Linux 3.7+
--workers WORKERS number of workers, available on Unix/Linux --workers WORKERS number of workers, available on Unix/Linux
--forbidden-ip IPLIST comma seperated IP list forbidden to connect --forbidden-ip IPLIST comma seperated IP list forbidden to connect

View file

@ -27,7 +27,9 @@ import traceback
import random import random
from shadowsocks import encrypt, eventloop, shell, common 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 # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time
TIMEOUTS_CLEAN_SIZE = 512 TIMEOUTS_CLEAN_SIZE = 512
@ -107,6 +109,14 @@ class TCPRelayHandler(object):
self._stage = STAGE_INIT self._stage = STAGE_INIT
self._encryptor = encrypt.Encryptor(config['password'], self._encryptor = encrypt.Encryptor(config['password'],
config['method']) 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._fastopen_connected = False
self._data_to_write_to_local = [] self._data_to_write_to_local = []
self._data_to_write_to_remote = [] self._data_to_write_to_remote = []
@ -224,8 +234,16 @@ class TCPRelayHandler(object):
def _handle_stage_connecting(self, data): def _handle_stage_connecting(self, data):
if self._is_local: if self._is_local:
if self._ota_enable:
data = self._ota_chunk_data_gen(data)
data = self._encryptor.encrypt(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 \ if self._is_local and not self._fastopen_connected and \
self._config['fast_open']: self._config['fast_open']:
# for sslocal and fastopen, we basically wait for data and use # 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) self._loop.add(remote_sock, eventloop.POLL_ERR, self._server)
data = b''.join(self._data_to_write_to_remote) data = b''.join(self._data_to_write_to_remote)
l = len(data) 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: if s < l:
data = data[s:] data = data[s:]
self._data_to_write_to_remote = [data] self._data_to_write_to_remote = [data]
@ -293,6 +312,20 @@ class TCPRelayHandler(object):
logging.info('connecting %s:%d from %s:%d' % logging.info('connecting %s:%d from %s:%d' %
(common.to_str(remote_addr), remote_port, (common.to_str(remote_addr), remote_port,
self._client_address[0], self._client_address[1])) 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) self._remote_address = (common.to_str(remote_addr), remote_port)
# pause reading # pause reading
self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._update_stream(STREAM_UP, WAIT_STATUS_WRITING)
@ -302,13 +335,23 @@ class TCPRelayHandler(object):
self._write_to_sock((b'\x05\x00\x00\x01' self._write_to_sock((b'\x05\x00\x00\x01'
b'\x00\x00\x00\x00\x10\x10'), b'\x00\x00\x00\x00\x10\x10'),
self._local_sock) 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) data_to_send = self._encryptor.encrypt(data)
self._data_to_write_to_remote.append(data_to_send) self._data_to_write_to_remote.append(data_to_send)
# notice here may go into _handle_dns_resolved directly # notice here may go into _handle_dns_resolved directly
self._dns_resolver.resolve(self._chosen_server[0], self._dns_resolver.resolve(self._chosen_server[0],
self._handle_dns_resolved) self._handle_dns_resolved)
else: 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:]) self._data_to_write_to_remote.append(data[header_length:])
# notice here may go into _handle_dns_resolved directly # notice here may go into _handle_dns_resolved directly
self._dns_resolver.resolve(remote_addr, self._dns_resolver.resolve(remote_addr,
@ -341,10 +384,8 @@ class TCPRelayHandler(object):
self._log_error(error) self._log_error(error)
self.destroy() self.destroy()
return return
if result: if result and result[1]:
ip = result[1] ip = result[1]
if ip:
try: try:
self._stage = STAGE_CONNECTING self._stage = STAGE_CONNECTING
remote_addr = ip remote_addr = ip
@ -355,7 +396,7 @@ class TCPRelayHandler(object):
if self._is_local and self._config['fast_open']: if self._is_local and self._config['fast_open']:
# for fastopen: # for fastopen:
# wait for more data to arrive and send them in one SYN # wait for more data arrive and send them in one SYN
self._stage = STAGE_CONNECTING self._stage = STAGE_CONNECTING
# we don't have to wait for remote since it's not # we don't have to wait for remote since it's not
# created # created
@ -384,6 +425,62 @@ class TCPRelayHandler(object):
traceback.print_exc() traceback.print_exc()
self.destroy() 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): def _on_local_read(self):
# handle all local read events and dispatch them to methods for # handle all local read events and dispatch them to methods for
# each stage # each stage
@ -406,9 +503,7 @@ class TCPRelayHandler(object):
if not data: if not data:
return return
if self._stage == STAGE_STREAM: if self._stage == STAGE_STREAM:
if self._is_local: self._handle_stage_stream(data)
data = self._encryptor.encrypt(data)
self._write_to_sock(data, self._remote_sock)
return return
elif is_local and self._stage == STAGE_INIT: elif is_local and self._stage == STAGE_INIT:
# TODO check auth method # TODO check auth method

View file

@ -69,7 +69,8 @@ import errno
import random import random
from shadowsocks import encrypt, eventloop, lru_cache, common, shell 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 BUF_SIZE = 65536
@ -97,6 +98,10 @@ class UDPRelay(object):
self._password = common.to_bytes(config['password']) self._password = common.to_bytes(config['password'])
self._method = config['method'] self._method = config['method']
self._timeout = config['timeout'] 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._is_local = is_local
self._cache = lru_cache.LRUCache(timeout=config['timeout'], self._cache = lru_cache.LRUCache(timeout=config['timeout'],
close_callback=self._close_client) close_callback=self._close_client)
@ -114,7 +119,7 @@ class UDPRelay(object):
addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0,
socket.SOCK_DGRAM, socket.SOL_UDP) socket.SOCK_DGRAM, socket.SOL_UDP)
if len(addrs) == 0: 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)) (self._listen_addr, self._listen_port))
af, socktype, proto, canonname, sa = addrs[0] af, socktype, proto, canonname, sa = addrs[0]
server_socket = socket.socket(af, socktype, proto) server_socket = socket.socket(af, socktype, proto)
@ -145,6 +150,8 @@ class UDPRelay(object):
def _handle_server(self): def _handle_server(self):
server = self._server_socket server = self._server_socket
data, r_addr = server.recvfrom(BUF_SIZE) data, r_addr = server.recvfrom(BUF_SIZE)
key = None
iv = None
if not data: if not data:
logging.debug('UDP handle_server: data is empty') logging.debug('UDP handle_server: data is empty')
if self._stat_callback: if self._stat_callback:
@ -152,15 +159,19 @@ class UDPRelay(object):
if self._is_local: if self._is_local:
frag = common.ord(data[2]) frag = common.ord(data[2])
if frag != 0: 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 return
else: else:
data = data[3:] data = data[3:]
else: else:
data = encrypt.encrypt_all(self._password, self._method, 0, data) data, key, iv = encrypt.dencrypt_all(self._password,
self._method,
data)
# decrypt data # decrypt data
if not 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 return
header_result = parse_header(data) header_result = parse_header(data)
if header_result is None: if header_result is None:
@ -171,7 +182,17 @@ class UDPRelay(object):
server_addr, server_port = self._get_a_server() server_addr, server_port = self._get_a_server()
else: else:
server_addr, server_port = dest_addr, dest_port 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) addrs = self._dns_cache.get(server_addr, None)
if addrs is None: if addrs is None:
addrs = socket.getaddrinfo(server_addr, server_port, 0, addrs = socket.getaddrinfo(server_addr, server_port, 0,
@ -202,7 +223,11 @@ class UDPRelay(object):
self._eventloop.add(client, eventloop.POLL_IN, self) self._eventloop.add(client, eventloop.POLL_IN, self)
if self._is_local: 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: if not data:
return return
else: else:
@ -243,7 +268,7 @@ class UDPRelay(object):
header_result = parse_header(data) header_result = parse_header(data)
if header_result is None: if header_result is None:
return 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 response = b'\x00\x00\x00' + data
client_addr = self._client_fd_to_server_addr.get(sock.fileno()) client_addr = self._client_fd_to_server_addr.get(sock.fileno())
if client_addr: if client_addr:
@ -253,6 +278,11 @@ class UDPRelay(object):
# simply drop that packet # simply drop that packet
pass 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): def add_to_loop(self, loop):
if self._eventloop: if self._eventloop:
raise Exception('already add to loop') raise Exception('already add to loop')

View file

@ -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/table.json
run_test python tests/test.py --with-coverage -c tests/server-multi-ports.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/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 -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 -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 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 -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" 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"

11
tests/rc4-md5-ota.json Normal file
View file

@ -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
}

View file

@ -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"]
}

View file

@ -0,0 +1,8 @@
{
"server": "127.0.0.1",
"local_port": 1081,
"port_password": {
},
"timeout": 60,
"method": "aes-256-cfb"
}

View file

@ -44,7 +44,7 @@ parser.add_argument('--dns', type=str, default='8.8.8.8')
config = parser.parse_args() config = parser.parse_args()
if config.with_coverage: if config.with_coverage:
python = ['coverage', 'run', '-p', '-a'] python = ['coverage', 'run', '-a']
client_args = python + ['shadowsocks/local.py', '-v'] client_args = python + ['shadowsocks/local.py', '-v']
server_args = python + ['shadowsocks/server.py', '-v'] server_args = python + ['shadowsocks/server.py', '-v']

View file

@ -2,7 +2,7 @@
. tests/assert.sh . tests/assert.sh
PYTHON="coverage run -a -p" PYTHON="coverage run -a"
LOCAL="$PYTHON shadowsocks/local.py" LOCAL="$PYTHON shadowsocks/local.py"
SERVER="$PYTHON shadowsocks/server.py" SERVER="$PYTHON shadowsocks/server.py"

View file

@ -18,7 +18,7 @@ function run_test {
for module in local server for module in local server
do do
command="coverage run -p -a shadowsocks/$module.py" command="coverage run -a shadowsocks/$module.py"
mkdir -p tmp mkdir -p tmp

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
PYTHON="coverage run -p -a" PYTHON="coverage run -a"
URL=http://127.0.0.1/file URL=http://127.0.0.1/file

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
PYTHON="coverage run -p -a" PYTHON="coverage run -a"
URL=http://127.0.0.1/file URL=http://127.0.0.1/file
mkdir -p tmp mkdir -p tmp

View file

@ -36,6 +36,7 @@ if __name__ == '__main__':
# make sure they're from the same source port # make sure they're from the same source port
assert result1 == result2 assert result1 == result2
"""
# Test 2: same source port IPv6 # Test 2: same source port IPv6
# try again from the same port but IPv6 # try again from the same port but IPv6
sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM,
@ -81,3 +82,4 @@ if __name__ == '__main__':
sock_out.close() sock_out.close()
sock_in1.close() sock_in1.close()
"""

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
PYTHON="coverage run -p -a" PYTHON="coverage run -a"
mkdir -p tmp mkdir -p tmp