Unable to start NEContentFilter on iOS

Previously, I added a post about the problem with NEFilterManager configuration. Since then, I explored the SimpleTunnel example project and I changed NEFilterManager setup to my own and it still worked well. Now, I simplified the code to just test that Content Filter is starting, but unfortunately it's displayed as 'Invalid' in System Settings.

Here are the samples of my code, but I still don't understand what I am doing wrong here. I would be very grateful for any help.

Test View

struct ContentFilterView: View {
    @ObservedObject var vm = FilterManager.shared
    @State private var toggleState = false 
    
    var body: some View {
        VStack {
            Toggle("Filter Status", isOn: $toggleState)
                .padding()
                .onChange(of: toggleState) { status in
                    vm.setupFilter(with: status)
                }
        }
        .onAppear {
            vm.loadFilterConfiguration { success in
                if success {
                    print("loadFilterConfiguration is successful")
                    toggleState = vm.isEnabled ?? false
                    print("NEFilterManager config: \(String(describing: NEFilterManager.shared().providerConfiguration?.organization))")
                } else {
                    print("loadFilterConfiguration failed")
                    toggleState = false
                }
            }
        }
    }
}

FilterManager

class FilterManager: ObservableObject {
    @Published
    private(set) var isEnabled: Bool? = nil
    
    // MARK: - Properties
    private let manager = NEFilterManager.shared()
    private var subs = Set<AnyCancellable>()
    static let shared = FilterManager()
    
    private init() {
        manager.isEnabledPublisher()
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] isEnabled in
                self?.setIsEnabled(isEnabled)
            })
            .store(in: &subs)
    }
    
    public func setupFilter(with status: Bool) {
        if status && manager.providerConfiguration == nil {
            let newConfiguration = NEFilterProviderConfiguration()
            newConfiguration.username = "TestUser"
            newConfiguration.organization = "Test Inc."
            newConfiguration.filterBrowsers = true
            newConfiguration.filterSockets = true
            manager.providerConfiguration = newConfiguration
            
            print("manager configuration saved successfully: \(String(describing: manager.providerConfiguration?.organization))")
        }
        
        manager.isEnabled = status
        
        manager.saveToPreferences { [weak self] error in
            if let error {
                print("Failed to save the filter configuration: \(error.localizedDescription)")
                self?.isEnabled = false
                return
            }
        }
    }
    
    public func loadFilterConfiguration(withCompletion completion: @escaping (Bool) -> Void) {
        manager.loadFromPreferences { error in
            if let loadError = error {
                print("Failed to load the filter configuration: \(loadError)")
                completion(false)
            } else {
                completion(true)
            }
        }
    }
    
    private func setIsEnabled(_ isEnabled: Bool) {
        guard self.isEnabled != isEnabled else { return }
        self.isEnabled = isEnabled
        print("NEFilter \(isEnabled ? "enabled" : "disabled")")
    }
}


extension NEFilterManager {
    // 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()
    }
}

NEFilterDataProvider


class FilterDataProvider: NEFilterDataProvider {
    
    // MARK: - Properties
    /// A record of where in a particular flow the filter is looking.
    var flowOffSetMapping = [URL: Int]()
    /// The list of flows that should be blocked after fetching new rules.
    var blockNeedRules = [String]()
    /// The list of flows that should be allowed after fetching new rules.
    var allowNeedRules = [String]()
    
    override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
        Log("Will handle filter flow \(flow)", prefix: "[Filter Data]")
        return .drop()
    }
}

NEFilterControlProvider is the same as SimpleTunnel example project NEFilterControlProvider implementation.

I also followed suggested steps mentioned in this post but it didn't seem to help.

Accepted Reply

I used MDM solution for Content Filter. Now app works as expected. Thank you for your help!

Replies

Now, I simplified the code to just test that Content Filter is starting, but unfortunately it's displayed as 'Invalid' in System Settings.

Lemme see if I understand this correctly. You had working code, and you tweaked it, and now the code no longer works. Is that right?

If so, I recommend that undo your tweaks until it works again. Debugging these problems is hard, so it’s best avoid that when you can.

Share and Enjoy

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

  • No, I tried to use my NEFilterManger for the SimpleTunnel project Content Filter and it works properly, however, I can't make it work in my project on SwiftUI.

Add a Comment

No, I tried to use my NEFilterManger for the SimpleTunnel project Content Filter and it works properly, however, I can't make it work in my project on SwiftUI.

OK, but that’s pretty much the same thing. You have a working example and a failing example and you’re trying to figure out why the latter is failing. In that case I usually mutate the failing example by removing stuff, mutate the working example by adding stuff, and then see where they meet in the middle.


In this case, though, you almost certainly have a packaging problem. That is, your filter is packaged incorrectly such that the system won’t recognise it. My standard check list for such things is:

  • Check the location and name of the appex.

  • Check that its bundle ID is an immediate child of your container app.

  • Check that its Info.plist has the right values for the NSExtension property.

  • Specifically, check NSExtensionPrincipalClass is correct.

  • Check that the appex deployment target matches the container app’s deployment target.

  • Check the NE entitlement on both the appex and its container app.

As you’re creating a content filter, repeat this for both the filter data and filter control providers.

And if you’re unsure as to what something should look like, check the corresponding value in your working case.

Also, check the built binary, not your project. After all, the binary is the thing that the OS is looking at. If you see a value in the built binary that doesn’t match what you think it should be based on the project, you have a build problem.

Share and Enjoy

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

Add a Comment

I decided to use configuration profile with MDM for Content Filter. I read this post about enabling plug-in content filter using configuration profile. I thought that I followed all the steps but for some reason I am unable to start my app's content filter.

If you can’t get this to work programmatically, it’s likely that there’s something borked in the way that your NE content filter appex is packaged. If so, switching to a configuration profile won’t help and just complicates things. I recommend that you get it working programmatically first.

Share and Enjoy

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

I tried to use configuration profile on the new project so it wasn't the initial project, but it didn't work. I only added two extension targets for Filter Data Provider and Filter Control Provider, and set handleNewFlow in Filter Data Provider to .drop() by default. I am just trying to test the concept of Content Filter, so that I could move on to implementing some custom filtering logic.

Not sure if that's the right approach but by just adding files with Extension Targets' principle classes to the main app's target compile sources, the filter starts and works as expected. Could you give me some advice on this?

I used MDM solution for Content Filter. Now app works as expected. Thank you for your help!