Tags
NetworkExtension, NEFilterManager, Content-Filter, TestFlight, iOS, Swift, Entitlements, App-Groups
Problem Summary
I'm experiencing a critical issue with a Network Extension Content Filter that works perfectly in debug mode but fails in TestFlight with:
``` -[NEFilterManager saveToPreferencesWithCompletionHandler:]_block_invoke_3: failed to save the new configuration: Error Domain=NEFilterErrorDomain Code=5 "permission denied" UserInfo={NSLocalizedDescription=permission denied} ```
This is blocking completion of a client project and requires urgent assistance.
Environment
• Platform: iOS • Minimum Deployment: iOS 16.0 • Development: Xcode with Flutter integration • Testing Method: TestFlight (production build) • Works in: Debug mode (direct device deployment) • Fails in: TestFlight builds
What Works vs. What Fails
WORKS IN DEBUG MODE (✓): • Network extension installs successfully • System permission dialog appears correctly • Filter starts and blocks content as expected • All domain management functions work
FAILS IN TESTFLIGHT (✗): • System permission dialog never appears • NEFilterManager.saveToPreferences fails immediately • Error Code 5: "permission denied" • Cannot set up the filter at all
Implementation Details
ARCHITECTURE: The implementation consists of:
- Main App (Flutter) - handles UI and configuration
- Network Extension Plugin (Swift) - bridges Flutter to NetworkExtension framework
- FilterDataProvider (Swift) - implements content filtering logic
- App Group - shared storage for configuration (group.app.v1.dev0)
PERMISSION REQUEST CODE:
```swift func requestPermissions(completion: @escaping (Result<Bool, Error>) -> Void) { NEFilterManager.shared().loadFromPreferences { error in if let error = error { DispatchQueue.main.async { completion(.failure(error)) } return }
let config = NEFilterProviderConfiguration()
config.organization = "Testing
config.filterBrowsers = true
config.filterSockets = true
let manager = NEFilterManager.shared()
manager.providerConfiguration = config
manager.localizedDescription = " Screen Shield"
manager.isEnabled = true
manager.saveToPreferences { saveError in
DispatchQueue.main.async {
completion(saveError == nil ? .success(true) : .failure(saveError!))
}
}
}
} ```
EXTENSION INFO.PLIST:
```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSExtension</key> <dict> <key>NSExtensionAttributes</key> <dict> <key>com.apple.networkextension.filter</key> <dict> <key>FilterBrowsers</key> <true/> <key>FilterPackets</key> <false/> <key>FilterSockets</key> <true/> </dict> </dict> <key>NSExtensionPointIdentifier</key> <string>com.apple.networkextension.filter-data</string> <key>NSExtensionPrincipalClass</key> <string>$(PRODUCT_MODULE_NAME).FilterDataProvider</string> </dict> </dict> </plist> ```
ENTITLEMENTS:
```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.developer.networking.networkextension</key> <array> <string>content-filter-provider</string> </array> <key>com.apple.security.application-groups</key> <array> <string>group.app.v1.dev0</string> </array> </dict> </plist> ```
What I've Already Tried
-
VERIFIED ENTITLEMENTS (✓) • Both main app and extension have matching entitlements • App Group identifier is identical in both targets • content-filter-provider capability is set
-
CHECKED PROVISIONING PROFILES (✓) • Created distribution provisioning profiles with Network Extension capability • App Group is included in all profiles • All capabilities are enabled in App Store Connect
-
VERIFIED APP GROUP CONFIGURATION (✓) • App Group exists in Apple Developer portal • Added to both App ID and Extension App ID • Regenerated provisioning profiles after adding
-
CODE SIGNING (✓) • Both targets build and sign successfully • No code signing errors during archive • Extension is embedded in main app bundle
-
TESTFLIGHT REQUIREMENTS (✓) • Using distribution certificate for archive • Archive validation passes without warnings • Upload to TestFlight successful
-
BUILD CONFIGURATION (✓) • Minimum deployment target is iOS 16.0 for both targets • Extension deployment target matches main app • All required frameworks are properly linked
Specific Questions
-
Permission Dialog: In debug mode, the system permission dialog appears. In TestFlight, it never shows. Is there a TestFlight-specific permission issue with Network Extensions?
-
Entitlements Propagation: Are there known issues with entitlements not being properly included in TestFlight builds despite being present in the archive?
-
Distribution vs Development: Are there any differences in how Network Extensions are authorized between development builds and distribution builds?
Additional Context
• The extension works flawlessly when deployed directly from Xcode • No console errors or warnings in TestFlight build • UserDefaults(suiteName:) successfully accesses the App Group in both modes • Filter logic itself is tested and working (confirmed in debug mode) • This is urgent as it's blocking client project completion I tested this with both adult acc and also with child app
What I Need
- Specific steps to diagnose why NEFilterManager.saveToPreferences returns Code 5 in TestFlight
- Confirmation of whether Network Extension entitlements require special handling for TestFlight
- Any known issues or workarounds for this specific error in production builds
- Debugging techniques that work in TestFlight environment (since console logs are limited)
System Information
• Xcode Version: Latest stable • iOS Target: 16.0+ • Swift Version: 5.0 • Framework: Flutter with native iOS plugin • Build Type: Distribution (Ad Hoc via TestFlight)
Thank you for any assistance. This is blocking critical client work and I need to resolve it urgently.
See this thread, and specifically this post, which explains why this works in during development but fails when you deploy.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"