diff --git a/Basis/Methods.py b/Basis/Methods.py index 49e19f5..b8df602 100644 --- a/Basis/Methods.py +++ b/Basis/Methods.py @@ -84,6 +84,7 @@ plugins = { plugins = {x: [plugins[x][0], plugins[x][1 if len(plugins[x]) == 2 else 0]] for x in plugins} 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 @@ -112,3 +113,6 @@ ssrObfuscations = [ # obfuscations of ShadowsocksR (obfs) 'plain', 'http_post', 'http_simple', 'random_head', 'tls_simple', 'tls1.2_ticket_auth', 'tls1.2_ticket_fastauth', ] + +# VMess Info +vmessMethods = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero'] diff --git a/Builder/V2ray.py b/Builder/V2ray.py new file mode 100644 index 0000000..4cad99e --- /dev/null +++ b/Builder/V2ray.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import copy + +httpConfig = { + 'type': 'http', + 'request': { + 'version': '1.1', + 'method': 'GET', + 'path': [], + 'headers': { + 'Host': [], + 'User-Agent': [ + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 ' + '(KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46', + ], + 'Accept-Encoding': ['gzip, deflate'], + 'Connection': ['keep-alive'], + 'Pragma': 'no-cache', + } + } +} + +kcpConfig = { + 'mtu': 1350, + 'tti': 50, + 'uplinkCapacity': 12, + 'downlinkCapacity': 100, + 'congestion': False, + 'readBufferSize': 2, + 'writeBufferSize': 2, + 'header': {} +} + + +def loadSecure(secureInfo: dict or None) -> dict: # TLS encrypt config + if secureInfo is None: + return {'security': 'none'} # without TLS options + tlsObject = { + 'allowInsecure': not secureInfo['verify'] # whether verify server's certificate + } + if secureInfo['alpn'] is not None: + tlsObject['alpn'] = secureInfo['alpn'].split(',') # multi-alpn like `h2,http/1.1` + if secureInfo['sni'] != '': + tlsObject['serverName'] = secureInfo['sni'] # SNI field in TLS protocol + return { + 'security': 'tls', + 'tlsSettings': tlsObject + } + + +def tcpStream(streamInfo: dict) -> dict: # TCP stream config + tcpObject = {} + if streamInfo['obfs'] is not None: # enable http obfs options + httpObject = copy.deepcopy(httpConfig) + httpObject['request']['path'].append(streamInfo['obfs']['path']) # obfs path (start with '/') + httpObject['request']['headers']['Host'] = streamInfo['obfs']['host'].split(',') # obfs host (maybe multiple) + tcpObject['header'] = httpObject + return { + 'network': 'tcp', + 'tcpSettings': tcpObject + } + + +def kcpStream(streamInfo: dict) -> dict: # mKCP stream config + kcpObject = copy.deepcopy(kcpConfig) + kcpObject['header']['type'] = streamInfo['obfs'] # mKCP header type -> none / srtp / utp / ... + if streamInfo['seed'] is not None: + kcpObject['seed'] = streamInfo['seed'] # mKCP obfs password + return { + 'network': 'kcp', + 'kcpSettings': kcpObject + } + + +def wsStream(streamInfo: dict) -> dict: # WebSocket stream config + wsObject = { + 'path': streamInfo['path'] # websocket connection path + } + if streamInfo['host'] != '': # empty host should not be set + wsObject['headers'] = {} + wsObject['headers']['Host'] = streamInfo['host'] + if streamInfo['ed'] is not None: # with early data options + wsObject['maxEarlyData'] = streamInfo['ed'] + wsObject['earlyDataHeaderName'] = 'Sec-WebSocket-Protocol' + return { + 'network': 'ws', + 'wsSettings': wsObject + } + + +def h2Stream(streamInfo: dict) -> dict: # HTTP/2 stream config + h2Object = { + 'path': streamInfo['path'] # http/2 connection path + } + if streamInfo['host'] != '': # empty host should not be set + h2Object['host'] = streamInfo['host'].split(',') # http/2 host maybe multiple + return { + 'network': 'http', + 'httpSettings': h2Object + } + + +def quicStream(streamInfo: dict) -> dict: # QUIC stream config + return { + 'network': 'quic', + 'quicSettings': { + 'security': streamInfo['method'], + 'key': streamInfo['passwd'], + 'header': { + 'type': streamInfo['obfs'] + } + } + } + + +def grpcStream(streamInfo: dict) -> dict: # gRPC stream config + grpcObject = { + 'serviceName': streamInfo['service'] # gRPC service name + } + if streamInfo['mode'] == 'multi': # gRPC multi-mode not work in v2fly-core + grpcObject['multiMode'] = True + return { + 'network': 'grpc', + 'grpcSettings': grpcObject + } + + +def loadStream(streamInfo: dict) -> dict: + streamEntry = { + 'tcp': tcpStream, + 'kcp': kcpStream, + 'ws': wsStream, + 'h2': h2Stream, + 'quic': quicStream, + 'grpc': grpcStream, + } + if streamInfo['type'] not in streamEntry: + raise Exception('Unknown stream type') + streamObject = streamEntry[streamInfo['type']](streamInfo) + return { + **streamObject, + **loadSecure(streamInfo['secure']) + } + + +def loadConfig(socksInfo: dict, outboundObject: dict) -> dict: # load config by socks and outbound info + return { + 'log': { + 'loglevel': 'debug' + }, + 'inbounds': [{ # expose socks5 interface (inbound) + 'port': socksInfo['port'], + 'listen': socksInfo['addr'], + 'protocol': 'socks', + 'settings': { + 'udp': True, + 'auth': 'noauth' + } + }], + 'outbounds': [outboundObject] # outbound without route object + } diff --git a/Builder/VMess.py b/Builder/VMess.py new file mode 100644 index 0000000..693d9af --- /dev/null +++ b/Builder/VMess.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from Builder import V2ray +from Basis.Methods import vmessMethods + + +def load(proxyInfo: dict, socksInfo: dict, configFile: str) -> tuple[list, str, dict]: + if proxyInfo['method'] not in vmessMethods: + raise RuntimeError('Unknown vmess method') + outboundConfig = { + 'protocol': 'vmess', + 'settings': { + 'vnext': [{ + 'address': proxyInfo['server'], + 'port': proxyInfo['port'], + 'users': [{ + 'id': proxyInfo['id'], + 'alterId': proxyInfo['aid'], + 'security': proxyInfo['method'] + }] + }] + }, + 'streamSettings': V2ray.loadStream(proxyInfo['stream']) + } + vmessConfig = V2ray.loadConfig(socksInfo, outboundConfig) # load config file for v2ray-core + return ['v2ray', '-c', configFile], json.dumps(vmessConfig), {} diff --git a/Builder/__init__.py b/Builder/__init__.py index 07dc6db..7250525 100644 --- a/Builder/__init__.py +++ b/Builder/__init__.py @@ -6,11 +6,19 @@ import copy from Builder import Shadowsocks from Builder import ShadowsocksR +from Builder import VMess from Basis.Logger import logging from Basis.Process import Process from Basis.Functions import genFlag, getAvailablePort +pathEnv = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' +clientEntry = { + 'ss': [Shadowsocks.load, '.json'], + 'ssr': [ShadowsocksR.load, '.json'], + 'vmess': [VMess.load, '.json'], +} + class Builder(object): """ Build the proxy client process and expose socks5 port. @@ -32,40 +40,38 @@ class Builder(object): output = None def __loadClient(self): - loadFunction = { - 'ss': Shadowsocks.load, - 'ssr': ShadowsocksR.load, - } - if self.proxyType not in loadFunction: - raise RuntimeError('Unknown proxy type') logging.info('[%s] Load %s proxy client at %s -> %s' % (self.id, self.proxyType, ( (('[%s]' if ':' in self.socksAddr else '%s') + ':%i') % (self.socksAddr, self.socksPort) ), str(self.proxyInfo))) - configFile = os.path.join(self.__workDir, self.id + '.json') - command, fileContent, envVar = loadFunction[self.proxyType](self.proxyInfo, { + configFile = os.path.join( # config file path + self.__workDir, self.id + clientEntry[self.proxyType][1] # workDir + taskId + suffix + ) + command, fileContent, envVar = clientEntry[self.proxyType][0](self.proxyInfo, { # load client boot info 'addr': self.socksAddr, 'port': self.socksPort, }, configFile) - envVar['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' - fileObject = { + fileObject = { # add config file settings 'path': configFile, 'content': fileContent } + envVar['PATH'] = pathEnv # add PATH env (some programs need it) self.__process = Process(self.__workDir, taskId = self.id, cmd = command, env = envVar, file = fileObject) def __init__(self, proxyType: str, proxyInfo: dict, taskId: str = '', - bind: str = '127.0.0.1', workDir: str = '/tmp/ProxyC') -> None: - self.id = genFlag(length = 12) if taskId == '' else taskId + bind: str = '127.0.0.1', workDir: str = '/tmp/ProxyC') -> None: # init proxy client + if proxyType not in clientEntry: + raise RuntimeError('Unknown proxy type') + self.id = genFlag(length = 12) if taskId == '' else taskId # load task ID self.__workDir = workDir - self.proxyType = proxyType - self.proxyInfo = copy.copy(proxyInfo) + self.proxyType = proxyType # proxy type -> ss / ssr / vmess ... + self.proxyInfo = copy.copy(proxyInfo) # proxy object -> contain connection info self.socksAddr = bind - self.socksPort = getAvailablePort() + self.socksPort = getAvailablePort() # random port for socks5 exposed self.__loadClient() - def status(self) -> bool: + def status(self) -> bool: # check if the sub process is still running return self.__process.status() - def destroy(self) -> None: + def destroy(self) -> None: # kill sub process and remove config file self.__process.quit() self.output = self.__process.output diff --git a/demo.py b/demo.py index b7bc5a8..8208735 100755 --- a/demo.py +++ b/demo.py @@ -24,8 +24,26 @@ proxySSR = { 'obfsParam': '', } +proxyVMess = { + 'server': '127.0.0.1', + 'port': 12345, + 'method': 'auto', + 'id': '614d3a56-8a04-4c65-88a2-45896f0bd13c', + 'aid': 0, + 'stream': { + 'type': 'tcp', + 'obfs': None, + 'secure': { + 'sni': '343.re', + 'alpn': None, + 'verify': True, + }, + } +} + # client = Builder('ss', proxySS) -client = Builder('ssr', proxySSR) +# client = Builder('ssr', proxySSR) +client = Builder('vmess', proxyVMess) logging.critical(client.id) logging.critical(client.proxyType)