mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-06 22:08:11 +03:00
276 lines
9.2 KiB
Python
276 lines
9.2 KiB
Python
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||
#
|
||
# This file is part of Furious.
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
"""Integrate Hysteria 2 configuration, editor, and process startup."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from Furious.Frozenlib import *
|
||
from Furious.Plugins.API import *
|
||
from Furious.Backends.Configuration import *
|
||
|
||
from .Process import Hysteria2
|
||
from .ProtocolEditors import HYSTERIA2_PROTOCOL_EDITORS
|
||
from .Protocols import HYSTERIA2_PROTOCOL_HANDLERS
|
||
from .Stats import (
|
||
HYSTERIA2_STATS_CLIENT_ID_SETTING,
|
||
HYSTERIA2_STATS_SECRET_SETTING,
|
||
HYSTERIA2_STATS_URL_SETTING,
|
||
Hysteria2StatsProvider,
|
||
configuredHysteria2StatsTarget,
|
||
)
|
||
from .TUN import *
|
||
|
||
import logging
|
||
|
||
__all__ = ['Hysteria2Plugin']
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _placeholder(x):
|
||
return x
|
||
|
||
|
||
_ = _placeholder
|
||
|
||
# Register host-owned descriptor text with the static translation extractor
|
||
# without making the headless plugin module import Qt presentation code.
|
||
_TRANSLATABLE_SETTINGS = (
|
||
_('Hysteria2 Traffic Statistics'),
|
||
_('Traffic Stats API URL'),
|
||
_('Hysteria2 server API address; /traffic is appended automatically.'),
|
||
_('Traffic Stats Client ID'),
|
||
_('Authentication ID reported by the Hysteria2 server.'),
|
||
_('Traffic Stats API Secret'),
|
||
_('Authorization value configured by the Hysteria2 server.'),
|
||
)
|
||
|
||
|
||
class Hysteria2SettingsProvider(PluginSettingsProvider):
|
||
"""Declare Hysteria 2 traffic-API preferences for the host UI."""
|
||
|
||
providerId = 'official.hysteria2.settings'
|
||
|
||
def createSections(self, parent=None, **kwargs):
|
||
"""Return host-rendered Hysteria 2 settings descriptors."""
|
||
del parent, kwargs
|
||
|
||
return (
|
||
PluginSettingsSection(
|
||
'traffic-statistics',
|
||
'Hysteria2 Traffic Statistics',
|
||
(
|
||
PluginSettingDescriptor(
|
||
'stats-url',
|
||
'Traffic Stats API URL',
|
||
'Hysteria2 server API address; /traffic is appended automatically.',
|
||
'activity.svg',
|
||
PluginSettingControl.Text,
|
||
HYSTERIA2_STATS_URL_SETTING,
|
||
placeholder='http://server:9999',
|
||
translatable=True,
|
||
),
|
||
PluginSettingDescriptor(
|
||
'stats-client-id',
|
||
'Traffic Stats Client ID',
|
||
'Authentication ID reported by the Hysteria2 server.',
|
||
'person-badge.svg',
|
||
PluginSettingControl.Text,
|
||
HYSTERIA2_STATS_CLIENT_ID_SETTING,
|
||
translatable=True,
|
||
),
|
||
PluginSettingDescriptor(
|
||
'stats-secret',
|
||
'Traffic Stats API Secret',
|
||
'Authorization value configured by the Hysteria2 server.',
|
||
'key.svg',
|
||
PluginSettingControl.Password,
|
||
HYSTERIA2_STATS_SECRET_SETTING,
|
||
translatable=True,
|
||
strip=False,
|
||
),
|
||
),
|
||
translatable=True,
|
||
),
|
||
)
|
||
|
||
|
||
class Hysteria2ActionProvider(ActionProvider):
|
||
"""Provide optional Hysteria 2 management UI independently of its runtime."""
|
||
|
||
providerId = 'official.hysteria2.management'
|
||
category = 'core'
|
||
|
||
def createActions(self, parent=None, **kwargs):
|
||
"""Create Hysteria 2 native TUN management actions."""
|
||
isCoreActive = kwargs.pop('isCoreActive', lambda coreType: False)
|
||
|
||
# These modules require a fully initialized Furious.Qt package.
|
||
from Furious.Qt import AppQAction, showMBoxNewChangesNextTime
|
||
from Furious.Qt import gettext as _
|
||
|
||
from .TunSettingsDialog import Hysteria2TunSettingsDialog
|
||
|
||
useHysteria2TUNAction = AppQAction(
|
||
_('Use Hysteria2 TUN'),
|
||
checkable=True,
|
||
checked=isHysteria2TUNEnabled(),
|
||
)
|
||
|
||
def updateUseHysteria2TUN():
|
||
"""Persist the native Hysteria 2 TUN action state."""
|
||
setHysteria2TUNEnabled(useHysteria2TUNAction.isChecked())
|
||
|
||
if SystemRuntime.isTUNMode() and isCoreActive(Hysteria2):
|
||
showMBoxNewChangesNextTime()
|
||
|
||
useHysteria2TUNAction.callback = updateUseHysteria2TUN
|
||
|
||
return (
|
||
useHysteria2TUNAction,
|
||
AppQAction(
|
||
_('Customize Hysteria2 TUN Settings...'),
|
||
callback=lambda: Hysteria2TunSettingsDialog(
|
||
parent=parent,
|
||
isConnectionActive=lambda: isCoreActive(Hysteria2),
|
||
).open(),
|
||
),
|
||
)
|
||
|
||
|
||
class Hysteria2KernelFactory(KernelFactory):
|
||
"""Construct Hysteria 2 kernels independently of protocol handling."""
|
||
|
||
factoryId = 'official.hysteria2'
|
||
configurationTypes = (ConfigHysteria2,)
|
||
kernelTypes = (Hysteria2,)
|
||
|
||
def prepareTUN(self, config) -> bool:
|
||
"""Add Hysteria 2 native TUN mode when enabled and safe to route."""
|
||
if not isHysteria2TUNEnabled():
|
||
# Work on ConnectionManager's copy so a stored native TUN block
|
||
# cannot run alongside the external tun2socks implementation.
|
||
config.pop('tun', None)
|
||
|
||
return False
|
||
|
||
if PLATFORM == 'Linux' and not SystemRuntime.isAdmin():
|
||
# Native Hysteria 2 creates the interface and routing table in its
|
||
# own process, so it cannot use ConnectionManager's privileged helper.
|
||
# Keep the existing external tun2socks path available instead.
|
||
config.pop('tun', None)
|
||
|
||
logger.warning(
|
||
'Hysteria 2 native TUN requires superuser privileges on '
|
||
'Linux; falling back to external tun2socks'
|
||
)
|
||
|
||
return False
|
||
|
||
settings = getHysteria2TUNSettings()
|
||
serverAddresses = resolveHysteria2ServerAddresses(config)
|
||
route = settings.get('route', {})
|
||
hasManualExclusions = bool(route.get('ipv4Exclude') or route.get('ipv6Exclude'))
|
||
|
||
if not serverAddresses and not hasManualExclusions:
|
||
config.pop('tun', None)
|
||
|
||
logger.error(
|
||
'Hysteria 2 native TUN disabled for this connection because '
|
||
'the server address could not be resolved and no manual route '
|
||
'exclusion is configured'
|
||
)
|
||
|
||
return False
|
||
|
||
config['tun'] = buildHysteria2TUNConfig(settings, serverAddresses)
|
||
|
||
return True
|
||
|
||
def create(self, request: KernelRequest):
|
||
"""Create a prepared Hysteria 2 kernel launch."""
|
||
if request.log:
|
||
logger.info(f'core {Hysteria2.name()} configured')
|
||
|
||
process = Hysteria2(
|
||
exitCallback=request.exitCallback,
|
||
msgCallback=request.messageCallback,
|
||
)
|
||
|
||
setattr(process, 'hysteria2StatsTarget', configuredHysteria2StatsTarget())
|
||
|
||
return KernelLaunch(
|
||
process,
|
||
request.configuration,
|
||
options=request.options,
|
||
)
|
||
|
||
def prepareDownloadTest(self, config, port: int):
|
||
"""Create a Hysteria 2 configuration with one local HTTP proxy."""
|
||
configcopy = config.deepcopy()
|
||
configcopy['http'] = {
|
||
'listen': f'127.0.0.1:{port}',
|
||
'timeout': 300,
|
||
'disable_udp': False,
|
||
}
|
||
configcopy.pop('socks5', '')
|
||
configcopy.pop('tun', None)
|
||
|
||
return configcopy
|
||
|
||
def coreVersions(self):
|
||
"""Return the bundled Hysteria 2 version."""
|
||
return (Hysteria2.version(),)
|
||
|
||
def logTimestampPatterns(self):
|
||
"""Return the timestamp format emitted by Hysteria 2."""
|
||
return (r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})',)
|
||
|
||
|
||
def _placeholder(x):
|
||
return x
|
||
|
||
|
||
_ = _placeholder
|
||
|
||
_TRANSLATABLE = [
|
||
_('Official Hysteria 2 protocol, editor, and runtime support.'),
|
||
]
|
||
|
||
|
||
class Hysteria2Plugin(FuriousPlugin):
|
||
"""Bundle official Hysteria 2 protocol and runtime capabilities."""
|
||
|
||
metadata = PluginMetadata(
|
||
'official.hysteria2',
|
||
'Hysteria2',
|
||
description='Official Hysteria 2 protocol, editor, and runtime support.',
|
||
provider='Furious',
|
||
)
|
||
|
||
def __init__(self):
|
||
"""Create an isolated Hysteria 2 runtime factory."""
|
||
self.capabilities = (
|
||
*HYSTERIA2_PROTOCOL_HANDLERS,
|
||
*HYSTERIA2_PROTOCOL_EDITORS,
|
||
Hysteria2KernelFactory(),
|
||
Hysteria2StatsProvider(),
|
||
Hysteria2SettingsProvider(),
|
||
Hysteria2ActionProvider(),
|
||
)
|