Neuer iOS-Layer-1-Filter: ein NEPacketTunnelProvider-DNS-Sinkhole — MDM-frei, ab iOS 16, Parität zum Android-VPN-DNS-Filter. Ersetzt den Apple-seitig blockierten NEURLFilter als Default. NEURLFilter-/PIR-Code bleibt inaktiv als iOS-26-Upgrade-Pfad erhalten (User-Entscheidung). Neues Extension-Target RebreakPacketTunnelExtension/: - PacketTunnelProvider.swift — TUN-Setup (virtuelle DNS-IP 10.0.0.1, nur diese Route ins TUN), Read-Loop, NXDOMAIN-Sinkhole, Upstream-Forward via NWConnection zu 1.1.1.1, Blocklist-Reload via Darwin-Notification. - DnsFilter.swift / HashList.swift / DomainHasher.swift — Swift-Ports der Android-DNS-Filter-Logik. blocklist.bin-Format (sortierte big-endian UInt64, SHA-256-Prefix) 1:1 beibehalten, mmap statt Heap-Load. RebreakProtectionModule.swift: - activateUrlFilter startet jetzt den Packet-Tunnel via NETunnelProviderManager (Default-Layer-1, On-Demand-Auto-Reconnect aktiv). - NEURLFilter-Code in activateNeUrlFilter ausgelagert (inaktiv, behalten). - getDeviceState/disable lesen bzw. stoppen den Tunnel-Status. with-rebreak-protection-ios.js: zweites app_extension-Target, klassischer Embed-Pfad (dstSubfolderSpec 13), packet-tunnel-provider-Entitlement + App-Group. app.config.ts: zweites appExtensions-Target. NICHT auf echtem Gerät verifiziert — NE-Packet-Tunnel laufen nicht im Simulator. Ungetestete Annahmen im Code mit "UNGETESTETE ANNAHME" markiert. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
180 lines
7.5 KiB
TypeScript
180 lines
7.5 KiB
TypeScript
import { NativeModule, requireNativeModule } from 'expo';
|
|
|
|
import type {
|
|
ActivateResult,
|
|
DeviceLayers,
|
|
DisableResult,
|
|
HealthProbeOpts,
|
|
HealthProbeResult,
|
|
RebreakProtectionEvents,
|
|
SyncBlocklistOpts,
|
|
SyncBlocklistResult,
|
|
SyncWebContentDomainsOpts,
|
|
SyncWebContentDomainsResult,
|
|
SystemSettingsTarget,
|
|
WebContentFilterResult,
|
|
} from './RebreakProtection.types';
|
|
|
|
declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEvents> {
|
|
/**
|
|
* iOS: aktiviert Layer 1 = den Packet-Tunnel-DNS-Filter
|
|
* (`NEPacketTunnelProvider`). Startet/konfiguriert den Tunnel via
|
|
* `NETunnelProviderManager` — beim ersten Aufruf erscheint der iOS-VPN-
|
|
* System-Permission-Dialog. MDM-frei, ab iOS 16. Das ist der neue
|
|
* Default-Layer-1 (ersetzt NEURLFilter, der Apple-seitig blockiert ist).
|
|
*
|
|
* `opts` wird auf iOS NICHT mehr ausgewertet (der Packet-Tunnel braucht
|
|
* keine PIR-Config) — bleibt für API-Kompatibilität in der Signatur.
|
|
*/
|
|
activateUrlFilter(opts: {
|
|
pirServerURL: string;
|
|
pirAuthToken: string;
|
|
}): Promise<{ enabled: boolean; error?: string }>;
|
|
|
|
/**
|
|
* iOS: aktiviert den NEURLFilter (iOS 26) via `NEURLFilterManager` mit dem
|
|
* PIR-Server. INAKTIV — NICHT der Default-Filter. Behalten als optionaler
|
|
* iOS-26-Upgrade-Pfad, falls Apple den NEURLFilter-DTS-Bug fixt. Der
|
|
* Default-Layer-1 ist der Packet-Tunnel (`activateUrlFilter`).
|
|
* Braucht `pirServerURL` + `pirAuthToken`. iOS 26+.
|
|
*/
|
|
activateNeUrlFilter(opts: {
|
|
pirServerURL: string;
|
|
pirAuthToken: string;
|
|
}): Promise<{ enabled: boolean; error?: string }>;
|
|
|
|
/**
|
|
* iOS: nach "Nicht erlauben" beim NEFilter-Permission-Dialog hat iOS den
|
|
* Denied-State gecached und zeigt beim erneuten activateUrlFilter() den
|
|
* Dialog nicht mehr (code 5 silent). resetUrlFilter() macht ein
|
|
* removeFromPreferences vor dem saveToPreferences — iOS behandelt das als
|
|
* brandneuen Permission-Request → frischer System-Dialog.
|
|
*
|
|
* Nicht aufrufen wenn der User schon einmal "Erlauben" getippt hat — dann
|
|
* würde ein unnötiger Dialog kommen. Nur als Workaround bei code 5 nutzen.
|
|
*/
|
|
resetUrlFilter(): Promise<{ enabled: boolean; error?: string }>;
|
|
|
|
/**
|
|
* iOS: aktiviert NUR Family Controls (Auth + denyAppRemoval = der Lock).
|
|
* Triggert iOS-Dialog "Bildschirmzeit verwalten".
|
|
* Sobald aktiv, kann der User den Schutz nur über Cooldown deaktivieren.
|
|
*/
|
|
activateFamilyControls(): Promise<{ enabled: boolean; error?: string }>;
|
|
|
|
/**
|
|
* Aktiviert ALLE Schutz-Layer in einem Call (legacy, beide Dialoge nacheinander).
|
|
* Bevorzugt activateUrlFilter() + activateFamilyControls() einzeln aufrufen.
|
|
*/
|
|
activate(): Promise<ActivateResult>;
|
|
|
|
/**
|
|
* Schaltet ALLE Schutz-Layer ab. NUR aufrufen wenn JS-Layer verifiziert
|
|
* hat dass der 24h-Cooldown abgelaufen ist. Native-Modul prüft das nicht
|
|
* — der Backend-Cooldown ist Single Source of Truth, das ist Aufgabe der
|
|
* JS-Schicht.
|
|
*/
|
|
disable(): Promise<DisableResult>;
|
|
|
|
/**
|
|
* iOS Layer 2 — webContent-Filter (ManagedSettings). Bestimmt das Land via
|
|
* Locale.current.region, lädt die gebündelte Top-Gambling-Domain-Liste für
|
|
* dieses Land (≤50 Domains, Apple-Hartlimit) und setzt
|
|
* `ManagedSettingsStore().webContent.blockedByFilter = .auto(...)` — blockt
|
|
* die Domains in WebKit (Safari u.a.) plus systemseitig Adult-Content.
|
|
*
|
|
* Setzt eine gültige Family-Controls-Authorization voraus (wie der App-Lock).
|
|
* Auf Android/iOS<16 no-op. Stilles Sicherheitsnetz; KEINE Auto-Trigger-Logik
|
|
* — muss explizit aufgerufen werden (siehe TODO(layer2-gating) im Swift-Modul).
|
|
*/
|
|
applyWebContentFilter(): Promise<WebContentFilterResult>;
|
|
|
|
/**
|
|
* iOS Layer 2 — setzt den webContent-Filter zurück (blockedByFilter = .none).
|
|
* Rührt denyAppRemoval (App-Lock) NICHT an. Auf Android/iOS<16 no-op.
|
|
*/
|
|
clearWebContentFilter(): Promise<{ cleared: boolean; error?: string }>;
|
|
|
|
/**
|
|
* iOS Layer 2 — synct die kuratierte Gambling-Domain-Liste vom Backend
|
|
* (`GET /api/protection/webcontent-domains`) und cached sie als
|
|
* `webcontent-domains.json` im App-Group-Container. `loadWebContentDomains`
|
|
* liest danach cache-first; die gebündelte `gambling-domains.json` bleibt
|
|
* nur noch Offline-Seed/Fallback.
|
|
*
|
|
* Gespiegelt von `syncBlocklist`: baseURL + authToken aus der Supabase-
|
|
* Session, Bearer-Auth, ETag/If-None-Match, Retry mit Backoff. Nach
|
|
* erfolgreichem Sync wird — wenn Family Controls authorisiert ist —
|
|
* `applyWebContentLayer()` erneut ausgeführt, damit die neue Liste sofort
|
|
* greift. Server respondet 304 wenn ETag matched → updated=false.
|
|
*/
|
|
syncWebContentDomains(
|
|
opts: SyncWebContentDomainsOpts,
|
|
): Promise<SyncWebContentDomainsResult>;
|
|
|
|
/** Aktueller Device-State. Polling- und Health-Check-Pfad. */
|
|
getDeviceState(): Promise<DeviceLayers>;
|
|
|
|
/**
|
|
* Lädt blocklist.bin vom Server, schreibt atomisch in App-Group/internal
|
|
* storage, postet Reload-Notification an die Filter-Extension. Server
|
|
* respondet 304 wenn ETag matched → updated=false. Plan-aware:
|
|
* Free → nur personal-domains (≤5), Pro/Legend → 208k+ + personal.
|
|
*/
|
|
syncBlocklist(opts: SyncBlocklistOpts): Promise<SyncBlocklistResult>;
|
|
|
|
/**
|
|
* E2E-Verifikation: Hidden WebView lädt eine bekannte Gambling-Domain,
|
|
* prüft ob WebKit/Browser den Load aborted (Filter funktioniert) oder die
|
|
* Page lädt (Filter ist tot — Alarm).
|
|
*/
|
|
runHealthProbe(opts?: HealthProbeOpts): Promise<HealthProbeResult>;
|
|
|
|
/** Öffnet System-Settings auf dem entsprechenden Tab. */
|
|
openSystemSettings(target?: SystemSettingsTarget): Promise<void>;
|
|
|
|
/**
|
|
* iOS: liest die nativen Protection-Logs (SharedLogStore — NEFilter/
|
|
* FamilyControls Flow) aus dem App-Group-UserDefaults. Für Debug-Page,
|
|
* damit TestFlight-Tester den nativen Flow ohne Mac/Console.app sehen.
|
|
*/
|
|
getProtectionLogs(): Promise<string[]>;
|
|
|
|
/** iOS: leert die nativen Protection-Logs. */
|
|
clearProtectionLogs(): Promise<void>;
|
|
|
|
// ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ───────
|
|
|
|
/** Android: Live-Check ob unser AccessibilityService aktuell als enabled
|
|
* registriert ist (Settings.Secure + AccessibilityManager). */
|
|
isAccessibilityEnabled(): Promise<{ enabled: boolean }>;
|
|
|
|
/** Android: Öffnet Settings → Bedienungshilfen, möglichst tief auf die
|
|
* Rebreak-Detail-Page (deep-link). Fallback: generelle A11y-Liste. */
|
|
openAccessibilitySettings(): Promise<{ opened: boolean }>;
|
|
|
|
/** Android: Aktiviert Tamper-Lock-Watchdog (Settings-Page-Blockade durch
|
|
* AccessibilityService). Wirft `preconditions_not_met` wenn VPN oder A11y
|
|
* nicht beide live. */
|
|
armTamperLock(): Promise<{ armed: boolean }>;
|
|
|
|
/** Android: Disarm Tamper-Lock. Schutz-Layers laufen weiter, aber Settings-
|
|
* Watchdog blockt nicht mehr. Im normalen Flow nur nach Cooldown-Ablauf. */
|
|
disarmTamperLock(): Promise<{ armed: boolean }>;
|
|
|
|
/** Android: kombinierter Status aller 3 Layers + Blocklist-Count. */
|
|
getProtectionStatus(): Promise<{
|
|
vpnEnabled: boolean;
|
|
accessibilityEnabled: boolean;
|
|
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');
|