Merge remote-tracking branch 'shadowsocks/master'
This commit is contained in:
commit
52179f472d
20 changed files with 331 additions and 128 deletions
54
README.md
54
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"
|
||||
|
|
11
tests/rc4-md5-ota.json
Normal file
11
tests/rc4-md5-ota.json
Normal 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
|
||||
}
|
11
tests/server-dnsserver.json
Normal file
11
tests/server-dnsserver.json
Normal 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"]
|
||||
}
|
8
tests/server-multi-passwd-empty.json
Normal file
8
tests/server-multi-passwd-empty.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"server": "127.0.0.1",
|
||||
"local_port": 1081,
|
||||
"port_password": {
|
||||
},
|
||||
"timeout": 60,
|
||||
"method": "aes-256-cfb"
|
||||
}
|
|
@ -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']
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
PYTHON="coverage run -p -a"
|
||||
PYTHON="coverage run -a"
|
||||
URL=http://127.0.0.1/file
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
PYTHON="coverage run -p -a"
|
||||
PYTHON="coverage run -a"
|
||||
|
||||
mkdir -p tmp
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue