v3.0.0: domain-free client, bootstrap tunnel, tunnel-internal API
This commit is contained in:
1 parent
75cf59f22c
commit
4fde4a9217
6 files changed
+158
-105
No files matched your search
@@ -100,3 +100,4 @@ scraped.zip
|
|||||||
hivemind.zip
|
hivemind.zip
|
||||||
XRAY_AUDIT.md
|
XRAY_AUDIT.md
|
||||||
TECHNICAL_DEBT.md
|
TECHNICAL_DEBT.md
|
||||||
|
DOCUMENTATION-ARCHITECTURE-V2.md
|
||||||
+27
-43
@@ -7,52 +7,36 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH
|
|||||||
|
|
||||||
## [3.0.0] — 2026-08
|
## [3.0.0] — 2026-08
|
||||||
|
|
||||||
### Protocol — TCP → XHTTP migration
|
### Architecture — domain-free client
|
||||||
- **Transport changed from TCP to XHTTP** (H2 stream-up with Reality).
|
- **Zero domain strings in APK.** RKN has nothing to DNS-block. API calls go
|
||||||
XTLS-Vision (`xtls-rprx-vision`) removed — it only applies to TCP+TLS/Reality,
|
through the Reality tunnel to `10.254.254.1:5000`, redirected to Flask by
|
||||||
not XHTTP. XHTTP provides H2 multiplexing, CDN passthrough, and path-based
|
Xray's `api` freedom outbound (`redirect: 127.0.0.1:5000`).
|
||||||
hiding behind `/revolt`.
|
- **Bootstrap tunnel.** Bundled Reality config in APK → connect → fetch real
|
||||||
- **`flow` field removed from VLESS client config.** Xray falls back to standard
|
per-session VLESS URL from inside the tunnel → reconnect. Bootstrap client
|
||||||
TLS proxying inside the XHTTP tunnel — correct and intentional per official
|
permanently in `xray_config_reality.json`, restricted by Xray routing to
|
||||||
VLESS inbound docs.
|
internal API only.
|
||||||
|
- `serverDomain` removed from `AppConfig`. `fetchConfigThroughTunnel()`
|
||||||
|
replaces `fetchConfigWithPolling()`. `connect()` rewritten.
|
||||||
|
|
||||||
### Fixed — critical server bug
|
### Protocol — TCP to XHTTP
|
||||||
- **Client hot-reload was broken.** `xray.py` called `xray api adi` (add inbounds,
|
- Transport: TCP → XHTTP (H2 stream-up with Reality). No more XTLS-Vision,
|
||||||
zero arguments) after writing `config.json`. This was a no-op — Xray never
|
no `flow` field. Path-hidden behind `/revolt`.
|
||||||
learned about new VLESS clients. Every session created since the modular
|
|
||||||
rewrite was silently rejected at the VLESS layer.
|
|
||||||
Fixed: `add_client()` now writes a temp JSON and calls `xray api adu`
|
|
||||||
(add users). `remove_client()` calls `xray api rmu -tag=X email`.
|
|
||||||
Verified against `XTLS/Xray-core` HandlerService source.
|
|
||||||
|
|
||||||
### Fixed — client
|
### Fixed
|
||||||
- **Missing XHTTP path in VLESS URL.** Server requires `path: "/revolt"` in
|
- Client hot-reload was silently broken — `xray api adi` no-op. Fixed:
|
||||||
`xhttpSettings` but the VLESS URL never included a `path` parameter.
|
`adu`/`rmu` commands per Xray HandlerService spec.
|
||||||
Fixed: `hivemind_service.dart` reads `xhttp_path` from `/session/status`,
|
- Missing XHTTP `path` param in VLESS URL — now reads `xhttp_path` from
|
||||||
falls back to `AppConfig.vlessPath`.
|
`/session/status`.
|
||||||
- **`providerBundleIdentifier` mismatch.** Was `com.revoltvpn.app` — must match
|
- `providerBundleIdentifier` was `com.revoltvpn.app` — must match
|
||||||
Android `applicationId` (`com.paladinvpn.app`) for VPN service registration.
|
`com.paladinvpn.app` for VPN service registration.
|
||||||
|
- Reality keypair generated (was placeholder). Throttle `uplinkOnly`/`downlinkOnly`
|
||||||
|
corrected (seconds, not bytes/sec). Fallback rate limits removed (fingerprint).
|
||||||
|
|
||||||
### Fixed — server config
|
### Server
|
||||||
- **Reality keypair generated.** Replaced `GENERATE_ME_WITH_xray_x25519`
|
- `api` outbound + 3 routing rules in `xray_config_reality.json` (bootstrap
|
||||||
placeholders in both `config.py` and `xray_config_reality.json` with a real
|
isolation + API routing). Permanent bootstrap client. Hivemind unchanged.
|
||||||
X25519 keypair. Private key stays on server; public key sent to clients.
|
- `xhttp_path` in `/session/status` response.
|
||||||
- **Throttle policy corrected.** `uplinkOnly`/`downlinkOnly` were set to
|
- Full deployment guide: `SERVER_DEPLOYMENT.md`.
|
||||||
`187500` — that's 52 hours, not 1.5 Mbps. These fields are **seconds**
|
|
||||||
(idle timeouts), not bandwidth rate limiters (per official Policy docs).
|
|
||||||
Set to defaults (2s / 5s). Real 1.5 Mbps cap requires Linux `tc`.
|
|
||||||
- **Fallback rate limits removed.** Live Reality docs warn fallback limiting
|
|
||||||
*"is itself a fingerprint and is not recommended."* Ports are firewalled
|
|
||||||
so fallback traffic is impossible anyway. Comment added for future reference.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **`xhttp_path` in `/session/status` response.** Server now sends the XHTTP
|
|
||||||
path to clients, same pattern as `reality_pbk`/`reality_sid`/`reality_sni`.
|
|
||||||
`VLESS_XHTTP_PATH` constant in `config.py` is the single source of truth.
|
|
||||||
- **Updated deployment guide** (`SERVER_DEPLOYMENT.md`) — full step-by-step
|
|
||||||
for XHTTP+Reality, verified against live Xray docs at xtls.github.io.
|
|
||||||
- **Updated cheatsheet** (`scraped/VLESS_XRAY_CHEATSHEET.md`) — all sections
|
|
||||||
rewritten for XHTTP. Old TCP/Vision info replaced.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,26 @@ abstract final class AppConfig {
|
|||||||
/// Your server's public IP. The VPN tunnel is pinned to this.
|
/// Your server's public IP. The VPN tunnel is pinned to this.
|
||||||
static const String serverIp = '0.0.0.0'; // ◄── REPLACE
|
static const String serverIp = '0.0.0.0'; // ◄── REPLACE
|
||||||
|
|
||||||
/// API domain for session management (TLS traffic blending).
|
/// API base — routed through the tunnel to Flask via Xray "api" outbound.
|
||||||
static const String serverDomain = 'yourdomain.com'; // ◄── REPLACE
|
static const String hivemindApiBase = 'http://10.254.254.1:5000/api';
|
||||||
|
|
||||||
/// Hivemind API base URL — standard TLS to the domain.
|
// ── Bootstrap (first-connect config fetch) ────────────────────────────
|
||||||
static String get hivemindApiBase => 'https://$serverDomain/api';
|
/// Hardcoded Reality config. App connects with this, fetches the real
|
||||||
|
/// per-session VLESS URL through the tunnel, then reconnects.
|
||||||
|
static String get bootstrapVlessUrl =>
|
||||||
|
'vless://$bootstrapUuid@$serverIp:8443'
|
||||||
|
'?security=reality&type=xhttp&path=/revolt'
|
||||||
|
'&pbk=$realityPbk&sni=www.github.com&sid=$realitySid&fp=chrome'
|
||||||
|
'#ReVoltVPN';
|
||||||
|
|
||||||
|
/// Generate with `xray uuid` on the server. Must match xray_config_reality.json.
|
||||||
|
static const String bootstrapUuid = '00000000-0000-0000-0000-000000000000'; // ◄── REPLACE
|
||||||
|
|
||||||
|
/// Server's Reality public key (from `xray x25519`).
|
||||||
|
static const String realityPbk = 'REPLACE_WITH_YOUR_PUBLIC_KEY';
|
||||||
|
|
||||||
|
/// Must be in xray_config_reality.json shortIds[].
|
||||||
|
static const String realitySid = 'abc123';
|
||||||
|
|
||||||
// ── Reality (VPN tunnel) ──────────────────────────────────────────────
|
// ── Reality (VPN tunnel) ──────────────────────────────────────────────
|
||||||
/// The server picks a random SNI per session from REALITY_SNI_POOL.
|
/// The server picks a random SNI per session from REALITY_SNI_POOL.
|
||||||
@@ -26,8 +41,7 @@ abstract final class AppConfig {
|
|||||||
static const String vlessSecurity = 'reality';
|
static const String vlessSecurity = 'reality';
|
||||||
static const String vlessType = 'xhttp';
|
static const String vlessType = 'xhttp';
|
||||||
|
|
||||||
/// XHTTP path — must match the server's xhttpSettings.path.
|
/// XHTTP path — must match xray_config_reality.json xhttpSettings.path.
|
||||||
/// Server default: "/revolt" (hardcoded in xray_config_reality.json).
|
|
||||||
static const String vlessPath = '/revolt';
|
static const String vlessPath = '/revolt';
|
||||||
|
|
||||||
// ── Updates ────────────────────────────────────────────────────────────
|
// ── Updates ────────────────────────────────────────────────────────────
|
||||||
@@ -36,9 +50,9 @@ abstract final class AppConfig {
|
|||||||
static const String githubOwner = 'YOUR_USERNAME'; // ◄── REPLACE
|
static const String githubOwner = 'YOUR_USERNAME'; // ◄── REPLACE
|
||||||
static const String githubRepo = 'revoltvpn'; // ◄── REPLACE
|
static const String githubRepo = 'revoltvpn'; // ◄── REPLACE
|
||||||
|
|
||||||
/// Fallback URL if the GitHub API call fails — just opens the releases page.
|
/// Fallback URL if the GitHub API call fails.
|
||||||
static const String githubReleasesUrl = 'https://github.com/$githubOwner/$githubRepo/releases/latest';
|
static const String githubReleasesUrl = 'https://github.com/$githubOwner/$githubRepo/releases/latest';
|
||||||
|
|
||||||
// ── AdMob ──────────────────────────────────────────────────────────────
|
// ── AdMob ──────────────────────────────────────────────────────────────
|
||||||
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE
|
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE
|
||||||
}
|
}
|
||||||
@@ -18,10 +18,14 @@ class HivemindService {
|
|||||||
_expectedNonce = nonce;
|
_expectedNonce = nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> fetchConfigWithPolling({
|
/// Fetch a real VLESS session config from the tunnel-internal API.
|
||||||
|
///
|
||||||
|
/// Called AFTER the bootstrap tunnel is already up — all HTTP requests
|
||||||
|
/// go through the Reality tunnel to 10.254.254.1:5000 (redirected to
|
||||||
|
/// Flask by Xray's "api" freedom outbound).
|
||||||
|
static Future<String> fetchConfigThroughTunnel({
|
||||||
void Function(int attempt, int total)? onAttempt,
|
void Function(int attempt, int total)? onAttempt,
|
||||||
bool skipAdBypass = false,
|
bool skipAdBypass = false,
|
||||||
bool quickReconnect = false,
|
|
||||||
}) async {
|
}) async {
|
||||||
final deviceId = await CryptoService.getDeviceId();
|
final deviceId = await CryptoService.getDeviceId();
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ class HivemindService {
|
|||||||
final url = _url('/session/status?device_id=$deviceId');
|
final url = _url('/session/status?device_id=$deviceId');
|
||||||
|
|
||||||
// Dev: fake ad callback when running in debug mode.
|
// Dev: fake ad callback when running in debug mode.
|
||||||
|
// Goes through the tunnel to 10.254.254.1:5000 — same internal API.
|
||||||
if (kDebugMode && !skipAdBypass) {
|
if (kDebugMode && !skipAdBypass) {
|
||||||
try {
|
try {
|
||||||
final customData = jsonEncode({'device_id': deviceId, 'nonce': nonce});
|
final customData = jsonEncode({'device_id': deviceId, 'nonce': nonce});
|
||||||
@@ -42,7 +47,7 @@ class HivemindService {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
final maxAttempts = quickReconnect ? 1 : 5;
|
const maxAttempts = 5;
|
||||||
for (int i = 1; i <= maxAttempts; i++) {
|
for (int i = 1; i <= maxAttempts; i++) {
|
||||||
if (_currentCallId != callId) throw Exception('Cancelled');
|
if (_currentCallId != callId) throw Exception('Cancelled');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_vless/flutter_vless.dart';
|
import 'package:flutter_vless/flutter_vless.dart';
|
||||||
import 'package:revoltvpn/logic/hivemind_service.dart';
|
import 'package:revoltvpn/logic/hivemind_service.dart';
|
||||||
|
import 'package:revoltvpn/logic/app_config.dart';
|
||||||
|
|
||||||
enum VpnStatus {
|
enum VpnStatus {
|
||||||
disconnected,
|
disconnected,
|
||||||
@@ -58,7 +59,8 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
debugPrint('[VPN] VLESS init error (expected on emulator): $e');
|
debugPrint('[VPN] VLESS init error (expected on emulator): $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodic server health check
|
// Periodic server health check — now goes through the tunnel
|
||||||
|
// (AppConfig.hivemindApiBase = http://10.254.254.1:5000/api)
|
||||||
_checkHealth();
|
_checkHealth();
|
||||||
_healthTimer =
|
_healthTimer =
|
||||||
Timer.periodic(const Duration(seconds: 30), (_) => _checkHealth());
|
Timer.periodic(const Duration(seconds: 30), (_) => _checkHealth());
|
||||||
@@ -103,6 +105,18 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Connect (V2: bootstrap → poll → real tunnel) ────────────────────
|
||||||
|
//
|
||||||
|
// Flow:
|
||||||
|
// 1. Connect with hardcoded bootstrap VLESS URL (bundle in APK)
|
||||||
|
// 2. Through bootstrap tunnel, poll 10.254.254.1:5000 for real config
|
||||||
|
// 3. Disconnect bootstrap
|
||||||
|
// 4. Connect with real per-session VLESS URL
|
||||||
|
//
|
||||||
|
// Parameter quickReconnect is used by the session timer for throttle
|
||||||
|
// port-swaps — same bootstrap flow, but skip the AdMob bypass (session
|
||||||
|
// is already active).
|
||||||
|
|
||||||
Future<bool> connect(
|
Future<bool> connect(
|
||||||
{bool skipAdBypass = false, bool quickReconnect = false}) async {
|
{bool skipAdBypass = false, bool quickReconnect = false}) async {
|
||||||
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) {
|
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) {
|
||||||
@@ -116,7 +130,7 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setStatus(VpnStatus.connecting, 'Fetching config…');
|
_setStatus(VpnStatus.connecting, 'Establishing secure channel…');
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
@@ -125,11 +139,36 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String vlessUrl;
|
// ── Phase 1: Bootstrap tunnel ────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
vlessUrl = await HivemindService.fetchConfigWithPolling(
|
final bootstrapParsed = FlutterVless.parse(AppConfig.bootstrapVlessUrl);
|
||||||
|
await _vless.startVless(
|
||||||
|
remark: 'ReVoltVPN-Bootstrap',
|
||||||
|
config: bootstrapParsed.getFullConfiguration(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[VPN] Bootstrap tunnel error: $e');
|
||||||
|
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
|
||||||
|
_setStatus(VpnStatus.error, 'Connection failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cancelled) {
|
||||||
|
await _safeStopVless();
|
||||||
|
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the bootstrap tunnel stabilise before API calls
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// ── Phase 2: Fetch real config through bootstrap tunnel ──────────
|
||||||
|
_setStatus(VpnStatus.connecting, 'Fetching config…');
|
||||||
|
|
||||||
|
String realUrl;
|
||||||
|
try {
|
||||||
|
realUrl = await HivemindService.fetchConfigThroughTunnel(
|
||||||
skipAdBypass: skipAdBypass,
|
skipAdBypass: skipAdBypass,
|
||||||
quickReconnect: quickReconnect,
|
|
||||||
onAttempt: (attempt, total) {
|
onAttempt: (attempt, total) {
|
||||||
if (!quickReconnect) {
|
if (!quickReconnect) {
|
||||||
_setStatus(
|
_setStatus(
|
||||||
@@ -138,21 +177,17 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[VPN] Hivemind config error: $e');
|
debugPrint('[VPN] Config fetch error: $e');
|
||||||
|
await _safeStopVless();
|
||||||
|
|
||||||
final raw = e.toString().replaceAll('Exception: ', '');
|
final raw = e.toString().replaceAll('Exception: ', '');
|
||||||
if (raw.contains('Cancelled')) {
|
if (raw.contains('Cancelled')) {
|
||||||
// User tapped disconnect — silently abort
|
|
||||||
return false;
|
return false;
|
||||||
} else if (raw.contains('timed out') ||
|
} else if (raw.contains('timed out') ||
|
||||||
raw.contains('Session not activated')) {
|
raw.contains('Session not activated')) {
|
||||||
_errorMessage =
|
_errorMessage =
|
||||||
'The server did not respond in time.\nCheck your connection and try again.';
|
'The server did not respond in time.\nCheck your connection and try again.';
|
||||||
_setStatus(VpnStatus.error, 'Server unreachable');
|
_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 {
|
} else {
|
||||||
_errorMessage = raw;
|
_errorMessage = raw;
|
||||||
final short = raw.length > 35 ? '${raw.substring(0, 35)}…' : raw;
|
final short = raw.length > 35 ? '${raw.substring(0, 35)}…' : raw;
|
||||||
@@ -162,56 +197,59 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_cancelled) {
|
if (_cancelled) {
|
||||||
|
await _safeStopVless();
|
||||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 3: Switch to real tunnel ───────────────────────────────
|
||||||
|
_setStatus(VpnStatus.connecting, 'Securing connection…');
|
||||||
|
await _safeStopVless();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final parsed = FlutterVless.parse(vlessUrl);
|
final realParsed = FlutterVless.parse(realUrl);
|
||||||
final config = parsed.getFullConfiguration();
|
|
||||||
|
|
||||||
await _vless.startVless(
|
await _vless.startVless(
|
||||||
remark: parsed.remark.isNotEmpty ? parsed.remark : 'ReVoltVPN',
|
remark: realParsed.remark.isNotEmpty ? realParsed.remark : 'ReVoltVPN',
|
||||||
config: config,
|
config: realParsed.getFullConfiguration(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (quickReconnect) {
|
|
||||||
_setStatus(VpnStatus.connected, 'Secured');
|
|
||||||
} else {
|
|
||||||
// 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) {
|
} catch (e) {
|
||||||
debugPrint('[VPN] VLESS tunnel error: $e');
|
debugPrint('[VPN] Real tunnel error: $e');
|
||||||
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
|
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
|
||||||
_setStatus(VpnStatus.error, 'Connection failed');
|
_setStatus(VpnStatus.error, 'Connection failed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (quickReconnect) {
|
||||||
|
_setStatus(VpnStatus.connected, 'Secured');
|
||||||
|
} else {
|
||||||
|
// Give the tunnel a moment to stabilise, then verify
|
||||||
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disconnect({bool skipCleanup = false}) async {
|
Future<void> disconnect({bool skipCleanup = false}) async {
|
||||||
@@ -256,6 +294,17 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _safeStopVless() async {
|
||||||
|
try {
|
||||||
|
await _vless.stopVless().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => debugPrint('[VPN] Bootstrap stopVless() timed out.'),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[VPN] Bootstrap stop error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_healthTimer?.cancel();
|
_healthTimer?.cancel();
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 2.0.7+20
|
version: 3.0.0+21
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|||||||
Reference in new issue
Block a user