From bfde7133ca9f3213167570e9353490a716e47c9d Mon Sep 17 00:00:00 2001 From: klockky Date: Sat, 16 May 2026 07:59:56 +0300 Subject: [PATCH] =?UTF-8?q?refactor(network):=20VPN=20bypass=20as=20fallba?= =?UTF-8?q?ck=20=E2=80=94=20try=20VPN=20first,=20bypass=20only=20on=20fail?= =?UTF-8?q?ure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/transport/connection.dart | 68 ++++++++++++++++++++++-------- lib/core/transport/vpn_bypass.dart | 48 ++++++--------------- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/lib/core/transport/connection.dart b/lib/core/transport/connection.dart index 72c250a..dfee30a 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -35,29 +35,38 @@ class Connection { _setState(SocketState.connecting); try { - try { - await VpnBypassService.instance.applyIfNeeded(); - } catch (e) { - logger.w('VPN bypass: пропущено ($e)'); - } - final proxySettings = await ProxyConfig.load(); - RawSocket rawSocket; - if (proxySettings.isEnabled) { - final connector = ProxyConnector(proxySettings); - rawSocket = await connector.connect(host, port); - logger.i('Подключено через прокси ${proxySettings.type.name}'); - } else { - rawSocket = await RawSocket.connect(host, port); + // Обход VPN — только запасной путь: сначала обычное подключение + // (через VPN, если он поднят), и лишь при неудаче — мимо туннеля. + final bypassArmed = await VpnBypassService.instance.shouldArm(); + if (bypassArmed) { + await VpnBypassService.instance.restoreDefault(); } - _socket = await RawSecureSocket.secure( - rawSocket, - host: host, - onBadCertificate: (_) => true, - ); + RawSecureSocket socket; + try { + socket = await _openSecureSocket( + host, + port, + proxySettings, + timeout: bypassArmed ? const Duration(seconds: 8) : null, + ); + } catch (e) { + if (!bypassArmed) rethrow; + final r = await VpnBypassService.instance.bind(); + if (!r.bound) { + logger.w('VPN bypass: обходить нечего (${r.reason})'); + rethrow; + } + logger.w( + 'Подключение через VPN не удалось ($e) — ' + 'идём в обход → ${r.boundInterface} (${r.transport})', + ); + socket = await _openSecureSocket(host, port, proxySettings); + } + _socket = socket; _setState(SocketState.connected); logger.i('Подключено к $host:$port'); @@ -90,6 +99,29 @@ class Connection { } } + Future _openSecureSocket( + String host, + int port, + ProxySettings proxySettings, { + Duration? timeout, + }) async { + RawSocket rawSocket; + if (proxySettings.isEnabled) { + final connector = ProxyConnector(proxySettings); + rawSocket = await connector.connect(host, port); + logger.i('Подключено через прокси ${proxySettings.type.name}'); + } else { + rawSocket = timeout == null + ? await RawSocket.connect(host, port) + : await RawSocket.connect(host, port, timeout: timeout); + } + return RawSecureSocket.secure( + rawSocket, + host: host, + onBadCertificate: (_) => true, + ); + } + void write(Uint8List data) { if (_socket == null || !isConnected) { throw StateError('Нельзя писать: сокет не подключён'); diff --git a/lib/core/transport/vpn_bypass.dart b/lib/core/transport/vpn_bypass.dart index 3798253..2eeb729 100644 --- a/lib/core/transport/vpn_bypass.dart +++ b/lib/core/transport/vpn_bypass.dart @@ -53,33 +53,21 @@ class VpnBypassService { await prefs.setBool(prefKey, value); } - /// Вызывается перед каждым (ре)коннектом. - Future applyIfNeeded() async { - if (!_supported) { - return const VpnBypassResult( - enabled: false, - reason: 'unsupported_platform', - ); - } - - if (!await isEnabled()) { - await _restoreDefault(); - return const VpnBypassResult(enabled: false); - } - - final tunDetected = await _hasTunInterface(); - if (!tunDetected) { - await _restoreDefault(); - logger.i('VPN bypass: tun-интерфейс не найден — маршрут по умолчанию'); - return const VpnBypassResult(enabled: true, tunDetected: false); - } + /// true — обход включён, платформа поддерживается и виден VPN-туннель. + Future shouldArm() async { + if (!_supported) return false; + if (!await isEnabled()) return false; + return _hasTunInterface(); + } + /// Привязывает процесс к non-VPN сети (wlan*/rmnet*). + Future bind() async { try { final res = await _channel .invokeMapMethod('bindToNonVpnNetwork'); final bound = res?['bound'] == true; _bound = bound; - final result = VpnBypassResult( + return VpnBypassResult( enabled: true, tunDetected: true, bound: bound, @@ -87,22 +75,9 @@ class VpnBypassService { transport: res?['transport'] as String?, reason: res?['reason'] as String?, ); - if (bound) { - logger.i( - 'VPN bypass: трафик направлен мимо VPN → ' - '${result.boundInterface} (${result.transport})', - ); - } else { - logger.w('VPN bypass: не удалось обойти VPN (${result.reason})'); - } - return result; } on PlatformException catch (e) { logger.e('VPN bypass: ошибка платформы: ${e.message}'); - return VpnBypassResult( - enabled: true, - tunDetected: true, - reason: e.code, - ); + return VpnBypassResult(enabled: true, tunDetected: true, reason: e.code); } on MissingPluginException { return const VpnBypassResult( enabled: true, @@ -137,7 +112,8 @@ class VpnBypassService { } } - Future _restoreDefault() async { + /// Возвращает маршрутизацию процесса к системной (через VPN, если он есть). + Future restoreDefault() async { if (!_bound) return; try { await _channel.invokeMethod('unbindNetwork');