fix(android): self-heal — restart VpnService if it should be running but isn't
After an APK reinstall (or an OS low-memory kill that START_STICKY didn't recover promptly), the VpnService dies but `filter_enabled` stays true. isVpnEffectivelyOn then reports vpn:true (from the flag) → tamperLock:true → lockedIn:true → the green "protection active" card with no toggles, while in reality nothing is filtering. New native reconcileVpn(): if `filter_enabled` && !RebreakVpnService.isRunning && VpnService.prepare()==null → startVpnService(). Wired into _layout.tsx enforceProtection() (runs on launch / foreground / 15s poll), called before reading combined state. No-op on iOS/web. If the VPN consent was revoked, isVpnEffectivelyOn already clears the flag, so that case self-resolves too. Net behavior: while `filter_enabled` is true (user hasn't exited via the cooldown), the app keeps the VPN alive. Exiting still goes through the cooldown → forceDisable → filter_enabled=false → reconcile leaves it off. DiGA-compliant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4492c7b265
commit
af87893eb9
@ -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') {
|
||||
|
||||
@ -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<void> {
|
||||
if (Platform.OS !== "android") return;
|
||||
try {
|
||||
await RebreakProtection.reconcileVpn();
|
||||
} catch (e) {
|
||||
console.warn("[protection] reconcileVpn failed:", e);
|
||||
}
|
||||
},
|
||||
|
||||
syncBlocklist(opts: SyncBlocklistOpts): Promise<SyncBlocklistResult> {
|
||||
return RebreakProtection.syncBlocklist(opts);
|
||||
},
|
||||
|
||||
@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
@ -87,6 +87,11 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
|
||||
blocklistCount: number;
|
||||
tamperArmed: boolean;
|
||||
}>;
|
||||
|
||||
/** 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<RebreakProtectionModule>('RebreakProtection');
|
||||
|
||||
@ -64,6 +64,9 @@ class RebreakProtectionModuleWeb extends NativeModule<RebreakProtectionEvents> {
|
||||
tamperArmed: false,
|
||||
};
|
||||
}
|
||||
async reconcileVpn() {
|
||||
return { restarted: false };
|
||||
}
|
||||
}
|
||||
|
||||
export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user