- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet) - DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked - Group chat unchanged - Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state - deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle - NEXT_RELEASE.md: DM reactions release note - Includes other staged work across binder-mac, marketing, ops/mdm, ios/
19 KiB
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:
- Es gibt KEINE Gambling-
WebDomainCategory.WebContentSettings.FilterPolicykennt nur.none / .specific / .auto / .all..autoblockt ausschließlich Adult Content (Apple-Wortlaut). Eine Gambling-Kategorie existiert nirgends — also bleibt nur die manuelle 50er-Liste. - 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:). - 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 viaManagedSettingsStore().webContent. - Relevante Property:
„The current policy for filtering websites." Defaultvar blockedByFilter: WebContentSettings.FilterPolicy?nil(kein Effekt).
WebContentSettings.FilterPolicy — alle vier Cases (Apple-Doc, verifiziert)
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.
FilterPolicyhat keinen kategoriebasierten Case. Nur Adult-Content (.auto) ist kategorieähnlich, und das ist hardcoded auf Adult — nicht Gambling, nicht erweiterbar.WebDomainCategory/webDomainCategoriesexistiert — aber gehört zuShieldSettings, nicht zumwebContent-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
.specificund.auto. Harte Obergrenze, keine Konfigurations-Option. - Damit ist eine 208k-Domain-Liste (wie bei NEURLFilter/PIR) über
webContentprinzipiell 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.blockedByFilterwirkt auf Safari — Apple erwähnt explizit den Nebeneffekt „Setting any filter policy besides.nonewill 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
blockedByFilterauch 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 gebautenactivateFamilyControls-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=1im 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 → Bildschirmzeitwiderrufen — 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, siehewith-rebreak-protection-ios.jsZ. 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) undshouldFailClosed(Bool — beitruewird Traffic geblockt, wenn der Filter nicht erreichbar ist; das Repo setztshouldFailClosed = true,RebreakProtectionModule.swiftZ. 110). Beides setzt die App, nicht der User.- System-Toggle für den User? NetworkExtension-Filter erscheinen üblicherweise unter
Einstellungen → Allgemein → VPN & Geräteverwaltungbzw. als Filter-Eintrag, den der User abschalten/löschen kann. Genau dieses Verhalten ist im Repo-Code bereits sichtbar:resetUrlFilterexistiert nur, weil der User „Nicht erlauben" tippen kann und iOS den Denied-State cached (protection.tsZ. 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
Bildschirmzeitdie FC-Authorization widerruft (oder Bildschirmzeit ganz deaktiviert), fallendenyAppRemovalund 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:
- Device-Region (
Locale.current.region/NSLocale.countryCode) — lokal, kein Netz, datensparsam. Empfehlung. Schwäche: Region ≠ Aufenthaltsort. - 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. - IP-Geo — am genausten für „wo bin ich", aber Netzabhängig + datenschutzkritisch (Glücksspiel-Stigma, DiGA). Nicht empfohlen.
- Device-Region (
- 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 eintop50?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
webContentweiterhin 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
- Gambling-Kategorie statt 50er-Liste: auf iOS nicht möglich (§1). Entfällt.
.automitnehmen — kostenlos: Statt.specific(top50)→.auto([...top50...])..autoblockt 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.specificaber ohnehin auch der Fall — jede Policy ≠.nonetut das).- Statische Bundle-Liste > Backend-Endpoint (§7). Datensparsam, offline-fähig, kein neuer Server-Code.
- Das eigentliche Loch zuerst schließen: Der höhere Hebel ist FC-Widerruf-Erkennung.
AuthorizationCenter.shared.authorizationStatusbei jedem App-Foreground prüfen → wenn nicht mehr.approved→ aggressiver Nudge + Backend-Flag (analogrecoveringFromBypass). Das adressiert §4/§6 direkt und ist mehr wert als Layer 2. - 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.
- Aufwand grob: klein. ~0,5–1 Tag. Ein neuer
AsyncFunctionim 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:
- 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.tsist verwandter Kontext). - Land via
Locale.current.regionbestimmen (optional Profil-Override); JSON-Lookup. WebDomain-Set bauen,blockedByFiltersetzen (.autoempfohlen — Adult-Content gratis mit). FC-Auth-Status vorher prüfen.- Disable-Pfad: sicherstellen, dass
clearAllSettings()auchwebContentzurücksetzt (vermutlich ja — verifizieren), sonst explizit.nonesetzen. - 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?
- UI-Wording festlegen — kein „Fallback"-Versprechen.
Offene Entscheidungen für den User
- 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.
.auto(Adult-Content gratis mit) oder.specific(nur Gambling)?.autoempfohlen, falls Adult-Content-Block für die Zielgruppe ok ist.- Bundle-JSON oder Backend-Endpoint für die Top-50? Empfehlung Bundle (datensparsam, offline). Backend nur falls Updates ohne App-Release wichtig.
- Welche Länder zum Start? Vorschlag: DE, GB, FR (= vorhandene App-Sprachen).
- Landbestimmung: Device-Region (empfohlen) vs. optionales Profil-Feld?
- 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
- WebContentSettings.FilterPolicy | Apple Developer
- blockedByFilter | Apple Developer — 50-Domain-Limit + Private-Browsing-Hinweis
- FilterPolicy.specific(_:) | Apple Developer
- FilterPolicy.auto(_:except:) | Apple Developer
- ShieldSettings.webDomainCategories | Apple Developer — Shielding = UI-Overlay, kein Traffic-Block
- NEURLFilterManager | Apple Developer
- Filter and tunnel network traffic with NetworkExtension — WWDC25
- iOS 26 Network Extension URL Filtering — dev.to/arshtechpro
- AuthorizationCenter | Apple Developer
- A Developer's Guide to Apple's Screen Time APIs — Medium — Restrictions fallen lautlos bei Auth-Widerruf