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() {
|
async function enforceProtection() {
|
||||||
if (cancelled || rearmInFlightRef.current) return;
|
if (cancelled || rearmInFlightRef.current) return;
|
||||||
try {
|
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();
|
const state = await protection.getCombinedState();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (state.phase !== 'recoveringFromBypass') {
|
if (state.phase !== 'recoveringFromBypass') {
|
||||||
|
|||||||
@ -150,6 +150,18 @@ export const protection = {
|
|||||||
return RebreakProtection.getDeviceState();
|
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> {
|
syncBlocklist(opts: SyncBlocklistOpts): Promise<SyncBlocklistResult> {
|
||||||
return RebreakProtection.syncBlocklist(opts);
|
return RebreakProtection.syncBlocklist(opts);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -282,6 +282,37 @@ class RebreakProtectionModule : Module() {
|
|||||||
"tamperArmed" to armed,
|
"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 ────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -87,6 +87,11 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
|
|||||||
blocklistCount: number;
|
blocklistCount: number;
|
||||||
tamperArmed: boolean;
|
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');
|
export default requireNativeModule<RebreakProtectionModule>('RebreakProtection');
|
||||||
|
|||||||
@ -64,6 +64,9 @@ class RebreakProtectionModuleWeb extends NativeModule<RebreakProtectionEvents> {
|
|||||||
tamperArmed: false,
|
tamperArmed: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async reconcileVpn() {
|
||||||
|
return { restarted: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection');
|
export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user