# iOS VPN-/DNS-Tunnel Gambling-Filter — Recherche & Architektur-Proposal

Status: Proposal, KEINE Code-Änderungen. Datum: 2026-05-21.
Autor-Modus: honest consultant — Trade-offs explizit, unbelegte Limitierungen als „Hypothese, ungeprüft" markiert.

---

## 0. Warum dieser Auftrag

`NEURLFilter` (iOS 26, `org.rebreak.app.URLFilterExtension`) ist Apple-seitig
blockiert (`configurationInvalid` / `serverSetupIncomplete` — Framework-Bug,
DTS-Incident pending; siehe `ops/pir-server/apple-dts-neurlfilter-report.md`).
Die App braucht eine **lieferbare** iOS-Schutzschicht, die

- **nicht** von Apples NEURLFilter/PIR-Stack abhängt,
- auch auf **iOS < 26** läuft,
- **MDM-frei** ist (User-Consent-basiert wie AdGuard / NextDNS / Lockdown),
- **Parität** zum bereits funktionierenden Android-VPN-DNS-Filter herstellt.

Ziel-Ansatz: lokaler On-Device-Tunnel, der Gambling-Domains blockt.

---

## 1. Bestandsaufnahme (Code gelesen)

### 1.1 Android-VPN-Filter — das Referenz-Design

Dateien: `modules/rebreak-protection/android/.../vpn/RebreakVpnService.kt`,
`filter/DnsFilter.kt`, `filter/HashList.kt`, `filter/DomainHasher.kt`.

Funktionsweise (das wollen wir auf iOS spiegeln):

- `VpnService`-Subclass etabliert ein **TUN-Interface** mit virtuellem Subnet
  `10.0.0.0/24`. Nur die virtuelle DNS-IP `10.0.0.1` wird geroutet
  (`addRoute(VIRTUAL_DNS_ADDR, 32)`) — also **nur DNS-Traffic** läuft durch den
  Tunnel, sonstiger Traffic geht direkt. Eigene App ist via
  `addDisallowedApplication(packageName)` ausgenommen.
- Worker-Thread liest IPv4/UDP-Pakete vom TUN, `DnsFilter.process()` parst den
  QNAME. Bei Treffer in der Blocklist → **synchrone NXDOMAIN-Response**
  (RCODE=3). Bei Miss → async an Upstream `1.1.1.1:53` weitergeleitet, Response
  zurück ins TUN.
- Blocklist-Format: `HashList` = memory-mapped `blocklist.bin`, sortierte
  **64-bit big-endian UInt64s**, Binary-Search O(log n). Jeder Hash = die ersten
  8 Bytes von `SHA-256(normalize(host))`. **Identisch zu Server und iOS**
  (`DomainHasher`). Subdomain-Match via `matchesAnySuffix` (max 5 Ebenen).
- Foreground-Service + Notification, `START_STICKY` für Auto-Restart.
- `onRevoke()` (User schaltet VPN in System-Settings ab) → `clearEnabledFlag()`
  setzt `filter_enabled=false` UND `tamper_armed=false`.

Wichtig: Der Android-Filter ist ein **reiner DNS-Sinkhole** über ein TUN — er
inspiziert keine Nicht-DNS-Pakete. Das ist die simpelste lauffähige Variante.

### 1.2 iOS-Bestand

`modules/rebreak-protection/ios/RebreakProtectionModule.swift` exponiert:

- `activateUrlFilter` — NEURLFilter (iOS 26, **aktuell tot**).
- `resetUrlFilter` — NEFilterManager denied-cache-Reset.
- `activateFamilyControls` — `AuthorizationCenter.requestAuthorization` +
  `ManagedSettingsStore.application.denyAppRemoval = true` (App-Lock).
- `applyWebContentFilter` / `clearWebContentFilter` — **Layer 2**:
  `ManagedSettingsStore().webContent.blockedByFilter = .auto(...)` mit einer
  kuratierten, länderabhängigen Domain-Liste, **Apple-Hartlimit 50 Domains**.
- `disable` — schaltet alle Layer ab (`clearAllSettings()` etc.).
- `syncBlocklist` — lädt `blocklist.bin` vom Backend (`/api/url-filter/blocklist.bin`),
  atomic write in den App-Group-Container `group.org.rebreak.app`, ETag-Caching,
  Darwin-Notification `rebreak.blocklist.updated` an die Extension.
- `runHealthProbe` — verstecktes WKWebView gegen `bet365.com`.
- `getDeviceState`, `getProtectionLogs` etc.

Die App-Group `group.org.rebreak.app` + `blocklist.bin`-Pipeline existiert
bereits und ist plattformübergreifend kompatibel — **das wiederverwenden wir
1:1**.

### 1.3 Expo-Config-Plugin

`plugins/with-rebreak-protection-ios.js` scaffoldet das NEURLFilter-ExtensionKit-
Target. Es zeigt das vollständige Muster, das wir für ein Packet-Tunnel-Target
brauchen:

