Files
sevenhill f74057ce9b
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Rebrand application as Qlyra
2026-08-30 13:16:17 +03:00

149 lines
4.3 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/logger.dart';
class VpnBypassResult {
final bool enabled;
final bool tunDetected;
final bool bound;
final String? boundInterface;
final String? transport;
final String? reason;
const VpnBypassResult({
required this.enabled,
this.tunDetected = false,
this.bound = false,
this.boundInterface,
this.transport,
this.reason,
});
@override
String toString() =>
'VpnBypassResult(enabled: $enabled, tun: $tunDetected, bound: $bound, '
'iface: $boundInterface, transport: $transport, reason: $reason)';
}
/// При активном VPN (tun-интерфейс) привязывает процесс к не-VPN сети
/// (wlan*/rmnet*). Только Android, по умолчанию выключено.
class VpnBypassService {
VpnBypassService._();
static final VpnBypassService instance = VpnBypassService._();
static const String prefKey = 'dev_vpn_bypass';
static const MethodChannel _channel = MethodChannel(
'ru.qlyra.app/vpn_bypass',
);
bool _bound = false;
final _eventController = StreamController<VpnBypassResult>.broadcast();
/// Эмитит результат каждой попытки обхода (для уведомления в UI).
Stream<VpnBypassResult> get events => _eventController.stream;
bool get _supported => Platform.isAndroid;
Future<bool> isEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(prefKey) ?? false;
}
Future<void> setEnabled(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(prefKey, value);
}
/// true — обход включён, платформа поддерживается и активен VPN.
Future<bool> shouldArm() async {
if (!_supported) return false;
if (!await isEnabled()) return false;
return _isVpnActive();
}
/// Привязывает процесс к non-VPN сети (wlan*/rmnet*).
Future<VpnBypassResult> bind() async {
VpnBypassResult result;
try {
final res = await _channel.invokeMapMethod<String, dynamic>(
'bindToNonVpnNetwork',
);
final bound = res?['bound'] == true;
_bound = bound;
result = VpnBypassResult(
enabled: true,
tunDetected: true,
bound: bound,
boundInterface: res?['interface'] as String?,
transport: res?['transport'] as String?,
reason: res?['reason'] as String?,
);
} on PlatformException catch (e) {
logger.e('VPN bypass: ошибка платформы: ${e.message}');
result = VpnBypassResult(
enabled: true,
tunDetected: true,
reason: e.code,
);
} on MissingPluginException {
result = const VpnBypassResult(
enabled: true,
tunDetected: true,
reason: 'no_plugin',
);
}
if (result.bound) {
logger.i(
'VPN bypass: привязано к ${result.boundInterface} '
'(${result.transport})',
);
} else {
logger.w('VPN bypass: обойти не удалось (${result.reason})');
}
_eventController.add(result);
return result;
}
Future<bool> _isVpnActive() async {
try {
final res = await _channel.invokeMapMethod<String, dynamic>(
'detectInterfaces',
);
if (res != null) {
if (res['hasTun'] == true || res['hasVpn'] == true) return true;
if (res.containsKey('hasTun')) return false;
}
} catch (_) {}
try {
final ifaces = await NetworkInterface.list(
includeLoopback: false,
includeLinkLocal: true,
);
return ifaces.any((i) {
final n = i.name.toLowerCase();
return n.startsWith('tun') ||
n.startsWith('ppp') ||
n.startsWith('ipsec') ||
n.startsWith('wg');
});
} catch (_) {
return false;
}
}
/// Возвращает маршрутизацию процесса к системной (через VPN, если он есть).
Future<void> restoreDefault() async {
if (!_bound) return;
try {
await _channel.invokeMethod('unbindNetwork');
} catch (_) {}
_bound = false;
}
}