Files
ReVoltVPN/lib/logic/vpn_connection.dart
T

227 lines
7.7 KiB
Dart

// =============================================================================
// vpn_connection.dart — VLESS VPN Tunnel Manager
// =============================================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_vless/flutter_vless.dart';
import 'package:paladinvpn/logic/hivemind_service.dart';
// ── VPN States ────────────────────────────────────────────────────────────────
enum VpnStatus {
disconnected,
connecting,
connected,
disconnecting,
error,
}
class VpnConnection extends ChangeNotifier {
VpnStatus _status = VpnStatus.disconnected;
VpnStatus get status => _status;
String _statusMessage = 'Tap to connect';
String get statusMessage => _statusMessage;
String? _errorMessage;
String? get errorMessage => _errorMessage;
// True when the app was killed and reopened while the VPN was running.
bool _isStartupRestoration = false;
bool get isStartupRestoration => _isStartupRestoration;
late final FlutterVless _vless;
bool _initialized = false;
VpnConnection() {
_init();
}
Future<void> _init() async {
if (kIsWeb) return;
_vless = FlutterVless(
onStatusChanged: (status) {
debugPrint('[VPN] Status: state=${status.state} '
'connection=${status.connectionState.name}');
_mapStatus(status);
},
);
try {
await _vless.initializeVless(
providerBundleIdentifier: 'com.paladinvpn.app',
);
_initialized = true;
} catch (e) {
debugPrint('[VPN] VLESS init error (expected on emulator): $e');
}
// Check if VPN was already running (app killed and reopened)
try {
final coreVersion = await _vless.getCoreVersion();
debugPrint('[VPN] Xray core version: $coreVersion');
final delay = await _vless.getConnectedServerDelay();
if (delay > 0) {
_isStartupRestoration = true;
_setStatus(VpnStatus.connected, 'Secured');
Future.delayed(const Duration(seconds: 5), () {
_isStartupRestoration = false;
});
}
} catch (_) {}
}
void _mapStatus(VlessStatus status) {
switch (status.connectionState) {
case VlessConnectionState.connected:
_setStatus(VpnStatus.connected, 'Secured');
case VlessConnectionState.disconnected:
_setStatus(VpnStatus.disconnected, 'Tap to connect');
case VlessConnectionState.connecting:
_setStatus(VpnStatus.connecting, 'Establishing tunnel…');
case VlessConnectionState.disconnecting:
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
case VlessConnectionState.unknown:
// Native backend emitted an unrecognized state — treat as error
// only if we aren't already in a terminal state.
if (_status != VpnStatus.connected &&
_status != VpnStatus.disconnected) {
_setStatus(VpnStatus.error, 'Connection failed');
}
}
}
// ── Connect ───────────────────────────────────────────────────────────────
Future<bool> connect() async {
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) {
return false;
}
if (!kIsWeb && !_initialized) {
_errorMessage = 'VPN service unavailable.';
_setStatus(VpnStatus.error, 'Service unavailable');
return false;
}
_setStatus(VpnStatus.connecting, 'Fetching config…');
_errorMessage = null;
if (kIsWeb) {
await Future.delayed(const Duration(seconds: 1));
_setStatus(VpnStatus.connected, 'Secured (dev mode)');
return true;
}
// ── Step 1: Fetch VLESS URL from Hivemind ──────────────────────────
String vlessUrl;
try {
vlessUrl = await HivemindService.fetchConfigWithPolling(
onAttempt: (attempt, total) {
_setStatus(VpnStatus.connecting, 'Contacting server ($attempt/$total)…');
},
);
} catch (e) {
debugPrint('[VPN] Hivemind config error: $e');
final raw = e.toString().replaceAll('Exception: ', '');
if (raw.contains('AWG protocol') || raw.contains('old AWG')) {
_errorMessage = 'Server update required.\nThe VPN server needs to be upgraded to VLESS.';
_setStatus(VpnStatus.error, 'Server outdated');
} else if (raw.contains('timed out') || raw.contains('Session not activated')) {
_errorMessage = 'The server did not respond in time.\nCheck your connection and try again.';
_setStatus(VpnStatus.error, 'Server unreachable');
} else if (raw.contains('SocketException') || raw.contains('Failed host lookup')) {
_errorMessage = 'Cannot reach the server.\nMake sure you have internet access.';
_setStatus(VpnStatus.error, 'No internet');
} else {
_errorMessage = raw;
final short = raw.length > 35 ? '${raw.substring(0, 35)}…' : raw;
_setStatus(VpnStatus.error, short);
}
return false;
}
// ── Step 2: Parse and start VLESS tunnel ───────────────────────────
try {
final parsed = FlutterVless.parse(vlessUrl);
final config = parsed.getFullConfiguration();
await _vless.startVless(
remark: parsed.remark.isNotEmpty ? parsed.remark : 'PaladinVPN',
config: config,
);
// Give the tunnel a moment to stabilise
_setStatus(VpnStatus.connecting, 'Verifying session…');
await Future.delayed(const Duration(seconds: 1));
bool sessionOk = false;
for (int attempt = 0; attempt < 2; attempt++) {
sessionOk = await HivemindService.verifySession();
if (sessionOk) break;
if (attempt == 0) {
debugPrint('[VPN] verifySession attempt 1 failed, retrying…');
await Future.delayed(const Duration(seconds: 2));
}
}
if (!sessionOk) {
debugPrint('[VPN] Tunnel started but no server session — tearing down.');
try {
await _vless.stopVless();
} catch (_) {}
_errorMessage = 'Session not confirmed by server.\nPlease try again.';
_setStatus(VpnStatus.error, 'Session rejected');
return false;
}
_setStatus(VpnStatus.connected, 'Secured');
return true;
} catch (e) {
debugPrint('[VPN] VLESS tunnel error: $e');
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
_setStatus(VpnStatus.error, 'Connection failed');
return false;
}
}
// ── Disconnect ────────────────────────────────────────────────────────────
Future<void> disconnect() async {
if (_status == VpnStatus.disconnected || _status == VpnStatus.disconnecting) {
return;
}
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 500));
_setStatus(VpnStatus.disconnected, 'Tap to connect');
return;
}
try {
await _vless.stopVless().timeout(
const Duration(seconds: 5),
onTimeout: () => debugPrint('[VPN] stopVless() timed out.'),
);
} catch (e) {
debugPrint('[VPN] VLESS stop error: $e');
}
HivemindService.disconnectAndCleanup();
_setStatus(VpnStatus.disconnected, 'Tap to connect');
}
void _setStatus(VpnStatus s, String msg) {
_status = s;
_statusMessage = msg;
notifyListeners();
}
@override
void dispose() {
super.dispose();
}
}