chahinebrini 5a16cf771b feat(ios-protection): v1 NEPacketTunnelProvider DNS-Sinkhole als Layer-1
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>
2026-05-21 23:13:54 +02:00

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');