Browse Source

update: enhance Tester module

master
dnomd343 2 years ago
parent
commit
f30324c39e
  1. 2
      Basis/Methods.py
  2. 46
      Basis/Process.py
  3. 25
      Builder/__init__.py
  4. 156
      Tester/Shadowsocks.py
  5. 167
      test.py

2
Basis/Methods.py

@ -63,7 +63,7 @@ ssMethods = { # methods support of different Shadowsocks project
ssAllMethods = set() ssAllMethods = set()
[ssAllMethods.update(ssMethods[x]) for x in ssMethods] [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 ssrMethods = [ # methods of ShadowsocksR
'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr',

46
Basis/Process.py

@ -49,7 +49,7 @@ class Process(object):
isStart: Start the process after class init complete. isStart: Start the process after class init complete.
Attributes: Attributes:
id, workDir, output id, cmd, env, file, workDir, output
""" """
output = None # sub process output if capture is True output = None # sub process output if capture is True
__capture = None # capture the sub process output or not __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: 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.id = genFlag(length = 12) if taskId == '' else taskId
self.workDir = workDir self.workDir = workDir
self.__env = copy.copy(env) # depth = 1 self.env = copy.copy(env) # depth = 1
self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd) # 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.file = copy.deepcopy([file] if type(file) == dict else file) # depth = 2
self.__checkWorkDir() # ensure the working direction is normal self.__checkWorkDir() # ensure the working direction is normal
logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.__cmd, self.__env)) logging.debug('[%s] Process command -> %s (%s)' % (self.id, self.cmd, self.env))
if self.__file is not None: if self.file is not None:
if len(self.__file) > 1: if len(self.file) > 1:
logging.debug('[%s] Manage %i files' % (self.id, len(self.__file))) logging.debug('[%s] Manage %i files' % (self.id, len(self.file)))
for file in self.__file: # traverse all files for file in self.file: # traverse all files
if not isStart: # don't print log twice if not isStart: # don't print log twice
logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content'])) logging.debug('[%s] File %s -> %s' % (self.id, file['path'], file['content']))
if isStart: if isStart:
self.start() self.start()
def setCmd(self, cmd: str or list) -> None: def setCmd(self, cmd: str or list) -> None:
self.__cmd = copy.copy([cmd] if type(cmd) == str else cmd) self.cmd = copy.copy([cmd] if type(cmd) == str else cmd)
logging.info('[%s] Process setting command -> %s' % (self.id, self.__cmd)) logging.info('[%s] Process setting command -> %s' % (self.id, self.cmd))
def setEnv(self, env: dict or None) -> None: def setEnv(self, env: dict or None) -> None:
self.__env = copy.copy(env) self.env = copy.copy(env)
logging.info('[%s] Process setting environ -> %s' % (self.id, self.__env)) logging.info('[%s] Process setting environ -> %s' % (self.id, self.env))
def setFile(self, file: dict or list or None) -> None: def setFile(self, file: dict or list or None) -> None:
self.__file = copy.deepcopy([file] if type(file) == dict else file) self.file = copy.deepcopy([file] if type(file) == dict else file)
if self.__file is None: if self.file is None:
logging.info('[%s] Process setting file -> None' % self.id) logging.info('[%s] Process setting file -> None' % self.id)
return 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'])) logging.info('[%s] Process setting file %s -> %s' % (self.id, file['path'], file['content']))
def start(self, isCapture: bool = True) -> None: def start(self, isCapture: bool = True) -> None:
@ -132,16 +132,16 @@ class Process(object):
logging.debug('[%s] Process ready to start (%s)' % (self.id, ( logging.debug('[%s] Process ready to start (%s)' % (self.id, (
'with output capture' if self.__capture else 'without output capture' '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) logging.error('[%s] Process miss start command' % self.id)
raise RuntimeError('miss start command') raise RuntimeError('miss start command')
if self.__process is not None and self.__process.poll() is None: # ERROR CASE if self.__process is not None and self.__process.poll() is None: # ERROR CASE
logging.error('[%s] Sub process is still running' % self.id) logging.error('[%s] Sub process is still running' % self.id)
raise RuntimeError('sub process is still running') 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) 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 if self.file is not None: # create and write file contents
for file in self.__file: for file in self.file:
with open(file['path'], 'w', encoding = 'utf-8') as fileObject: # save file content with open(file['path'], 'w', encoding = 'utf-8') as fileObject: # save file content
fileObject.write(file['content']) fileObject.write(file['content'])
logging.debug('[%s] File %s -> %s' % (self.id, file['path'], 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 else: # discard all the output of sub process
stdout = DEVNULL stdout = DEVNULL
stderr = 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) stderr = stderr, preexec_fn = None if libcPath is None else Process.__preExec)
logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid)) logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid))
@ -195,6 +195,6 @@ class Process(object):
self.__deleteFile(self.__logfile) self.__deleteFile(self.__logfile)
except: except:
logging.error('[%s] Failed to read capture log file -> %s' % (self.id, self.__logfile)) logging.error('[%s] Failed to read capture log file -> %s' % (self.id, self.__logfile))
if self.__file is not None: # with config file if self.file is not None: # with config file
for file in self.__file: for file in self.file:
self.__deleteFile(file['path']) self.__deleteFile(file['path'])

25
Builder/__init__.py

@ -11,18 +11,14 @@ from Basis.Logger import logging
from Basis.Process import Process from Basis.Process import Process
from Basis.Functions import genFlag, getAvailablePort 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): class Builder(object):
""" Build the proxy client process and expose socks5 port. """ Build the proxy client process and expose socks5 port.
Arguments: Arguments:
proxy: Proxy node information. proxyType: Proxy node type.
proxyInfo: Proxy node information.
bind: Socks5 proxy bind address. bind: Socks5 proxy bind address.
@ -30,14 +26,12 @@ class Builder(object):
taskId: Task ID, defaults to 12 random characters length. taskId: Task ID, defaults to 12 random characters length.
isStart: Start the process after class init complete.
Attributes: Attributes:
id, proxyType, proxyInfo, socksAddr, socksPort, output id, proxyType, proxyInfo, socksAddr, socksPort, output
""" """
output = None output = None
def __loadClient(self, isStart: bool): def __loadClient(self):
loadFunction = { loadFunction = {
'ss': Shadowsocks.load, 'ss': Shadowsocks.load,
'ssr': ShadowsocksR.load, 'ssr': ShadowsocksR.load,
@ -52,23 +46,22 @@ class Builder(object):
'addr': self.socksAddr, 'addr': self.socksAddr,
'port': self.socksPort, 'port': self.socksPort,
}, configFile) }, configFile)
envVar['PATH'] = default['binDir'] envVar['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
fileObject = { fileObject = {
'path': configFile, 'path': configFile,
'content': fileContent 'content': fileContent
} }
self.__process = Process(self.__workDir, taskId = self.id, self.__process = Process(self.__workDir, taskId = self.id, cmd = command, env = envVar, file = fileObject)
isStart = isStart, cmd = command, env = envVar, file = fileObject)
def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '', isStart: bool = True, def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '',
bind: str = default['bindAddr'], workDir: str = default['workDir']) -> None: bind: str = '127.0.0.1', workDir: str = '/tmp/ProxyC') -> None:
self.id = genFlag(length = 12) if taskId == '' else taskId self.id = genFlag(length = 12) if taskId == '' else taskId
self.__workDir = workDir self.__workDir = workDir
self.proxyType = proxyType self.proxyType = proxyType
self.proxyInfo = copy.copy(proxyInfo) self.proxyInfo = copy.copy(proxyInfo)
self.socksAddr = bind self.socksAddr = bind
self.socksPort = getAvailablePort() self.socksPort = getAvailablePort()
self.__loadClient(isStart) self.__loadClient()
def status(self) -> bool: def status(self) -> bool:
return self.__process.status() return self.__process.status()

