mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-13 01:38:33 +03:00
Quote Linux tun2socks script arguments
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -69,6 +69,7 @@ import copy
|
||||
import time
|
||||
import ipaddress
|
||||
import weakref
|
||||
import shlex
|
||||
|
||||
__all__ = ['ConnectionManager', 'ConnectionStartOperation', 'ConnectionStartStage']
|
||||
|
||||
@@ -1193,27 +1194,31 @@ class ConnectionStartOperation(QtCore.QObject):
|
||||
|
||||
self._tun.cleanup = _linuxCleanup
|
||||
|
||||
deviceName, interface = (
|
||||
shlex.quote(APPLICATION_TUN2SOCKS_DEVICE_NAME),
|
||||
shlex.quote(str(self._interface)),
|
||||
)
|
||||
|
||||
commandBringUpTUN = ''
|
||||
|
||||
if not SystemRoutingTable.LinuxFindTUNDevice(APPLICATION_TUN2SOCKS_DEVICE_NAME):
|
||||
commandBringUpTUN = (
|
||||
f'ip tuntap add mode tun dev '
|
||||
f'{APPLICATION_TUN2SOCKS_DEVICE_NAME}\n'
|
||||
f'{deviceName}\n'
|
||||
f'ip addr add 10.10.10.10/24 dev '
|
||||
f'{APPLICATION_TUN2SOCKS_DEVICE_NAME}\n'
|
||||
f'ip link set dev {APPLICATION_TUN2SOCKS_DEVICE_NAME} up'
|
||||
f'{deviceName}\n'
|
||||
f'ip link set dev {deviceName} up'
|
||||
)
|
||||
|
||||
commandAddDefaultRoute = (
|
||||
f'ip route add default dev {APPLICATION_TUN2SOCKS_DEVICE_NAME} ' 'metric 5'
|
||||
)
|
||||
commandAddDefaultRoute = f'ip route add default dev {deviceName} ' 'metric 5'
|
||||
|
||||
def route(source, destination):
|
||||
return f'{source} via {destination} dev {self._interface}'
|
||||
|
||||
iproute = SystemRoutingTable.LinuxGetIpRoute()
|
||||
commandBypass = '\n'.join(
|
||||
f'ip route add {route(sourceIP, destinationIP)}'
|
||||
f'ip route add {shlex.quote(str(sourceIP))} '
|
||||
f'via {shlex.quote(str(destinationIP))} dev {interface}'
|
||||
for sourceIP, destinationIP in SystemRoutingTable.managedRoutes
|
||||
if iproute.find(route(sourceIP, destinationIP)) == -1
|
||||
)
|
||||
@@ -2048,6 +2053,11 @@ class ConnectionManager(Mixins.CleanupOnExit):
|
||||
|
||||
tun.cleanup = functools.partial(_linuxCleanup)
|
||||
|
||||
deviceName, quotedInterface = (
|
||||
shlex.quote(APPLICATION_TUN2SOCKS_DEVICE_NAME),
|
||||
shlex.quote(str(interface)),
|
||||
)
|
||||
|
||||
if SystemRoutingTable.LinuxFindTUNDevice(APPLICATION_TUN2SOCKS_DEVICE_NAME):
|
||||
logger.info(
|
||||
f'find TUN device {APPLICATION_TUN2SOCKS_DEVICE_NAME} success. '
|
||||
@@ -2062,14 +2072,13 @@ class ConnectionManager(Mixins.CleanupOnExit):
|
||||
)
|
||||
|
||||
commandBringUpTUN = (
|
||||
f'ip tuntap add mode tun dev {APPLICATION_TUN2SOCKS_DEVICE_NAME}\n'
|
||||
f'ip addr add 10.10.10.10/24 dev {APPLICATION_TUN2SOCKS_DEVICE_NAME}\n'
|
||||
f'ip link set dev {APPLICATION_TUN2SOCKS_DEVICE_NAME} up'
|
||||
f'ip tuntap add mode tun dev {deviceName}\n'
|
||||
f'ip addr add 10.10.10.10/24 dev {deviceName}\n'
|
||||
f'ip link set dev {deviceName} up'
|
||||
)
|
||||
|
||||
commandAddDefaultRoute = (
|
||||
f'ip route add default dev {APPLICATION_TUN2SOCKS_DEVICE_NAME} '
|
||||
'metric 5'
|
||||
f'ip route add default dev {deviceName} ' 'metric 5'
|
||||
)
|
||||
|
||||
def route(source, destination) -> str:
|
||||
@@ -2080,7 +2089,8 @@ class ConnectionManager(Mixins.CleanupOnExit):
|
||||
|
||||
commandBypass = '\n'.join(
|
||||
list(
|
||||
f'ip route add {route(sourceIP, destinationIP)}'
|
||||
f'ip route add {shlex.quote(str(sourceIP))} '
|
||||
f'via {shlex.quote(str(destinationIP))} dev {quotedInterface}'
|
||||
for sourceIP, destinationIP in SystemRoutingTable.managedRoutes
|
||||
if iproute.find(route(sourceIP, destinationIP)) == -1
|
||||
)
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
|
||||
| --- | --- |
|
||||
| [test_application_process.py](test_application_process.py) | Exact application-child ownership, spawned factory/runner failure classification and crash reporting, captured and asserted expected child diagnostics, partial Qt startup, crash-write failure, intentional exits, exception/signal handling and command-line dispatch. |
|
||||
| [test_architecture_refactors.py](test_architecture_refactors.py) | Startup acquisition/rollback including partial controller construction and cleanup retry, singleton election and real isolated IPC race, tray/exit policy, host integration ownership, bounded core-log transport, connection transactions, stylesheet composition. |
|
||||
| [test_connection_startup_async.py](test_connection_startup_async.py) | Real local-listener readiness, timeout/cancel/replacement, semantic exits, DNS reply lifetime, mocked platform-specific TUN sequencing. |
|
||||
| [test_connection_startup_async.py](test_connection_startup_async.py) | Real local-listener readiness, timeout/cancel/replacement, semantic exits, DNS reply lifetime, mocked platform-specific TUN sequencing, quoted Linux tun2socks scripts and unchanged route-existence detection in both startup paths. |
|
||||
| [test_controllers.py](test_controllers.py) | Connection state/error/reconnect transitions, reentrant cancellation/replacement before launch and during completion, startup restoration, shared settings, routing fallback persistence and tray/selector agreement after custom-routing disable/re-enable. |
|
||||
| [test_runtime_lifecycle.py](test_runtime_lifecycle.py) | Qt-thread exit dispatch, commit/exit races, duplicate and late exits, idempotent release and retained failure/retry ownership, failed reap/handle close, spawn failure, queue/timer disposal on preparation failure. |
|
||||
| [test_external_core.py](test_external_core.py) | Harmless real process launch/output/shutdown, partial thread-start rollback, non-finite timeout rejection, failed reap/thread-join retry, readiness/TUN metadata, Windows paths with spaces, subscription rejection of executable profiles, bounded DNS references. |
|
||||
|
||||
@@ -27,6 +27,7 @@ from Furious.Service.ConnectionManager import (
|
||||
ConnectionManager,
|
||||
ConnectionStartStage,
|
||||
_ConditionProbe,
|
||||
ConnectionStartOperation,
|
||||
)
|
||||
from Furious.Service.DnsResolver import DnsResolutionOperation, DnsResolver
|
||||
from Furious.Frozenlib import AppSettings
|
||||
@@ -38,8 +39,11 @@ from shiboken6 import isValid, delete as deleteQObject
|
||||
from tests.support import application, processQtEvents, waitFor
|
||||
|
||||
from unittest import TestCase, mock
|
||||
from types import SimpleNamespace
|
||||
|
||||
import importlib
|
||||
import io
|
||||
import shlex
|
||||
import unittest
|
||||
|
||||
|
||||
@@ -1185,6 +1189,120 @@ class ConnectionStartupAsyncTest(TestCase):
|
||||
self.assertIsNone(manager._activeStartOperation)
|
||||
|
||||
|
||||
class LinuxTun2socksScriptTest(TestCase):
|
||||
def testBothStartupPathsQuoteArgumentsWithoutChangingRouteDetection(self):
|
||||
module = importlib.import_module('Furious.Service.ConnectionManager')
|
||||
interface = "eth'$x"
|
||||
device = "tun'$x"
|
||||
source, gateway = '192.0.2.1', '192.0.2.254'
|
||||
|
||||
for asynchronous in (True, False):
|
||||
for existing in (True, False):
|
||||
with self.subTest(asynchronous=asynchronous, existing=existing):
|
||||
file = io.StringIO()
|
||||
file.name = 'tmpfixture.sh'
|
||||
scripts = []
|
||||
tun = _Runtime()
|
||||
rawRoute = f'{source} via {gateway} dev {interface}'
|
||||
|
||||
def executeScript(_path, **kwargs):
|
||||
scripts.append(file.getvalue())
|
||||
self.assertEqual(kwargs['shell'], 'bash')
|
||||
return True
|
||||
|
||||
with (
|
||||
mock.patch.object(module, 'PLATFORM', 'Linux'),
|
||||
mock.patch.object(
|
||||
module, 'APPLICATION_TUN2SOCKS_DEVICE_NAME', device
|
||||
),
|
||||
mock.patch.object(module, 'Tun2socks', return_value=tun),
|
||||
mock.patch.object(
|
||||
module.tempfile, 'NamedTemporaryFile', return_value=file
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRuntime, 'flatpakID', return_value=''
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRoutingTable,
|
||||
'getDefaultGateway',
|
||||
return_value=[(gateway, interface)],
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRoutingTable,
|
||||
'managedRoutes',
|
||||
[] if not asynchronous else [[source, gateway]],
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRoutingTable,
|
||||
'LinuxFindTUNDevice',
|
||||
return_value=False,
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRoutingTable,
|
||||
'LinuxGetIpRoute',
|
||||
return_value=rawRoute if existing else '',
|
||||
),
|
||||
mock.patch.object(
|
||||
module.SystemRoutingTable,
|
||||
'LinuxExecutePrivilegedScript',
|
||||
side_effect=executeScript,
|
||||
),
|
||||
):
|
||||
if asynchronous:
|
||||
operation = SimpleNamespace(
|
||||
_isCurrent=lambda: True,
|
||||
_setStage=mock.Mock(),
|
||||
_interface=interface,
|
||||
_tun=tun,
|
||||
_waitForTunDevice=mock.Mock(),
|
||||
_fail=mock.Mock(),
|
||||
)
|
||||
ConnectionStartOperation._prepareLinuxTun(operation)
|
||||
operation._fail.assert_not_called()
|
||||
else:
|
||||
attempt = SimpleNamespace(
|
||||
runtimeConfiguration=_Configuration(),
|
||||
tun2socksSettingsCallers=module._Tun2socksSettingsCallers(
|
||||
{}
|
||||
),
|
||||
ownRuntime=mock.Mock(),
|
||||
rollback=mock.Mock(),
|
||||
)
|
||||
manager = SimpleNamespace(
|
||||
waitForTUNDeviceBroughtUp=mock.Mock(return_value=True)
|
||||
)
|
||||
self.assertTrue(
|
||||
ConnectionManager._startApplicationTun2socks(
|
||||
manager, attempt, None, None
|
||||
)
|
||||
)
|
||||
tun.stop()
|
||||
attempt.rollback.assert_not_called()
|
||||
|
||||
self.assertEqual(len(scripts), 1)
|
||||
self.assertIn(shlex.quote(device), scripts[0])
|
||||
self.assertEqual('via ' in scripts[0], not existing)
|
||||
|
||||
if not existing:
|
||||
self.assertIn(shlex.quote(interface), scripts[0])
|
||||
bypass = next(
|
||||
line for line in scripts[0].splitlines() if 'via ' in line
|
||||
)
|
||||
self.assertEqual(
|
||||
shlex.split(bypass),
|
||||
[
|
||||
'ip',
|
||||
'route',
|
||||
'add',
|
||||
source,
|
||||
'via',
|
||||
gateway,
|
||||
'dev',
|
||||
interface,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application()
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user