refactor(network): VPN bypass as fallback — try VPN first, bypass only on failure
This commit is contained in:
@@ -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('Нельзя писать: сокет не подключён');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user