diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift index 8187c1d..b97bb3b 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift @@ -260,6 +260,7 @@ public class RebreakProtectionModule: Module { AsyncFunction("activateFamilyControls") { () async -> [String: Any] in var error: String? = nil var enabled = false + var webContentCount = 0 if #available(iOS 16.0, *) { // Retry-Loop: FamilyControls XPC-Daemon kann auf den ersten Call // mit NSCocoaErrorDomain:4099 antworten (Communication-Failure, oft @@ -283,6 +284,12 @@ public class RebreakProtectionModule: Module { if !lockActive { error = "denyAppRemoval_not_active" } + // Layer 2 — der webContent-Filter ist Teil des FC-Schutzes: + // greift mit, sobald FC aktiv ist (stilles Sicherheitsnetz für + // den Fall, dass der User Layer 1 / VPN abschaltet). Best-effort + // — ein Fehlschlag hier kippt die FC-Aktivierung NICHT. + let wc = Self.applyWebContentLayer() + webContentCount = wc.count } else { enabled = false } @@ -304,7 +311,7 @@ public class RebreakProtectionModule: Module { } else { error = "iOS 16+ required for FamilyControls" } - var result: [String: Any] = ["enabled": enabled] + var result: [String: Any] = ["enabled": enabled, "webContentDomains": webContentCount] if let error = error { result["error"] = error } return result } @@ -367,6 +374,8 @@ public class RebreakProtectionModule: Module { store.application.denyAppRemoval = true store.application.denyAppInstallation = false SharedLogStore.append("🔒 denyAppRemoval = true") + // Layer 2 — webContent-Filter als Teil des FC-Schutzes mit-aktivieren. + _ = Self.applyWebContentLayer() } return [ @@ -457,36 +466,13 @@ public class RebreakProtectionModule: Module { ] } - // 1) Land bestimmen — Locale.current.region (Fallback: erstes Element, - // sonst "DE"). region ist iOS 16+; .regionCode als Pre-16-Fallback. - if let region = Locale.current.region?.identifier, !region.isEmpty { - resolvedRegion = region.uppercased() - } else if let region = Locale.current.regionCode, !region.isEmpty { - resolvedRegion = region.uppercased() - } - SharedLogStore.append("🌍 [applyWebContentFilter] region=\(resolvedRegion)") - - // 2) Domain-Liste für das Land aus dem gebündelten JSON laden. - let domains = Self.loadWebContentDomains(forRegion: resolvedRegion) - if domains.isEmpty { - SharedLogStore.append("⚠️ [applyWebContentFilter] keine Domains für \(resolvedRegion)") - return [ - "enabled": false, - "appliedCount": 0, - "region": resolvedRegion, - "error": "no_domains_for_region", - ] - } - - // 3) blockedByFilter setzen. .auto(_, except:) blockt die gelisteten - // Domains PLUS systemseitig Adult-Content gratis mit. Hartlimit 50. - let webDomains = Set(domains.prefix(WEBCONTENT_MAX_DOMAINS).map { WebDomain(domain: $0) }) - appliedCount = webDomains.count - - let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) - store.webContent.blockedByFilter = .auto(webDomains, except: []) - enabled = true - SharedLogStore.append("🛡️ [applyWebContentFilter] webContent.blockedByFilter=.auto — \(appliedCount) Domains (\(resolvedRegion))") + // Gemeinsame Layer-2-Logik (applyWebContentLayer) — exakt dieselbe, + // die auch bei der FC-Aktivierung mitläuft. + let wc = Self.applyWebContentLayer() + enabled = wc.enabled + appliedCount = wc.count + resolvedRegion = wc.region + if !enabled { error = "no_domains_for_region" } } else { error = "iOS 16+ required for webContent filter" SharedLogStore.append("❌ [applyWebContentFilter] \(error!)") @@ -806,6 +792,34 @@ public class RebreakProtectionModule: Module { // ─── Helpers ──────────────────────────────────────────────────────────────── + /// Wendet den Layer-2-webContent-Filter an: Land bestimmen → Domain-Liste + /// laden → `webContent.blockedByFilter = .auto`. Gemeinsame Logik für die + /// explizite `applyWebContentFilter`-Function UND die FC-Aktivierung + /// (`activateFamilyControls`/`activate`) — Layer 2 ist Teil des FC-Schutzes + /// und läuft mit, sobald Family Controls aktiv ist. Voraussetzung: FC + /// authorisiert (der Aufrufer prüft das). + @available(iOS 16.0, *) + private static func applyWebContentLayer() -> (enabled: Bool, count: Int, region: String) { + var resolvedRegion = WEBCONTENT_FALLBACK_REGION + if let region = Locale.current.region?.identifier, !region.isEmpty { + resolvedRegion = region.uppercased() + } else if let region = Locale.current.regionCode, !region.isEmpty { + resolvedRegion = region.uppercased() + } + let domains = loadWebContentDomains(forRegion: resolvedRegion) + guard !domains.isEmpty else { + SharedLogStore.append("⚠️ [webContent] keine Domains für \(resolvedRegion)") + return (false, 0, resolvedRegion) + } + // .auto(_, except:) blockt die gelisteten Domains PLUS systemseitig + // Adult-Content. Hartlimit 50 Domains. + let webDomains = Set(domains.prefix(WEBCONTENT_MAX_DOMAINS).map { WebDomain(domain: $0) }) + let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) + store.webContent.blockedByFilter = .auto(webDomains, except: []) + SharedLogStore.append("🛡️ [webContent] blockedByFilter=.auto — \(webDomains.count) Domains (\(resolvedRegion))") + return (true, webDomains.count, resolvedRegion) + } + /// Lädt die kuratierte Gambling-Domain-Liste für ein Land aus dem /// gebündelten JSON (gambling-domains.json). Das JSON wird von der Podspec /// als RebreakProtectionResources.bundle ins App-Bundle gepackt. diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist index e9d62e9..69b3caa 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + 0.3.4 CFBundleVersion - 1 + 13 EXAppExtensionAttributes EXExtensionPointIdentifier diff --git a/ops/pir-server/apple-dts-neurlfilter-report.md b/ops/pir-server/apple-dts-neurlfilter-report.md index 08fc483..942020b 100644 --- a/ops/pir-server/apple-dts-neurlfilter-report.md +++ b/ops/pir-server/apple-dts-neurlfilter-report.md @@ -1,8 +1,9 @@ # Apple DTS / Feedback — NEURLFilter `serverSetupIncomplete` on a development-signed build -**Zweck:** Vorlage für einen Apple Developer Technical Support Incident (TSI) bzw. einen -Beitrag im Developer-Forum-Thread 791352 ("Getting a basic URL Filter to work"). -Stand: 2026-05-21. Einfach Inhalt anpassen/kopieren und einreichen. +**Zweck:** Einreichfertiger Bericht für (1) einen Feedback-Assistant-Bug-Report und +(2) einen Apple Developer Technical Support Incident (TSI). Stand: 2026-05-21. +Vor dem Einreichen die Platzhalter `pir.staging.rebreak.org` und `` durch die echten +Werte ersetzen. Einreich-Anleitung siehe Abschnitt „How to submit" am Ende. --- @@ -37,10 +38,12 @@ calling `/config`, and what "server setup" the framework considers incomplete. ```swift try manager.setConfiguration( - pirServerURL: URL(string: "https://")!, // no trailing slash - pirPrivacyPassIssuerURL: URL(string: "https://")!, // same host serves both + pirServerURL: URL(string: "https://pir.staging.rebreak.org")!, // no trailing slash + pirPrivacyPassIssuerURL: URL(string: "https://pir.staging.rebreak.org")!, // same host serves both pirAuthenticationToken: "", controlProviderBundleIdentifier: "org.rebreak.app.URLFilterExtension") +manager.localizedDescription = "ReBreak URL Filter" // added for WWDC-sample parity +manager.prefilterFetchInterval = 86400 // added for WWDC-sample parity manager.isEnabled = true manager.shouldFailClosed = true try await manager.saveToPreferences() @@ -54,8 +57,8 @@ urlFilter = { FailClosed = YES AppBundleIdentifier = org.rebreak.app ControlProviderBundleIdentifier = org.rebreak.app.URLFilterExtension - pirServerURL = https:// - pirPrivacyPassIssuerURL = https:// + pirServerURL = https://pir.staging.rebreak.org + pirPrivacyPassIssuerURL = https://pir.staging.rebreak.org AuthenticationToken = pirPrivacyProxyFailOpen = NO pirSkipRegistration = NO @@ -77,7 +80,7 @@ urlFilter = { ``` ciphermld Request to fetchConfigs has started for useCases ['org.rebreak.app.url.filtering'] -ciphermld error Failed to fetch configs. URL: https:///config Status Code: 401 +ciphermld error Failed to fetch configs. URL: https://pir.staging.rebreak.org/config Status Code: 401 ciphermld error queryStatus(for:options:) threw an error: CipherML.CipherMLError.serverError("{\"error\":{\"message\":\"No private token\"}}") neagent requestStatusForClientConfig… XPC complete, error: com.apple.CipherML Code=1800 @@ -151,3 +154,24 @@ Standard `apple/pir-service-example` `PIRService` as the PIR + Privacy Pass issu a development-signed app with an `NEURLFilterControlProvider` extension, and the `setConfiguration` call shown above. Happy to provide a sysdiagnose and full `neagent`/`ciphermld` logs. + +--- + +## How to submit (internal note — not part of the report text) + +File in this order: + +1. **Feedback Assistant** (`feedbackassistant.apple.com`) — file as a bug against + *NetworkExtension / NEURLFilter*. Attach a **sysdiagnose** taken right after + reproducing, plus the full `neagent` + `com.apple.cipherml` Console export. + Note the resulting **FB number**. +2. **Developer Technical Support Incident** (developer.apple.com → Account → + Support → *Technical Support* / „Request Technical Support") — paste this + report, reference the **FB number** from step 1, and ask the four + „Questions for Apple". A DTS engineer replies directly (effectively the + written line to Apple — there is no plain support-email channel for this). + Costs one Technical Support Incident (2 included per membership year). + +**Attach:** sysdiagnose (`.tar.gz`), the full `neagent`/`ciphermld` log export, +and — if asked — a minimal sample project. Replace `pir.staging.rebreak.org` and +`` with the real values first.