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: