Browse Source

Merge branch 'dev'

master
dnomd343 2 years ago
parent
commit
51675b1698
  1. 53
      Basis/Api.py
  2. 18
      Basis/Check.py
  3. 11
      Basis/Compile.py
  4. 72
      Basis/Constant.py
  5. 8
      Basis/DnsProxy.py
  6. 26
      Basis/Exception.py
  7. 28
      Basis/Filter.py
  8. 4
      Basis/Functions.py
  9. 26
      Basis/Logger.py
  10. 35
      Basis/Manager.py
  11. 13
      Basis/Process.py
  12. 159
      Basis/Test.py
  13. 26
      Builder/Brook.py
  14. 6
      Builder/Hysteria.py
  15. 51
      Builder/Shadowsocks.py
  16. 7
      Builder/ShadowsocksR.py
  17. 2
      Builder/Trojan.py
  18. 7
      Builder/V2ray.py
  19. 5
      Builder/VLESS.py
  20. 3
      Builder/VMess.py
  21. 7
      Builder/Xray.py
  22. 14
      Builder/__init__.py
  23. 23
      Dockerfile
  24. 8
      Filter/__init__.py
  25. 5
      Tester/Brook.py
  26. 5
      Tester/Hysteria.py
  27. 10
      Tester/Plugin.py
  28. 14
      Tester/Settings.py
  29. 69
      Tester/Shadowsocks.py
  30. 5
      Tester/ShadowsocksR.py
  31. 5
      Tester/Trojan.py
  32. 14
      Tester/TrojanGo.py
  33. 2
      Tester/V2ray.py
  34. 5
      Tester/VLESS.py
  35. 5
      Tester/VMess.py
  36. 2
      Tester/Xray.py
  37. 128
      Tester/__init__.py
  38. 8
      env.yaml
  39. 133
      main.py
  40. 66
      test.py

53
Basis/Api.py

@ -1,16 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
from gevent import pywsgi
from Checker import formatCheck
from Basis.Logger import logging
from Basis.Manager import Manager
from Basis.Constant import Version
from flask import Flask, Response, request
from Basis.Exception import managerException
from Basis.Constant import ApiPort, ApiPath, ApiToken, Version
token = ''
webPath = '/' # root of api server
webApi = Flask(__name__) # init flask server
@ -43,16 +43,16 @@ def genError(message: str) -> Response:
def tokenCheck() -> bool:
if token == '': return True # without token check
if ApiToken == '': return True # without token check
if request.method == 'GET':
return request.args.get('token') == token
return request.args.get('token') == ApiToken
elif request.method == 'POST':
return request.json.get('token') == token
return request.json.get('token') == ApiToken
else:
return False # polyfill
@webApi.route('/task', methods = ['GET'])
@webApi.route(os.path.join(ApiPath, 'task'), methods = ['GET'])
def getTaskList() -> Response:
if not tokenCheck(): # token check
return genError('Invalid token')
@ -64,17 +64,22 @@ def getTaskList() -> Response:
})
@webApi.route('/task', methods = ['POST'])
@webApi.route(os.path.join(ApiPath, 'task'), methods = ['POST'])
def createTask() -> Response:
if not tokenCheck(): # token check
return genError('Invalid token')
try:
# TODO: format check and proxy list
checkList = formatCheck(request.json.get('check'))
except:
return genError('Some error in check options')
proxyList = []
for proxy in request.json.get('proxy'):
try:
proxyList.append(formatProxy(proxy))
logging.critical(proxyList)
except Exception as exp:
return genError('Proxy error in %s -> %s' % (proxy, exp))
logging.debug('API create task -> check = %s | proxy = %s' % (checkList, proxyList))
tasks = []
@ -93,7 +98,7 @@ def createTask() -> Response:
})
@webApi.route('/task/<taskId>', methods = ['GET'])
@webApi.route(os.path.join(ApiPath, 'task/<taskId>'), methods = ['GET'])
def getTaskInfo(taskId: str) -> Response:
if not tokenCheck(): # token check
return genError('Invalid token')
@ -106,7 +111,23 @@ def getTaskInfo(taskId: str) -> Response:
})
@webApi.route('/version', methods = ['GET'])
@webApi.route(os.path.join(ApiPath, 'task/<taskId>'), methods = ['DELETE'])
def deleteTask(taskId: str) -> Response:
if not tokenCheck(): # token check
return genError('Invalid token')
logging.debug('API get task -> %s' % taskId)
if not Manager.isUnion(taskId):
return genError('Task not found')
try:
Manager.delUnion(taskId)
return jsonResponse({
'success': True
})
except managerException as exp:
return genError(str(exp))
@webApi.route(os.path.join(ApiPath, 'version'), methods = ['GET'])
def getVersion() -> Response:
logging.debug('API get version -> %s' + Version)
return jsonResponse({
@ -115,9 +136,7 @@ def getVersion() -> Response:
})
def startServer(apiToken: str = '', apiPort: int = 7839) -> None:
global token
token = apiToken # api token (default empty)
logging.warning('API server at http://:%i/' % apiPort)
logging.warning('API ' + ('without token' if apiToken == '' else 'token -> %s' % apiToken))
pywsgi.WSGIServer(('0.0.0.0', apiPort), webApi).serve_forever() # powered by gevent
def startServer() -> None:
logging.warning('API server at http://:%i%s' % (ApiPort, ApiPath))
logging.warning('API ' + ('without token' if ApiToken == '' else 'token -> %s' % ApiToken))
pywsgi.WSGIServer(('0.0.0.0', ApiPort), webApi).serve_forever() # powered by gevent

18
Basis/Check.py

@ -6,6 +6,8 @@ import time
from Checker import Checker
from Basis.Logger import logging
from Builder import Builder, clientEntry
from Basis.Exception import checkException
from Basis.Functions import checkPortStatus
def buildClient(taskId: str, taskInfo: dict) -> Builder:
@ -18,24 +20,27 @@ def buildClient(taskId: str, taskInfo: dict) -> Builder:
)
except Exception as reason:
logging.error('[%s] Client build error -> %s' % (taskId, reason))
raise RuntimeError('Client build error')
raise checkException('Client build error')
def waitClient(taskId: str, client: Builder):
# TODO: wait port occupied (client.socksPort)
time.sleep(1) # TODO: simple delay for now
def waitClient(taskId: str, client: Builder, times: int = 150, delay: int = 100): # wait until client port occupied
for i in range(times):
if not checkPortStatus(client.socksPort): # port occupied
break
time.sleep(delay / 1000) # wait in default: 100ms * 150 => 15s
time.sleep(1) # wait a short time before check process
if not client.status(): # client unexpected exit
logging.warning('[%s] Client unexpected exit' % taskId)
client.destroy() # remove file and kill sub process
logging.debug('[%s] Client output\n%s', (taskId, client.output))
raise RuntimeError('Client unexpected exit')
raise checkException('Client unexpected exit')
def Check(taskId: str, taskInfo: dict) -> dict:
logging.info('[%s] Start checking process -> %s' % (taskId, taskInfo))
if taskInfo['type'] not in clientEntry: # unknown proxy type
logging.error('[%s] Unknown proxy type %s' % (taskId, taskInfo['type']))
raise RuntimeError('Unknown proxy type')
raise checkException('Unknown proxy type')
client = buildClient(taskId, taskInfo) # build proxy client
logging.info('[%s] Client loaded successfully' % taskId)
waitClient(taskId, client) # wait for the client to start
@ -49,5 +54,6 @@ def Check(taskId: str, taskInfo: dict) -> dict:
taskInfo.pop('check') # remove check items
return {
**taskInfo,
'success': True,
'result': checkResult, # add check result
}

11
Basis/Compile.py

@ -1,11 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import compileall
from Basis.Logger import logging
def startCompile(dirRange: str = '/') -> None:
for optimize in [-1, 1, 2]:
compileall.compile_dir(dirRange, quiet = 1, maxlevels = 256, optimize = optimize)
logging.warning('Python optimize compile -> %s (level = %i)' % (dirRange, optimize))

72
Basis/Constant.py

