refactor(network): VPN bypass as fallback — try VPN first, bypass only on failure

This commit is contained in:
klockky
2026-05-16 07:59:56 +03:00
parent cd848f73fb
commit bfde7133ca
2 changed files with 62 additions and 54 deletions
+50 -18
View File
@@ -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<RawSecureSocket> _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('Нельзя писать: сокет не подключён');
+12 -36
View File
@@ -53,33 +53,21 @@ class VpnBypassService {
await prefs.setBool(prefKey, value);
}
/// Вызывается перед каждым (ре)коннектом.
Future<VpnBypassResult> 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<bool> shouldArm() async {
if (!_supported) return false;
if (!await isEnabled()) return false;
return _hasTunInterface();
}
/// Привязывает процесс к non-VPN сети (wlan*/rmnet*).
Future<VpnBypassResult> bind() async {
try {
final res = await _channel
.invokeMapMethod<String, dynamic>('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<void> _restoreDefault() async {
/// Возвращает маршрутизацию процесса к системной (через VPN, если он есть).
Future<void> restoreDefault() async {
if (!_bound) return;
try {
await _channel.invokeMethod('unbindNetwork');