- `withEntitlementsPlist` — Haupt-App-Entitlements
  (`com.apple.developer.networking.networkextension`, `application-groups`).
- `withDangerousMod('ios')` — kopiert Extension-Sources nach `ios/`.
- `withXcodeProject` — `proj.addTarget(NAME, 'app_extension', ...)`, Build-Phasen,
  PBXGroup, Build-Settings, Embed-Phase, Target-Dependency.

Unterschied für einen Packet Tunnel: Es ist eine **klassische PluginKit-App-
Extension** (`NSExtensionPointIdentifier = com.apple.networkextension.packet-tunnel`),
also bleibt die Embed-Phase die normale „Embed App Extensions"
(`dstSubfolderSpec = 13`, PlugIns/) — NICHT der ExtensionKit-Sonderweg
(`16`/Extensions/), den das NEURLFilter-Plugin gerade braucht.

### 1.4 JS-Orchestrierung

`lib/protection.ts`: Cooldown ist **Backend-driven** (JWT-Claim
`cooldown_ends_at`, Server-Zeit = Source of Truth). Native-Modul macht nur
Device-State. `getCombinedState()` merged Device-Layer + Backend-Cooldown zu
einer `ProtectionPhase`. Phase `recoveringFromBypass` = Backend sagt „Schutz
soll an sein", aber `layers.urlFilter !== true` (jemand hat den Filter extern
abgeschaltet). **Diese Logik trägt 1:1 — der VPN-Filter belegt einfach den
`urlFilter`-Slot wie heute schon der Android-VPN ihn belegt** (siehe Alias-Code
in `getCombinedState`: `urlFilter: rawLayers.vpn`).

---

## 2. API-Wahl: `NEPacketTunnelProvider` vs `NEDNSProxyProvider` vs `NEAppProxyProvider`

### 2.1 `NEDNSProxyProvider` — AUSGESCHLOSSEN

Apple DTS, Apple Developer Forums Thread 689889: NEDNSProxyProvider ist
**nur auf supervised devices** verfügbar. Zitat (DTS-Mitarbeiter, sinngemäß):
„NEDNSProxyProvider is available for iOS supervised devices only."

→ Für ein MDM-/Supervision-freies Consumer-Produkt **disqualifiziert**. Das wäre
sonst die sauberste DNS-only-API gewesen (kein Paket-Parsing nötig), aber das
Supervision-Gate killt es. **Belegt.**

### 2.2 `NEAppProxyProvider` — UNGEEIGNET

Flow-basierter Transparent-Proxy auf TCP/UDP-**Flow**-Ebene, primär für
Per-App-VPN in Enterprise-Kontexten. Sieht keine rohen DNS-Pakete; DNS-Auflösung
passiert außerhalb des App-Proxy-Flows. Kein natürlicher Ankerpunkt für ein
domain-basiertes DNS-Sinkholing. → nicht passend.

### 2.3 `NEPacketTunnelProvider` — EMPFOHLEN

Das ist die API hinter AdGuard, NextDNS, Lockdown, 1Blocker-DNS etc. — exakt das
Pendant zum Android-`VpnService`.

- Etabliert ein **TUN-Interface**; die Extension bekommt rohe IP-Pakete via
  `packetFlow.readPackets` und schreibt zurück mit `packetFlow.writePackets`.
- Konfigurierbar als **lokaler** Tunnel: `NEPacketTunnelNetworkSettings` mit
  einer virtuellen Tunnel-Remote-Address, die nie real kontaktiert wird.
- DNS-Server lässt sich auf eine **virtuelle IP** setzen (`NEDNSSettings`), und
  via `NEIPv4Settings.includedRoutes` routet man **nur** diese DNS-IP in den
  Tunnel — sonstiger Traffic geht direkt vorbei. **Das ist 1:1 das Android-
  Design** (`addRoute(VIRTUAL_DNS_ADDR, 32)`).

**Empfehlung: `NEPacketTunnelProvider`, betrieben als reiner lokaler DNS-Sinkhole.**

Begründung: einzige API ohne Supervision-Gate, die DNS sehen kann; direkt
analog zum funktionierenden Android-Code; Blocklist-Pipeline (`blocklist.bin`,
`HashList`, `DomainHasher`) ist vollständig wiederverwendbar.

> Honesty-Hinweis: Apple DTS (Thread 114550) sagt explizit „you should not be
> using a NEPacketTunnelProvider in this manner for DNS filtering" und verweist
> auf NEDNSProxyProvider. Das ist Apples *Stil-Empfehlung*. **Faktisch** tun
> AdGuard / NextDNS / Lockdown genau das seit Jahren mit App-Store-Approval, weil
> NEDNSProxyProvider auf unsupervised devices schlicht nicht verfügbar ist. Das
> Risiko ist also primär „Apple könnte die Praxis irgendwann einschränken" —
> realistisch niedrig (zu viele etablierte Apps), aber nicht null. Kein
> Doc-Beleg für ein hartes Verbot. Einordnung: **Hypothese (Review-Risiko),
> nach Marktlage gering.**

