diff --git a/Basis/Methods.py b/Basis/Methods.py index d03c7d8..cd57a7d 100644 --- a/Basis/Methods.py +++ b/Basis/Methods.py @@ -63,7 +63,7 @@ ssMethods = { # methods support of different Shadowsocks project ssAllMethods = set() [ssAllMethods.update(ssMethods[x]) for x in ssMethods] -ssAllMethods = list(ssAllMethods) # methods of Shadowsocks +ssAllMethods = sorted(list(ssAllMethods)) # methods of Shadowsocks ssrMethods = [ # methods of ShadowsocksR 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', diff --git a/Basis/Process.py b/Basis/Process.py index 45339f1..f050e19 100644 --- a/Basis/Process.py +++ b/Basis/Process.py @@ -49,7 +49,7 @@ class Process(object): isStart: Start the process after class init complete. Attributes: - id, workDir, output + id, cmd, env, file, workDir, output """ output = None # sub process output if capture is True __capture = None # capture the sub process output or not @@ -97,34 +97,34 @@ class Process(object): cmd: str or list or None = None, env: dict or None = None, file: dict or list or None = None) -> None: self.id = genFlag(length = 12) if taskId == '' else taskId self.workDir = workDir - self.__env = copy.copy(env) # depth = 1 - self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd) # depth = 1 - self.__file = copy.deepcopy([file] if type(file) == dict else file) # depth = 2 + self.env = copy.copy(env) # depth = 1 + self.cmd = copy.copy([cmd] if type(cmd) == str else cmd) # depth = 1 + self.file = copy.deepcopy([file] if type(file) == dict else file) # depth = 2 self.__checkWorkDir() # ensure the working direction is normal - logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.__cmd, self.__env)) - if self.__file is not None: - if len(self.__file) > 1: - logging.debug('[%s] Manage %i files' % (self.id, len(self.__file))) - for file in self.__file: # traverse all files + logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.cmd, self.env)) + if self.file is not None: + if len(self.file) > 1: + logging.debug('[%s] Manage %i files' % (self.id, len(self.file))) + for file in self.file: # traverse all files if not isStart: # don't print log twice logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content'])) if isStart: self.start() def setCmd(self, cmd: str or list) -> None: - self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd) - logging.info('[%s] Process setting command -> %s' % (self.id, self.__cmd)) + self.cmd = copy.copy([cmd] if type(cmd) == str else cmd) + logging.info('[%s] Process setting command -> %s' % (self.id, self.cmd)) def setEnv(self, env: dict or None) -> None: - self.__env = copy.copy(env) - logging.info('[%s] Process setting environ -> %s' % (self.id, self.__env)) + self.env = copy.copy(env) + logging.info('[%s] Process setting environ -> %s' % (self.id, self.env)) def setFile(self, file: dict or list or None) -> None: - self.__file = copy.deepcopy([file] if type(file) == dict else file) - if self.__file is None: + self.file = copy.deepcopy([file] if type(file) == dict else file) + if self.file is None: logging.info('[%s] Process setting file -> None' % self.id) return - for file in self.__file: # traverse all files + for file in self.file: # traverse all files logging.info('[%s] Process setting file %s -> %s' % (self.id, file['path'], file['content'])) def start(self, isCapture: bool = True) -> None: @@ -132,16 +132,16 @@ class Process(object): logging.debug('[%s] Process ready to start (%s)' % (self.id, ( 'with output capture' if self.__capture else 'without output capture' ))) - if self.__cmd is None: # ERROR CASE + if self.cmd is None: # ERROR CASE logging.error('[%s] Process miss start command' % self.id) raise RuntimeError('miss start command') if self.__process is not None and self.__process.poll() is None: # ERROR CASE logging.error('[%s] Sub process is still running' % self.id) raise RuntimeError('sub process is still running') - if self.__env is not None and 'PATH' not in self.__env and '/' not in self.__cmd[0]: # WARNING CASE + if self.env is not None and 'PATH' not in self.env and '/' not in self.cmd[0]: # WARNING CASE logging.warning('[%s] Executable file in relative path but miss PATH in environ' % self.id) - if self.__file is not None: # create and write file contents - for file in self.__file: + if self.file is not None: # create and write file contents + for file in self.file: with open(file['path'], 'w', encoding = 'utf-8') as fileObject: # save file content fileObject.write(file['content']) logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content'])) @@ -153,7 +153,7 @@ class Process(object): else: # discard all the output of sub process stdout = DEVNULL stderr = DEVNULL - self.__process = Popen(self.__cmd, env = self.__env, stdout = stdout, + self.__process = Popen(self.cmd, env = self.env, stdout = stdout, stderr = stderr, preexec_fn = None if libcPath is None else Process.__preExec) logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid)) @@ -195,6 +195,6 @@ class Process(object): self.__deleteFile(self.__logfile) except: logging.error('[%s] Failed to read capture log file -> %s' % (self.id, self.__logfile)) - if self.__file is not None: # with config file - for file in self.__file: + if self.file is not None: # with config file + for file in self.file: self.__deleteFile(file['path']) diff --git a/Builder/__init__.py b/Builder/__init__.py index a8be8bb..07dc6db 100644 --- a/Builder/__init__.py +++ b/Builder/__init__.py @@ -11,18 +11,14 @@ from Basis.Logger import logging from Basis.Process import Process from Basis.Functions import genFlag, getAvailablePort -default = { - 'workDir': '/tmp/ProxyC', - 'bindAddr': '127.0.0.1', - 'binDir': '/bin:/usr/bin:/usr/local/bin', -} - class Builder(object): """ Build the proxy client process and expose socks5 port. Arguments: - proxy: Proxy node information. + proxyType: Proxy node type. + + proxyInfo: Proxy node information. bind: Socks5 proxy bind address. @@ -30,14 +26,12 @@ class Builder(object): taskId: Task ID, defaults to 12 random characters length. - isStart: Start the process after class init complete. - Attributes: id, proxyType, proxyInfo, socksAddr, socksPort, output """ output = None - def __loadClient(self, isStart: bool): + def __loadClient(self): loadFunction = { 'ss': Shadowsocks.load, 'ssr': ShadowsocksR.load, @@ -52,23 +46,22 @@ class Builder(object): 'addr': self.socksAddr, 'port': self.socksPort, }, configFile) - envVar['PATH'] = default['binDir'] + envVar['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' fileObject = { 'path': configFile, 'content': fileContent } - self.__process = Process(self.__workDir, taskId = self.id, - isStart = isStart, cmd = command, env = envVar, file = fileObject) + self.__process = Process(self.__workDir, taskId = self.id, cmd = command, env = envVar, file = fileObject) - def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '', isStart: bool = True, - bind: str = default['bindAddr'], workDir: str = default['workDir']) -> None: + def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '', + bind: str = '127.0.0.1', workDir: str = '/tmp/ProxyC') -> None: self.id = genFlag(length = 12) if taskId == '' else taskId self.__workDir = workDir self.proxyType = proxyType self.proxyInfo = copy.copy(proxyInfo) self.socksAddr = bind self.socksPort = getAvailablePort() - self.__loadClient(isStart) + self.__loadClient() def status(self) -> bool: return self.__process.status() diff --git a/Tester/Shadowsocks.py b/Tester/Shadowsocks.py new file mode 100644 index 0000000..bf6ca62 --- /dev/null +++ b/Tester/Shadowsocks.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import json +import base64 +from Builder import Shadowsocks +from Basis.Logger import logging +from Basis.Process import Process +from Basis.Methods import ssMethods, ssAllMethods +from Basis.Functions import genFlag, getAvailablePort + +settings = { + 'serverBind': '127.0.0.1', + 'clientBind': '127.0.0.1', # aka socks5 address + 'workDir': '/tmp/ProxyC' +} + + +def loadConfig(proxyInfo: dict) -> dict: # load basic config option + config = { + 'server': proxyInfo['server'], + 'server_port': proxyInfo['port'], # type -> int + 'method': proxyInfo['method'], + 'password': proxyInfo['passwd'], + } + if proxyInfo['plugin'] is not None: # with plugin + config['plugin'] = proxyInfo['plugin']['type'] + config['plugin_opts'] = proxyInfo['plugin']['param'] + return config + + +def pluginUdp(plugin: str, pluginParam: str) -> bool: # whether the plugin uses UDP + if plugin in ['obfs-local', 'simple-tls', 'ck-client', 'gq-client', 'mtt-client', 'rabbit-plugin']: + return False # UDP is not used + if plugin in ['v2ray-plugin', 'xray-plugin', 'gost-plugin']: + if 'mode=quic' not in pluginParam.split(';'): # non-quic mode does not use UDP + return False + return True # UDP is assumed by default + + +def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: + config = loadConfig(proxyInfo) + if isUdp: # proxy UDP traffic + config['mode'] = 'tcp_and_udp' + return config, ['ss-rust-server', '-v'] + + +def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: + config = loadConfig(proxyInfo) + if isUdp: # proxy UDP traffic + config['mode'] = 'tcp_and_udp' + return config, ['ss-libev-server', '-v'] + + +def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: + config = loadConfig(proxyInfo) + mbedtlsMethods = [ + 'aes-128-cfb128', + 'aes-192-cfb128', + 'aes-256-cfb128', + 'camellia-128-cfb128', + 'camellia-192-cfb128', + 'camellia-256-cfb128', + ] + if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:` + config['method'] = 'mbedtls:' + config['method'] + if config['method'] in ['idea-cfb', 'seed-cfb']: # only older versions of openssl are supported + config['extra_opts'] = '--libopenssl=libcrypto.so.1.0.0' + if not isUdp: + config['no_udp'] = True # UDP traffic is not proxied + config['shadowsocks'] = 'ss-python-server' + return config, ['ss-bootstrap-server', '--debug', '-vv'] + + +def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]: + config = loadConfig(proxyInfo) + if not isUdp: + config['no_udp'] = True # UDP traffic is not proxied + config['shadowsocks'] = 'ss-python-legacy-server' + return config, ['ss-bootstrap-server', '--debug', '-vv'] + + +def loadPassword(method: str) -> str: + b64 = lambda x: base64.b64encode(x.encode(encoding = 'utf-8')).decode(encoding = 'utf-8') + if not method.startswith('2022-blake3-'): + return genFlag(length = 8) + if method == '2022-blake3-aes-128-gcm': + return b64(genFlag(length = 16)) + return b64(genFlag(length = 32)) # three other 2022-blake3-* methods use 32 byte length password + + +def testConnection(serverType: str, clientType: str, method: str) -> dict: + proxyInfo = { + 'server': settings['serverBind'], + 'port': getAvailablePort(), + 'method': method, + 'passwd': loadPassword(method), + 'plugin': None + } + socksInfo = { + 'addr': settings['clientBind'], + 'port': getAvailablePort() + } + + ssClientLoad = { + 'ss-rust': Shadowsocks.ssRust, + 'ss-libev': Shadowsocks.ssLibev, + 'ss-python': Shadowsocks.ssPython, + 'ss-python-legacy': Shadowsocks.ssPythonLegacy + }[clientType] + ssConfig, ssClient = ssClientLoad(proxyInfo, socksInfo, isUdp = False) + clientFile = os.path.join(settings['workDir'], '%s_%s_%s' % (serverType, clientType, method) + '_client.json') + client = Process(settings['workDir'], cmd = ssClient + ['-c', clientFile], file = { + 'path': clientFile, + 'content': json.dumps(ssConfig) + }, isStart = False) + + ssServerLoad = { + 'ss-rust': ssRust, + 'ss-libev': ssLibev, + 'ss-python': ssPython, + 'ss-python-legacy': ssPythonLegacy + }[serverType] + ssConfig, ssServer = ssServerLoad(proxyInfo, isUdp = False) + serverFile = os.path.join(settings['workDir'], '%s_%s_%s' % (serverType, clientType, method) + '_server.json') + server = Process(settings['workDir'], cmd = ssServer + ['-c', serverFile], file = { + 'path': serverFile, + 'content': json.dumps(ssConfig) + }, isStart = False) + + testInfo = { + 'title': 'Shadowsocks test: {%s <- %s -> %s}' % (serverType, method, clientType), + 'socks': socksInfo, + 'client': client, + 'server': server, + } + logging.debug('New shadowsocks test connection -> %s' % testInfo) + return testInfo + + +def load(isExtra: bool = False) -> list: + result = [] + if isExtra: + for ssServer in ssMethods: + for method in ssMethods[ssServer]: + for ssClient in ssMethods: + if method not in ssMethods[ssClient]: continue + result.append(testConnection(ssServer, ssClient, method)) + else: + for method in ssAllMethods: + for ssType in ssMethods: + if method not in ssMethods[ssType]: continue + result.append(testConnection(ssType, ssType, method)) + break + return result diff --git a/test.py b/test.py index 5209b26..c24ba53 100755 --- a/test.py +++ b/test.py @@ -1,160 +1,53 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os -import json import time import requests -from Builder import Shadowsocks +from Tester import Shadowsocks from Basis.Logger import logging -from Basis.Process import Process -from Basis.Methods import ssMethods -from Basis.Functions import getAvailablePort -ssPassword = '' -def loadConfig(serverPort: int, method: str) -> dict: # load basic config option - config = { - 'server': '127.0.0.1', - 'server_port': serverPort, # type -> int - 'method': method, - 'password': ssPassword, - } - return config +def test(testObj: dict) -> None: + logging.warning(testObj['title']) + testObj['client'].start() + testObj['server'].start() + time.sleep(1) -def ssRust(serverPort: int, method: str) -> tuple[dict, list]: - config = loadConfig(serverPort, method) - return config, ['ss-rust-server', '-v'] - - -def ssLibev(serverPort: int, method: str) -> tuple[dict, list]: - config = loadConfig(serverPort, method) - return config, ['ss-libev-server', '-v'] - - -def ssPython(serverPort: int, method: str) -> tuple[dict, list]: - config = loadConfig(serverPort, method) - mbedtlsMethods = [ - 'aes-128-cfb128', - 'aes-192-cfb128', - 'aes-256-cfb128', - 'camellia-128-cfb128', - 'camellia-192-cfb128', - 'camellia-256-cfb128', - ] - if config['method'] in mbedtlsMethods: # mbedtls methods should use prefix `mbedtls:` - config['method'] = 'mbedtls:' + config['method'] - if config['method'] in ['idea-cfb', 'seed-cfb']: # Only older versions of openssl are supported - config['extra_opts'] = '--libopenssl=libcrypto.so.1.0.0' - config['shadowsocks'] = 'ss-python-server' - return config, ['ss-bootstrap-server', '--debug', '-vv'] - - -def ssPythonLegacy(serverPort: int, method: str) -> tuple[dict, list]: - config = loadConfig(serverPort, method) - config['shadowsocks'] = 'ss-python-legacy-server' - return config, ['ss-bootstrap-server', '--debug', '-vv'] - - -def loadTest(serverType: str, clientType: str, method: str, timeout: float) -> None: - logging.warning('Shadowsocks test -> server = %s | client = %s | method = %s' % (serverType, clientType, method)) - global ssPassword - if method.startswith('2022-blake3-'): - ssPassword = 'ZG5vbWQzNDNkbm9tZDM0M2Rub21kMzQzZG5vbWQzNDM=' # base64 encode of 'dnomd343' * 4 - if method == '2022-blake3-aes-128-gcm': - ssPassword = 'ZG5vbWQzNDNkbm9tZDM0Mw==' # base64 encode of 'dnomd343' * 2 - else: - ssPassword = 'dnomd343' - title = '%s_%s_%s' % (serverType, clientType, method) - workDir = '/tmp/ProxyC' - serverPort = getAvailablePort() - socksPort = getAvailablePort() - proxyInfo = { - 'server': '127.0.0.1', - 'port': serverPort, - 'method': method, - 'passwd': ssPassword, - 'plugin': None - } - socksInfo = { - 'addr': '127.0.0.1', - 'port': socksPort - } - ssClientLoad = { - 'ss-rust': Shadowsocks.ssRust, - 'ss-libev': Shadowsocks.ssLibev, - 'ss-python': Shadowsocks.ssPython, - 'ss-python-legacy': Shadowsocks.ssPythonLegacy - }[clientType] - ssConfig, ssClient = ssClientLoad(proxyInfo, socksInfo, isUdp = False) - client = Process(workDir, cmd = ssClient + ['-c', os.path.join(workDir, title + '_client.json')], file = { - 'path': os.path.join(workDir, title + '_client.json'), - 'content': json.dumps(ssConfig) - }, isStart = False) - - ssServerLoad = { - 'ss-rust': ssRust, - 'ss-libev': ssLibev, - 'ss-python': ssPython, - 'ss-python-legacy': ssPythonLegacy - }[serverType] - ssConfig, ssServer = ssServerLoad(serverPort, method) - server = Process(workDir, cmd = ssServer + ['-c', os.path.join(workDir, title + '_server.json')], file = { - 'path': os.path.join(workDir, title + '_server.json'), - 'content': json.dumps(ssConfig) - }, isStart = False) - - client.start() - server.start() - time.sleep(timeout) errFlag = False try: request = requests.get( 'http://baidu.com', proxies = { - 'http': 'socks5://127.0.0.1:%i' % socksPort, - 'https': 'socks5://127.0.0.1:%i' % socksPort + 'http': 'socks5://127.0.0.1:%i' % testObj['socks']['port'], + 'https': 'socks5://127.0.0.1:%i' % testObj['socks']['port'], }, timeout = 10 ) request.raise_for_status() - logging.info('socks5 127.0.0.1:%i -> ok' % socksPort) + logging.info('socks5 127.0.0.1:%i -> ok' % testObj['socks']['port']) except Exception as exp: - logging.error('socks5 127.0.0.1:%i -> error' % socksPort) + logging.error('socks5 127.0.0.1:%i -> error' % testObj['socks']['port']) logging.error('requests exception\n' + str(exp)) errFlag = True - client.quit() - server.quit() - if errFlag: - logging.error('client capture output\n' + str(client.output)) - logging.error('server capture output\n' + str(server.output)) - - -def test_1() -> None: - for ssType in ssMethods: - for method in ssMethods[ssType]: - timeout = 0.1 - if 'python' in ssType: timeout = 0.3 - if 'python-legacy' in ssType: - timeout = 0.8 - if method == 'table' or method == 'salsa20-ctr': timeout = 2 - loadTest(ssType, ssType, method, timeout) - -def test_2() -> None: - for ssServer in ssMethods: - for method in ssMethods[ssServer]: - for ssClient in ssMethods: - if method not in ssMethods[ssClient]: continue - timeout = 0.1 - if 'python' in ssServer or 'python' in ssClient: timeout = 0.3 - if method == 'table': timeout = 0.8 - if 'python-legacy' in ssServer or 'python-legacy' in ssClient: timeout = 1 - if method == 'salsa20-ctr': timeout = 3 - loadTest(ssServer, ssClient, method, timeout) - - -# test_1() -test_2() -# loadTest('ss-python-legacy', 'ss-python-legacy', 'salsa20-ctr', 3) + testObj['client'].quit() + testObj['server'].quit() + if errFlag: + logging.warning('client info') + logging.error('command -> ' + str(testObj['client'].cmd)) + logging.error('envVar -> ' + str(testObj['client'].env)) + logging.error('file -> ' + str(testObj['client'].file)) + logging.warning('client capture output') + logging.error('\n' + str(testObj['client'].output)) + logging.warning('server info') + logging.error('command -> ' + str(testObj['server'].cmd)) + logging.error('envVar -> ' + str(testObj['server'].env)) + logging.error('file -> ' + str(testObj['server'].file)) + logging.warning('server capture output') + logging.error('\n' + str(testObj['server'].output)) + + +testList = Shadowsocks.load(isExtra = True) +for testObject in testList: + test(testObject)