From 24e42c294ebdd3aac8035ac6d5c746d325e08d26 Mon Sep 17 00:00:00 2001 From: dnomd343 Date: Fri, 29 Jul 2022 20:09:20 +0800 Subject: [PATCH] feat: plugin test process --- Basis/Functions.py | 5 + Basis/Methods.py | 18 +++ Builder/Shadowsocks.py | 5 +- Dockerfile | 16 +-- Tester/Plugin.py | 311 +++++++++++++++++++++++++++++++++++++++++ Tester/Shadowsocks.py | 45 +++--- test.py | 8 +- 7 files changed, 374 insertions(+), 34 deletions(-) create mode 100644 Tester/Plugin.py diff --git a/Basis/Functions.py b/Basis/Functions.py index 25da675..4fdc369 100644 --- a/Basis/Functions.py +++ b/Basis/Functions.py @@ -4,9 +4,14 @@ import time import psutil import random +import hashlib from Basis.Logger import logging +def md5Sum(data: str, encode: str = 'UTF-8') -> str: + return hashlib.md5(data.encode(encoding = encode)).hexdigest() + + def genFlag(length: int = 12) -> str: # generate random task flag flag = '' for i in range(0, length): diff --git a/Basis/Methods.py b/Basis/Methods.py index cd57a7d..a9a7d4d 100644 --- a/Basis/Methods.py +++ b/Basis/Methods.py @@ -91,3 +91,21 @@ ssrObfuscations = [ # obfuscations of ShadowsocksR (obfs) 'plain', 'http_post', 'http_simple', 'random_head', 'tls_simple', 'tls1.2_ticket_auth', 'tls1.2_ticket_fastauth', ] + +plugin = { + 'simple-obfs': ['obfs-local', 'obfs-server'], + 'simple-tls': ['simple-tls'], + 'v2ray': ['v2ray-plugin'], + 'xray': ['xray-plugin'], + 'kcptun': ['kcptun-client', 'kcptun-server'], + 'gost': ['gost-plugin'], + 'cloak': ['ck-client', 'ck-server'], + 'go-quiet': ['gq-client', 'gq-server'], + 'mos-tls-tunnel': ['mtt-client', 'mtt-server'], + 'rabbit': ['rabbit-plugin', 'rabbit'], + 'qtun': ['qtun-client', 'qtun-server'], + 'gun': ['gun-plugin'], +} + +plugin = {x: [plugin[x][0], plugin[x][1 if len(plugin[x]) == 2 else 0]] for x in plugin} +plugin = {x: {'client': plugin[x][0], 'server': plugin[x][1]} for x in plugin} # format plugin info diff --git a/Builder/Shadowsocks.py b/Builder/Shadowsocks.py index 8499ca6..1464a33 100644 --- a/Builder/Shadowsocks.py +++ b/Builder/Shadowsocks.py @@ -83,11 +83,10 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, for client in ssMethods: # traverse all shadowsocks client if proxyInfo['method'] not in ssMethods[client]: continue - ssLoadConfig = { + ssConfig, ssClient = { 'ss-rust': ssRust, 'ss-libev': ssLibev, 'ss-python': ssPython, 'ss-python-legacy': ssPythonLegacy - }[client] - ssConfig, ssClient = ssLoadConfig(proxyInfo, socksInfo, isUdp) # generate config file + }[client](proxyInfo, socksInfo, isUdp) # generate config file return ssClient + ['-c', configFile], json.dumps(ssConfig), {} # tuple[command, fileContent, envVar] diff --git a/Dockerfile b/Dockerfile index 09d0721..f392f3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -132,8 +132,8 @@ RUN BZIP2=-9 tar czf /packages.tar.gz ./site-packages/ FROM rust:1.62-alpine3.16 AS plugin-1 RUN \ apk add git && mkdir /plugins/ && \ - git clone https://github.com/shadowsocks/simple-obfs.git && \ - git clone https://github.com/shadowsocks/qtun.git + git clone https://github.com/shadowsocks/qtun.git && \ + git clone https://github.com/shadowsocks/simple-obfs.git # Compile simple-obfs RUN \ apk add autoconf automake build-base libev-dev libtool linux-headers && \ @@ -174,14 +174,12 @@ RUN \ mv ./kcptun-client ./kcptun-server /plugins/ # Compile gost-plugin RUN \ - cd ./gost-plugin/ && \ - git checkout ${GOST_PLUGIN} -b build && \ + cd ./gost-plugin/ && git checkout ${GOST_PLUGIN} && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-X main.VERSION=$(git describe --tags) -s -w" && \ mv ./gost-plugin /plugins/ # Compile GoQuiet RUN \ - cd ./GoQuiet/ && \ - go mod init github.com/cbeuw/GoQuiet && \ + cd ./GoQuiet/ && go mod init github.com/cbeuw/GoQuiet && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-X main.version=$(git describe --tags) -s -w" ./cmd/gq-client && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-X main.version=$(git describe --tags) -s -w" ./cmd/gq-server && \ mv ./gq-client ./gq-server /plugins/ @@ -213,6 +211,7 @@ RUN upx -9 /plugins/* # Compile sip003 plugins (part3 -> go1.17) FROM golang:1.17-alpine3.16 AS plugin-3 ENV SIMPLE_TLS="v0.7.0" +ENV CLOAK="v2.6.0" RUN \ apk add git && mkdir /plugins/ && \ git clone https://github.com/cbeuw/Cloak.git && \ @@ -220,8 +219,7 @@ RUN \ git clone https://github.com/IrineSistiana/simple-tls.git # Compile simple-tls RUN \ - cd ./simple-tls/ && \ - git checkout ${SIMPLE_TLS} -b build && \ + cd ./simple-tls/ && git checkout ${SIMPLE_TLS} && \ sed -i 's/version = "unknown\/dev"/version = "'$(git describe --tags)'"/g' main.go && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" && \ mv ./simple-tls /plugins/ @@ -232,7 +230,7 @@ RUN \ mv ./xray-plugin /plugins/ # Compile Cloak RUN \ - cd ./Cloak/ && \ + cd ./Cloak/ && git checkout ${CLOAK} && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-X main.version=$(git describe --tags) -s -w" ./cmd/ck-client && \ env CGO_ENABLED=0 go build -trimpath -ldflags "-X main.version=$(git describe --tags) -s -w" ./cmd/ck-server && \ mv ./ck-client ./ck-server /plugins/ diff --git a/Tester/Plugin.py b/Tester/Plugin.py new file mode 100644 index 0000000..e4ae4af --- /dev/null +++ b/Tester/Plugin.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import re +import json +from Basis.Logger import logging +from Basis.Methods import plugin +from Basis.Process import Process +from Basis.Functions import genFlag, getAvailablePort + +settings = { + 'serverBind': '127.0.0.1', + 'workDir': '/tmp/ProxyC' +} + +pluginParams = { + 'SITE': 'www.bing.com', + 'PATH': '/test', + 'HOST': '343.re', + 'CERT': '/etc/ssl/certs/343.re/fullchain.pem', + 'KEY': '/etc/ssl/certs/343.re/privkey.pem', + 'PASSWD': 'dnomd343', +} + +pluginConfig = { + 'simple-obfs': { + 'http mode': [ + 'obfs=http', + 'obfs=http;obfs-host=${SITE}', + ], + 'tls mode': [ + 'obfs=tls', + 'obfs=tls;obfs-host=${SITE}', + ], + 'http mode (with uri)': [ + 'obfs=http', + 'obfs=http;obfs-host=${SITE};obfs-uri=${PATH}', + ], + 'http mode (POST method)': [ + 'obfs=http', + 'obfs=http;http-method=POST;obfs-host=${SITE}', + ], + }, + 'simple-tls': { + 'http mode': [ + 's;n=${HOST};cert=${CERT};key=${KEY}', + 'n=${HOST}', + ], + 'websocket mode': [ + 's;n=${HOST};cert=${CERT};key=${KEY};ws;ws-path=${PATH}', + 'n=${HOST};ws;ws-path=${PATH}', + ], + 'http mode (with mux)': [ + 's;cert=${CERT};key=${KEY};n=${HOST}', + 'n=${HOST};mux=8', + ], + 'http mode (with auth key)': [ + 's;n=${HOST};cert=${CERT};key=${KEY};auth=${PASSWD}', + 'n=${HOST};auth=${PASSWD}', + ], + }, + 'v2ray': { + 'websocket mode': [ + 'server', + '', + ], + 'websocket mode (with tls)': [ + 'server;tls;host=${HOST};cert=${CERT};key=${KEY}', + 'tls;host=${HOST}', + ], + 'websocket mode (with path)': [ + 'server;path=${PATH}', + 'path=${PATH}', + ], + 'quic mode': [ + 'server;mode=quic;host=${HOST};cert=${CERT};key=${KEY}', + 'mode=quic;host=${HOST}', + ], + }, + 'xray': { + 'websocket mode': [ + 'server', + '', + ], + 'websocket mode (with tls)': [ + 'server;tls;host=${HOST};cert=${CERT};key=${KEY}', + 'tls;host=${HOST}', + ], + 'websocket mode (with path)': [ + 'server;path=${PATH}', + 'path=${PATH}', + ], + 'quic mode': [ + 'server;mode=quic;host=${HOST};cert=${CERT};key=${KEY}', + 'mode=quic;host=${HOST}', + ], + 'grpc mode': [ + 'server;mode=grpc', + 'mode=grpc', + ], + 'grpc mode (with tls)': [ + 'server;tls;mode=grpc;host=${HOST};cert=${CERT};key=${KEY}', + 'tls;mode=grpc;host=${HOST}', + ], + }, + 'kcptun': { + 'basic mode': [ + '', '' # aka fast mode + ], + 'with nocomp': [ + 'nocomp', 'nocomp' + ], + 'with key': [ + 'key=${PASSWD}', 'key=${PASSWD}' + ], + 'with multi conn': [ + 'conn=8', 'conn=8' + ], + }, + 'gost': { + 'ws mode': [ + 'server;mode=ws', + 'mode=ws', + ], + 'mws mode': [ + 'server;mode=mws', + 'mode=mws;mux=1', + ], + 'tls mode': [ + 'server;cert=${CERT};key=${KEY};mode=tls', + 'serverName=${HOST};mode=tls', + ], + 'mtls mode': [ + 'server;cert=${CERT};key=${KEY};mode=mtls', + 'serverName=${HOST};mode=mtls;mux=1', + ], + 'xtls mode': [ + 'server;cert=${CERT};key=${KEY};mode=xtls', + 'serverName=${HOST};mode=xtls', + ], + 'h2 mode': [ + 'server;cert=${CERT};key=${KEY};mode=h2', + 'serverName=${HOST};mode=h2', + ], + 'wss mode': [ + 'server;cert=${CERT};key=${KEY};mode=wss', + 'serverName=${HOST};mode=wss', + ], + 'mwss mode': [ + 'server;cert=${CERT};key=${KEY};mode=mwss', + 'serverName=${HOST};mode=mwss;mux=1', + ], + 'quic mode': [ + 'server;cert=${CERT};key=${KEY};mode=quic', + 'serverName=${HOST};mode=quic', + ], + 'grpc mode': [ + 'server;cert=${CERT};key=${KEY};mode=grpc', + 'serverName=${HOST};mode=grpc', + ], + }, + 'cloak': {}, + 'go-quiet': { + 'chrome fingerprint': [ + os.path.join(settings['workDir'], 'go-quiet_config_${RANDOM}.json'), + 'ServerName=${SITE};key=${PASSWD};TicketTimeHint=300;Browser=chrome', + ], + 'firefox fingerprint': [ + os.path.join(settings['workDir'], 'go-quiet_config_${RANDOM}.json'), + 'ServerName=${SITE};key=${PASSWD};TicketTimeHint=300;Browser=firefox', + ], + }, + 'mos-tls-tunnel': { + 'basic mode': [ + 'cert=${CERT};key=${KEY}', + 'n=${HOST}', + ], + 'basic mode (with mux)': [ + 'cert=${CERT};key=${KEY};mux', + 'n=${HOST};mux', + ], + 'wss mode': [ + 'wss;cert=${CERT};key=${KEY}', + 'wss;n=${HOST}', + ], + 'wss mode (with path)': [ + 'wss;cert=${CERT};key=${KEY};wss-path=${PATH}', + 'wss;n=${HOST};wss-path=${PATH}', + ], + 'wss mode (with mux)': [ + 'wss;cert=${CERT};key=${KEY};mux', + 'wss;n=${HOST};mux', + ], + }, + 'rabbit': { + 'basic mode': [ + '${RABBIT_PORT}', + 'serviceAddr=127.0.0.1:${RABBIT_PORT};password=${PASSWD};tunnelN=6' # emulate SIP003 (ipv4 localhost) + ], + }, + 'qtun': { + 'basic mode': [ + 'cert=${CERT};key=${KEY}', + 'host=${HOST}', + ], + }, + 'gun': { + 'basic mode': [ + 'server:cleartext', + 'client:cleartext', + ], + 'basic mode (with tls)': [ + 'server:${CERT}:${KEY}', + 'client:${HOST}', + ], + }, +} + + +def kcptunLoad() -> None: + for kcptunMode in ['fast', 'fast2', 'fast3', 'normal', 'manual']: # traverse kcptun modes + pluginConfig['kcptun'][kcptunMode + ' mode'] = ['mode=' + kcptunMode, 'mode=' + kcptunMode] + for kcptunCrypt in ['aes', 'aes-128', 'aes-192', 'salsa20', 'blowfish', + 'twofish', 'cast5', '3des', 'tea', 'xtea', 'xor', 'none']: # traverse kcptun crypt + pluginConfig['kcptun']['with %s crypt' % kcptunCrypt] = ['crypt=' + kcptunCrypt, 'crypt=' + kcptunCrypt] + + +def cloakLoad() -> None: + ckKey = os.popen('ck-server -key').read() # generate public and private key for cloak + pluginParams['CK_PUBLIC'] = re.search(r'\s+(\S+)$', ckKey.split('\n')[0])[1] + pluginParams['CK_PRIVATE'] = re.search(r'\s+(\S+)$', ckKey.split('\n')[1])[1] + pluginParams['CK_UID'] = re.search(r'\s+(\S+)\n', os.popen('ck-server -uid').read())[1] # generate uid for clock + logging.info('generate clock uid -> %s' % pluginParams['CK_UID']) + logging.info('generate clock key -> %s (Public) | %s (Private)' % ( + pluginParams['CK_PUBLIC'], pluginParams['CK_PRIVATE'] + )) + ckPrefix = 'UID=${CK_UID};PublicKey=${CK_PUBLIC};ServerName=${SITE};' # cloak plugin's basic command + ckConfigPath = os.path.join(settings['workDir'], 'cloak_config_${RANDOM}.json') # clock server's config + for ckMethod in ['plain', 'aes-128-gcm', 'aes-256-gcm', 'chacha20-poly1305']: # traverse cloak encrypt methods + pluginConfig['cloak']['%s method' % ckMethod] = [ + ckConfigPath, ckPrefix + 'EncryptionMethod=' + ckMethod + ] + for ckBrowser in ['chrome', 'firefox']: # traverse cloak browser fingerprints + pluginConfig['cloak']['%s fingerprint' % ckBrowser] = [ + ckConfigPath, ckPrefix + 'EncryptionMethod=plain;BrowserSig=' + ckBrowser + ] + pluginConfig['cloak']['single connection'] = [ # disable connection multiplexing + ckConfigPath, ckPrefix + 'EncryptionMethod=plain;NumConn=0' + ] + + +def ssInject(server: Process, pluginInfo: dict) -> Process: + if pluginInfo['type'] == 'cloak': + ckConfig = paramFill(json.dumps({ + 'BypassUID': ['${CK_UID}'], + 'RedirAddr': '${SITE}', + 'PrivateKey': '${CK_PRIVATE}' + })) + server.setFile(server.file + [{ # add cloak config file + 'path': pluginInfo['server']['param'], + 'content': ckConfig + }]) + elif pluginInfo['type'] == 'go-quiet': + server.setFile(server.file + [{ # add gq-quiet config file + 'path': pluginInfo['server']['param'], + 'content': paramFill(json.dumps({'key': '${PASSWD}'})) + }]) + elif pluginInfo['type'] == 'rabbit': # hijack rabbit plugin config + ssConfig = json.loads(server.file[0]['content']) # modify origin config + ssConfig.pop('plugin') # remove plugin option + ssConfig.pop('plugin_opts') + rabbitBind = ('[%s]' if ':' in ssConfig['server'] else '%s') % ssConfig['server'] # ipv4 / [ipv6] + rabbitPort = ssConfig['server_port'] + ssConfig['server'] = '127.0.0.1' # SIP003 use ipv4 localhost for communication + ssConfig['server_port'] = int(pluginInfo['server']['param']) # aka ${RABBIT_PORT} + server.file[0]['content'] = json.dumps(ssConfig) + server.setCmd(['sh', '-c', paramFill( + 'rabbit -mode s -password ${PASSWD} -rabbit-addr %s:%s' % (rabbitBind, rabbitPort) # start rabbit-tcp + ) + ' &\nexec ' + ' '.join(server.cmd)]) # shadowsocks as main process (rabbit as sub process) + return server + + +def paramFill(param: str) -> str: + if '${RANDOM}' in param: # refresh RANDOM field + pluginParams['RANDOM'] = genFlag(length = 8) + for field in pluginParams: + param = param.replace('${%s}' % field, pluginParams[field]) # fill ${XXX} field + return param + + +def load(): + cloakLoad() # init cloak config + kcptunLoad() # init kcptun config + for pluginType in pluginConfig: + for pluginTest, pluginTestInfo in pluginConfig[pluginType].items(): # traverse all plugin test item + if pluginType == 'rabbit': + pluginParams['RABBIT_PORT'] = str(getAvailablePort()) # allocate port before rabbit plugin start + yield { + 'type': pluginType, + 'caption': pluginTest, + 'server': { # plugin info for server + 'type': plugin[pluginType]['server'], + 'param': paramFill(pluginTestInfo[0]), + }, + 'client': { # plugin info for client + 'type': plugin[pluginType]['client'], + 'param': paramFill(pluginTestInfo[1]), + }, + 'inject': ssInject # for some special plugins (only server part) + } diff --git a/Tester/Shadowsocks.py b/Tester/Shadowsocks.py index 6d8e0bd..cdcb7c2 100644 --- a/Tester/Shadowsocks.py +++ b/Tester/Shadowsocks.py @@ -5,9 +5,11 @@ import os import json import base64 import itertools +from Tester import Plugin from Builder import Shadowsocks from Basis.Logger import logging from Basis.Process import Process +from Basis.Functions import md5Sum from Basis.Methods import ssMethods, ssAllMethods from Basis.Functions import genFlag, getAvailablePort @@ -31,15 +33,6 @@ def loadConfig(proxyInfo: dict) -> dict: # load basic config option 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 @@ -97,7 +90,7 @@ def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) - 'ss-libev': Shadowsocks.ssLibev, 'ss-python': Shadowsocks.ssPython, 'ss-python-legacy': Shadowsocks.ssPythonLegacy - }[ssType](proxyInfo, socksInfo, isUdp = False) + }[ssType](proxyInfo, socksInfo, isUdp = False) # disable udp in test mode clientFile = os.path.join(settings['workDir'], configFile) return Process(settings['workDir'], cmd = ssClient + ['-c', clientFile], file = { # load client process 'path': clientFile, @@ -111,7 +104,7 @@ def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process: 'ss-libev': ssLibev, 'ss-python': ssPython, 'ss-python-legacy': ssPythonLegacy - }[ssType](proxyInfo, isUdp = False) + }[ssType](proxyInfo, isUdp = False) # disable udp in test mode serverFile = os.path.join(settings['workDir'], configFile) return Process(settings['workDir'], cmd = ssServer + ['-c', serverFile], file = { # load server process 'path': serverFile, @@ -119,42 +112,62 @@ def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process: }, isStart = False) -def loadTest(serverType: str, clientType: str, method: str) -> dict: +def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None = None) -> dict: proxyInfo = { # connection info 'server': settings['serverBind'], 'port': getAvailablePort(), 'method': method, 'passwd': loadPassword(method), - 'plugin': None } socksInfo = { # socks5 interface for test 'addr': settings['clientBind'], 'port': getAvailablePort() } + pluginClient = {'plugin': None if plugin is None else plugin['client']} + pluginServer = {'plugin': None if plugin is None else plugin['server']} configName = '%s_%s_%s' % (serverType, clientType, method) # prefix of config file name + if plugin is not None: + configName += '_%s_%s' % (plugin['type'], md5Sum(plugin['caption'])[:8]) + pluginText = '' if plugin is None else (' [%s -> %s]' % (plugin['type'], plugin['caption'])) testInfo = { # release test info - 'title': 'Shadowsocks test: {%s <- %s -> %s}' % (serverType, method, clientType), - 'client': loadClient(clientType, configName + '_client.json', proxyInfo, socksInfo), - 'server': loadServer(serverType, configName + '_server.json', proxyInfo), + 'title': 'Shadowsocks test: {%s <- %s -> %s}%s' % (serverType, method, clientType, pluginText), + 'client': loadClient(clientType, configName + '_client.json', {**proxyInfo, **pluginClient}, socksInfo), + 'server': loadServer(serverType, configName + '_server.json', {**proxyInfo, **pluginServer}), 'socks': socksInfo, # exposed socks5 address 'interface': { 'addr': proxyInfo['server'], 'port': proxyInfo['port'] } } + if plugin is not None: + testInfo['server'] = plugin['inject'](testInfo['server'], plugin) logging.debug('New shadowsocks test -> %s' % testInfo) return testInfo def load(isExtra: bool = False): + pluginTest = [] + pluginIter = Plugin.load() + while True: + try: + pluginTest.append(next(pluginIter)) # export data of plugin generator + except StopIteration: + break if not isExtra: # just test basic connection for method in ssAllMethods: # test every method for once for ssType in ssMethods: # found the client which support this method if method not in ssMethods[ssType]: continue yield loadTest(ssType, ssType, method) # ssType <-- method --> ssType break # don't need other client + for ssType in ssMethods: # test plugin for every shadowsocks project + yield loadTest(ssType, ssType, ssMethods[ssType][0], pluginTest[0]) + ssType = list(ssMethods.keys())[0] # choose the first one + for plugin in pluginTest[1:]: # test every plugin (except the first one that has been checked) + yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin) return for ssServer in ssMethods: # traverse all shadowsocks type as server for method, ssClient in itertools.product(ssMethods[ssServer], ssMethods): # supported methods and clients if method not in ssMethods[ssClient]: continue yield loadTest(ssServer, ssClient, method) # ssServer <-- method --> ssClient + for ssType, plugin in itertools.product(ssMethods, pluginTest): # test every plugin with different ss project + yield loadTest(ssType, ssType, ssMethods[ssType][0], plugin) diff --git a/test.py b/test.py index 39ec189..94c151b 100755 --- a/test.py +++ b/test.py @@ -28,6 +28,8 @@ def test(testObj: dict) -> None: logging.debug('server start complete') logging.debug('start test process') + time.sleep(1) + errFlag = False try: request = requests.get( @@ -87,11 +89,5 @@ logging.critical('test start') runTest(ss, 64) runTest(ssr, 64) -# ssThread = Thread(target=runTest, args=(ss, 64)) -# ssrThread = Thread(target=runTest, args=(ssr, 64)) -# ssThread.start() -# ssrThread.start() -# ssThread.join() -# ssrThread.join() logging.critical('test complete')