### 2.4 Wie konkret blocken?

Verfahren: **DNS-Sinkholing per NXDOMAIN** (identisch Android `DnsFilter`):

- IPv4 + UDP + Port 53 parsen, QNAME extrahieren.
- `HashList.matchesAnySuffix(domain)` (SHA-256-Prefix-Hash, Binary-Search).
- Treffer → synthetische DNS-Response mit `RCODE=3 (NXDOMAIN)`, Quell-/Ziel-
  IP+Port getauscht, IP-Checksum neu — direkt zurück ins TUN.
- Miss → Query an Upstream (`1.1.1.1:53`) forwarden, Response zurückschreiben.

Alternativen zu NXDOMAIN: A-Record auf `0.0.0.0` ("blackhole") oder Drop ohne
Antwort. NXDOMAIN ist die sauberste Wahl (Browser zeigt klaren „Server nicht
gefunden"-Fehler statt Timeout-Hänger), und der Android-Code macht es bereits
so → für Parität NXDOMAIN.

> Optionaler Bonus: Statt NXDOMAIN könnte man bei Treffern auf eine **lokale
> Block-Seite** (`127.0.0.1` + in-Extension-HTTP, oder eine eigene
> rebreak.org-Interstitial-Domain) umleiten. Das ist UX-Politur, kein
> Kern-Scope — würde TCP-Handling erfordern. Für v1 NXDOMAIN.

### 2.5 Die 208k Domains rein — Performance/Speicher

Genau wie Android: **kein Klartext im Tunnel**. `blocklist.bin` = 208k × 8 Byte
≈ **1,6 MB**, sortiert. Die Packet-Tunnel-Extension `mmap`t die Datei aus dem
App-Group-Container und macht Binary-Search (O(log n), ~18 Vergleiche bei 208k).

- Swift-Portierung von `HashList` existiert konzeptionell schon — die Repo-
  Komponenten `BloomFilter.swift` / `Murmur3Hash.swift` / `FNV1aHash.swift` im
  NEURLFilter-Extension-Ordner sind eine andere Hash-Strategie (Bloom-Filter für
  NEURLFilter-Prefilter) und **nicht** wiederverwendbar. Wir brauchen einen
  schlanken `DomainHasher.swift` + `HashList.swift` (mmap + Binary-Search auf
  big-endian UInt64), 1:1 portiert aus Kotlin. ~120 Zeilen Swift.
- **Speicher-Budget kritisch:** Packet-Tunnel-Extensions haben ein hartes
  Memory-Limit. Apple-Docs/Forenkonsens: NE-Extensions ~**15 MB** (älteres
  Limit; neuere iOS-Versionen großzügiger, aber kein dokumentierter Wert für
  Packet Tunnel → **Hypothese, ungeprüft, konservativ auf 15 MB planen**).
  Eine 1,6-MB-mmap-Datei ist unkritisch — `mmap` lädt nur angefragte Pages,
  Working Set bleibt im KB-Bereich. **Niemals die ganze Liste als Array/Set in
  den Heap laden.** Der mmap-Ansatz von `HashList` ist genau deshalb richtig.

---

## 3. Entitlements

### 3.1 Was gebraucht wird

Haupt-App **und** Packet-Tunnel-Extension brauchen je:

```
com.apple.developer.networking.networkextension = [ "packet-tunnel-provider" ]
com.apple.security.application-groups          = [ "group.org.rebreak.app" ]
```

`personal-vpn` (`com.apple.developer.networking.vpn.api`) wird **nicht**
gebraucht — das ist für `NEVPNManager`/IKEv2-Personal-VPN. Ein
Packet-Tunnel-Provider (`NETunnelProviderManager`) zählt als „enterprise/
custom VPN" und kommt allein mit dem `networkextension`-Entitlement aus
(Apple Developer Forums Thread 67613).

### 3.2 Braucht es Apple-Freigabe?

**Nein — kein Capability-Request-Formular.** Beleg: Apple-Doc „Provisioning with
managed capabilities" + Forenkonsens — `packet-tunnel-provider` (wie auch
`content-filter-provider`) wird **direkt in Xcode Signing & Capabilities**
bzw. im Developer-Portal aktiviert. Die **einzigen** NE-Provider, die ein
managed-capability-Approval brauchen, sind **App Push Provider** und
**Hotspot Helper**.

Das ist ein **deutlicher Vorteil gegenüber Family Controls**: FC brauchte das
Distribution-Entitlement-Approval von Apple (siehe Commit `c6604f0`,
`REBREAK_ENABLE_FAMILY_CONTROLS`-Flag, Memory `feedback_ios_builds_eas_only`).
`packet-tunnel-provider` braucht das **nicht** — kein Wochen-langes Warten.

### 3.3 Dev-Build vs Distribution — die wichtige Asymmetrie

