From 9d67679fa8130f04ebad16a903fc6a3f8e3015a3 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sun, 6 Sep 2026 11:41:15 +0300 Subject: [PATCH] fix(app): do not throw from ConnectionSettings.initialize The migration write is best-effort again. `_mode` is already the safe TUN default in memory by the time it runs, so a failed write leaves nothing insecure -- it only means the migration is retried on the next launch. Throwing there was a startup brick rather than a fail-closed guard. `initialize()` is awaited from `main()` at lib/main.dart:19, before `runApp()`, with no try/catch and no `runZonedGuarded`; `FlutterError.onError` and `PlatformDispatcher.onError` are installed later and would not catch it either. The branch also runs on every fresh install, since `storedMode` is null and `null != 'tun'`. A `setString` returning false would therefore have meant a black screen with no error UI, repeating every launch, in exchange for no security benefit. Also restores two comments this PR removed from code it does not otherwise touch -- the `_reconnect` note on why the repair path shows no rewarded ad, and the explanation of how an orphaned proxy outlives the app process -- and drops a stray 'ass' from the legacy-values comment. The `setMode` half of the PR is unchanged: persisting before mutating memory and surfacing failures to the UI is the right behaviour. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019HT2x2JvJNg7XNsJubfHFf --- lib/logic/connection_settings.dart | 12 +++++++++--- .../settings/in_settings/connection_mode.dart | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/logic/connection_settings.dart b/lib/logic/connection_settings.dart index e7a6e36..658e9c2 100644 --- a/lib/logic/connection_settings.dart +++ b/lib/logic/connection_settings.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum ConnectionMode { tun, proxy } @@ -19,16 +20,21 @@ abstract final class ConnectionSettings { final storedMode = prefs.getString(_modeKey); // Only the explicit current proxy value restores SOCKS5. Unknown and - // obsolete values, including the former 'auto'/'ass' labels, fail closed - // to full-tunnel TUN instead of silently selecting a weaker route. + // obsolete values, including the pre-V3 'auto' label, fail closed to + // full-tunnel TUN instead of silently selecting a weaker route. _mode = storedMode == ConnectionMode.proxy.name ? ConnectionMode.proxy : ConnectionMode.tun; if (storedMode != _mode.name) { + // Best-effort. _mode is already the safe TUN default in memory, so a + // failed write leaves nothing insecure — it only means the migration + // is retried next launch. initialize() runs from main() before + // runApp(), with no zone guard, so throwing here would black-screen + // the app on the fresh-install path for no security gain. final migrated = await prefs.setString(_modeKey, _mode.name); if (!migrated) { - throw StateError('Could not persist migrated connection mode.'); + debugPrint('[ConnectionSettings] Could not persist migrated mode.'); } } diff --git a/lib/screens/settings/in_settings/connection_mode.dart b/lib/screens/settings/in_settings/connection_mode.dart index dde1703..46519d7 100644 --- a/lib/screens/settings/in_settings/connection_mode.dart +++ b/lib/screens/settings/in_settings/connection_mode.dart @@ -67,6 +67,10 @@ class _ConnectionModeTileState extends State { widget.onChanged?.call(next); } + /// Recovers from an orphaned proxy: tear the runtime down and start a fresh + /// session so a readable set of credentials exists again. No rewarded ad — + /// the user is repairing a connection they already paid for, not opening a + /// new one. Future _reconnect() async { if (_reconnecting) return; final vpn = context.read(); @@ -170,6 +174,11 @@ class _ConnectionModeTileState extends State { builder: (context, vpn, _) { final session = vpn.activeSocksSession; if (session == null) { + // The proxy can be running while this session is unknown: + // Android killed the app process, the foreground service kept + // the listener alive, and its credentials existed only in the + // memory that died with it. Saying "Connect" there would be a + // lie — the user is already connected. final orphaned = vpn.status == VpnStatus.connected && vpn.adoptedRunningRuntime; if (orphaned) {