# 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) // blockt genau diese Domains case auto(Set = [], except: Set = []) // System blockt ADULT CONTENT // (+ optional zusätzliche Domains, - Ausnahmen) case all(except: Set) // blockt ALLES außer Ausnahmen (Allowlist-Modus) ``` ### `WebDomain` Token-Typ, der eine Domain repräsentiert (Initialisierung typ. mit `WebDomain(domain: "bet365.com")`). `Set` 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*