Recherche-Treffer (Apple-Doc „Content filter providers", Forenkonsens):

| Provider | Dev-signed | Distribution-signed |
|---|---|---|
| `content-filter-provider` (NEFilterDataProvider) | jedes Gerät | **nur supervised** |
| `packet-tunnel-provider` | jedes Gerät | **jedes Gerät** ✅ |

Das ist **der entscheidende Punkt** und genau der Grund, warum
`NEPacketTunnelProvider` und nicht `NEFilterDataProvider` gewählt wird:

- `NEFilterDataProvider` (das alte „url-filter"-Layer aus `resetUrlFilter`/
  `activate`) hat dieselbe Supervised-Falle wie NEDNSProxyProvider, sobald man
  distribution-signed (TestFlight/Store) ausliefert.
- `NEPacketTunnelProvider` hat **keine** Supervised-Beschränkung — weder dev
  noch distribution. → läuft in TestFlight und im App Store auf normalen,
  unsupervised iPhones.

Konsequenz für die jetzige Lage:
- **Lokaler Xcode-Dev-Build OHNE jegliche Apple-Freigabe**: ja, sofort
  testbar (`./clean-ios.sh --xcode`, Team in Xcode wählen — analog
  Memory `feedback_ios_builds_eas_only`).
- **TestFlight/EAS**: ebenfalls ohne Freigabe, da Capability nur in Xcode/Portal
  toggled wird, kein Review-Antrag.

---

## 4. iOS-Versionen

- `NEPacketTunnelProvider`: seit **iOS 9.0** verfügbar.
- `NETunnelProviderManager`: seit iOS 9.0.
- `NEDNSSettings` auf Tunnel-Settings: iOS 9.0+.

→ Praktische Untergrenze = das **Deployment-Target der Haupt-App**. Aktuell ist
das `IPHONEOS_DEPLOYMENT_TARGET` der App iOS 16+ (siehe Plugin-Code: Main-App
iOS 16, NEURLFilter-Extension iOS 26). Der Packet-Tunnel-Filter läuft also auf
**allen** von der App unterstützten iOS-Versionen — Ziel „deutlich < 26" klar
erfüllt. Empfehlung: Extension-Deployment-Target = Main-App-Target (16.0), kein
26.0-Gate.

---

## 5. Tamper-Resistance — die ehrliche Einordnung

**Kernaussage: Auf einem unsupervised iPhone kann der User den VPN-Tunnel
jederzeit abschalten. Das ist nicht umgehbar ohne MDM/Supervision. Belegt.**

### 5.1 Was der User tun kann

- **Settings → VPN → Toggle aus** oder **Settings → Allgemein → VPN & Geräte-
  verwaltung → Konfiguration löschen**: schaltet den Tunnel ab. iOS feuert dann
  das Pendant zu Androids `onRevoke` (`stopTunnel`-Aufruf an die Extension).
- **App löschen**: nimmt die Extension mit.
- Es gibt **keine** App-API, die einen Consumer-VPN „always-on" und für den
  User unabschaltbar macht.

Beleg: NextDNS-Help-Center + Hexnode-Doku — „Without device supervision and
MDM, users can typically disable local VPN configurations. […] only supervised
devices with MDM can enforce a true always-on VPN that users cannot disable."

### 5.2 `includeAllNetworks` / On-Demand — kein echter Lock

- `NEVPNProtocol.includeAllNetworks` (Kill-Switch) verhindert Traffic-Leaks
  *wenn der Tunnel an ist* — es verhindert **nicht**, dass der User den Tunnel
  abschaltet.
- `NETunnelProviderManager.isOnDemandEnabled` + On-Demand-Rules können den
  Tunnel **automatisch wieder hochfahren**, wenn ein Netzwerk-Wechsel passiert.
  Das erhöht die Friction (Tunnel kommt von selbst zurück), ist aber **kein
  Lock**: der User kann On-Demand im selben Settings-Screen abschalten. Nützlich
  als „self-healing", nicht als Tamper-Schutz. **Belegt (API-Semantik).**

### 5.3 Family Controls / Screen Time hier

Family Controls greift hier **nicht direkt**: `ManagedSettingsStore` hat keine
Restriction „VPN-Profil darf nicht entfernt werden". `denyAppRemoval` schützt
nur die **App** vor Löschung, nicht das VPN-Profil. Hypothese, dass FC den
VPN-Toggle sperren kann: **nicht belegt — und kein Beleg gefunden, dass es geht
→ als nicht verfügbar behandeln.**

### 5.4 Realistischer Tamper-Schutz = Friction, nicht Hard-Lock

Das Produkt-Konzept trägt das bereits: der **Backend-Cooldown** (`lib/protection.ts`,
24h, server-time JWT) ist die eigentliche Schutzschicht gegen Impuls-Rückfälle —
nicht ein technisch unabschaltbarer Tunnel. Realistische iOS-Schicht:

1. VPN-Tunnel blockt im Normalbetrieb zuverlässig.
2. Schaltet der User den Tunnel ab → die App erkennt das (Status-Polling +
   `getDeviceState`, Phase `recoveringFromBypass`) und triggert die bestehende
   Bypass-/Cooldown-Logik.
3. `isOnDemandEnabled` lässt den Tunnel nach Netzwerk-Events von selbst
   zurückkehren → der „Aus"-Zustand ist instabil/unbequem.
4. Wer **echte** Unabschaltbarkeit will: Pfad ist die bereits dokumentierte
   MDM-Productization (Memory `project_mdm_productization`,
   `project_legend_multi_device_usp`) — ABM-ADE/Supervision, separates Add-On.
   Der VPN-Filter und MDM schließen sich nicht aus; MDM kann ein
   `VPN`-Payload-Profil mit `ProhibitDisablement` non-removable ausrollen → dann
   wird derselbe Tunnel doch hart verriegelt.

**Ehrliche Bottom-Line:** Der VPN-Filter liefert **funktionierenden Schutz +
Friction**, nicht **Hard-Enforcement**. Genau dieselbe Tamper-Realität wie der
Android-VPN-Filter heute schon (`onRevoke` existiert dort, weil der User auch
dort abschalten kann — Android hat mit dem A11y-Service nur einen zusätzlichen
Tamper-Layer, den iOS nicht hat). Für die Onboarding-Kommunikation: Schutz als
„hilft dir, nicht rückfällig zu werden", nicht als „macht es unmöglich".

---

## 6. Bypass-Vektoren

| Vektor | DNS-Sinkhole-Tunnel | Voll-Packet-Tunnel (alle Pakete) |
|---|---|---|
| VPN abschalten (Settings) | umgeht alles | umgeht alles |
| **DoH/DoT im Browser** (Firefox „Sicheres DNS", Chrome custom DoH) | **umgeht** — DoH ist HTTPS auf 443, kein Klartext-DNS auf 53 | umgeht, solange wir nicht IP/SNI filtern |
| iOS-System-Encrypted-DNS-Profil (anderes `.mobileconfig`) | überschreibt unseren DNS — aber: bei aktivem System-VPN wird das iOS-DNS-Profil ignoriert → **gefangen** | gefangen |
| **iCloud Private Relay** | Apple **deaktiviert Private Relay automatisch, sobald ein System-VPN aktiv ist** → **gefangen** | gefangen |
| Safari Private Browsing oblivious-DNS (iOS 17+ routet DNS zu Apple) | **Hypothese: läuft als HTTPS, umgeht den :53-Sinkhole** — ungeprüft, ob ein aktiver Tunnel das überschreibt | wahrscheinlich gefangen, da Traffic im Tunnel |
| **IP-Direktzugriff** (`https://203.0.113.5`, kein DNS) | **umgeht** — kein DNS-Lookup | nur fangbar via SNI/IP-Filter |
| App statt Browser (native Casino-App) | DNS-Lookup wird gefangen | gefangen — aber iOS-App-Store hat keine echten Glücksspiel-Apps (Memory `feedback_mdm_minimal_scope`), Risiko gering |
| Mobilfunk statt WLAN | egal — Tunnel ist netzunabhängig | egal |

### 6.1 Ehrliche Abwägung DNS-Sinkhole vs Voll-Packet-Tunnel

**Option A — DNS-Sinkhole** (nur :53 ins TUN routen, wie Android):
- + simpel, batterieschonend, geringe Bug-Fläche, 1:1-Parität zu Android,
  blockt 90%+ der realen Casino-Zugriffe (Domains).
- − DoH-im-Browser und IP-Direktzugriff umgehen es.

**Option B — Voll-Packet-Tunnel** (alle Pakete ins TUN, TLS-SNI inspizieren,
auch HTTPS-Verbindungen zu Block-Hosts droppen):
- + fängt zusätzlich DoH (man sieht SNI der DoH-Server und kann bekannte
  DoH-Endpoints — Cloudflare/Google DoH — blocken) und kann SNI-basiert auch
  ohne DNS blocken.
- − deutlich komplexer (TCP-Reassembly oder zumindest TLS-ClientHello-Parsing),
  höherer Akku-/CPU-Verbrauch, größere Bug-Fläche, mehr Memory-Druck (15-MB-
  Limit!), und SNI-Filtering ist gegen **Encrypted Client Hello (ECH)** ohnehin
  endlich.

**Empfehlung: v1 = Option A (DNS-Sinkhole).** Begründung: Parität zu Android
ist explizites Ziel; Android macht genau das; die DoH-Lücke besteht auf Android
identisch und wurde dort akzeptiert. Den **DoH-Vektor kann man auch im
DNS-Sinkhole verkleinern**, indem man die bekannten **DoH-Provider-Domains**
(`mozilla.cloudflare-dns.com`, `dns.google`, `chrome.cloudflare-dns.com`, …) mit
in die Blocklist aufnimmt → dann scheitert Browser-DoH am Bootstrap-Lookup.
Das ist ein billiger 80/20-Gewinn ohne Voll-Tunnel.

Option B wäre ein späteres Hardening (evtl. Legend-USP), aber **kein v1-Scope**
— und sollte vor Bau eine eigene User-GO-Runde bekommen (Memory
`feedback_no_workarounds_core_architecture` / `feedback_clarify_before_act`).

---

## 7. Integration ins bestehende Modul

### 7.1 Layer-Modell danach

| Layer | Mechanismus | Status nach diesem Proposal |
|---|---|---|
| Layer 1 (iOS) | **NEU: `NEPacketTunnelProvider` DNS-Filter** | ersetzt NEURLFilter als primären, lieferbaren Filter |
| Layer 1-alt | `NEURLFilter` (iOS 26) | bleibt als **optionaler** iOS-26-Upgrade-Pfad daneben — NICHT löschen |
| Layer 2 (iOS) | `ManagedSettings.webContent` (≤50 Domains) | **bleibt unverändert** — stilles Netz, FC-gated |
| App-Lock | `denyAppRemoval` (Family Controls) | bleibt — orthogonal zum Filter |
| Cooldown | Backend-JWT | bleibt — Source of Truth |

**Verhältnis VPN-Filter ↔ NEURLFilter:** Der VPN-Filter **ersetzt** NEURLFilter
als den Filter, den die App tatsächlich aktiviert. NEURLFilter bleibt als Code
liegen (`activateUrlFilter` iOS-26-Branch, Extension-Target, Plugin), wird aber
**nicht mehr der Default-Pfad**. Sobald Apple den DTS-Incident löst, kann man
NEURLFilter auf iOS 26 als „privacy-besseren" Filter optional reaktivieren (PIR =
Domains verlassen das Gerät nie nicht mal gehasht). Bis dahin: VPN-Filter ist
der einzige aktive Layer 1. Empfehlung: **nicht zwei Layer-1-Filter gleichzeitig
laufen lassen** — wenn der VPN-Tunnel aktiv ist, NEURLFilter aus, sonst kollidiert
das DNS-Handling. Das `activateUrlFilter` in `lib/protection.ts` würde auf iOS
künftig den VPN-Tunnel starten statt NEURLFilter (gleicher `urlFilter`-Slot).

### 7.2 Betroffene Dateien (Skizze, KEIN Code hier)

Neue native Komponenten:

- `modules/rebreak-protection/ios/RebreakPacketTunnelExtension/` (neues Verzeichnis)
  - `PacketTunnelProvider.swift` — `NEPacketTunnelProvider`-Subclass: `startTunnel`
    setzt `NEPacketTunnelNetworkSettings` (virtuelle Adresse, DNS auf virtuelle IP,
    `includedRoutes` nur die DNS-IP), Read-Loop über `packetFlow`.
  - `DnsFilter.swift` — IPv4/UDP/DNS-Parser + NXDOMAIN-Builder (Port aus Kotlin
    `DnsFilter.kt`).
  - `HashList.swift` — mmap + Binary-Search auf big-endian UInt64 (Port aus
    `HashList.kt`).
  - `DomainHasher.swift` — `SHA-256(normalize(host)).first(8)` (Port aus
    `DomainHasher.kt`; teilt die Normalisierungs-Regeln mit Server/Android).
  - `Info.plist` — `NSExtensionPointIdentifier =
    com.apple.networkextension.packet-tunnel`, `NSExtensionPrincipalClass`.
  - `RebreakPacketTunnelExtension.entitlements` — `networkextension =
    [packet-tunnel-provider]`, `application-groups`.

Geänderte Dateien:

- `modules/rebreak-protection/ios/RebreakProtectionModule.swift`
  - iOS-`activateUrlFilter`: statt NEURLFilter den Packet-Tunnel via
    `NETunnelProviderManager.loadAllFromPreferences` → `NETunnelProviderProtocol`
    (mit `providerBundleIdentifier = org.rebreak.app.PacketTunnelExtension`) →
    `saveToPreferences` (löst den **VPN-System-Permission-Dialog** aus, einmalig)
    → `connection.startVPNTunnel()`.
  - `getDeviceState`: `urlFilter` aus `NETunnelProviderManager … connection.status
    == .connected` lesen.
  - `disable`: `connection.stopVPNTunnel()` + `removeFromPreferences()`.
  - Optional `isOnDemandEnabled = true` + On-Demand-Rules für Self-Healing.
  - `syncBlocklist` bleibt **wie es ist** — schreibt `blocklist.bin` in den
    App-Group-Container, Darwin-Notification triggert die Tunnel-Extension zum
    Neu-mmap'en (Extension registriert `CFNotificationCenter`-Observer auf
    `rebreak.blocklist.updated`, genau wie die NEURLFilter-Extension es täte).
  - `runHealthProbe` funktioniert unverändert (WKWebView gegen bet365).

