I have added a Network Extension to my iOS project to use the WireGuard library. Everything was working fine up to Xcode 16, but after updating, I’m facing a build issue.
The build fails with the following error:
No such file or directory: '@rpath/WireGuardNetworkExtensioniOS.debug.dylib'
I haven’t explicitly added any .dylib to my project. The Network Extension target builds and runs fine on Xcode 16.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I have written the Transparent App Proxy and can capture the network flow and send it to my local server. I want to avoid any processing on the traffic outgoing from my server and establish a connection with a remote server, but instead of connecting to the remote server, it again gets captured and sent back to my local server.
I am not getting any clue on how to ignore these flows originating from my server.
Any pointers, API, or mechanisms that will help me?
Topic:
App & System Services
SubTopic:
Networking
Tags:
Network Extension
System Extensions
Endpoint Security
I'm working on a Network Extension using NEDNSProxyProvider to inspect DNS traffic. However, I've run into a couple of issues:
DNS Proxy is not capturing traffic when a public DNS (like 8.8.8.8 or 1.1.1.1) is manually configured in the WiFi settings. It seems like the system bypasses the proxy in this case. Is this expected behavior? Is there a way to force DNS traffic through the proxy even if a public DNS is set?
Using DNS Proxy and DNS Settings simultaneously doesn't work. Is there a known limitation or a correct way to combine these?
How to set DNS or DNSSettings using DNSProxy?
import NetworkExtension
import SystemExtensions
import SwiftUI
protocol DNSProxyManagerDelegate {
func managerStateDidChange(_ manager: DNSProxyManager)
}
class DNSProxyManager: NSObject {
private let manager = NEDNSProxyManager.shared()
var delegate: DNSProxyManagerDelegate?
private(set) var isEnabled: Bool = false {
didSet {
delegate?.managerStateDidChange(self)
}
}
var completion: (() -> Void)?
override init() {
super.init()
self.load()
}
func toggle() {
isEnabled ? disable() : start()
}
private func start() {
let request = OSSystemExtensionRequest
.activationRequest(forExtensionWithIdentifier: Constants.extensionBundleID,
queue: DispatchQueue.main)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
log.info("Submitted extension activation request")
}
private func enable() {
update {
self.manager.localizedDescription = "DNS Proxy"
let proto = NEDNSProxyProviderProtocol()
proto.providerBundleIdentifier = Constants.extensionBundleID
self.manager.providerProtocol = proto
self.manager.isEnabled = true
}
}
private func disable() {
update {
self.manager.isEnabled = false
}
}
private func remove() {
update {
self.manager.removeFromPreferences { _ in
self.isEnabled = self.manager.isEnabled
}
}
}
private func update(_ body: @escaping () -> Void) {
self.manager.loadFromPreferences { (error) in
if let error = error {
log.error("Failed to load DNS manager: \(error)")
return
}
self.manager.saveToPreferences { (error) in
if let error = error {
return
}
log.info("Saved DNS manager")
self.isEnabled = self.manager.isEnabled
}
}
}
private func load() {
manager.loadFromPreferences { error in
guard error == nil else { return }
self.isEnabled = self.manager.isEnabled
}
}
}
extension DNSProxyManager: OSSystemExtensionRequestDelegate {
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
log.info("Extension activation request needs user approval")
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
log.error("Extension activation request failed: \(error)")
}
func request(_ request: OSSystemExtensionRequest, foundProperties properties: [OSSystemExtensionProperties]) {
log.info("Extension activation request found properties: \(properties)")
}
func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
guard result == .completed else {
log.error("Unexpected result \(result.description) for system extension request")
return
}
log.info("Extension activation request did finish with result: \(result.description)")
enable()
}
func request(_ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
log.info("Existing extension willt be replaced: \(existing.bundleIdentifier) -> \(ext.bundleIdentifier)")
return .replace
}
}
import NetworkExtension
class DNSProxyProvider: NEDNSProxyProvider {
var handlers: [String: FlowHandler] = [:]
var isReady = false
let queue = DispatchQueue(label: "DNSProxyProvider")
override func startProxy(options:[String: Any]? = nil, completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
override func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool {
let id = shortUUID()
handlers[id] = FlowHandler(flow: flow, remoteEndpoint: remoteEndpoint, id: id, delegate: self)
return true
}
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
return false
}
}
class FlowHandler {
let id: String
let flow: NEAppProxyUDPFlow
let remoteEndpoint: NWHostEndpoint
let delegate: FlowHandlerDelegate
private var connections: [String: RemoteConnection] = [:]
private var pendingPacketsByDomain: [String: [(packet: Data, endpoint: NWEndpoint, uniqueID: String, timestamp: Date)]] = [:]
private let packetQueue = DispatchQueue(label: "com.flowhandler.packetQueue")
init(flow: NEAppProxyUDPFlow, remoteEndpoint: NWEndpoint, id: String, delegate: FlowHandlerDelegate) {
log.info("Flow received for \(id) flow: \(String(describing: flow))")
self.flow = flow
self.remoteEndpoint = remoteEndpoint as! NWHostEndpoint
self.id = id
self.delegate = delegate
defer { start() }
}
deinit {
closeAll(nil)
}
func start() {
flow.open(withLocalEndpoint: flow.localEndpoint as? NWHostEndpoint) { error in
if let error = error {
self.delegate.flowClosed(self)
return
}
self.readFromFlow()
}
}
func readFromFlow() {
self.flow.readDatagrams { packets, endpoint, error in
if let error = error {
self.closeAll(error)
return
}
guard let packets = packets, let endpoints = endpoint, !packets.isEmpty, !endpoints.isEmpty else {
self.closeAll(nil)
return
}
self.processFlowPackets(packets, endpoints)
self.readFromFlow()
}
}
}
Any insights or suggestions would be greatly appreciated. Thanks!
PLATFORM AND VERSION
macOS
Development environment: Xcode 15.0, macOS 15.0.1
Run-time configuration: macOS 15.0.1
DESCRIPTION OF PROBLEM
We are currently developing a macOS app using the NEFilterDataProvider in the Network Extension framework, and we've encountered an issue regarding hostname resolution that we would like your guidance on.
In our implementation, we need to drop network flows based on the hostname. The app successfully receives the remoteHostname or remoteEndpoint.hostname for browsers such as Safari and Mozilla Firefox. However, for other browsers like Chrome, Opera Mini, Arc, Brave, and Edge, we only receive the IP address instead of the hostname.
We are particularly looking for a way to retrieve the hostname for all browsers to apply our filtering logic consistently. Could you please advise whether there is any additional configuration or API we can use to ensure that we receive hostnames for these browsers as well? Alternatively, is this a limitation of the browsers themselves, and should we expect to only receive IP addresses for certain cases?
STEPS TO REPRODUCE
For Chrome, Brave, Edge, and Arc browsers you won't receive the hostname in NEFilterFlow.
Using the same sample project provided in WWDC 2019 https://developer.apple.com/documentation/networkextension/filtering_network_traffic
import NetworkExtension
import os.log
import Network
/**
The FilterDataProvider class handles connections that match the installed rules by prompting
the user to allow or deny the connections.
*/
class FilterDataProvider: NEFilterDataProvider {
// MARK: NEFilterDataProvider
override func startFilter(completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
guard let socketFlow = flow as? NEFilterSocketFlow,
let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint,
let localEndpoint = socketFlow.localEndpoint as? NWHostEndpoint else {
return .allow()
}
var hostName: String? = nil
// Attempt to use the URL host for native apps (e.g., Safari)
if let url = socketFlow.url {
hostName = url.host
os_log("URL-based Host: %@", hostName ?? "No host found")
}
// Fallback: Use remote hostname for third-party browsers like Chrome
if hostName == nil {
if #available(macOS 11.0, *), let remoteHostname = socketFlow.remoteHostname {
hostName = remoteHostname
os_log("Remote Hostname: %@", hostName ?? "No hostname found")
} else {
hostName = remoteEndpoint.hostname
os_log("IP-based Hostname: %@", hostName ?? "No hostname found")
}
}
let flowInfo = [
FlowInfoKey.localPort.rawValue: localEndpoint.port,
FlowInfoKey.remoteAddress.rawValue: remoteEndpoint.hostname,
FlowInfoKey.hostName.rawValue: hostName ?? "No host found"
]
// Ask the app to prompt the user
let prompted = IPCConnection.shared.promptUser(aboutFlow: flowInfo, rawFlow: flow) { allow in
let userVerdict: NEFilterNewFlowVerdict = allow ? .allow() : .drop()
self.resumeFlow(flow, with: userVerdict)
}
guard prompted else {
return .allow()
}
return .pause()
}
// Helper function to check if a string is an IP address
func isIPAddress(_ hostName: String) -> Bool {
var sin = sockaddr_in()
var sin6 = sockaddr_in6()
if hostName.withCString({ inet_pton(AF_INET, $0, &sin.sin_addr) }) == 1 {
return true
} else if hostName.withCString({ inet_pton(AF_INET6, $0, &sin6.sin6_addr) }) == 1 {
return true
}
return false
}
}
PLATFORM AND VERSION
iOS
Development environment: Xcode 16.0, macOS 15.0.1
Run-time configuration: iOS 17.5.1
DESCRIPTION OF PROBLEM
We are working on an iOS application that utilizes the NEFilterDataProvider class from the Network Extension framework to control network flows. However, we are encountering an issue where network flows are not being detected as expected.
Here are the details of our setup:
We are using the NEFilterDataProvider class to filter network traffic in our app.
The filtering setup works well for certain flows/apps, but we cannot detect Facebook network flows as intended.
The app is correctly configured with the necessary entitlements, and we have set up the required App Groups and Network Extension capabilities.
We would like to request guidance on how to troubleshoot or resolve this issue. Could you provide insights on:
Whether there are any known limitations or conditions under which network flows may not be detected by NEFilterDataProvider.
Recommendations for additional debugging techniques to better understand why some flows might not be captured.
Recommendations for additional code to be added to detect some flows that might not be captured.
Any specific scenarios or configurations that might be causing this issue in iOS.
STEPS TO CHECK
Replace below code in FilterDataProvider.
Try running the app and set debugger in FilterDataProvider.
Launch Facebook app.
You will observe that no NEFilterFlow is detected in handleNewFlow for actions such as posts, reels, etc.
import NetworkExtension
class FilterDataProvider: NEFilterDataProvider {
let blockedDomains = [
"facebook.com"
]
override func startFilter(completionHandler: @escaping (Error?) -> Void) {
// Perform any necessary setup here.
DNSLogger.shared.log(message: "Filter started")
completionHandler(nil)
}
override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
// Perform any necessary cleanup here.
DNSLogger.shared.log(message: "Filter stopped with reason: \(reason)")
completionHandler()
}
override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
var url: URL?
if let urlFlow = flow as? NEFilterBrowserFlow {
url = urlFlow.url
}
else {
let urlFlow = flow as? NEFilterSocketFlow
url = urlFlow?.url
}
guard let hostName = url?.host else { return .allow() }
DNSLogger.shared.log(message: "Domain reveived: \(hostName)")
return .allow()
}
// Handle inbound data (data received from the network)
override func handleInboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict {
DNSLogger.shared.log(message: "Inbound data: \(readBytes)")
return .needRules()
}
// Handle outbound data (data sent to the network)
override func handleOutboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict {
// Inspect or modify outbound data if needed
// For example, you could log the data or modify it before sending
DNSLogger.shared.log(message: "Outbound data: \(readBytes)")
return .needRules()
}
override func handleRemediation(for flow: NEFilterFlow) -> NEFilterRemediationVerdict {
return .needRules()
}
override func handleRulesChanged() {
// Handle any changes to the rules
}
}