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.