- `plugins/with-rebreak-protection-ios.js`
  - Zweites `addTarget('RebreakPacketTunnelExtension', 'app_extension', …)`.
  - Embed-Phase = **klassisch** (`dstSubfolderSpec 13`, „Embed App Extensions",
    PlugIns/) — NICHT der ExtensionKit-`16`-Sonderweg. Wichtig: nicht den
    NEURLFilter-Embed-Fix versehentlich auf dieses Target anwenden.
  - Haupt-App-Entitlements: `networkextension`-Array um `packet-tunnel-provider`
    erweitern. **Entscheidung offen:** Array darf mehrere Werte halten
    (`[url-filter-provider, packet-tunnel-provider]`) — beide gleichzeitig ist
    technisch erlaubt. Wenn NEURLFilter komplett ausgemustert wird, kann
    `url-filter-provider` raus.
  - Frameworks-Build-Phase: `NetworkExtension.framework` (schon vorhanden).

- `lib/protection.ts` — minimal: `activateUrlFilter` ruft iOS denselben
  Native-Call, der jetzt den Tunnel startet. Die `recoveringFromBypass`-Phase
  und der `urlFilter`-Alias funktionieren **ohne Änderung** (der Slot ist
  semantisch „Layer-1-Filter an/aus", nicht „NEURLFilter").

