diff --git a/README.md b/README.md index 1c4a805..c29d836 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,34 @@ About shadowsocks-python manyuser ================================= -This is a multi-user version of shadowsocks-python. Requires a mysql database. +This is a multi-user version of shadowsocks-python. Requires a mysql database or a panel support SS MU API. -Install -------- -1. install MySQL 5.x.x +Install instruction for database user +------------------------------------- +1. install MySQL Server 5.x.x 2. install cymysql library by `pip install cymysql` 3. create a database named `shadowsocks` 4. import `shadowsocks.sql` into `shadowsocks` 5. copy `config_example.py` to `config.py` and edit it following the notes inside (but DO NOT delete the example file) 6. TestRun `cd shadowsocks && python servers.py` (not server.py) +Install instruction for MU API user +----------------------------------- +1. install a panel which supports MU API (the known one is [SS-Panel V3](https://github.com/orvice/ss-panel)) +2. copy `config_example.py` to `config.py` and edit it following the notes inside (but DO NOT delete the example file) +3. TestRun `cd shadowsocks && python servers.py` (not server.py) + + if no exception the server will startup. By default logging is enabled. You should be able to see this kind of thing in `shadowsocks.log`(default log file name) ``` -May 25 23:03:16 INFO Multi-User Shadowsocks Server Starting... -May 25 23:03:17 INFO Current Server Version: 2.8.3-83-gf8dd2f8 - -May 25 23:03:18 INFO db skipped port 443 -May 25 23:03:19 INFO db downloaded -May 25 23:03:19 INFO Server Added: P[XXXX], M[aes-256-cfb], E[XXX@XXX.XXX] +Jun 24 01:06:08 INFO ----------------------------------------- +Jun 24 01:06:08 INFO Multi-User Shadowsocks Server Starting... +Jun 24 01:06:08 INFO Current Server Version: 3.1.0-1-gc2ac618 + +Jun 24 01:10:11 INFO api downloaded +Jun 24 01:10:13 INFO api skipped port 443 +Jun 24 01:10:13 INFO Server Added: P[XXXXX], M[rc4-md5], E[XXXXX@gmail.com] +Jun 24 01:10:13 INFO Server Added: P[XXXXX], M[rc4-md5], E[XXXXX@gmail.com] ``` Explanation of the log output @@ -58,7 +67,7 @@ Database user table column Compatibility with other frontend UIs ------------------------------------- -It is compatible with [ss-panel](https://github.com/orvice/ss-panel). +It is fully compatible (SS MU API) with [ss-panel V3](https://github.com/orvice/ss-panel). Open source license ------------------- diff --git a/shadowsocks/config_example.py b/shadowsocks/config_example.py index 2e9bad2..28d94ab 100644 --- a/shadowsocks/config_example.py +++ b/shadowsocks/config_example.py @@ -1,8 +1,18 @@ -# !!! Please rename this file as config.py BEFORE editing it !!! +# !!! Please rename config_example.py as config.py BEFORE editing it !!! import logging -# !!! Do NOT touch this line !!! -CONFIG_VERSION = '20160618-1' +# !!! Only edit this line when you update your configuration file !!! +# After you update, the value of CONFIG_VERSION in config.py and +# config_example.py should be the same in order to start the server +CONFIG_VERSION = '20160623-2' + + +# Manyuser Interface Settings +# --------------------------- +# If API is enabled, database will be no longer used +# The known app that supports API is SS-Panel V3 +# Be careful and check whether your app supports this API BEFORE you enable this feature +API_ENABLED = False # Database Config MYSQL_HOST = 'mengsky.net' @@ -10,31 +20,39 @@ MYSQL_USER = 'root' MYSQL_PASS = 'root' MYSQL_DB = 'shadowsocks' +# USUALLY this variable do not need to be changed MYSQL_USER_TABLE = 'user' +# This is also the timeout of connecting to the API MYSQL_TIMEOUT = 30 -# It is not necessary to change the password if you only listen on 127.0.0.1 -MANAGE_PASS = 'passwd' -# if you want manage in other server you should set this value to global ip -MANAGE_BIND_IP = '127.0.0.1' -# make sure this port is idle -MANAGE_PORT = 23333 - -# SS Panel API Setting -# Version of Panel: V2 or V3. V2 not support API thus no need to change -# anything in the following settings -PANEL_VERSION = 'V2' +# Shadowsocks MultiUser API Settings API_URL = 'http://domain/mu' -# API Key of SS-Panel V3 (you can find this in the .env file) +# API Key (you can find this in the .env file if you are using SS-Panel V3) API_PASS = 'mupass' NODE_ID = '1' + +# Time interval between 2 pulls from the database CHECKTIME = 15 +# Time interval between 2 pushes from the database SYNCTIME = 600 - -# Choose True if you want to use custom method +# Choose True if you want to use custom method and False if you don't CUSTOM_METHOD = True -# BIND IP + +# Manager Settings +# ---------------- +# USUALLY you can just keep this section unchanged +# It is not necessary to change the password if you only listen on 127.0.0.1 +MANAGE_PASS = 'passwd' +# if you want manage in other server you should set this value to global ip +MANAGE_BIND_IP = '127.0.0.1' +# make sure this port is idle +MANAGE_PORT = 65000 + + +# Network Settings +# ---------------- +# Address binding settings # if you want to bind ipv4 and ipv6 please use '::' # if you want to bind only all of ipv4 please use '0.0.0.0' # if you want to bind a specific IP you may use something like '4.4.4.4' @@ -45,23 +63,35 @@ # OTA will still be enabled for the client if it sends an AUTH Address type(0x10) SS_OTA = False # Skip listening these ports -SS_SKIP_PORTS = ['80'] -# Ban these outbound ports -# Members should be INTEGERS -SS_BAN_PORTS = [22, 23, 25] - +SS_SKIP_PORTS = [80] +# TCP Fastopen (Some OS may not support this, Eg.: Windows) +SS_FASTOPEN = False # Shadowsocks Time Out # It should > 180s as some protocol has keep-alive packet of 3 min, Eg.: bt SS_TIMEOUT = 185 -# Shadowsocks TCP Fastopen (Some OS may not support this, Eg.: Windows) -SS_FASTOPEN = False -# Shadowsocks verbose -SS_VERBOSE = False + + +# Firewall Settings +# ----------------- +# These settings are to prevent user from abusing your service +SS_FIREWALL_ENABLED = True +# Mode = whitelist or blacklist +SS_FIREWALL_MODE = 'blacklist' +# Member ports should be INTEGERS +# Only Ban these target ports (for blacklist mode) +SS_BAN_PORTS = [22, 23, 25] +# Only Allow these target ports (for whitelist mode) +SS_ALLOW_PORTS = [53, 80, 443, 8080, 8081] +# Trusted users (all target ports will be not be blocked for these users) +SS_FIREWALL_TRUSTED = [443] # Banned Target IP List SS_FORBIDDEN_IP = [] -# LOG CONFIG + +# Logging and Debugging Settings +# -------------------------- LOG_ENABLE = True +SS_VERBOSE = False # Available Log Level: logging.NOTSET|DEBUG|INFO|WARNING|ERROR|CRITICAL LOG_LEVEL = logging.INFO LOG_FILE = 'shadowsocks.log' diff --git a/shadowsocks/dbtransfer.py b/shadowsocks/dbtransfer.py index f28e4d1..b182bd1 100644 --- a/shadowsocks/dbtransfer.py +++ b/shadowsocks/dbtransfer.py @@ -22,6 +22,9 @@ import socket import config import json +import urllib +# TODO: urllib2 does not exist in python 3.5+ +import urllib2 class DbTransfer(object): @@ -48,8 +51,11 @@ def send_command(cmd): cli.close() # TODO: bad way solve timed out time.sleep(0.05) - except: - logging.warn('send_command response') + except Exception as e: + if config.SS_VERBOSE: + import traceback + traceback.print_exc() + logging.warn('send_command exception: %s' % e) return data @staticmethod @@ -69,29 +75,82 @@ def get_servers_transfer(): return dt_transfer def push_db_all_user(self): - import urllib2, urllib - import time dt_transfer = self.get_servers_transfer() - if config.PANEL_VERSION == 'V2': + if config.API_ENABLED: + i = 0 + if config.SS_VERBOSE: + logging.info('api upload: pushing transfer statistics') + for port in dt_transfer.keys(): + user = DbTransfer.pull_api_user(port) + if config.SS_VERBOSE: + logging.info('port:%s ----> id:%s' % (port, user[0])) + tran = str(dt_transfer[port]) + data = {'d': tran, 'node_id': config.NODE_ID, 'u': '0'} + url = config.API_URL + '/users/' + \ + str(user[0]) + '/traffic?key=' + config.API_PASS + data = urllib.urlencode(data) + req = urllib2.Request(url, data) + response = urllib2.urlopen(req) + the_page = response.read() + if config.SS_VERBOSE: + logging.info('%s - %s - %s' % (url, data, the_page)) + i += 1 + + # online user count + if config.SS_VERBOSE: + logging.info('api upload: pushing online user count') + data = {'count': i} + url = config.API_URL + '/nodes/' + config.NODE_ID + \ + '/online_count?key=' + config.API_PASS + data = urllib.urlencode(data) + req = urllib2.Request(url, data) + response = urllib2.urlopen(req) + the_page = response.read() + if config.SS_VERBOSE: + logging.info('%s - %s - %s' % (url, data, the_page)) + + # load info + if config.SS_VERBOSE: + logging.info('api upload: pushing node status') + url = config.API_URL + '/nodes/' + config.NODE_ID + '/info?key=' + config.API_PASS + f = open("/proc/loadavg") + load = f.read().split() + f.close() + loadavg = load[0] + ' ' + load[1] + ' ' + \ + load[2] + ' ' + load[3] + ' ' + load[4] + f = open("/proc/uptime") + uptime = f.read().split() + uptime = uptime[0] + f.close() + data = {'load': loadavg, 'uptime': uptime} + data = urllib.urlencode(data) + req = urllib2.Request(url, data) + response = urllib2.urlopen(req) + the_page = response.read() + if config.SS_VERBOSE: + logging.info('%s - %s - %s' % (url, data, the_page)) + logging.info('api uploaded') + else: query_head = 'UPDATE `user`' query_sub_when = '' query_sub_when2 = '' query_sub_in = None last_time = time.time() - for id in dt_transfer.keys(): - query_sub_when += ' WHEN %s THEN `u`+%s' % (id, 0) # all in d - query_sub_when2 += ' WHEN %s THEN `d`+%s' % (id, dt_transfer[id]) + for port in dt_transfer.keys(): + query_sub_when += ' WHEN %s THEN `u`+%s' % (port, 0) # all in d + query_sub_when2 += ' WHEN %s THEN `d`+%s' % ( + port, dt_transfer[port]) if query_sub_in is not None: - query_sub_in += ',%s' % id + query_sub_in += ',%s' % port else: - query_sub_in = '%s' % id + query_sub_in = '%s' % port if query_sub_when == '': return 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 + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in # print query_sql conn = cymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT, user=config.MYSQL_USER, passwd=config.MYSQL_PASS, db=config.MYSQL_DB, charset='utf8') @@ -101,62 +160,7 @@ def push_db_all_user(self): conn.commit() conn.close() if config.SS_VERBOSE: - logging.info('db upload') - else: - if config.PANEL_VERSION == 'V3': - i = 0 - for id in dt_transfer.keys(): - string = ' WHERE `port` = %s' % id - conn = cymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT, user=config.MYSQL_USER, - passwd=config.MYSQL_PASS, db=config.MYSQL_DB, charset='utf8') - cur = conn.cursor() - cur.execute('SELECT id FROM %s%s ' - % (config.MYSQL_USER_TABLE, string)) - rows = [] - for r in cur.fetchall(): - rows.append(list(r)) - cur.close() - conn.close() - if config.SS_VERBOSE: - logging.info('port:%s ----> id:%s' % (id, rows[0][0])) - tran = str(dt_transfer[id]) - data = {'d': tran, 'node_id': config.NODE_ID, 'u': '0'} - url = config.API_URL + '/users/' + str(rows[0][0]) + '/traffic?key=' + config.API_PASS - data = urllib.urlencode(data) - req = urllib2.Request(url, data) - response = urllib2.urlopen(req) - the_page = response.read() - if config.SS_VERBOSE: - logging.info('%s - %s - %s' % (url, data, the_page)) - i += 1 - # online user count - data = {'count': i} - url = config.API_URL + '/nodes/' + config.NODE_ID + '/online_count?key=' + config.API_PASS - data = urllib.urlencode(data) - req = urllib2.Request(url, data) - response = urllib2.urlopen(req) - the_page = response.read() - if config.SS_VERBOSE: - logging.info('%s - %s - %s' % (url, data, the_page)) - - # load info - url = config.API_URL + '/nodes/' + config.NODE_ID + '/info?key=' + config.API_PASS - f = open("/proc/loadavg") - load = f.read().split() - f.close() - loadavg = load[0]+' '+load[1]+' '+load[2]+' '+load[3]+' '+load[4] - f = open("/proc/uptime") - time = f.read().split() - uptime = time[0] - f.close() - data = {'load': loadavg, 'uptime': uptime} - data = urllib.urlencode(data) - req = urllib2.Request(url, data) - response = urllib2.urlopen(req) - the_page = response.read() - if config.SS_VERBOSE: - logging.info('%s - %s - %s' % (url, data, the_page)) - + logging.info('db uploaded') @staticmethod def del_server_out_of_bound_safe(rows): @@ -185,7 +189,7 @@ def del_server_out_of_bound_safe(rows): else: if not config.CUSTOM_METHOD: row[7] = config.SS_METHOD - if server['method'] != row[7] : + if server['method'] != row[7]: # encryption method changed logging.info( 'db stop server at port [%d] reason: encryption method changed' % row[0]) @@ -226,32 +230,78 @@ def thread_push(): import traceback if config.SS_VERBOSE: traceback.print_exc() - logging.warn('db thread except:%s' % e) + logging.error('db thread except:%s' % e) finally: time.sleep(config.SYNCTIME) @staticmethod def pull_db_all_user(): - string = '' - for index in range(len(config.SS_SKIP_PORTS)): - port = config.SS_SKIP_PORTS[index] + if config.API_ENABLED: + rows = DbTransfer.pull_api_user() if config.SS_VERBOSE: - logging.info('db skipped port %s' % port) - if index == 0: - string = ' WHERE `port`<>%s' % port - else: - string = '%s AND `port`<>%s' % (string, port) - conn = cymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT, user=config.MYSQL_USER, - passwd=config.MYSQL_PASS, db=config.MYSQL_DB, charset='utf8') - cur = conn.cursor() - cur.execute('SELECT port, u, d, transfer_enable, passwd, switch, enable, method, email FROM %s%s ORDER BY `port` ASC' - % (config.MYSQL_USER_TABLE, string)) - rows = [] - for r in cur.fetchall(): - rows.append(list(r)) - # Release resources - cur.close() - conn.close() - if config.SS_VERBOSE: - logging.info('db downloaded') - return rows + logging.info('api downloaded') + return rows + else: + string = '' + for index in range(len(config.SS_SKIP_PORTS)): + port = config.SS_SKIP_PORTS[index] + if config.SS_VERBOSE: + logging.info('db skipped port %d' % port) + if index == 0: + string = ' WHERE `port`<>%d' % port + else: + string = '%s AND `port`<>%d' % (string, port) + conn = cymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT, user=config.MYSQL_USER, + passwd=config.MYSQL_PASS, db=config.MYSQL_DB, charset='utf8') + cur = conn.cursor() + cur.execute('SELECT port, u, d, transfer_enable, passwd, switch, enable, method, email FROM %s%s ORDER BY `port` ASC' + % (config.MYSQL_USER_TABLE, string)) + rows = [] + for r in cur.fetchall(): + rows.append(list(r)) + # Release resources + cur.close() + conn.close() + if config.SS_VERBOSE: + logging.info('db downloaded') + return rows + + @staticmethod + def pull_api_user(port=None): + url = config.API_URL + '/users?key=' + config.API_PASS + f = urllib.urlopen(url) + data = json.load(f) + f.close() + if port: + for user in data['data']: + if user['port'] == port: + return [ + user['port'], + user['u'], + user['d'], + user['transfer_enable'], + user['passwd'], + user['switch'], + user['enable'], + user['method'], + user['email'] + ] + else: + rows = [] + for user in data['data']: + if user['port'] in config.SS_SKIP_PORTS: + if config.SS_VERBOSE: + logging.info('api skipped port %d' % user['port']) + else: + rows.append([ + user['port'], + user['u'], + user['d'], + user['transfer_enable'], + user['passwd'], + user['switch'], + user['enable'], + user['method'], + user['email'] + ]) + return rows diff --git a/shadowsocks/servers.py b/shadowsocks/servers.py index 4ce19be..76c2f66 100755 --- a/shadowsocks/servers.py +++ b/shadowsocks/servers.py @@ -46,7 +46,7 @@ try: import config_example if not hasattr(config, 'CONFIG_VERSION') or config.CONFIG_VERSION != config_example.CONFIG_VERSION: - logging.error('Your configuration file is out-dated. Please update `config.py` according to `config.example.py`.') + logging.error('Your configuration file is out-dated. Please update `config.py` according to `config_example.py`.') sys.exit('config out-dated') except ImportError: logging.error('DO NOT delete the example configuration! Please re-upload it or use `git reset` to recover the file!') @@ -63,10 +63,17 @@ else: VERSION = subprocess.check_output(["git", "describe", "--tags"]) else: - VERSION = '3.0.1' + VERSION = '3.1.0' def main(): + if config.SS_FIREWALL_ENABLED: + if config.SS_FIREWALL_MODE == 'blacklist': + firewall_ports = config.SS_BAN_PORTS + else: + firewall_ports = config.SS_ALLOW_PORTS + else: + firewall_ports = None configer = { 'server': config.SS_BIND_IP, 'local_port': 1081, @@ -78,14 +85,17 @@ def main(): 'verbose': config.SS_VERBOSE, 'one_time_auth': config.SS_OTA, 'forbidden_ip': config.SS_FORBIDDEN_IP, - 'banned_ports': config.SS_BAN_PORTS + 'firewall_mode': config.SS_FIREWALL_MODE, + 'firewall_trusted': config.SS_FIREWALL_TRUSTED, + 'firewall_ports': firewall_ports } logging.info('-----------------------------------------') logging.info('Multi-User Shadowsocks Server Starting...') logging.info('Current Server Version: %s' % VERSION) - if config.PANEL_VERSION != 'V3': - logging.warn('Not support ss-panel version: %s' % config.PANEL_VERSION) - logging.warn('Please upgrade your ss-panel to V3 to enable all features.') + if config.API_ENABLED: + logging.warn('Now using MultiUser API as the user interface') + else: + logging.warn('Now using MySQL Database as the user interface') thread.start_new_thread(manager.run, (configer,)) time.sleep(1) thread.start_new_thread(DbTransfer.thread_db, ()) diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index f28a36f..76a1594 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -316,10 +316,20 @@ def _handle_stage_addr(self, data): return header_result = parse_header(data) if header_result is None: - raise Exception('U[%d] TCP Can not parse header' % - self._config['server_port']) + raise Exception('TCP Can not parse header') + addrtype, remote_addr, remote_port, header_length = header_result - if remote_port in self._config['banned_ports']: + if self._config['firewall_ports'] and self._config['server_port'] not in self._config['firewall_trusted']: + # Firewall enabled + if self._config['firewall_mode'] == 'blacklist' and remote_port in self._config['firewall_ports']: + firewall_blocked = True + elif self._config['firewall_mode'] == 'whitelist' and remote_port not in self._config['firewall_ports']: + firewall_blocked = True + else: + firewall_blocked = False + else: + firewall_blocked = False + if firewall_blocked: logging.warning('U[%d] TCP PORT BANNED: RP[%d] A[%s-->%s]' % ( self._config['server_port'], remote_port, self._client_address[0], common.to_str(remote_addr) @@ -330,6 +340,7 @@ def _handle_stage_addr(self, data): self._config['server_port'], remote_port, self._client_address[0], common.to_str(remote_addr) )) + if self._is_local is False: # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._ota_enable or (addrtype & ADDRTYPE_AUTH == ADDRTYPE_AUTH): @@ -631,7 +642,7 @@ def handle_event(self, sock, event): logging.warn('unknown socket') def _log_error(self, e): - logging.error('U[%d] UDP %s when handling connection from %s:%d' % + logging.error('U[%d] %s when handling connection from %s:%d' % (self._config['server_port'], e, self._client_address[0], self._client_address[1])) def destroy(self): diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index d468ee9..9b06b0c 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -185,7 +185,17 @@ def _handle_server(self): return addrtype, dest_addr, dest_port, header_length = header_result - if dest_port in self._config['banned_ports']: + if self._config['firewall_ports'] and self._config['server_port'] not in self._config['firewall_trusted']: + # Firewall enabled + if self._config['firewall_mode'] == 'blacklist' and dest_port in self._config['firewall_ports']: + firewall_blocked = True + elif self._config['firewall_mode'] == 'whitelist' and dest_port not in self._config['firewall_ports']: + firewall_blocked = True + else: + firewall_blocked = False + else: + firewall_blocked = False + if firewall_blocked: logging.warning('U[%d] UDP PORT BANNED: RP[%d] A[%s-->%s]' % ( self._config['server_port'], dest_port, client_address, common.to_str(dest_addr)