v2.0.7: security hardening, config sync, cleanup
- 6 new security audits (technical_debt_8 through 12) - SNI rotation pool per session (5 sites) - vless_ip pinned to hardcoded server IP - TLS cert validation restored (domain-based) - Swarm monitoring removed from public domain - Xray routing blocks abuse ports (SMTP, NetBIOS, SSDP) - flow: xtls-rprx-vision in add_vless_client - TimerBox shows Connecting/Syncing spinner - Outbound port blocking in Xray config - Cleaned up comments, synced config templates
This commit is contained in:
1 parent
f01a610182
commit
f8abda9323
7 files changed
+109
-86
No files matched your search
@@ -108,7 +108,7 @@ class SidebarDrawer extends StatelessWidget {
|
||||
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text('ReVoltVPN · v1.0.7', textAlign: TextAlign.center,
|
||||
child: Text('ReVoltVPN · v2.0.7', textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.slate, fontSize: 11, letterSpacing: 1),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,34 +1,49 @@
|
||||
// =============================================================================
|
||||
// app_config.example.dart — TEMPLATE FOR SERVER SETTINGS
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Copy this file to `app_config.dart` and fill in your real values.
|
||||
// ⚠️ Do not commit your real `app_config.dart` to GitHub!
|
||||
// =============================================================================
|
||||
|
||||
abstract final class AppConfig {
|
||||
AppConfig._();
|
||||
AppConfig._(); // static members only
|
||||
|
||||
// ── Server (API) ──────────────────────────────────────────────────────
|
||||
static const String serverDomain = 'yourdomain.com';
|
||||
// ── Server ───────────────────────────────────────────────────────────
|
||||
/// Your server's public IP. The VPN tunnel is pinned to this.
|
||||
static const String serverIp = '0.0.0.0'; // ◄── REPLACE
|
||||
|
||||
/// API domain for session management (TLS traffic blending).
|
||||
static const String serverDomain = 'yourdomain.com'; // ◄── REPLACE
|
||||
|
||||
/// Hivemind API base URL — standard TLS to the domain.
|
||||
static String get hivemindApiBase => 'https://$serverDomain/api';
|
||||
|
||||
// ── Reality (VPN tunnel) ──────────────────────────────────────────────
|
||||
// These are fallbacks — normally overridden by /session/status response.
|
||||
static const String realitySni = 'www.microsoft.com';
|
||||
static const String realityFp = 'chrome';
|
||||
/// TLS camouflage — which site the Reality tunnel impersonates.
|
||||
/// Server overrides this per session from REALITY_SNI_POOL.
|
||||
/// www.microsoft.com — classic, never blocked
|
||||
/// cloudflare.com — CDN, clean TLS
|
||||
/// yandex.ru — Russian, absolutely never blocked in Russia
|
||||
static const String realitySni = 'www.microsoft.com'; // ◄── REPLACE
|
||||
|
||||
// These are authoritative — never sent by the server.
|
||||
/// TLS fingerprint: chrome | firefox | safari | random
|
||||
static const String realityFp = 'chrome';
|
||||
|
||||
// These are normally overridden by /session/status response fields.
|
||||
// The constants below serve as fallbacks if the server doesn't provide them.
|
||||
static const String vlessSecurity = 'reality';
|
||||
static const String vlessType = 'tcp';
|
||||
static const String vlessFlow = 'xtls-rprx-vision';
|
||||
|
||||
// ── Updates ────────────────────────────────────────────────────────────
|
||||
/// Used by the updater.
|
||||
static const String applicationId = 'com.paladinvpn.app';
|
||||
static const String githubOwner = 'YOUR_USERNAME';
|
||||
static const String githubRepo = 'revoltvpn';
|
||||
static const String applicationId = 'com.paladinvpn.app'; // ◄── REPLACE
|
||||
static const String githubOwner = 'YOUR_USERNAME'; // ◄── REPLACE
|
||||
static const String githubRepo = 'revoltvpn'; // ◄── REPLACE
|
||||
|
||||
/// Fallback URL if the GitHub API call fails.
|
||||
/// Fallback URL if the GitHub API call fails — just opens the releases page.
|
||||
static const String githubReleasesUrl = 'https://github.com/$githubOwner/$githubRepo/releases/latest';
|
||||
|
||||
// ── AdMob ──────────────────────────────────────────────────────────────
|
||||
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE
|
||||
}
|
||||
}
|
||||
@@ -34,14 +34,14 @@ class HivemindService {
|
||||
_expectedNonce = nonce;
|
||||
debugPrint('[HivemindService] Call #$callId — nonce: $nonce');
|
||||
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||
final url = _url('/session/status?device_id=$deviceId');
|
||||
|
||||
// ── AD BYPASS (debug only) ────────────────────────────────────────
|
||||
if (kDebugMode && !skipAdBypass) {
|
||||
try {
|
||||
final customData = jsonEncode({'device_id': deviceId, 'nonce': nonce});
|
||||
final fakeUrl = Uri.parse(
|
||||
'${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
|
||||
final fakeUrl = _url(
|
||||
'/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
|
||||
await http.get(fakeUrl).timeout(const Duration(seconds: 8));
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -62,8 +62,13 @@ class HivemindService {
|
||||
} else if (data['active'] == true && data['vless_uuid'] != null) {
|
||||
final vlessUuid = data['vless_uuid'];
|
||||
|
||||
// Reality tunnel fields — server is source of truth, AppConfig is fallback.
|
||||
final vlessIp = data['vless_ip'] ?? AppConfig.serverDomain;
|
||||
// vless_ip is pinned — even if the API is MITM'd, the tunnel
|
||||
// never goes anywhere except the real server IP.
|
||||
final vlessIp = data['vless_ip'] ?? AppConfig.serverIp;
|
||||
if (vlessIp != AppConfig.serverIp) {
|
||||
throw Exception('API returned unexpected tunnel IP — possible MITM');
|
||||
}
|
||||
|
||||
final vlessPort = data['vless_port'] ?? 8443;
|
||||
final pbk = data['reality_pbk'] ?? '';
|
||||
final sid = data['reality_sid'] ?? '';
|
||||
@@ -96,7 +101,7 @@ class HivemindService {
|
||||
|
||||
static Future<bool> checkHealth() async {
|
||||
try {
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/health');
|
||||
final url = _url('/health');
|
||||
final response = await http.get(url).timeout(const Duration(seconds: 3));
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
@@ -107,7 +112,7 @@ class HivemindService {
|
||||
static Future<bool> verifySession() async {
|
||||
try {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||
final url = _url('/session/status?device_id=$deviceId');
|
||||
final response = await http.get(url).timeout(const Duration(seconds: 5));
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
@@ -119,7 +124,7 @@ class HivemindService {
|
||||
|
||||
static Future<void> disconnectAndCleanup() async {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop');
|
||||
final url = _url('/session/stop');
|
||||
try {
|
||||
await http.post(
|
||||
url,
|
||||
@@ -128,4 +133,9 @@ class HivemindService {
|
||||
).timeout(const Duration(seconds: 3));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Builds a URI pointing directly at the server IP — no domain involved.
|
||||
static Uri _url(String path) {
|
||||
return Uri.parse('${AppConfig.hivemindApiBase}$path');
|
||||
}
|
||||
}
|
||||
+11
-27
@@ -168,39 +168,23 @@ class Updater {
|
||||
}
|
||||
|
||||
static Future<InstallSource> _detectInstallerSource() async {
|
||||
// ╔══════════════════════════════════════════════════════════════════════╗
|
||||
// ║ TODO — BEFORE PLAY STORE RELEASE ║
|
||||
// ║ ║
|
||||
// ║ Wire this method channel in MainActivity.kt so Play Store users ║
|
||||
// ║ get redirected to the Play Store listing instead of GitHub. ║
|
||||
// ║ ║
|
||||
// ║ Kotlin (android/app/src/main/kotlin/.../MainActivity.kt): ║
|
||||
// ║ ║
|
||||
// ║ override fun configureFlutterEngine(flutterEngine: FlutterEngine) { ║
|
||||
// ║ super.configureFlutterEngine(flutterEngine) ║
|
||||
// ║ MethodChannel( ║
|
||||
// ║ flutterEngine.dartExecutor.binaryMessenger, ║
|
||||
// ║ "com.revoltvpn.app/installer" ║
|
||||
// ║ ).setMethodCallHandler { call, result -> ║
|
||||
// ║ if (call.method == "getInstallerPackage") { ║
|
||||
// ║ val installer = packageManager ║
|
||||
// ║ .getInstallerPackageName(packageName) ║
|
||||
// ║ result.success(installer) ║
|
||||
// ║ } else { result.notImplemented() } ║
|
||||
// ║ } ║
|
||||
// ║ } ║
|
||||
// ║ ║
|
||||
// ║ Then uncomment the 7 lines below this block. ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════╝
|
||||
// ── Uncomment after wiring MainActivity.kt ──────────────────────────
|
||||
// Install source detection is stubbed until the native method channel
|
||||
// is wired in MainActivity.kt. Without it, all users default to the
|
||||
// GitHub update path — which works, but Play Store users should ideally
|
||||
// be sent to the Play Store listing instead.
|
||||
//
|
||||
// To wire: add a MethodChannel named "com.revoltvpn.app/installer" in
|
||||
// MainActivity.kt that calls PackageManager.getInstallerPackageName().
|
||||
// If it returns "com.android.vending", return InstallSource.playStore.
|
||||
// Then uncomment the 7 lines below and delete the fallback return.
|
||||
//
|
||||
// const channel = MethodChannel('com.revoltvpn.app/installer');
|
||||
// final installer = await channel.invokeMethod<String>('getInstallerPackage');
|
||||
// if (installer == 'com.android.vending') {
|
||||
// return InstallSource.playStore;
|
||||
// }
|
||||
// return InstallSource.sideload;
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
return InstallSource.sideload; // TODO: remove this line after uncommenting above
|
||||
return InstallSource.sideload; // Stubbed — see above.
|
||||
}
|
||||
|
||||
/// Compares two semver strings. Positive = a > b, negative = a < b, 0 = equal.
|
||||
|
||||
Reference in new issue
Block a user