feat(protection): Layer 2 in FC-Aktivierung einhaengen + URLFilter-Extension-Version

Layer-2-webContent-Filter laeuft jetzt automatisch bei activateFamilyControls/
activate mit (Helper applyWebContentLayer). URLFilterExtension CFBundleVersion/
ShortVersion an die App angeglichen. Apple-DTS-Report einreichfertig.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-21 19:20:13 +02:00
parent 627ddce995
commit ad51ce5099
3 changed files with 79 additions and 41 deletions

View File

@ -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.

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>0.3.4</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>13</string>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>

View File

@ -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 `<user token>` 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://<our-host>")!, // no trailing slash
pirPrivacyPassIssuerURL: URL(string: "https://<our-host>")!, // 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: "<user token>",
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://<our-host>
pirPrivacyPassIssuerURL = https://<our-host>
pirServerURL = https://pir.staging.rebreak.org
pirPrivacyPassIssuerURL = https://pir.staging.rebreak.org
AuthenticationToken = <user token>
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://<our-host>/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
`<user token>` with the real values first.