Files
ReVoltVPN/test/security_regression_test.dart
T

231 lines
9.8 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:revoltvpn/logic/secure_socks_session.dart';
void main() {
test('secure SOCKS session owns the entire inbound surface', () async {
final session = await SecureSocksSession.create(jsonEncode({
'inbounds': [
{
'tag': 'remote-listener',
'listen': '0.0.0.0',
'port': 9999,
'protocol': 'dokodemo-door',
},
{
'tag': 'old-http',
'listen': '127.0.0.1',
'port': 8080,
'protocol': 'http',
},
],
'outbounds': [
{'tag': 'proxy', 'protocol': 'freedom'}
],
}));
final decoded = jsonDecode(session.configJson) as Map<String, dynamic>;
final inbounds = decoded['inbounds'] as List<dynamic>;
expect(inbounds, hasLength(1));
final inbound = inbounds.single as Map<String, dynamic>;
expect(inbound['tag'], SecureSocksSession.inboundTag);
expect(inbound['listen'], '127.0.0.1');
expect(inbound['protocol'], 'socks');
expect(inbound['port'], session.port);
final settings = inbound['settings'] as Map<String, dynamic>;
expect(settings['auth'], 'password');
final users = settings['users'] as List<dynamic>;
expect(users, hasLength(1));
final account = users.single as Map<String, dynamic>;
expect(account['user'], session.username);
expect(account['pass'], session.password);
});
test('vendored native source keeps full-tunnel and private IPC invariants', () {
const root =
'local_packages/flutter_vless_android-1.1.5/android/src/main';
final service = File(
'$root/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt',
).readAsStringSync();
final core = File(
'$root/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt',
).readAsStringSync();
final plugin = File(
'$root/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt',
).readAsStringSync();
final manifest = File('$root/AndroidManifest.xml').readAsStringSync();
expect(service, contains('builder.addRoute("0.0.0.0", 0)'));
expect(service, contains('builder.addRoute("::", 0)'));
expect(service, contains('Android refused to establish VPN interface'));
expect(service, contains('Per-app VPN bypass is disabled in ReVolt'));
expect(service, contains('Failed to configure VPN DNS'));
expect(service, contains('reader.forEachLine { _ -> }'));
expect(plugin, contains('ContextCompat.RECEIVER_NOT_EXPORTED'));
expect(core, contains('.setPackage(context.packageName)'));
expect(core, contains('REVOLT_VPN_SERVICE'));
expect(core, contains('NotificationManager.IMPORTANCE_LOW'));
expect(core, contains('deleteEphemeralConfigOnFailure'));
expect(core, contains('reader.forEachLine { _ -> }'));
expect(manifest, contains('android:exported="false"'));
expect(manifest, contains('android:permission="android.permission.BIND_VPN_SERVICE"'));
expect(manifest, contains('android.net.VpnService.SUPPORTS_ALWAYS_ON'));
expect(manifest, contains('android:value="false"'));
expect(manifest, isNot(contains('android.permission.CHANGE_NETWORK_STATE')));
expect(manifest, isNot(contains('android.permission.READ_EXTERNAL_STORAGE')));
});
test('native app bridge keeps runtime adoption on private heartbeat only', () {
final source = File(
'android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt',
).readAsStringSync();
final vpnSource = File('lib/logic/vpn_connection.dart').readAsStringSync();
final powerSource = File('lib/logic/power_settings.dart').readAsStringSync();
expect(source, isNot(contains('openVpnSettings')));
expect(source, isNot(contains('ACTION_VPN_SETTINGS')));
expect(source, isNot(contains('FROM_DISCONNECT_BTN')));
expect(source, isNot(contains('isVpnRuntimeAlive')));
expect(source, isNot(contains('isVpnServiceProcessAlive')));
expect(source, contains('REVOLT_VPN_SERVICE'));
expect(vpnSource, contains('if (_connectEpoch == 0) _adoptedRunningRuntime = true;'));
expect(vpnSource, isNot(contains('PowerSettings.runtimeState')));
expect(vpnSource, isNot(contains('_adoptRunningRuntime')));
expect(powerSource, isNot(contains('runtimeState')));
expect(powerSource, isNot(contains('VpnRuntimeState')));
});
test('configured AdMob bypass contract remains intact', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('await _runConfiguredBypass(deviceId, nonce);'));
expect(
source,
contains('/admob/callback?signature=test&key_id=test&custom_data='),
);
});
test('local SOCKS diagnostics cannot fall back to unauthenticated legacy mode', () {
final source = File('lib/logic/local_socks_tester.dart').readAsStringSync();
expect(source, contains('required int port'));
expect(source, contains('required String username'));
expect(source, contains('required String password'));
expect(source, contains('const <int>[0x05, 0x01, 0x02]'));
expect(source, isNot(contains('const <int>[0x05, 0x01, 0x00]')));
expect(source, isNot(contains('int port = 10807')));
expect(source, isNot(contains('paladinvpn.duckdns.org')));
expect(source, contains("String targetHost = 'example.com'"));
expect(source, contains('const totalTimeout = Duration(seconds: 8);'));
});
test('hivemind rejects missing nonce and stale session responses', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('_expectedNonce != nonce ||'));
expect(source, contains('serverNonce is! String ||'));
expect(
source,
contains(
'final session = await _fetchActiveSession(deviceId, nonce);\n'
' _throwIfCancelled(callId);',
),
);
});
test('hivemind validates and pins session fields before constructing VLESS', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('_uuidPattern'));
expect(source, contains('_maxControlResponseBytes = 256 * 1024'));
expect(source, contains('controlGet('));
expect(source, contains('_validatedPublicIp(AppConfig.serverIp'));
expect(source, contains('const port = 443;'));
expect(source, contains("_requiredString(json, 'reality_pbk', maxLength: 128)"));
expect(source, contains('_validatedHost('));
expect(
source,
contains("final pinnedHost = _validatedPublicIp(AppConfig.serverIp, 'serverIp');"),
);
expect(source, contains('advertisedHost.trim() != pinnedHost'));
expect(
source,
isNot(contains("_stringOr(json['vless_ip'], AppConfig.serverIp)")),
);
expect(source, contains('return Uri('));
expect(source, contains('queryParameters: <String, String>{'));
expect(source, isNot(contains("return 'vless://\$uuid@\$host:\$port'")));
});
test('VPN connect work cannot outlive its generation', () {
final source = File('lib/logic/vpn_connection.dart').readAsStringSync();
expect(source, contains('int _connectEpoch = 0;'));
expect(source, contains('final connectEpoch = ++_connectEpoch;'));
expect(source, contains('epoch == _connectEpoch && !_userDisconnecting'));
expect(source, contains('_connectEpoch++;\n _suppressNativeConnect = true;'));
expect(source, contains('if (_suppressNativeConnect || _userDisconnecting) return;'));
expect(source, isNot(contains('bool _cancelled = false;')));
});
test('native recovery is fail-closed without hot process restart loops', () {
const root =
'local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless';
final service =
File('$root/xray/service/XrayVPNService.kt').readAsStringSync();
final core = File('$root/xray/core/XrayCoreManager.kt').readAsStringSync();
expect(service, contains('scheduleTun2socksRecovery'));
expect(service, contains('tun2socksRecoveryAttempt'));
expect(service, contains('coerceAtMost(30_000L)'));
expect(service, contains('recoveringTun2socks = false'));
expect(service, contains('tun2socksRecoveryAttempt = 0'));
expect(service, contains('XrayCoreManager.markRuntimeConnecting(this, config)'));
expect(service, isNot(contains('Thread.sleep(350)')));
expect(service, isNot(contains('Thread.sleep(600)')));
expect(
service,
isNot(contains('attempt = 0\n Thread.sleep(3000)')),
);
expect(core, contains('context.handleXrayCoreExit(config)'));
expect(core, contains('AppConfigs.RUNTIME_READY = false'));
});
test('data disclosure only persists an explicit acknowledged action', () {
final source =
File('lib/components/data_disclosure_dialog.dart').readAsStringSync();
expect(source, contains('final accepted = await showDialog<bool>('));
expect(source, contains('if (accepted != true) return;'));
expect(source, contains('final saved = await prefs.setBool(_key, true);'));
expect(source, contains('if (!saved)'));
expect(source, contains('Navigator.of(context).pop(true)'));
expect(source, isNot(contains('prefs.setBool(_key, true);\n Navigator')));
});
test('unsupported server picker is not wired into app state', () {
final mainSource = File('lib/main.dart').readAsStringSync();
final statusBar = File('lib/components/status_bar.dart').readAsStringSync();
expect(mainSource, isNot(contains('ServerList')));
expect(statusBar, isNot(contains('SelectionScreen')));
});
test('session timer rejects stale session work by generation', () {
final source = File('lib/logic/session_timer.dart').readAsStringSync();
expect(source, contains('_sessionEpoch'));
expect(source, contains('Stopwatch'));
expect(source, contains("_doDisconnect('VPN entered error state')"));
expect(source, isNot(contains('_stopForVpnFailure')));
});
}