@ -1,11 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import yaml
# Global Options
Version = 'dev'
ApiPath = '/'
ApiPort = 7839
ApiToken = ''
CheckThread = 64
LogLevel = 'INFO'
LogFile = 'runtime.log'
DnsServer = None
WorkDir = '/tmp/ProxyC'
TestHost = 'proxyc.net'
TestSite = 'www.bing.com'
PathEnv = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
# Load Env Options
envOptions = {}
try:
yamlFile = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../env.yaml')
yamlContent = open(yamlFile, 'r', encoding = 'utf-8').read()
envOptions = yaml.load(yamlContent, Loader = yaml.FullLoader)
except: # something error in env.yaml
pass
if 'version' in envOptions:
Version = envOptions['version']
if 'loglevel' in envOptions:
LogLevel = envOptions['loglevel']
if 'dir' in envOptions:
WorkDir = envOptions['dir']
if 'dns' in envOptions:
DnsServer = envOptions['dns']
if 'api' in envOptions:
if 'port' in envOptions['api']:
ApiPort = envOptions['api']['port']
if 'path' in envOptions['api']:
ApiPath = envOptions['api']['path']
if 'token' in envOptions['api']:
ApiToken = envOptions['api']['token']
# WorkDir Create
try:
os.makedirs(WorkDir) # just like `mkdir -p ...`
except:
pass # folder exist or target is another thing
# Shadowsocks Info
mbedtlsMethods = [
'aes-128-cfb128',
'aes-192-cfb128',
'aes-256-cfb128',
'camellia-128-cfb128',
'camellia-192-cfb128',
'camellia-256-cfb128',
]
ssMethods = { # methods support of different Shadowsocks project
'ss-rust': [ # table method removed refer to https://github.com/shadowsocks/shadowsocks-rust/issues/887
'none', 'plain', 'rc4', 'rc4-md5',
@ -70,6 +129,7 @@ ssAllMethods = set()
[ssAllMethods.update(ssMethods[x]) for x in ssMethods]
ssAllMethods = sorted(list(ssAllMethods)) # methods of Shadowsocks
# Plugin Info
Plugins = {
'simple-obfs': ['obfs-local', 'obfs-server'],
@ -90,6 +150,7 @@ Plugins = {x: [Plugins[x][0], Plugins[x][1 if len(Plugins[x]) == 2 else 0]] for
Plugins = {x: {'client': Plugins[x][0], 'server': Plugins[x][1]} for x in Plugins} # format plugins info
pluginClients = [Plugins[x]['client'] for x in Plugins] # plugin client list -> obfs-local / simple-tls / ...
# ShadowsocksR Info
ssrMethods = [ # methods of ShadowsocksR
'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr',
@ -118,19 +179,20 @@ ssrObfuscations = [ # obfuscations of ShadowsocksR (obfs)
'tls_simple', 'tls1.2_ticket_auth', 'tls1.2_ticket_fastauth',
]
# VMess Info
# V2ray / Xray Info
vmessMethods = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero']
# XTLS Info
quicMethods = ['none', 'aes-128-gcm', 'chacha20-poly1305']
udpObfuscations = ['none', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard']
xtlsFlows = ['xtls-origin', 'xtls-direct', 'xtls-splice']
xtlsFlows = {x: x.replace('-', '-rprx-') for x in xtlsFlows}
# v2ray / Xray Info
quicMethods = ['none', 'aes-128-gcm', 'chacha20-poly1305']
udpObfuscations = ['none', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard']
# Trojan-Go Info
trojanGoMethods = ['aes-128-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305']
# Hysteria Info
hysteriaProtocols = ['udp', 'wechat-video', 'faketcp']

8
Basis/DnsProxy.py

@ -15,12 +15,16 @@ def daemon(process: subprocess.Popen, command: list, gap: int = 2) -> None: # d
while True: # start daemon
time.sleep(gap) # check time gap
if process.poll() is not None: # unexpected exit
logging.warning('dnsproxy unexpected exit')
logging.debug('output of dnsproxy\n%s' % process.stdout.read().decode('utf-8'))
logging.error('DnsProxy unexpected exit\n%s\n%s%s' % (
'-' * 96, process.stdout.read().decode('utf-8'), '-' * 96)
)
process = run(command)
def start(servers: list or None, port: int = 53, cache: int = 4194304) -> None: # default cache size -> 4MiB
if servers is not None and type(servers) != list: # invalid server content
logging.error('Invalid DNS server -> %s' % servers)
return
if servers is None or len(servers) == 0: # use origin dns server
logging.info('Skip dnsproxy process')
return

26
Basis/Exception.py

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class buildException(Exception): # for build error
def __init__(self, reason):
self.reason = reason
class filterException(Exception): # for filter error
def __init__(self, reason):
self.reason = reason
class processException(Exception): # for process error
def __init__(self, reason):
self.reason = reason
class managerException(Exception): # for manager error
def __init__(self, reason):
self.reason = reason
class checkException(Exception): # for check error
def __init__(self, reason):
self.reason = reason

28
Basis/Filter.py

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import copy
from Basis.Exception import filterException
filterObject = {
'optional': {
@ -67,7 +68,7 @@ for field in filterObject:
def Filter(raw: dict, rules: dict) -> dict:
if type(raw) != dict:
raise RuntimeError('Invalid input for filter')
raise filterException('Invalid input for filter')
data = {}
raw = copy.deepcopy(raw)
rules = copy.deepcopy(rules)
@ -75,19 +76,19 @@ def Filter(raw: dict, rules: dict) -> dict:
# pretreatment process (raw --[copy / default value]--> data)
if key not in raw: # key not exist
if not rule['optional']: # force require key not exist
raise RuntimeError('Missing `%s` field' % key)
raise filterException('Missing `%s` field' % key)
data[key] = rule['default'] # set default value
else: # key exist
data[key] = raw[key]
# format process (data --[format]--> data)
if data[key] is None: # key content is None
if not rule['allowNone']: # key is not allow None
raise RuntimeError('Field `%s` shouldn\'t be None' % key)
raise filterException('Field `%s` shouldn\'t be None' % key)
continue # skip following process
try:
data[key] = rule['format'](data[key]) # run format
except:
raise RuntimeError(rule['errMsg']) # format error
raise filterException(rule['errMsg']) # format error
# filter process (data --[type check (& filter check)]--> pass / non-pass)
if type(rule['type']) == type: # str / int / bool / ...
rule['type'] = [rule['type']] # str -> [str] / int -> [int] / ...
@ -95,35 +96,38 @@ def Filter(raw: dict, rules: dict) -> dict:
if data[key] == any and any in rule['type']: # special case -> skip type filter
pass
elif type(data[key]) not in rule['type']: # type not in allow list
raise RuntimeError('Invalid `%s` field' % key)
raise filterException('Invalid `%s` field' % key)
elif type(rule['type']) == dict: # check subObject
if type(data[key]) != dict:
raise RuntimeError('Invalid sub object in `%s`' % key) # subObject content should be dict
raise filterException('Invalid sub object in `%s`' % key) # subObject content should be dict
if not rule['multiSub']: # single subObject
subRules = rule['type']
else: # multi subObject
if rule['indexKey'] not in data[key]: # confirm index key exist
raise RuntimeError('Index key `%s` not found in `%s`' % (rule['indexKey'], key))
raise filterException('Index key `%s` not found in `%s`' % (rule['indexKey'], key))
subType = data[key][rule['indexKey']].lower()
if subType not in rule['type']: # confirm subObject rule exist
raise RuntimeError('Unknown index `%s` in key `%s`' % (subType, key))
raise filterException('Unknown index `%s` in key `%s`' % (subType, key))
subRules = rule['type'][subType]
try:
data[key] = Filter(data[key], subRules)
except RuntimeError as exp:
raise RuntimeError('%s (in `%s`)' % (exp, key)) # add located info
except filterException as exp:
raise filterException('%s (in `%s`)' % (exp, key)) # add located info
continue
elif rule['type'] != any: # type == any -> skip type filter
raise RuntimeError('Unknown `type` in rules')
raise filterException('Unknown `type` in rules')
if not rule['filter'](data[key]): # run filter
raise RuntimeError(rule['errMsg'])
raise filterException(rule['errMsg'])
return data
def rulesFilter(rules: dict) -> dict:
result = {}
for key, rule in rules.items(): # filter by basic rules
try:
result[key] = Filter(rule, filterObject)
except filterException as exp:
raise filterException('%s (`%s` in rules)' % (exp, key)) # rules error
return result

4
Basis/Functions.py

@ -55,6 +55,10 @@ def hostFormat(host: str, v6Bracket: bool = False) -> str:
return host
def v6AddBracket(host: str) -> str: # add bracket for ipv6
return hostFormat(host, v6Bracket = True)
def genFlag(length: int = 12) -> str: # generate random task flag
flag = ''
for i in range(0, length):

26
Basis/Logger.py

@ -4,17 +4,23 @@
import sys
import logging
from colorlog import ColoredFormatter
from Basis.Constant import LogLevel, LogFile
logLevel = { # log level
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}[LogLevel.lower()]
dateFormat = '%Y-%m-%d %H:%M:%S' # log date format
logFormat = '[%(asctime)s] [%(levelname)s] %(message)s (%(module)s.%(funcName)s)' # log format
logFile = 'runtime.log'
logLevel = logging.DEBUG
# logLevel = logging.WARNING
dateFormat = '%Y-%m-%d %H:%M:%S'
logFormat = '[%(asctime)s] [%(levelname)s] %(message)s (%(module)s.%(funcName)s)'
logging.basicConfig(
level = logLevel,
format = logFormat,
datefmt = dateFormat,
filename = logFile,
filename = LogFile,
)
logHandler = logging.StreamHandler(stream = sys.stdout)
logHandler.setFormatter(ColoredFormatter(
@ -29,11 +35,3 @@ logHandler.setFormatter(ColoredFormatter(
}
))
logging.getLogger().addHandler(logHandler)
if __name__ == '__main__':
logging.debug('debug')
logging.info('info')
logging.warning('warn')
logging.error('error')
logging.critical('critical')

35
Basis/Manager.py

@ -4,7 +4,7 @@
import copy
from Basis.Logger import logging
from Basis.Functions import genFlag
from Basis.Exception import managerException
class Task(object):
""" Manage global check task.
@ -39,15 +39,26 @@ class Task(object):
logging.info('Manager add task [%s] -> %s' % (taskId, task))
self.__tasks.update(tasks) # load into task list
self.__unions[unionId] = {
'finish': False,
'items': taskIds # record task items
}
logging.info('Manager add union [%s] -> %s' % (unionId, taskIds))
return unionId
def delUnion(self, unionId) -> None: # remove union
if unionId not in self.__unions:
logging.error('Manager union [%s] not found' % unionId)
raise managerException('Union id not found')
if not self.__unions[unionId]['finish']: # some tasks are still running
raise managerException('Couldn\'t remove working union')
self.__unions.pop(unionId)
def getUnion(self, unionId: str) -> dict: # get union status (remove tasks when all completed)
if unionId not in self.__unions:
logging.error('Manager union [%s] not found' % unionId)
raise RuntimeError('Union id not found')
raise managerException('Union id not found')
if self.__unions[unionId]['finish']: # all tasks are finished
return self.__unions[unionId]
tasks = self.__unions[unionId]['items']
finishNum = 0
for taskId in tasks:
@ -60,17 +71,15 @@ class Task(object):
'finish': False,
'percent': round(finishNum / len(tasks), 2)
}
self.__unions.pop(unionId) # remove from union list
unionResult = [] # temporary storage
self.__unions[unionId]['result'] = []
for taskId in tasks:
task = self.__tasks[taskId]
self.__tasks.pop(taskId) # remove from task list
unionResult.append(task['data'])
logging.info('Manager release union [%s] -> %s' % (unionId, unionResult))
return {
'finish': True,
'result': unionResult
}
self.__unions[unionId]['result'].append(task['data'])
self.__unions[unionId]['finish'] = True
self.__unions[unionId].pop('items')
logging.info('Manager release union [%s] -> %s' % (unionId, self.__unions[unionId]['result']))
return self.__unions[unionId]
def popTask(self) -> tuple[str or None, any]: # fetch a loaded task
for taskId, task in self.__tasks.items():
@ -78,13 +87,13 @@ class Task(object):
task['status'] = self.__TASK_RUNNING # set task status as running
logging.info('Manager pop task [%s] -> %s' % (taskId, task['data']))
return taskId, copy.deepcopy(task['data'])
logging.debug('Manager has no more loaded tasks')
raise RuntimeError('No more tasks')
logging.debug('Manager has no more task')
raise managerException('No more tasks')
def finishTask(self, taskId: str, taskData: dict) -> None: # update task data when completed
if taskId not in self.__tasks:
logging.error('Manager task [%s] not found' % taskId)
raise RuntimeError('Task id not found')
raise managerException('Task id not found')
self.__tasks[taskId]['data'] = copy.deepcopy(taskData)
self.__tasks[taskId]['status'] = self.__TASK_FINISH # set task status as completed

13
Basis/Process.py

@ -8,6 +8,7 @@ import ctypes
import signal
from Basis.Logger import logging
from Basis.Functions import genFlag
from Basis.Exception import processException
from subprocess import Popen, STDOUT, DEVNULL
libcPaths = [
@ -73,7 +74,7 @@ class Process(object):
logging.error('[%s] %s already exist but not folder' % (self.id, self.workDir))
else:
logging.error('[%s] Unable to create new folder -> %s' % (self.id, self.workDir))
raise RuntimeError('Working directory error') # fatal error
raise processException('Working directory error') # fatal error
def __killProcess(self, killSignal: int) -> None:
try:
@ -134,10 +135,10 @@ class Process(object):
))
if self.cmd is None: # ERROR CASE
logging.error('[%s] Process miss start command' % self.id)
raise RuntimeError('Miss start command')
raise processException('Miss start command')
if self.__process is not None and self.__process.poll() is None: # ERROR CASE
logging.error('[%s] Process is still running' % self.id)
raise RuntimeError('Process is still running')
logging.error('[%s] Process try to start but it is running' % self.id)
raise processException('Process is still running')
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
@ -153,11 +154,15 @@ class Process(object):
else: # discard all the output of sub process
stdout = DEVNULL
stderr = DEVNULL
try:
self.__process = Popen(
self.cmd, env = self.env,
stdout = stdout, stderr = stderr,
preexec_fn = None if libcPath is None else Process.__preExec
)
except Exception as exp:
logging.error('[%s] Process unable to start -> %s' % (self.id, exp))
raise processException('Unable to start process')
logging.info('[%s] Process running -> PID = %i' % (self.id, self.__process.pid))
def signal(self, signalNum: int) -> None: # send specified signal to sub process

159
Basis/Test.py

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import requests
from threading import Thread
from Basis.Logger import logging
from Basis.Constant import WorkDir, TestHost, TestSite
from Basis.Functions import md5Sum, genFlag, hostFormat, checkPortStatus
Settings = {
'workDir': WorkDir,
'site': TestSite,
'serverBind': '',
'clientBind': '',
'host': '',
'cert': '',
'key': '',
}
def loadBind(serverV6: bool = False, clientV6: bool = False) -> None:
Settings['serverBind'] = '::1' if serverV6 else '127.0.0.1'
Settings['clientBind'] = '::1' if clientV6 else '127.0.0.1'
def waitPort(port: int, times: int = 50, delay: int = 100) -> bool: # wait until port occupied
for i in range(times):
if not checkPortStatus(port): # port occupied
return True
time.sleep(delay / 1000) # default wait 100ms * 50 => 5s
return False # timeout
def genCert(host: str, certInfo: dict, remark: str = 'ProxyC') -> None: # generate self-signed certificate
certOrgInfo = ['--organization', remark, '--organizationUnit', remark] # organization info
logging.critical('Load self-signed certificate')
os.system('mkdir -p %s' % Settings['workDir']) # make sure that work directory exist
# create CA data at first (by mad)
logging.critical('Creating CA certificate and key...')
os.system(' '.join( # generate CA certificate and privkey
['mad', 'ca', '--ca', certInfo['caCert'], '--key', certInfo['caKey'], '--commonName', remark] + certOrgInfo
))
# generate private key and sign certificate
logging.critical('Signing certificate...')
os.system(' '.join(['mad', 'cert', '--domain', host] + [ # generate certificate and privkey, then signed by CA
'--ca', certInfo['caCert'], '--ca_key', certInfo['caKey'],
'--cert', certInfo['cert'], '--key', certInfo['key'],
] + certOrgInfo))
# install CA certificate and record self-signed cert info
logging.critical('Installing CA certificate...')
os.system('cat %s >> /etc/ssl/certs/ca-certificates.crt' % certInfo['caCert']) # add into system's trust list
def loadCert(host: str = TestHost, certId: str = '') -> None: # load certificate
newCert = (certId == '')
certId = genFlag(length = 8) if certId == '' else certId
certInfo = {
'caCert': os.path.join(Settings['workDir'], 'proxyc_%s_ca.pem' % certId),
'caKey': os.path.join(Settings['workDir'], 'proxyc_%s_ca_key.pem' % certId),
'cert': os.path.join(Settings['workDir'], 'proxyc_%s_cert.pem' % certId),
'key': os.path.join(Settings['workDir'], 'proxyc_%s_cert_key.pem' % certId),
}
if newCert:
genCert(host, certInfo) # generate new certificate
Settings['host'] = host
Settings['cert'] = certInfo['cert']
Settings['key'] = certInfo['key']
logging.warning('Certificate load complete -> ID = %s' % certId)
def httpCheck(socksInfo: dict, url: str, testId: str, timeout: int = 10) -> None:
socksProxy = 'socks5://%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
try:
logging.debug('[%s] Http request via %s' % (testId, socksProxy))
request = requests.get(url, timeout = timeout, proxies = { # http request via socks5
'http': socksProxy,
'https': socksProxy,
})
request.raise_for_status() # throw error when server return 4xx or 5xx (don't actually need)
logging.info('[%s] %s -> ok' % (testId, socksProxy))
except Exception as exp:
logging.error('[%s] %s -> error\n%s' % (testId, socksProxy, exp)) # show detail of error reason
raise RuntimeError('Http request via socks5 failed')
def runTest(testInfo: dict, testUrl: str, testSelect: set or None, delay: int = 1) -> None:
testId = md5Sum(testInfo['caption'])[:12] # generate test ID
if testSelect is not None: # testSelect is None -> run all test
if testId not in testSelect: # skip unselected task
return
logging.warning('[%s] %s' % (testId, testInfo['caption'])) # show caption
logging.debug('[%s] Server ID -> %s | Client ID -> %s' % (
testId, testInfo['server'].id, testInfo['client'].id
))
testInfo['server'].id = testId + '-server'
testInfo['client'].id = testId + '-client'
# build server and client and wait them start
testInfo['server'].start() # start test server
if waitPort(testInfo['interface']['port']): # wait for server
logging.debug('[%s] Test server start complete' % testId)
testInfo['client'].start() # start test client
if waitPort(testInfo['socks']['port']): # wait for client
logging.debug('[%s] Test client start complete' % testId)
# start test process
try:
logging.debug('[%s] Test process start' % testId)
time.sleep(delay) # delay a short time before check
httpCheck(testInfo['socks'], testUrl, testId) # run http request test
testInfo['client'].quit() # clean up client
testInfo['server'].quit() # clean up server
except:
testInfo['client'].quit()
testInfo['server'].quit()
logging.warning('[%s] Client info' % testId)
logging.error('[%(id)s-server]\n▲ CMD => %(cmd)s\n▲ ENV => %(env)s\n▲ FILE => %(file)s\n%(output)s' % {
'id': testId,
'cmd': testInfo['client'].cmd,
'env': testInfo['client'].env,
'file': testInfo['client'].file,
'output': '-' * 96 + '\n' + testInfo['client'].output + '-' * 96,
})
logging.warning('[%s] Server info' % testId)
logging.error('[%(id)s-client]\n▲ CMD => %(cmd)s\n▲ ENV => %(env)s\n▲ FILE => %(file)s\n%(output)s' % {
'id': testId,
'cmd': testInfo['server'].cmd,
'env': testInfo['server'].env,
'file': testInfo['server'].file,
'output': '-' * 96 + '\n' + testInfo['server'].output + '-' * 96,
})
def Test(testIter: iter, threadNum: int, testUrl: str, testFilter: set or None = None) -> None:
threads = []
while True: # infinite loop
try:
for thread in threads:
if thread.is_alive(): # skip running thread
continue
threads.remove(thread) # remove dead thread
if len(threads) < threadNum:
for i in range(threadNum - len(threads)): # start threads within limit
thread = Thread( # create new thread
target = runTest,
args = (next(testIter), testUrl, testFilter)
)
thread.start()
threads.append(thread) # record thread info
time.sleep(0.1)
except StopIteration: # traverse completed
break
for thread in threads: # wait until all threads exit
thread.join()

26
Builder/Brook.py

@ -1,30 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Basis.Functions import hostFormat
from Basis.Functions import v6AddBracket
def loadOrigin(proxyInfo: dict) -> list: # origin stream
return ['client'] + [
'--server', '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']),
'--server', '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']),
'--password', proxyInfo['passwd'],
] + (['--udpovertcp'] if proxyInfo['stream']['uot'] else [])
] + (['--udpovertcp'] if proxyInfo['stream']['uot'] else []) # add uot option
def loadWebsocket(proxyInfo: dict) -> list:
isTls = proxyInfo['stream']['secure'] is not None
wsAddress = (('wss' if isTls else 'ws') + '://%s:%i%s') % (
hostFormat(proxyInfo['stream']['host'], v6Bracket = True), proxyInfo['port'], proxyInfo['stream']['path']
def loadWebsocket(proxyInfo: dict) -> list: # websocket stream
isTls = proxyInfo['stream']['secure'] is not None # ws or wss
wsAddress = (('wss' if isTls else 'ws') + '://%s:%i%s') % ( # websocket address
v6AddBracket(proxyInfo['stream']['host']), proxyInfo['port'], proxyInfo['stream']['path']
)
brookCommand = [
'wssclient' if isTls else 'wsclient',
'--address', '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']),
'--address', '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']), # real address
'--password', proxyInfo['passwd'],
] + (['--withoutBrookProtocol'] if proxyInfo['stream']['raw'] else [])
] + (['--withoutBrookProtocol'] if proxyInfo['stream']['raw'] else []) # raw transmission on ws or wss
if not isTls:
return brookCommand + ['--wsserver', wsAddress]
return brookCommand + ['--wssserver', wsAddress] + (
[] if proxyInfo['stream']['secure']['verify'] else ['--insecure']
[] if proxyInfo['stream']['secure']['verify'] else ['--insecure'] # add tls options
)
@ -32,7 +31,6 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
brookCommand = ['brook', '--debug', '--listen', ':'] + { # debug module listen on random port
'origin': loadOrigin,
'ws': loadWebsocket,
}[proxyInfo['stream']['type']](proxyInfo) + [
'--socks5', '%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
]
}[proxyInfo['stream']['type']](proxyInfo) # choose origin or websocket stream
brookCommand += ['--socks5', '%s:%i' % (v6AddBracket(socksInfo['addr']), socksInfo['port'])]
return brookCommand, 'Config file %s no need' % configFile, {} # command, fileContent, envVar

6
Builder/Hysteria.py

@ -2,19 +2,19 @@
# -*- coding: utf-8 -*-
import json
from Basis.Functions import hostFormat
from Basis.Functions import v6AddBracket
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
hysteriaConfig = {
'server': '%s:%i' % (hostFormat(proxyInfo['server'], v6Bracket = True), proxyInfo['port']),
'server': '%s:%i' % (v6AddBracket(proxyInfo['server']), proxyInfo['port']),
'protocol': proxyInfo['protocol'],
'up_mbps': proxyInfo['up'],
'down_mbps': proxyInfo['down'],
'retry_interval': 2,
'retry': 3,
'socks5': {
'listen': '%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
'listen': '%s:%i' % (v6AddBracket(socksInfo['addr']), socksInfo['port'])
},
**({} if proxyInfo['obfs'] is None else {
'obfs': proxyInfo['obfs']

51
Builder/Shadowsocks.py

@ -2,7 +2,8 @@
# -*- coding: utf-8 -*-
import json
from Basis.Constant import ssMethods, ssAllMethods
from Basis.Exception import buildException
from Basis.Constant import ssMethods, ssAllMethods, mbedtlsMethods
def loadConfig(proxyInfo: dict, socksInfo: dict) -> dict: # load basic config option
@ -20,70 +21,62 @@ def loadConfig(proxyInfo: dict, socksInfo: dict) -> dict: # load basic config o
return config
def pluginUdp(plugin: str, pluginParam: str) -> bool: # whether the plugin uses UDP
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
if 'mode=quic' not in pluginParam.split(';'): # non-quic mode does not use udp
return False
return True # UDP is assumed by default
return True # udp is assumed by default
def ssRust(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssRust(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo)
if isUdp: # proxy UDP traffic
if isUdp: # proxy udp traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-rust-local', '-v']
return config, ['ss-rust-local', '-v'], {'RUST_BACKTRACE': 'full'} # enable rust trace
def ssLibev(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssLibev(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo)
if isUdp: # proxy UDP traffic
if isUdp: # proxy udp traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-libev-local', '-v']
return config, ['ss-libev-local', '-v'], {}
def ssPython(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssPython(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo)
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['no_udp'] = True # udp traffic is not proxied
config['shadowsocks'] = 'ss-python-local'
return config, ['ss-bootstrap-local', '--debug', '-vv']
return config, ['ss-bootstrap-local', '--debug', '-vv'], {}
def ssPythonLegacy(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssPythonLegacy(proxyInfo: dict, socksInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo, socksInfo)
if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied
config['no_udp'] = True # udp traffic is not proxied
config['shadowsocks'] = 'ss-python-legacy-local'
return config, ['ss-bootstrap-local', '--debug', '-vv']
return config, ['ss-bootstrap-local', '--debug', '-vv'], {}
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
isUdp = True if proxyInfo['plugin'] is None else ( # UDP enabled when server without plugin
not pluginUdp(proxyInfo['plugin']['type'], proxyInfo['plugin']['param']) # UDP conflict status of plugins
isUdp = True if proxyInfo['plugin'] is None else ( # udp enabled when server without plugin
not pluginUdp(proxyInfo['plugin']['type'], proxyInfo['plugin']['param']) # udp conflict status of plugins
)
if proxyInfo['method'] not in ssAllMethods: # unknown shadowsocks method
raise RuntimeError('Unknown shadowsocks method')
raise buildException('Unknown shadowsocks method')
for client in ssMethods: # traverse all shadowsocks client
if proxyInfo['method'] not in ssMethods[client]:
continue
ssConfig, ssClient = {
ssConfig, ssClient, ssEnv = { # found appropriate client
'ss-rust': ssRust,
'ss-libev': ssLibev,
'ss-python': ssPython,
'ss-python-legacy': ssPythonLegacy
}[client](proxyInfo, socksInfo, isUdp) # generate config file
return ssClient + ['-c', configFile], json.dumps(ssConfig), {} # command, fileContent, envVar
return ssClient + ['-c', configFile], json.dumps(ssConfig), ssEnv # command, fileContent, envVar

7
Builder/ShadowsocksR.py

@ -2,16 +2,17 @@
# -*- coding: utf-8 -*-
import json
from Basis.Exception import buildException
from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] not in ssrMethods:
raise RuntimeError('Unknown shadowsocksr method')
raise buildException('Unknown shadowsocksr method')
if proxyInfo['protocol'] not in ssrProtocols:
raise RuntimeError('Unknown shadowsocksr protocol')
raise buildException('Unknown shadowsocksr protocol')
if proxyInfo['obfs'] not in ssrObfuscations:
raise RuntimeError('Unknown shadowsocksr obfuscation')
raise buildException('Unknown shadowsocksr obfuscation')
ssrConfig = {
'server': proxyInfo['server'],
'server_port': proxyInfo['port'], # type -> int

2
Builder/Trojan.py

@ -13,7 +13,7 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
'address': proxyInfo['server'],
'port': proxyInfo['port'],
'password': proxyInfo['passwd'],
**Xray.xtlsFlow(proxyInfo['stream'])
**Xray.xtlsFlow(proxyInfo['stream']) # add xtls flow option
}]
},
'streamSettings': Xray.loadStream(proxyInfo['stream'])

7
Builder/V2ray.py

@ -2,8 +2,9 @@
# -*- coding: utf-8 -*-
import copy
from Basis.Exception import buildException
httpConfig = {
httpConfig = { # http obfs configure in default
'type': 'http',
'request': {
'version': '1.1',
@ -24,7 +25,7 @@ httpConfig = {
}
}
kcpConfig = {
kcpConfig = { # kcp options in default
'mtu': 1350,
'tti': 50,
'uplinkCapacity': 12,
@ -139,7 +140,7 @@ def loadStream(streamInfo: dict) -> dict:
'grpc': grpcStream,
}
if streamInfo['type'] not in streamEntry:
raise RuntimeError('Unknown stream type')
raise buildException('Unknown v2ray stream type')
streamObject = streamEntry[streamInfo['type']](streamInfo)
return {
**streamObject,

5
Builder/VLESS.py

@ -3,9 +3,12 @@
import json
from Builder import Xray
from Basis.Exception import buildException
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] != 'none':
raise buildException('Unknown VLESS method')
outboundConfig = {
'protocol': 'vless',
'settings': {
@ -15,7 +18,7 @@ def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str,
'users': [{
'id': proxyInfo['id'],
'encryption': proxyInfo['method'],
**Xray.xtlsFlow(proxyInfo['stream'])
**Xray.xtlsFlow(proxyInfo['stream']) # add xtls flow option
}]
}]
},

3
Builder/VMess.py

@ -4,11 +4,12 @@
import json
from Builder import V2ray
from Basis.Constant import vmessMethods
from Basis.Exception import buildException
def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]:
if proxyInfo['method'] not in vmessMethods:
raise RuntimeError('Unknown vmess method')
raise buildException('Unknown VMess method')
outboundConfig = {
'protocol': 'vmess',
'settings': {

7
Builder/Xray.py

@ -3,6 +3,7 @@
from Builder import V2ray
from Basis.Constant import xtlsFlows
from Basis.Exception import buildException
loadConfig = V2ray.loadConfig
@ -11,7 +12,7 @@ def loadSecure(secureInfo: dict or None) -> dict: # TLS / XTLS encrypt config
if secureInfo is None:
return {'security': 'none'} # without TLS / XTLS options
if secureInfo['type'] not in ['tls', 'xtls']:
raise RuntimeError('Unknown secure type')
raise buildException('Unknown xray secure type')
secureObject = {
'allowInsecure': not secureInfo['verify'] # whether verify server's certificate
}
@ -53,7 +54,7 @@ def loadStream(streamInfo: dict) -> dict:
'grpc': V2ray.grpcStream,
}
if streamInfo['type'] not in streamEntry:
raise RuntimeError('Unknown stream type')
raise buildException('Unknown xray stream type')
streamObject = streamEntry[streamInfo['type']](streamInfo)
return {
**streamObject,
@ -67,7 +68,7 @@ def xtlsFlow(streamInfo: dict or None) -> dict:
if streamInfo['secure']['type'] != 'xtls': # not XTLS secure type
return {}
if streamInfo['secure']['flow'] not in xtlsFlows:
raise RuntimeError('Unknown xtls flow')
raise buildException('Unknown xtls flow')
return {
'flow': xtlsFlows[streamInfo['secure']['flow']] + ( # xtls-rprx-xxx
'-udp443' if streamInfo['secure']['udp443'] else '' # whether block udp/443 (disable http/3)

14
Builder/__init__.py

@ -3,10 +3,13 @@
import os
import copy
from Filter import Filter
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Functions import v6AddBracket
from Basis.Constant import WorkDir, PathEnv
from Basis.Functions import hostFormat, genFlag, getAvailablePort
from Basis.Functions import genFlag, getAvailablePort
from Basis.Exception import buildException, filterException
from Builder import Brook
from Builder import VMess
@ -44,19 +47,19 @@ class Builder(object):
Attributes:
id, proxyType, proxyInfo, socksAddr, socksPort, output
"""
output = None
output = None # output capture of proxy client (after process exit)
def __loadClient(self):
logging.info('[%s] Builder load %s client at %s -> %s' % (
self.id, self.proxyType,
'socks5://%s:%i' % (hostFormat(self.socksAddr, v6Bracket = True), self.socksPort), self.proxyInfo
'socks5://%s:%i' % (v6AddBracket(self.socksAddr), self.socksPort), self.proxyInfo
))
configFile = os.path.join( # config file path
WorkDir, self.id + clientEntry[self.proxyType][1] # workDir + taskId + file suffix
)
logging.debug('[%s] Builder config file -> %s' % (self.id, configFile))
command, fileContent, envVar = clientEntry[self.proxyType][0](self.proxyInfo, { # load client boot info
'addr': self.socksAddr,
'addr': self.socksAddr, # specify socks5 info
'port': self.socksPort,
}, configFile)
envVar['PATH'] = PathEnv # add PATH env (some programs need it)
@ -69,8 +72,9 @@ class Builder(object):
self.id = genFlag(length = 12) if taskId == '' else taskId # load task ID
if proxyType not in clientEntry:
logging.error('[%s] Builder receive unknown proxy type %s' % (self.id, proxyType))
raise RuntimeError('Unknown proxy type')
raise buildException('Unknown proxy type')
self.proxyType = proxyType # proxy type -> ss / ssr / vmess ...
self.proxyInfo = Filter(proxyType, proxyInfo) # filter input proxy info
self.proxyInfo = copy.copy(proxyInfo) # connection info
self.socksAddr = bindAddr
self.socksPort = getAvailablePort() # random port for socks5 exposed

23
Dockerfile

@ -31,7 +31,7 @@ FROM ${PYTHON_IMG} AS wheels
WORKDIR /wheels/
RUN apk add linux-headers
COPY --from=build-base /apk/ /apk/
RUN /apk/build-base && pip wheel colorlog flask IPy psutil pysocks requests salsa20
RUN /apk/build-base && pip wheel colorlog flask IPy psutil pysocks pyyaml requests salsa20
COPY --from=gevent /wheels/*.whl /wheels/
COPY --from=numpy /wheels/*.whl /wheels/
@ -53,7 +53,7 @@ COPY --from=build-base /apk/ /apk/
RUN wget https://github.com/shadowsocks/shadowsocks-rust/archive/refs/tags/v${SS_RUST}.tar.gz && \
tar xf v${SS_RUST}.tar.gz && /apk/build-base
WORKDIR ./shadowsocks-rust-${SS_RUST}/
RUN cargo update
RUN cargo fetch
RUN cargo build --target-dir ./ --release --bin sslocal --bin ssserver \
--features "stream-cipher aead-cipher-extra aead-cipher-2022 aead-cipher-2022-extra" && \
mv ./release/sslocal /tmp/ss-rust-local && mv ./release/ssserver /tmp/ss-rust-server && \
@ -169,7 +169,7 @@ RUN git submodule update --init --recursive && \
mv ./src/obfs-local ./src/obfs-server /plugins/
# Compile qtun
WORKDIR ../qtun/
RUN cargo update
RUN cargo fetch
RUN cargo build --target-dir ./ --release && \
mv ./release/qtun-client ./release/qtun-server /plugins/ && \
strip /plugins/*
@ -359,16 +359,17 @@ COPY --from=upx /upx/ /usr/
RUN upx -9 /tmp/clash
# Download naiveproxy
FROM ${ALPINE_IMG} AS naiveproxy
ENV NAIVE_VERSION="v103.0.5060.53-3"
FROM ${ALPINE_IMG} AS naive
ENV NAIVE_VERSION="v104.0.5112.79-2"
RUN apk add curl libgcc jq
RUN curl -sL https://api.github.com/repos/klzgrad/naiveproxy/releases/tags/${NAIVE_VERSION} \
| jq .assets | jq .[].name | grep naiveproxy-${NAIVE_VERSION}-openwrt-$(uname -m) \
| cut -b 2- | rev | cut -b 2- | rev | tac > list.dat
RUN echo -e "while read FILE_NAME;do\nwget https://github.com/klzgrad/naiveproxy/releases/download/\${NAIVE_VERSION}/\${FILE_NAME}\n \
tar xf \${FILE_NAME} && ldd ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive\n \
[ \$? -eq 0 ] && cp ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive /tmp/ && break\ndone < list.dat" > naiveproxy.sh && \
sh naiveproxy.sh
RUN echo "while read FILE_NAME; do" >> naive.sh && \
echo "wget https://github.com/klzgrad/naiveproxy/releases/download/\${NAIVE_VERSION}/\${FILE_NAME}" >> naive.sh && \
echo "tar xf \${FILE_NAME} && ldd ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive" >> naive.sh && \
echo "[ \$? -eq 0 ] && cp ./\$(echo \$FILE_NAME | rev | cut -b 8- | rev)/naive /tmp/ && break" >> naive.sh && \
echo "done < list.dat" >> naive.sh && sh naive.sh
COPY --from=build-base /apk/ /apk/
RUN /apk/build-base && strip /tmp/naive
@ -475,9 +476,9 @@ COPY --from=trojan /tmp/trojan* /asset/usr/bin/
COPY --from=gost /tmp/gost* /asset/usr/bin/
COPY --from=brook /tmp/brook /asset/usr/bin/
COPY --from=clash /tmp/clash /asset/usr/bin/
COPY --from=naive /tmp/naive /asset/usr/bin/
COPY --from=snell /tmp/snell-* /asset/usr/bin/
COPY --from=hysteria /tmp/hysteria /asset/usr/bin/
COPY --from=naiveproxy /tmp/naive /asset/usr/bin/
COPY --from=relaybaton /tmp/relaybaton /asset/usr/bin/
COPY --from=pingtunnel /tmp/pingtunnel /asset/usr/bin/
COPY --from=wireproxy /tmp/wireproxy /asset/usr/bin/
@ -492,4 +493,4 @@ RUN apk add --no-cache boost-program_options c-ares \
ln -s /usr/local/share/ProxyC/main.py /usr/bin/proxyc
COPY --from=asset /asset /
EXPOSE 7839
CMD ["proxyc"]
ENTRYPOINT ["proxyc"]

8
Filter/__init__.py

@ -9,6 +9,7 @@ from Filter.Shadowsocks import ssFilter
from Filter.ShadowsocksR import ssrFilter
from Filter.TrojanGo import trojanGoFilter
from Filter.Hysteria import hysteriaFilter
from Basis.Exception import filterException
filterEntry = {
'ss': ssFilter,
@ -23,5 +24,10 @@ filterEntry = {
def Filter(proxyType: str, proxyInfo: dict) -> dict:
if proxyType not in filterEntry:
raise RuntimeError('Unknown proxy type')
raise filterException('Unknown proxy type')
try:
return filterEntry[proxyType](proxyInfo)
except filterException as exp:
raise filterException(exp)
except:
raise filterException('Unknown filter error')

5
Tester/Brook.py

@ -4,9 +4,9 @@
import copy
import itertools
from Builder import Brook
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Functions import hostFormat, genFlag, getAvailablePort
@ -78,7 +78,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New brook test -> %s' % testInfo)
logging.debug('New Brook test -> %s' % testInfo)
return testInfo
@ -91,3 +91,4 @@ def load():
addStream(wsStream(isRaw, isSecure)) # websocket stream test
for stream in streams:
yield loadTest(stream)
logging.info('Brook test yield complete')

5
Tester/Hysteria.py

@ -5,9 +5,9 @@ import os
import json
import itertools
from Builder import Hysteria
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import hysteriaProtocols
from Basis.Functions import hostFormat, genFlag, getAvailablePort
@ -78,10 +78,11 @@ def loadTest(protocol: str, isObfs: bool, isAuth: bool) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New hysteria test -> %s' % testInfo)
logging.debug('New Hysteria test -> %s' % testInfo)
return testInfo
def load():
for protocol, isObfs, isAuth in itertools.product(hysteriaProtocols, [False, True], [False, True]):
yield loadTest(protocol, isObfs, isAuth)
logging.info('Hysteria test yield complete')

10
Tester/Plugin.py

@ -4,10 +4,10 @@
import os
import re
import json
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Constant import Plugins
from Tester.Settings import Settings
from Basis.Functions import genFlag, hostFormat, getAvailablePort
pluginParams = {}
@ -310,7 +310,7 @@ def paramFill(param: str) -> str:
return param
def load(proxyType: str):
def load(proxyType: str) -> list:
if proxyType not in ['ss', 'trojan-go']:
raise RuntimeError('Unknown proxy type for sip003 plugin')
pluginParams.update({
@ -321,13 +321,14 @@ def load(proxyType: str):
'PASSWD': genFlag(length = 8), # random password for test
'PATH': '/' + genFlag(length = 6), # random uri path for test
})
result = []
cloakLoad() # init cloak config
kcptunLoad() # init kcptun config
for pluginType in pluginConfig:
for pluginTest, pluginTestInfo in pluginConfig[pluginType].items(): # traverse all plugin test item
pluginParams['RANDOM'] = genFlag(length = 8) # refresh RANDOM field
pluginParams['RABBIT_PORT'] = str(getAvailablePort()) # allocate port before rabbit plugin start
yield {
result.append({
'type': pluginType,
'caption': pluginTest,
'server': { # plugin info for server
@ -339,4 +340,5 @@ def load(proxyType: str):
'param': paramFill(pluginTestInfo[1]),
},
'inject': ssInject if proxyType == 'ss' else trojanInject # for some special plugins
}
})
return result

14
Tester/Settings.py

@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Basis.Constant import WorkDir
Settings = {
'workDir': WorkDir,
'site': 'www.bing.com',
'serverBind': '',
'clientBind': '',
'host': '',
'cert': '',
'key': '',
}

69
Tester/Shadowsocks.py

@ -7,11 +7,11 @@ import base64
import itertools
from Tester import Plugin
from Builder import Shadowsocks
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import ssMethods, ssAllMethods
from Basis.Functions import md5Sum, genFlag, getAvailablePort
from Basis.Constant import PathEnv, ssMethods, ssAllMethods, mbedtlsMethods
def loadConfig(proxyInfo: dict) -> dict: # load basic config option
@ -27,30 +27,29 @@ def loadConfig(proxyInfo: dict) -> dict: # load basic config option
return config
def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
def addPathEnv(env: dict) -> dict:
return {
**env,
'PATH': PathEnv # add PATH env
}
def ssRust(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-rust-server', '-v']
return config, ['ss-rust-server', '-v'], {'RUST_BACKTRACE': 'full'}
def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssLibev(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
config = loadConfig(proxyInfo)
if isUdp: # proxy UDP traffic
config['mode'] = 'tcp_and_udp'
return config, ['ss-libev-server', '-v']
return config, ['ss-libev-server', '-v'], {}
def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
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
@ -58,15 +57,15 @@ def ssPython(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
if not isUdp:
config['no_udp'] = True # UDP traffic is not proxied
config['shadowsocks'] = 'ss-python-server'
return config, ['ss-bootstrap-server', '--debug', '-vv']
return config, ['ss-bootstrap-server', '--debug', '-vv'], {}
def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list]:
def ssPythonLegacy(proxyInfo: dict, isUdp: bool) -> tuple[dict, list, dict]:
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']
return config, ['ss-bootstrap-server', '--debug', '-vv'], {}
def loadPassword(method: str) -> str:
@ -79,7 +78,7 @@ def loadPassword(method: str) -> str:
def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) -> Process:
ssConfig, ssClient = { # generate client start command and its config file
ssConfig, ssClient, ssEnv = { # generate client start command and its config file
'ss-rust': Shadowsocks.ssRust,
'ss-libev': Shadowsocks.ssLibev,
'ss-python': Shadowsocks.ssPython,
@ -89,11 +88,11 @@ def loadClient(ssType: str, configFile: str, proxyInfo: dict, socksInfo: dict) -
return Process(Settings['workDir'], cmd = ssClient + ['-c', clientFile], file = { # load client process
'path': clientFile,
'content': json.dumps(ssConfig)
}, isStart = False)
}, env = addPathEnv(ssEnv), isStart = False)
def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process:
ssConfig, ssServer = { # generate server start command and its config file
ssConfig, ssServer, ssEnv = { # generate server start command and its config file
'ss-rust': ssRust,
'ss-libev': ssLibev,
'ss-python': ssPython,
@ -103,7 +102,7 @@ def loadServer(ssType: str, configFile: str, proxyInfo: dict) -> Process:
return Process(Settings['workDir'], cmd = ssServer + ['-c', serverFile], file = { # load server process
'path': serverFile,
'content': json.dumps(ssConfig)
}, isStart = False)
}, env = addPathEnv(ssEnv), isStart = False)
def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None = None) -> dict:
@ -135,19 +134,11 @@ def loadTest(serverType: str, clientType: str, method: str, plugin: dict or None
}
if plugin is not None:
testInfo['server'] = plugin['inject'](testInfo['server'], plugin)
logging.debug('New shadowsocks test -> %s' % testInfo)
logging.debug('New Shadowsocks test -> %s' % testInfo)
return testInfo
def load(isExtra: bool = False):
pluginTest = []
pluginIter = Plugin.load('ss')
while True:
try:
pluginTest.append(next(pluginIter)) # export data of plugin generator
except StopIteration:
break
if not isExtra: # just test basic connection
def loadCommon(pluginTest: list): # shadowsocks basic test
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
@ -158,10 +149,22 @@ def load(isExtra: bool = False):
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
def loadExtra(pluginTest: list):
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)
def load(isExtra: bool = False):
ssIter = (loadExtra if isExtra else loadCommon)(Plugin.load('ss'))
while True:
try:
yield next(ssIter)
except StopIteration:
break
logging.info('Shadowsocks test yield complete')

5
Tester/ShadowsocksR.py

@ -3,10 +3,10 @@
import os
import json
from Basis.Test import Settings
from Builder import ShadowsocksR
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Functions import genFlag, getAvailablePort
from Basis.Constant import ssrMethods, ssrProtocols, ssrObfuscations
@ -64,7 +64,7 @@ def loadTest(method: str, protocol: str, obfs: str) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New shadowsocksr test -> %s' % testInfo)
logging.debug('New ShadowsocksR test -> %s' % testInfo)
return testInfo
@ -75,3 +75,4 @@ def load():
yield loadTest('aes-128-ctr', protocol, 'plain')
for obfs in ssrObfuscations:
yield loadTest('aes-128-ctr', 'origin', obfs)
logging.info('ShadowsocksR test yield complete')

5
Tester/Trojan.py

@ -5,10 +5,10 @@ import os
import json
from Tester import Xray
from Builder import Trojan
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Constant import xtlsFlows
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genFlag, getAvailablePort
@ -110,7 +110,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New trojan test -> %s' % testInfo)
logging.debug('New Trojan test -> %s' % testInfo)
return testInfo
@ -119,3 +119,4 @@ def load():
yield loadBasicTest(streams[1]) # Trojan basic test -> TCP stream with TLS
for stream in streams: # test all stream cases
yield loadTest(stream)
logging.info('Trojan test yield complete')

14
Tester/TrojanGo.py

@ -5,9 +5,9 @@ import os
import json
from Tester import Plugin
from Builder import TrojanGo
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import trojanGoMethods
from Basis.Functions import md5Sum, genFlag, getAvailablePort
@ -85,18 +85,11 @@ def loadTest(wsObject: dict or None, ssObject: dict or None, plugin: dict or Non
}
if plugin is not None:
testInfo['server'] = plugin['inject'](testInfo['server'], plugin)
logging.debug('New trojan-go test -> %s' % testInfo)
logging.debug('New Trojan-Go test -> %s' % testInfo)
return testInfo
def load():
pluginTest = []
pluginIter = Plugin.load('trojan-go')
while True:
try:
pluginTest.append(next(pluginIter)) # export data of plugin generator
except StopIteration:
break
wsObject = {
'host': Settings['host'],
'path': '/' + genFlag(length = 6),
@ -108,5 +101,6 @@ def load():
'passwd': genFlag(length = 8)
}
yield loadTest(wsObject, None if ssObject['method'] == '' else ssObject, None)
for plugin in pluginTest: # different plugin for trojan-go
for plugin in Plugin.load('trojan-go'): # different plugin for trojan-go
yield loadTest(None, None, plugin)
logging.info('Trojan-Go test yield complete')

2
Tester/V2ray.py

@ -3,8 +3,8 @@
import copy
import itertools
from Basis.Test import Settings
from Basis.Functions import genFlag
from Tester.Settings import Settings
from Basis.Constant import quicMethods, udpObfuscations
httpConfig = {

5
Tester/VLESS.py

@ -5,10 +5,10 @@ import os
import json
from Tester import Xray
from Builder import VLESS
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Basis.Constant import xtlsFlows
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genUUID, getAvailablePort
@ -71,7 +71,7 @@ def loadTest(stream: dict) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New vless test -> %s' % testInfo)
logging.debug('New VLESS test -> %s' % testInfo)
return testInfo
@ -79,3 +79,4 @@ def load():
streams = Xray.loadStream() # load xray-core stream list
for stream in streams: # test all stream cases
yield loadTest(stream)
logging.info('VLESS test yield complete')

5
Tester/VMess.py

@ -6,9 +6,9 @@ import json
import itertools
from Tester import V2ray
from Builder import VMess
from Basis.Test import Settings
from Basis.Logger import logging
from Basis.Process import Process
from Tester.Settings import Settings
from Basis.Constant import PathEnv, vmessMethods
from Basis.Functions import md5Sum, genUUID, getAvailablePort
@ -69,7 +69,7 @@ def loadTest(method: str, aid: int, stream: dict) -> dict:
'port': proxyInfo['port'],
}
}
logging.debug('New vmess test -> %s' % testInfo)
logging.debug('New VMess test -> %s' % testInfo)
return testInfo
@ -79,3 +79,4 @@ def load():
yield loadTest(method, aid, streams[0])
for stream in streams[1:]: # skip first stream that has benn checked
yield loadTest('auto', 0, stream) # aead with auto security
logging.info('VMess test yield complete')

2
Tester/Xray.py

@ -4,8 +4,8 @@
import copy
import itertools
from Tester import V2ray
from Basis.Test import Settings
from Basis.Functions import genFlag
from Tester.Settings import Settings
from Basis.Constant import xtlsFlows, quicMethods, udpObfuscations
loadConfig = V2ray.loadConfig

128
Tester/__init__.py

@ -1,14 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import requests
from threading import Thread
from Basis.Logger import logging
from Tester.Settings import Settings
from Basis.Functions import md5Sum, genFlag, hostFormat, checkPortStatus
from Tester import Brook
from Tester import VMess
from Tester import VLESS
@ -18,7 +10,7 @@ from Tester import Hysteria
from Tester import Shadowsocks
from Tester import ShadowsocksR
entry = {
testEntry = {
'ss': Shadowsocks.load(),
'ss-all': Shadowsocks.load(isExtra = True),
'ssr': ShadowsocksR.load(),
@ -29,121 +21,3 @@ entry = {
'brook': Brook.load(),
'hysteria': Hysteria.load(),
}
def waitPort(port: int, times: int = 100, delay: int = 100) -> bool: # wait until port occupied
for i in range(times):
if not checkPortStatus(port): # port occupied
return True
time.sleep(delay / 1000) # default wait 100ms
return False # timeout
def httpCheck(socksInfo: dict, url: str, testId: str, timeout: int = 10) -> None:
socksProxy = 'socks5://%s:%i' % (hostFormat(socksInfo['addr'], v6Bracket = True), socksInfo['port'])
try:
proxy = {
'http': socksProxy,
'https': socksProxy,
}
request = requests.get(url, timeout = timeout, proxies = proxy)
request.raise_for_status()
logging.info('[%s] %s -> ok' % (testId, socksProxy))
except Exception as exp:
logging.error('[%s] %s -> error' % (testId, socksProxy))
logging.error('requests exception\n' + str(exp))
raise RuntimeError('socks5 test failed')
def runTest(testInfo: dict, testUrl: str, testFilter: set or None, delay: int = 1) -> None:
testInfo['hash'] = md5Sum(testInfo['caption'])[:12]
if testFilter is not None and testInfo['hash'] not in testFilter: return
logging.warning('[%s] %s' % (testInfo['hash'], testInfo['caption']))
testInfo['server'].start() # start test server
if waitPort(testInfo['interface']['port']): # wait for server
logging.debug('server start complete')
testInfo['client'].start() # start test client
if waitPort(testInfo['socks']['port']): # wait for client
logging.debug('client start complete')
try:
logging.debug('start test process')
time.sleep(delay)
httpCheck(testInfo['socks'], testUrl, testInfo['hash'])
testInfo['client'].quit()
testInfo['server'].quit()
except:
# client debug info
testInfo['client'].quit()
logging.warning('[%s] client info' % testInfo['hash'])
logging.error('command -> %s' % testInfo['client'].cmd)
logging.error('envVar -> %s' % testInfo['client'].env)
logging.error('file -> %s' % testInfo['client'].file)
logging.warning('[%s] client capture output' % testInfo['hash'])
logging.error('\n%s' % testInfo['client'].output)
# server debug info
testInfo['server'].quit()
logging.warning('[%s] server info' % testInfo['hash'])
logging.error('command -> %s' % testInfo['server'].cmd)
logging.error('envVar -> %s' % testInfo['server'].env)
logging.error('file -> %s' % testInfo['server'].file)
logging.warning('[%s] server capture output' % testInfo['hash'])
logging.error('\n%s' % testInfo['server'].output)
def test(testIter: iter, threadNum: int, testUrl: str, testFilter: set or None = None) -> None:
threads = []
while True: # infinite loop
try:
for thread in threads:
if thread.is_alive(): continue
threads.remove(thread) # remove dead thread
if len(threads) < threadNum:
for i in range(threadNum - len(threads)): # start threads within limit
thread = Thread( # create new thread
target = runTest,
args = (next(testIter), testUrl, testFilter)
)
thread.start()
threads.append(thread) # record thread info
time.sleep(0.1)
except StopIteration: # traverse completed
break
for thread in threads: # wait until all threads exit
thread.join()
def loadBind(serverV6: bool = False, clientV6: bool = False) -> None:
Settings['serverBind'] = '::1' if serverV6 else '127.0.0.1'
Settings['clientBind'] = '::1' if clientV6 else '127.0.0.1'
def loadCert(host: str = 'proxyc.net', remark: str = 'ProxyC') -> None:
loadPath = lambda x: os.path.join(Settings['workDir'], x)
certFlag = genFlag(length = 8)
caCert = loadPath('proxyc_%s_ca.pem' % certFlag)
caKey = loadPath('proxyc_%s_ca_key.pem' % certFlag)
cert = loadPath('proxyc_%s_cert.pem' % certFlag)
key = loadPath('proxyc_%s_cert_key.pem' % certFlag)
logging.critical('Load self-signed certificate')
os.system('mkdir -p %s' % Settings['workDir']) # create work directory
logging.critical('Creating CA certificate and key...')
os.system(' '.join(['mad', 'ca'] + [ # generate CA certificate and privkey
'--ca', caCert, '--key', caKey,
'--commonName', remark,
'--organization', remark,
'--organizationUnit', remark,
]))
logging.critical('Signing certificate...')
os.system(' '.join(['mad', 'cert'] + [ # generate certificate and privkey, then signed by CA
'--ca', caCert, '--ca_key', caKey,
'--cert', cert, '--key', key,
'--domain', host,
'--organization', remark,
'--organizationUnit', remark,
]))
logging.critical('Installing CA certificate...')
os.system('cat %s >> /etc/ssl/certs/ca-certificates.crt' % caCert) # add into system's trust list
Settings['host'] = host
Settings['cert'] = cert
Settings['key'] = key
logging.warning('Certificate loading complete')

8
env.yaml

@ -0,0 +1,8 @@
version: 'v0.1'
loglevel: 'INFO'
dir: '/tmp/ProxyC'
dns: null
api:
port: 7839
path: '/'
token: ''

133
main.py

@ -1,41 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import copy
import time
import _thread
from Basis import DnsProxy
import argparse
import compileall
from Basis import Constant
from Basis.Exception import checkException
def mainArgParse(rawArgs: list) -> argparse.Namespace:
mainParser = argparse.ArgumentParser(description = 'Start running API server')
mainParser.add_argument('--log', type = str, default = Constant.LogLevel, help = 'output log level')
mainParser.add_argument('--dns', type = str, default = Constant.DnsServer, nargs = '+', help = 'specify dns server')
mainParser.add_argument('--port', type = int, default = Constant.ApiPort, help = 'port for running')
mainParser.add_argument('--path', type = str, default = Constant.ApiPath, help = 'root path for api server')
mainParser.add_argument('--token', type = str, default = Constant.ApiToken, help = 'token for api server')
mainParser.add_argument('--thread', type = int, default = Constant.CheckThread, help = 'number of check thread')
mainParser.add_argument('-v', '--version', help = 'show version', action = 'store_true')
return mainParser.parse_args(rawArgs)
def testArgParse(rawArgs: list) -> argparse.Namespace:
testParser = argparse.ArgumentParser(description = 'Test that each function is working properly')
testParser.add_argument('PROTOCOL', type = str, help = 'test protocol name')
testParser.add_argument('-a', '--all', help = 'test extra shadowsocks items', action = 'store_true')
testParser.add_argument('-6', '--ipv6', help = 'test on ipv6 network', action = 'store_true')
testParser.add_argument('--debug', help = 'enable debug log level', action = 'store_true')
testParser.add_argument('--url', type = str, default = 'http://baidu.com', help = 'http request url')
testParser.add_argument('--cert', type = str, default = '', help = 'specify the certificate id')
testParser.add_argument('--thread', type = int, default = 16, help = 'thread number in check process')
testParser.add_argument('--select', type = str, nargs = '+', help = 'select id list for test')
return testParser.parse_args(rawArgs)
testArgs = None
testMode = False
inputArgs = copy.copy(sys.argv)
if len(inputArgs) >= 0: # remove first arg (normally file name)
inputArgs.pop(0)
if len(inputArgs) != 0 and inputArgs[0].lower() == 'test': # test mode
inputArgs.pop(0) # remove `test`
if len(inputArgs) == 0 or inputArgs[0].startswith('-'): # no protocol is specified
inputArgs = ['all'] + inputArgs
testArgs = testArgParse(inputArgs)
Constant.LogLevel = 'debug' if testArgs.debug else 'warning'
testMode = True
else:
mainArgs = mainArgParse(inputArgs)
if mainArgs.version: # output version and exit
print('ProxyC version -> %s' % Constant.Version)
sys.exit(0)
Constant.LogLevel = mainArgs.log # overwrite global options
Constant.DnsServer = mainArgs.dns
Constant.ApiPort = mainArgs.port
Constant.ApiPath = mainArgs.path
Constant.ApiToken = mainArgs.token
Constant.CheckThread = mainArgs.thread
from Tester import testEntry
from Basis.Check import Check
from Basis import Api, DnsProxy
from Basis.Logger import logging
from Basis.Manager import Manager
from Basis.Api import startServer
from Basis.Constant import Version
from Basis.Compile import startCompile
from Basis.Test import Test, loadBind, loadCert
from concurrent.futures import ThreadPoolExecutor
# dnsServers = None
dnsServers = ['223.5.5.5', '119.28.28.28']
def pythonCompile(dirRange: str = '/') -> None: # python optimize compile
for optimize in [-1, 1, 2]:
compileall.compile_dir(dirRange, quiet = 1, optimize = optimize)
logging.warning('Python optimize compile -> %s (level = %i)' % (dirRange, optimize))
def runCheck(taskId: str, taskInfo: dict) -> None:
checkResult = Check(taskId, taskInfo)
success = True
checkResult = {}
try:
checkResult = Check(taskId, taskInfo) # check by task info
logging.warning('[%s] Task finish' % taskId)
Manager.finishTask(taskId, checkResult)
except checkException as exp:
success = False
logging.error('[%s] Task error -> %s' % (taskId, exp))
except:
success = False
logging.error('[%s] Task error -> Unknown error' % taskId)
finally:
if not success: # got some error in check process
taskInfo.pop('check')
checkResult = {
**taskInfo,
'success': False,
}
Manager.finishTask(taskId, checkResult) # commit check result
def loopCheck(threadNum: int = 16) -> None:
threadPool = ThreadPoolExecutor(max_workers = threadNum)
def loop(threadNum: int) -> None:
logging.warning('Loop check start -> %i threads' % threadNum)
threadPool = ThreadPoolExecutor(max_workers = threadNum) # init thread pool
while True:
try:
taskId, taskInfo = Manager.popTask()
taskId, taskInfo = Manager.popTask() # pop a task
logging.warning('[%s] Load new task' % taskId)
except: # no more task
time.sleep(2)
continue
threadPool.submit(runCheck, taskId, taskInfo)
threadPool.submit(runCheck, taskId, taskInfo) # submit into thread pool
if testMode: # test mode
loadBind(serverV6 = testArgs.ipv6, clientV6 = testArgs.ipv6) # ipv4 / ipv6 (127.0.0.1 / ::1)
loadCert(certId = testArgs.cert) # cert config
logging.critical('TEST ITEM: %s' % testArgs.PROTOCOL)
logging.critical('SELECT: ' + str(testArgs.select))
logging.critical('URL: %s' % testArgs.url)
logging.critical('THREAD NUMBER: %i' % testArgs.thread)
logging.critical('-' * 32 + ' TEST START ' + '-' * 32)
if testArgs.PROTOCOL == 'all': # run all test items
for item in testEntry:
if item == ('ss' if testArgs.all else 'ss-all'): # skip ss / ss-all
continue
logging.critical('TEST ITEM -> ' + item)
Test(testEntry[item], testArgs.thread, testArgs.url, testArgs.select)
else: # run single item
if testArgs.PROTOCOL == 'ss' and testArgs.all: # test shadowsocks extra items
testItem = 'ss-all'
Test(testEntry[testArgs.PROTOCOL], testArgs.thread, testArgs.url, testArgs.select)
logging.critical('-' * 32 + ' TEST COMPLETE ' + '-' * 32)
sys.exit(0) # test complete
logging.warning('ProxyC starts running (%s)' % Version)
_thread.start_new_thread(startCompile, ('/usr', )) # python compile (generate .pyc file)
_thread.start_new_thread(DnsProxy.start, (dnsServers, 53)) # start dns server
_thread.start_new_thread(loopCheck, ()) # start loop check
startServer(apiToken = '') # start api server
logging.warning('ProxyC starts running (%s)' % Constant.Version)
_thread.start_new_thread(pythonCompile, ('/usr',)) # python compile (generate .pyc file)
_thread.start_new_thread(DnsProxy.start, (Constant.DnsServer, 53)) # start dns server
_thread.start_new_thread(loop, (Constant.CheckThread, )) # start check loop
Api.startServer() # start api server

66
test.py

@ -1,66 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import Tester
from Basis.Logger import logging
threadNum = 16
testItem = None
testFilter = None
testUrl = 'http://baidu.com'
helpMsg = '''
./test.py [ITEM] [OPTIONS]
[ITEM]: ss / ssr / vmess / vless / trojan / trojan-go / brook / hysteria
[OPTIONS]:
--thread NUM thread number
--url URL http check url
--filter ID1[,ID2...] test the specified id
--all test extra shadowsocks items
--ipv6 test on ipv6 network
--help show this message
'''
def getArg(field: str) -> str or None:
try:
index = sys.argv.index(field)
return sys.argv[index + 1]
except:
return None
if '--help' in sys.argv:
print(helpMsg)
sys.exit(0)
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
testItem = sys.argv[1]
if getArg('--url') is not None:
testUrl = getArg('--url')
if getArg('--thread') is not None:
threadNum = int(getArg('--thread'))
if getArg('--filter') is not None:
testFilter = set(getArg('--filter').split(','))
isV6 = '--ipv6' in sys.argv
Tester.loadBind(serverV6 = isV6, clientV6 = isV6) # ipv4 / ipv6 (127.0.0.1 / ::1)
Tester.loadCert('proxyc.net', 'ProxyC') # default cert config
logging.critical('TEST ITEM: ' + ('all' if testItem is None else testItem))
logging.critical('FILTER: %s' % testFilter)
logging.critical('URL: ' + testUrl)
logging.critical('THREAD NUMBER: %i' % threadNum)
logging.critical('-------------------------------- TEST START --------------------------------')
if testItem is not None:
if testItem == 'ss' and '--all' in sys.argv:
testItem = 'ss-all'
Tester.test(Tester.entry[testItem], threadNum, testUrl, testFilter)
else:
for item in Tester.entry:
if item == ('ss' if '--all' in sys.argv else 'ss-all'): # skip ss / ss-all
continue
logging.critical('TEST ITEM -> ' + item)
Tester.test(Tester.entry[item], threadNum, testUrl, testFilter)
logging.critical('-------------------------------- TEST COMPLETE --------------------------------')
Loading…
Cancel
Save