- `src/RebreakProtection.types.ts` — keine Pflicht-Änderung; ggf. ein
  optionales Feld `vpnStatus` für detaillierteres Debug-UI.

- `app.config.ts` — `pirServerURL`/`pirAuthToken` werden für den VPN-Filter
  **nicht** gebraucht; können bleiben (NEURLFilter-Fallback) oder später raus.

Wiederverwendet, **nicht** angefasst: `syncBlocklist`-Backend-Endpoint,
`blocklist.bin`-Format, `DomainHasher`-Normalisierung,
`backend/scripts/generate-pir-input.ts` ist PIR-spezifisch und **irrelevant**
für den VPN-Filter (der VPN-Filter nutzt `blocklist.bin`, nicht `input.txtpb`).

### 7.3 Cooldown / Layer-Logik

Nichts an der Cooldown-Logik ändert sich. `forceDisable` ruft `disable` →
stoppt den Tunnel. Der einzige iOS-spezifische Punkt: **Tunnel-Disconnect-
Detection.** Android hat `onRevoke`; iOS' `NEPacketTunnelProvider.stopTunnel`
wird ebenfalls aufgerufen, wenn der User den VPN-Toggle umlegt. Die Extension
kann das in den App-Group-`UserDefaults` schreiben (analog Androids
`clearEnabledFlag`), und `getDeviceState` liest den `connection.status` direkt
— damit greift die bestehende `recoveringFromBypass`-Phase.

