Hi, I’ve encountered an issue with Safari’s behavior when Prevent Cross-Site Tracking is enabled in iOS, related to DNS filtering via an implemented NEDNSProxyProvider. Here’s a step-by-step breakdown:
In Safari, when attempting to query a blocked domain (according to the filtering policy of the NEDNSProxyProvider), the page is blocked as expected.
Closing Safari without closing the tab with the blocked domain.
Reopening Safari – Expected result: The page remains blocked; Actual result: The page loads and bypasses the NEDNSProxyProvider (no logs are received for this flow).
Tapping the refresh button causes the page to be blocked, as the DNS Proxy Provider intercepts the new request.
Note: This issue is only reproducible in general tabs in Safari. In private tabs, a fresh DNS query is generated each time, and the blocking behavior works as expected. I also tested Google Chrome, where the domain is blocked consistently.
I attempted to filter this issue via Content Filter, but the only connection received by NEFilterDataProvider is for com.apple.Safari.SearchHelper with ssl.gstatic.com.
Could you advise on how to handle this behaviour? Would be grateful to hear any ideas
Post
Replies
Boosts
Views
Activity
Hi, I have been working on the app that implements DNS Proxy Extension for a while now, and after a couple builds to TestFlight I noticed that I got a couple crashes that seem to be triggered by EXC_BREAKPOINT (SIGTRAP)
After some investigation, it was found that crashes are connected to CFNetwork framework. So, I decided to additionally look into memory issues, but I found the app has no obvious memory leaks, no memory regression (within recommended 25%, actual value is at 20% as of right now), but the app still uses 11mb of memory footprint and most of it (6.5 mb is Swift metadata).
At this point, not sure what's triggering those crashes, but I noticed that sometimes app will return message like this to the console (this example is for PostHog api that I use in the app):
Task <0ABDCF4A-9653-4583-9150-EC11D852CA9E>.<1> finished with error [18 446 744 073 709 550 613] Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found." UserInfo={_kCFStreamErrorCodeKey=8, NSUnderlyingError=0x1072df0f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1003 "(null)" UserInfo={_kCFStreamErrorDomainKey=12, _kCFStreamErrorCodeKey=8, _NSURLErrorNWResolutionReportKey=Resolved 0 endpoints in 2ms using unknown from cache, _NSURLErrorNWPathKey=satisfied (Path is satisfied), interface: en0[802.11], ipv4, dns, uses wifi}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalUploadTask <0ABDCF4A-9653-4583-9150-EC11D852CA9E>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalUploadTask <0ABDCF4A-9653-4583-9150-EC11D852CA9E>.<1>"
), NSLocalizedDescription=A server with the specified hostname could not be found., NSErrorFailingURLStringKey=https://us.i.posthog.com/batch, NSErrorFailingURLKey=https://us.i.posthog.com/batch, _kCFStreamErrorDomainKey=12}
If DNS Proxy Provider uses custom DoH server for resolving packets, could the cache policy for URLSession be a reason?
I had a couple other ideas (HTTP3 failure, CFNetwork core issues like described here) but not sure if they are valid
Would be grateful if someone could give me a hint of what I should look at
Hey, I have been working on the app that implements both Content Filter Providers and DNS Proxy for custom network security app. However, I would like to display traffic logs from Content Filter. What's the best way to do that?
I know that it works with UserDefaults under shared container with App Group. But I am not sure that it's the best approach for storing data that is constantly changing. I also tried to use CoreData with:
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
But I receive error that "The file couldn’t be saved because you don’t have permission., ["reason": No permissions to create file; code = 1]" because my FilterDataProvider has access to CoreData model.
Hi, I have been working on the app with NE Filter Providers for a while now and it seems to work well. However, unlike Content Filter Providers, DNS Proxy is invalidated when device is inactive state.
It shows status "Invalid" for just a couple seconds before to changes to "Starting" and eventually "Running". That's not a major issue, but I would like to know what's causing this behaviour and if there is a way to fix it. I am using custom DoH in my DNS Proxy for flows proxying. And if the server times out to respond, app sends rcode 5 (Refused) for requested flow. At the same time, app shouldn't crash because all errors are handled appropriately.
Would be very grateful for any thoughts, thank you!
Hi, I have been working on the app with NE Filter Providers for a while now and it seems to work well. However, unlike Content Filter Providers, DNS Proxy is invalidated when device is inactive state.
It shows status "Invalid" for just a couple seconds before to changes to "Starting" and eventually "Running". That's not a major issue, but I would like to know what's causing this behaviour and if there is a way to fix it. I am using custom DoH in my DNS Proxy for flows proxying. And if the server times out to respond, app sends rcode 5 (Refused) for requested flow. At the same time, app shouldn't crash because all errors are handled appropriately.
Would be very grateful for any thoughts, thank you!
Hi! I have been working on one idea for a while but can't figure out the proper way to do that. My app includes Content Filter and DNSProxy providers for filtering logic. And for the NEFilterSocketFlow everything works well, because the connection is first handled from DNSProxy and if it's blocked, NEFilterDataProvider returns datagrams that I wrote from DNSProxy (I return nxdomain). However, for NEFilterBrowserFlow it doesn't work, because webkit generated flows are for some reason intercepted by Content Filter first and at the time when the flow is checked for rules, there're none yet as DNSProxy didn't handle connection yet.
So the app returns the following behaviour:
In case the requested domain is not filtered by DNSProxy, the user is able to visit requested page, but if it's filtered, the flow just freezes and the page will never load for user. But I wanted to add proper handling and display block page.
In case I am using some third-party apps for testing like ICS Dig, filtered domains return nxdomain properly.
Not sure if there's a way to achieve desired result, but would be very grateful for any suggestions
Hi, I have been trying to add Settings Bundle to my app that utilizes DNS Proxy and Content Filter, however, I noticed some weird behavior. I worked with Settings Bundle before, but the initial implementation didn't work for some reason.
So, I simplified it to just one toggle switch and tried again.
Initially, I had an observer adding in the init of my SettingsBundleService class and I was using a shared instance in DNS Proxy target, as a result, SettingsBundleService was observing changes but would always return false. But when I tried to use a shared instance from the main target, it worked just fine.
public final class SettingsBundleService {
static let shared = SettingsBundleService()
private (set) var test: Bool = false
init() {
registerSettingsBundle()
NotificationCenter.default.addObserver(self, selector: #selector(defaultsChanged), name: UserDefaults.didChangeNotification, object: nil)
defaultsChanged()
Logger.statistics.log("[SettingsBundleService] – added Settings Bundle observing")
}
private func registerSettingsBundle(){
let appDefaults = [String: AnyObject]()
UserDefaults.standard.register(defaults: appDefaults)
}
@objc
private func defaultsChanged(){
self.test = UserDefaults.standard.bool(forKey: "enable_feature_preference")
Logger.statistics.log("[SettingsBundleService] – changed: \(test)")
}
deinit {
NotificationCenter.default.removeObserver(self)
Logger.statistics.log("[SettingsBundleService] – removed Settings Bundle observing")
}
Could somebody advise me on what I am doing wrong here?
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!
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!
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
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?
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.
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
}
}