mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-03 12:28:00 +03:00
Fix native TUN ownership semantics
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -161,47 +161,40 @@ class Hysteria2KernelFactory(KernelFactory):
|
||||
kernelTypes = (Hysteria2,)
|
||||
|
||||
def prepareTUN(self, config) -> bool:
|
||||
"""Add Hysteria 2 native TUN mode when enabled and safe to route."""
|
||||
"""Preserve user TUN or replace it with Furious-managed native TUN."""
|
||||
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
|
||||
# Presence is authoritative even when the block is malformed: the
|
||||
# core must report that error instead of Furious silently changing
|
||||
# the connection to application tun2socks.
|
||||
return hasHysteria2TUNConfig(config)
|
||||
|
||||
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'
|
||||
raise TUNPreparationError(
|
||||
'Hysteria 2 native TUN requires Linux superuser privileges'
|
||||
)
|
||||
|
||||
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 '
|
||||
raise TUNPreparationError(
|
||||
'Hysteria 2 native TUN cannot start 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 usesApplicationTun2socks(self, config) -> bool:
|
||||
"""Use host tun2socks only when the runtime has no native TUN block."""
|
||||
return not hasHysteria2TUNConfig(config)
|
||||
|
||||
def create(self, request: KernelRequest):
|
||||
"""Create a prepared Hysteria 2 kernel launch."""
|
||||
if request.log:
|
||||
|
||||
@@ -31,6 +31,7 @@ __all__ = [
|
||||
'DEFAULT_HYSTERIA2_TUN_SETTINGS',
|
||||
'buildHysteria2TUNConfig',
|
||||
'getHysteria2TUNSettings',
|
||||
'hasHysteria2TUNConfig',
|
||||
'isHysteria2TUNEnabled',
|
||||
'resolveHysteria2ServerAddresses',
|
||||
'saveHysteria2TUNSettings',
|
||||
@@ -72,6 +73,17 @@ def setHysteria2TUNEnabled(enabled: bool):
|
||||
)
|
||||
|
||||
|
||||
def hasHysteria2TUNConfig(config) -> bool:
|
||||
"""Return whether a Hysteria 2 document explicitly defines native TUN.
|
||||
|
||||
Presence, rather than shape, is intentional. A malformed user-provided
|
||||
block remains authoritative for a normal connection so Hysteria 2 can
|
||||
report its validation error instead of Furious silently selecting
|
||||
application tun2socks.
|
||||
"""
|
||||
return 'tun' in config
|
||||
|
||||
|
||||
def _normalizedStringList(value) -> list[str]:
|
||||
"""Return stripped, non-empty strings from a list-like value."""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
|
||||
@@ -170,9 +170,12 @@ class XrayKernelFactory(KernelFactory):
|
||||
return None
|
||||
|
||||
def prepareTUN(self, config) -> bool:
|
||||
"""Add the configured Xray native TUN inbound when enabled."""
|
||||
"""Preserve user TUN or replace it with Furious-managed native TUN."""
|
||||
if not isXrayTUNEnabled():
|
||||
return False
|
||||
# An explicit user inbound is authoritative when Furious-managed
|
||||
# native TUN is disabled. Reporting it as handled prevents the
|
||||
# normal connection path from also starting application tun2socks.
|
||||
return hasXrayTUNInbound(config)
|
||||
|
||||
inbounds = config.get('inbounds')
|
||||
|
||||
@@ -191,6 +194,10 @@ class XrayKernelFactory(KernelFactory):
|
||||
|
||||
return True
|
||||
|
||||
def usesApplicationTun2socks(self, config) -> bool:
|
||||
"""Use host tun2socks only when the runtime has no native TUN inbound."""
|
||||
return not hasXrayTUNInbound(config)
|
||||
|
||||
def routingOptions(self, config=None):
|
||||
"""Return built-in and named routing modes supported by Xray."""
|
||||
options = [
|
||||
|
||||
+13
-1
@@ -51,11 +51,16 @@ __all__ = [
|
||||
'TrafficCounters',
|
||||
'TrafficStatsMonitor',
|
||||
'TrafficStatsProvider',
|
||||
'TUNPreparationError',
|
||||
]
|
||||
|
||||
PLUGIN_API_VERSION = 3
|
||||
|
||||
|
||||
class TUNPreparationError(RuntimeError):
|
||||
"""Report that requested native TUN cannot be prepared safely."""
|
||||
|
||||
|
||||
class CapabilityKind(str, Enum):
|
||||
"""Identify independently discoverable plugin extension points."""
|
||||
|
||||
@@ -429,7 +434,14 @@ class KernelFactory(PluginCapability):
|
||||
return None
|
||||
|
||||
def prepareTUN(self, config) -> bool:
|
||||
"""Prepare native TUN and return whether the backend handles it."""
|
||||
"""Prepare normal-connection TUN and report native TUN ownership.
|
||||
|
||||
Implementations must preserve an explicit user native-TUN definition
|
||||
when host-managed native TUN is disabled. Proxy-only operations strip
|
||||
native TUN explicitly in their own preparation method instead. Raise
|
||||
``TUNPreparationError`` when requested managed TUN cannot be prepared
|
||||
safely; the host must not silently choose another TUN implementation.
|
||||
"""
|
||||
return False
|
||||
|
||||
def usesApplicationTun2socks(self, config) -> bool:
|
||||
|
||||
@@ -904,6 +904,10 @@ class PluginRegistry:
|
||||
raise TypeError('kernel TUN preparation result must be a boolean')
|
||||
|
||||
return handled
|
||||
except TUNPreparationError:
|
||||
# A managed native-TUN request must fail the connection rather than
|
||||
# silently changing the user's selected networking implementation.
|
||||
raise
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ from .API import (
|
||||
TrafficCounters,
|
||||
TrafficStatsMonitor,
|
||||
TrafficStatsProvider,
|
||||
TUNPreparationError,
|
||||
)
|
||||
from .Profile import (
|
||||
blankConfiguration,
|
||||
@@ -95,6 +96,7 @@ __all__ = [
|
||||
'TrafficCounters',
|
||||
'TrafficStatsMonitor',
|
||||
'TrafficStatsProvider',
|
||||
'TUNPreparationError',
|
||||
'blankConfiguration',
|
||||
'blankProfile',
|
||||
'configurationFromAny',
|
||||
|
||||
@@ -157,7 +157,15 @@ class ConnectionManager(Mixins.CleanupOnExit):
|
||||
|
||||
if tunModeRequested:
|
||||
registry = getPluginRegistry()
|
||||
pluginTUN = registry.prepareTUN(configcopy)
|
||||
|
||||
try:
|
||||
pluginTUN = registry.prepareTUN(configcopy)
|
||||
except TUNPreparationError as ex:
|
||||
self._lastStartError = str(ex)
|
||||
|
||||
return abortStart(
|
||||
f'native TUN preparation failed: {self._lastStartError}'
|
||||
)
|
||||
|
||||
if not pluginTUN:
|
||||
applicationTun2socks = registry.usesApplicationTun2socks(configcopy)
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ system proxy, TUN, routing, update network clients, or real proxy cores.
|
||||
| SOCKS/SIP002 Shadowsocks codecs and generated round trips | `test_socks_uri.py`, `test_shadowsocks_uri.py` |
|
||||
| Subscription-group reconciliation and ownership isolation | `test_subscription_sync.py` |
|
||||
| External process launch, output, shutdown, threads, TUN metadata | `test_external_core.py` |
|
||||
| Xray/Hysteria2 native-TUN ownership and proxy-only stripping | `test_native_tun_semantics.py` |
|
||||
| Rolling metrics, stable buckets, lazy rendering, and hover | `test_metrics_behavior.py` |
|
||||
| Settings sandbox and navigation overlay behavior | `test_isolation_and_navigation.py` |
|
||||
| Editor mappings, lazy log rendering, routing/message-box/connection UI | `test_ui_behavior.py` |
|
||||
@@ -61,7 +62,7 @@ environment, so activate the project's virtual environment first when needed.
|
||||
python -m unittest discover -s tests -v
|
||||
|
||||
# Fast logic, persistence, plugin, controller, codec, and UI behavior
|
||||
python -m unittest tests.test_models_and_services tests.test_plugin_architecture tests.test_controllers tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_metrics_behavior tests.test_isolation_and_navigation tests.test_ui_behavior -v
|
||||
python -m unittest tests.test_models_and_services tests.test_plugin_architecture tests.test_controllers tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_native_tun_semantics tests.test_metrics_behavior tests.test_isolation_and_navigation tests.test_ui_behavior -v
|
||||
|
||||
# Direct Qt/process integration and destruction/lifetime checks
|
||||
python -m unittest tests.test_external_core tests.test_qt_lifetime -v
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
# 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/>.
|
||||
|
||||
"""Protect native-TUN ownership for normal and proxy-only operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
from Furious.Backends.Configuration import ConfigHysteria2, ConfigXray
|
||||
from Furious.Backends.Hysteria2 import Plugin as Hysteria2PluginModule
|
||||
from Furious.Backends.Hysteria2.Plugin import Hysteria2KernelFactory
|
||||
from Furious.Backends.Xray import Plugin as XrayPluginModule
|
||||
from Furious.Backends.Xray.Plugin import XrayKernelFactory
|
||||
from Furious.Plugins.API import TUNPreparationError
|
||||
from Furious.Plugins.Registry import PluginRegistry
|
||||
from Furious.Service.ConnectionManager import ConnectionManager
|
||||
|
||||
|
||||
class _RecordingConnectionManager(ConnectionManager):
|
||||
"""Capture the runtime document without starting a core process."""
|
||||
|
||||
def _startKernel(self, config, *_args, **_kwargs):
|
||||
"""Record the prepared runtime configuration as a successful launch."""
|
||||
self.runtimeConfiguration = config
|
||||
|
||||
return None, True
|
||||
|
||||
|
||||
class XrayNativeTUNTest(unittest.TestCase):
|
||||
"""Verify Xray runtime copies preserve or replace native TUN intentionally."""
|
||||
|
||||
customTUN = {
|
||||
'tag': 'user-tun',
|
||||
'protocol': 'tun',
|
||||
'settings': {'gateway': ['192.0.2.1/24']},
|
||||
}
|
||||
generatedTUN = {
|
||||
'tag': 'tun',
|
||||
'protocol': 'tun',
|
||||
'settings': {'gateway': ['10.0.0.1/16']},
|
||||
}
|
||||
httpInbound = {'tag': 'http', 'protocol': 'http'}
|
||||
|
||||
def setUp(self):
|
||||
"""Create one stateless Xray kernel capability."""
|
||||
self.factory = XrayKernelFactory()
|
||||
|
||||
def configuration(self, *, nativeTUN: bool) -> ConfigXray:
|
||||
"""Return one minimal Xray document with optional custom native TUN."""
|
||||
inbounds = [copy.deepcopy(self.httpInbound)]
|
||||
|
||||
if nativeTUN:
|
||||
inbounds.append(copy.deepcopy(self.customTUN))
|
||||
|
||||
return ConfigXray({'inbounds': inbounds, 'outbounds': []})
|
||||
|
||||
def prepare(self, original, enabled):
|
||||
"""Prepare an independent runtime copy under one toggle state."""
|
||||
runtime = original.deepcopy()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
XrayPluginModule,
|
||||
'isXrayTUNEnabled',
|
||||
return_value=enabled,
|
||||
),
|
||||
mock.patch.object(
|
||||
XrayPluginModule,
|
||||
'buildXrayTUNInbound',
|
||||
return_value=copy.deepcopy(self.generatedTUN),
|
||||
),
|
||||
):
|
||||
handled = self.factory.prepareTUN(runtime)
|
||||
|
||||
return runtime, handled
|
||||
|
||||
def testExistingTUNIsReplacedWhenManagedTUNIsEnabled(self):
|
||||
"""Replace every runtime TUN inbound without changing persisted JSON."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, True)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertEqual(runtime['inbounds'], [self.httpInbound, self.generatedTUN])
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testExistingTUNIsPreservedWhenManagedTUNIsDisabled(self):
|
||||
"""Treat a user TUN inbound as authoritative for normal connection."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testGeneratedTUNIsInjectedWhenEnabledAndMissing(self):
|
||||
"""Inject Furious-managed Xray TUN into only the runtime copy."""
|
||||
original = self.configuration(nativeTUN=False)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, True)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertEqual(runtime['inbounds'], [self.httpInbound, self.generatedTUN])
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testMissingTUNLeavesApplicationFallbackAvailableWhenDisabled(self):
|
||||
"""Report no native owner so global TUN may use application tun2socks."""
|
||||
original = self.configuration(nativeTUN=False)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
|
||||
self.assertFalse(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertTrue(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testDownloadTestExplicitlyStripsNativeTUN(self):
|
||||
"""Keep normal custom TUN while deriving a TUN-free speed-test copy."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
speedTest = self.factory.prepareDownloadTest(original, 18080)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertFalse(
|
||||
any(inbound.get('protocol') == 'tun' for inbound in speedTest['inbounds'])
|
||||
)
|
||||
|
||||
def testConnectionManagerDoesNotPairCustomTUNWithTun2socks(self):
|
||||
"""Stop host tun2socks selection once Xray reports custom native TUN."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
registry = mock.Mock()
|
||||
registry.prepareTUN.side_effect = self.factory.prepareTUN
|
||||
registry.usesApplicationTun2socks.side_effect = (
|
||||
self.factory.usesApplicationTun2socks
|
||||
)
|
||||
manager = _RecordingConnectionManager()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
XrayPluginModule,
|
||||
'isXrayTUNEnabled',
|
||||
return_value=False,
|
||||
),
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.SystemRuntime.isTUNMode',
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.getPluginRegistry',
|
||||
return_value=registry,
|
||||
),
|
||||
mock.patch('Furious.Service.ConnectionManager.Tun2socks') as tun2socks,
|
||||
):
|
||||
self.assertTrue(manager.start(original, 'Global'))
|
||||
|
||||
registry.usesApplicationTun2socks.assert_not_called()
|
||||
tun2socks.assert_not_called()
|
||||
self.assertEqual(manager.runtimeConfiguration, original)
|
||||
manager.cleanup()
|
||||
|
||||
def testManagedTUNPreparationFailureDoesNotFallBackToTun2socks(self):
|
||||
"""Fail an unavailable managed TUN instead of changing networking mode."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
registry = mock.Mock()
|
||||
registry.prepareTUN.side_effect = TUNPreparationError('managed TUN failed')
|
||||
manager = _RecordingConnectionManager()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.SystemRuntime.isTUNMode',
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.getPluginRegistry',
|
||||
return_value=registry,
|
||||
),
|
||||
mock.patch('Furious.Service.ConnectionManager.Tun2socks') as tun2socks,
|
||||
):
|
||||
self.assertFalse(manager.start(original, 'Global'))
|
||||
|
||||
self.assertEqual(manager.lastStartError, 'managed TUN failed')
|
||||
registry.usesApplicationTun2socks.assert_not_called()
|
||||
tun2socks.assert_not_called()
|
||||
self.assertFalse(hasattr(manager, 'runtimeConfiguration'))
|
||||
|
||||
def testRegistryPropagatesIntentionalTUNPreparationFailure(self):
|
||||
"""Keep the explicit failure distinct from an unsupported capability."""
|
||||
registry = PluginRegistry()
|
||||
factory = mock.Mock(factoryId='managed-tun-factory')
|
||||
factory.prepareTUN.side_effect = TUNPreparationError('managed TUN failed')
|
||||
registry.factoryForConfig = mock.Mock(return_value=factory)
|
||||
|
||||
with self.assertRaisesRegex(TUNPreparationError, 'managed TUN failed'):
|
||||
registry.prepareTUN(self.configuration(nativeTUN=False))
|
||||
|
||||
|
||||
class Hysteria2NativeTUNTest(unittest.TestCase):
|
||||
"""Verify Hysteria 2 runtime copies preserve or replace native TUN."""
|
||||
|
||||
customTUN = {
|
||||
'name': 'user-tun',
|
||||
'address': {'ipv4': '192.0.2.1/30'},
|
||||
}
|
||||
generatedTUN = {
|
||||
'name': 'hytun',
|
||||
'address': {'ipv4': '100.100.100.101/30'},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
"""Create one stateless Hysteria 2 kernel capability."""
|
||||
self.factory = Hysteria2KernelFactory()
|
||||
|
||||
def configuration(self, *, nativeTUN: bool) -> ConfigHysteria2:
|
||||
"""Return one minimal Hysteria 2 document with optional custom TUN."""
|
||||
config = ConfigHysteria2(
|
||||
{
|
||||
'server': '203.0.113.10:443',
|
||||
'http': {'listen': '127.0.0.1:10809'},
|
||||
}
|
||||
)
|
||||
|
||||
if nativeTUN:
|
||||
config['tun'] = copy.deepcopy(self.customTUN)
|
||||
|
||||
return config
|
||||
|
||||
def prepare(self, original, enabled):
|
||||
"""Prepare an independent runtime copy under one toggle state."""
|
||||
runtime = original.deepcopy()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'isHysteria2TUNEnabled',
|
||||
return_value=enabled,
|
||||
),
|
||||
mock.patch.object(Hysteria2PluginModule, 'PLATFORM', 'Windows'),
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'getHysteria2TUNSettings',
|
||||
return_value={},
|
||||
),
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'resolveHysteria2ServerAddresses',
|
||||
return_value=['203.0.113.10'],
|
||||
),
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'buildHysteria2TUNConfig',
|
||||
return_value=copy.deepcopy(self.generatedTUN),
|
||||
),
|
||||
):
|
||||
handled = self.factory.prepareTUN(runtime)
|
||||
|
||||
return runtime, handled
|
||||
|
||||
def testExistingTUNIsReplacedWhenManagedTUNIsEnabled(self):
|
||||
"""Replace the runtime block without changing the persisted document."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, True)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime['tun'], self.generatedTUN)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testExistingTUNIsPreservedWhenManagedTUNIsDisabled(self):
|
||||
"""Treat a user TUN block as authoritative for normal connection."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testMalformedExplicitTUNStillPreventsSilentFallback(self):
|
||||
"""Leave validation of an explicit malformed block to Hysteria 2."""
|
||||
original = self.configuration(nativeTUN=False)
|
||||
original['tun'] = 'malformed-user-value'
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime['tun'], 'malformed-user-value')
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testGeneratedTUNIsInjectedWhenEnabledAndMissing(self):
|
||||
"""Inject Furious-managed Hysteria 2 TUN into only the runtime copy."""
|
||||
original = self.configuration(nativeTUN=False)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, True)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime['tun'], self.generatedTUN)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertFalse(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testMissingTUNLeavesApplicationFallbackAvailableWhenDisabled(self):
|
||||
"""Report no native owner so global TUN may use application tun2socks."""
|
||||
original = self.configuration(nativeTUN=False)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
|
||||
self.assertFalse(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertTrue(self.factory.usesApplicationTun2socks(runtime))
|
||||
|
||||
def testUnavailableManagedTUNFailsWithoutRemovingUserTUN(self):
|
||||
"""Do not replace a requested native TUN with application tun2socks."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
runtime = original.deepcopy()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'isHysteria2TUNEnabled',
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch.object(Hysteria2PluginModule, 'PLATFORM', 'Linux'),
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule.SystemRuntime,
|
||||
'isAdmin',
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(TUNPreparationError, 'superuser privileges'):
|
||||
self.factory.prepareTUN(runtime)
|
||||
|
||||
self.assertEqual(runtime, original)
|
||||
|
||||
def testDownloadTestExplicitlyStripsNativeTUN(self):
|
||||
"""Keep normal custom TUN while deriving a TUN-free speed-test copy."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
snapshot = copy.deepcopy(original)
|
||||
|
||||
runtime, handled = self.prepare(original, False)
|
||||
speedTest = self.factory.prepareDownloadTest(original, 18080)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(runtime, original)
|
||||
self.assertEqual(original, snapshot)
|
||||
self.assertNotIn('tun', speedTest)
|
||||
|
||||
def testConnectionManagerDoesNotPairCustomTUNWithTun2socks(self):
|
||||
"""Stop host tun2socks selection once Hysteria 2 owns native TUN."""
|
||||
original = self.configuration(nativeTUN=True)
|
||||
registry = mock.Mock()
|
||||
registry.prepareTUN.side_effect = self.factory.prepareTUN
|
||||
registry.usesApplicationTun2socks.side_effect = (
|
||||
self.factory.usesApplicationTun2socks
|
||||
)
|
||||
manager = _RecordingConnectionManager()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
Hysteria2PluginModule,
|
||||
'isHysteria2TUNEnabled',
|
||||
return_value=False,
|
||||
),
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.SystemRuntime.isTUNMode',
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch(
|
||||
'Furious.Service.ConnectionManager.getPluginRegistry',
|
||||
return_value=registry,
|
||||
),
|
||||
mock.patch('Furious.Service.ConnectionManager.Tun2socks') as tun2socks,
|
||||
):
|
||||
self.assertTrue(manager.start(original, 'Global'))
|
||||
|
||||
registry.usesApplicationTun2socks.assert_not_called()
|
||||
tun2socks.assert_not_called()
|
||||
self.assertEqual(manager.runtimeConfiguration, original)
|
||||
manager.cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user