diff --git a/.gitignore b/.gitignore index 6c1b61ea..529cc258 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ htmlcov .DS_Store .idea + +# Ignore user config +user-config.json +userapiconfig.py +usermysql.json diff --git a/.travis.yml b/.travis.yml index f29cb960..014fa071 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ before_install: - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" - sudo service nginx restart - - pip install pep8 pyflakes nose coverage + - pip install pep8 pyflakes nose coverage PySocks cymysql - sudo tests/socksify/install.sh - sudo tests/libsodium/install.sh - sudo tests/setup_tc.sh diff --git a/CHANGES b/CHANGES index 4db142a6..0cd91768 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,104 @@ +3.4.0 2017-07-27 +- add auth_chain_b +- add initmudbjson.sh +- allow set speed limit in runtime +- fix bugs & mem leak + +3.3.3 2017-06-03 +- add DNS cache +- add tls1.2_ticket_fastauth +- fix bugs + +3.3.2 2017-05-20 +- revert http reply +- refine tls1.2_ticket_auth error detector + +3.3.1 2017-05-18 +- fix stop script +- Async DNS query under UDP +- fix old version of OpenSSL +- http reply + +3.3.0 2017-05-11 +- connect_log include local addr & port +- fix auth_chain_a UDP bug +- add "additional_ports_only" +- add interface legendsockssr +- run with newest python version +- parse comment in hosts +- update mujson_mgr +- add cymysql setup script +- new speed tester +- fix leaks +- bugs fixed + +3.2.0 2017-04-27 +- add auth_chain_a +- remove auth_aes128, auth_sha1, auth_sha1_v2, verify_simple, auth_simple, verify_sha1 + +3.1.2 2017-04-07 +- display UID +- auto adjust TCP MSS + +3.1.1 2017-03-25 +- add "New session ticket" +- ignore bind 10.0.0.0/8 and 192.168.0.0/16 by default +- improve rand size under auth_aes128_* +- fix bugs + +3.1.0 2017-03-16 +- add "glzjinmod" interface +- rate limit +- add additional_ports in config + +3.0.4 2017-01-08 +- multi-user in single port + +3.0.1 2017-01-03 +- remove auth_aes128_*_compatible + +3.0.0 2016-12-23 +- http_simple fix bugs +- tls1.2_ticket_auth fix bug & defaule time diff set to 86400s + +2.9.7 2016-11-22 +- manage client with LRUCache +- catch bind error +- fix import error of resource on windows +- print RLIMIT_NOFILE +- always close cymysql objects +- add init script + +2.9.6 2016-10-17 +- tls1.2_ticket_auth random packet size + +2.9.5.1 2016-10-16 +- UDP bind address + +2.9.5 2016-10-13 +- add auth_aes128_md5 and auth_aes128_sha1 + +2.9.4 2016-10-11 +- sync client version + +2.6.13 2015-11-02 +- add protocol setting + +2.6.12 2015-10-27 +- IPv6 first +- Fix mem leaks +- auth_simple plugin +- remove FORCE_NEW_PROTOCOL +- optimize code + +2.6.11 2015-10-20 +- Obfs plugin +- Obfs parameters +- UDP over TCP +- TCP over UDP (experimental) +- Fix socket leaks +- Catch abnormal UDP package + 2.6.10 2015-06-08 - Optimize LRU cache - Refine logging diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c23b6eb0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM alpine:3.6 + +ENV SERVER_ADDR 0.0.0.0 +ENV SERVER_PORT 51348 +ENV PASSWORD psw +ENV METHOD aes-128-ctr +ENV PROTOCOL auth_aes128_md5 +ENV PROTOCOLPARAM 32 +ENV OBFS tls1.2_ticket_auth_compatible +ENV TIMEOUT 300 +ENV DNS_ADDR 8.8.8.8 +ENV DNS_ADDR_2 8.8.4.4 + +ARG BRANCH=manyuser +ARG WORK=~ + + +RUN apk --no-cache add python \ + libsodium \ + wget + + +RUN mkdir -p $WORK && \ + wget -qO- --no-check-certificate https://github.com/shadowsocksr-backup/shadowsocksr/archive/$BRANCH.tar.gz | tar -xzf - -C $WORK + + +WORKDIR $WORK/shadowsocksr-$BRANCH/shadowsocks + + +EXPOSE $SERVER_PORT +CMD python server.py -p $SERVER_PORT -k $PASSWORD -m $METHOD -O $PROTOCOL -o $OBFS -G $PROTOCOLPARAM diff --git a/README.md b/README.md index 76d759a2..53eafeaa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -shadowsocks +ShadowsocksR =========== -[![PyPI version]][PyPI] [![Build Status]][Travis CI] -[![Coverage Status]][Coverage] A fast tunnel proxy that helps you bypass firewalls. @@ -14,41 +12,52 @@ Server Debian / Ubuntu: - apt-get install python-pip - pip install shadowsocks + apt-get install git + git clone https://github.com/shadowsocksr/shadowsocksr.git CentOS: - yum install python-setuptools && easy_install pip - pip install shadowsocks + yum install git + git clone https://github.com/shadowsocksr/shadowsocksr.git Windows: -See [Install Server on Windows] + git clone https://github.com/shadowsocksr/shadowsocksr.git -### Usage +### Usage for single user on linux platform - ssserver -p 443 -k password -m aes-256-cfb +If you clone it into "~/shadowsocksr" +move to "~/shadowsocksr", then run: + + bash initcfg.sh + +move to "~/shadowsocksr/shadowsocks", then run: + + python server.py -p 443 -k password -m aes-128-cfb -O auth_aes128_md5 -o tls1.2_ticket_auth_compatible + +Check all the options via `-h`. + +You can also use a configuration file instead (recommend), move to "~/shadowsocksr" and edit the file "user-config.json", then move to "~/shadowsocksr/shadowsocks" again, just run: + + python server.py To run in the background: - sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start + ./logrun.sh To stop: - sudo ssserver -d stop + ./stop.sh -To check the log: +To monitor the log: - sudo less /var/log/shadowsocks.log + ./tail.sh -Check all the options via `-h`. You can also use a [Configuration] file -instead. Client ------ -* [Windows] / [OS X] +* [Windows] / [macOS] * [Android] / [iOS] * [OpenWRT] @@ -80,27 +89,17 @@ 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 -[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 +[Android]: https://github.com/shadowsocksr/shadowsocksr-android +[Build Status]: https://travis-ci.org/shadowsocksr/shadowsocksr.svg?branch=manyuser [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 +[Issue Tracker]: https://github.com/shadowsocksr/shadowsocksr/issues?state=open [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 +[macOS]: https://github.com/shadowsocksr/ShadowsocksX-NG +[Travis CI]: https://travis-ci.org/shadowsocksr/shadowsocksr +[Windows]: https://github.com/shadowsocksr/shadowsocksr-csharp +[Wiki]: https://github.com/breakwa11/shadowsocks-rss/wiki diff --git a/apiconfig.py b/apiconfig.py new file mode 100644 index 00000000..5ee8be12 --- /dev/null +++ b/apiconfig.py @@ -0,0 +1,15 @@ +# Config +API_INTERFACE = 'sspanelv2' #mudbjson, sspanelv2, sspanelv3, sspanelv3ssr, glzjinmod, legendsockssr, muapiv2(not support) +UPDATE_TIME = 60 +SERVER_PUB_ADDR = '127.0.0.1' # mujson_mgr need this to generate ssr link + +#mudb +MUDB_FILE = 'mudb.json' + +# Mysql +MYSQL_CONFIG = 'usermysql.json' + +# API +MUAPI_CONFIG = 'usermuapi.json' + + diff --git a/asyncmgr.py b/asyncmgr.py new file mode 100644 index 00000000..9bf4d093 --- /dev/null +++ b/asyncmgr.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import time +import os +import socket +import struct +import re +import logging +from shadowsocks import common +from shadowsocks import lru_cache +from shadowsocks import eventloop +import server_pool +import Config + +class ServerMgr(object): + + def __init__(self): + self._loop = None + self._request_id = 1 + self._hosts = {} + self._hostname_status = {} + self._hostname_to_cb = {} + self._cb_to_hostname = {} + self._last_time = time.time() + self._sock = None + self._servers = None + + def add_to_loop(self, loop): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.bind((Config.MANAGE_BIND_IP, Config.MANAGE_PORT)) + self._sock.setblocking(False) + loop.add(self._sock, eventloop.POLL_IN, self) + + def _handle_data(self, sock): + data, addr = sock.recvfrom(128) + #manage pwd:port:passwd:action + args = data.split(':') + if len(args) < 4: + return + if args[0] == Config.MANAGE_PASS: + if args[3] == '0': + server_pool.ServerPool.get_instance().cb_del_server(args[1]) + elif args[3] == '1': + server_pool.ServerPool.get_instance().new_server(args[1], args[2]) + + def handle_event(self, sock, fd, event): + if sock != self._sock: + return + if event & eventloop.POLL_ERR: + logging.error('mgr socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN, self) + else: + self._handle_data(sock) + + def close(self): + if self._sock: + if self._loop: + self._loop.remove(self._sock) + self._sock.close() + self._sock = None + + +def test(): + pass + +if __name__ == '__main__': + test() diff --git a/config.json b/config.json new file mode 100644 index 00000000..55f12b5f --- /dev/null +++ b/config.json @@ -0,0 +1,25 @@ +{ + "server": "0.0.0.0", + "server_ipv6": "::", + "server_port": 8388, + "local_address": "127.0.0.1", + "local_port": 1080, + + "password": "m", + "method": "aes-128-ctr", + "protocol": "auth_aes128_md5", + "protocol_param": "", + "obfs": "tls1.2_ticket_auth_compatible", + "obfs_param": "", + "speed_limit_per_con": 0, + "speed_limit_per_user": 0, + + "additional_ports" : {}, // only works under multi-user mode + "additional_ports_only" : false, // only works under multi-user mode + "timeout": 120, + "udp_timeout": 60, + "dns_ipv6": false, + "connect_verbose_info": 0, + "redirect": "", + "fast_open": false +} diff --git a/configloader.py b/configloader.py new file mode 100644 index 00000000..cf9d6196 --- /dev/null +++ b/configloader.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import importloader + +g_config = None + +def load_config(): + global g_config + g_config = importloader.loads(['userapiconfig', 'apiconfig']) + +def get_config(): + return g_config + +load_config() + diff --git a/db_transfer.py b/db_transfer.py new file mode 100644 index 00000000..67bda608 --- /dev/null +++ b/db_transfer.py @@ -0,0 +1,631 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import logging +import time +import sys +from server_pool import ServerPool +import traceback +from shadowsocks import common, shell, lru_cache, obfs +from configloader import load_config, get_config +import importloader + +switchrule = None +db_instance = None + +class TransferBase(object): + def __init__(self): + import threading + self.event = threading.Event() + self.key_list = ['port', 'u', 'd', 'transfer_enable', 'passwd', 'enable'] + self.last_get_transfer = {} #上一次的实际流量 + self.last_update_transfer = {} #上一次更新到的流量(小于等于实际流量) + self.force_update_transfer = set() #强制推入数据库的ID + self.port_uid_table = {} #端口到uid的映射(仅v3以上有用) + self.onlineuser_cache = lru_cache.LRUCache(timeout=60*30) #用户在线状态记录 + self.pull_ok = False #记录是否已经拉出过数据 + self.mu_ports = {} + + def load_cfg(self): + pass + + def push_db_all_user(self): + if self.pull_ok is False: + return + #更新用户流量到数据库 + last_transfer = self.last_update_transfer + curr_transfer = ServerPool.get_instance().get_servers_transfer() + #上次和本次的增量 + dt_transfer = {} + for id in self.force_update_transfer: #此表中的用户统计上次未计入的流量 + if id in self.last_get_transfer and id in last_transfer: + dt_transfer[id] = [self.last_get_transfer[id][0] - last_transfer[id][0], self.last_get_transfer[id][1] - last_transfer[id][1]] + + for id in curr_transfer.keys(): + if id in self.force_update_transfer or id in self.mu_ports: + continue + #算出与上次记录的流量差值,保存于dt_transfer表 + if id in last_transfer: + if curr_transfer[id][0] + curr_transfer[id][1] - last_transfer[id][0] - last_transfer[id][1] <= 0: + continue + dt_transfer[id] = [curr_transfer[id][0] - last_transfer[id][0], + curr_transfer[id][1] - last_transfer[id][1]] + else: + if curr_transfer[id][0] + curr_transfer[id][1] <= 0: + continue + dt_transfer[id] = [curr_transfer[id][0], curr_transfer[id][1]] + + #有流量的,先记录在线状态 + if id in self.last_get_transfer: + if curr_transfer[id][0] + curr_transfer[id][1] > self.last_get_transfer[id][0] + self.last_get_transfer[id][1]: + self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1] + else: + self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1] + + self.onlineuser_cache.sweep() + + update_transfer = self.update_all_user(dt_transfer) #返回有更新的表 + for id in update_transfer.keys(): #其增量加在此表 + if id not in self.force_update_transfer: #但排除在force_update_transfer内的 + last = self.last_update_transfer.get(id, [0,0]) + self.last_update_transfer[id] = [last[0] + update_transfer[id][0], last[1] + update_transfer[id][1]] + self.last_get_transfer = curr_transfer + for id in self.force_update_transfer: + if id in self.last_update_transfer: + del self.last_update_transfer[id] + if id in self.last_get_transfer: + del self.last_get_transfer[id] + self.force_update_transfer = set() + + def del_server_out_of_bound_safe(self, last_rows, rows): + #停止超流量的服务 + #启动没超流量的服务 + try: + switchrule = importloader.load('switchrule') + except Exception as e: + logging.error('load switchrule.py fail') + cur_servers = {} + new_servers = {} + allow_users = {} + mu_servers = {} + config = shell.get_config(False) + for row in rows: + try: + allow = switchrule.isTurnOn(row) and row['enable'] == 1 and row['u'] + row['d'] < row['transfer_enable'] + except Exception as e: + allow = False + + port = row['port'] + passwd = common.to_bytes(row['passwd']) + if hasattr(passwd, 'encode'): + passwd = passwd.encode('utf-8') + cfg = {'password': passwd} + if 'id' in row: + self.port_uid_table[row['port']] = row['id'] + + read_config_keys = ['method', 'obfs', 'obfs_param', 'protocol', 'protocol_param', 'forbidden_ip', 'forbidden_port', 'speed_limit_per_con', 'speed_limit_per_user'] + for name in read_config_keys: + if name in row and row[name]: + cfg[name] = row[name] + + merge_config_keys = ['password'] + read_config_keys + for name in cfg.keys(): + if hasattr(cfg[name], 'encode'): + try: + cfg[name] = cfg[name].encode('utf-8') + except Exception as e: + logging.warning('encode cfg key "%s" fail, val "%s"' % (name, cfg[name])) + + if port not in cur_servers: + cur_servers[port] = passwd + else: + logging.error('more than one user use the same port [%s]' % (port,)) + continue + + if 'protocol' in cfg and 'protocol_param' in cfg and common.to_str(cfg['protocol']) in obfs.mu_protocol(): + if '#' in common.to_str(cfg['protocol_param']): + mu_servers[port] = passwd + allow = True + + if allow: + if port not in mu_servers: + allow_users[port] = cfg + + cfgchange = False + if port in ServerPool.get_instance().tcp_servers_pool: + relay = ServerPool.get_instance().tcp_servers_pool[port] + for name in merge_config_keys: + if name in cfg and not self.cmp(cfg[name], relay._config[name]): + cfgchange = True + break + if not cfgchange and port in ServerPool.get_instance().tcp_ipv6_servers_pool: + relay = ServerPool.get_instance().tcp_ipv6_servers_pool[port] + for name in merge_config_keys: + if (name in cfg) and ((name not in relay._config) or not self.cmp(cfg[name], relay._config[name])): + cfgchange = True + break + + if port in mu_servers: + if ServerPool.get_instance().server_is_run(port) > 0: + if cfgchange: + logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + new_servers[port] = (passwd, cfg) + else: + self.new_server(port, passwd, cfg) + else: + if ServerPool.get_instance().server_is_run(port) > 0: + if config['additional_ports_only'] or not allow: + logging.info('db stop server at port [%s]' % (port,)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + else: + if cfgchange: + logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + new_servers[port] = (passwd, cfg) + + elif not config['additional_ports_only'] and allow and port > 0 and port < 65536 and ServerPool.get_instance().server_run_status(port) is False: + self.new_server(port, passwd, cfg) + + for row in last_rows: + if row['port'] in cur_servers: + pass + else: + logging.info('db stop server at port [%s] reason: port not exist' % (row['port'])) + ServerPool.get_instance().cb_del_server(row['port']) + self.clear_cache(row['port']) + if row['port'] in self.port_uid_table: + del self.port_uid_table[row['port']] + + if len(new_servers) > 0: + from shadowsocks import eventloop + self.event.wait(eventloop.TIMEOUT_PRECISION + eventloop.TIMEOUT_PRECISION / 2) + for port in new_servers.keys(): + passwd, cfg = new_servers[port] + self.new_server(port, passwd, cfg) + + logging.debug('db allow users %s \nmu_servers %s' % (allow_users, mu_servers)) + for port in mu_servers: + ServerPool.get_instance().update_mu_users(port, allow_users) + + self.mu_ports = mu_servers + + def clear_cache(self, port): + if port in self.force_update_transfer: del self.force_update_transfer[port] + if port in self.last_get_transfer: del self.last_get_transfer[port] + if port in self.last_update_transfer: del self.last_update_transfer[port] + + def new_server(self, port, passwd, cfg): + protocol = cfg.get('protocol', ServerPool.get_instance().config.get('protocol', 'origin')) + method = cfg.get('method', ServerPool.get_instance().config.get('method', 'None')) + obfs = cfg.get('obfs', ServerPool.get_instance().config.get('obfs', 'plain')) + logging.info('db start server at port [%s] pass [%s] protocol [%s] method [%s] obfs [%s]' % (port, passwd, protocol, method, obfs)) + ServerPool.get_instance().new_server(port, cfg) + + def cmp(self, val1, val2): + if type(val1) is bytes: + val1 = common.to_str(val1) + if type(val2) is bytes: + val2 = common.to_str(val2) + return val1 == val2 + + @staticmethod + def del_servers(): + for port in [v for v in ServerPool.get_instance().tcp_servers_pool.keys()]: + if ServerPool.get_instance().server_is_run(port) > 0: + ServerPool.get_instance().cb_del_server(port) + for port in [v for v in ServerPool.get_instance().tcp_ipv6_servers_pool.keys()]: + if ServerPool.get_instance().server_is_run(port) > 0: + ServerPool.get_instance().cb_del_server(port) + + @staticmethod + def thread_db(obj): + import socket + import time + global db_instance + timeout = 60 + socket.setdefaulttimeout(timeout) + last_rows = [] + db_instance = obj() + ServerPool.get_instance() + shell.log_shadowsocks_version() + + try: + import resource + logging.info('current process RLIMIT_NOFILE resource: soft %d hard %d' % resource.getrlimit(resource.RLIMIT_NOFILE)) + except: + pass + + try: + while True: + load_config() + db_instance.load_cfg() + try: + db_instance.push_db_all_user() + rows = db_instance.pull_db_all_user() + if rows: + db_instance.pull_ok = True + config = shell.get_config(False) + for port in config['additional_ports']: + val = config['additional_ports'][port] + val['port'] = int(port) + val['enable'] = 1 + val['transfer_enable'] = 1024 ** 7 + val['u'] = 0 + val['d'] = 0 + if "password" in val: + val["passwd"] = val["password"] + rows.append(val) + db_instance.del_server_out_of_bound_safe(last_rows, rows) + last_rows = rows + except Exception as e: + trace = traceback.format_exc() + logging.error(trace) + #logging.warn('db thread except:%s' % e) + if db_instance.event.wait(get_config().UPDATE_TIME) or not ServerPool.get_instance().thread.is_alive(): + break + except KeyboardInterrupt as e: + pass + db_instance.del_servers() + ServerPool.get_instance().stop() + db_instance = None + + @staticmethod + def thread_db_stop(): + global db_instance + db_instance.event.set() + +class DbTransfer(TransferBase): + def __init__(self): + super(DbTransfer, self).__init__() + self.user_pass = {} #记录更新此用户流量时被跳过多少次 + self.cfg = { + "host": "127.0.0.1", + "port": 3306, + "user": "ss", + "password": "pass", + "db": "shadowsocks", + "node_id": 0, + "transfer_mul": 1.0, + "ssl_enable": 0, + "ssl_ca": "", + "ssl_cert": "", + "ssl_key": ""} + self.load_cfg() + + def load_cfg(self): + import json + config_path = get_config().MYSQL_CONFIG + cfg = None + with open(config_path, 'rb+') as f: + cfg = json.loads(f.read().decode('utf8')) + + if cfg: + self.cfg.update(cfg) + + def update_all_user(self, dt_transfer): + import cymysql + update_transfer = {} + + query_head = 'UPDATE user' + query_sub_when = '' + query_sub_when2 = '' + query_sub_in = None + last_time = time.time() + + for id in dt_transfer.keys(): + transfer = dt_transfer[id] + #小于最低更新流量的先不更新 + update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64) + if transfer[0] + transfer[1] < update_trs and id not in self.force_update_transfer: + self.user_pass[id] = self.user_pass.get(id, 0) + 1 + continue + if id in self.user_pass: + del self.user_pass[id] + + query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"])) + query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"])) + update_transfer[id] = transfer + + if query_sub_in is not None: + query_sub_in += ',%s' % id + else: + query_sub_in = '%s' % id + + if query_sub_when == '': + return update_transfer + query_sql = query_head + ' SET u = CASE port' + query_sub_when + \ + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + + try: + cur = conn.cursor() + try: + cur.execute(query_sql) + except Exception as e: + logging.error(e) + update_transfer = {} + + cur.close() + conn.commit() + except Exception as e: + logging.error(e) + update_transfer = {} + finally: + conn.close() + + return update_transfer + + def pull_db_all_user(self): + import cymysql + #数据库所有用户信息 + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + + try: + rows = self.pull_db_users(conn) + finally: + conn.close() + + if not rows: + logging.warn('no user in db') + return rows + + def pull_db_users(self, conn): + try: + switchrule = importloader.load('switchrule') + keys = switchrule.getKeys(self.key_list) + except Exception as e: + keys = self.key_list + + cur = conn.cursor() + cur.execute("SELECT " + ','.join(keys) + " FROM user") + rows = [] + for r in cur.fetchall(): + d = {} + for column in range(len(keys)): + d[keys[column]] = r[column] + rows.append(d) + cur.close() + return rows + +class Dbv3Transfer(DbTransfer): + def __init__(self): + super(Dbv3Transfer, self).__init__() + self.update_node_state = True if get_config().API_INTERFACE != 'legendsockssr' else False + if self.update_node_state: + self.key_list += ['id'] + self.key_list += ['method'] + if self.update_node_state: + self.ss_node_info_name = 'ss_node_info_log' + if get_config().API_INTERFACE == 'sspanelv3ssr': + self.key_list += ['obfs', 'protocol'] + if get_config().API_INTERFACE == 'glzjinmod': + self.key_list += ['obfs', 'protocol'] + self.ss_node_info_name = 'ss_node_info' + else: + self.key_list += ['obfs', 'protocol'] + self.start_time = time.time() + + def update_all_user(self, dt_transfer): + import cymysql + update_transfer = {} + + query_head = 'UPDATE user' + query_sub_when = '' + query_sub_when2 = '' + query_sub_in = None + last_time = time.time() + + alive_user_count = len(self.onlineuser_cache) + bandwidth_thistime = 0 + + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + conn.autocommit(True) + + for id in dt_transfer.keys(): + transfer = dt_transfer[id] + bandwidth_thistime = bandwidth_thistime + transfer[0] + transfer[1] + + update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64) + if transfer[0] + transfer[1] < update_trs: + self.user_pass[id] = self.user_pass.get(id, 0) + 1 + continue + if id in self.user_pass: + del self.user_pass[id] + + query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"])) + query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"])) + update_transfer[id] = transfer + + if self.update_node_state: + cur = conn.cursor() + try: + if id in self.port_uid_table: + cur.execute("INSERT INTO `user_traffic_log` (`id`, `user_id`, `u`, `d`, `node_id`, `rate`, `traffic`, `log_time`) VALUES (NULL, '" + \ + str(self.port_uid_table[id]) + "', '" + str(transfer[0]) + "', '" + str(transfer[1]) + "', '" + \ + str(self.cfg["node_id"]) + "', '" + str(self.cfg["transfer_mul"]) + "', '" + \ + self.traffic_format((transfer[0] + transfer[1]) * self.cfg["transfer_mul"]) + "', unix_timestamp()); ") + except: + logging.warn('no `user_traffic_log` in db') + cur.close() + + if query_sub_in is not None: + query_sub_in += ',%s' % id + else: + query_sub_in = '%s' % id + + if query_sub_when != '': + query_sql = query_head + ' SET u = CASE port' + query_sub_when + \ + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in + cur = conn.cursor() + try: + cur.execute(query_sql) + except Exception as e: + logging.error(e) + cur.close() + + if self.update_node_state: + try: + cur = conn.cursor() + try: + cur.execute("INSERT INTO `ss_node_online_log` (`id`, `node_id`, `online_user`, `log_time`) VALUES (NULL, '" + \ + str(self.cfg["node_id"]) + "', '" + str(alive_user_count) + "', unix_timestamp()); ") + except Exception as e: + logging.error(e) + cur.close() + + cur = conn.cursor() + try: + cur.execute("INSERT INTO `" + self.ss_node_info_name + "` (`id`, `node_id`, `uptime`, `load`, `log_time`) VALUES (NULL, '" + \ + str(self.cfg["node_id"]) + "', '" + str(self.uptime()) + "', '" + \ + str(self.load()) + "', unix_timestamp()); ") + except Exception as e: + logging.error(e) + cur.close() + except: + logging.warn('no `ss_node_online_log` or `" + self.ss_node_info_name + "` in db') + + conn.close() + return update_transfer + + def pull_db_users(self, conn): + try: + switchrule = importloader.load('switchrule') + keys = switchrule.getKeys(self.key_list) + except Exception as e: + keys = self.key_list + + cur = conn.cursor() + + if self.update_node_state: + node_info_keys = ['traffic_rate'] + try: + cur.execute("SELECT " + ','.join(node_info_keys) +" FROM ss_node where `id`='" + str(self.cfg["node_id"]) + "'") + nodeinfo = cur.fetchone() + except Exception as e: + logging.error(e) + nodeinfo = None + + if nodeinfo == None: + rows = [] + cur.close() + conn.commit() + logging.warn('None result when select node info from ss_node in db, maybe you set the incorrect node id') + return rows + cur.close() + + node_info_dict = {} + for column in range(len(nodeinfo)): + node_info_dict[node_info_keys[column]] = nodeinfo[column] + self.cfg['transfer_mul'] = float(node_info_dict['traffic_rate']) + + cur = conn.cursor() + try: + rows = [] + cur.execute("SELECT " + ','.join(keys) + " FROM user") + for r in cur.fetchall(): + d = {} + for column in range(len(keys)): + d[keys[column]] = r[column] + rows.append(d) + except Exception as e: + logging.error(e) + cur.close() + return rows + + def load(self): + import os + return os.popen("cat /proc/loadavg | awk '{ print $1\" \"$2\" \"$3 }'").readlines()[0] + + def uptime(self): + return time.time() - self.start_time + + def traffic_format(self, traffic): + if traffic < 1024 * 8: + return str(int(traffic)) + "B"; + + if traffic < 1024 * 1024 * 2: + return str(round((traffic / 1024.0), 2)) + "KB"; + + return str(round((traffic / 1048576.0), 2)) + "MB"; + +class MuJsonTransfer(TransferBase): + def __init__(self): + super(MuJsonTransfer, self).__init__() + + def update_all_user(self, dt_transfer): + import json + rows = None + + config_path = get_config().MUDB_FILE + with open(config_path, 'rb+') as f: + rows = json.loads(f.read().decode('utf8')) + for row in rows: + if "port" in row: + port = row["port"] + if port in dt_transfer: + row["u"] += dt_transfer[port][0] + row["d"] += dt_transfer[port][1] + + if rows: + output = json.dumps(rows, sort_keys=True, indent=4, separators=(',', ': ')) + with open(config_path, 'r+') as f: + f.write(output) + f.truncate() + + return dt_transfer + + def pull_db_all_user(self): + import json + rows = None + + config_path = get_config().MUDB_FILE + with open(config_path, 'rb+') as f: + rows = json.loads(f.read().decode('utf8')) + for row in rows: + try: + if 'forbidden_ip' in row: + row['forbidden_ip'] = common.IPNetwork(row['forbidden_ip']) + except Exception as e: + logging.error(e) + try: + if 'forbidden_port' in row: + row['forbidden_port'] = common.PortRange(row['forbidden_port']) + except Exception as e: + logging.error(e) + + if not rows: + logging.warn('no user in json file') + return rows + diff --git a/importloader.py b/importloader.py new file mode 100644 index 00000000..c917cb7d --- /dev/null +++ b/importloader.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +def load(name): + try: + obj = __import__(name) + reload(obj) + return obj + except: + pass + + try: + import importlib + obj = importlib.__import__(name) + importlib.reload(obj) + return obj + except: + pass + +def loads(namelist): + for name in namelist: + obj = load(name) + if obj is not None: + return obj diff --git a/initcfg.bat b/initcfg.bat new file mode 100644 index 00000000..6d0bce69 --- /dev/null +++ b/initcfg.bat @@ -0,0 +1,4 @@ +@echo off +If Not Exist "userapiconfig.py" Copy "apiconfig.py" "userapiconfig.py" +If Not Exist "user-config.json" Copy "config.json" "user-config.json" +If Not Exist "usermysql.json" Copy "mysql.json" "usermysql.json" diff --git a/initcfg.sh b/initcfg.sh new file mode 100755 index 00000000..862d1abe --- /dev/null +++ b/initcfg.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +chmod +x *.sh +chmod +x shadowsocks/*.sh +cp -n apiconfig.py userapiconfig.py +cp -n config.json user-config.json +cp -n mysql.json usermysql.json + diff --git a/initmudbjson.sh b/initmudbjson.sh new file mode 100755 index 00000000..09b07f33 --- /dev/null +++ b/initmudbjson.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +bash initcfg.sh +sed -i "s/API_INTERFACE = .\+\?\#/API_INTERFACE = \'mudbjson\' \#/g" userapiconfig.py +ip_addr=`ifconfig -a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'|tr -d "addr:"` +ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` + +if [[ $ip_count == 1 ]]; then + ip_addr=`ip a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'` + ip_addr=${ip_addr%/*} + ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` +fi +if [[ $ip_count == 1 ]]; then + echo "server IP is "${ip_addr} + sed -i "s/SERVER_PUB_ADDR = .\+/SERVER_PUB_ADDR = \'"${ip_addr}"\'/g" userapiconfig.py + user_count=`python mujson_mgr.py -l|grep -c -e "[0-9]"` + if [[ $user_count == 0 ]]; then + port=`python -c 'import random;print(random.randint(10000, 65536))'` + python mujson_mgr.py -a -p ${port} + fi +else + echo "unable to detect server IP" +fi + diff --git a/logrun.sh b/logrun.sh new file mode 100755 index 00000000..94153fe9 --- /dev/null +++ b/logrun.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py m>> ssserver.log 2>&1 & + diff --git a/mudb.json b/mudb.json new file mode 100644 index 00000000..0d4f101c --- /dev/null +++ b/mudb.json @@ -0,0 +1,2 @@ +[ +] diff --git a/mujson_mgr.py b/mujson_mgr.py new file mode 100644 index 00000000..2eb05d59 --- /dev/null +++ b/mujson_mgr.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import traceback +from shadowsocks import shell, common +from configloader import load_config, get_config +import random +import getopt +import sys +import json +import base64 + + +class MuJsonLoader(object): + def __init__(self): + self.json = None + + def load(self, path): + l = "[]" + try: + with open(path, 'rb+') as f: + l = f.read().decode('utf8') + except: + pass + self.json = json.loads(l) + + def save(self, path): + if self.json is not None: + output = json.dumps(self.json, sort_keys=True, indent=4, separators=(',', ': ')) + with open(path, 'a'): + pass + with open(path, 'rb+') as f: + f.write(output.encode('utf8')) + f.truncate() + + +class MuMgr(object): + def __init__(self): + self.config_path = get_config().MUDB_FILE + try: + self.server_addr = get_config().SERVER_PUB_ADDR + except: + self.server_addr = '127.0.0.1' + self.data = MuJsonLoader() + + if self.server_addr == '127.0.0.1': + self.server_addr = self.getipaddr() + + def getipaddr(self, ifname='eth0'): + import socket + import struct + ret = '127.0.0.1' + try: + ret = socket.gethostbyname(socket.getfqdn(socket.gethostname())) + except: + pass + if ret == '127.0.0.1': + try: + import fcntl + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ret = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24]) + except: + pass + return ret + + def ssrlink(self, user, encode, muid): + protocol = user.get('protocol', '') + obfs = user.get('obfs', '') + protocol = protocol.replace("_compatible", "") + obfs = obfs.replace("_compatible", "") + protocol_param = '' + if muid is not None: + protocol_param_ = user.get('protocol_param', '') + param = protocol_param_.split('#') + if len(param) == 2: + for row in self.data.json: + if int(row['port']) == muid: + param = str(muid) + ':' + row['passwd'] + protocol_param = '/?protoparam=' + common.to_str(base64.urlsafe_b64encode(common.to_bytes(param))).replace("=", "") + break + link = ("%s:%s:%s:%s:%s:%s" % (self.server_addr, user['port'], protocol, user['method'], obfs, common.to_str(base64.urlsafe_b64encode(common.to_bytes(user['passwd']))).replace("=", ""))) + protocol_param + return "ssr://" + (encode and common.to_str(base64.urlsafe_b64encode(common.to_bytes(link))).replace("=", "") or link) + + def userinfo(self, user, muid = None): + ret = "" + key_list = ['user', 'port', 'method', 'passwd', 'protocol', 'protocol_param', 'obfs', 'obfs_param', 'transfer_enable', 'u', 'd'] + for key in sorted(user): + if key not in key_list: + key_list.append(key) + for key in key_list: + if key in ['enable'] or key not in user: + continue + ret += '\n' + if (muid is not None) and (key in ['protocol_param']): + for row in self.data.json: + if int(row['port']) == muid: + ret += " %s : %s" % (key, str(muid) + ':' + row['passwd']) + break + elif key in ['transfer_enable', 'u', 'd']: + if muid is not None: + for row in self.data.json: + if int(row['port']) == muid: + val = row[key] + break + else: + val = user[key] + if val / 1024 < 4: + ret += " %s : %s" % (key, val) + elif val / 1024 ** 2 < 4: + val /= float(1024) + ret += " %s : %s K Bytes" % (key, val) + elif val / 1024 ** 3 < 4: + val /= float(1024 ** 2) + ret += " %s : %s M Bytes" % (key, val) + else: + val /= float(1024 ** 3) + ret += " %s : %s G Bytes" % (key, val) + else: + ret += " %s : %s" % (key, user[key]) + ret += "\n " + self.ssrlink(user, False, muid) + ret += "\n " + self.ssrlink(user, True, muid) + return ret + + def rand_pass(self): + return ''.join([random.choice('''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-_=+(){}[]^&%$@''') for i in range(8)]) + + def add(self, user): + up = {'enable': 1, 'u': 0, 'd': 0, 'method': "aes-128-ctr", + 'protocol': "auth_aes128_md5", + 'obfs': "tls1.2_ticket_auth_compatible", + 'transfer_enable': 9007199254740992} + up['passwd'] = self.rand_pass() + up.update(user) + + self.data.load(self.config_path) + for row in self.data.json: + match = False + if 'user' in user and row['user'] == user['user']: + match = True + if 'port' in user and row['port'] == user['port']: + match = True + if match: + print("user [%s] port [%s] already exist" % (row['user'], row['port'])) + return + self.data.json.append(up) + print("### add user info %s" % self.userinfo(up)) + self.data.save(self.config_path) + + def edit(self, user): + self.data.load(self.config_path) + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + print("edit user [%s]" % (row['user'],)) + row.update(user) + print("### new user info %s" % self.userinfo(row)) + break + self.data.save(self.config_path) + + def delete(self, user): + self.data.load(self.config_path) + index = 0 + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + print("delete user [%s]" % row['user']) + del self.data.json[index] + break + index += 1 + self.data.save(self.config_path) + + def clear_ud(self, user): + up = {'u': 0, 'd': 0} + self.data.load(self.config_path) + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + row.update(up) + print("clear user [%s]" % row['user']) + self.data.save(self.config_path) + + def list_user(self, user): + self.data.load(self.config_path) + if not user: + for row in self.data.json: + print("user [%s] port %s" % (row['user'], row['port'])) + return + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + muid = None + if 'muid' in user: + muid = user['muid'] + print("### user [%s] info %s" % (row['user'], self.userinfo(row, muid))) + + +def print_server_help(): + print('''usage: python mujson_manage.py -a|-d|-e|-c|-l [OPTION]... + +Actions: + -a add/edit a user + -d delete a user + -e edit a user + -c set u&d to zero + -l display a user infomation or all users infomation + +Options: + -u USER the user name + -p PORT server port (only this option must be set if add a user) + -k PASSWORD password + -m METHOD encryption method, default: aes-128-ctr + -O PROTOCOL protocol plugin, default: auth_aes128_md5 + -o OBFS obfs plugin, default: tls1.2_ticket_auth_compatible + -G PROTOCOL_PARAM protocol plugin param + -g OBFS_PARAM obfs plugin param + -t TRANSFER max transfer for G bytes, default: 8388608 (8 PB or 8192 TB) + -f FORBID set forbidden ports. Example (ban 1~79 and 81~100): -f "1-79,81-100" + -i MUID set sub id to display (only work with -l) + -s SPEED set speed_limit_per_con + -S SPEED set speed_limit_per_user + +General options: + -h, --help show this help message and exit +''') + + +def main(): + shortopts = 'adeclu:i:p:k:O:o:G:g:m:t:f:hs:S:' + longopts = ['help'] + action = None + user = {} + fast_set_obfs = {'0': 'plain', + '+1': 'http_simple_compatible', + '1': 'http_simple', + '+2': 'tls1.2_ticket_auth_compatible', + '2': 'tls1.2_ticket_auth'} + fast_set_protocol = {'0': 'origin', + 's4': 'auth_sha1_v4', + '+s4': 'auth_sha1_v4_compatible', + 'am': 'auth_aes128_md5', + 'as': 'auth_aes128_sha1', + 'ca': 'auth_chain_a', + } + fast_set_method = {'0': 'none', + 'a1c': 'aes-128-cfb', + 'a2c': 'aes-192-cfb', + 'a3c': 'aes-256-cfb', + 'r': 'rc4-md5', + 'r6': 'rc4-md5-6', + 'c': 'chacha20', + 'ci': 'chacha20-ietf', + 's': 'salsa20', + 'a1': 'aes-128-ctr', + 'a2': 'aes-192-ctr', + 'a3': 'aes-256-ctr'} + try: + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + for key, value in optlist: + if key == '-a': + action = 1 + elif key == '-d': + action = 2 + elif key == '-e': + action = 3 + elif key == '-l': + action = 4 + elif key == '-c': + action = 0 + elif key == '-u': + user['user'] = value + elif key == '-i': + user['muid'] = int(value) + elif key == '-p': + user['port'] = int(value) + elif key == '-k': + user['passwd'] = value + elif key == '-o': + if value in fast_set_obfs: + user['obfs'] = fast_set_obfs[value] + else: + user['obfs'] = value + elif key == '-O': + if value in fast_set_protocol: + user['protocol'] = fast_set_protocol[value] + else: + user['protocol'] = value + elif key == '-g': + user['obfs_param'] = value + elif key == '-G': + user['protocol_param'] = value + elif key == '-s': + user['speed_limit_per_con'] = int(value) + elif key == '-S': + user['speed_limit_per_user'] = int(value) + elif key == '-m': + if value in fast_set_method: + user['method'] = fast_set_method[value] + else: + user['method'] = value + elif key == '-f': + user['forbidden_port'] = value + elif key == '-t': + val = float(value) + try: + val = int(value) + except: + pass + user['transfer_enable'] = int(val * 1024) * (1024 ** 2) + elif key in ('-h', '--help'): + print_server_help() + sys.exit(0) + except getopt.GetoptError as e: + print(e) + sys.exit(2) + + manage = MuMgr() + if action == 0: + manage.clear_ud(user) + elif action == 1: + if 'user' not in user and 'port' in user: + user['user'] = str(user['port']) + if 'user' in user and 'port' in user: + manage.add(user) + else: + print("You have to set the port with -p") + elif action == 2: + if 'user' in user or 'port' in user: + manage.delete(user) + else: + print("You have to set the user name or port with -u/-p") + elif action == 3: + if 'user' in user or 'port' in user: + manage.edit(user) + else: + print("You have to set the user name or port with -u/-p") + elif action == 4: + manage.list_user(user) + elif action is None: + print_server_help() + +if __name__ == '__main__': + main() diff --git a/mysql.json b/mysql.json new file mode 100644 index 00000000..1849e9e8 --- /dev/null +++ b/mysql.json @@ -0,0 +1,13 @@ +{ + "host": "127.0.0.1", + "port": 3306, + "user": "ss", + "password": "pass", + "db": "sspanel", + "node_id": 0, + "transfer_mul": 1.0, + "ssl_enable": 0, + "ssl_ca": "", + "ssl_cert": "", + "ssl_key": "" +} diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..0de8d204 --- /dev/null +++ b/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py m>> /dev/null 2>&1 & + diff --git a/server.py b/server.py new file mode 100644 index 00000000..ba863b68 --- /dev/null +++ b/server.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2015 breakwall +# +# 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. + +import time +import sys +import threading +import os + +if __name__ == '__main__': + import inspect + os.chdir(os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))) + +import server_pool +import db_transfer +from shadowsocks import shell +from configloader import load_config, get_config + +class MainThread(threading.Thread): + def __init__(self, obj): + super(MainThread, self).__init__() + self.daemon = True + self.obj = obj + + def run(self): + self.obj.thread_db(self.obj) + + def stop(self): + self.obj.thread_db_stop() + +def main(): + shell.check_python() + if False: + db_transfer.DbTransfer.thread_db() + else: + if get_config().API_INTERFACE == 'mudbjson': + thread = MainThread(db_transfer.MuJsonTransfer) + elif get_config().API_INTERFACE == 'sspanelv2': + thread = MainThread(db_transfer.DbTransfer) + else: + thread = MainThread(db_transfer.Dbv3Transfer) + thread.start() + try: + while thread.is_alive(): + thread.join(10.0) + except (KeyboardInterrupt, IOError, OSError) as e: + import traceback + traceback.print_exc() + thread.stop() + +if __name__ == '__main__': + main() + diff --git a/server_pool.py b/server_pool.py new file mode 100644 index 00000000..d159817a --- /dev/null +++ b/server_pool.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import logging +import struct +import time +from shadowsocks import shell, eventloop, tcprelay, udprelay, asyncdns, common +import threading +import sys +import traceback +from socket import * +from configloader import load_config, get_config + +class MainThread(threading.Thread): + def __init__(self, params): + super(MainThread, self).__init__() + self.params = params + + def run(self): + ServerPool._loop(*self.params) + +class ServerPool(object): + + instance = None + + def __init__(self): + shell.check_python() + self.config = shell.get_config(False) + self.dns_resolver = asyncdns.DNSResolver() + if not self.config.get('dns_ipv6', False): + asyncdns.IPV6_CONNECTION_SUPPORT = False + + self.mgr = None #asyncmgr.ServerMgr() + + self.tcp_servers_pool = {} + self.tcp_ipv6_servers_pool = {} + self.udp_servers_pool = {} + self.udp_ipv6_servers_pool = {} + self.stat_counter = {} + + self.loop = eventloop.EventLoop() + self.thread = MainThread( (self.loop, self.dns_resolver, self.mgr) ) + self.thread.start() + + @staticmethod + def get_instance(): + if ServerPool.instance is None: + ServerPool.instance = ServerPool() + return ServerPool.instance + + def stop(self): + self.loop.stop() + + @staticmethod + def _loop(loop, dns_resolver, mgr): + try: + if mgr is not None: + mgr.add_to_loop(loop) + dns_resolver.add_to_loop(loop) + loop.run() + except (KeyboardInterrupt, IOError, OSError) as e: + logging.error(e) + traceback.print_exc() + os.exit(0) + except Exception as e: + logging.error(e) + traceback.print_exc() + + def server_is_run(self, port): + port = int(port) + ret = 0 + if port in self.tcp_servers_pool: + ret = 1 + if port in self.tcp_ipv6_servers_pool: + ret |= 2 + return ret + + def server_run_status(self, port): + if 'server' in self.config: + if port not in self.tcp_servers_pool: + return False + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + return False + return True + + def new_server(self, port, user_config): + ret = True + port = int(port) + ipv6_ok = False + + if 'server_ipv6' in self.config: + if port in self.tcp_ipv6_servers_pool: + logging.info("server already at %s:%d" % (self.config['server_ipv6'], port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config.update(user_config) + if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]": + a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] + a_config['server'] = a_config['server_ipv6'] + a_config['server_port'] = port + a_config['max_connect'] = 128 + a_config['method'] = common.to_str(a_config['method']) + try: + logging.info("starting server at [%s]:%d" % (common.to_str(a_config['server']), port)) + + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) + tcp_server.add_to_loop(self.loop) + self.tcp_ipv6_servers_pool.update({port: tcp_server}) + + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) + udp_server.add_to_loop(self.loop) + self.udp_ipv6_servers_pool.update({port: udp_server}) + + if common.to_str(a_config['server_ipv6']) == "::": + ipv6_ok = True + except Exception as e: + logging.warn("IPV6 %s " % (e,)) + + if 'server' in self.config: + if port in self.tcp_servers_pool: + logging.info("server already at %s:%d" % (common.to_str(self.config['server']), port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config.update(user_config) + a_config['server_port'] = port + a_config['max_connect'] = 128 + a_config['method'] = common.to_str(a_config['method']) + try: + logging.info("starting server at %s:%d" % (common.to_str(a_config['server']), port)) + + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False) + tcp_server.add_to_loop(self.loop) + self.tcp_servers_pool.update({port: tcp_server}) + + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False) + udp_server.add_to_loop(self.loop) + self.udp_servers_pool.update({port: udp_server}) + + except Exception as e: + if not ipv6_ok: + logging.warn("IPV4 %s " % (e,)) + + return True + + def del_server(self, port): + port = int(port) + logging.info("del server at %d" % port) + try: + udpsock = socket(AF_INET, SOCK_DGRAM) + udpsock.sendto('%s:%s:0:0' % (get_config().MANAGE_PASS, port), (get_config().MANAGE_BIND_IP, get_config().MANAGE_PORT)) + udpsock.close() + except Exception as e: + logging.warn(e) + return True + + def cb_del_server(self, port): + port = int(port) + + if port not in self.tcp_servers_pool: + logging.info("stopped server at %s:%d already stop" % (self.config['server'], port)) + else: + logging.info("stopped server at %s:%d" % (self.config['server'], port)) + try: + self.tcp_servers_pool[port].close(True) + del self.tcp_servers_pool[port] + except Exception as e: + logging.warn(e) + try: + self.udp_servers_pool[port].close(True) + del self.udp_servers_pool[port] + except Exception as e: + logging.warn(e) + + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + logging.info("stopped server at [%s]:%d already stop" % (self.config['server_ipv6'], port)) + else: + logging.info("stopped server at [%s]:%d" % (self.config['server_ipv6'], port)) + try: + self.tcp_ipv6_servers_pool[port].close(True) + del self.tcp_ipv6_servers_pool[port] + except Exception as e: + logging.warn(e) + try: + self.udp_ipv6_servers_pool[port].close(True) + del self.udp_ipv6_servers_pool[port] + except Exception as e: + logging.warn(e) + + return True + + def update_mu_users(self, port, users): + port = int(port) + if port in self.tcp_servers_pool: + try: + self.tcp_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + try: + self.udp_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + if port in self.tcp_ipv6_servers_pool: + try: + self.tcp_ipv6_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + try: + self.udp_ipv6_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + + def get_server_transfer(self, port): + port = int(port) + uid = struct.pack('= 2: - server = parts[1] - if common.is_ip(server) == socket.AF_INET: - if type(server) != str: - server = server.decode('utf8') - self._servers.append(server) + parts = line.split(b' ', 1) + if len(parts) >= 2: + server = parts[0] + port = int(parts[1]) + else: + server = parts[0] + port = 53 + if common.is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append((server, port)) except IOError: pass if not self._servers: - self._servers = ['8.8.4.4', '8.8.8.8'] + try: + with open('/etc/resolv.conf', 'rb') as f: + content = f.readlines() + for line in content: + line = line.strip() + if line: + if line.startswith(b'nameserver'): + parts = line.split() + if len(parts) >= 2: + server = parts[1] + if common.is_ip(server) == socket.AF_INET: + if type(server) != str: + server = server.decode('utf8') + self._servers.append((server, 53)) + except IOError: + pass + if not self._servers: + self._servers = [('8.8.4.4', 53), ('8.8.8.8', 53)] + logging.info('dns server: %s' % (self._servers,)) def _parse_hosts(self): etc_path = '/etc/hosts' @@ -293,6 +331,8 @@ def _parse_hosts(self): with open(etc_path, 'rb') as f: for line in f.readlines(): line = line.strip() + if b"#" in line: + line = line[:line.find(b'#')] parts = line.split() if len(parts) >= 2: ip = parts[0] @@ -304,7 +344,7 @@ def _parse_hosts(self): except IOError: self._hosts['localhost'] = '127.0.0.1' - def add_to_loop(self, loop, ref=False): + def add_to_loop(self, loop): if self._loop: raise Exception('already add to loop') self._loop = loop @@ -312,8 +352,8 @@ def add_to_loop(self, loop, ref=False): self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.SOL_UDP) self._sock.setblocking(False) - loop.add(self._sock, eventloop.POLL_IN) - loop.add_handler(self.handle_events, ref=ref) + loop.add(self._sock, eventloop.POLL_IN, self) + loop.add_periodic(self.handle_periodic) def _call_callback(self, hostname, ip, error=None): callbacks = self._hostname_to_cb.get(hostname, []) @@ -324,7 +364,7 @@ def _call_callback(self, hostname, ip, error=None): callback((hostname, ip), error) else: callback((hostname, None), - Exception('unknown hostname %s' % hostname)) + Exception('unable to parse hostname %s' % hostname)) if hostname in self._hostname_to_cb: del self._hostname_to_cb[hostname] if hostname in self._hostname_status: @@ -340,44 +380,56 @@ def _handle_data(self, data): answer[2] == QCLASS_IN: ip = answer[0] break - if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ - == STATUS_IPV4: - self._hostname_status[hostname] = STATUS_IPV6 - self._send_req(hostname, QTYPE_AAAA) - else: - if ip: - self._cache[hostname] = ip - self._call_callback(hostname, ip) - elif self._hostname_status.get(hostname, None) == STATUS_IPV6: - for question in response.questions: - if question[1] == QTYPE_AAAA: - self._call_callback(hostname, None) - break - - def handle_events(self, events): - for sock, fd, event in events: - if sock != self._sock: - continue - if event & eventloop.POLL_ERR: - logging.error('dns socket err') - self._loop.remove(self._sock) - self._sock.close() - # TODO when dns server is IPv6 - self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, - socket.SOL_UDP) - self._sock.setblocking(False) - self._loop.add(self._sock, eventloop.POLL_IN) + if IPV6_CONNECTION_SUPPORT: + if not ip and self._hostname_status.get(hostname, STATUS_IPV4) \ + == STATUS_IPV6: + self._hostname_status[hostname] = STATUS_IPV4 + self._send_req(hostname, QTYPE_A) + else: + if ip: + self._cache[hostname] = ip + self._call_callback(hostname, ip) + elif self._hostname_status.get(hostname, None) == STATUS_IPV4: + for question in response.questions: + if question[1] == QTYPE_A: + self._call_callback(hostname, None) + break else: - data, addr = sock.recvfrom(1024) - if addr[0] not in self._servers: - logging.warn('received a packet other than our dns') - break - self._handle_data(data) - break - now = time.time() - if now - self._last_time > CACHE_SWEEP_INTERVAL: - self._cache.sweep() - self._last_time = now + if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ + == STATUS_IPV4: + self._hostname_status[hostname] = STATUS_IPV6 + self._send_req(hostname, QTYPE_AAAA) + else: + if ip: + self._cache[hostname] = ip + self._call_callback(hostname, ip) + elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + for question in response.questions: + if question[1] == QTYPE_AAAA: + self._call_callback(hostname, None) + break + + def handle_event(self, sock, fd, event): + if sock != self._sock: + return + if event & eventloop.POLL_ERR: + logging.error('dns socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN, self) + else: + data, addr = sock.recvfrom(1024) + if addr not in self._servers: + logging.warn('received a packet other than our dns') + return + self._handle_data(data) + + def handle_periodic(self): + self._cache.sweep() def remove_callback(self, callback): hostname = self._cb_to_hostname.get(callback) @@ -396,7 +448,7 @@ def _send_req(self, hostname, qtype): for server in self._servers: logging.debug('resolving %s with type %d using server %s', hostname, qtype, server) - self._sock.sendto(req, (server, 53)) + self._sock.sendto(req, server) def resolve(self, hostname, callback): if type(hostname) != bytes: @@ -417,19 +469,38 @@ def resolve(self, hostname, callback): if not is_valid_hostname(hostname): callback(None, Exception('invalid hostname: %s' % hostname)) return + if False: + addrs = socket.getaddrinfo(hostname, 0, 0, + socket.SOCK_DGRAM, socket.SOL_UDP) + if addrs: + af, socktype, proto, canonname, sa = addrs[0] + logging.debug('DNS resolve %s %s' % (hostname, sa[0]) ) + self._cache[hostname] = sa[0] + callback((hostname, sa[0]), None) + return arr = self._hostname_to_cb.get(hostname, None) if not arr: - self._hostname_status[hostname] = STATUS_IPV4 - self._send_req(hostname, QTYPE_A) + if IPV6_CONNECTION_SUPPORT: + self._hostname_status[hostname] = STATUS_IPV6 + self._send_req(hostname, QTYPE_AAAA) + else: + self._hostname_status[hostname] = STATUS_IPV4 + self._send_req(hostname, QTYPE_A) self._hostname_to_cb[hostname] = [callback] self._cb_to_hostname[callback] = hostname else: arr.append(callback) # TODO send again only if waited too long - self._send_req(hostname, QTYPE_A) + if IPV6_CONNECTION_SUPPORT: + self._send_req(hostname, QTYPE_AAAA) + else: + self._send_req(hostname, QTYPE_A) def close(self): if self._sock: + if self._loop: + self._loop.remove_periodic(self.handle_periodic) + self._loop.remove(self._sock) self._sock.close() self._sock = None @@ -437,7 +508,7 @@ def close(self): def test(): dns_resolver = DNSResolver() loop = eventloop.EventLoop() - dns_resolver.add_to_loop(loop, ref=True) + dns_resolver.add_to_loop(loop) global counter counter = 0 @@ -451,8 +522,8 @@ def callback(result, error): print(result, error) counter += 1 if counter == 9: - loop.remove_handler(dns_resolver.handle_events) dns_resolver.close() + loop.stop() a_callback = callback return a_callback @@ -481,3 +552,4 @@ def callback(result, error): if __name__ == '__main__': test() + diff --git a/shadowsocks/common.py b/shadowsocks/common.py index fc03d556..c4484c04 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -21,7 +21,10 @@ import socket import struct import logging +import binascii +import re +from shadowsocks import lru_cache def compat_ord(s): if type(s) == int: @@ -40,6 +43,7 @@ def compat_chr(d): ord = compat_ord chr = compat_chr +connect_log = logging.debug def to_bytes(s): if bytes != str: @@ -54,6 +58,16 @@ def to_str(s): return s.decode('utf-8') return s +def int32(x): + if x > 0xFFFFFFFF or x < 0: + x &= 0xFFFFFFFF + if x > 0x7FFFFFFF: + x = int(0x100000000 - x) + if x < 0x80000000: + return -x + else: + return -2147483648 + return x def inet_ntop(family, ipstr): if family == socket.AF_INET: @@ -74,7 +88,7 @@ def inet_pton(family, addr): if '.' in addr: # a v4 addr v4addr = addr[addr.rindex(':') + 1:] v4addr = socket.inet_aton(v4addr) - v4addr = map(lambda x: ('%02X' % ord(x)), v4addr) + v4addr = ['%02X' % ord(x) for x in v4addr] v4addr.insert(2, ':') newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr) return inet_pton(family, newaddr) @@ -107,6 +121,13 @@ def is_ip(address): return False +def match_regex(regex, text): + regex = re.compile(regex) + for item in regex.findall(text): + return True + return False + + def patch_socket(): if not hasattr(socket, 'inet_pton'): socket.inet_pton = inet_pton @@ -138,12 +159,54 @@ def pack_addr(address): address = address[:255] # TODO return b'\x03' + chr(len(address)) + address +def pre_parse_header(data): + if not data: + return None + datatype = ord(data[0]) + if datatype == 0x80: + if len(data) <= 2: + return None + rand_data_size = ord(data[1]) + if rand_data_size + 2 >= len(data): + logging.warn('header too short, maybe wrong password or ' + 'encryption method') + return None + data = data[rand_data_size + 2:] + elif datatype == 0x81: + data = data[1:] + elif datatype == 0x82: + if len(data) <= 3: + return None + rand_data_size = struct.unpack('>H', data[1:3])[0] + if rand_data_size + 3 >= len(data): + logging.warn('header too short, maybe wrong password or ' + 'encryption method') + return None + data = data[rand_data_size + 3:] + elif datatype == 0x88 or (~datatype & 0xff) == 0x88: + if len(data) <= 7 + 7: + return None + data_size = struct.unpack('>H', data[1:3])[0] + ogn_data = data + data = data[:data_size] + crc = binascii.crc32(data) & 0xffffffff + if crc != 0xffffffff: + logging.warn('uncorrect CRC32, maybe wrong password or ' + 'encryption method') + return None + start_pos = 3 + ord(data[3]) + data = data[start_pos:-4] + if data_size < len(ogn_data): + data += ogn_data[data_size:] + return data def parse_header(data): addrtype = ord(data[0]) dest_addr = None dest_port = None header_length = 0 + connecttype = (addrtype & 0x8) and 1 or 0 + addrtype &= ~0x8 if addrtype == ADDRTYPE_IPV4: if len(data) >= 7: dest_addr = socket.inet_ntoa(data[1:5]) @@ -154,10 +217,10 @@ def parse_header(data): elif addrtype == ADDRTYPE_HOST: if len(data) > 2: addrlen = ord(data[1]) - if len(data) >= 2 + addrlen: + if len(data) >= 4 + addrlen: dest_addr = data[2:2 + addrlen] dest_port = struct.unpack('>H', data[2 + addrlen:4 + - addrlen])[0] + addrlen])[0] header_length = 4 + addrlen else: logging.warn('header is too short') @@ -175,13 +238,14 @@ def parse_header(data): 'encryption method' % addrtype) if dest_addr is None: return None - return addrtype, to_bytes(dest_addr), dest_port, header_length + return connecttype, addrtype, to_bytes(dest_addr), dest_port, header_length class IPNetwork(object): ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0} def __init__(self, addrs): + self.addrs_str = addrs self._network_list_v4 = [] self._network_list_v6 = [] if type(addrs) == str: @@ -232,6 +296,79 @@ def __contains__(self, addr): else: return False + def __cmp__(self, other): + return cmp(self.addrs_str, other.addrs_str) + + def __eq__(self, other): + return self.addrs_str == other.addrs_str + + def __ne__(self, other): + return self.addrs_str != other.addrs_str + +class PortRange(object): + def __init__(self, range_str): + self.range_str = to_str(range_str) + self.range = set() + range_str = to_str(range_str).split(',') + for item in range_str: + try: + int_range = item.split('-') + if len(int_range) == 1: + if item: + self.range.add(int(item)) + elif len(int_range) == 2: + int_range[0] = int(int_range[0]) + int_range[1] = int(int_range[1]) + if int_range[0] < 0: + int_range[0] = 0 + if int_range[1] > 65535: + int_range[1] = 65535 + i = int_range[0] + while i <= int_range[1]: + self.range.add(i) + i += 1 + except Exception as e: + logging.error(e) + + def __contains__(self, val): + return val in self.range + + def __cmp__(self, other): + return cmp(self.range_str, other.range_str) + + def __eq__(self, other): + return self.range_str == other.range_str + + def __ne__(self, other): + return self.range_str != other.range_str + +class UDPAsyncDNSHandler(object): + dns_cache = lru_cache.LRUCache(timeout=1800) + def __init__(self, params): + self.params = params + self.remote_addr = None + self.call_back = None + + def resolve(self, dns_resolver, remote_addr, call_back): + if remote_addr in UDPAsyncDNSHandler.dns_cache: + if call_back: + call_back("", remote_addr, UDPAsyncDNSHandler.dns_cache[remote_addr], self.params) + else: + self.call_back = call_back + self.remote_addr = remote_addr + dns_resolver.resolve(remote_addr[0], self._handle_dns_resolved) + UDPAsyncDNSHandler.dns_cache.sweep() + + def _handle_dns_resolved(self, result, error): + if error: + logging.error("%s when resolve DNS" % (error,)) #drop + return self.call_back(error, self.remote_addr, None, self.params) + if result: + ip = result[1] + if ip: + return self.call_back("", self.remote_addr, ip, self.params) + logging.warning("can't resolve %s" % (self.remote_addr,)) + return self.call_back("fail to resolve", self.remote_addr, None, self.params) def test_inet_conv(): ipv4 = b'8.8.4.4' @@ -244,12 +381,12 @@ def test_inet_conv(): def test_parse_header(): assert parse_header(b'\x03\x0ewww.google.com\x00\x50') == \ - (3, b'www.google.com', 80, 18) + (0, b'www.google.com', 80, 18) assert parse_header(b'\x01\x08\x08\x08\x08\x00\x35') == \ - (1, b'8.8.8.8', 53, 7) + (0, b'8.8.8.8', 53, 7) assert parse_header((b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00' b'\x00\x10\x11\x00\x50')) == \ - (4, b'2404:6800:4005:805::1011', 80, 19) + (0, b'2404:6800:4005:805::1011', 80, 19) def test_pack_header(): diff --git a/shadowsocks/crypto/ctypes_libsodium.py b/shadowsocks/crypto/ctypes_libsodium.py new file mode 100644 index 00000000..efecfd41 --- /dev/null +++ b/shadowsocks/crypto/ctypes_libsodium.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ + create_string_buffer, c_void_p + +__all__ = ['ciphers'] + +libsodium = None +loaded = False + +buf_size = 2048 + +# for salsa20 and chacha20 +BLOCK_SIZE = 64 + + +def load_libsodium(): + global loaded, libsodium, buf + + from ctypes.util import find_library + for p in ('sodium',): + libsodium_path = find_library(p) + if libsodium_path: + break + else: + raise Exception('libsodium not found') + logging.info('loading libsodium from %s', libsodium_path) + libsodium = CDLL(libsodium_path) + libsodium.sodium_init.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + libsodium.crypto_stream_chacha20_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + + libsodium.sodium_init() + + buf = create_string_buffer(buf_size) + loaded = True + + +class Salsa20Crypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_libsodium() + self.key = key + self.iv = iv + self.key_ptr = c_char_p(key) + self.iv_ptr = c_char_p(iv) + if cipher_name == b'salsa20': + self.cipher = libsodium.crypto_stream_salsa20_xor_ic + elif cipher_name == b'chacha20': + self.cipher = libsodium.crypto_stream_chacha20_xor_ic + else: + raise Exception('Unknown cipher') + # byte counter, not block counter + self.counter = 0 + + def update(self, data): + global buf_size, buf + l = len(data) + + # we can only prepend some padding to make the encryption align to + # blocks + padding = self.counter % BLOCK_SIZE + if buf_size < padding + l: + buf_size = (padding + l) * 2 + buf = create_string_buffer(buf_size) + + if padding: + data = (b'\0' * padding) + data + self.cipher(byref(buf), c_char_p(data), padding + l, + self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) + self.counter += l + # buf is copied to a str object when we access buf.raw + # strip off the padding + return buf.raw[padding:padding + l] + + +ciphers = { + b'salsa20': (32, 8, Salsa20Crypto), + b'chacha20': (32, 8, Salsa20Crypto), +} + + +def test_salsa20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +if __name__ == '__main__': + test_chacha20() + test_salsa20() diff --git a/shadowsocks/crypto/ctypes_openssl.py b/shadowsocks/crypto/ctypes_openssl.py new file mode 100644 index 00000000..0ef8ce0f --- /dev/null +++ b/shadowsocks/crypto/ctypes_openssl.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ + create_string_buffer, c_void_p + +__all__ = ['ciphers'] + +libcrypto = None +loaded = False + +buf_size = 2048 + + +def load_openssl(): + global loaded, libcrypto, buf + + from ctypes.util import find_library + for p in ('crypto', 'eay32', 'libeay32'): + libcrypto_path = find_library(p) + if libcrypto_path: + break + else: + raise Exception('libcrypto(OpenSSL) not found') + logging.info('loading libcrypto from %s', libcrypto_path) + libcrypto = CDLL(libcrypto_path) + libcrypto.EVP_get_cipherbyname.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p + + libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p, c_char_p, c_int) + + libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, + c_char_p, c_int) + + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): + libcrypto.OpenSSL_add_all_ciphers() + + buf = create_string_buffer(buf_size) + loaded = True + + +def load_cipher(cipher_name): + func_name = b'EVP_' + cipher_name.replace(b'-', b'_') + if bytes != str: + func_name = str(func_name, 'utf-8') + cipher = getattr(libcrypto, func_name, None) + if cipher: + cipher.restype = c_void_p + return cipher() + return None + + +class CtypesCrypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_openssl() + self._ctx = None + cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + if not cipher: + cipher = load_cipher(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libcrypto' % cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + self._ctx = libcrypto.EVP_CIPHER_CTX_new() + if not self._ctx: + raise Exception('can not create cipher context') + r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def update(self, data): + global buf_size, buf + cipher_out_len = c_long(0) + l = len(data) + if buf_size < l: + buf_size = l * 2 + buf = create_string_buffer(buf_size) + libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l) + # buf is copied to a str object when we access buf.raw + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + libcrypto.EVP_CIPHER_CTX_free(self._ctx) + + +ciphers = { + b'aes-128-cfb': (16, 16, CtypesCrypto), + b'aes-192-cfb': (24, 16, CtypesCrypto), + b'aes-256-cfb': (32, 16, CtypesCrypto), + b'aes-128-ofb': (16, 16, CtypesCrypto), + b'aes-192-ofb': (24, 16, CtypesCrypto), + b'aes-256-ofb': (32, 16, CtypesCrypto), + b'aes-128-ctr': (16, 16, CtypesCrypto), + b'aes-192-ctr': (24, 16, CtypesCrypto), + b'aes-256-ctr': (32, 16, CtypesCrypto), + b'aes-128-cfb8': (16, 16, CtypesCrypto), + b'aes-192-cfb8': (24, 16, CtypesCrypto), + b'aes-256-cfb8': (32, 16, CtypesCrypto), + b'aes-128-cfb1': (16, 16, CtypesCrypto), + b'aes-192-cfb1': (24, 16, CtypesCrypto), + b'aes-256-cfb1': (32, 16, CtypesCrypto), + b'bf-cfb': (16, 8, CtypesCrypto), + b'camellia-128-cfb': (16, 16, CtypesCrypto), + b'camellia-192-cfb': (24, 16, CtypesCrypto), + b'camellia-256-cfb': (32, 16, CtypesCrypto), + b'cast5-cfb': (16, 8, CtypesCrypto), + b'des-cfb': (8, 8, CtypesCrypto), + b'idea-cfb': (16, 8, CtypesCrypto), + b'rc2-cfb': (16, 8, CtypesCrypto), + b'rc4': (16, 0, CtypesCrypto), + b'seed-cfb': (16, 16, CtypesCrypto), +} + + +def run_method(method): + from shadowsocks.crypto import util + + cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_aes_128_cfb(): + run_method(b'aes-128-cfb') + + +def test_aes_256_cfb(): + run_method(b'aes-256-cfb') + + +def test_aes_128_cfb8(): + run_method(b'aes-128-cfb8') + + +def test_aes_256_ofb(): + run_method(b'aes-256-ofb') + + +def test_aes_256_ctr(): + run_method(b'aes-256-ctr') + + +def test_bf_cfb(): + run_method(b'bf-cfb') + + +def test_rc4(): + run_method(b'rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/crypto/openssl.py b/shadowsocks/crypto/openssl.py index 3775b6c4..0a8ca53f 100644 --- a/shadowsocks/crypto/openssl.py +++ b/shadowsocks/crypto/openssl.py @@ -49,8 +49,15 @@ def load_openssl(): libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, c_char_p, c_int) - libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"): + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + else: + libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,) libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + + libcrypto.RAND_bytes.restype = c_int + libcrypto.RAND_bytes.argtypes = (c_void_p, c_int) + if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): libcrypto.OpenSSL_add_all_ciphers() @@ -60,22 +67,27 @@ def load_openssl(): def load_cipher(cipher_name): func_name = 'EVP_' + cipher_name.replace('-', '_') - if bytes != str: - func_name = str(func_name, 'utf-8') cipher = getattr(libcrypto, func_name, None) if cipher: cipher.restype = c_void_p return cipher() return None +def rand_bytes(length): + if not loaded: + load_openssl() + buf = create_string_buffer(length) + r = libcrypto.RAND_bytes(buf, length) + if r <= 0: + raise Exception('RAND_bytes return error') + return buf.raw class OpenSSLCrypto(object): def __init__(self, cipher_name, key, iv, op): self._ctx = None if not loaded: load_openssl() - cipher_name = common.to_bytes(cipher_name) - cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(cipher_name)) if not cipher: cipher = load_cipher(cipher_name) if not cipher: @@ -108,11 +120,17 @@ def __del__(self): def clean(self): if self._ctx: - libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"): + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + else: + libcrypto.EVP_CIPHER_CTX_reset(self._ctx) libcrypto.EVP_CIPHER_CTX_free(self._ctx) ciphers = { + 'aes-128-cbc': (16, 16, OpenSSLCrypto), + 'aes-192-cbc': (24, 16, OpenSSLCrypto), + 'aes-256-cbc': (32, 16, OpenSSLCrypto), 'aes-128-cfb': (16, 16, OpenSSLCrypto), 'aes-192-cfb': (24, 16, OpenSSLCrypto), 'aes-256-cfb': (32, 16, OpenSSLCrypto), diff --git a/shadowsocks/crypto/rc4_md5.py b/shadowsocks/crypto/rc4_md5.py index 1f07a82f..b0105ec2 100644 --- a/shadowsocks/crypto/rc4_md5.py +++ b/shadowsocks/crypto/rc4_md5.py @@ -35,6 +35,7 @@ def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, ciphers = { 'rc4-md5': (16, 16, create_cipher), + 'rc4-md5-6': (16, 6, create_cipher), } diff --git a/shadowsocks/crypto/sodium.py b/shadowsocks/crypto/sodium.py index ae86fef3..51d476be 100644 --- a/shadowsocks/crypto/sodium.py +++ b/shadowsocks/crypto/sodium.py @@ -17,7 +17,7 @@ from __future__ import absolute_import, division, print_function, \ with_statement -from ctypes import c_char_p, c_int, c_ulonglong, byref, \ +from ctypes import c_char_p, c_int, c_ulong, c_ulonglong, byref, \ create_string_buffer, c_void_p from shadowsocks.crypto import util @@ -29,7 +29,7 @@ buf_size = 2048 -# for salsa20 and chacha20 +# for salsa20 and chacha20 and chacha20-ietf BLOCK_SIZE = 64 @@ -52,6 +52,15 @@ def load_libsodium(): c_char_p, c_ulonglong, c_char_p) + try: + libsodium.crypto_stream_chacha20_ietf_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulong, + c_char_p) + except: + pass + buf = create_string_buffer(buf_size) loaded = True @@ -68,6 +77,8 @@ def __init__(self, cipher_name, key, iv, op): self.cipher = libsodium.crypto_stream_salsa20_xor_ic elif cipher_name == 'chacha20': self.cipher = libsodium.crypto_stream_chacha20_xor_ic + elif cipher_name == 'chacha20-ietf': + self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic else: raise Exception('Unknown cipher') # byte counter, not block counter @@ -97,6 +108,7 @@ def update(self, data): ciphers = { 'salsa20': (32, 8, SodiumCrypto), 'chacha20': (32, 8, SodiumCrypto), + 'chacha20-ietf': (32, 12, SodiumCrypto), } @@ -115,6 +127,14 @@ def test_chacha20(): util.run_cipher(cipher, decipher) +def test_chacha20_ietf(): + + cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1) + decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + if __name__ == '__main__': + test_chacha20_ietf() test_chacha20() test_salsa20() diff --git a/shadowsocks/crypto/table.py b/shadowsocks/crypto/table.py index bc693f51..60c2f245 100644 --- a/shadowsocks/crypto/table.py +++ b/shadowsocks/crypto/table.py @@ -65,9 +65,16 @@ def update(self, data): else: return translate(data, self._decrypt_table) +class NoneCipher(object): + def __init__(self, cipher_name, key, iv, op): + pass + + def update(self, data): + return data ciphers = { - 'table': (0, 0, TableCipher) + 'none': (16, 0, NoneCipher), + 'table': (16, 0, TableCipher) } diff --git a/shadowsocks/crypto/util.py b/shadowsocks/crypto/util.py index e579455e..212df860 100644 --- a/shadowsocks/crypto/util.py +++ b/shadowsocks/crypto/util.py @@ -88,7 +88,8 @@ def find_library(possible_lib_names, search_symbol, library_name): logging.warn('can\'t find symbol %s in %s', search_symbol, path) except Exception: - pass + if path == paths[-1]: + raise return None diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 4e87f415..44f90525 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -34,8 +34,10 @@ def random_string(length): - return os.urandom(length) - + try: + return os.urandom(length) + except NotImplementedError as e: + return openssl.rand_bytes(length) cached_keys = {} @@ -47,6 +49,8 @@ def try_cipher(key, method=None): def EVP_BytesToKey(password, key_len, iv_len): # equivalent to OpenSSL's EVP_BytesToKey() with count 1 # so that we make the same key and iv as nodejs version + if hasattr(password, 'encode'): + password = password.encode('utf-8') cached_key = '%s-%d-%d' % (password, key_len, iv_len) r = cached_keys.get(cached_key, None) if r: @@ -69,18 +73,23 @@ def EVP_BytesToKey(password, key_len, iv_len): class Encryptor(object): - def __init__(self, key, method): + def __init__(self, key, method, iv = None): self.key = key self.method = method self.iv = None self.iv_sent = False self.cipher_iv = b'' + self.iv_buf = b'' + self.cipher_key = b'' self.decipher = None method = method.lower() self._method_info = self.get_method_info(method) if self._method_info: - self.cipher = self.get_cipher(key, method, 1, + if iv is None or len(iv) != self._method_info[1]: + self.cipher = self.get_cipher(key, method, 1, random_string(self._method_info[1])) + else: + self.cipher = self.get_cipher(key, method, 1, iv) else: logging.error('method %s not supported' % method) sys.exit(1) @@ -106,6 +115,7 @@ def get_cipher(self, password, method, op, iv): if op == 1: # this iv is for cipher not decipher self.cipher_iv = iv[:m[1]] + self.cipher_key = key return m[2](method, key, iv, op) def encrypt(self, buf): @@ -120,16 +130,21 @@ def encrypt(self, buf): def decrypt(self, buf): if len(buf) == 0: return buf - if self.decipher is None: - decipher_iv_len = self._method_info[1] - decipher_iv = buf[:decipher_iv_len] + if self.decipher is not None: #optimize + return self.decipher.update(buf) + + decipher_iv_len = self._method_info[1] + if len(self.iv_buf) <= decipher_iv_len: + self.iv_buf += buf + if len(self.iv_buf) > decipher_iv_len: + decipher_iv = self.iv_buf[:decipher_iv_len] self.decipher = self.get_cipher(self.key, self.method, 0, iv=decipher_iv) - buf = buf[decipher_iv_len:] - if len(buf) == 0: - return buf - return self.decipher.update(buf) - + buf = self.iv_buf[decipher_iv_len:] + del self.iv_buf + return self.decipher.update(buf) + else: + return b'' def encrypt_all(password, method, op, data): result = [] @@ -149,6 +164,40 @@ def encrypt_all(password, method, op, data): result.append(cipher.update(data)) return b''.join(result) +def encrypt_key(password, method): + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + if key_len > 0: + key, _ = EVP_BytesToKey(password, key_len, iv_len) + else: + key = password + return key + +def encrypt_iv_len(method): + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + return iv_len + +def encrypt_new_iv(method): + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + return random_string(iv_len) + +def encrypt_all_iv(key, method, op, data, ref_iv): + result = [] + method = method.lower() + (key_len, iv_len, m) = method_supported[method] + if op: + iv = ref_iv[0] + result.append(iv) + else: + iv = data[:iv_len] + data = data[iv_len:] + ref_iv[0] = iv + cipher = m(method, key, iv, op) + result.append(cipher.update(data)) + return b''.join(result) + CIPHERS_TO_TEST = [ 'aes-128-cfb', diff --git a/shadowsocks/encrypt_test.py b/shadowsocks/encrypt_test.py new file mode 100644 index 00000000..d5e50778 --- /dev/null +++ b/shadowsocks/encrypt_test.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) + + +from shadowsocks.crypto import rc4_md5 +from shadowsocks.crypto import openssl +from shadowsocks.crypto import sodium +from shadowsocks.crypto import table + +def run(func): + try: + func() + except: + pass + +def run_n(func, name): + try: + func(name) + except: + pass + +def main(): + print("\n""rc4_md5") + rc4_md5.test() + print("\n""aes-256-cfb") + openssl.test_aes_256_cfb() + print("\n""aes-128-cfb") + openssl.test_aes_128_cfb() + print("\n""bf-cfb") + run(openssl.test_bf_cfb) + print("\n""camellia-128-cfb") + run_n(openssl.run_method, "camellia-128-cfb") + print("\n""cast5-cfb") + run_n(openssl.run_method, "cast5-cfb") + print("\n""idea-cfb") + run_n(openssl.run_method, "idea-cfb") + print("\n""seed-cfb") + run_n(openssl.run_method, "seed-cfb") + print("\n""salsa20") + run(sodium.test_salsa20) + print("\n""chacha20") + run(sodium.test_chacha20) + +if __name__ == '__main__': + main() + diff --git a/shadowsocks/eventloop.py b/shadowsocks/eventloop.py index 42f9205b..341620ef 100644 --- a/shadowsocks/eventloop.py +++ b/shadowsocks/eventloop.py @@ -22,6 +22,7 @@ with_statement import os +import time import socket import select import errno @@ -51,23 +52,8 @@ POLL_NVAL: 'POLL_NVAL', } - -class EpollLoop(object): - - def __init__(self): - self._epoll = select.epoll() - - def poll(self, timeout): - return self._epoll.poll(timeout) - - def add_fd(self, fd, mode): - self._epoll.register(fd, mode) - - def remove_fd(self, fd): - self._epoll.unregister(fd) - - def modify_fd(self, fd, mode): - self._epoll.modify(fd, mode) +# we check timeouts every TIMEOUT_PRECISION seconds +TIMEOUT_PRECISION = 2 class KqueueLoop(object): @@ -100,17 +86,20 @@ def poll(self, timeout): results[fd] |= POLL_OUT return results.items() - def add_fd(self, fd, mode): + def register(self, fd, mode): self._fds[fd] = mode self._control(fd, mode, select.KQ_EV_ADD) - def remove_fd(self, fd): + def unregister(self, fd): self._control(fd, self._fds[fd], select.KQ_EV_DELETE) del self._fds[fd] - def modify_fd(self, fd, mode): - self.remove_fd(fd) - self.add_fd(fd, mode) + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + self._kqueue.close() class SelectLoop(object): @@ -129,7 +118,7 @@ def poll(self, timeout): results[fd] |= p[1] return results.items() - def add_fd(self, fd, mode): + def register(self, fd, mode): if mode & POLL_IN: self._r_list.add(fd) if mode & POLL_OUT: @@ -137,7 +126,7 @@ def add_fd(self, fd, mode): if mode & POLL_ERR: self._x_list.add(fd) - def remove_fd(self, fd): + def unregister(self, fd): if fd in self._r_list: self._r_list.remove(fd) if fd in self._w_list: @@ -145,16 +134,18 @@ def remove_fd(self, fd): if fd in self._x_list: self._x_list.remove(fd) - def modify_fd(self, fd, mode): - self.remove_fd(fd) - self.add_fd(fd, mode) + def modify(self, fd, mode): + self.unregister(fd) + self.register(fd, mode) + + def close(self): + pass class EventLoop(object): def __init__(self): - self._iterating = False if hasattr(select, 'epoll'): - self._impl = EpollLoop() + self._impl = select.epoll() model = 'epoll' elif hasattr(select, 'kqueue'): self._impl = KqueueLoop() @@ -165,72 +156,81 @@ def __init__(self): else: raise Exception('can not find any available functions in select ' 'package') - self._fd_to_f = {} - self._handlers = [] - self._ref_handlers = [] - self._handlers_to_remove = [] + self._fdmap = {} # (f, handler) + self._last_time = time.time() + self._periodic_callbacks = [] + self._stopping = False logging.debug('using event model: %s', model) def poll(self, timeout=None): events = self._impl.poll(timeout) - return [(self._fd_to_f[fd], fd, event) for fd, event in events] + return [(self._fdmap[fd][0], fd, event) for fd, event in events] - def add(self, f, mode): + def add(self, f, mode, handler): fd = f.fileno() - self._fd_to_f[fd] = f - self._impl.add_fd(fd, mode) + self._fdmap[fd] = (f, handler) + self._impl.register(fd, mode) def remove(self, f): fd = f.fileno() - del self._fd_to_f[fd] - self._impl.remove_fd(fd) + del self._fdmap[fd] + self._impl.unregister(fd) + + def removefd(self, fd): + del self._fdmap[fd] + self._impl.unregister(fd) + + def add_periodic(self, callback): + self._periodic_callbacks.append(callback) + + def remove_periodic(self, callback): + self._periodic_callbacks.remove(callback) def modify(self, f, mode): fd = f.fileno() - self._impl.modify_fd(fd, mode) - - def add_handler(self, handler, ref=True): - self._handlers.append(handler) - if ref: - # when all ref handlers are removed, loop stops - self._ref_handlers.append(handler) - - def remove_handler(self, handler): - if handler in self._ref_handlers: - self._ref_handlers.remove(handler) - if self._iterating: - self._handlers_to_remove.append(handler) - else: - self._handlers.remove(handler) + self._impl.modify(fd, mode) + + def stop(self): + self._stopping = True def run(self): events = [] - while self._ref_handlers: + while not self._stopping: + asap = False try: - events = self.poll(1) + events = self.poll(TIMEOUT_PRECISION) except (OSError, IOError) as e: if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): # EPIPE: Happens when the client closes the connection # EINTR: Happens when received a signal # handles them as soon as possible + asap = True logging.debug('poll:%s', e) else: logging.error('poll:%s', e) import traceback traceback.print_exc() continue - self._iterating = True - for handler in self._handlers: - # TODO when there are a lot of handlers - try: - handler(events) - except (OSError, IOError) as e: - shell.print_exception(e) - if self._handlers_to_remove: - for handler in self._handlers_to_remove: - self._handlers.remove(handler) - self._handlers_to_remove = [] - self._iterating = False + + handle = False + for sock, fd, event in events: + handler = self._fdmap.get(fd, None) + if handler is not None: + handler = handler[1] + try: + handle = handler.handle_event(sock, fd, event) or handle + except (OSError, IOError) as e: + shell.print_exception(e) + now = time.time() + if asap or now - self._last_time >= TIMEOUT_PRECISION: + for callback in self._periodic_callbacks: + callback() + self._last_time = now + if events and not handle: + time.sleep(0.001) + + def __del__(self): + self._impl.close() # from tornado diff --git a/shadowsocks/local.py b/shadowsocks/local.py index 4255a2ee..9f54d930 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -23,7 +23,11 @@ import logging import signal -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +if __name__ == '__main__': + import inspect + file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))) + sys.path.insert(0, os.path.join(file_path, '../')) + from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns @@ -38,7 +42,12 @@ def main(): config = shell.get_config(True) + if not config.get('dns_ipv6', False): + asyncdns.IPV6_CONNECTION_SUPPORT = False + daemon.daemon_exec(config) + logging.info("local start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % + (config['protocol'], config['password'], config['method'], config['obfs'], config['obfs_param'])) try: logging.info("starting local at %s:%d" % diff --git a/shadowsocks/logrun.sh b/shadowsocks/logrun.sh new file mode 100755 index 00000000..fc081e1d --- /dev/null +++ b/shadowsocks/logrun.sh @@ -0,0 +1,8 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py a>> ssserver.log 2>&1 & + + diff --git a/shadowsocks/lru_cache.py b/shadowsocks/lru_cache.py index 401f19b5..ab0d2108 100644 --- a/shadowsocks/lru_cache.py +++ b/shadowsocks/lru_cache.py @@ -22,14 +22,24 @@ import logging import time +if __name__ == '__main__': + import os, sys, inspect + file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))) + sys.path.insert(0, os.path.join(file_path, '../')) + +try: + from collections import OrderedDict +except: + from shadowsocks.ordereddict import OrderedDict # this LRUCache is optimized for concurrency, not QPS # n: concurrency, keys stored in the cache # m: visits not timed out, proportional to QPS * timeout # get & set is O(1), not O(n). thus we can support very large n -# TODO: if timeout or QPS is too large, then this cache is not very efficient, -# as sweep() causes long pause +# sweep is O((n - m)) or O(1024) at most, +# no metter how large the cache or timeout value is +SWEEP_MAX_ITEMS = 1024 class LRUCache(collections.MutableMapping): """This class is not thread safe""" @@ -38,73 +48,92 @@ def __init__(self, timeout=60, close_callback=None, *args, **kwargs): self.timeout = timeout self.close_callback = close_callback self._store = {} - self._time_to_keys = collections.defaultdict(list) - self._keys_to_last_time = {} - self._last_visits = collections.deque() - self._closed_values = set() + self._keys_to_last_time = OrderedDict() self.update(dict(*args, **kwargs)) # use the free update to set keys def __getitem__(self, key): # O(1) t = time.time() + last_t = self._keys_to_last_time[key] + del self._keys_to_last_time[key] self._keys_to_last_time[key] = t - self._time_to_keys[t].append(key) - self._last_visits.append(t) return self._store[key] def __setitem__(self, key, value): # O(1) t = time.time() + if key in self._keys_to_last_time: + del self._keys_to_last_time[key] self._keys_to_last_time[key] = t self._store[key] = value - self._time_to_keys[t].append(key) - self._last_visits.append(t) def __delitem__(self, key): # O(1) + last_t = self._keys_to_last_time[key] del self._store[key] del self._keys_to_last_time[key] + def __contains__(self, key): + return key in self._store + def __iter__(self): return iter(self._store) def __len__(self): return len(self._store) - def sweep(self): - # O(m) + def first(self): + if len(self._keys_to_last_time) > 0: + for key in self._keys_to_last_time: + return key + + def sweep(self, sweep_item_cnt = SWEEP_MAX_ITEMS): + # O(n - m) now = time.time() c = 0 - while len(self._last_visits) > 0: - least = self._last_visits[0] - if now - least <= self.timeout: + while c < sweep_item_cnt: + if len(self._keys_to_last_time) == 0: + break + for key in self._keys_to_last_time: break + last_t = self._keys_to_last_time[key] + if now - last_t <= self.timeout: + break + value = self._store[key] + del self._store[key] + del self._keys_to_last_time[key] if self.close_callback is not None: - for key in self._time_to_keys[least]: - if key in self._store: - if now - self._keys_to_last_time[key] > self.timeout: - value = self._store[key] - if value not in self._closed_values: - self.close_callback(value) - self._closed_values.add(value) - for key in self._time_to_keys[least]: - self._last_visits.popleft() - if key in self._store: - if now - self._keys_to_last_time[key] > self.timeout: - del self._store[key] - del self._keys_to_last_time[key] - c += 1 - del self._time_to_keys[least] + self.close_callback(value) + c += 1 if c: - self._closed_values.clear() logging.debug('%d keys swept' % c) + return c < SWEEP_MAX_ITEMS + def clear(self, keep): + now = time.time() + c = 0 + while len(self._keys_to_last_time) > keep: + if len(self._keys_to_last_time) == 0: + break + for key in self._keys_to_last_time: + break + last_t = self._keys_to_last_time[key] + value = self._store[key] + if self.close_callback is not None: + self.close_callback(value) + del self._store[key] + del self._keys_to_last_time[key] + c += 1 + if c: + logging.debug('%d keys swept' % c) + return c < SWEEP_MAX_ITEMS def test(): c = LRUCache(timeout=0.3) c['a'] = 1 assert c['a'] == 1 + c['a'] = 1 time.sleep(0.5) c.sweep() diff --git a/shadowsocks/manager.py b/shadowsocks/manager.py new file mode 100644 index 00000000..80d0a320 --- /dev/null +++ b/shadowsocks/manager.py @@ -0,0 +1,291 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import errno +import traceback +import socket +import logging +import json +import collections + +from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell + + +BUF_SIZE = 1506 +STAT_SEND_LIMIT = 50 + + +class Manager(object): + + def __init__(self, config): + self._config = config + self._relays = {} # (tcprelay, udprelay) + self._loop = eventloop.EventLoop() + self._dns_resolver = asyncdns.DNSResolver() + self._dns_resolver.add_to_loop(self._loop) + + self._statistics = collections.defaultdict(int) + self._control_client_addr = None + try: + manager_address = common.to_str(config['manager_address']) + if ':' in manager_address: + addr = manager_address.rsplit(':', 1) + addr = addr[0], int(addr[1]) + addrs = socket.getaddrinfo(addr[0], addr[1]) + if addrs: + family = addrs[0][0] + else: + logging.error('invalid address: %s', manager_address) + exit(1) + else: + addr = manager_address + family = socket.AF_UNIX + self._control_socket = socket.socket(family, + socket.SOCK_DGRAM) + self._control_socket.bind(addr) + self._control_socket.setblocking(False) + except (OSError, IOError) as e: + logging.error(e) + logging.error('can not bind to manager address') + exit(1) + self._loop.add(self._control_socket, + eventloop.POLL_IN, self) + self._loop.add_periodic(self.handle_periodic) + + port_password = config['port_password'] + del config['port_password'] + for port, password in port_password.items(): + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password + self.add_port(a_config) + + def add_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.error("server already exists at %s:%d" % (config['server'], + port)) + return + logging.info("adding server at %s:%d" % (config['server'], port)) + t = tcprelay.TCPRelay(config, self._dns_resolver, False, + stat_callback=self.stat_callback) + u = udprelay.UDPRelay(config, self._dns_resolver, False, + stat_callback=self.stat_callback) + t.add_to_loop(self._loop) + u.add_to_loop(self._loop) + self._relays[port] = (t, u) + + def remove_port(self, config): + port = int(config['server_port']) + servers = self._relays.get(port, None) + if servers: + logging.info("removing server at %s:%d" % (config['server'], port)) + t, u = servers + t.close(next_tick=False) + u.close(next_tick=False) + del self._relays[port] + else: + logging.error("server not exist at %s:%d" % (config['server'], + port)) + + def handle_event(self, sock, fd, event): + if sock == self._control_socket and event == eventloop.POLL_IN: + data, self._control_client_addr = sock.recvfrom(BUF_SIZE) + parsed = self._parse_command(data) + if parsed: + command, config = parsed + a_config = self._config.copy() + if config: + # let the command override the configuration file + a_config.update(config) + if 'server_port' not in a_config: + logging.error('can not find server_port in config') + else: + if command == 'add': + self.add_port(a_config) + self._send_control_data(b'ok') + elif command == 'remove': + self.remove_port(a_config) + self._send_control_data(b'ok') + elif command == 'ping': + self._send_control_data(b'pong') + else: + logging.error('unknown command %s', command) + + def _parse_command(self, data): + # commands: + # add: {"server_port": 8000, "password": "foobar"} + # remove: {"server_port": 8000"} + data = common.to_str(data) + parts = data.split(':', 1) + if len(parts) < 2: + return data, None + command, config_json = parts + try: + config = shell.parse_json_in_str(config_json) + return command, config + except Exception as e: + logging.error(e) + return None + + def stat_callback(self, port, data_len): + self._statistics[port] += data_len + + def handle_periodic(self): + r = {} + i = 0 + + def send_data(data_dict): + if data_dict: + # use compact JSON format (without space) + data = common.to_bytes(json.dumps(data_dict, + separators=(',', ':'))) + self._send_control_data(b'stat: ' + data) + + for k, v in self._statistics.items(): + r[k] = v + i += 1 + # split the data into segments that fit in UDP packets + if i >= STAT_SEND_LIMIT: + send_data(r) + r.clear() + i = 0 + if len(r) > 0 : + send_data(r) + self._statistics.clear() + + def _send_control_data(self, data): + if self._control_client_addr: + try: + self._control_socket.sendto(data, self._control_client_addr) + except (socket.error, OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + + def run(self): + self._loop.run() + + +def run(config): + Manager(config).run() + + +def test(): + import time + import threading + import struct + from shadowsocks import encrypt + + logging.basicConfig(level=5, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + enc = [] + eventloop.TIMEOUT_PRECISION = 1 + + def run_server(): + config = shell.get_config(True) + config = config.copy() + a_config = { + 'server': '127.0.0.1', + 'local_port': 1081, + 'port_password': { + '8381': 'foobar1', + '8382': 'foobar2' + }, + 'method': 'aes-256-cfb', + 'manager_address': '127.0.0.1:6001', + 'timeout': 60, + 'fast_open': False, + 'verbose': 2 + } + config.update(a_config) + manager = Manager(config) + enc.append(manager) + manager.run() + + t = threading.Thread(target=run_server) + t.start() + time.sleep(1) + manager = enc[0] + cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cli.connect(('127.0.0.1', 6001)) + + # test add and remove + time.sleep(1) + cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') + time.sleep(1) + assert 7001 in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + + cli.send(b'remove: {"server_port":8381}') + time.sleep(1) + assert 8381 not in manager._relays + data, addr = cli.recvfrom(1506) + assert b'ok' in data + logging.info('add and remove test passed') + + # test statistics for TCP + header = common.pack_addr(b'google.com') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, + header + b'GET /\r\n\r\n') + tcp_cli = socket.socket() + tcp_cli.connect(('127.0.0.1', 7001)) + tcp_cli.send(data) + tcp_cli.recv(4096) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = shell.parse_json_in_str(data) + assert '7001' in stats + logging.info('TCP statistics test passed') + + # test statistics for UDP + header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) + data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, + header + b'test') + udp_cli = socket.socket(type=socket.SOCK_DGRAM) + udp_cli.sendto(data, ('127.0.0.1', 8382)) + tcp_cli.close() + + data, addr = cli.recvfrom(1506) + data = common.to_str(data) + assert data.startswith('stat: ') + data = data.split('stat:')[1] + stats = json.loads(data) + assert '8382' in stats + logging.info('UDP statistics test passed') + + manager._loop.stop() + t.join() + + +if __name__ == '__main__': + test() diff --git a/shadowsocks/obfs.py b/shadowsocks/obfs.py new file mode 100644 index 00000000..3dfdb141 --- /dev/null +++ b/shadowsocks/obfs.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# +# Copyright 2015-2015 breakwa11 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import hashlib +import logging + +from shadowsocks import common +from shadowsocks.obfsplugin import plain, http_simple, obfs_tls, verify, auth, auth_chain + + +method_supported = {} +method_supported.update(plain.obfs_map) +method_supported.update(http_simple.obfs_map) +method_supported.update(obfs_tls.obfs_map) +method_supported.update(verify.obfs_map) +method_supported.update(auth.obfs_map) +method_supported.update(auth_chain.obfs_map) + +def mu_protocol(): + return ["auth_aes128_md5", "auth_aes128_sha1", "auth_chain_a"] + +class server_info(object): + def __init__(self, data): + self.data = data + +class obfs(object): + def __init__(self, method): + method = common.to_str(method) + self.method = method + self._method_info = self.get_method_info(method) + if self._method_info: + self.obfs = self.get_obfs(method) + else: + raise Exception('obfs plugin [%s] not supported' % method) + + def init_data(self): + return self.obfs.init_data() + + def set_server_info(self, server_info): + return self.obfs.set_server_info(server_info) + + def get_server_info(self): + return self.obfs.get_server_info() + + def get_method_info(self, method): + method = method.lower() + m = method_supported.get(method) + return m + + def get_obfs(self, method): + m = self._method_info + return m[0](method) + + def get_overhead(self, direction): + return self.obfs.get_overhead(direction) + + def client_pre_encrypt(self, buf): + return self.obfs.client_pre_encrypt(buf) + + def client_encode(self, buf): + return self.obfs.client_encode(buf) + + def client_decode(self, buf): + return self.obfs.client_decode(buf) + + def client_post_decrypt(self, buf): + return self.obfs.client_post_decrypt(buf) + + def server_pre_encrypt(self, buf): + return self.obfs.server_pre_encrypt(buf) + + def server_encode(self, buf): + return self.obfs.server_encode(buf) + + def server_decode(self, buf): + return self.obfs.server_decode(buf) + + def server_post_decrypt(self, buf): + return self.obfs.server_post_decrypt(buf) + + def client_udp_pre_encrypt(self, buf): + return self.obfs.client_udp_pre_encrypt(buf) + + def client_udp_post_decrypt(self, buf): + return self.obfs.client_udp_post_decrypt(buf) + + def server_udp_pre_encrypt(self, buf, uid): + return self.obfs.server_udp_pre_encrypt(buf, uid) + + def server_udp_post_decrypt(self, buf): + return self.obfs.server_udp_post_decrypt(buf) + + def dispose(self): + self.obfs.dispose() + del self.obfs + diff --git a/shadowsocks/obfsplugin/__init__.py b/shadowsocks/obfsplugin/__init__.py new file mode 100644 index 00000000..401c7b72 --- /dev/null +++ b/shadowsocks/obfsplugin/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# 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. + +from __future__ import absolute_import, division, print_function, \ + with_statement diff --git a/shadowsocks/obfsplugin/auth.py b/shadowsocks/obfsplugin/auth.py new file mode 100755 index 00000000..a745e098 --- /dev/null +++ b/shadowsocks/obfsplugin/auth.py @@ -0,0 +1,787 @@ +#!/usr/bin/env python +# +# Copyright 2015-2015 breakwa11 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import hashlib +import logging +import binascii +import base64 +import time +import datetime +import random +import math +import struct +import zlib +import hmac +import hashlib + +import shadowsocks +from shadowsocks import common, lru_cache, encrypt +from shadowsocks.obfsplugin import plain +from shadowsocks.common import to_bytes, to_str, ord, chr + +def create_auth_sha1_v4(method): + return auth_sha1_v4(method) + +def create_auth_aes128_md5(method): + return auth_aes128_sha1(method, hashlib.md5) + +def create_auth_aes128_sha1(method): + return auth_aes128_sha1(method, hashlib.sha1) + +obfs_map = { + 'auth_sha1_v4': (create_auth_sha1_v4,), + 'auth_sha1_v4_compatible': (create_auth_sha1_v4,), + 'auth_aes128_md5': (create_auth_aes128_md5,), + 'auth_aes128_sha1': (create_auth_aes128_sha1,), +} + +def match_begin(str1, str2): + if len(str1) >= len(str2): + if str1[:len(str2)] == str2: + return True + return False + +class auth_base(plain.plain): + def __init__(self, method): + super(auth_base, self).__init__(method) + self.method = method + self.no_compatible_method = '' + self.overhead = 7 + + def init_data(self): + return '' + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return self.overhead + + def set_server_info(self, server_info): + self.server_info = server_info + + def client_encode(self, buf): + return buf + + def client_decode(self, buf): + return (buf, False) + + def server_encode(self, buf): + return buf + + def server_decode(self, buf): + return (buf, True, False) + + def not_match_return(self, buf): + self.raw_trans = True + self.overhead = 0 + if self.method == self.no_compatible_method: + return (b'E'*2048, False) + return (buf, False) + +class client_queue(object): + def __init__(self, begin_id): + self.front = begin_id - 64 + self.back = begin_id + 1 + self.alloc = {} + self.enable = True + self.last_update = time.time() + + def update(self): + self.last_update = time.time() + + def is_active(self): + return time.time() - self.last_update < 60 * 3 + + def re_enable(self, connection_id): + self.enable = True + self.front = connection_id - 64 + self.back = connection_id + 1 + self.alloc = {} + + def insert(self, connection_id): + if not self.enable: + logging.warn('obfs auth: not enable') + return False + if not self.is_active(): + self.re_enable(connection_id) + self.update() + if connection_id < self.front: + logging.warn('obfs auth: deprecated id, someone replay attack') + return False + if connection_id > self.front + 0x4000: + logging.warn('obfs auth: wrong id') + return False + if connection_id in self.alloc: + logging.warn('obfs auth: duplicate id, someone replay attack') + return False + if self.back <= connection_id: + self.back = connection_id + 1 + self.alloc[connection_id] = 1 + while (self.front in self.alloc) or self.front + 0x1000 < self.back: + if self.front in self.alloc: + del self.alloc[self.front] + self.front += 1 + return True + +class obfs_auth_v2_data(object): + def __init__(self): + self.client_id = lru_cache.LRUCache() + self.local_client_id = b'' + self.connection_id = 0 + self.set_max_client(64) # max active client count + + def update(self, client_id, connection_id): + if client_id in self.client_id: + self.client_id[client_id].update() + + def set_max_client(self, max_client): + self.max_client = max_client + self.max_buffer = max(self.max_client * 2, 1024) + + def insert(self, client_id, connection_id): + if self.client_id.get(client_id, None) is None or not self.client_id[client_id].enable: + if self.client_id.first() is None or len(self.client_id) < self.max_client: + if client_id not in self.client_id: + #TODO: check + self.client_id[client_id] = client_queue(connection_id) + else: + self.client_id[client_id].re_enable(connection_id) + return self.client_id[client_id].insert(connection_id) + + if not self.client_id[self.client_id.first()].is_active(): + del self.client_id[self.client_id.first()] + if client_id not in self.client_id: + #TODO: check + self.client_id[client_id] = client_queue(connection_id) + else: + self.client_id[client_id].re_enable(connection_id) + return self.client_id[client_id].insert(connection_id) + + logging.warn('auth_sha1_v2: no inactive client') + return False + else: + return self.client_id[client_id].insert(connection_id) + +class auth_sha1_v4(auth_base): + def __init__(self, method): + super(auth_sha1_v4, self).__init__(method) + self.recv_buf = b'' + self.unit_len = 8100 + self.decrypt_packet_num = 0 + self.raw_trans = False + self.has_sent_header = False + self.has_recv_header = False + self.client_id = 0 + self.connection_id = 0 + self.max_time_dif = 60 * 60 * 24 # time dif (second) setting + self.salt = b"auth_sha1_v4" + self.no_compatible_method = 'auth_sha1_v4' + + def init_data(self): + return obfs_auth_v2_data() + + def set_server_info(self, server_info): + self.server_info = server_info + try: + max_client = int(server_info.protocol_param) + except: + max_client = 64 + self.server_info.data.set_max_client(max_client) + + def rnd_data(self, buf_size): + if buf_size > 1200: + return b'\x01' + + if buf_size > 400: + rnd_data = os.urandom(common.ord(os.urandom(1)[0]) % 256) + else: + rnd_data = os.urandom(struct.unpack('>H', os.urandom(2))[0] % 512) + + if len(rnd_data) < 128: + return common.chr(len(rnd_data) + 1) + rnd_data + else: + return common.chr(255) + struct.pack('>H', len(rnd_data) + 3) + rnd_data + + def pack_data(self, buf): + data = self.rnd_data(len(buf)) + buf + data_len = len(data) + 8 + crc = binascii.crc32(struct.pack('>H', data_len)) & 0xFFFF + data = struct.pack('H', data_len) + data + adler32 = zlib.adler32(data) & 0xFFFFFFFF + data += struct.pack('H', data_len) + self.salt + self.server_info.key) & 0xFFFFFFFF + data = struct.pack('H', data_len) + data + data += hmac.new(self.server_info.iv + self.server_info.key, data, hashlib.sha1).digest()[:10] + return data + + def auth_data(self): + utc_time = int(time.time()) & 0xFFFFFFFF + if self.server_info.data.connection_id > 0xFF000000: + self.server_info.data.local_client_id = b'' + if not self.server_info.data.local_client_id: + self.server_info.data.local_client_id = os.urandom(4) + logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),)) + self.server_info.data.connection_id = struct.unpack(' self.unit_len: + ret += self.pack_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_data(buf) + return ret + + def client_post_decrypt(self, buf): + if self.raw_trans: + return buf + self.recv_buf += buf + out_buf = b'' + while len(self.recv_buf) > 4: + crc = struct.pack('H', self.recv_buf[:2])[0] + if length >= 8192 or length < 7: + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data error') + if length > len(self.recv_buf): + break + + if struct.pack('H', self.recv_buf[5:7])[0] + 4 + out_buf += self.recv_buf[pos:length - 4] + self.recv_buf = self.recv_buf[length:] + + if out_buf: + self.decrypt_packet_num += 1 + return out_buf + + def server_pre_encrypt(self, buf): + if self.raw_trans: + return buf + ret = b'' + while len(buf) > self.unit_len: + ret += self.pack_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_data(buf) + return ret + + def server_post_decrypt(self, buf): + if self.raw_trans: + return (buf, False) + self.recv_buf += buf + out_buf = b'' + sendback = False + + if not self.has_recv_header: + if len(self.recv_buf) <= 6: + return (b'', False) + crc = struct.pack('H', self.recv_buf[:2])[0] + if length > len(self.recv_buf): + return (b'', False) + sha1data = hmac.new(self.server_info.recv_iv + self.server_info.key, self.recv_buf[:length - 10], hashlib.sha1).digest()[:10] + if sha1data != self.recv_buf[length - 10:length]: + logging.error('auth_sha1_v4 data uncorrect auth HMAC-SHA1') + return self.not_match_return(self.recv_buf) + pos = common.ord(self.recv_buf[6]) + if pos < 255: + pos += 6 + else: + pos = struct.unpack('>H', self.recv_buf[7:9])[0] + 6 + out_buf = self.recv_buf[pos:length - 10] + if len(out_buf) < 12: + logging.info('auth_sha1_v4: too short, data %s' % (binascii.hexlify(self.recv_buf),)) + return self.not_match_return(self.recv_buf) + utc_time = struct.unpack(' self.max_time_dif: + logging.info('auth_sha1_v4: wrong timestamp, time_dif %d, data %s' % (time_dif, binascii.hexlify(out_buf),)) + return self.not_match_return(self.recv_buf) + elif self.server_info.data.insert(client_id, connection_id): + self.has_recv_header = True + out_buf = out_buf[12:] + self.client_id = client_id + self.connection_id = connection_id + else: + logging.info('auth_sha1_v4: auth fail, data %s' % (binascii.hexlify(out_buf),)) + return self.not_match_return(self.recv_buf) + self.recv_buf = self.recv_buf[length:] + self.has_recv_header = True + sendback = True + + while len(self.recv_buf) > 4: + crc = struct.pack('H', self.recv_buf[:2])[0] + if length >= 8192 or length < 7: + self.raw_trans = True + self.recv_buf = b'' + if self.decrypt_packet_num == 0: + logging.info('auth_sha1_v4: over size') + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data error') + if length > len(self.recv_buf): + break + + if struct.pack('H', self.recv_buf[5:7])[0] + 4 + out_buf += self.recv_buf[pos:length - 4] + self.recv_buf = self.recv_buf[length:] + if pos == length - 4: + sendback = True + + if out_buf: + self.server_info.data.update(self.client_id, self.connection_id) + self.decrypt_packet_num += 1 + return (out_buf, sendback) + +class obfs_auth_mu_data(object): + def __init__(self): + self.user_id = {} + self.local_client_id = b'' + self.connection_id = 0 + self.set_max_client(64) # max active client count + + def update(self, user_id, client_id, connection_id): + if user_id not in self.user_id: + self.user_id[user_id] = lru_cache.LRUCache() + local_client_id = self.user_id[user_id] + + if client_id in local_client_id: + local_client_id[client_id].update() + + def set_max_client(self, max_client): + self.max_client = max_client + self.max_buffer = max(self.max_client * 2, 1024) + + def insert(self, user_id, client_id, connection_id): + if user_id not in self.user_id: + self.user_id[user_id] = lru_cache.LRUCache() + local_client_id = self.user_id[user_id] + + if local_client_id.get(client_id, None) is None or not local_client_id[client_id].enable: + if local_client_id.first() is None or len(local_client_id) < self.max_client: + if client_id not in local_client_id: + #TODO: check + local_client_id[client_id] = client_queue(connection_id) + else: + local_client_id[client_id].re_enable(connection_id) + return local_client_id[client_id].insert(connection_id) + + if not local_client_id[local_client_id.first()].is_active(): + del local_client_id[local_client_id.first()] + if client_id not in local_client_id: + #TODO: check + local_client_id[client_id] = client_queue(connection_id) + else: + local_client_id[client_id].re_enable(connection_id) + return local_client_id[client_id].insert(connection_id) + + logging.warn('auth_aes128: no inactive client') + return False + else: + return local_client_id[client_id].insert(connection_id) + +class auth_aes128_sha1(auth_base): + def __init__(self, method, hashfunc): + super(auth_aes128_sha1, self).__init__(method) + self.hashfunc = hashfunc + self.recv_buf = b'' + self.unit_len = 8100 + self.raw_trans = False + self.has_sent_header = False + self.has_recv_header = False + self.client_id = 0 + self.connection_id = 0 + self.max_time_dif = 60 * 60 * 24 # time dif (second) setting + self.salt = hashfunc == hashlib.md5 and b"auth_aes128_md5" or b"auth_aes128_sha1" + self.no_compatible_method = hashfunc == hashlib.md5 and "auth_aes128_md5" or 'auth_aes128_sha1' + self.extra_wait_size = struct.unpack('>H', os.urandom(2))[0] % 1024 + self.pack_id = 1 + self.recv_id = 1 + self.user_id = None + self.user_key = None + self.last_rnd_len = 0 + self.overhead = 9 + + def init_data(self): + return obfs_auth_mu_data() + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return self.overhead + + def set_server_info(self, server_info): + self.server_info = server_info + try: + max_client = int(server_info.protocol_param.split('#')[0]) + except: + max_client = 64 + self.server_info.data.set_max_client(max_client) + + def trapezoid_random_float(self, d): + if d == 0: + return random.random() + s = random.random() + a = 1 - d + return (math.sqrt(a * a + 4 * d * s) - a) / (2 * d) + + def trapezoid_random_int(self, max_val, d): + v = self.trapezoid_random_float(d) + return int(v * max_val) + + def rnd_data_len(self, buf_size, full_buf_size): + if full_buf_size >= self.server_info.buffer_size: + return 0 + tcp_mss = self.server_info.tcp_mss + rev_len = tcp_mss - buf_size - 9 + if rev_len == 0: + return 0 + if rev_len < 0: + if rev_len > -tcp_mss: + return self.trapezoid_random_int(rev_len + tcp_mss, -0.3) + return common.ord(os.urandom(1)[0]) % 32 + if buf_size > 900: + return struct.unpack('>H', os.urandom(2))[0] % rev_len + return self.trapezoid_random_int(rev_len, -0.3) + + def rnd_data(self, buf_size, full_buf_size): + data_len = self.rnd_data_len(buf_size, full_buf_size) + + if data_len < 128: + return common.chr(data_len + 1) + os.urandom(data_len) + + return common.chr(255) + struct.pack(' 400: + rnd_len = struct.unpack(' 0xFF000000: + self.server_info.data.local_client_id = b'' + if not self.server_info.data.local_client_id: + self.server_info.data.local_client_id = os.urandom(4) + logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),)) + self.server_info.data.connection_id = struct.unpack(' self.unit_len: + ret += self.pack_data(buf[:self.unit_len], ogn_data_len) + buf = buf[self.unit_len:] + ret += self.pack_data(buf, ogn_data_len) + self.last_rnd_len = ogn_data_len + return ret + + def client_post_decrypt(self, buf): + if self.raw_trans: + return buf + self.recv_buf += buf + out_buf = b'' + while len(self.recv_buf) > 4: + mac_key = self.user_key + struct.pack('= 8192 or length < 7: + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data error') + if length > len(self.recv_buf): + break + + if hmac.new(mac_key, self.recv_buf[:length - 4], self.hashfunc).digest()[:4] != self.recv_buf[length - 4:length]: + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data uncorrect checksum') + + self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF + pos = common.ord(self.recv_buf[4]) + if pos < 255: + pos += 4 + else: + pos = struct.unpack(' self.unit_len: + ret += self.pack_data(buf[:self.unit_len], ogn_data_len) + buf = buf[self.unit_len:] + ret += self.pack_data(buf, ogn_data_len) + self.last_rnd_len = ogn_data_len + return ret + + def server_post_decrypt(self, buf): + if self.raw_trans: + return (buf, False) + self.recv_buf += buf + out_buf = b'' + sendback = False + + if not self.has_recv_header: + if len(self.recv_buf) >= 7 or len(self.recv_buf) in [2, 3]: + recv_len = min(len(self.recv_buf), 7) + mac_key = self.server_info.recv_iv + self.server_info.key + sha1data = hmac.new(mac_key, self.recv_buf[:1], self.hashfunc).digest()[:recv_len - 1] + if sha1data != self.recv_buf[1:recv_len]: + return self.not_match_return(self.recv_buf) + + if len(self.recv_buf) < 31: + return (b'', False) + sha1data = hmac.new(mac_key, self.recv_buf[7:27], self.hashfunc).digest()[:4] + if sha1data != self.recv_buf[27:31]: + logging.error('%s data uncorrect auth HMAC-SHA1 from %s:%d, data %s' % (self.no_compatible_method, self.server_info.client, self.server_info.client_port, binascii.hexlify(self.recv_buf))) + if len(self.recv_buf) < 31 + self.extra_wait_size: + return (b'', False) + return self.not_match_return(self.recv_buf) + + uid = self.recv_buf[7:11] + if uid in self.server_info.users: + self.user_id = uid + self.user_key = self.hashfunc(self.server_info.users[uid]).digest() + self.server_info.update_user_func(uid) + else: + if not self.server_info.users: + self.user_key = self.server_info.key + else: + self.user_key = self.server_info.recv_iv + encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + self.salt, 'aes-128-cbc') + head = encryptor.decrypt(b'\x00' * 16 + self.recv_buf[11:27] + b'\x00') # need an extra byte or recv empty + length = struct.unpack(' self.max_time_dif: + logging.info('%s: wrong timestamp, time_dif %d, data %s' % (self.no_compatible_method, time_dif, binascii.hexlify(head))) + return self.not_match_return(self.recv_buf) + elif self.server_info.data.insert(self.user_id, client_id, connection_id): + self.has_recv_header = True + out_buf = self.recv_buf[31 + rnd_len:length - 4] + self.client_id = client_id + self.connection_id = connection_id + else: + logging.info('%s: auth fail, data %s' % (self.no_compatible_method, binascii.hexlify(out_buf))) + return self.not_match_return(self.recv_buf) + self.recv_buf = self.recv_buf[length:] + self.has_recv_header = True + sendback = True + + while len(self.recv_buf) > 4: + mac_key = self.user_key + struct.pack('= 8192 or length < 7: + self.raw_trans = True + self.recv_buf = b'' + if self.recv_id == 0: + logging.info(self.no_compatible_method + ': over size') + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data error') + if length > len(self.recv_buf): + break + + if hmac.new(mac_key, self.recv_buf[:length - 4], self.hashfunc).digest()[:4] != self.recv_buf[length - 4:length]: + logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length]))) + self.raw_trans = True + self.recv_buf = b'' + if self.recv_id == 0: + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data uncorrect checksum') + + self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF + pos = common.ord(self.recv_buf[4]) + if pos < 255: + pos += 4 + else: + pos = struct.unpack('> 17) ^ (y >> 26)) & xorshift128plus.max_int + self.v1 = x + return (x + y) & xorshift128plus.max_int + + def init_from_bin(self, bin): + bin += b'\0' * 16 + self.v0 = struct.unpack('= len(str2): + if str1[:len(str2)] == str2: + return True + return False + +class auth_base(plain.plain): + def __init__(self, method): + super(auth_base, self).__init__(method) + self.method = method + self.no_compatible_method = '' + self.overhead = 4 + + def init_data(self): + return '' + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return self.overhead + + def set_server_info(self, server_info): + self.server_info = server_info + + def client_encode(self, buf): + return buf + + def client_decode(self, buf): + return (buf, False) + + def server_encode(self, buf): + return buf + + def server_decode(self, buf): + return (buf, True, False) + + def not_match_return(self, buf): + self.raw_trans = True + self.overhead = 0 + if self.method == self.no_compatible_method: + return (b'E'*2048, False) + return (buf, False) + +class client_queue(object): + def __init__(self, begin_id): + self.front = begin_id - 64 + self.back = begin_id + 1 + self.alloc = {} + self.enable = True + self.last_update = time.time() + self.ref = 0 + + def update(self): + self.last_update = time.time() + + def addref(self): + self.ref += 1 + + def delref(self): + if self.ref > 0: + self.ref -= 1 + + def is_active(self): + return (self.ref > 0) and (time.time() - self.last_update < 60 * 10) + + def re_enable(self, connection_id): + self.enable = True + self.front = connection_id - 64 + self.back = connection_id + 1 + self.alloc = {} + + def insert(self, connection_id): + if not self.enable: + logging.warn('obfs auth: not enable') + return False + if not self.is_active(): + self.re_enable(connection_id) + self.update() + if connection_id < self.front: + logging.warn('obfs auth: deprecated id, someone replay attack') + return False + if connection_id > self.front + 0x4000: + logging.warn('obfs auth: wrong id') + return False + if connection_id in self.alloc: + logging.warn('obfs auth: duplicate id, someone replay attack') + return False + if self.back <= connection_id: + self.back = connection_id + 1 + self.alloc[connection_id] = 1 + while (self.front in self.alloc) or self.front + 0x1000 < self.back: + if self.front in self.alloc: + del self.alloc[self.front] + self.front += 1 + self.addref() + return True + +class obfs_auth_chain_data(object): + def __init__(self, name): + self.name = name + self.user_id = {} + self.local_client_id = b'' + self.connection_id = 0 + self.set_max_client(64) # max active client count + + def update(self, user_id, client_id, connection_id): + if user_id not in self.user_id: + self.user_id[user_id] = lru_cache.LRUCache() + local_client_id = self.user_id[user_id] + + if client_id in local_client_id: + local_client_id[client_id].update() + + def set_max_client(self, max_client): + self.max_client = max_client + self.max_buffer = max(self.max_client * 2, 1024) + + def insert(self, user_id, client_id, connection_id): + if user_id not in self.user_id: + self.user_id[user_id] = lru_cache.LRUCache() + local_client_id = self.user_id[user_id] + + if local_client_id.get(client_id, None) is None or not local_client_id[client_id].enable: + if local_client_id.first() is None or len(local_client_id) < self.max_client: + if client_id not in local_client_id: + #TODO: check + local_client_id[client_id] = client_queue(connection_id) + else: + local_client_id[client_id].re_enable(connection_id) + return local_client_id[client_id].insert(connection_id) + + if not local_client_id[local_client_id.first()].is_active(): + del local_client_id[local_client_id.first()] + if client_id not in local_client_id: + #TODO: check + local_client_id[client_id] = client_queue(connection_id) + else: + local_client_id[client_id].re_enable(connection_id) + return local_client_id[client_id].insert(connection_id) + + logging.warn(self.name + ': no inactive client') + return False + else: + return local_client_id[client_id].insert(connection_id) + + def remove(self, user_id, client_id): + if user_id in self.user_id: + local_client_id = self.user_id[user_id] + if client_id in local_client_id: + local_client_id[client_id].delref() + +class auth_chain_a(auth_base): + def __init__(self, method): + super(auth_chain_a, self).__init__(method) + self.hashfunc = hashlib.md5 + self.recv_buf = b'' + self.unit_len = 2800 + self.raw_trans = False + self.has_sent_header = False + self.has_recv_header = False + self.client_id = 0 + self.connection_id = 0 + self.max_time_dif = 60 * 60 * 24 # time dif (second) setting + self.salt = b"auth_chain_a" + self.no_compatible_method = 'auth_chain_a' + self.pack_id = 1 + self.recv_id = 1 + self.user_id = None + self.user_id_num = 0 + self.user_key = None + self.overhead = 4 + self.client_over_head = 4 + self.last_client_hash = b'' + self.last_server_hash = b'' + self.random_client = xorshift128plus() + self.random_server = xorshift128plus() + self.encryptor = None + + def init_data(self): + return obfs_auth_chain_data(self.method) + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return self.overhead + + def set_server_info(self, server_info): + self.server_info = server_info + try: + max_client = int(server_info.protocol_param.split('#')[0]) + except: + max_client = 64 + self.server_info.data.set_max_client(max_client) + + def trapezoid_random_float(self, d): + if d == 0: + return random.random() + s = random.random() + a = 1 - d + return (math.sqrt(a * a + 4 * d * s) - a) / (2 * d) + + def trapezoid_random_int(self, max_val, d): + v = self.trapezoid_random_float(d) + return int(v * max_val) + + def rnd_data_len(self, buf_size, last_hash, random): + if buf_size > 1440: + return 0 + random.init_from_bin_len(last_hash, buf_size) + if buf_size > 1300: + return random.next() % 31 + if buf_size > 900: + return random.next() % 127 + if buf_size > 400: + return random.next() % 521 + return random.next() % 1021 + + def udp_rnd_data_len(self, last_hash, random): + random.init_from_bin(last_hash) + return random.next() % 127 + + def rnd_start_pos(self, rand_len, random): + if rand_len > 0: + return random.next() % 8589934609 % rand_len + return 0 + + def rnd_data(self, buf_size, buf, last_hash, random): + rand_len = self.rnd_data_len(buf_size, last_hash, random) + + rnd_data_buf = os.urandom(rand_len) + + if buf_size == 0: + return rnd_data_buf + else: + if rand_len > 0: + start_pos = self.rnd_start_pos(rand_len, random) + return rnd_data_buf[:start_pos] + buf + rnd_data_buf[start_pos:] + else: + return buf + + def pack_client_data(self, buf): + buf = self.encryptor.encrypt(buf) + data = self.rnd_data(len(buf), buf, self.last_client_hash, self.random_client) + data_len = len(data) + 8 + mac_key = self.user_key + struct.pack(' 0xFF000000: + self.server_info.data.local_client_id = b'' + if not self.server_info.data.local_client_id: + self.server_info.data.local_client_id = os.urandom(4) + logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),)) + self.server_info.data.connection_id = struct.unpack(' self.unit_len: + ret += self.pack_client_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_client_data(buf) + return ret + + def client_post_decrypt(self, buf): + if self.raw_trans: + return buf + self.recv_buf += buf + out_buf = b'' + while len(self.recv_buf) > 4: + mac_key = self.user_key + struct.pack('= 4096: + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data error') + + if length + 4 > len(self.recv_buf): + break + + server_hash = hmac.new(mac_key, self.recv_buf[:length + 2], self.hashfunc).digest() + if server_hash[:2] != self.recv_buf[length + 2 : length + 4]: + logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length]))) + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data uncorrect checksum') + + pos = 2 + if data_len > 0 and rand_len > 0: + pos = 2 + self.rnd_start_pos(rand_len, self.random_server) + out_buf += self.encryptor.decrypt(self.recv_buf[pos : data_len + pos]) + self.last_server_hash = server_hash + if self.recv_id == 1: + self.server_info.tcp_mss = struct.unpack(' self.unit_len: + ret += self.pack_server_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_server_data(buf) + return ret + + def server_post_decrypt(self, buf): + if self.raw_trans: + return (buf, False) + self.recv_buf += buf + out_buf = b'' + sendback = False + + if not self.has_recv_header: + if len(self.recv_buf) >= 12 or len(self.recv_buf) in [7, 8]: + recv_len = min(len(self.recv_buf), 12) + mac_key = self.server_info.recv_iv + self.server_info.key + md5data = hmac.new(mac_key, self.recv_buf[:4], self.hashfunc).digest() + if md5data[:recv_len - 4] != self.recv_buf[4:recv_len]: + return self.not_match_return(self.recv_buf) + + if len(self.recv_buf) < 12 + 24: + return (b'', False) + + self.last_client_hash = md5data + uid = struct.unpack(' self.max_time_dif: + logging.info('%s: wrong timestamp, time_dif %d, data %s' % (self.no_compatible_method, time_dif, binascii.hexlify(head))) + return self.not_match_return(self.recv_buf) + elif self.server_info.data.insert(self.user_id, client_id, connection_id): + self.has_recv_header = True + self.client_id = client_id + self.connection_id = connection_id + else: + logging.info('%s: auth fail, data %s' % (self.no_compatible_method, binascii.hexlify(out_buf))) + return self.not_match_return(self.recv_buf) + + self.encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + to_bytes(base64.b64encode(self.last_client_hash)), 'rc4') + self.recv_buf = self.recv_buf[36:] + self.has_recv_header = True + sendback = True + + while len(self.recv_buf) > 4: + mac_key = self.user_key + struct.pack('= 4096: + self.raw_trans = True + self.recv_buf = b'' + if self.recv_id == 0: + logging.info(self.no_compatible_method + ': over size') + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data error') + + if length + 4 > len(self.recv_buf): + break + + client_hash = hmac.new(mac_key, self.recv_buf[:length + 2], self.hashfunc).digest() + if client_hash[:2] != self.recv_buf[length + 2 : length + 4]: + logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length]))) + self.raw_trans = True + self.recv_buf = b'' + if self.recv_id == 0: + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data uncorrect checksum') + + self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF + pos = 2 + if data_len > 0 and rand_len > 0: + pos = 2 + self.rnd_start_pos(rand_len, self.random_client) + out_buf += self.encryptor.decrypt(self.recv_buf[pos : data_len + pos]) + self.last_client_hash = client_hash + self.recv_buf = self.recv_buf[length + 4:] + if data_len == 0: + sendback = True + + if out_buf: + self.server_info.data.update(self.user_id, self.client_id, self.connection_id) + return (out_buf, sendback) + + def client_udp_pre_encrypt(self, buf): + if self.user_key is None: + if b':' in to_bytes(self.server_info.protocol_param): + try: + items = to_bytes(self.server_info.protocol_param).split(':') + self.user_key = self.hashfunc(items[1]).digest() + self.user_id = struct.pack('= 1440: + return 0 + random.init_from_bin_len(last_hash, buf_size) + pos = bisect.bisect_left(self.data_size_list, buf_size + self.server_info.overhead) + final_pos = pos + random.next() % (len(self.data_size_list)) + if final_pos < len(self.data_size_list): + return self.data_size_list[final_pos] - buf_size - self.server_info.overhead + + pos = bisect.bisect_left(self.data_size_list2, buf_size + self.server_info.overhead) + final_pos = pos + random.next() % (len(self.data_size_list2)) + if final_pos < len(self.data_size_list2): + return self.data_size_list2[final_pos] - buf_size - self.server_info.overhead + if final_pos < pos + len(self.data_size_list2) - 1: + return 0 + + if buf_size > 1300: + return random.next() % 31 + if buf_size > 900: + return random.next() % 127 + if buf_size > 400: + return random.next() % 521 + return random.next() % 1021 + diff --git a/shadowsocks/obfsplugin/http_simple.py b/shadowsocks/obfsplugin/http_simple.py new file mode 100644 index 00000000..6f1a05e4 --- /dev/null +++ b/shadowsocks/obfsplugin/http_simple.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python +# +# Copyright 2015-2015 breakwa11 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import hashlib +import logging +import binascii +import struct +import base64 +import datetime +import random + +from shadowsocks import common +from shadowsocks.obfsplugin import plain +from shadowsocks.common import to_bytes, to_str, ord, chr + +def create_http_simple_obfs(method): + return http_simple(method) + +def create_http_post_obfs(method): + return http_post(method) + +def create_random_head_obfs(method): + return random_head(method) + +obfs_map = { + 'http_simple': (create_http_simple_obfs,), + 'http_simple_compatible': (create_http_simple_obfs,), + 'http_post': (create_http_post_obfs,), + 'http_post_compatible': (create_http_post_obfs,), + 'random_head': (create_random_head_obfs,), + 'random_head_compatible': (create_random_head_obfs,), +} + +def match_begin(str1, str2): + if len(str1) >= len(str2): + if str1[:len(str2)] == str2: + return True + return False + +class http_simple(plain.plain): + def __init__(self, method): + self.method = method + self.has_sent_header = False + self.has_recv_header = False + self.host = None + self.port = 0 + self.recv_buffer = b'' + self.user_agent = [b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0", + b"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + b"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36", + b"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0", + b"Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)", + b"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + b"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)", + b"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", + b"Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36", + b"Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", + b"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"] + + def encode_head(self, buf): + hexstr = binascii.hexlify(buf) + chs = [] + for i in range(0, len(hexstr), 2): + chs.append(b"%" + hexstr[i:i+2]) + return b''.join(chs) + + def client_encode(self, buf): + if self.has_sent_header: + return buf + head_size = len(self.server_info.iv) + self.server_info.head_len + if len(buf) - head_size > 64: + headlen = head_size + random.randint(0, 64) + else: + headlen = len(buf) + headdata = buf[:headlen] + buf = buf[headlen:] + port = b'' + if self.server_info.port != 80: + port = b':' + to_bytes(str(self.server_info.port)) + body = None + hosts = (self.server_info.obfs_param or self.server_info.host) + pos = hosts.find("#") + if pos >= 0: + body = hosts[pos + 1:].replace("\n", "\r\n") + body = body.replace("\\n", "\r\n") + hosts = hosts[:pos] + hosts = hosts.split(',') + host = random.choice(hosts) + http_head = b"GET /" + self.encode_head(headdata) + b" HTTP/1.1\r\n" + http_head += b"Host: " + to_bytes(host) + port + b"\r\n" + if body: + http_head += body + "\r\n\r\n" + else: + http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n" + http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nDNT: 1\r\nConnection: keep-alive\r\n\r\n" + self.has_sent_header = True + return http_head + buf + + def client_decode(self, buf): + if self.has_recv_header: + return (buf, False) + pos = buf.find(b'\r\n\r\n') + if pos >= 0: + self.has_recv_header = True + return (buf[pos + 4:], False) + else: + return (b'', False) + + def server_encode(self, buf): + if self.has_sent_header: + return buf + + header = b'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: gzip\r\nContent-Type: text/html\r\nDate: ' + header += to_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')) + header += b'\r\nServer: nginx\r\nVary: Accept-Encoding\r\n\r\n' + self.has_sent_header = True + return header + buf + + def get_data_from_http_header(self, buf): + ret_buf = b'' + lines = buf.split(b'\r\n') + if lines and len(lines) > 1: + hex_items = lines[0].split(b'%') + if hex_items and len(hex_items) > 1: + for index in range(1, len(hex_items)): + if len(hex_items[index]) < 2: + ret_buf += binascii.unhexlify('0' + hex_items[index]) + break + elif len(hex_items[index]) > 2: + ret_buf += binascii.unhexlify(hex_items[index][:2]) + break + else: + ret_buf += binascii.unhexlify(hex_items[index]) + return ret_buf + return b'' + + def get_host_from_http_header(self, buf): + ret_buf = b'' + lines = buf.split(b'\r\n') + if lines and len(lines) > 1: + for line in lines: + if match_begin(line, b"Host: "): + return common.to_str(line[6:]) + + def not_match_return(self, buf): + self.has_sent_header = True + self.has_recv_header = True + if self.method == 'http_simple': + return (b'E'*2048, False, False) + return (buf, True, False) + + def error_return(self, buf): + self.has_sent_header = True + self.has_recv_header = True + return (b'E'*2048, False, False) + + def server_decode(self, buf): + if self.has_recv_header: + return (buf, True, False) + + self.recv_buffer += buf + buf = self.recv_buffer + if len(buf) > 10: + if match_begin(buf, b'GET ') or match_begin(buf, b'POST '): + if len(buf) > 65536: + self.recv_buffer = None + logging.warn('http_simple: over size') + return self.not_match_return(buf) + else: #not http header, run on original protocol + self.recv_buffer = None + logging.debug('http_simple: not match begin') + return self.not_match_return(buf) + else: + return (b'', True, False) + + if b'\r\n\r\n' in buf: + datas = buf.split(b'\r\n\r\n', 1) + ret_buf = self.get_data_from_http_header(buf) + host = self.get_host_from_http_header(buf) + if host and self.server_info.obfs_param: + pos = host.find(":") + if pos >= 0: + host = host[:pos] + hosts = self.server_info.obfs_param.split(',') + if host not in hosts: + return self.not_match_return(buf) + if len(ret_buf) < 4: + return self.error_return(buf) + if len(datas) > 1: + ret_buf += datas[1] + if len(ret_buf) >= 13: + self.has_recv_header = True + return (ret_buf, True, False) + return self.not_match_return(buf) + else: + return (b'', True, False) + +class http_post(http_simple): + def __init__(self, method): + super(http_post, self).__init__(method) + + def boundary(self): + return to_bytes(''.join([random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") for i in range(32)])) + + def client_encode(self, buf): + if self.has_sent_header: + return buf + head_size = len(self.server_info.iv) + self.server_info.head_len + if len(buf) - head_size > 64: + headlen = head_size + random.randint(0, 64) + else: + headlen = len(buf) + headdata = buf[:headlen] + buf = buf[headlen:] + port = b'' + if self.server_info.port != 80: + port = b':' + to_bytes(str(self.server_info.port)) + body = None + hosts = (self.server_info.obfs_param or self.server_info.host) + pos = hosts.find("#") + if pos >= 0: + body = hosts[pos + 1:].replace("\\n", "\r\n") + hosts = hosts[:pos] + hosts = hosts.split(',') + host = random.choice(hosts) + http_head = b"POST /" + self.encode_head(headdata) + b" HTTP/1.1\r\n" + http_head += b"Host: " + to_bytes(host) + port + b"\r\n" + if body: + http_head += body + "\r\n\r\n" + else: + http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n" + http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n" + http_head += b"Content-Type: multipart/form-data; boundary=" + self.boundary() + b"\r\nDNT: 1\r\n" + http_head += b"Connection: keep-alive\r\n\r\n" + self.has_sent_header = True + return http_head + buf + + def not_match_return(self, buf): + self.has_sent_header = True + self.has_recv_header = True + if self.method == 'http_post': + return (b'E'*2048, False, False) + return (buf, True, False) + +class random_head(plain.plain): + def __init__(self, method): + self.method = method + self.has_sent_header = False + self.has_recv_header = False + self.raw_trans_sent = False + self.raw_trans_recv = False + self.send_buffer = b'' + + def client_encode(self, buf): + if self.raw_trans_sent: + return buf + self.send_buffer += buf + if not self.has_sent_header: + self.has_sent_header = True + data = os.urandom(common.ord(os.urandom(1)[0]) % 96 + 4) + crc = (0xffffffff - binascii.crc32(data)) & 0xffffffff + return data + struct.pack('= len(str2): + if str1[:len(str2)] == str2: + return True + return False + +class obfs_auth_data(object): + def __init__(self): + self.client_data = lru_cache.LRUCache(60 * 5) + self.client_id = os.urandom(32) + self.startup_time = int(time.time() - 60 * 30) & 0xFFFFFFFF + self.ticket_buf = {} + +class tls_ticket_auth(plain.plain): + def __init__(self, method): + self.method = method + self.handshake_status = 0 + self.send_buffer = b'' + self.recv_buffer = b'' + self.client_id = b'' + self.max_time_dif = 60 * 60 * 24 # time dif (second) setting + self.tls_version = b'\x03\x03' + self.overhead = 5 + + def init_data(self): + return obfs_auth_data() + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return self.overhead + + def sni(self, url): + url = common.to_bytes(url) + data = b"\x00" + struct.pack('>H', len(url)) + url + data = b"\x00\x00" + struct.pack('>H', len(data) + 2) + struct.pack('>H', len(data)) + data + return data + + def pack_auth_data(self, client_id): + utc_time = int(time.time()) & 0xFFFFFFFF + data = struct.pack('>I', utc_time) + os.urandom(18) + data += hmac.new(self.server_info.key + client_id, data, hashlib.sha1).digest()[:10] + return data + + def client_encode(self, buf): + if self.handshake_status == -1: + return buf + if self.handshake_status == 8: + ret = b'' + while len(buf) > 2048: + size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) + ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] + buf = buf[size:] + if len(buf) > 0: + ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf + return ret + if len(buf) > 0: + self.send_buffer += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf + if self.handshake_status == 0: + self.handshake_status = 1 + data = self.tls_version + self.pack_auth_data(self.server_info.data.client_id) + b"\x20" + self.server_info.data.client_id + binascii.unhexlify(b"001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a" + b"0100") + ext = binascii.unhexlify(b"ff01000100") + host = self.server_info.obfs_param or self.server_info.host + if host and host[-1] in string.digits: + host = '' + hosts = host.split(',') + host = random.choice(hosts) + ext += self.sni(host) + ext += b"\x00\x17\x00\x00" + if host not in self.server_info.data.ticket_buf: + self.server_info.data.ticket_buf[host] = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 17 + 8) * 16) + ext += b"\x00\x23" + struct.pack('>H', len(self.server_info.data.ticket_buf[host])) + self.server_info.data.ticket_buf[host] + ext += binascii.unhexlify(b"000d001600140601060305010503040104030301030302010203") + ext += binascii.unhexlify(b"000500050100000000") + ext += binascii.unhexlify(b"00120000") + ext += binascii.unhexlify(b"75500000") + ext += binascii.unhexlify(b"000b00020100") + ext += binascii.unhexlify(b"000a0006000400170018") + data += struct.pack('>H', len(ext)) + ext + data = b"\x01\x00" + struct.pack('>H', len(data)) + data + data = b"\x16\x03\x01" + struct.pack('>H', len(data)) + data + return data + elif self.handshake_status == 1 and len(buf) == 0: + data = b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec + data += b"\x16" + self.tls_version + b"\x00\x20" + os.urandom(22) #Finished + data += hmac.new(self.server_info.key + self.server_info.data.client_id, data, hashlib.sha1).digest()[:10] + ret = data + self.send_buffer + self.send_buffer = b'' + self.handshake_status = 8 + return ret + return b'' + + def client_decode(self, buf): + if self.handshake_status == -1: + return (buf, False) + + if self.handshake_status == 8: + ret = b'' + self.recv_buffer += buf + while len(self.recv_buffer) > 5: + if ord(self.recv_buffer[0]) != 0x17: + logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) + raise Exception('server_decode appdata error') + size = struct.unpack('>H', self.recv_buffer[3:5])[0] + if len(self.recv_buffer) < size + 5: + break + buf = self.recv_buffer[5:size+5] + ret += buf + self.recv_buffer = self.recv_buffer[size+5:] + return (ret, False) + + if len(buf) < 11 + 32 + 1 + 32: + raise Exception('client_decode data error') + verify = buf[11:33] + if hmac.new(self.server_info.key + self.server_info.data.client_id, verify, hashlib.sha1).digest()[:10] != buf[33:43]: + raise Exception('client_decode data error') + if hmac.new(self.server_info.key + self.server_info.data.client_id, buf[:-10], hashlib.sha1).digest()[:10] != buf[-10:]: + raise Exception('client_decode data error') + return (b'', True) + + def server_encode(self, buf): + if self.handshake_status == -1: + return buf + if (self.handshake_status & 8) == 8: + ret = b'' + while len(buf) > 2048: + size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) + ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] + buf = buf[size:] + if len(buf) > 0: + ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf + return ret + self.handshake_status |= 8 + data = self.tls_version + self.pack_auth_data(self.client_id) + b"\x20" + self.client_id + binascii.unhexlify(b"c02f000005ff01000100") + data = b"\x02\x00" + struct.pack('>H', len(data)) + data #server hello + data = b"\x16" + self.tls_version + struct.pack('>H', len(data)) + data + if random.randint(0, 8) < 1: + ticket = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 164) * 2 + 64) + ticket = struct.pack('>H', len(ticket) + 4) + b"\x04\x00" + struct.pack('>H', len(ticket)) + ticket + data += b"\x16" + self.tls_version + ticket #New session ticket + data += b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec + finish_len = random.choice([32, 40]) + data += b"\x16" + self.tls_version + struct.pack('>H', finish_len) + os.urandom(finish_len - 10) #Finished + data += hmac.new(self.server_info.key + self.client_id, data, hashlib.sha1).digest()[:10] + if buf: + data += self.server_encode(buf) + return data + + def decode_error_return(self, buf): + self.handshake_status = -1 + if self.overhead > 0: + self.server_info.overhead -= self.overhead + self.overhead = 0 + if self.method in ['tls1.2_ticket_auth', 'tls1.2_ticket_fastauth']: + return (b'E'*2048, False, False) + return (buf, True, False) + + def server_decode(self, buf): + if self.handshake_status == -1: + return (buf, True, False) + + if (self.handshake_status & 4) == 4: + ret = b'' + self.recv_buffer += buf + while len(self.recv_buffer) > 5: + if ord(self.recv_buffer[0]) != 0x17 or ord(self.recv_buffer[1]) != 0x3 or ord(self.recv_buffer[2]) != 0x3: + logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) + raise Exception('server_decode appdata error') + size = struct.unpack('>H', self.recv_buffer[3:5])[0] + if len(self.recv_buffer) < size + 5: + break + ret += self.recv_buffer[5:size+5] + self.recv_buffer = self.recv_buffer[size+5:] + return (ret, True, False) + + if (self.handshake_status & 1) == 1: + self.recv_buffer += buf + buf = self.recv_buffer + verify = buf + if len(buf) < 11: + raise Exception('server_decode data error') + if not match_begin(buf, b"\x14" + self.tls_version + b"\x00\x01\x01"): #ChangeCipherSpec + raise Exception('server_decode data error') + buf = buf[6:] + if not match_begin(buf, b"\x16" + self.tls_version + b"\x00"): #Finished + raise Exception('server_decode data error') + verify_len = struct.unpack('>H', buf[3:5])[0] + 1 # 11 - 10 + if len(verify) < verify_len + 10: + return (b'', False, False) + if hmac.new(self.server_info.key + self.client_id, verify[:verify_len], hashlib.sha1).digest()[:10] != verify[verify_len:verify_len+10]: + raise Exception('server_decode data error') + self.recv_buffer = verify[verify_len + 10:] + status = self.handshake_status + self.handshake_status |= 4 + ret = self.server_decode(b'') + return ret; + + #raise Exception("handshake data = %s" % (binascii.hexlify(buf))) + self.recv_buffer += buf + buf = self.recv_buffer + ogn_buf = buf + if len(buf) < 3: + return (b'', False, False) + if not match_begin(buf, b'\x16\x03\x01'): + return self.decode_error_return(ogn_buf) + buf = buf[3:] + header_len = struct.unpack('>H', buf[:2])[0] + if header_len > len(buf) - 2: + return (b'', False, False) + + self.recv_buffer = self.recv_buffer[header_len + 5:] + self.handshake_status = 1 + buf = buf[2:header_len + 2] + if not match_begin(buf, b'\x01\x00'): #client hello + logging.info("tls_auth not client hello message") + return self.decode_error_return(ogn_buf) + buf = buf[2:] + if struct.unpack('>H', buf[:2])[0] != len(buf) - 2: + logging.info("tls_auth wrong message size") + return self.decode_error_return(ogn_buf) + buf = buf[2:] + if not match_begin(buf, self.tls_version): + logging.info("tls_auth wrong tls version") + return self.decode_error_return(ogn_buf) + buf = buf[2:] + verifyid = buf[:32] + buf = buf[32:] + sessionid_len = ord(buf[0]) + if sessionid_len < 32: + logging.info("tls_auth wrong sessionid_len") + return self.decode_error_return(ogn_buf) + sessionid = buf[1:sessionid_len + 1] + buf = buf[sessionid_len+1:] + self.client_id = sessionid + sha1 = hmac.new(self.server_info.key + sessionid, verifyid[:22], hashlib.sha1).digest()[:10] + utc_time = struct.unpack('>I', verifyid[:4])[0] + time_dif = common.int32((int(time.time()) & 0xffffffff) - utc_time) + if self.server_info.obfs_param: + try: + self.max_time_dif = int(self.server_info.obfs_param) + except: + pass + if self.max_time_dif > 0 and (time_dif < -self.max_time_dif or time_dif > self.max_time_dif \ + or common.int32(utc_time - self.server_info.data.startup_time) < -self.max_time_dif / 2): + logging.info("tls_auth wrong time") + return self.decode_error_return(ogn_buf) + if sha1 != verifyid[22:]: + logging.info("tls_auth wrong sha1") + return self.decode_error_return(ogn_buf) + if self.server_info.data.client_data.get(verifyid[:22]): + logging.info("replay attack detect, id = %s" % (binascii.hexlify(verifyid))) + return self.decode_error_return(ogn_buf) + self.server_info.data.client_data.sweep() + self.server_info.data.client_data[verifyid[:22]] = sessionid + if len(self.recv_buffer) >= 11: + ret = self.server_decode(b'') + return (ret[0], True, True) + # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) + return (b'', False, True) + diff --git a/shadowsocks/obfsplugin/plain.py b/shadowsocks/obfsplugin/plain.py new file mode 100644 index 00000000..8c6355c3 --- /dev/null +++ b/shadowsocks/obfsplugin/plain.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# +# Copyright 2015-2015 breakwa11 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import hashlib +import logging + +from shadowsocks.common import ord + +def create_obfs(method): + return plain(method) + +obfs_map = { + 'plain': (create_obfs,), + 'origin': (create_obfs,), +} + +class plain(object): + def __init__(self, method): + self.method = method + self.server_info = None + + def init_data(self): + return b'' + + def get_overhead(self, direction): # direction: true for c->s false for s->c + return 0 + + def get_server_info(self): + return self.server_info + + def set_server_info(self, server_info): + self.server_info = server_info + + def client_pre_encrypt(self, buf): + return buf + + def client_encode(self, buf): + return buf + + def client_decode(self, buf): + # (buffer_to_recv, is_need_to_encode_and_send_back) + return (buf, False) + + def client_post_decrypt(self, buf): + return buf + + def server_pre_encrypt(self, buf): + return buf + + def server_encode(self, buf): + return buf + + def server_decode(self, buf): + # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) + return (buf, True, False) + + def server_post_decrypt(self, buf): + return (buf, False) + + def client_udp_pre_encrypt(self, buf): + return buf + + def client_udp_post_decrypt(self, buf): + return buf + + def server_udp_pre_encrypt(self, buf, uid): + return buf + + def server_udp_post_decrypt(self, buf): + return (buf, None) + + def dispose(self): + pass + + def get_head_size(self, buf, def_value): + if len(buf) < 2: + return def_value + head_type = ord(buf[0]) & 0x7 + if head_type == 1: + return 7 + if head_type == 4: + return 19 + if head_type == 3: + return 4 + ord(buf[1]) + return def_value + diff --git a/shadowsocks/obfsplugin/verify.py b/shadowsocks/obfsplugin/verify.py new file mode 100644 index 00000000..0dc0ca6d --- /dev/null +++ b/shadowsocks/obfsplugin/verify.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# +# Copyright 2015-2015 breakwa11 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import hashlib +import logging +import binascii +import base64 +import time +import datetime +import random +import struct +import zlib +import hmac +import hashlib + +import shadowsocks +from shadowsocks import common +from shadowsocks.obfsplugin import plain +from shadowsocks.common import to_bytes, to_str, ord, chr + +def create_verify_deflate(method): + return verify_deflate(method) + +obfs_map = { + 'verify_deflate': (create_verify_deflate,), +} + +def match_begin(str1, str2): + if len(str1) >= len(str2): + if str1[:len(str2)] == str2: + return True + return False + +class obfs_verify_data(object): + def __init__(self): + pass + +class verify_base(plain.plain): + def __init__(self, method): + super(verify_base, self).__init__(method) + self.method = method + + def init_data(self): + return obfs_verify_data() + + def set_server_info(self, server_info): + self.server_info = server_info + + def client_encode(self, buf): + return buf + + def client_decode(self, buf): + return (buf, False) + + def server_encode(self, buf): + return buf + + def server_decode(self, buf): + return (buf, True, False) + +class verify_deflate(verify_base): + def __init__(self, method): + super(verify_deflate, self).__init__(method) + self.recv_buf = b'' + self.unit_len = 32700 + self.decrypt_packet_num = 0 + self.raw_trans = False + + def pack_data(self, buf): + if len(buf) == 0: + return b'' + data = zlib.compress(buf) + data = struct.pack('>H', len(data)) + data[2:] + return data + + def client_pre_encrypt(self, buf): + ret = b'' + while len(buf) > self.unit_len: + ret += self.pack_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_data(buf) + return ret + + def client_post_decrypt(self, buf): + if self.raw_trans: + return buf + self.recv_buf += buf + out_buf = b'' + while len(self.recv_buf) > 2: + length = struct.unpack('>H', self.recv_buf[:2])[0] + if length >= 32768 or length < 6: + self.raw_trans = True + self.recv_buf = b'' + raise Exception('client_post_decrypt data error') + if length > len(self.recv_buf): + break + + out_buf += zlib.decompress(b'x\x9c' + self.recv_buf[2:length]) + self.recv_buf = self.recv_buf[length:] + + if out_buf: + self.decrypt_packet_num += 1 + return out_buf + + def server_pre_encrypt(self, buf): + ret = b'' + while len(buf) > self.unit_len: + ret += self.pack_data(buf[:self.unit_len]) + buf = buf[self.unit_len:] + ret += self.pack_data(buf) + return ret + + def server_post_decrypt(self, buf): + if self.raw_trans: + return (buf, False) + self.recv_buf += buf + out_buf = b'' + while len(self.recv_buf) > 2: + length = struct.unpack('>H', self.recv_buf[:2])[0] + if length >= 32768 or length < 6: + self.raw_trans = True + self.recv_buf = b'' + if self.decrypt_packet_num == 0: + return (b'E'*2048, False) + else: + raise Exception('server_post_decrype data error') + if length > len(self.recv_buf): + break + + out_buf += zlib.decompress(b'\x78\x9c' + self.recv_buf[2:length]) + self.recv_buf = self.recv_buf[length:] + + if out_buf: + self.decrypt_packet_num += 1 + return (out_buf, False) + diff --git a/shadowsocks/ordereddict.py b/shadowsocks/ordereddict.py new file mode 100644 index 00000000..e1918f5e --- /dev/null +++ b/shadowsocks/ordereddict.py @@ -0,0 +1,214 @@ +import collections + +################################################################################ +### OrderedDict +################################################################################ + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as regular dictionaries. + + # The internal self.__map dict maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(*args, **kwds): + '''Initialize an ordered dictionary. The signature is the same as + regular dictionaries, but keyword arguments are not recommended because + their insertion order is arbitrary. + + ''' + if not args: + raise TypeError("descriptor '__init__' of 'OrderedDict' object " + "needs an argument") + self = args[0] + args = args[1:] + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link at the end of the linked list, + # and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + return dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which gets + # removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, _ = self.__map.pop(key) + link_prev[1] = link_next # update link_prev[NEXT] + link_next[0] = link_prev # update link_next[PREV] + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + # Traverse the linked list in order. + root = self.__root + curr = root[1] # start at the first node + while curr is not root: + yield curr[2] # yield the curr[KEY] + curr = curr[1] # move to next node + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + # Traverse the linked list in reverse order. + root = self.__root + curr = root[0] # start at the last node + while curr is not root: + yield curr[2] # yield the curr[KEY] + curr = curr[0] # move to previous node + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + dict.clear(self) + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) pairs in od' + for k in self: + yield (k, self[k]) + + update = collections.MutableMapping.update + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding + value. If key is not found, d is returned if given, otherwise KeyError + is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + key = next(reversed(self) if last else iter(self)) + value = self.pop(key) + return key, value + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. + If not specified, the value defaults to None. + + ''' + self = cls() + for key in iterable: + self[key] = value + return self + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return dict.__eq__(self, other) and all(_imap(_eq, self, other)) + return dict.__eq__(self, other) + + def __ne__(self, other): + 'od.__ne__(y) <==> od!=y' + return not self == other + + # -- the following methods support python 3.x style dictionary views -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) + diff --git a/shadowsocks/run.sh b/shadowsocks/run.sh new file mode 100755 index 00000000..43720308 --- /dev/null +++ b/shadowsocks/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py a>> /dev/null 2>&1 & + diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 429a20a3..c18ad1cc 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -23,8 +23,13 @@ import logging import signal -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns +if __name__ == '__main__': + import inspect + file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))) + sys.path.insert(0, os.path.join(file_path, '../')) + +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ + asyncdns, manager, common def main(): @@ -32,13 +37,18 @@ def main(): config = shell.get_config(False) + shell.log_shadowsocks_version() + daemon.daemon_exec(config) + try: + import resource + logging.info('current process RLIMIT_NOFILE resource: soft %d hard %d' % resource.getrlimit(resource.RLIMIT_NOFILE)) + except ImportError: + pass + if config['port_password']: - if config['password']: - logging.warn('warning: port_password should not be used with ' - 'server_port and password. server_port and password ' - 'will be ignored') + pass else: config['port_password'] = {} server_port = config['server_port'] @@ -48,17 +58,93 @@ def main(): else: config['port_password'][str(server_port)] = config['password'] + if not config.get('dns_ipv6', False): + asyncdns.IPV6_CONNECTION_SUPPORT = False + + if config.get('manager_address', 0): + logging.info('entering manager mode') + manager.run(config) + return + tcp_servers = [] udp_servers = [] dns_resolver = asyncdns.DNSResolver() - for port, password in config['port_password'].items(): + if int(config['workers']) > 1: + stat_counter_dict = None + else: + stat_counter_dict = {} + port_password = config['port_password'] + config_password = config.get('password', 'm') + del config['port_password'] + for port, password_obfs in port_password.items(): + method = config["method"] + protocol = config.get("protocol", 'origin') + protocol_param = config.get("protocol_param", '') + obfs = config.get("obfs", 'plain') + obfs_param = config.get("obfs_param", '') + bind = config.get("out_bind", '') + bindv6 = config.get("out_bindv6", '') + if type(password_obfs) == list: + password = password_obfs[0] + obfs = common.to_str(password_obfs[1]) + if len(password_obfs) > 2: + protocol = common.to_str(password_obfs[2]) + elif type(password_obfs) == dict: + password = password_obfs.get('password', config_password) + method = common.to_str(password_obfs.get('method', method)) + protocol = common.to_str(password_obfs.get('protocol', protocol)) + protocol_param = common.to_str(password_obfs.get('protocol_param', protocol_param)) + obfs = common.to_str(password_obfs.get('obfs', obfs)) + obfs_param = common.to_str(password_obfs.get('obfs_param', obfs_param)) + bind = password_obfs.get('out_bind', bind) + bindv6 = password_obfs.get('out_bindv6', bindv6) + else: + password = password_obfs a_config = config.copy() - a_config['server_port'] = int(port) - a_config['password'] = password - logging.info("starting server at %s:%d" % - (a_config['server'], int(port))) - tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False)) - udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False)) + ipv6_ok = False + logging.info("server start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % + (protocol, password, method, obfs, obfs_param)) + if 'server_ipv6' in a_config: + try: + if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]": + a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] + a_config['server_port'] = int(port) + a_config['password'] = password + a_config['method'] = method + a_config['protocol'] = protocol + a_config['protocol_param'] = protocol_param + a_config['obfs'] = obfs + a_config['obfs_param'] = obfs_param + a_config['out_bind'] = bind + a_config['out_bindv6'] = bindv6 + a_config['server'] = a_config['server_ipv6'] + logging.info("starting server at [%s]:%d" % + (a_config['server'], int(port))) + tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) + udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) + if a_config['server_ipv6'] == b"::": + ipv6_ok = True + except Exception as e: + shell.print_exception(e) + + try: + a_config = config.copy() + a_config['server_port'] = int(port) + a_config['password'] = password + a_config['method'] = method + a_config['protocol'] = protocol + a_config['protocol_param'] = protocol_param + a_config['obfs'] = obfs + a_config['obfs_param'] = obfs_param + a_config['out_bind'] = bind + a_config['out_bindv6'] = bindv6 + logging.info("starting server at %s:%d" % + (a_config['server'], int(port))) + tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) + udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) + except Exception as e: + if not ipv6_ok: + shell.print_exception(e) def run_server(): def child_handler(signum, _): diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py old mode 100644 new mode 100755 index f8ae81f5..6246d98a --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -23,7 +23,7 @@ import sys import getopt import logging -from shadowsocks.common import to_bytes, to_str, IPNetwork +from shadowsocks.common import to_bytes, to_str, IPNetwork, PortRange from shadowsocks import encrypt @@ -52,26 +52,37 @@ def print_exception(e): import traceback traceback.print_exc() - -def print_shadowsocks(): - version = '' +def __version(): + version_str = '' try: import pkg_resources - version = pkg_resources.get_distribution('shadowsocks').version + version_str = pkg_resources.get_distribution('shadowsocks').version except Exception: - pass - print('Shadowsocks %s' % version) + try: + from shadowsocks import version + version_str = version.version() + except Exception: + pass + return version_str + +def print_shadowsocks(): + print('ShadowsocksR %s' % __version()) + +def log_shadowsocks_version(): + logging.info('ShadowsocksR %s' % __version()) def find_config(): + user_config_path = 'user-config.json' config_path = 'config.json' - if os.path.exists(config_path): - return config_path - config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json') - if os.path.exists(config_path): - return config_path - return None + def sub_find(file_name): + if os.path.exists(file_name): + return file_name + file_name = os.path.join(os.path.abspath('..'), file_name) + return file_name if os.path.exists(file_name) else None + + return sub_find(user_config_path) or sub_find(config_path) def check_config(config, is_local): if config.get('daemon', None) == 'stop': @@ -96,21 +107,15 @@ def check_config(config, is_local): config['server_port'] = int(config['server_port']) if config.get('local_address', '') in [b'0.0.0.0']: - logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') + logging.warning('warning: local set to listen on 0.0.0.0, it\'s not safe') if config.get('server', '') in ['127.0.0.1', 'localhost']: - logging.warn('warning: server set to listen on %s:%s, are you sure?' % + logging.warning('warning: server set to listen on %s:%s, are you sure?' % (to_str(config['server']), config['server_port'])) - if (config.get('method', '') or '').lower() == 'table': - logging.warn('warning: table is not safe; please use a safer cipher, ' - 'like AES-256-CFB') - if (config.get('method', '') or '').lower() == 'rc4': - logging.warn('warning: RC4 is not safe; please use a safer cipher, ' - 'like AES-256-CFB') if config.get('timeout', 300) < 100: - logging.warn('warning: your timeout %d seems too short' % + logging.warning('warning: your timeout %d seems too short' % int(config.get('timeout'))) if config.get('timeout', 300) > 600: - logging.warn('warning: your timeout %d seems too long' % + logging.warning('warning: your timeout %d seems too long' % int(config.get('timeout'))) if config.get('password') in [b'mypassword']: logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' @@ -126,36 +131,45 @@ def check_config(config, is_local): def get_config(is_local): global verbose - + config = {} + config_path = None 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:O:o:G:g:c:t:vq' 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:O:o:G:g:c:t:vq' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', - 'forbidden-ip=', 'user=', 'version'] + 'forbidden-ip=', 'user=', 'manager-address=', 'version'] try: - config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) for key, value in optlist: if key == '-c': config_path = value + elif key in ('-h', '--help'): + print_help(is_local) + sys.exit(0) + elif key == '--version': + print_shadowsocks() + sys.exit(0) + else: + continue + + if config_path is None: + config_path = find_config() + if config_path: - logging.info('loading config from %s' % config_path) + logging.debug('loading config from %s' % config_path) with open(config_path, 'rb') as f: try: - config = json.loads(f.read().decode('utf8'), - object_hook=_decode_dict) + config = parse_json_in_str(remove_comment(f.read().decode('utf8'))) except ValueError as e: - logging.error('found an error in config.json: %s', - e.message) + logging.error('found an error in config.json: %s', str(e)) sys.exit(1) - else: - config = {} + v_count = 0 for key, value in optlist: @@ -169,6 +183,14 @@ def get_config(is_local): config['server'] = to_str(value) elif key == '-m': config['method'] = to_str(value) + elif key == '-O': + config['protocol'] = to_str(value) + elif key == '-o': + config['obfs'] = to_str(value) + elif key == '-G': + config['protocol_param'] = to_str(value) + elif key == '-g': + config['obfs_param'] = to_str(value) elif key == '-b': config['local_address'] = to_str(value) elif key == '-v': @@ -181,19 +203,13 @@ def get_config(is_local): config['fast_open'] = True elif key == '--workers': config['workers'] = int(value) + elif key == '--manager-address': + config['manager_address'] = value elif key == '--user': config['user'] = to_str(value) elif key == '--forbidden-ip': - config['forbidden_ip'] = to_str(value).split(',') - elif key in ('-h', '--help'): - if is_local: - print_local_help() - else: - print_server_help() - sys.exit(0) - elif key == '--version': - print_shadowsocks() - sys.exit(0) + config['forbidden_ip'] = to_str(value) + elif key == '-d': config['daemon'] = to_str(value) elif key == '--pid-file': @@ -203,6 +219,8 @@ def get_config(is_local): elif key == '-q': v_count -= 1 config['verbose'] = v_count + else: + continue except getopt.GetoptError as e: print(e, file=sys.stderr) print_help(is_local) @@ -215,13 +233,22 @@ def get_config(is_local): config['password'] = to_bytes(config.get('password', b'')) config['method'] = to_str(config.get('method', 'aes-256-cfb')) + config['protocol'] = to_str(config.get('protocol', 'origin')) + config['protocol_param'] = to_str(config.get('protocol_param', '')) + config['obfs'] = to_str(config.get('obfs', 'plain')) + config['obfs_param'] = to_str(config.get('obfs_param', '')) config['port_password'] = config.get('port_password', None) + config['additional_ports'] = config.get('additional_ports', {}) + config['additional_ports_only'] = config.get('additional_ports_only', False) config['timeout'] = int(config.get('timeout', 300)) + config['udp_timeout'] = int(config.get('udp_timeout', 120)) + config['udp_cache'] = int(config.get('udp_cache', 64)) config['fast_open'] = config.get('fast_open', False) config['workers'] = config.get('workers', 1) - config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid') - config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log') + config['pid-file'] = config.get('pid-file', '/var/run/shadowsocksr.pid') + config['log-file'] = config.get('log-file', '/var/log/shadowsocksr.log') config['verbose'] = config.get('verbose', False) + config['connect_verbose_info'] = config.get('connect_verbose_info', 0) config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) if is_local: @@ -239,6 +266,17 @@ def get_config(is_local): except Exception as e: logging.error(e) sys.exit(2) + try: + config['forbidden_port'] = PortRange(config.get('forbidden_port', '')) + except Exception as e: + logging.error(e) + sys.exit(2) + try: + config['ignore_bind'] = \ + IPNetwork(config.get('ignore_bind', '127.0.0.0/8,::1/128,10.0.0.0/8,192.168.0.0/16')) + except Exception as e: + logging.error(e) + sys.exit(2) config['server_port'] = config.get('server_port', 8388) logging.getLogger('').handlers = [] @@ -255,7 +293,7 @@ def get_config(is_local): level = logging.INFO verbose = config['verbose'] logging.basicConfig(level=level, - format='%(asctime)s %(levelname)-8s %(message)s', + format='%(asctime)s %(levelname)-8s %(filename)s:%(lineno)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') check_config(config, is_local) @@ -284,6 +322,7 @@ def print_local_help(): -l LOCAL_PORT local port, default: 1080 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + -o OBFS obfsplugin, default: http_simple -t TIMEOUT timeout in seconds, default: 300 --fast-open use TCP_FASTOPEN, requires Linux 3.7+ @@ -313,10 +352,12 @@ def print_server_help(): -p SERVER_PORT server port, default: 8388 -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb + -o OBFS obfsplugin, default: http_simple -t TIMEOUT timeout in seconds, default: 300 --fast-open use TCP_FASTOPEN, requires Linux 3.7+ --workers WORKERS number of workers, available on Unix/Linux --forbidden-ip IPLIST comma seperated IP list forbidden to connect + --manager-address ADDR optional server manager UDP address, see wiki General options: -h, --help show this help message and exit @@ -356,3 +397,49 @@ def _decode_dict(data): value = _decode_dict(value) rv[key] = value return rv + +class JSFormat: + def __init__(self): + self.state = 0 + + def push(self, ch): + ch = ord(ch) + if self.state == 0: + if ch == ord('"'): + self.state = 1 + return to_str(chr(ch)) + elif ch == ord('/'): + self.state = 3 + else: + return to_str(chr(ch)) + elif self.state == 1: + if ch == ord('"'): + self.state = 0 + return to_str(chr(ch)) + elif ch == ord('\\'): + self.state = 2 + return to_str(chr(ch)) + elif self.state == 2: + self.state = 1 + if ch == ord('"'): + return to_str(chr(ch)) + return "\\" + to_str(chr(ch)) + elif self.state == 3: + if ch == ord('/'): + self.state = 4 + else: + return "/" + to_str(chr(ch)) + elif self.state == 4: + if ch == ord('\n'): + self.state = 0 + return "\n" + return "" + +def remove_comment(json): + fmt = JSFormat() + return "".join([fmt.push(c) for c in json]) + + +def parse_json_in_str(data): + # parse json and convert everything from unicode to str + return json.loads(data, object_hook=_decode_dict) diff --git a/shadowsocks/stop.sh b/shadowsocks/stop.sh new file mode 100755 index 00000000..d7d29589 --- /dev/null +++ b/shadowsocks/stop.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') + diff --git a/shadowsocks/tail.sh b/shadowsocks/tail.sh new file mode 100755 index 00000000..aa371393 --- /dev/null +++ b/shadowsocks/tail.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +tail -f ssserver.log diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 4834883a..595e2be7 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -23,18 +23,18 @@ import errno import struct import logging +import binascii import traceback import random +import platform +import threading -from shadowsocks import encrypt, eventloop, shell, common -from shadowsocks.common import parse_header +from shadowsocks import encrypt, obfs, eventloop, shell, common, lru_cache, version +from shadowsocks.common import pre_parse_header, parse_header # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time TIMEOUTS_CLEAN_SIZE = 512 -# we check timeouts every TIMEOUT_PRECISION seconds -TIMEOUT_PRECISION = 4 - MSG_FASTOPEN = 0x20000000 # SOCKS command definition @@ -90,8 +90,38 @@ WAIT_STATUS_WRITING = 2 WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING +NETWORK_MTU = 1500 +TCP_MSS = NETWORK_MTU - 40 BUF_SIZE = 32 * 1024 +UDP_MAX_BUF_SIZE = 65536 + +class SpeedTester(object): + def __init__(self, max_speed = 0): + self.max_speed = max_speed * 1024 + self.last_time = time.time() + self.sum_len = 0 + + def update_limit(self, max_speed): + self.max_speed = max_speed * 1024 + + def add(self, data_len): + if self.max_speed > 0: + cut_t = time.time() + self.sum_len -= (cut_t - self.last_time) * self.max_speed + if self.sum_len < 0: + self.sum_len = 0 + self.last_time = cut_t + self.sum_len += data_len + def isExceed(self): + if self.max_speed > 0: + cut_t = time.time() + self.sum_len -= (cut_t - self.last_time) * self.max_speed + if self.sum_len < 0: + self.sum_len = 0 + self.last_time = cut_t + return self.sum_len >= self.max_speed + return False class TCPRelayHandler(object): def __init__(self, server, fd_to_handlers, loop, local_sock, config, @@ -101,34 +131,108 @@ def __init__(self, server, fd_to_handlers, loop, local_sock, config, self._loop = loop self._local_sock = local_sock self._remote_sock = None + self._remote_sock_v6 = None + self._local_sock_fd = None + self._remote_sock_fd = None + self._remotev6_sock_fd = None + self._remote_udp = False self._config = config self._dns_resolver = dns_resolver + self._add_ref = 0 + if not self._create_encryptor(config): + return + + self._client_address = local_sock.getpeername()[:2] + self._accept_address = local_sock.getsockname()[:2] + self._user = None + self._user_id = server._listen_port + self._update_tcp_mss(local_sock) # TCP Relay works as either sslocal or ssserver # if is_local, this is sslocal self._is_local = is_local - self._stage = STAGE_INIT - self._encryptor = encrypt.Encryptor(config['password'], - config['method']) + self._encrypt_correct = True + self._obfs = obfs.obfs(config['obfs']) + self._protocol = obfs.obfs(config['protocol']) + self._overhead = self._obfs.get_overhead(self._is_local) + self._protocol.get_overhead(self._is_local) + self._recv_buffer_size = BUF_SIZE - self._overhead + + server_info = obfs.server_info(server.obfs_data) + server_info.host = config['server'] + server_info.port = server._listen_port + #server_info.users = server.server_users + #server_info.update_user_func = self._update_user + server_info.client = self._client_address[0] + server_info.client_port = self._client_address[1] + server_info.protocol_param = '' + server_info.obfs_param = config['obfs_param'] + server_info.iv = self._encryptor.cipher_iv + server_info.recv_iv = b'' + server_info.key_str = common.to_bytes(config['password']) + server_info.key = self._encryptor.cipher_key + server_info.head_len = 30 + server_info.tcp_mss = self._tcp_mss + server_info.buffer_size = self._recv_buffer_size + server_info.overhead = self._overhead + self._obfs.set_server_info(server_info) + + server_info = obfs.server_info(server.protocol_data) + server_info.host = config['server'] + server_info.port = server._listen_port + server_info.users = server.server_users + server_info.update_user_func = self._update_user + server_info.client = self._client_address[0] + server_info.client_port = self._client_address[1] + server_info.protocol_param = config['protocol_param'] + server_info.obfs_param = '' + server_info.iv = self._encryptor.cipher_iv + server_info.recv_iv = b'' + server_info.key_str = common.to_bytes(config['password']) + server_info.key = self._encryptor.cipher_key + server_info.head_len = 30 + server_info.tcp_mss = self._tcp_mss + server_info.buffer_size = self._recv_buffer_size + server_info.overhead = self._overhead + self._protocol.set_server_info(server_info) + + self._redir_list = config.get('redirect', ["*#0.0.0.0:0"]) + self._is_redirect = False + self._bind = config.get('out_bind', '') + self._bindv6 = config.get('out_bindv6', '') + self._ignore_bind_list = config.get('ignore_bind', []) + self._fastopen_connected = False self._data_to_write_to_local = [] self._data_to_write_to_remote = [] + self._udp_data_send_buffer = b'' self._upstream_status = WAIT_STATUS_READING self._downstream_status = WAIT_STATUS_INIT - self._client_address = local_sock.getpeername()[:2] self._remote_address = None - if 'forbidden_ip' in config: - self._forbidden_iplist = config['forbidden_ip'] - else: - self._forbidden_iplist = None + + self._forbidden_iplist = config.get('forbidden_ip', None) + self._forbidden_portset = config.get('forbidden_port', None) if is_local: self._chosen_server = self._get_a_server() - fd_to_handlers[local_sock.fileno()] = self - local_sock.setblocking(False) - local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) - loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR) + self.last_activity = 0 self._update_activity() + self._server.add_connection(1) + self._server.stat_add(self._client_address[0], 1) + self._add_ref = 1 + self.speed_tester_u = SpeedTester(config.get("speed_limit_per_con", 0)) + self.speed_tester_d = SpeedTester(config.get("speed_limit_per_con", 0)) + self._recv_u_max_size = BUF_SIZE + self._recv_d_max_size = BUF_SIZE + self._recv_pack_id = 0 + self._udp_send_pack_id = 0 + self._udpv6_send_pack_id = 0 + + local_sock.setblocking(False) + local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + self._local_sock_fd = local_sock.fileno() + fd_to_handlers[self._local_sock_fd] = self + loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR, self._server) + self._stage = STAGE_INIT def __hash__(self): # default __hash__ is id / 16 @@ -149,10 +253,38 @@ def _get_a_server(self): logging.debug('chosen server: %s:%d', server, server_port) return server, server_port - def _update_activity(self): + def _update_tcp_mss(self, local_sock): + self._tcp_mss = TCP_MSS + try: + tcp_mss = local_sock.getsockopt(socket.SOL_TCP, socket.TCP_MAXSEG) + if tcp_mss > 500 and tcp_mss <= 1500: + self._tcp_mss = tcp_mss + logging.debug("TCP MSS = %d" % (self._tcp_mss,)) + except: + pass + + def _create_encryptor(self, config): + try: + self._encryptor = encrypt.Encryptor(config['password'], + config['method']) + return True + except Exception: + self._stage = STAGE_DESTROYED + logging.error('create encryptor fail at port %d', self._server._listen_port) + + def _update_user(self, user): + self._user = user + self._user_id = struct.unpack(' 6: + length = struct.unpack('>H', self._udp_data_send_buffer[:2])[0] + + if length > len(self._udp_data_send_buffer): + break + + data = self._udp_data_send_buffer[:length] + self._udp_data_send_buffer = self._udp_data_send_buffer[length:] + + frag = common.ord(data[2]) + if frag != 0: + logging.warn('drop a message since frag is %d' % (frag,)) + continue + else: + data = data[3:] + header_result = parse_header(data) + if header_result is None: + continue + connecttype, addrtype, dest_addr, dest_port, header_length = header_result + if (addrtype & 7) == 3: + af = common.is_ip(dest_addr) + if af == False: + handler = common.UDPAsyncDNSHandler(data[header_length:]) + handler.resolve(self._dns_resolver, (dest_addr, dest_port), self._handle_server_dns_resolved) + else: + return self._handle_server_dns_resolved("", (dest_addr, dest_port), dest_addr, data[header_length:]) + else: + return self._handle_server_dns_resolved("", (dest_addr, dest_port), dest_addr, data[header_length:]) + + except Exception as e: + #trace = traceback.format_exc() + #logging.error(trace) + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + uncomplete = True + else: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return False + return True + else: + try: + if self._encrypt_correct: + if sock == self._remote_sock: + self._server.add_transfer_u(self._user, len(data)) + self._update_activity(len(data)) + if data: + l = len(data) + s = sock.send(data) + if s < l: + data = data[s:] + uncomplete = True + else: + return + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + uncomplete = True + else: + #traceback.print_exc() + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return False + except Exception as e: shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() return False if uncomplete: @@ -214,20 +409,134 @@ def _write_to_sock(self, data, sock): self._data_to_write_to_remote.append(data) self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) else: - logging.error('write_all_to_sock:unknown socket') + logging.error('write_all_to_sock:unknown socket from %s:%d' % (self._client_address[0], self._client_address[1])) else: if sock == self._local_sock: self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) elif sock == self._remote_sock: self._update_stream(STREAM_UP, WAIT_STATUS_READING) else: - logging.error('write_all_to_sock:unknown socket') + logging.error('write_all_to_sock:unknown socket from %s:%d' % (self._client_address[0], self._client_address[1])) return True + def _handle_server_dns_resolved(self, error, remote_addr, server_addr, data): + if error: + return + try: + addrs = socket.getaddrinfo(server_addr, remote_addr[1], 0, socket.SOCK_DGRAM, socket.SOL_UDP) + if not addrs: # drop + return + af, socktype, proto, canonname, sa = addrs[0] + if af == socket.AF_INET6: + self._remote_sock_v6.sendto(data, (server_addr, remote_addr[1])) + if self._udpv6_send_pack_id == 0: + addr, port = self._remote_sock_v6.getsockname()[:2] + common.connect_log('UDPv6 sendto %s(%s):%d from %s:%d by user %d' % + (common.to_str(remote_addr[0]), common.to_str(server_addr), remote_addr[1], addr, port, self._user_id)) + self._udpv6_send_pack_id += 1 + else: + self._remote_sock.sendto(data, (server_addr, remote_addr[1])) + if self._udp_send_pack_id == 0: + addr, port = self._remote_sock.getsockname()[:2] + common.connect_log('UDP sendto %s(%s):%d from %s:%d by user %d' % + (common.to_str(remote_addr[0]), common.to_str(server_addr), remote_addr[1], addr, port, self._user_id)) + self._udp_send_pack_id += 1 + return True + except Exception as e: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + + def _get_redirect_host(self, client_address, ogn_data): + host_list = self._redir_list or ["*#0.0.0.0:0"] + + if type(host_list) != list: + host_list = [host_list] + + items_sum = common.to_str(host_list[0]).rsplit('#', 1) + if len(items_sum) < 2: + hash_code = binascii.crc32(ogn_data) + addrs = socket.getaddrinfo(client_address[0], client_address[1], 0, socket.SOCK_STREAM, socket.SOL_TCP) + af, socktype, proto, canonname, sa = addrs[0] + address_bytes = common.inet_pton(af, sa[0]) + if af == socket.AF_INET6: + addr = struct.unpack('>Q', address_bytes[8:])[0] + elif af == socket.AF_INET: + addr = struct.unpack('>I', address_bytes)[0] + else: + addr = 0 + + host_port = [] + match_port = False + for host in host_list: + items = common.to_str(host).rsplit(':', 1) + if len(items) > 1: + try: + port = int(items[1]) + if port == self._server._listen_port: + match_port = True + host_port.append((items[0], port)) + except: + pass + else: + host_port.append((host, 80)) + + if match_port: + last_host_port = host_port + host_port = [] + for host in last_host_port: + if host[1] == self._server._listen_port: + host_port.append(host) + + return host_port[((hash_code & 0xffffffff) + addr) % len(host_port)] + + else: + host_port = [] + for host in host_list: + items_sum = common.to_str(host).rsplit('#', 1) + items_match = common.to_str(items_sum[0]).rsplit(':', 1) + items = common.to_str(items_sum[1]).rsplit(':', 1) + if len(items_match) > 1: + if items_match[1] != "*": + try: + if self._server._listen_port != int(items_match[1]) and int(items_match[1]) != 0: + continue + except: + pass + + if items_match[0] != "*" and common.match_regex( + items_match[0], ogn_data) == False: + continue + if len(items) > 1: + try: + port = int(items[1]) + return (items[0], port) + except: + pass + else: + return (items[0], 80) + + return ("0.0.0.0", 0) + + def _handel_protocol_error(self, client_address, ogn_data): + logging.warn("Protocol ERROR, TCP ogn data %s from %s:%d via port %d by UID %d" % (binascii.hexlify(ogn_data), client_address[0], client_address[1], self._server._listen_port, self._user_id)) + self._encrypt_correct = False + #create redirect or disconnect by hash code + host, port = self._get_redirect_host(client_address, ogn_data) + if port == 0: + raise Exception('can not parse header') + data = b"\x03" + common.to_bytes(common.chr(len(host))) + common.to_bytes(host) + struct.pack('>H', port) + self._is_redirect = True + logging.warn("TCP data redir %s:%d %s" % (host, port, binascii.hexlify(data))) + return data + ogn_data + def _handle_stage_connecting(self, data): if self._is_local: - data = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data) + if self._encryptor is not None: + data = self._protocol.client_pre_encrypt(data) + data = self._encryptor.encrypt(data) + data = self._obfs.client_encode(data) + if data: + 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 @@ -238,7 +547,7 @@ def _handle_stage_connecting(self, data): remote_sock = \ self._create_remote_socket(self._chosen_server[0], self._chosen_server[1]) - self._loop.add(remote_sock, eventloop.POLL_ERR) + 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) @@ -260,9 +569,22 @@ def _handle_stage_connecting(self, data): shell.print_exception(e) if self._config['verbose']: traceback.print_exc() + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() - def _handle_stage_addr(self, data): + def _get_head_size(self, buf, def_value): + if len(buf) < 2: + return def_value + head_type = common.ord(buf[0]) & 0xF + if head_type == 1: + return 7 + if head_type == 4: + return 19 + if head_type == 3: + return 4 + common.ord(buf[1]) + return def_value + + def _handle_stage_addr(self, ogn_data, data): try: if self._is_local: cmd = common.ord(data[1]) @@ -285,17 +607,42 @@ def _handle_stage_addr(self, data): # just trim VER CMD RSV data = data[3:] else: - logging.error('unknown command %d', cmd) + logging.error('invalid command %d', cmd) self.destroy() return - header_result = parse_header(data) - if header_result is None: - raise Exception('can not parse header') - addrtype, remote_addr, remote_port, header_length = header_result - logging.info('connecting %s:%d from %s:%d' % - (common.to_str(remote_addr), remote_port, - self._client_address[0], self._client_address[1])) + + before_parse_data = data + if self._is_local: + header_result = parse_header(data) + else: + data = pre_parse_header(data) + if data is None: + data = self._handel_protocol_error(self._client_address, ogn_data) + header_result = parse_header(data) + if header_result is not None: + try: + common.to_str(header_result[2]) + except Exception as e: + header_result = None + if header_result is None: + data = self._handel_protocol_error(self._client_address, ogn_data) + header_result = parse_header(data) + self._overhead = self._obfs.get_overhead(self._is_local) + self._protocol.get_overhead(self._is_local) + self._recv_buffer_size = BUF_SIZE - self._overhead + server_info = self._obfs.get_server_info() + server_info.buffer_size = self._recv_buffer_size + server_info = self._protocol.get_server_info() + server_info.buffer_size = self._recv_buffer_size + connecttype, addrtype, remote_addr, remote_port, header_length = header_result + if connecttype != 0: + pass + #common.connect_log('UDP over TCP by user %d' % + # (self._user_id, )) + else: + common.connect_log('TCP request %s:%d by user %d' % + (common.to_str(remote_addr), remote_port, self._user_id)) self._remote_address = (common.to_str(remote_addr), remote_port) + self._remote_udp = (connecttype != 0) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS @@ -304,8 +651,15 @@ def _handle_stage_addr(self, data): self._write_to_sock((b'\x05\x00\x00\x01' b'\x00\x00\x00\x00\x10\x10'), self._local_sock) - data_to_send = self._encryptor.encrypt(data) - self._data_to_write_to_remote.append(data_to_send) + head_len = self._get_head_size(data, 30) + self._obfs.obfs.server_info.head_len = head_len + self._protocol.obfs.server_info.head_len = head_len + if self._encryptor is not None: + data = self._protocol.client_pre_encrypt(data) + data_to_send = self._encryptor.encrypt(data) + data_to_send = self._obfs.client_encode(data_to_send) + if data_to_send: + 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) @@ -319,24 +673,75 @@ def _handle_stage_addr(self, data): self._log_error(e) if self._config['verbose']: traceback.print_exc() - # TODO use logging when debug completed self.destroy() + def _socket_bind_addr(self, sock, af): + bind_addr = '' + if self._bind and af == socket.AF_INET: + bind_addr = self._bind + elif self._bindv6 and af == socket.AF_INET6: + bind_addr = self._bindv6 + else: + bind_addr = self._accept_address[0] + + bind_addr = bind_addr.replace("::ffff:", "") + if bind_addr in self._ignore_bind_list: + bind_addr = None + if bind_addr: + local_addrs = socket.getaddrinfo(bind_addr, 0, 0, socket.SOCK_STREAM, socket.SOL_TCP) + if local_addrs[0][0] == af: + logging.debug("bind %s" % (bind_addr,)) + try: + sock.bind((bind_addr, 0)) + except Exception as e: + logging.warn("bind %s fail" % (bind_addr,)) + def _create_remote_socket(self, ip, port): - addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, - socket.SOL_TCP) + if self._remote_udp: + addrs_v6 = socket.getaddrinfo("::", 0, 0, socket.SOCK_DGRAM, socket.SOL_UDP) + addrs = socket.getaddrinfo("0.0.0.0", 0, 0, socket.SOCK_DGRAM, socket.SOL_UDP) + else: + addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, socket.SOL_TCP) if len(addrs) == 0: raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) af, socktype, proto, canonname, sa = addrs[0] - if self._forbidden_iplist: - if common.to_str(sa[0]) in self._forbidden_iplist: - raise Exception('IP %s is in forbidden list, reject' % - common.to_str(sa[0])) + if not self._remote_udp and not self._is_redirect: + if self._forbidden_iplist: + if common.to_str(sa[0]) in self._forbidden_iplist: + if self._remote_address: + raise Exception('IP %s is in forbidden list, when connect to %s:%d via port %d by UID %d' % + (common.to_str(sa[0]), self._remote_address[0], self._remote_address[1], self._server._listen_port, self._user_id)) + raise Exception('IP %s is in forbidden list, reject' % + common.to_str(sa[0])) + if self._forbidden_portset: + if sa[1] in self._forbidden_portset: + if self._remote_address: + raise Exception('Port %d is in forbidden list, when connect to %s:%d via port %d by UID %d' % + (sa[1], self._remote_address[0], self._remote_address[1], self._server._listen_port, self._user_id)) + raise Exception('Port %d is in forbidden list, reject' % sa[1]) remote_sock = socket.socket(af, socktype, proto) self._remote_sock = remote_sock - self._fd_to_handlers[remote_sock.fileno()] = self + self._remote_sock_fd = remote_sock.fileno() + self._fd_to_handlers[self._remote_sock_fd] = self + + if self._remote_udp: + af, socktype, proto, canonname, sa = addrs_v6[0] + remote_sock_v6 = socket.socket(af, socktype, proto) + self._remote_sock_v6 = remote_sock_v6 + self._remotev6_sock_fd = remote_sock_v6.fileno() + self._fd_to_handlers[self._remotev6_sock_fd] = self + remote_sock.setblocking(False) - remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + if self._remote_udp: + remote_sock_v6.setblocking(False) + + if not self._is_local: + self._socket_bind_addr(remote_sock, af) + self._socket_bind_addr(remote_sock_v6, af) + else: + remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + if not self._is_local: + self._socket_bind_addr(remote_sock, af) return remote_sock def _handle_dns_resolved(self, result, error): @@ -347,7 +752,6 @@ def _handle_dns_resolved(self, result, error): if result: ip = result[1] if ip: - try: self._stage = STAGE_CONNECTING remote_addr = ip @@ -368,34 +772,76 @@ def _handle_dns_resolved(self, result, error): # 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) + if self._remote_udp: + self._loop.add(remote_sock, + eventloop.POLL_IN, + self._server) + if self._remote_sock_v6: + self._loop.add(self._remote_sock_v6, + eventloop.POLL_IN, + self._server) + else: + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) in (errno.EINPROGRESS, + errno.EWOULDBLOCK): + pass # always goto here + else: + raise e + addr, port = self._remote_sock.getsockname()[:2] + common.connect_log('TCP connecting %s(%s):%d from %s:%d by user %d' % + (common.to_str(self._remote_address[0]), common.to_str(remote_addr), remote_port, addr, port, self._user_id)) + + 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) + if self._remote_udp: + while self._data_to_write_to_remote: + data = self._data_to_write_to_remote[0] + del self._data_to_write_to_remote[0] + self._write_to_sock(data, self._remote_sock) return except Exception as e: shell.print_exception(e) if self._config['verbose']: traceback.print_exc() + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() + def _get_read_size(self, sock, recv_buffer_size, up): + if self._overhead == 0: + return recv_buffer_size + buffer_size = len(sock.recv(recv_buffer_size, socket.MSG_PEEK)) + frame_size = self._tcp_mss - self._overhead + if up: + buffer_size = min(buffer_size, self._recv_u_max_size) + self._recv_u_max_size = min(self._recv_u_max_size + frame_size, BUF_SIZE) + else: + buffer_size = min(buffer_size, self._recv_d_max_size) + self._recv_d_max_size = min(self._recv_d_max_size + frame_size, BUF_SIZE) + if buffer_size == recv_buffer_size: + return buffer_size + if buffer_size > frame_size: + buffer_size = int(buffer_size / frame_size) * frame_size + return buffer_size + def _on_local_read(self): # handle all local read events and dispatch them to methods for # each stage - self._update_activity() if not self._local_sock: return is_local = self._is_local + if is_local: + recv_buffer_size = self._get_read_size(self._local_sock, self._recv_buffer_size, True) + else: + recv_buffer_size = BUF_SIZE data = None try: - data = self._local_sock.recv(BUF_SIZE) + data = self._local_sock.recv(recv_buffer_size) except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): @@ -403,50 +849,160 @@ def _on_local_read(self): if not data: self.destroy() return + + self.speed_tester_u.add(len(data)) + self._server.speed_tester_u(self._user_id).add(len(data)) + ogn_data = data if not is_local: - data = self._encryptor.decrypt(data) + if self._encryptor is not None: + if self._encrypt_correct: + try: + obfs_decode = self._obfs.server_decode(data) + if self._stage == STAGE_INIT: + self._overhead = self._obfs.get_overhead(self._is_local) + self._protocol.get_overhead(self._is_local) + server_info = self._protocol.get_server_info() + server_info.overhead = self._overhead + except Exception as e: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + if obfs_decode[2]: + data = self._obfs.server_encode(b'') + try: + self._write_to_sock(data, self._local_sock) + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + if obfs_decode[1]: + if not self._protocol.obfs.server_info.recv_iv: + iv_len = len(self._protocol.obfs.server_info.iv) + self._protocol.obfs.server_info.recv_iv = obfs_decode[0][:iv_len] + data = self._encryptor.decrypt(obfs_decode[0]) + else: + data = obfs_decode[0] + try: + data, sendback = self._protocol.server_post_decrypt(data) + if sendback: + backdata = self._protocol.server_pre_encrypt(b'') + backdata = self._encryptor.encrypt(backdata) + backdata = self._obfs.server_encode(backdata) + try: + self._write_to_sock(backdata, self._local_sock) + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + except Exception as e: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + else: + return if not data: return if self._stage == STAGE_STREAM: if self._is_local: - data = self._encryptor.encrypt(data) + if self._encryptor is not None: + data = self._protocol.client_pre_encrypt(data) + data = self._encryptor.encrypt(data) + data = self._obfs.client_encode(data) self._write_to_sock(data, self._remote_sock) - return elif is_local and self._stage == STAGE_INIT: # TODO check auth method self._write_to_sock(b'\x05\00', self._local_sock) self._stage = STAGE_ADDR - return elif self._stage == STAGE_CONNECTING: self._handle_stage_connecting(data) elif (is_local and self._stage == STAGE_ADDR) or \ (not is_local and self._stage == STAGE_INIT): - self._handle_stage_addr(data) + self._handle_stage_addr(ogn_data, data) - def _on_remote_read(self): + def _on_remote_read(self, is_remote_sock): # handle all remote read events - self._update_activity() data = None try: - data = self._remote_sock.recv(BUF_SIZE) + if self._remote_udp: + if is_remote_sock: + data, addr = self._remote_sock.recvfrom(UDP_MAX_BUF_SIZE) + else: + data, addr = self._remote_sock_v6.recvfrom(UDP_MAX_BUF_SIZE) + port = struct.pack('>H', addr[1]) + try: + ip = socket.inet_aton(addr[0]) + data = b'\x00\x01' + ip + port + data + except Exception as e: + ip = socket.inet_pton(socket.AF_INET6, addr[0]) + data = b'\x00\x04' + ip + port + data + size = len(data) + 2 + data = struct.pack('>H', size) + data + #logging.info('UDP over TCP recvfrom %s:%d %d bytes to %s:%d' % (addr[0], addr[1], len(data), self._client_address[0], self._client_address[1])) + else: + if self._is_local: + recv_buffer_size = BUF_SIZE + else: + recv_buffer_size = self._get_read_size(self._remote_sock, self._recv_buffer_size, False) + data = self._remote_sock.recv(recv_buffer_size) + self._recv_pack_id += 1 except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ - (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK, 10035): #errno.WSAEWOULDBLOCK return if not data: self.destroy() return - if self._is_local: - data = self._encryptor.decrypt(data) + + self.speed_tester_d.add(len(data)) + self._server.speed_tester_d(self._user_id).add(len(data)) + if self._encryptor is not None: + if self._is_local: + try: + obfs_decode = self._obfs.client_decode(data) + except Exception as e: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + if obfs_decode[1]: + send_back = self._obfs.client_encode(b'') + self._write_to_sock(send_back, self._remote_sock) + if not self._protocol.obfs.server_info.recv_iv: + iv_len = len(self._protocol.obfs.server_info.iv) + self._protocol.obfs.server_info.recv_iv = obfs_decode[0][:iv_len] + data = self._encryptor.decrypt(obfs_decode[0]) + try: + data = self._protocol.client_post_decrypt(data) + if self._recv_pack_id == 1: + self._tcp_mss = self._protocol.get_server_info().tcp_mss + except Exception as e: + shell.print_exception(e) + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) + self.destroy() + return + else: + if self._encrypt_correct: + data = self._protocol.server_pre_encrypt(data) + data = self._encryptor.encrypt(data) + data = self._obfs.server_encode(data) + self._server.add_transfer_d(self._user, len(data)) + self._update_activity(len(data)) else: - data = self._encryptor.encrypt(data) + return try: self._write_to_sock(data, self._local_sock) except Exception as e: shell.print_exception(e) if self._config['verbose']: traceback.print_exc() - # TODO use logging when debug completed + logging.error("exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() def _on_local_write(self): @@ -469,52 +1025,80 @@ def _on_remote_write(self): self._update_stream(STREAM_UP, WAIT_STATUS_READING) def _on_local_error(self): - logging.debug('got local error') if self._local_sock: - logging.error(eventloop.get_sock_error(self._local_sock)) + err = eventloop.get_sock_error(self._local_sock) + if err.errno not in [errno.ECONNRESET, errno.EPIPE]: + logging.error(err) + logging.error("local error, exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() def _on_remote_error(self): - logging.debug('got remote error') if self._remote_sock: - logging.error(eventloop.get_sock_error(self._remote_sock)) + err = eventloop.get_sock_error(self._remote_sock) + if err.errno not in [errno.ECONNRESET]: + logging.error(err) + if self._remote_address: + logging.error("remote error, when connect to %s:%d" % (self._remote_address[0], self._remote_address[1])) + else: + logging.error("remote error, exception from %s:%d" % (self._client_address[0], self._client_address[1])) self.destroy() - def handle_event(self, sock, event): + def handle_event(self, sock, fd, event): # handle all events in this handler and dispatch them to methods + handle = False if self._stage == STAGE_DESTROYED: logging.debug('ignore handle_event: destroyed') - return - # order is important - if sock == self._remote_sock: + return True + if self._user is not None and self._user not in self._server.server_users: + self.destroy() + return True + if fd == self._remote_sock_fd or fd == self._remotev6_sock_fd: if event & eventloop.POLL_ERR: + handle = True self._on_remote_error() - if self._stage == STAGE_DESTROYED: - return - if event & (eventloop.POLL_IN | eventloop.POLL_HUP): - self._on_remote_read() - if self._stage == STAGE_DESTROYED: - return - if event & eventloop.POLL_OUT: + elif event & (eventloop.POLL_IN | eventloop.POLL_HUP): + if not self.speed_tester_d.isExceed() and not self._server.speed_tester_d(self._user_id).isExceed(): + handle = True + self._on_remote_read(sock == self._remote_sock) + else: + self._recv_d_max_size = self._tcp_mss - self._overhead + elif event & eventloop.POLL_OUT: + handle = True self._on_remote_write() - elif sock == self._local_sock: + elif fd == self._local_sock_fd: if event & eventloop.POLL_ERR: + handle = True self._on_local_error() - if self._stage == STAGE_DESTROYED: - return - if event & (eventloop.POLL_IN | eventloop.POLL_HUP): - self._on_local_read() - if self._stage == STAGE_DESTROYED: - return - if event & eventloop.POLL_OUT: + elif event & (eventloop.POLL_IN | eventloop.POLL_HUP): + if not self.speed_tester_u.isExceed() and not self._server.speed_tester_u(self._user_id).isExceed(): + handle = True + self._on_local_read() + else: + self._recv_u_max_size = self._tcp_mss - self._overhead + elif event & eventloop.POLL_OUT: + handle = True self._on_local_write() else: - logging.warn('unknown socket') + logging.warn('unknown socket from %s:%d' % (self._client_address[0], self._client_address[1])) + try: + self._loop.removefd(fd) + except Exception as e: + shell.print_exception(e) + try: + del self._fd_to_handlers[fd] + except Exception as e: + shell.print_exception(e) + sock.close() + + return handle def _log_error(self, e): logging.error('%s when handling connection from %s:%d' % (e, self._client_address[0], self._client_address[1])) + def stage(self): + return self._stage + def destroy(self): # destroy the handler and release any resources # promises: @@ -535,35 +1119,83 @@ def destroy(self): logging.debug('destroy') if self._remote_sock: logging.debug('destroying remote') - self._loop.remove(self._remote_sock) - del self._fd_to_handlers[self._remote_sock.fileno()] + try: + self._loop.removefd(self._remote_sock_fd) + except Exception as e: + shell.print_exception(e) + try: + if self._remote_sock_fd is not None: + del self._fd_to_handlers[self._remote_sock_fd] + except Exception as e: + shell.print_exception(e) self._remote_sock.close() self._remote_sock = None + if self._remote_sock_v6: + logging.debug('destroying remote_v6') + try: + self._loop.removefd(self._remotev6_sock_fd) + except Exception as e: + shell.print_exception(e) + try: + if self._remotev6_sock_fd is not None: + del self._fd_to_handlers[self._remotev6_sock_fd] + except Exception as e: + shell.print_exception(e) + self._remote_sock_v6.close() + self._remote_sock_v6 = None if self._local_sock: logging.debug('destroying local') - self._loop.remove(self._local_sock) - del self._fd_to_handlers[self._local_sock.fileno()] + try: + self._loop.removefd(self._local_sock_fd) + except Exception as e: + shell.print_exception(e) + try: + if self._local_sock_fd is not None: + del self._fd_to_handlers[self._local_sock_fd] + except Exception as e: + shell.print_exception(e) self._local_sock.close() self._local_sock = None + if self._obfs: + self._obfs.dispose() + self._obfs = None + if self._protocol: + self._protocol.dispose() + self._protocol = None + self._encryptor = None self._dns_resolver.remove_callback(self._handle_dns_resolved) self._server.remove_handler(self) - + if self._add_ref > 0: + self._server.add_connection(-1) + self._server.stat_add(self._client_address[0], -1) class TCPRelay(object): - def __init__(self, config, dns_resolver, is_local): + def __init__(self, config, dns_resolver, is_local, stat_callback=None, stat_counter=None): self._config = config self._is_local = is_local self._dns_resolver = dns_resolver self._closed = False self._eventloop = None self._fd_to_handlers = {} - self._last_time = time.time() + self.server_transfer_ul = 0 + self.server_transfer_dl = 0 + self.server_users = {} + self.server_users_cfg = {} + self.server_user_transfer_ul = {} + self.server_user_transfer_dl = {} + self.mu = False + self._speed_tester_u = {} + self._speed_tester_d = {} + self.server_connections = 0 + self.protocol_data = obfs.obfs(config['protocol']).init_data() + self.obfs_data = obfs.obfs(config['obfs']).init_data() + + if config.get('connect_verbose_info', 0) > 0: + common.connect_log = logging.info self._timeout = config['timeout'] - self._timeouts = [] # a list for all the handlers - # we trim the timeouts once a while - self._timeout_offset = 0 # last checked position for timeout - self._handler_to_timeouts = {} # key: handler value: index in timeouts + self._timeout_cache = lru_cache.LRUCache(timeout=self._timeout, + close_callback=self._close_tcp_client) if is_local: listen_addr = config['local_address'] @@ -573,6 +1205,9 @@ def __init__(self, config, dns_resolver, is_local): listen_port = config['server_port'] self._listen_port = listen_port + if common.to_str(config['protocol']) in obfs.mu_protocol(): + self._update_users(None, None) + addrs = socket.getaddrinfo(listen_addr, listen_port, 0, socket.SOCK_STREAM, socket.SOL_TCP) if len(addrs) == 0: @@ -589,8 +1224,11 @@ def __init__(self, config, dns_resolver, is_local): except socket.error: logging.error('warning: fast open is not available') self._config['fast_open'] = False - server_socket.listen(1024) + server_socket.listen(config.get('max_connect', 1024)) self._server_socket = server_socket + self._server_socket_fd = server_socket.fileno() + self._stat_counter = stat_counter + self._stat_callback = stat_callback def add_to_loop(self, loop): if self._eventloop: @@ -598,114 +1236,241 @@ def add_to_loop(self, loop): if self._closed: raise Exception('already closed') self._eventloop = loop - loop.add_handler(self._handle_events) - self._eventloop.add(self._server_socket, - eventloop.POLL_IN | eventloop.POLL_ERR) - - def remove_handler(self, handler): - index = self._handler_to_timeouts.get(hash(handler), -1) - if index >= 0: - # delete is O(n), so we just set it to None - self._timeouts[index] = None - del self._handler_to_timeouts[hash(handler)] - - def update_activity(self, handler): - # set handler to active - now = int(time.time()) - if now - handler.last_activity < TIMEOUT_PRECISION: - # thus we can lower timeout modification frequency - return - handler.last_activity = now - index = self._handler_to_timeouts.get(hash(handler), -1) - if index >= 0: - # delete is O(n), so we just set it to None - self._timeouts[index] = None - length = len(self._timeouts) - self._timeouts.append(handler) - self._handler_to_timeouts[hash(handler)] = length + eventloop.POLL_IN | eventloop.POLL_ERR, self) + self._eventloop.add_periodic(self.handle_periodic) - def _sweep_timeout(self): - # tornado's timeout memory management is more flexible than we need - # we just need a sorted last_activity queue and it's faster than heapq - # in fact we can do O(1) insertion/remove so we invent our own - if self._timeouts: - logging.log(shell.VERBOSE_LEVEL, 'sweeping timeouts') - now = time.time() - length = len(self._timeouts) - pos = self._timeout_offset - while pos < length: - handler = self._timeouts[pos] - if handler: - if now - handler.last_activity < self._timeout: - break - else: - if handler.remote_address: - logging.warn('timed out: %s:%d' % - handler.remote_address) + def remove_handler(self, client): + if hash(client) in self._timeout_cache: + del self._timeout_cache[hash(client)] + + def add_connection(self, val): + self.server_connections += val + logging.debug('server port %5d connections = %d' % (self._listen_port, self.server_connections,)) + + def get_ud(self): + return (self.server_transfer_ul, self.server_transfer_dl) + + def get_users_ud(self): + return (self.server_user_transfer_ul.copy(), self.server_user_transfer_dl.copy()) + + def _update_users(self, protocol_param, acl): + if protocol_param is None: + protocol_param = self._config['protocol_param'] + param = common.to_bytes(protocol_param).split(b'#') + if len(param) == 2: + self.mu = True + user_list = param[1].split(b',') + if user_list: + for user in user_list: + items = user.split(b':') + if len(items) == 2: + user_int_id = int(items[0]) + uid = struct.pack(' TIMEOUTS_CLEAN_SIZE and pos > length >> 1: - # clean up the timeout queue when it gets larger than half - # of the queue - self._timeouts = self._timeouts[pos:] - for key in self._handler_to_timeouts: - self._handler_to_timeouts[key] -= pos - pos = 0 - self._timeout_offset = pos - - def _handle_events(self, events): + passwd = items[1] + self.add_user(uid, {'password':passwd}) + + def _update_user(self, id, passwd): + uid = struct.pack('= stat_dict.get(-1, 0) + connections_step: + logging.info('port %d connections up to %d' % (port, newval)) + stat_dict[-1] = stat_dict.get(-1, 0) + connections_step + elif newval <= stat_dict.get(-1, 0) - connections_step: + logging.info('port %d connections down to %d' % (port, newval)) + stat_dict[-1] = stat_dict.get(-1, 0) - connections_step + + def stat_add(self, local_addr, val): + if self._stat_counter is not None: + if self._listen_port not in self._stat_counter: + self._stat_counter[self._listen_port] = {} + newval = self._stat_counter[self._listen_port].get(local_addr, 0) + val + logging.debug('port %d addr %s connections %d' % (self._listen_port, local_addr, newval)) + self._stat_counter[self._listen_port][local_addr] = newval + self.update_stat(self._listen_port, self._stat_counter[self._listen_port], val) + if newval <= 0: + if local_addr in self._stat_counter[self._listen_port]: + del self._stat_counter[self._listen_port][local_addr] + + newval = self._stat_counter.get(0, 0) + val + self._stat_counter[0] = newval + logging.debug('Total connections %d' % newval) + + connections_step = 50 + if newval >= self._stat_counter.get(-1, 0) + connections_step: + logging.info('Total connections up to %d' % newval) + self._stat_counter[-1] = self._stat_counter.get(-1, 0) + connections_step + elif newval <= self._stat_counter.get(-1, 0) - connections_step: + logging.info('Total connections down to %d' % newval) + self._stat_counter[-1] = self._stat_counter.get(-1, 0) - connections_step + + def update_activity(self, client, data_len): + if data_len and self._stat_callback: + self._stat_callback(self._listen_port, data_len) + + self._timeout_cache[hash(client)] = client + + def _sweep_timeout(self): + self._timeout_cache.sweep() + + def _close_tcp_client(self, client): + if client.remote_address: + logging.debug('timed out: %s:%d' % + client.remote_address) + else: + logging.debug('timed out') + client.destroy() + + def handle_event(self, sock, fd, event): # handle events and dispatch to handlers - for sock, fd, event in events: + handle = False + if sock: + logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd, + eventloop.EVENT_NAMES.get(event, event)) + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + # TODO + raise Exception('server_socket error') + handler = None + handle = True + try: + logging.debug('accept') + conn = self._server_socket.accept() + handler = TCPRelayHandler(self, self._fd_to_handlers, + self._eventloop, conn[0], self._config, + self._dns_resolver, self._is_local) + if handler.stage() == STAGE_DESTROYED: + conn[0].close() + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + if error_no in (errno.EAGAIN, errno.EINPROGRESS, + errno.EWOULDBLOCK): + return + else: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + if handler: + handler.destroy() + else: if sock: - logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd, - eventloop.EVENT_NAMES.get(event, event)) - if sock == self._server_socket: - if event & eventloop.POLL_ERR: - # TODO - raise Exception('server_socket error') - try: - logging.debug('accept') - conn = self._server_socket.accept() - TCPRelayHandler(self, self._fd_to_handlers, - self._eventloop, conn[0], self._config, - self._dns_resolver, self._is_local) - except (OSError, IOError) as e: - error_no = eventloop.errno_from_exception(e) - if error_no in (errno.EAGAIN, errno.EINPROGRESS, - errno.EWOULDBLOCK): - continue - else: + handler = self._fd_to_handlers.get(fd, None) + if handler: + handle = handler.handle_event(sock, fd, event) + else: + logging.warn('unknown fd') + handle = True + try: + self._eventloop.removefd(fd) + except Exception as e: shell.print_exception(e) - if self._config['verbose']: - traceback.print_exc() + sock.close() else: - if sock: - handler = self._fd_to_handlers.get(fd, None) - if handler: - handler.handle_event(sock, event) - else: - logging.warn('poll removed fd') + logging.warn('poll removed fd') + handle = True + if fd in self._fd_to_handlers: + try: + del self._fd_to_handlers[fd] + except Exception as e: + shell.print_exception(e) + return handle - now = time.time() - if now - self._last_time > TIMEOUT_PRECISION: - self._sweep_timeout() - self._last_time = now + def handle_periodic(self): if self._closed: if self._server_socket: - self._eventloop.remove(self._server_socket) + self._eventloop.removefd(self._server_socket_fd) self._server_socket.close() self._server_socket = None - logging.info('closed listen port %d', self._listen_port) - if not self._fd_to_handlers: - self._eventloop.remove_handler(self._handle_events) + logging.info('closed TCP port %d', self._listen_port) + for handler in list(self._fd_to_handlers.values()): + handler.destroy() + self._sweep_timeout() def close(self, next_tick=False): + logging.debug('TCP close') self._closed = True if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.removefd(self._server_socket_fd) self._server_socket.close() + for handler in list(self._fd_to_handlers.values()): + handler.destroy() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 98bfaaa7..b9606cd8 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -68,21 +68,70 @@ import struct import errno import random +import binascii +import traceback +import threading -from shadowsocks import encrypt, eventloop, lru_cache, common, shell -from shadowsocks.common import parse_header, pack_addr +from shadowsocks import encrypt, obfs, eventloop, lru_cache, common, shell +from shadowsocks.common import pre_parse_header, parse_header, pack_addr +# for each handler, we have 2 stream directions: +# upstream: from client to server direction +# read local and write to remote +# downstream: from server to client direction +# read remote and write to local + +STREAM_UP = 0 +STREAM_DOWN = 1 + +# for each stream, it's waiting for reading, or writing, or both +WAIT_STATUS_INIT = 0 +WAIT_STATUS_READING = 1 +WAIT_STATUS_WRITING = 2 +WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING BUF_SIZE = 65536 +DOUBLE_SEND_BEG_IDS = 16 +POST_MTU_MIN = 500 +POST_MTU_MAX = 1400 +SENDING_WINDOW_SIZE = 8192 + +STAGE_INIT = 0 +STAGE_RSP_ID = 1 +STAGE_DNS = 2 +STAGE_CONNECTING = 3 +STAGE_STREAM = 4 +STAGE_DESTROYED = -1 +CMD_CONNECT = 0 +CMD_RSP_CONNECT = 1 +CMD_CONNECT_REMOTE = 2 +CMD_RSP_CONNECT_REMOTE = 3 +CMD_POST = 4 +CMD_SYN_STATUS = 5 +CMD_POST_64 = 6 +CMD_SYN_STATUS_64 = 7 +CMD_DISCONNECT = 8 -def client_key(a, b, c, d): - return '%s:%s:%s:%s' % (a, b, c, d) +CMD_VER_STR = b"\x08" +RSP_STATE_EMPTY = b"" +RSP_STATE_REJECT = b"\x00" +RSP_STATE_CONNECTED = b"\x01" +RSP_STATE_CONNECTEDREMOTE = b"\x02" +RSP_STATE_ERROR = b"\x03" +RSP_STATE_DISCONNECT = b"\x04" +RSP_STATE_REDIRECT = b"\x05" + +def client_key(source_addr, server_af): + # notice this is server af, not dest af + return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af) class UDPRelay(object): - def __init__(self, config, dns_resolver, is_local): + def __init__(self, config, dns_resolver, is_local, stat_callback=None, stat_counter=None): self._config = config + if config.get('connect_verbose_info', 0) > 0: + common.connect_log = logging.info if is_local: self._listen_addr = config['local_address'] self._listen_port = config['local_port'] @@ -94,22 +143,66 @@ def __init__(self, config, dns_resolver, is_local): self._remote_addr = None self._remote_port = None self._dns_resolver = dns_resolver - self._password = config['password'] + self._password = common.to_bytes(config['password']) self._method = config['method'] self._timeout = config['timeout'] self._is_local = is_local - self._cache = lru_cache.LRUCache(timeout=config['timeout'], - close_callback=self._close_client) - self._client_fd_to_server_addr = \ - lru_cache.LRUCache(timeout=config['timeout']) + self._udp_cache_size = config['udp_cache'] + self._cache = lru_cache.LRUCache(timeout=config['udp_timeout'], + close_callback=self._close_client_pair) + self._cache_dns_client = lru_cache.LRUCache(timeout=10, + close_callback=self._close_client_pair) + self._client_fd_to_server_addr = {} + #self._dns_cache = lru_cache.LRUCache(timeout=1800) self._eventloop = None self._closed = False - self._last_time = time.time() + self.server_transfer_ul = 0 + self.server_transfer_dl = 0 + self.server_users = {} + self.server_user_transfer_ul = {} + self.server_user_transfer_dl = {} + + if common.to_bytes(config['protocol']) in obfs.mu_protocol(): + self._update_users(None, None) + + self.protocol_data = obfs.obfs(config['protocol']).init_data() + self._protocol = obfs.obfs(config['protocol']) + server_info = obfs.server_info(self.protocol_data) + server_info.host = self._listen_addr + server_info.port = self._listen_port + server_info.users = self.server_users + server_info.protocol_param = config['protocol_param'] + server_info.obfs_param = '' + server_info.iv = b'' + server_info.recv_iv = b'' + server_info.key_str = common.to_bytes(config['password']) + server_info.key = encrypt.encrypt_key(self._password, self._method) + server_info.head_len = 30 + server_info.tcp_mss = 1452 + server_info.buffer_size = BUF_SIZE + server_info.overhead = 0 + self._protocol.set_server_info(server_info) + self._sockets = set() + self._fd_to_handlers = {} + self._reqid_to_hd = {} + self._data_to_write_to_server_socket = [] + + self._timeout_cache = lru_cache.LRUCache(timeout=self._timeout, + close_callback=self._close_tcp_client) + + self._bind = config.get('out_bind', '') + self._bindv6 = config.get('out_bindv6', '') + self._ignore_bind_list = config.get('ignore_bind', []) + if 'forbidden_ip' in config: self._forbidden_iplist = config['forbidden_ip'] else: self._forbidden_iplist = None + if 'forbidden_port' in config: + self._forbidden_portset = config['forbidden_port'] + else: + self._forbidden_portset = None addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) @@ -121,6 +214,7 @@ def __init__(self, config, dns_resolver, is_local): server_socket.bind((self._listen_addr, self._listen_port)) server_socket.setblocking(False) self._server_socket = server_socket + self._stat_callback = stat_callback def _get_a_server(self): server = self._config['server'] @@ -132,20 +226,123 @@ def _get_a_server(self): logging.debug('chosen server: %s:%d', server, server_port) return server, server_port + def get_ud(self): + return (self.server_transfer_ul, self.server_transfer_dl) + + def get_users_ud(self): + ret = (self.server_user_transfer_ul.copy(), self.server_user_transfer_dl.copy()) + return ret + + def _update_users(self, protocol_param, acl): + if protocol_param is None: + protocol_param = self._config['protocol_param'] + param = common.to_bytes(protocol_param).split(b'#') + if len(param) == 2: + user_list = param[1].split(b',') + if user_list: + for user in user_list: + items = user.split(b':') + if len(items) == 2: + user_int_id = int(items[0]) + uid = struct.pack(' header_length + 13 and data[header_length + 4 : header_length + 12] == b"\x00\x01\x00\x00\x00\x00\x00\x00": + is_dns = True + else: + pass + if sa[1] == 53 and is_dns: #DNS + logging.debug("DNS query %s from %s:%d" % (common.to_str(sa[0]), r_addr[0], r_addr[1])) + self._cache_dns_client[key] = (client, uid) + else: + self._cache[key] = (client, uid) + self._client_fd_to_server_addr[client.fileno()] = (r_addr, af) + + self._sockets.add(client.fileno()) + self._eventloop.add(client, eventloop.POLL_IN, self) + + logging.debug('UDP port %5d sockets %d' % (self._listen_port, len(self._sockets))) + + if uid is not None: + user_id = struct.unpack(' 255: # drop return data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data - response = encrypt.encrypt_all(self._password, self._method, 1, - data) + ref_iv = [encrypt.encrypt_new_iv(self._method)] + self._protocol.obfs.server_info.iv = ref_iv[0] + data = self._protocol.server_udp_pre_encrypt(data, client_uid) + response = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 1, + data, ref_iv) if not response: return else: - data = encrypt.encrypt_all(self._password, self._method, 0, - data) + ref_iv = [0] + data = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 0, + data, ref_iv) if not data: return + self._protocol.obfs.server_info.recv_iv = ref_iv[0] + data = self._protocol.client_udp_post_decrypt(data) header_result = parse_header(data) if header_result is None: return - # addrtype, dest_addr, dest_port, header_length = header_result + #connecttype, dest_addr, dest_port, header_length = header_result + #logging.debug('UDP handle_client %s:%d to %s:%d' % (common.to_str(r_addr[0]), r_addr[1], dest_addr, dest_port)) + response = b'\x00\x00\x00' + data - client_addr = self._client_fd_to_server_addr.get(sock.fileno()) + if client_addr: - self._server_socket.sendto(response, client_addr) + if client_uid: + self.add_transfer_d(client_uid, len(response)) + else: + self.server_transfer_dl += len(response) + self.write_to_server_socket(response, client_addr[0]) + if client_dns_pair: + logging.debug("remove dns client %s:%d" % (client_addr[0][0], client_addr[0][1])) + del self._cache_dns_client[key] + self._close_client(client_dns_pair[0]) else: # this packet is from somewhere else we know # simply drop that packet pass + def write_to_server_socket(self, data, addr): + uncomplete = False + retry = 0 + try: + self._server_socket.sendto(data, addr) + data = None + while self._data_to_write_to_server_socket: + data_buf = self._data_to_write_to_server_socket[0] + retry = data_buf[1] + 1 + del self._data_to_write_to_server_socket[0] + data, addr = data_buf[0] + self._server_socket.sendto(data, addr) + except (OSError, IOError) as e: + error_no = eventloop.errno_from_exception(e) + uncomplete = True + if error_no in (errno.EWOULDBLOCK,): + pass + else: + shell.print_exception(e) + return False + #if uncomplete and data is not None and retry < 3: + # self._data_to_write_to_server_socket.append([(data, addr), retry]) + #''' + def add_to_loop(self, loop): if self._eventloop: raise Exception('already add to loop') if self._closed: raise Exception('already closed') self._eventloop = loop - loop.add_handler(self._handle_events) server_socket = self._server_socket self._eventloop.add(server_socket, - eventloop.POLL_IN | eventloop.POLL_ERR) + eventloop.POLL_IN | eventloop.POLL_ERR, self) + loop.add_periodic(self.handle_periodic) + + def remove_handler(self, client): + if hash(client) in self._timeout_cache: + del self._timeout_cache[hash(client)] + + def update_activity(self, client): + self._timeout_cache[hash(client)] = client + + def _sweep_timeout(self): + self._timeout_cache.sweep() + + def _close_tcp_client(self, client): + if client.remote_address: + logging.debug('timed out: %s:%d' % + client.remote_address) + else: + logging.debug('timed out') + client.destroy() + client.destroy_local() - def _handle_events(self, events): - for sock, fd, event in events: - if sock == self._server_socket: - if event & eventloop.POLL_ERR: - logging.error('UDP server_socket err') + def handle_event(self, sock, fd, event): + if sock == self._server_socket: + if event & eventloop.POLL_ERR: + logging.error('UDP server_socket err') + try: self._handle_server() - elif sock and (fd in self._sockets): - if event & eventloop.POLL_ERR: - logging.error('UDP client_socket err') + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + elif sock and (fd in self._sockets): + if event & eventloop.POLL_ERR: + logging.error('UDP client_socket err') + try: self._handle_client(sock) - now = time.time() - if now - self._last_time > 3: - self._cache.sweep() - self._client_fd_to_server_addr.sweep() - self._last_time = now + except Exception as e: + shell.print_exception(e) + if self._config['verbose']: + traceback.print_exc() + else: + if sock: + handler = self._fd_to_handlers.get(fd, None) + if handler: + handler.handle_event(sock, event) + else: + logging.warn('poll removed fd') + + def handle_periodic(self): if self._closed: - self._server_socket.close() - for sock in self._sockets: - sock.close() - self._eventloop.remove_handler(self._handle_events) + self._cache.clear(0) + self._cache_dns_client.clear(0) + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) + if self._server_socket: + self._server_socket.close() + self._server_socket = None + logging.info('closed UDP port %d', self._listen_port) + else: + before_sweep_size = len(self._sockets) + self._cache.sweep() + self._cache_dns_client.sweep() + if before_sweep_size != len(self._sockets): + logging.debug('UDP port %5d sockets %d' % (self._listen_port, len(self._sockets))) + self._sweep_timeout() def close(self, next_tick=False): + logging.debug('UDP close') self._closed = True if not next_tick: + if self._eventloop: + self._eventloop.remove_periodic(self.handle_periodic) + self._eventloop.remove(self._server_socket) self._server_socket.close() + self._cache.clear(0) + self._cache_dns_client.clear(0) diff --git a/shadowsocks/version.py b/shadowsocks/version.py new file mode 100644 index 00000000..f3e1ef79 --- /dev/null +++ b/shadowsocks/version.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2017 breakwa11 +# +# 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. + +def version(): + return '3.4.0 2017-07-27' + diff --git a/stop.sh b/stop.sh new file mode 100755 index 00000000..56567daa --- /dev/null +++ b/stop.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') + diff --git a/switchrule.py b/switchrule.py new file mode 100644 index 00000000..6687e12c --- /dev/null +++ b/switchrule.py @@ -0,0 +1,8 @@ +def getKeys(key_list): + return key_list + #return key_list + ['plan'] # append the column name 'plan' + +def isTurnOn(row): + return True + #return row['plan'] == 'B' # then judge here + diff --git a/tail.sh b/tail.sh new file mode 100755 index 00000000..f36f605e --- /dev/null +++ b/tail.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd `dirname $0` +tail -f ssserver.log diff --git a/tests/assert.sh b/tests/assert.sh old mode 100644 new mode 100755 diff --git a/tests/jenkins.sh b/tests/jenkins.sh index 71d5b1ca..ea5c1630 100755 --- a/tests/jenkins.sh +++ b/tests/jenkins.sh @@ -69,7 +69,7 @@ if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then fi run_test tests/test_large_file.sh - +run_test tests/test_udp_src.sh run_test tests/test_command.sh coverage combine && coverage report --include=shadowsocks/* diff --git a/tests/test.py b/tests/test.py index 29b57d42..40834013 100755 --- a/tests/test.py +++ b/tests/test.py @@ -44,7 +44,7 @@ config = parser.parse_args() if config.with_coverage: - python = ['coverage', 'run', '-p', '-a'] + python = ['coverage', 'run', '-p'] 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 be05704f..a1a777b0 100755 --- a/tests/test_command.sh +++ b/tests/test_command.sh @@ -2,18 +2,13 @@ . tests/assert.sh -PYTHON="coverage run -a -p" +PYTHON="coverage run -p" LOCAL="$PYTHON shadowsocks/local.py" SERVER="$PYTHON shadowsocks/server.py" assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" -assert "$LOCAL 2>&1 | grep ERROR" "ERROR: config not specified" -assert "$LOCAL 2>&1 | grep usage | cut -d: -f1" "usage" - -assert "$SERVER 2>&1 | grep ERROR" "ERROR: config not specified" -assert "$SERVER 2>&1 | grep usage | cut -d: -f1" "usage" assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: server set to listen on 127.0.0.1:8388, are you sure?" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop @@ -30,14 +25,6 @@ $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d sto assert "$LOCAL 2>&1 -m rc4-md5 -k mypassword -s 0.0.0.0 -p 8388 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " DON'T USE DEFAULT PASSWORD! Please change it in your config.json!" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop -assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified" -$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop - -assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password not specified" -$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop - -assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password or port_password not specified" -$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop assert "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": Not a valid CIDR notation: 127.0.0.1/4a" $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop diff --git a/tests/test_daemon.sh b/tests/test_daemon.sh index 40f35ef0..7a192bdb 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 -p shadowsocks/$module.py" mkdir -p tmp diff --git a/tests/test_large_file.sh b/tests/test_large_file.sh index 33bcb590..7a61caff 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 -p" URL=http://127.0.0.1/file mkdir -p tmp diff --git a/tests/test_udp_src.py b/tests/test_udp_src.py new file mode 100644 index 00000000..e8fa5057 --- /dev/null +++ b/tests/test_udp_src.py @@ -0,0 +1,83 @@ +#!/usr/bin/python + +import socket +import socks + + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 1081 + + +if __name__ == '__main__': + # Test 1: same source port IPv4 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('127.0.0.1', 9001)) + sock_in2.bind(('127.0.0.1', 9002)) + + sock_out.sendto(b'data', ('127.0.0.1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('127.0.0.1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # 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, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9000)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + + sock_in1.bind(('::1', 9001)) + sock_in2.bind(('::1', 9002)) + + sock_out.sendto(b'data', ('::1', 9001)) + result1 = sock_in1.recvfrom(8) + + sock_out.sendto(b'data', ('::1', 9002)) + result2 = sock_in2.recvfrom(8) + + sock_out.close() + sock_in1.close() + sock_in2.close() + + # make sure they're from the same source port + assert result1 == result2 + + # Test 3: different source ports IPv6 + sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) + sock_out.bind(('127.0.0.1', 9003)) + + sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, + socket.SOL_UDP) + sock_in1.bind(('::1', 9001)) + sock_out.sendto(b'data', ('::1', 9001)) + result3 = sock_in1.recvfrom(8) + + # make sure they're from different source ports + assert result1 != result3 + + sock_out.close() + sock_in1.close() diff --git a/tests/test_udp_src.sh b/tests/test_udp_src.sh new file mode 100755 index 00000000..6a778abc --- /dev/null +++ b/tests/test_udp_src.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PYTHON="coverage run -p" + +mkdir -p tmp + +$PYTHON shadowsocks/local.py -c tests/aes.json -v & +LOCAL=$! + +$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & +SERVER=$! + +sleep 3 + +python tests/test_udp_src.py +r=$? + +kill -s SIGINT $LOCAL +kill -s SIGINT $SERVER + +sleep 2 + +exit $r diff --git a/utils/fail2ban/shadowsocks.conf b/utils/fail2ban/shadowsocks.conf new file mode 100644 index 00000000..9b1c7ec7 --- /dev/null +++ b/utils/fail2ban/shadowsocks.conf @@ -0,0 +1,5 @@ +[Definition] + +_daemon = shadowsocks + +failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$