NEFilterManager and NEDNSProxyManager

Hi, I am working on the app for some basic concept, I would like to intercept both DNS and IP connections. I succeeded in intercepting DNS using NEDNSProxyProvider, however I seem to have some troubles with IPConnections using NEFilterDataProvider.

First thing, I have three targets in my app. For some reason, when I run DNS Proxy Extension target it doesn't ask me to choose the app for target run, and after the application if launched, it correctly intercepts DNS traffic and inits NEDNSProxyManager

ps: all logs are correctly displayed for NEFilterDataProvider

However, when I try to run Filter Data Extension target with Content Filter capability, it asks me to choose the app for run. Even tho I checked the Build Settings and those are identical to DNS Proxy Extension target.

And finally, when I run main target it still inits NEDNSProxyManager properly and the NEFilterManager returns this warning

-[NEFilterManager saveToPreferencesWithCompletionHandler:]_block_invoke_3: failed to save the new configuration: (null)

I tried to log the configuration and compared to some code samples, but I can't identify the problem.

I'd very grateful if somebody could suggest where the problems might be (targets builds difference & NEFilterManager config)

I will attach a sample of code where I add configuration to my NEFilterManager


// MARK: - FilterDataManager
final class FilterDataManager: NSObject, ObservableObject {
    
    
    // MARK: - Properties
    private let manager = NEFilterManager.shared()
    private let filterName = "Data Filter"
    @Published
    private(set) var isEnabled: Bool? = nil
    
    // MARK: - Singleton
    static let shared = FilterDataManager()
    
    // Cancellables set
    private var subs: Set<AnyCancellable> = []

    private override init() {
        super.init()
        enable()
        
        manager.isEnabledPublisher()
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] isEnabled in
                self?.setIsEnabled(isEnabled)
            })
            .store(in: &subs)
    }
    
    // MARK: - Filter Configurations
    func enable() {
        manager.updateConfiguration { [unowned self] manager in
            manager.localizedDescription = filterName
            manager.providerConfiguration = createFilterProviderConfiguration()
            manager.isEnabled = true
        } completion: { result in
            guard case let .failure(error) = result else { return }
            Log("Filter enable failed: \(error)", prefix: "[Filter]")
        }
    }
    
    private func createFilterProviderConfiguration() -> NEFilterProviderConfiguration {
        let configuration = NEFilterProviderConfiguration()
        configuration.organization = "***"
        configuration.filterBrowsers = true
        configuration.filterSockets = true
        return configuration
    }

    func disable() {
        Log("Will disable filter", prefix: "[Filter]")
        manager.updateConfiguration { manager in
            manager.isEnabled = false
        } completion: { result in
            guard case let .failure(error) = result else { return }
            Log("Filter enable failed: \(error)")
        }
    }
    
    private func setIsEnabled(_ isEnabled: Bool) {
        guard self.isEnabled != isEnabled else { return }
        self.isEnabled = isEnabled
        Log("Filter \(isEnabled ? "enabled" : "disabled")", prefix: "[Filter]")
    }
}

```Swift

extension NEFilterManager {
    // MARK: - NEFilterManager config update
    func updateConfiguration(_ body: @escaping (NEFilterManager) -> Void, completion: @escaping (Result<Void, Error>) -> Void) {
        loadFromPreferences { [unowned self] error in
            if let error,
                let filterError = FilterError(error) {
                completion(.failure(filterError))
                return
            }
            
            body(self)
            
            saveToPreferences { (error) in
                if let error, 
                    let filterError = FilterError(error) {
                    completion(.failure(filterError))
                    return
                }
                completion(.success(()))
            }
        }
    }
    
    // MARK: - Publisher enabling
    func isEnabledPublisher() -> AnyPublisher<Bool, Never> {
        NotificationCenter.default
            .publisher(for: NSNotification.Name.NEFilterConfigurationDidChange)
            .compactMap { [weak self] notification in
                guard let self else { return nil }
                return self.isEnabled
            }
            .eraseToAnyPublisher()
    }
}

// MARK: - FilterError
@available(iOS 8.0, *)
enum FilterError: Error {
    /// The Filter configuration is invalid
    case configurationInvalid
    /// The Filter configuration is not enabled.
    case configurationDisabled
    /// The Filter configuration needs to be loaded.
    case configurationStale
    /// The Filter configuration cannot be removed.
    case configurationCannotBeRemoved
    /// Permission denied to modify the configuration
    case configurationPermissionDenied
    /// Internal error occurred while managing the configuration
    case configurationInternalError
    case unknown
    
    init?(_ error: Error) {
        switch error {
        case let error as NSError:
            switch NEFilterManagerError(rawValue: error.code) {
            case .configurationInvalid:
                self = .configurationInvalid
                return
            case .configurationDisabled:
                self = .configurationDisabled
                return
            case .configurationStale:
                self = .configurationStale
                return
            case .configurationCannotBeRemoved:
                self = .configurationCannotBeRemoved
                return
            case .some(.configurationPermissionDenied):
                self = .configurationPermissionDenied
                return
            case .some(.configurationInternalError):
                self = .configurationInternalError
                return
            case .none:
                return nil
            @unknown default:
                break
            }
        default:
            break
        }
        assertionFailure("Invalid error \(error)")
        return nil
    }
}

Replies

UPDATE:

I figured out how to set up the target schemes for extensions, but the problem with the init of the NEFilterManager remains. I tried to use com.apple.security.get-task-allow key in entitlements to test it on the device without supervised mode, however, that didn't work. I still receive -[NEFilterManager saveToPreferencesWithCompletionHandler:]_block_invoke_3: failed to save the new configuration: (null) in the console.

I'd be grateful to hear if anybody has some thoughts or suggestions about that.

You seems to be missing a piece here: On iOS and its child platforms [1] a content filter needs two app extensions:

  • A filter data provider which has access to raw networking bytes but runs in a very strict sandbox.

  • A filter control provider that’s the opposite.

The docs cover this in some detail.

Looking at your first screen shot it seems that you only have one filter target, and that’s not going to work.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] So, everything except macOS.

  • Oh, I see now. I'll try this. Thank you a lot!

Add a Comment

I added a new target for NEFilterControlExtension however if I run main target I still receive the same message: -[NEFilterManager saveToPreferencesWithCompletionHandler:]_block_invoke_3: failed to save the new configuration: (null)

I left NEFilterManager without changes. For both NEFilterControlProvider and NEFilterDataProvider return .drop() in handleNewFlow to drop every flow in order to test the POC. Indeed, not sure why but when I run my dns proxy target, it works as expected and I have all the information in debug session but when I run one of the Content Filter Extension targets looks like it's not able to attach

Not sure what causes the problem at this point. Would be grateful for any thoughts or suggestions!