---

## 8. Battery / Performance

- Ein **DNS-only-Sinkhole-Tunnel** (nur :53 geroutet) ist sehr
  batterieschonend: der Tunnel verarbeitet nur DNS-Pakete (wenige hundert Bytes,
  wenige Dutzend pro Minute im Idle), nicht den gesamten Traffic. Sonstiger
  Traffic (Video, Downloads) läuft **nicht** durch die Extension. Das ist genau
  das Android-Modell und dort akzeptiert.
- mmap-Binary-Search ist CPU-vernachlässigbar (~18 Vergleiche/Query).
- Forwarding der Nicht-Block-Queries an `1.1.1.1` ist ein zusätzlicher
  Netzwerk-Hop pro Lookup — minimaler Latenz-Aufschlag, OS-DNS-Cache federt das.
- Ein **Voll-Packet-Tunnel** (Option B) hätte spürbar höheren Verbrauch (jedes
  Paket durch die Extension) — weiteres Argument gegen Option B für v1.
- Memory: 15-MB-Extension-Limit beachten (Abschnitt 2.5) — mmap statt Heap-Load.
  **Hypothese, ungeprüft:** exaktes aktuelles Packet-Tunnel-Limit auf iOS 17/18
  ist nicht dokumentiert; konservativ planen, im Profiler verifizieren.

---

## 9. Expo / RN-Realität — Scaffolding

Das Repo hat das Muster bereits zweimal gelöst (NEFilter, dann NEURLFilter über
`with-rebreak-protection-ios.js`). Ein Packet-Tunnel-Target ist **einfacher** als
das NEURLFilter-ExtensionKit-Target:

- Es ist eine **klassische PluginKit-App-Extension** → der normale
  `addTarget(…, 'app_extension', …)`-Pfad, normale „Embed App Extensions"-Phase
  (`dstSubfolderSpec 13`). **Kein** ExtensionKit-Sonderweg, kein
  `EXAppExtensionAttributes`, kein `dstSubfolderSpec 16` — der knifflige Teil des
  NEURLFilter-Plugins (`MIInstallerError 39`-Workaround) entfällt hier.
- Pods/Codegen: das Target braucht nur `NetworkExtension.framework` (System-
  Framework), keine RN-Pods → kein Codegen-Risiko (Memory
  `project_ios_codegen_pipeline_setup` betrifft das nicht).
