General:
Forums subtopic: App & System Services > Networking
DevForums tag: Network Extension
Network Extension framework documentation
Routing your VPN network traffic article
Filtering traffic by URL sample code
Filtering Network Traffic sample code
TN3120 Expected use cases for Network Extension packet tunnel providers technote
TN3134 Network Extension provider deployment technote
TN3165 Packet Filter is not API technote
Network Extension and VPN Glossary forums post
Debugging a Network Extension Provider forums post
Exporting a Developer ID Network Extension forums post
Network Extension vs ad hoc techniques on macOS forums post
Network Extension Provider Packaging forums post
NWEndpoint History and Advice forums post
Extra-ordinary Networking forums post
Wi-Fi management:
Wi-Fi Fundamentals forums post
TN3111 iOS Wi-Fi API overview technote
How to modernize your captive network developer news post
iOS Network Signal Strength forums post
See also Networking Resources.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Network Extension
RSS for tagCustomize and extend the core networking features of iOS, iPad OS, and macOS using Network Extension.
Posts under Network Extension tag
200 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Following previous question here :https://developer.apple.com/forums/thread/801397, I've decided to move my VPN implementation using NEPacketTunnelProvider on a dedicated networkExtension.
My extension receives packets using readPacketsWithCompletionHandler and forwards them immediately to a daemon through a shared memory ring buffer with Mach port signaling. The daemon then encapsulates the packets with our VPN protocol and sends them over a UDP socket.
I'm seeing significant throughput degradation, much higher than the tunnel overhead itself. On our side, the IPC path supports parallel handling, but I'm not not sure whether the provider has any internal limitation that prevents packets from being processed in parallel. The tunnel protocol requires packet ordering, but preparation can be done in parallel if the provider allows it.
Is there any inherent constraint in NEPacketTunnelProvider that prevents concurrent packet handling, or any recommended approach to improve throughput in this model? For comparison, when I create a utun interface manually with ifconfig and route traffic through it, I observe performance that is about four times faster.
we use the api as
NEHotspotConfigurationManager.shared.apply(hotspotConfig)
to join a wifi, but we find that in in iphone 17+, some user report the time to join wifi is very slow
the full code as
let hotspotConfig = NEHotspotConfiguration(ssid: sSSID, passphrase: sPassword, isWEP: false)
hotspotConfig.joinOnce = bJoinOnce
if #available(iOS 13.0, *) {
hotspotConfig.hidden = true
}
NEHotspotConfigurationManager.shared.apply(hotspotConfig) { [weak self] (error) in
guard let self else {
return
}
if let error = error {
log.i("connectSSID Error while configuring WiFi: \(error.localizedDescription)")
if error.localizedDescription.contains("already associated") {
log.i("connectSSID Already connected to this WiFi.")
result(["status": 0])
} else {
result(["status": 0])
}
} else {
log.i("connectSSID Successfully connected to WiFi network \(sSSID)")
result(["status": 1])
}
}
Normally it might only take 5-10 seconds, but on the iPhone 17+ it might take 20-30 seconds.
Hello,
I’m developing a macOS application signed with a Developer ID (outside the App Store) that includes a Network Extension.
The app has been successfully notarized, and the network filter is registered, but the Network Extension itself remains inactive — it does not install or run properly.
It seems that the issue might be related to the entitlements configuration between the container app and the Network Extension target.
Could you please provide a detailed checklist for:
The required entitlements and configurations for the container app, and
The required entitlements and configurations for the Network Extension target?
Additionally, are there any specific Xcode settings that are mandatory for the Network Extension to be properly installed and activated on macOS when distributed via Developer ID?
Thank you in advance for your help.
Hello,
I’m developing a macOS application signed with a Developer ID (outside the App Store) that includes a Network Extension.
The app has been successfully notarized, and the network filter is registered, but the Network Extension itself remains inactive — it does not install or run properly.
It seems that the issue might be related to the entitlements configuration between the container app and the Network Extension target.
Could you please provide a detailed checklist for:
1.The required entitlements and configurations for the container app, and
2.The required entitlements and configurations for the Network Extension target?
Additionally, are there any specific Xcode settings that are mandatory for the Network Extension to be properly installed and activated on macOS when distributed via Developer ID?
Thank you in advance for your help.
I'm currently testing URLFilter for use in a macOS product. After calling loadFromPreferences, I set the following configuration parameters:
pirServerURL = URL(string: "http://localhost:8080")!
pirAuthenticationToken = "AAAA"
controlProviderBundleIdentifier = "{extension app bundle identifier}"
However, when I call saveToPreferences, I get an Invalid Configuration error.
Is there a way to determine which parameter is invalid or incorrectly set?
Also, I would appreciate any macOS-specific examples of using NEURLFilterManager, as most of the documentation I’ve found seems to focus on iOS.
Thank you.
Title:
iPhone 17 Wi-Fi connection via NEBOTspotConfigurationManager::applyConfiguration is significantly slower compared to other models
Description:
When using the NEBOTspotConfigurationManager::applyConfiguration API to connect to a Wi-Fi network, the connection process on iPhone 17 is extremely slow compared to other iPhone models.
For example, in one test case:
The API call to connect to Wi-Fi (LRA-AN00%6149%HonorConnect) was initiated at 16:16:29.
However, the Association Request was not actually initiated until 16:16:58.
During this ~29-second delay, the device appears to be scanning before starting the association process.
This issue is specific to iPhone 17 — the same code and network environment do not exhibit this delay on other iPhone models.
Steps to Reproduce:
On an iPhone 17, call NEBOTspotConfigurationManager::applyConfiguration to connect to a known Wi-Fi network.
Observe the timestamps between API invocation and the start of the Association Request.
Compare with the same process on other iPhone models.
Expected Result:
The Association Request should start almost immediately after the API call, similar to other iPhone models.
Actual Result:
On iPhone 17, there is a ~29-second delay between API call and Association Request initiation, during which the device appears to be scanning.
Impact:
This delay affects user experience and connection performance when using programmatic Wi-Fi configuration on iPhone 17.
Environment:
Device: iPhone 17
iOS Version:26.0.1
API: NEBOTspotConfigurationManager::applyConfiguration
Network: WPA2-Personal
IOS.txt
I’m building a Personal VPN app (non-MDM) that uses a NEPacketTunnelProvider extension for content filtering and blocking.
When configuring the VPN locally using NETunnelProviderManager.saveToPreferences, the call fails with:
Error Domain=NEConfigurationErrorDomain Code=10 "permission denied"
Error Domain=NEVPNErrorDomain Code=5 "permission denied"
The system does prompt for VPN permission (“Would Like to Add VPN Configurations”), but the error still occurs after the user allows it.
Setup:
• Main App ID – com.promisecouple.app
• Extension ID – com.promisecouple.app.PromiseVPN
• Capabilities – App Group + Personal VPN + Network Extensions
• Main app entitlements:
com.apple.developer.networking.vpn.api = allow-vpn
com.apple.developer.networking.networkextension = packet-tunnel-provider
• Extension entitlements: same + shared App Group
Problem:
• If I remove the networkextension entitlement, the app runs locally without the Code 5 error.
• But App Store Connect then rejects the build with:
Missing Entitlement: The bundle 'Promise.app' is missing entitlement 'com.apple.developer.networking.networkextension'.
Question:
What is the correct entitlement configuration for a Personal VPN app using NEPacketTunnelProvider (non-MDM)?
Is com.apple.developer.networking.networkextension required on the main app or only on the extension?
Why does including it cause saveToPreferences → Code 5/10 “permission denied” on device?
Environment:
Xcode 26.1 (17B55), iOS 17.3+ on physical device (non-MDM)
Both provisioning profiles and certificates are valid.
We’re implementing VPN application using the WireGuard protocol and aiming to support both split-tunnel and per-app VPN configurations. Each mode works correctly on its own: per-app VPN functions well when configured with a full tunnel and split-tunnel works as expected when per-app is disabled.
However, combining both configurations leads to issues. Specifically, the routing table is not set up properly, resulting in traffic that should not be routed through the tunnel is routed through the tunnel.
Detailed description:
Through our backend, we are pushing these two plist files to the iPad one after the other:
VPN config with allowed IPs 1.1.1.1/32
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Inc//DTD PLIST 1.0//EN" http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
<key>PayloadUUID</key>
<string>3fd861df-c917-4716-97e5-f5e96452436a</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadOrganization</key>
<string>someorganization</string>
<key>PayloadIdentifier</key>
<string>config.11ff5059-369f-4a71-afea-d5fdbfa99c91</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadDisplayName</key>
<string> test</string>
<key>PayloadDescription</key>
<string>(Version 13) </string>
<key>PayloadRemovalDisallowed</key>
<true />
<key>PayloadContent</key>
<array>
<dict>
<key>VPN</key>
<dict>
<key>AuthenticationMethod</key>
<string>Password</string>
<key>ProviderType</key>
<string>packet-tunnel</string>
<key>OnDemandUserOverrideDisabled</key>
<integer>1</integer>
<key>RemoteAddress</key>
<string>172.17.28.1:51820</string>
<key>OnDemandEnabled</key>
<integer>1</integer>
<key>OnDemandRules</key>
<array>
<dict>
<key>Action</key>
<string>Connect</string>
</dict>
</array>
<key>ProviderBundleIdentifier</key>
<string>some.bundle.id.network-extension</string>
</dict>
<key>VPNSubType</key>
<string>some.bundle.id</string>
<key>VPNType</key>
<string>VPN</string>
<key>VPNUUID</key>
<string>d2773557-b535-414f-968a-5447d9c02d52</string>
<key>OnDemandMatchAppEnabled</key>
<true />
<key>VendorConfig</key>
<dict>
<key>VPNConfig</key>
<string>
Some custom configuration here
</string>
</dict>
<key>UserDefinedName</key>
<string>TestVPNServerrra</string>
<key>PayloadType</key>
<string>com.apple.vpn.managed.applayer</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>vpn.5e6b56be-a4bb-41a5-949e-4e8195a83f0f</string>
<key>PayloadUUID</key>
<string>9bebe6e2-dbef-4849-a1fb-3cca37221116</string>
<key>PayloadDisplayName</key>
<string>Vpn</string>
<key>PayloadDescription</key>
<string>Configures VPN settings</string>
<key>PayloadOrganization</key>
<string>someorganization</string>
</dict>
</array>
</dict>
</plist>
Command to set up per-app with Chrome browser
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Inc//DTD PLIST 1.0//EN" http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>Settings</key>
<array>
<dict>
<key>Identifier</key>
<string>com.google.chrome.ios</string>
<key>Attributes</key>
<dict>
<key>VPNUUID</key>
<string>d2773557-b535-414f-968a-5447d9c02d52</string>
<key>TapToPayScreenLock</key>
<false />
<key>Removable</key>
<true />
</dict>
<key>Item</key>
<string>ApplicationAttributes</string>
</dict>
</array>
<key>RequestType</key>
<string>Settings</string>
</dict>
<key>CommandUUID</key>
<string>17ce3e19-35ef-4dbc-83d9-4ca2735ac430</string>
</dict>
</plist>
From the log we see that our VPN application set up allowed IP 1.1.1.1 via NEIPv4Settings.includedRoutes but system routing all of the Chrome browser traffic through our application.
Is this expected Apple iOS behavior, or are we misconfiguring the profiles?
We have an iOS companion app that talks to our IoT device over the device’s own Wi‑Fi network (often with no internet). The app performs bi-directional, safety-critical duties over that link.
We use an NEAppPushProvider extension so the handset can keep exchanging data while the UI is backgrounded. During testing we noticed that if the user backgrounds the app (still connected to the device’s Wi‑Fi) and opens Safari, the extension’s stop is invoked with NEProviderStopReason.unrecoverableNetworkChange / noNetworkAvailable, and iOS tears the extension down. Until the system restarts the extension (e.g. the user foregrounds our app again), the app cannot send/receive its safety-critical data.
Questions:
Is there a supported way to stop a safety-critical NEAppPushProvider from being terminated in this “background app → open Safari” scenario when the device remains on the same Wi‑Fi network (possibly without internet)?
If not, is NEAppPushProvider the correct extension type for an always-on local-network use case like this, or is there another API we should be using?
For safety-critical applications, can Apple grant entitlements/exemptions so the system does not terminate the extension when the user switches apps but stays on the local Wi‑Fi?
Any guidance on the expected lifecycle or alternative patterns for safety-critical local connectivity would be greatly appreciated.
I haven’t been able to get this to work at any level! I’m running into multiple issues, any light shed on any of these would be nice:
I can’t implement a bloom filter that produces the same output as can be found in the SimpleURLFilter sample project, after following the textual description of it that’s available in the documentation. No clue what my implementation is doing wrong, and because of the nature of hashing, there is no way to know. Specifically:
The web is full of implementations of FNV-1a and MurmurHash3, and they all produce different hashes for the same input. Can we get the proper hashes for some sample strings, so we know which is the “correct” one?
Similarly, different implementations use different encodings for the strings to hash. Which should we use here?
The formulas for numberOfBits and numberOfHashes give Doubles and assign them to Ints. It seems we should do this conversing by rounding them, is this correct?
Can we get a sample correct value for the combined hash, so we can verify our implementations against it?
Or ignoring all of the above, can we have the actual code instead of a textual description of it? 😓
I managed to get Settings to register my first attempt at this extension in beta 1. Now, in beta 2, any other project (including the sample code) will redirect to Settings, show the Allow/Deny message box, I tap Allow, and then nothing happens. This must be a bug, right?
Whenever I try to enable the only extension that Settings accepted (by setting its isEnabled to true), its status goes to .stopped and the error is, of course, .unknown. How do I debug this?
While the extension is .stopped, ALL URL LOADS are blocked on the device. Is this to be expected? (shouldFailClosed is set to false)
Is there any way to manually reload the bloom filter? My app ships blocklist updates with background push, so it would be wasteful to fetch the filter at a fixed interval. If so, can we opt out of the periodic fetch altogether?
I initially believed the API to be near useless because I didn’t know of its “fuzzy matching” capabilities, which I’ve discovered by accident in a forum post. It’d be nice if those were documented somewhere!
Thanks!!
Hi,
My app uses the NetworkExtension framework to connect to an access point.
For some reason, my app occasionally fails to find and/or connect to my AP (which I know is online and beaconing on a given frequency). This roughly happens 1/10 times.
I am using an iPhone 17, running iOS 26.0.1. I am connecting to a WPA2-Personal network.
In the iPhone system logs, I see the following:
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: Dequeuing command type: "Scan" pending commands: 0
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceCopyPreparedScanResults: network records count: 0
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: WiFi infra associated, NAN DISABLED, , DFS state Off, IR INACTIVE, llwLink ACTIVE, RTM-DP 0, allowing scans
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: isScanDisallowedByAwdl[1148] : InfraScanAllowed 1 (RTModeScan 0 NonSteering 0 assistDisc 0 HTMode 0 RTModeNeeded 0 Immin 0 ScanType 1 Flags 0 ScanOn2GOnly 0 DevAllows2G 1)
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: IO80211PeerManager::setScanningState:5756:_scanningState:0x2(oldState 0) on:1, source:ScanManagerFamily, err:0
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: setScanningState:: Scan request from ScanManagerFamily. Time since last scan(1.732 s) Number of channels(0), 2.4 only(no), isDFSScan 0, airplaying 0, scanningState 0x2
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: IO80211PeerManager::setScanningState:5756:_scanningState:0x2(oldState 0) on:1, source:ScanManagerFamily, err:0
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: Controller Scan Started, scan state 0 -> 2
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: IO80211PeerManager::setScanningState:5756:_scanningState:0x0(oldState 2) on:0, source:ScanError, err:3766617154
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: setScanningState[23946]:: Scan complete for source(8)ScanError. Time(0.000 s), airplaying 0, scanningState 0x0 oldState 0x2 rtModeActive 0 (ProxSetup 0 curSchedState 3)
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: IO80211PeerManager::setScanningState:5756:_scanningState:0x0(oldState 2) on:0, source:ScanError, err:3766617154
Oct 10 10:34:10 kernel()[0] <Notice>: wlan0:com.apple.p2p: Controller Scan Done, scan state 2 -> 0
Oct 10 10:34:10 wifid(IO80211)[54] <Notice>: Apple80211IOCTLSetWrapper:6536 @[35563.366221] ifname['en0'] IOUC type 10/'APPLE80211_IOC_SCAN_REQ', len[5528] return -528350142/0xe0820442
Oct 10 10:34:10 wifid[54] <Notice>: [WiFiPolicy] {SCAN-} Completed Apple80211ScanAsync on en0 (0xe0820442) with 0 networks
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Error>: __WiFiDeviceCreateFilteredScanResults: null scanResults
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceCreateFilteredScanResults: rssiThresh 0, doTrimming 0, scanResultsCount: 0, trimmedScanResultsCount: 0, filteredScanResultsCount: 0, nullNetworksCount: 0
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceManagerDispatchUserForcedAssociationCallback: result 1
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Error>: __WiFiDeviceManagerForcedAssociationCallback: failed to association error 1
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: WiFiLocalizationGetLocalizedString: lang='en_GB' key='WIFI_JOIN_NETWORK_FAILURE_TITLE' value='Unable to join the network
\M-b\M^@\M^\%@\M-b\M^@\M^]'
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: WiFiLocalizationGetLocalizedString: lang='en_GB' key='WIFI_FAILURE_OK' value='OK'
Oct 10 10:34:10 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceManagerUserForcedAssociationScanCallback: scan results were empty
It looks like there is a scan error, and I see the error: failed to association error 1.
I have also seen the iOS device find the SSID but fail to associate (associated error 2):
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Notice>: __WiFiMetricsManagerCopyLinkChangeNetworkParams: updating AccessPointInfo: {
DeviceNameElement = testssid;
ManufacturerElement = " ";
ModelName = " ";
ModelNumber = " ";
}
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Notice>: __WiFiMetricsManagerCopyLinkChangeNetworkParams: minSupportDataRate 6, maxSupportDataRate 54
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Error>: Disassociated.
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Error>: __WiFiMetricsManagerUpdateDBAndSubmitAssociationFailure: Failed to append deauthSourceOUI to CA event
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Error>: __WiFiMetricsManagerUpdateDBAndSubmitAssociationFailure: Failed to append bssidOUI to CA event
..... <log omitted>
..... <log omitted>
Oct 8 12:25:52 wifid(CoreWiFi)[54] <Notice>: [corewifi] END REQ [GET SSID] took 0.005530542s (pid=260 proc=mediaplaybackd bundleID=com.apple.mediaplaybackd codesignID=com.apple.mediaplaybackd service=com.apple.private.corewifi-xpc qos=21 intf=(null) uuid=D67EF err=-528342013 reply=(null)
Oct 8 12:25:52 SpringBoard(SpringBoard)[244] <Notice>: Presenting a CFUserNotification with reply port: 259427 on behalf of: wifid.54
Oct 8 12:25:52 SpringBoard(SpringBoard)[244] <Notice>: Received request to activate alertItem: <SBUserNotificationAlert: 0xc20a49b80; title: Unable to join the network
\M-b\M^@\M^\\134^Htestssid\134^?\M-b\M^@\M^]; source: wifid; pid: 54>
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceManagerUserForcedAssociationCallback: failed forced association
Oct 8 12:25:52 SpringBoard(SpringBoard)[244] <Notice>: Activation - Presenting <SBUserNotificationAlert: 0xc20a49b80; title: Unable to join the network
\M-b\M^@\M^\\134^Htestssid\134^?\M-b\M^@\M^]; source: wifid; pid: 54> with presenter: <SBUnlockedAlertItemPresenter: 0xc1d9f6530>
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Notice>: __WiFiDeviceManagerDispatchUserForcedAssociationCallback: result 2
Oct 8 12:25:52 SpringBoard(SpringBoard)[244] <Notice>: Activation - Presenter:<SBUnlockedAlertItemPresenter: 0xc1d9f6530> will present presentation: <SBAlertItemPresentation: 0xc1cd40820; alertItem: <SBUserNotificationAlert: 0xc20a49b80; presented: NO>; presenter: <SBUnlockedAlertItemPresenter: 0xc1d9f6530>>
Oct 8 12:25:52 wifid(WiFiPolicy)[54] <Error>: __WiFiDeviceManagerForcedAssociationCallback: failed to association error 2
Anyone able to help with this?
Hello Apple Developer Team / Community,
I’m developing an iOS app that needs to read a VPN configuration profile that’s pushed via Intune MDM using the NEVPNManager / NETunnelProviderManager APIs — specifically the loadAllFromPreferences() method.
I understand that certain entitlements and capabilities are required when working with the Network Extension / VPN frameworks. I came across the entitlement key com.apple.developer.vpn.managed (also referred to as the “Managed VPN” entitlement) and would like some clarification:
Is this entitlement mandatory for my use case — that is, reading a VPN profile that has been pushed via MDM? Or are there alternative entitlements or capabilities that would suffice?
If it is required, what is the exact process to request and enable this entitlement for my app? Could you please outline the necessary steps (e.g., updates in the Apple Developer portal → App ID → Capabilities → Provisioning Profiles, etc.)?
Context:
The app targets iOS and iPadOS.
Currently, the app creates and saves the VPN profile itself using NETunnelProviderManager and saveToPreferences(), which works perfectly.
However, we now want to deliver the same VPN configuration via MDM, so that users don’t have to manually install the profile or enter their device passcode during installation.
The goal is for the app to be able to read (not necessarily modify) the MDM-pushed VPN profile through NETunnelProviderManager.loadAllFromPreferences().
Thank you in advance for any guidance — especially a clear “yes, you need it” or “no, you can do without it” answer, along with any step-by-step instructions to request the entitlement (if it’s required).
Hi all,
I have a working macOS (Intel) system extension app that currently uses only a Content Filter (NEFilterDataProvider). I need to capture/log HTTP and HTTPS traffic in plain text, and I understand NETransparentProxyProvider is the right extension type for that.
For HTTPS I will need TLS inspection / a MITM proxy — I’m new to that and unsure how complex it will be.
For DNS data (in plain text), can I use the same extension, or do I need a separate extension type such as NEPacketTunnelProvider, NEFilterPacketProvider, or NEDNSProxyProvider?
Current architecture:
Two Xcode targets: MainApp and a SystemExtension target.
The SystemExtension target contains multiple network extension types.
MainApp ↔ SystemExtension communicate via a bidirectional NSXPC connection.
I can already enable two extensions (Content Filter and TransparentProxy). With the NETransparentProxy, I still need to implement HTTPS capture.
Questions I’d appreciate help with:
Can NETransparentProxy capture the DNS fields I need (dns_hostname, dns_query_type, dns_response_code, dns_answer_number, etc.), or do I need an additional extension type to capture DNS in plain text?
If a separate extension is required, is it possible or problematic to include that extension type (Packet Tunnel / DNS Proxy / etc.) in the same SystemExtension Xcode target as the TransparentProxy?
Any recommended resources or guidance on TLS inspection / MITM proxy setup for capturing HTTPS logs?
There are multiple DNS transport types — am I correct that capturing DNS over UDP (port 53) is not necessarily sufficient? Which DNS types should I plan to handle?
I’ve read that TransparentProxy and other extension types (e.g., Packet Tunnel) cannot coexist in the same Xcode target. Is that true?
Best approach for delivering logs from multiple extensions to the main app (is it feasible)? Or what’s the best way to capture logs so an external/independent process (or C/C++ daemon) can consume them?
Required data to capture (not limited to):
All HTTP/HTTPS (request, body, URL, response, etc.)
DNS fields: dns_hostname, dns_query_type, dns_response_code, dns_answer_number, and other DNS data — all in plain text.
I’ve read various resources but remain unclear which extension(s) to use and whether multiple extension types can be combined in one Xcode target. Please ask if you need more details.
Thank you.
Topic:
App & System Services
SubTopic:
Networking
Tags:
Swift
Frameworks
Network Extension
System Extensions
Hello,
As I've been tinkering with the new URL filtering API I've been struggling to create bloom filters. I have been referencing this developer's post heavily:
https://developer.apple.com/forums/thread/791352?answerId=851527022#851527022
He suggests that the expected FNV-1a implementation has the multiply and xor operations inverted, while an Apple engineer suggests that this will be updated on the offical iOS26 release (which has already happened). What is the "correct" FNV-1a implementation?
In this sample project (which is from july, so pre-release ios 26):
https://developer.apple.com/documentation/networkextension/filtering-traffic-by-url
Is the included bloom filter data correct? I can only assume the other developer used this as a reference to then conclude that multiplying and xor-ing should be inverted, so I'm really not sure if this bloom filter bitVectorData here is trustworthy.
Is there any reference implementation for this hashing procedure? How do we generate a bloom filter properly and know that it is correct?
Howdy,
I've been developing a packet tunnel extension meant to run on iOS and MacOS. For development I'm using xcodegen + xcodebuild to assemble a bunch of swift and rust code together.
I'm moving from direct TUN device management on Mac to shipping a Network Extension (appex). With that move I noticed that on some mac laptops NE fails to start completely, whilst on others everything works fine.
I'm using CODE_SIGN_STYLE: Automatic, Apple IDs are within the same team, all devices are registered as dev devices. Signing dev certificates, managed by xcode.
Some suspicious logs:
(NetworkExtension) [com.apple.networkextension:] Signature check failed: code failed to satisfy specified code requirement(s)
...
(NetworkExtension) [com.apple.networkextension:] Provider is not signed with a Developer ID certificate
What could be the issue? Where those inconsistencies across devices might come from?
On my iPhone 16 Pro and iPhone 16 Pro Max devices, running iOS 26.0, 26.0.1, and 26.1, Wi-Fi raw socket communication works flawlessly. Even after keeping the connection active for over 40 minutes, there are no disconnections during data transmission.
However, on the iPhone 17 and iPhone 17 Pro, the raw socket connection drops within 20 seconds. Once it disconnects, the socket cannot reconnect unless the Wi-Fi module itself is reset.
I believe this issue is caused by a bug in the iPhone 17 series’ communication module. I have looked into many cases, and it appears to be related to a bug in the N1 chipset.
Are there any possible solutions or workarounds for this issue?
Hello,
How long does it usually take for a URL Filter request to be reviewed?
It's been 2.5 weeks since we submitted the request form but we haven't received any feedback yet.
Just in case, the request ID is D3633USVZZ
I am trying to create an application extension which provides vpn functionality over network extension with packet-tunnel. But when I enable vpn it doesn't call related callbacks.
Currently, i didn't find any example in qt documentation. So I read the documents of ios and qt and trying to find the right path.
Here is the CMakeLists.txt
add_executable(overlay-service MACOSX_BUNDLE main.cpp tunnel_provider.h tunnel_provider.mm)
set_target_properties(overlay-service PROPERTIES
MACOSX_BUNDLE_IDENTIFIER org.zenarmor.zenoverlay.network-extension
BUNDLE YES
XCODE_PRODUCT_TYPE com.apple.product-type.app-extension
# XCODE_EMBED_FRAMEWORKS /System/Library/Frameworks/NetworkExtension.framework
)
target_link_libraries(
overlay-service
PUBLIC
Qt6::CorePrivate
overlay-lib
)
tunnel_provider.h
#ifndef _TUNNEL_PROVIDER_H
#define _TUNNEL_PROVIDER_H
#import <Foundation/Foundation.h>
#import <NetworkExtension/NetworkExtension.h>
@interface ZenTunnelProvider : NEPacketTunnelProvider {
int fd;
}
- (void) startTunnelWithOptions:(NSDictionary<NSString *,NSObject *> *) options
completionHandler:(void (^)(NSError * error)) completionHandler;
- (void) stopTunnelWithReason:(NEProviderStopReason) reason
completionHandler:(void (^)()) completionHandler;
@end
#endif
tunnel_provider.mm
#import <Foundation/Foundation.h>
#import <os/log.h>
@implementation ZenTunnelProvider
- (void) startTunnelWithOptions:(NSDictionary<NSString *,NSObject *> *) options
completionHandler:(void (^)(NSError * error)) completionHandler {
NSLog(@"===================== Tunnel Started, x=%i, %@", 5, self.protocolConfiguration);
completionHandler(nil);
}
- (void) stopTunnelWithReason:(NEProviderStopReason) reason
completionHandler:(void (^)()) completionHandler{
NSLog(@"===================== Tunnel Stopped");;
completionHandler();
}
@end
How I create configuration is:
provider_protocol.providerBundleIdentifier = @"org.zenarmor.zenoverlay.packet-tunnel";
provider_protocol.serverAddress = @"0.0.0.0";
provider_protocol.providerConfiguration = @{
@"helloString" : @"Hello, World!",
@"magicNumber" : @42
};
NSLog(@"===================== Vpn configuration is written, x=%i", 5);
vpn_manager.protocolConfiguration = provider_protocol;
vpn_manager.localizedDescription = @"ZenOverlayTunnel";
vpn_manager.enabled = true;
[vpn_manager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error)
{
NSLog(@"err: %@", error);
}
else
{
NSLog(@"Successfully saved");
}
}];
main.cpp
#include <QCoreApplication>
#include <iostream>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
std::cout << "Hello world" << std::endl;
return app.exec();
}
startTunnelWithOptions is not triggered when I enable vpn from settings on IOS. Could anyone. help to identify the issue?
Hello,
I am developing a macOS application that uses the Network Extension framework and I'm planning to distribute it outside the Mac App Store using a Developer ID certificate.
I am running into a persistent provisioning error when I try to manually assign my profile in Xcode:
"Provisioning profile "NetFilterCmd" doesn't match the entitlements file's value for the com.apple.developer.networking.networkextension entitlement."
Here is the process I followed:
1.I added the "Network Extensions" capability in Xcode's "Signing & Capabilities" tab. This automatically created a new App ID in my Apple Developer account.
2.I went to the developer portal, confirmed the App ID had "Network Extensions" enabled, and then generated a "Developer ID" Provisioning Profile associated with this App ID.
3.I downloaded and installed this new profile ("NetFilterCmd.provisionprofile").
4.Back in Xcode, I unchecked "Automatically manage signing" for my app target.
5.When I select the downloaded "NetFilterCmd" profile from the dropdown, the error message immediately appears.
I suspect my issue might be related to the "System Extension" requirement for macOS Network Extensions, or perhaps a mismatch between the specific NE values (e.g., content-filter-provider) in the entitlements file and the App ID configuration.
What is the correct, step-by-step sequence to configure a macOS app (main app + network system extension) for Developer ID distribution?
This is a topic that’s come up a few times on the forums, so I thought I’d write up a summary of the issues I’m aware of. If you have questions or comments, start a new thread in the App & System Services > Networking subtopic and tag it with Network Extension. That way I’ll be sure to see it go by.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Network Extension Provider Packaging
There are two ways to package a network extension provider:
App extension ( appex )
System extension ( sysex )
Different provider types support different packaging on different platforms. See TN3134 Network Extension provider deployment for the details.
Some providers, most notably packet tunnel providers on macOS, support both appex and sysex packaging. Sysex packaging has a number of advantages:
It supports direct distribution, using Developer ID signing.
It better matches the networking stack on macOS. An appex is tied to the logged in user, whereas a sysex, and the networking stack itself, is global to the system as a whole.
Given that, it generally makes sense to package your Network Extension (NE) provider as a sysex on macOS. If you’re creating a new product that’s fine, but if you have an existing iOS product that you want to bring to macOS, you have to account for the differences brought on by the move to sysex packaging. Similarly, if you have an existing sysex product on macOS that you want to bring to iOS, you have to account for the appex packaging. This post summarises those changes.
Keep the following in mind while reading this post:
The information here applies to all NE providers that can be packaged as either an appex or a sysex. When this post uses a specific provider type in an example, it’s just an example.
Unless otherwise noted, any information about iOS also applies to iPadOS, tvOS, and visionOS.
Process Lifecycle
With appex packaging, the system typically starts a new process for each instance of your NE provider. For example, with a packet tunnel provider:
When the users starts the VPN, the system creates a process and then instantiates and starts the NE provider in that process.
When the user stops the VPN, the system stops the NE provider and then terminates the process running it.
If the user starts the VPN again, the system creates an entirely new process and instantiates and starts the NE provider in that.
In contrast, with sysex packaging there’s typically a single process that runs all off the sysex’s NE providers. Returning to the packet tunnel provider example:
When the users starts the VPN, the system instantiates and starts the NE provider in the sysex process.
When the user stops the VPN, the system stops and deallocates the NE provider instances, but leaves the sysex process running.
If the user starts the VPN again, the system instantiates and starts a new instances of the NE provider in the sysex process.
This lifecycle reflects how the system runs the NE provider, which in turn has important consequences on what the NE provider can do:
An appex acts like a launchd agent [1], in that it runs in a user context and has access to that user’s state.
A sysex is effectively a launchd daemon. It runs in a context that’s global to the system as a whole. It does not have access to any single user’s state. Indeed, there might be no user logged in, or multiple users logged in.
The following sections explore some consequences of the NE provider lifecycle.
[1] It’s not actually run as a launchd agent. Rather, there’s a system launchd agent that acts as the host for the app extension.
App Groups
With an app extension, the app extension and its container app run as the same user. Thus it’s trivial to share state between them using an app group container.
Note When talking about extensions on Apple platforms, the container app is the app in which the extension is embedded and the host app is the app using the extension. For network extensions the host app is the system itself.
That’s not the case with a system extension. The system extension runs as root whereas the container app runs an the user who launched it. While both programs can claim access to the same app group, the app group container location they receive will be different. For the system extension that location will be inside the home directory for the root user. For the container app the location will be inside the home directory of the user who launched it.
This does not mean that app groups are useless in a Network Extension app. App groups are also a factor in communicating between the container app and its extensions, the subject of the next section.
IMPORTANT App groups have a long and complex history on macOS. For the full story, see App Groups: macOS vs iOS: Working Towards Harmony.
Communicating with Extensions
With an app extension there are two communication options:
App-provider messages
App groups
App-provider messages are supported by NE directly. In the container app, send a message to the provider by calling sendProviderMessage(_:responseHandler:) method. In the appex, receive that message by overriding the handleAppMessage(_:completionHandler:) method.
An appex can also implement inter-process communication (IPC) using various system IPC primitives. Both the container app and the appex claim access to the app group via the com.apple.security.application-groups entitlement. They can then set up IPC using various APIs, as explain in the documentation for that entitlement.
With a system extension the story is very different. App-provider messages are supported, but they are rarely used. Rather, most products use XPC for their communication. In the sysex, publish a named XPC endpoint by setting the NEMachServiceName property in its Info.plist. Listen for XPC connections on that endpoint using the XPC API of your choice.
Note For more information about the available XPC APIs, see XPC Resources.
In the container app, connect to that named XPC endpoint using the XPC Mach service name API. For example, with NSXPCConnection, initialise the connection with init(machServiceName:options:), passing in the string from NEMachServiceName. To maximise security, set the .privileged flag.
Note XPC Resources has a link to a post that explains why this flag is important.
If the container app is sandboxed — necessary if you ship on the Mac App Store — then the endpoint name must be prefixed by an app group ID that’s accessible to that app, lest the App Sandbox deny the connection. See the app groups documentation for the specifics.
When implementing an XPC listener in your sysex, keep in mind that:
Your sysex’s named XPC endpoint is registered in the global namespace. Any process on the system can open a connection to it [1]. Your XPC listener must be prepared for this. If you want to restrict connections to just your container app, see XPC Resources for a link to a post that explains how to do that.
Even if you restrict access in that way, it’s still possible for multiple instances of your container app to be running simultaneously, each with its own connection to your sysex. This happens, for example, if there are multiple GUI users logged in and different users run your container app. Design your XPC protocol with this in mind.
Your sysex only gets one named XPC endpoint, and thus one XPC listener. If your sysex includes multiple NE providers, take that into account when you design your XPC protocol.
[1] Assuming that connection isn’t blocked by some other mechanism, like the App Sandbox.
Inter-provider Communication
A sysex can include multiple types of NE providers. For example, a single sysex might include a content filter and a DNS proxy provider. In that case the system instantiates all of the NE providers in the same sysex process. These instances can communicate without using IPC, for example, by storing shared state in global variables (with suitable locking, of course).
It’s also possible for a single container app to contain multiple sysexen, each including a single NE provider. In that case the system instantiates the NE providers in separate processes, one for each sysex. If these providers need to communicate, they have to use IPC.
In the appex case, the system instantiates each provider in its own process. If two providers need to communicate, they have to use IPC.
Managing Secrets
An appex runs in a user context and thus can store secrets, like VPN credentials, in the keychain. On macOS this includes both the data protection keychain and the file-based keychain. It can also use a keychain access group to share secrets with its container app. See Sharing access to keychain items among a collection of apps.
Note If you’re not familiar with the different types of keychain available on macOS, see TN3137 On Mac keychain APIs and implementations.
A sysex runs in the global context and thus doesn’t have access to user state. It also doesn’t have access to the data protection keychain. It must use the file-based keychain, and specifically the System keychain. That means there’s no good way to share secrets with the container app.
Instead, do all your keychain operations in the sysex. If the container app needs to work with a secret, have it pass that request to the sysex via IPC. For example, if the user wants to use a digital identity as a VPN credential, have the container app get the PKCS#12 data and password and then pass that to the sysex so that it can import the digital identity into the keychain.
Memory Limits
iOS imposes strict memory limits an NE provider appexen [1]. macOS imposes no memory limits on NE provider appexen or sysexen.
[1] While these limits are not documented officially, you can get a rough handle on the current limits by reading the posts in this thread.
Frameworks
If you want to share code between a Mac app and its embedded appex, use a structure like this:
MyApp.app/
Contents/
MacOS/
MyApp
PlugIns/
MyExtension.appex/
Contents/
MacOS/
MyExtension
…
Frameworks/
MyFramework.framework/
…
There’s one copy of the framework, in the app’s Frameworks directory, and both the app and the appex reference it.
This approach works for an appex because the system always loads the appex from your app’s bundle. It does not work for a sysex. When you activate a sysex, the system copies it to a protected location. If that sysex references a framework in its container app, it will fail to start because that framework isn’t copied along with the sysex.
The solution is to structure your app like this:
MyApp.app/
Contents/
MacOS/
MyApp
Library/
SystemExtensions/
MyExtension.systemextension/
Contents/
MacOS/
MyExtension
Frameworks/
MyFramework.framework/
…
…
That is, have both the app and the sysex load the framework from the sysex’s Frameworks directory. When the system copies the sysex to its protected location, it’ll also copy the framework, allowing the sysex to load it.
To make this work you have to change the default rpath configuration set up by Xcode. Read Dynamic Library Standard Setup for Apps to learn how that works and then tweak things so that:
The framework is embedded in the sysex, not the container app.
The container app has an additional LC_RPATH load command for the sysex’s Frameworks directory (@executable_path/../Library/SystemExtensions/MyExtension.systemextension/Contents/Frameworks).
The sysex’s LC_RPATH load command doesn’t reference the container app’s Frameworks directory (@executable_path/../../../../Frameworks) but instead points to the sysex’s Framweorks directory (@executable_path/../Frameworks).
Entitlements
When you build an app with an embedded NE extension, both the app and the extension must be signed with the com.apple.developer.networking.networkextension entitlement. This is a restricted entitlement, that is, it must be authorised by a provisioning profile.
The value of this entitlement is an array, and the values in that array differ depend on your distribution channel:
If you distribute your app directly with Developer ID signing, use the values with the -systemextension suffix.
Otherwise — including when you distribute the app on the App Store and when signing for development — use the values without that suffix.
Make sure you authorise these values with your provisioning profile. If, for example, you use an App Store distribution profile with a Developer ID signed app, things won’t work because the profile doesn’t authorise the right values.
In general, the easiest option is to use Xcode’s automatic code signing. However, watch out for the pitfall described in Exporting a Developer ID Network Extension.
Revision History
2025-11-06 Added the Entitlements section. Explained that, with sysex packaging, multiple instances of your container app might connect simultaneously with your sysex.
2025-09-17 First posted.