156
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

167
test.py

@ -1,160 +1,53 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import json
import time import time
import requests import requests
from Builder import Shadowsocks from Tester import Shadowsocks
from Basis.Logger import logging 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 def test(testObj: dict) -> None:
config = { logging.warning(testObj['title'])
'server': '127.0.0.1', testObj['client'].start()
'server_port': serverPort, # type -> int testObj['server'].start()
'method': method,
'password': ssPassword,
}
return config
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 errFlag = False
try: try:
request = requests.get( request = requests.get(
'http://baidu.com', 'http://baidu.com',
proxies = { proxies = {
'http': 'socks5://127.0.0.1:%i' % socksPort, 'http': 'socks5://127.0.0.1:%i' % testObj['socks']['port'],
'https': 'socks5://127.0.0.1:%i' % socksPort 'https': 'socks5://127.0.0.1:%i' % testObj['socks']['port'],
}, },
timeout = 10 timeout = 10
) )
request.raise_for_status() 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: 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)) logging.error('requests exception\n' + str(exp))
errFlag = True 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)
testObj['client'].quit()
def test_2() -> None: testObj['server'].quit()
for ssServer in ssMethods: if errFlag:
for method in ssMethods[ssServer]: logging.warning('client info')
for ssClient in ssMethods: logging.error('command -> ' + str(testObj['client'].cmd))
if method not in ssMethods[ssClient]: continue logging.error('envVar -> ' + str(testObj['client'].env))
timeout = 0.1 logging.error('file -> ' + str(testObj['client'].file))
if 'python' in ssServer or 'python' in ssClient: timeout = 0.3 logging.warning('client capture output')
if method == 'table': timeout = 0.8 logging.error('\n' + str(testObj['client'].output))
if 'python-legacy' in ssServer or 'python-legacy' in ssClient: timeout = 1 logging.warning('server info')
if method == 'salsa20-ctr': timeout = 3 logging.error('command -> ' + str(testObj['server'].cmd))
loadTest(ssServer, ssClient, method, timeout) logging.error('envVar -> ' + str(testObj['server'].env))
logging.error('file -> ' + str(testObj['server'].file))
logging.warning('server capture output')
# test_1() logging.error('\n' + str(testObj['server'].output))
test_2()
# loadTest('ss-python-legacy', 'ss-python-legacy', 'salsa20-ctr', 3)
testList = Shadowsocks.load(isExtra = True)
for testObject in testList:
test(testObject)

Loading…
Cancel
Save