- Idempotenz: gleiche `pbxTargetByName`-Guard wie im Bestandsplugin.
- Build: lokaler Xcode-Build (`./clean-ios.sh --xcode`, Team wählen) testbar
  **ohne** Apple-Freigabe (Abschnitt 3.3). Auf dem iPhone testen, nicht im
  Simulator — Network-Extensions laufen **nicht** im iOS-Simulator (Memory
  `feedback_ios_build_targets_device_vs_sim`).
- Empfehlung: das Plugin so strukturieren, dass NEURLFilter-Target und
  Packet-Tunnel-Target **getrennte** `withXcodeProject`-Mods sind (oder ein
  Env-Flag, welches Target gebaut wird), damit man sie unabhängig togglen kann.

---

## 10. Grober Aufwand

| Block | Aufwand (grob) |
|---|---|
| `PacketTunnelProvider.swift` (Tunnel-Setup + Read-Loop) | 1–1,5 Tage |
| `DnsFilter.swift` + `HashList.swift` + `DomainHasher.swift` (Port aus Kotlin) | 1 Tag (Logik existiert, nur Swift-Übersetzung + Tests) |
| `RebreakProtectionModule.swift` — `NETunnelProviderManager`-Integration | 0,5–1 Tag |
| `with-rebreak-protection-ios.js` — zweites Target + Entitlements | 0,5–1 Tag (Embed-Phase ist hier der einfache Fall) |
| End-to-End-Debug auf echtem iPhone (Permission-Dialog, mmap-Reload, On-Demand) | 1–2 Tage (NE-Debugging ist erfahrungsgemäß zäh) |
| **Summe** | **~4–6,5 Tage** für lieferbare v1 (DNS-Sinkhole, Option A) |

Risiko-Aufschlag: NE-Extensions sind notorisch fummelig (Permission-Dialog-
Verhalten, Extension-Lifecycle, Logging nur via Console.app/SharedLogStore).
Der bestehende `SharedLogStore`-Mechanismus (App-Group-`UserDefaults`) ist
direkt wiederverwendbar fürs Extension-Logging — guter Startpunkt.

---

## 11. Offene Entscheidungen für den User

1. **Scope v1: DNS-Sinkhole (Option A) bestätigen?** Empfehlung ja — Parität zu
   Android, ~4–6,5 Tage, lieferbar. Voll-Packet-Tunnel (B) als späteres
   Hardening, separate GO-Runde.
2. **DoH-Provider-Domains in die Blocklist?** Billiger 80/20-Gewinn gegen
   Browser-DoH — aber muss serverseitig (`generate`-Pipeline / `BlocklistDomain`)
   eingepflegt werden. Ja/Nein?
3. **NEURLFilter behalten oder ausmustern?** Empfehlung: Code liegen lassen
   (iOS-26-Upgrade-Pfad, falls Apple den DTS-Bug fixt), aber als Default
   deaktivieren. Entitlement-Array kann beide Werte halten.
4. **On-Demand-Auto-Reconnect (`isOnDemandEnabled`) aktivieren?** Erhöht
   Friction (Tunnel kommt nach Netzwerk-Wechsel von selbst zurück), ist aber kein
   Hard-Lock. Empfehlung ja — es ist „self-healing", kostet nichts.
5. **Tamper-Kommunikation im Onboarding:** der iOS-VPN-Filter ist
   abschaltbar (Settings → VPN). Onboarding-Text muss das ehrlich framen
   („hilft dir" statt „macht es unmöglich"). Echte Unabschaltbarkeit nur über
   den separaten MDM-Pfad (Memory `project_mdm_productization`).
6. **iOS < 26 Bestätigung:** Extension-Deployment-Target = Main-App (16.0),
   nicht 26.0. Bestätigen, dass iOS 16 die Untergrenze ist.

---

## 12. Quellen

- NEDNSProxyProvider supervised-only / NEPacketTunnelProvider-Stilhinweis:
  Apple Developer Forums Thread 689889, Thread 114550.
- Entitlement-Werte, kein Capability-Request für packet-tunnel/content-filter,
  nur App-Push/Hotspot-Helper managed: Apple-Doc „Provisioning with managed
  capabilities", „Capability Requests", Apple Developer Forums Thread 67613.
- content-filter distribution = supervised-only, dev = jedes Gerät;
  packet-tunnel = jedes Gerät: Apple-Doc „Content filter providers",
  Apple-Doc „Network Extensions Entitlement", Forenkonsens.
- packet-tunnel braucht kein personal-vpn-Entitlement: Apple Developer Forums
  Thread 67613, NETunnelProviderManager-Doku.
- VPN auf unsupervised devices ist User-abschaltbar; ProhibitDisablement nur
  supervised: NextDNS Help Center, Hexnode „iOS VPN Settings".
- iCloud Private Relay deaktiviert sich bei aktivem VPN; DoH/DoT umgeht
  Klartext-DNS-Filter: DNSFilter-/CyberFOX-/ControlD-Doku, Apple-Support 102022.
- TN3134 Network Extension provider deployment (Provider-Typen-Übersicht).
</content>
</invoke>
