diff --git a/apps/rebreak-native/app/(app)/_layout.tsx b/apps/rebreak-native/app/(app)/_layout.tsx index 3c75e91..337fc95 100644 --- a/apps/rebreak-native/app/(app)/_layout.tsx +++ b/apps/rebreak-native/app/(app)/_layout.tsx @@ -81,6 +81,10 @@ export default function AppLayout() { async function enforceProtection() { if (cancelled || rearmInFlightRef.current) return; try { + // Self-Heal: wenn der Schutz an sein soll der VpnService aber tot ist + // (Reinstall / OS-Kill) → neu starten, bevor wir den State lesen. + await protection.reconcileVpn(); + if (cancelled) return; const state = await protection.getCombinedState(); if (cancelled) return; if (state.phase !== 'recoveringFromBypass') { diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts index 54de2c7..7cd7504 100644 --- a/apps/rebreak-native/lib/protection.ts +++ b/apps/rebreak-native/lib/protection.ts @@ -150,6 +150,18 @@ export const protection = { return RebreakProtection.getDeviceState(); }, + /** Android: VpnService neu starten falls er laufen sollte (`filter_enabled`) + * aber tot ist (Reinstall / OS-Kill). Bei App-Start/Foreground aufrufen, + * damit der State nicht „an aber tot" bleibt. No-op auf iOS/Web. */ + async reconcileVpn(): Promise { + if (Platform.OS !== "android") return; + try { + await RebreakProtection.reconcileVpn(); + } catch (e) { + console.warn("[protection] reconcileVpn failed:", e); + } + }, + syncBlocklist(opts: SyncBlocklistOpts): Promise { return RebreakProtection.syncBlocklist(opts); }, diff --git a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt index 98133ac..6808d15 100644 --- a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt +++ b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt @@ -282,6 +282,37 @@ class RebreakProtectionModule : Module() { "tamperArmed" to armed, ) } + + /** + * Reconcile: Wenn `filter_enabled == true` (User WILL Schutz) der VpnService + * aber nicht läuft (z.B. nach App-Reinstall oder Low-Memory-Kill den + * START_STICKY nicht zeitnah recovert hat) → neu starten. Sonst bleibt der + * State „an aber tot": UI zeigt grün/„aktiv", real ist nichts geschützt. + * Wird vom JS bei App-Start / Foreground aufgerufen. + * + * Wenn die VPN-Permission entzogen wurde, kann hier nicht ohne UI neu + * established werden — `isVpnEffectivelyOn` clear't in dem Fall bereits den + * Flag, also self-resolved das. + */ + AsyncFunction("reconcileVpn") { + val ctx = requireContext() + val shouldBeOn = isEnabledFlag(ctx) + val running = RebreakVpnService.isRunning + if (shouldBeOn && !running) { + val consentNeeded = try { VpnService.prepare(ctx) } catch (_: Exception) { null } + if (consentNeeded == null) { + startVpnService() + Log.i(TAG, "reconcileVpn: VpnService neu gestartet (flag=true, war aber nicht running)") + sendLayerChange() + mapOf("restarted" to true) + } else { + Log.w(TAG, "reconcileVpn: flag=true aber keine VPN-Permission — re-activate via UI nötig") + mapOf("restarted" to false, "needsConsent" to true) + } + } else { + mapOf("restarted" to false) + } + } } // ─── Helpers ──────────────────────────────────────────────────────────── diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts index 4958390..8fdc62c 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts @@ -87,6 +87,11 @@ declare class RebreakProtectionModule extends NativeModule; + + /** Android: Wenn der Schutz an sein soll (`filter_enabled`) der VpnService + * aber nicht läuft (Reinstall / Low-Mem-Kill) → neu starten. Bei App-Start / + * Foreground aufrufen, damit der State nicht „an aber tot" bleibt. */ + reconcileVpn(): Promise<{ restarted: boolean; needsConsent?: boolean }>; } export default requireNativeModule('RebreakProtection'); diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts index ceca35c..8869f12 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts @@ -64,6 +64,9 @@ class RebreakProtectionModuleWeb extends NativeModule { tamperArmed: false, }; } + async reconcileVpn() { + return { restarted: false }; + } } export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection');