# Layer 2 — ManagedSettings `webContent`-Filter als Always-On-Fallback

**Recherche + Bewertung — iOS 26, rebreak-native. Stand: 2026-05-21. KEINE Code-Änderungen.**

---

## TL;DR / Empfehlung

Die Idee in der vorgeschlagenen Form (**statische Top-50-Gambling-Domain-Liste**, länderabhängig, „Always-On-Fallback wenn NEURLFilter aus") ist **technisch machbar, aber strategisch schwach**. Drei Kernbefunde:

1. **Es gibt KEINE Gambling-`WebDomainCategory`.** `WebContentSettings.FilterPolicy` kennt nur `.none / .specific / .auto / .all`. `.auto` blockt ausschließlich **Adult Content** (Apple-Wortlaut). Eine Gambling-Kategorie existiert nirgends — also bleibt nur die manuelle 50er-Liste.
2. **Der „50-Domain-Cap" ist real und Apple-dokumentiert** (nicht Projekt-Hypothese): *„Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once."* Steht wörtlich in der `blockedByFilter`-Doc und bei `.specific(_:)` / `.auto(_:except:)`.
3. **Der „Fallback"-Nutzen ist fragwürdig**, weil Layer 1 und Layer 2 dieselbe einzelne Schwachstelle teilen: **wenn der User die Family-Controls-Authorization widerruft, fallen NICHT nur ManagedSettings, sondern faktisch der gesamte Tamper-Schutz weg.** Der „NEURLFilter-off"-Fall, gegen den Layer 2 absichern soll, ist nur eine Teilmenge des größeren Lochs.

**Empfehlung:** Layer 2 **nicht als 50er-Domain-Always-On-Fallback** bauen. Stattdessen — falls überhaupt — als **schmales Defense-in-Depth-Add-on**: die Top-~50 Gambling-Domains des Nutzerlandes via `webContent.blockedByFilter = .specific(...)` setzen, **erklärt als „Extra-Härtung", nicht als vollwertiger Fallback**. Der reale Gewinn ist gering; der ehrliche Rat ist, die Energie eher in **Bypass-Detection + Re-Aktivierungs-Nudges** (existiert schon ansatzweise: `recoveringFromBypass`, `/api/protection/state`) zu stecken. Details + Entscheidungsfragen unten.

---

## 1. `ManagedSettings.WebContentSettings`-API — verifizierte Fakten

### `WebContentSettings`
- Struct, conformt `ManagedSettingsGroup`, verfügbar **iOS 15.0+**. Zugriff via `ManagedSettingsStore().webContent`.
- Relevante Property:
  ```swift
  var blockedByFilter: WebContentSettings.FilterPolicy?
  ```
  *„The current policy for filtering websites."* Default `nil` (kein Effekt).

### `WebContentSettings.FilterPolicy` — alle vier Cases (Apple-Doc, verifiziert)
```swift
case none                                         // kein Effekt
case specific(Set<WebDomain>)                     // blockt genau diese Domains
case auto(Set<WebDomain> = [], except: Set<WebDomain> = [])
                                                  // System blockt ADULT CONTENT
                                                  // (+ optional zusätzliche Domains, - Ausnahmen)
case all(except: Set<WebDomain>)                  // blockt ALLES außer Ausnahmen (Allowlist-Modus)
```

### `WebDomain`
Token-Typ, der eine Domain repräsentiert (Initialisierung typ. mit `WebDomain(domain: "bet365.com")`). `Set<WebDomain>` ist das Argument bei `.specific`/`.auto`.

### Gibt es eine Gambling-`WebDomainCategory`? — NEIN.
- `FilterPolicy` hat **keinen kategoriebasierten Case**. Nur Adult-Content (`.auto`) ist kategorieähnlich, und das ist hardcoded auf Adult — **nicht** Gambling, **nicht** erweiterbar.
- `WebDomainCategory` / `webDomainCategories` existiert — aber gehört zu **`ShieldSettings`**, nicht zum `webContent`-Filter. Und (Apple-Doc wörtlich): *Shielding ist eine UI-Overlay-Funktion* — *„the system calls your extension that customizes the shield's appearance"*. **Shielding blockt keinen Traffic**, es zeigt ein Overlay über bereits-erlaubten Apps/Domains. Für tatsächliches Web-Blocking irrelevant.
- **Fazit Punkt 1 der Aufgabe:** Eine „Gambling-Kategorie statt 50er-Liste" gibt es auf iOS schlicht nicht. Die 50er-Liste ist der einzige Weg über `webContent`.

---

## 2. Der „50-Domain-Cap" — VERIFIZIERT, hart, Apple-dokumentiert

Frühere Projektannahme war korrekt. Apple-Doc-Wortlaut (`blockedByFilter`, `.specific(_:)`, `.auto(_:except:)`):

> **„Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once."**

- Gilt für `.specific` **und** `.auto`. Harte Obergrenze, keine Konfigurations-Option.
- Damit ist eine 208k-Domain-Liste (wie bei NEURLFilter/PIR) über `webContent` **prinzipiell unmöglich**. Layer 2 ist API-bedingt auf eine **kuratierte Top-50** beschränkt.
- (Abzugrenzen von der separaten WebKit-Content-Blocker-Grenze von 50.000 *Rules* — das ist eine andere API und nicht gemeint.)

---

## 3. Wirkungsbereich — Safari sicher; restliche WebKit-Browser unbelegt

- **Belegt:** `webContent.blockedByFilter` wirkt auf **Safari** — Apple erwähnt explizit den Nebeneffekt *„Setting any filter policy besides `.none` will disable Safari private browsing."*
- Das ist ein **systemweiter Screen-Time-Mechanismus** (ManagedSettings = der „Enforcer" hinter Screen Time), kein App-lokaler Filter. Drittanbieter-Browser, die WebKit nutzen (auf iOS müssen das de facto alle), greifen mit hoher Wahrscheinlichkeit auf dieselbe Web-Content-Restriction zu — das ist auch das beobachtbare Screen-Time-Verhalten.
- **Hypothese, ungeprüft:** Dass `blockedByFilter` *auch* in Chrome/Firefox/Drittanbieter-WKWebViews greift, ist plausibel (Screen-Time-Webcontent-Restriction wirkt klassischerweise browserübergreifend), aber **nicht per Apple-Doc-Zitat belegt**. Apple dokumentiert nur Safari namentlich. Vor Produktiv-Versprechen muss das auf dem iPhone-Build empirisch getestet werden (Chrome iOS → bet365 öffnen).

---

## 4. Family-Controls-Voraussetzung — ja, FC reicht; aber genau das ist die Crux

- `ManagedSettingsStore`-Restriktionen wirken **nur**, wenn die App eine gültige Family-Controls-Authorization hat (`AuthorizationCenter.shared.requestAuthorization(for: .individual)` → `.approved`). Ohne Authorization sind ManagedSettings-Settings stumm.
- **Kein MDM nötig** — `.individual`-Authorization genügt. Das deckt sich exakt mit dem schon im Repo gebauten `activateFamilyControls`-Pfad (`RebreakProtectionModule.swift`, Z. 246–296: FC-Auth → `ManagedSettingsStore(...).application.denyAppRemoval = true`).
- **Lokaler Xcode-Dev-Build:** funktioniert mit dem Development-FC-Entitlement (Repo nutzt `REBREAK_ENABLE_FAMILY_CONTROLS=1` im Plugin, Z. 65). v0.3.4 hat zusätzlich das Distribution-Entitlement (genehmigt) → auch TestFlight/Store ok.
- **Wichtige verifizierte Schwäche:** *„All ManagedSettingsStore restrictions are lifted immediately by the system when authorization is revoked, and the app receives no notification."* Der User kann FC in `Einstellungen → Bildschirmzeit` widerrufen — dann ist Layer 2 **lautlos weg**, ohne Callback. Das untergräbt die „Always-On"-Behauptung.

---

## 5. Koexistenz NEURLFilter + ManagedSettings — unkritisch

- Zwei **getrennte Subsysteme**: NEURLFilter = NetworkExtension (Netzwerk-Pfad), `webContent` = ManagedSettings/Screen-Time (WebKit-Restriction). Sie laufen auf verschiedenen Ebenen, kein gemeinsamer State, keine dokumentierte Konflikt-Konstellation.
- Effektiv ein logisches **OR**: eine Domain wird geblockt, wenn *einer* der beiden Layer sie fängt. Keine Reihenfolge-Abhängigkeit.
- Beide brauchen ohnehin schon Entitlements, die die App hat (`url-filter-provider` + `family-controls`, siehe `with-rebreak-protection-ios.js` Z. 60–67). Layer 2 fügt **kein neues Entitlement** hinzu.
- **Bewertung:** Koexistenz ist der unproblematischste Punkt. Technisch sauber kombinierbar.

---

## 6. Kann der User NEURLFilter „offtogglen"? — Die Kernfrage, ehrlich beantwortet

Das ist die Annahme, auf der „Fallback" steht. Nüchterner Befund:

- **`NEURLFilterManager`:** `isEnabled` (Bool, App-gesteuert) und `shouldFailClosed` (Bool — bei `true` wird Traffic geblockt, wenn der Filter nicht erreichbar ist; das Repo setzt `shouldFailClosed = true`, `RebreakProtectionModule.swift` Z. 110). Beides setzt **die App**, nicht der User.
- **System-Toggle für den User?** NetworkExtension-Filter erscheinen üblicherweise unter `Einstellungen → Allgemein → VPN & Geräteverwaltung` bzw. als Filter-Eintrag, den der User abschalten/löschen kann. Genau dieses Verhalten ist im Repo-Code bereits sichtbar: `resetUrlFilter` existiert nur, weil der User „Nicht erlauben" tippen kann und iOS den Denied-State cached (`protection.ts` Z. 146–160). **Hypothese, gut gestützt, aber nicht per Apple-Doc-Zitat zu iOS 26 final belegt:** Ja, der User kann NEURLFilter abschalten/ablehnen — entweder beim System-Permission-Dialog oder nachträglich in den Einstellungen.
- **ABER — das ist der Punkt:** Wenn der User NEURLFilter abschaltet, ohne FC anzufassen, **fängt Layer 2 das tatsächlich ab** → das ist der einzige saubere Gewinn-Fall.
- **Das größere Loch:** Wenn der User stattdessen in `Bildschirmzeit` die **FC-Authorization widerruft** (oder Bildschirmzeit ganz deaktiviert), fallen `denyAppRemoval` **und** Layer 2 gleichzeitig weg — lautlos, ohne App-Callback (siehe §4). Layer 2 schützt also **nicht** gegen den motiviertesten Bypass-Pfad eines spielsuchtgetriebenen Nutzers, sondern nur gegen die *halbherzige* Variante „NEURLFilter aus, FC vergessen".
- **Fazit:** Das Fallback-Szenario *kann* real eintreten — aber es ist der schwächere von zwei Bypass-Pfaden. Layer 2 deckt das kleinere Loch und lässt das größere offen.

---

## 7. Länderabhängigkeit — sinnvoll, aber simpel halten

- **Warum überhaupt pro Land?** Gambling-Märkte sind national stark segmentiert (DE: Tipico, bwin; UK: bet365, William Hill, Sky Bet; FR: Winamax, PMU/Betclic; etc.). Eine globale 50er-Liste verschwendet Slots an Domains, die im Land des Nutzers irrelevant sind. Bei nur **50 Slots** ist Kuratierung pro Land der Hebel, der den Cap erträglich macht.
- **Landbestimmung — Optionen, geordnet nach Eignung:**
  1. **Device-Region** (`Locale.current.region` / `NSLocale.countryCode`) — lokal, kein Netz, datensparsam. Empfehlung. Schwäche: Region ≠ Aufenthaltsort.
  2. **User-Profil** — das Repo hat bereits DiGA-Demographie (MEMORY: `birth_year/profession/...` user-initiiert). Ein optionales „Land"-Feld wäre DSGVO-konform und am genauesten. Aber: zusätzliche UX, und Demographie ist strikt user-initiated.
  3. **IP-Geo** — am genausten für „wo bin ich", aber Netzabhängig + datenschutzkritisch (Glücksspiel-Stigma, DiGA). **Nicht empfohlen.**
- **Empfehlung:** Device-Region als Default, optionales Profil-Override. Kein IP-Geo.
- **Datenquelle der Liste:** Da der Inhalt sich selten ändert, eignet sich eine **statische, mit der App gebundelte JSON** (`country → [top50 domains]`) — kein Backend-Roundtrip, funktioniert offline, kein neuer Endpoint. Alternative: bestehender Backend-Endpoint `/api/url-filter/...` um ein `top50?country=DE`-Feld erweitern, falls schnellere Updates ohne App-Release gewünscht sind. Für ~50 stabile Domains ist das Backend-Overkill.

---

## Bewertung — ehrlich, auch kritisch

### Mehrwert für Rebreak
- **Was Layer 2 abfängt, das Layer 1 nicht abdeckt:** ausschließlich den Fall „User hat NEURLFilter abgeschaltet/abgelehnt, FC aber noch aktiv". In diesem (und nur diesem) Fenster blockt `webContent` weiterhin die Top-50.
- **Ist das der echte Gewinn?** Eher **marginal**. Begründung:
  - Es deckt nur **50 von 208.000** Domains ab — ein spielsuchtgetriebener Nutzer findet trivial eine Casino-Domain außerhalb der Top-50.
  - Es schützt **nicht** gegen den motiviertesten Bypass (FC-Widerruf, §4/§6) — dann ist Layer 2 mit weg.
  - Layer 1 (NEURLFilter) ist bereits `shouldFailClosed = true` + es gibt Backend-Bypass-Detection (`recoveringFromBypass`-Phase, `/api/protection/state`, `mark-active`). Das Produkt hat also schon einen Mechanismus, der „NEURLFilter aus" *erkennt* und den User zur Re-Aktivierung *drängt*. Layer 2 dupliziert teilweise diesen Schutzgedanken, nur schwächer.
- **Honest-consultant-Fazit:** Layer 2 als „50er-Always-On-Fallback" verkauft ein Sicherheitsversprechen, das es nicht halten kann. Es ist „Defense in Depth light" — nett, aber kein echter zweiter Sicherheitsgurt. Wer es einbaut, sollte es intern und im UI als *„zusätzliche Härtung der bekanntesten Anbieter"* framen, **niemals** als „Schutz bleibt, wenn Layer 1 aus ist".

### Verbesserungsvorschläge / Alternativen
1. **Gambling-Kategorie statt 50er-Liste:** auf iOS **nicht möglich** (§1). Entfällt.
2. **`.auto` mitnehmen — kostenlos:** Statt `.specific(top50)` → `.auto([...top50...])`. `.auto` blockt zusätzlich **Adult Content** systemseitig gratis mit. Bei Spielsucht oft Begleit-Trigger; minimaler Mehraufwand, spürbarer Härtungs-Effekt. Trade-off: deaktiviert Safari-Private-Browsing (bei `.specific` aber ohnehin auch der Fall — *jede* Policy ≠ `.none` tut das).
3. **Statische Bundle-Liste > Backend-Endpoint** (§7). Datensparsam, offline-fähig, kein neuer Server-Code.
4. **Das eigentliche Loch zuerst schließen:** Der höhere Hebel ist **FC-Widerruf-Erkennung**. `AuthorizationCenter.shared.authorizationStatus` bei jedem App-Foreground prüfen → wenn nicht mehr `.approved` → aggressiver Nudge + Backend-Flag (analog `recoveringFromBypass`). Das adressiert §4/§6 direkt und ist mehr wert als Layer 2.
5. **Risiken:**
   - **Falsches Sicherheitsgefühl** beim User („ich bin geschützt") — bei einem DiGA-/Suchthilfe-Produkt ein ernstes Thema. UI-Wording streng.
   - **Private-Browsing-Deaktivierung in Safari** als Nebeneffekt — für die Zielgruppe vermutlich erwünscht, aber dokumentieren.
   - **FC-Auth-Verbrauch:** Layer 2 hängt an derselben FC-Authorization wie `denyAppRemoval`. Kein neues Risiko, aber: ein einziger Widerruf killt beides.
6. **Aufwand grob:** klein. ~0,5–1 Tag. Ein neuer `AsyncFunction` im bestehenden Swift-Modul, ein gebundeltes JSON, JS-Bridge-Methode, ein Aufruf im Aktivierungs-Flow. Kein neues Entitlement, kein Plugin-Eingriff (FC + App-Group sind schon da).

### Wann es sich doch lohnt
Wenn das Team es als **bewusste, klein gehaltene Zusatz-Härtung** akzeptiert (nicht als Fallback-Versprechen) und parallel die FC-Widerruf-Erkennung baut — dann ist der Aufwand niedrig genug, dass „nice to have" vertretbar ist. Als *alleinige* Layer-2-Strategie: zu schwach.

---

## Implementierungs-Skizze (KEIN Code — nur Plan)

**Betroffene Dateien:**
| Datei | Änderung |
|---|---|
| `modules/rebreak-protection/ios/RebreakProtectionModule.swift` | Neuer `AsyncFunction("activateWebContentFilter")`: Land empfangen, Top-50 setzen via `ManagedSettingsStore(named: MS_STORE_NAME).webContent.blockedByFilter = .auto(domains)` bzw. `.specific(domains)`. Setzt voraus, dass FC bereits authorisiert ist. `disable` (Z. 368) erweitern: `webContent.blockedByFilter = .none` bzw. via `clearAllSettings()` (deckt es schon ab — prüfen). |
| `modules/rebreak-protection/src/RebreakProtection.types.ts` | Typ für die neue Bridge-Methode + ggf. `webContentFilter`-Layer in `DeviceLayers`. |
| `modules/rebreak-protection/src/RebreakProtectionModule.ts` | Bridge-Deklaration. |
| `modules/rebreak-protection/src/RebreakProtectionModule.web.ts` | No-op-Stub. |
| `lib/protection.ts` | Orchestrierung: nach `activateFamilyControls()` zusätzlich `activateWebContentFilter({country})` aufrufen; `getDeviceState`/`getCombinedState` ggf. um Layer-Status erweitern. |
| `assets/`-Bundle (neu) | `gambling-top50-by-country.json` (`{ "DE": [...], "GB": [...], ... }`). Mit der App gebundelt. |
| Backend | **Nicht nötig** bei Bundle-Variante. Nur falls Server-Updates gewünscht: `/api/url-filter/`-Bereich um `top50.json?country=` ergänzen. |
| `plugins/with-rebreak-protection-ios.js` | **Keine Änderung** — FC-Entitlement + App-Group sind bereits vorhanden. |

**Grobe Schritte:**
1. Top-50-Gambling-Domains pro Zielland kuratieren (DE/GB/FR zuerst — die i18n-Sprachen des Repos). Quelle: vorhandene 208k-PIR-Liste nach Land/Traffic-Rang filtern (`backend/scripts/generate-pir-input.ts` ist verwandter Kontext).
2. Land via `Locale.current.region` bestimmen (optional Profil-Override); JSON-Lookup.
3. `WebDomain`-Set bauen, `blockedByFilter` setzen (`.auto` empfohlen — Adult-Content gratis mit). FC-Auth-Status vorher prüfen.
4. Disable-Pfad: sicherstellen, dass `clearAllSettings()` auch `webContent` zurücksetzt (vermutlich ja — verifizieren), sonst explizit `.none` setzen.
5. **Empirisch testen auf iPhone (iOS 26, kein Simulator — MEMORY-Regel):** (a) blockt es eine Top-50-Domain in Safari? (b) auch in Chrome iOS? (c) verträgt es sich sichtbar mit aktivem NEURLFilter? (d) was passiert bei FC-Widerruf?
6. UI-Wording festlegen — **kein** „Fallback"-Versprechen.

---

## Offene Entscheidungen für den User

1. **Layer 2 überhaupt bauen?** Ehrliche Empfehlung: nur als bewusst kleingehaltene Zusatzhärtung — und nur **zusammen mit** FC-Widerruf-Erkennung. Als alleiniger „Fallback" zu schwach. → Deine Entscheidung.
2. **`.auto` (Adult-Content gratis mit) oder `.specific` (nur Gambling)?** `.auto` empfohlen, falls Adult-Content-Block für die Zielgruppe ok ist.
3. **Bundle-JSON oder Backend-Endpoint** für die Top-50? Empfehlung Bundle (datensparsam, offline). Backend nur falls Updates ohne App-Release wichtig.
4. **Welche Länder zum Start?** Vorschlag: DE, GB, FR (= vorhandene App-Sprachen).
5. **Landbestimmung:** Device-Region (empfohlen) vs. optionales Profil-Feld?
6. **Soll stattdessen/zuerst die FC-Widerruf-Erkennung gebaut werden?** Das ist nach dieser Recherche der höhere Hebel — und schließt das größere Loch, gegen das Layer 2 nicht hilft.

---

## Quellen
- [WebContentSettings | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings)
- [WebContentSettings.FilterPolicy | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy)
- [blockedByFilter | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/blockedbyfilter-swift.property) — *50-Domain-Limit + Private-Browsing-Hinweis*
- [FilterPolicy.specific(_:) | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/specific(_:))
- [FilterPolicy.auto(_:except:) | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/auto(_:except:))
- [ShieldSettings.webDomainCategories | Apple Developer](https://developer.apple.com/documentation/managedsettings/shieldsettings/webdomaincategories-swift.property) — *Shielding = UI-Overlay, kein Traffic-Block*
- [NEURLFilterManager | Apple Developer](https://developer.apple.com/documentation/NetworkExtension/NEURLFilterManager)
- [Filter and tunnel network traffic with NetworkExtension — WWDC25](https://developer.apple.com/videos/play/wwdc2025/234/)
- [iOS 26 Network Extension URL Filtering — dev.to/arshtechpro](https://dev.to/arshtechpro/ios-26-network-extension-url-filtering-revolution-for-enterprise-and-consumer-apps-40ij)
- [AuthorizationCenter | Apple Developer](https://developer.apple.com/documentation/familycontrols/authorizationcenter)
- [A Developer's Guide to Apple's Screen Time APIs — Medium](https://medium.com/@juliusbrussee/a-developers-guide-to-apple-s-screen-time-apis-familycontrols-managedsettings-deviceactivity-e660147367d7) — *Restrictions fallen lautlos bei Auth-Widerruf*
