Post not yet marked as solved
Hi, I have been working on some kind of network filtering app for iOS using Content Filter Provider. And I have stored rules for each domain.
As of right now, I use UserDefaults with my app's bundle suite to store and observe rules. I have also read this documentation page for UserDefaults link.
Is it okay to use UserDefaults in my case, if I have rules added/modified dynamically as the flow is intercepted, or should I pick some other approach like Core Data, SwiftData, etc.?
Thank you!
Post not yet marked as solved
On the [documentation page](Implement a completely custom DNS proxying protocol) it says
For example, a DNS proxy provider might:
Implement a completely custom DNS proxying protocol
I would like to add some filtering logic to the NEDNSProxyProvider (for example, return nxdomain if the flow is not passing the filtering process). Is it possible to implement with NEDNSProxyProvider? It also says that
func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool
from NEDNSProxyProvider returns a Boolean value set to true if the proxy implementation decides to handle the flow, or false if it instead decides to terminate the flow link. Does it mean that the filtering logic could be added here by just returning false for the flows that are not matching the rules?
Because I first tried to handle UDP flows like this in handleNewFlow(_ flow: NEAppProxyUDPFlow) function and form my own packets in connection.transferData, by first passing empty Data object and then by setting RCODE to 3, which is supposedly a nxdomain response code. However, both implementations didn't work: even though I was getting logs about handling failure, the flow was still able to go through.
try await flow.open(withLocalEndpoint: flow.localEndpoint as? NWHostEndpoint)
let datagrams = try await flow.readDatagrams()
let results = try await datagrams.parallelMap {
let connection = try DatagramConnection($0)
return try await connection.transferData()
}
try await flow.writeDatagrams(results)
flow.closeReadWithError(nil)
flow.closeWriteWithError(nil)
I am new to NEDNSProxyProvider and my networking knowledge is on a pretty basic level, so I would be very grateful to hear any suggestions. Thank you!
I am trying to add DNSProxy configuration using .mobileconfig and MDM on supervised device. I have Content Filter payload in the same configuration file that works as expected, however I was unable to start my DNSProxy. My app has 3 extension targets for Filter Data/Control Providers and DNSProxy extension.
Here is my DNSProxy payload:
<dict>
<key>AppBundleIdentifier</key>
<string>my.app.bundle.id</string>
<key>PayloadDescription</key>
<string>Configures DNS proxy network extension</string>
<key>PayloadDisplayName</key>
<string>DNS Proxy</string>
<key>PayloadIdentifier</key>
<string>com.apple.dnsProxy.managed.AEE249BB-4F44-4ED9-912B-6A70CC0E01B6</string>
<key>PayloadType</key>
<string>com.apple.dnsProxy.managed</string>
<key>PayloadUUID</key>
<string>AEE249BB-4F44-4ED9-912B-6A70CC0E01B6</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ProviderBundleIdentifier</key>
<string>my.app.bundle.id.DNS-Proxy-Extension</string>
</dict>
Any thoughts on what I might be doing wrong?
Post not yet marked as solved
Hi, I was working on some new filtering logic for my Content Filter that I would like to add. It involves making requests to remote DNS resolvers. Is it possible to use it within sync override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict of the NEFilterDataProvider?
As of right now, I have a concept working in Command Line Tool and playground, however, when I try to add working module to the main project, it's not working (connections are not loading).
Function that makes requests to the servers: In this function I use DispatchGroup and notify for non-main queue
@available(iOS 12, *)
public class NetworkService {
private let nonMainQueue: DispatchQueue = DispatchQueue(label: "non-main-queue")
func isBlocked(hostname: String, completion: @escaping (Bool) -> Void) {
var isAnyBlocked = false
let group = DispatchGroup()
for server in servers {
group.enter()
let endpoint = NWEndpoint.Host(server)
query(host: endpoint, domain: hostname, queue: .global()) { response, error in
defer {
group.leave()
}
/*
* some code that determines the filtering logic
* if condition is true => isAnyBlocked = true & return
*/
}
}
group.notify(queue: nonMainQueue) {
completion(isAnyBlocked)
}
}
}
And, for example, in playground Semaphores make it work as expected, but the same approach doesn't work with the NEFilterDataProvider
playground code sample
let hostname = "google.com"
func returnResponse() -> String {
var result = ""
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
NetworkService.isBlocked(hostname: hostname) { isBlocked in
result = isBlocked ? "blocked" : "allowed"
semaphore.signal()
}
}
semaphore.wait()
return result
}
print(returnResponse())
Output: allowed
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.
Post not yet marked as solved
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
}
}