From 10c2e3baee43f27219d13b54750d386aa9fd8bbc Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sun, 10 Sep 2023 14:06:22 +0800 Subject: [PATCH] Add hysteria2 support Signed-off-by: Loren Eteval --- Furious/Action/Connect.py | 133 +++++++++++++++++++++++++++++++++-- Furious/Core/Core.py | 41 +++++++++++ Furious/Core/Intellisense.py | 55 ++++++++++++--- Furious/Utility/Utility.py | 1 + 4 files changed, 213 insertions(+), 17 deletions(-) diff --git a/Furious/Action/Connect.py b/Furious/Action/Connect.py index f3a43782..059751e9 100644 --- a/Furious/Action/Connect.py +++ b/Furious/Action/Connect.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Furious.Core.Core import XrayCore, Hysteria1 +from Furious.Core.Core import XrayCore, Hysteria1, Hysteria2 from Furious.Core.TorRelay import TorRelay from Furious.Core.Tun2socks import Tun2socks from Furious.Core.Intellisense import Intellisense @@ -123,6 +123,7 @@ class ConnectAction(Action): self.XrayCore = XrayCore() self.Hysteria1 = Hysteria1() + self.Hysteria2 = Hysteria2() self.Tun2socks = Tun2socks() self.TorRelay = TorRelay() @@ -224,6 +225,55 @@ class ConnectAction(Action): f'{Hysteria1.name()}: {_("Core terminated unexpectedly")}' ) + def Hysteria2ExitCallback(self, exitcode): + if self.coreName: + # If core is running + assert self.coreRunning + assert self.coreName == Hysteria2.name() + + if exitcode == Hysteria2.ExitCode.SystemShuttingDown: + # System shutting down. Do nothing + return + + if exitcode == Hysteria2.ExitCode.ConfigurationError: + if not self.isConnecting(): + # Protect connecting action. Mandatory + return self.disconnectAction( + f'{Hysteria2.name()}: {_("Invalid server configuration")}' + ) + else: + self.coreRunning = False + self.disconnectReason = ( + f'{Hysteria2.name()}: {_("Invalid server configuration")}' + ) + + return + + if exitcode == Hysteria2.ExitCode.ServerStartFailure: + if not self.isConnecting(): + # Protect connecting action. Mandatory + return self.disconnectAction( + f'{Hysteria2.name()}: {_("Failed to start core")}' + ) + else: + self.coreRunning = False + self.disconnectReason = ( + f'{Hysteria2.name()}: {_("Failed to start core")}' + ) + + return + + if not self.isConnecting(): + # Protect connecting action. Mandatory + self.disconnectAction( + f'{Hysteria2.name()}: {_("Core terminated unexpectedly")}' + ) + else: + self.coreRunning = False + self.disconnectReason = ( + f'{Hysteria2.name()}: {_("Core terminated unexpectedly")}' + ) + def Tun2socksExitCallback(self, exitcode): if exitcode == Tun2socks.ExitCode.SystemShuttingDown: # System shutting down. Do nothing @@ -247,10 +297,12 @@ class ConnectAction(Action): self.XrayCore.registerExitCallback(None) self.Hysteria1.registerExitCallback(None) + self.Hysteria2.registerExitCallback(None) # Stop any potentially running core self.XrayCore.stop() self.Hysteria1.stop() + self.Hysteria2.stop() self.coreRunning = False @@ -470,6 +522,9 @@ class ConnectAction(Action): self.Hysteria1.registerExitCallback( lambda exitcode: self.Hysteria1ExitCallback(exitcode) ) + self.Hysteria2.registerExitCallback( + lambda exitcode: self.Hysteria2ExitCallback(exitcode) + ) self.Tun2socks.registerExitCallback( lambda exitcode: self.Tun2socksExitCallback(exitcode) ) @@ -533,9 +588,6 @@ class ConnectAction(Action): self.errorHttpsProxyConf() else: if validateHttpsProxyServer(httpsProxyServer): - routing = APP().Routing - - logger.info(f'core {XrayCore.name()} configured') def fixLoggingRelativePath(attr): # Relative path fails if booting on start up @@ -560,9 +612,13 @@ class ConnectAction(Action): self.coreJSON['log'][attr] = fix + logger.info(f'core {XrayCore.name()} configured') + fixLoggingRelativePath('access') fixLoggingRelativePath('error') + routing = APP().Routing + # Filter Route My Traffic Through Tor, Custom if routing in list( filter( @@ -683,12 +739,12 @@ class ConnectAction(Action): self.errorHttpsProxyConf() else: if validateHttpsProxyServer(httpsProxyServer): + logger.info(f'core {Hysteria1.name()} configured') + self.coreRunning = True routing = APP().Routing - logger.info(f'core {Hysteria1.name()} configured') - # Filter Route My Traffic Through Tor, Global, Custom if routing in list( filter( @@ -772,6 +828,71 @@ class ConnectAction(Action): return Hysteria1.name() + if Intellisense.getCoreType(self.coreJSON) == Hysteria2.name(): + # Assuming is Hysteria2 configuration + + if isVPNMode(): + # Check socks inbound is valid + try: + socksProxyServer = self.coreJSON['socks5']['listen'] + + if socksProxyServer is None: + # No SOCKS proxy endpoint configured + raise SocksProxyServerError + except (KeyError, SocksProxyServerError): + self.reset() + self.errorSocksProxyConf() + + # Return to caller + return Hysteria2.name() + else: + # Validate socks proxy server + if not validateSocksProxyServer(socksProxyServer): + # Return to caller + return Hysteria2.name() + + try: + httpsProxyServer = self.coreJSON['http']['listen'] + + if httpsProxyServer is None: + # No HTTP proxy endpoint configured + raise HttpsProxyServerError + except (KeyError, HttpsProxyServerError): + self.reset() + self.errorHttpsProxyConf() + else: + if validateHttpsProxyServer(httpsProxyServer): + logger.info(f'core {Hysteria2.name()} configured') + + self.coreRunning = True + + routing = APP().Routing + + if routing == 'Route My Traffic Through Tor': + logger.info(f'routing is {routing}') + + if TorRelay.checkIfExists(): + logger.info( + f'find Tor CLI in path success. Version: {TorRelay.version()}' + ) + + self.Hysteria2.start(self.coreText) + + self.startTorRelay(Hysteria2.name(), httpsProxyServer) + else: + logger.error('find Tor CLI in path failed') + + self.coreRunning = False + self.disconnectReason = f'{Hysteria2.name()}: {_("Cannot find Tor CLI in PATH")}' + + return Hysteria2.name() + else: + logger.info(f'routing is {routing}') + + self.Hysteria2.start(self.coreText) + + return Hysteria2.name() + # No matching core return '' diff --git a/Furious/Core/Core.py b/Furious/Core/Core.py index 0a568f59..1d9ed080 100644 --- a/Furious/Core/Core.py +++ b/Furious/Core/Core.py @@ -216,3 +216,44 @@ class Hysteria1(Core): def start(self, json, rule, mmdb, **kwargs): super().start(target=startHysteria1, args=(json, rule, mmdb), **kwargs) + + +def startHysteria2(json): + try: + import hysteria2 + except ImportError: + # Fake running process + while True: + pass + else: + hysteria2.startFromJSON(json) + + +class Hysteria2(Core): + class ExitCode: + ConfigurationError = 23 + # Windows: 4294967295. Darwin, Linux: 255 (-1) + ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255 + # Windows shutting down + SystemShuttingDown = 0x40010004 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @staticmethod + def name(): + return 'Hysteria2' + + @staticmethod + def version(): + try: + import hysteria2 + + return hysteria2.__version__ + except Exception: + # Any non-exit exceptions + + return '0.0.0' + + def start(self, json, **kwargs): + super().start(target=startHysteria2, args=(json,), **kwargs) diff --git a/Furious/Core/Intellisense.py b/Furious/Core/Intellisense.py index 7583e47f..41097b5a 100644 --- a/Furious/Core/Intellisense.py +++ b/Furious/Core/Intellisense.py @@ -15,20 +15,50 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Furious.Core.Core import XrayCore, Hysteria1 +from Furious.Core.Core import XrayCore, Hysteria1, Hysteria2 from Furious.Utility.Utility import Protocol, protocolRepr class Intellisense: @staticmethod def getCoreType(ob): - if ob.get('inbounds') is not None or ob.get('outbounds') is not None: + def hasField(field): + return ob.get(field) is not None + + if hasField('inbounds') or hasField('outbounds'): # Assuming is XrayCore return XrayCore.name() - if ob.get('server') is not None: - # Assuming is Hysteria1. This behavior might be changed in the future - return Hysteria1.name() + if hasField('server'): + if ( + hasField('protocol') + or hasField('up_mbps') + or hasField('down_mbps') + or hasField('auth_str') + or hasField('alpn') + or hasField('server_name') + or hasField('insecure') + or hasField('recv_window_conn') + or hasField('recv_window') + or isinstance(ob.get('obfs'), str) + or hasField('fast_open') + or hasField('lazy_start') + ): + return Hysteria1.name() + elif ( + hasField('tls') + or hasField('transport') + or hasField('quic') + or hasField('bandwidth') + or hasField('tcpForwarding') + or hasField('udpForwarding') + or hasField('tcpTProxy') + or hasField('udpTProxy') + or isinstance(ob.get('obfs'), dict) + or hasField('fastOpen') + or hasField('lazy') + ): + return Hysteria2.name() return '' @@ -43,6 +73,9 @@ class Intellisense: if Intellisense.getCoreType(ob) == Hysteria1.name(): return Protocol.Hysteria1 + if Intellisense.getCoreType(ob) == Hysteria2.name(): + return Protocol.Hysteria2 + return '' except Exception: # Any non-exit exceptions @@ -76,6 +109,9 @@ class Intellisense: if Intellisense.getCoreType(ob) == Hysteria1.name(): return ob['server'].split(':')[0] + if Intellisense.getCoreType(ob) == Hysteria2.name(): + return ob['server'].split(':')[0] + return '' except Exception: # Any non-exit exceptions @@ -109,6 +145,9 @@ class Intellisense: if Intellisense.getCoreType(ob) == Hysteria1.name(): return ob['server'].split(':')[1] + if Intellisense.getCoreType(ob) == Hysteria2.name(): + return ob['server'].split(':')[1] + return '' except Exception: # Any non-exit exceptions @@ -123,9 +162,6 @@ class Intellisense: if outbound['tag'] == 'proxy': return outbound['streamSettings']['network'] - if Intellisense.getCoreType(ob) == Hysteria1.name(): - return '' - return '' except Exception: # Any non-exit exceptions @@ -140,9 +176,6 @@ class Intellisense: if outbound['tag'] == 'proxy': return outbound['streamSettings']['security'] - if Intellisense.getCoreType(ob) == Hysteria1.name(): - return '' - return '' except Exception: # Any non-exit exceptions diff --git a/Furious/Utility/Utility.py b/Furious/Utility/Utility.py index 48f075a5..d1f99261 100644 --- a/Furious/Utility/Utility.py +++ b/Furious/Utility/Utility.py @@ -58,6 +58,7 @@ class Protocol: VLESS = 'VLESS' Shadowsocks = 'shadowsocks' Hysteria1 = 'hysteria1' + Hysteria2 = 'hysteria2' class StateContext: