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.
Networking
RSS for tagExplore the networking protocols and technologies used by the device to connect to Wi-Fi networks, Bluetooth devices, and cellular data services.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I am seeking assistance with how to properly handle / save / reuse NWConnections when it comes to the NWBrowser vs NWListener.
Let me give some context surrounding why I am trying to do what I am.
I am building an iOS app that has peer to peer functionality. The design is for a user (for our example the user is Bob) to have N number of devices that have my app installed on it. All these devices are near each other or on the same wifi network. As such I want all the devices to be able to discover each other and automatically connect to each other. For example if Bob had three devices (A, B, C) then A discovers B and C and has a connection to each, B discovers B and C and has a connection to each and finally C discovers A and B and has a connection to each.
In the app there is a concept of a leader and a follower. A leader device issues commands to the follower devices. A follower device just waits for commands. For our example device A is the leader and devices B and C are followers. Any follower device can opt to become a leader. So if Bob taps the “become leader” button on device B - device B sends out a message to all the devices it’s connected to telling them it is becoming the new leader. Device B doesn’t need to do anything but device A needs to set itself as a follower. This detail is to show my need to have everyone connected to everyone.
Please note that I am using .includePeerToPeer = true in my NWParameters. I am using http/3 and QUIC. I am using P12 identity for TLS1.3. I am successfully able to verify certs in sec_protocal_options_set_verify_block. I am able to establish connections - both from the NWBrowser and from NWListener. My issue is that it’s flaky. I found that I have to put a 3 second delay prior to establishing a connection to a peer found by the NWBrowser. I also opted to not save the incoming connection from NWListener. I only save the connection I created from the peer I found in NWBrowser. For this example there is Device X and Device Y. Device X discovers device Y and connects to it and saves the connection. Device Y discovers device X and connects to it and saves the connection. When things work they work great - I am able to send messages back and forth. Device X uses the saved connection to send a message to device Y and device Y uses the saved connection to send a message to device X.
Now here come the questions.
Do I save the connection I create from the peer I discovered from the NWBrowser?
Do I save the connection I get from my NWListener via newConnectionHandler?
And when I save a connection (be it from NWBrowser or NWListener) am I able to reuse it to send data over (ie “i am the new leader command”)?
When my NWBrowser discovers a peer, should I be able to build a connection and connect to it immediately?
I know if I save the connection I create from the peer I discover I am able to send messages with it. I know if I save the connection from NWListener - I am NOT able to send messages with it — but should I be able to?
I have a deterministic algorithm for who makes a connection to who. Each device has an ID - it is a UUID I generate when the app loads - I store it in UserDefaults and the next time I try and fetch it so I’m not generating new UUIDs all the time. I set this deviceID as the name of the NWListener.Service I create. As a result the peer a NWBrowser discovers has the deviceID set as its name. Due to this the NWBrowser is able to determine if it should try and connect to the peer or if it should not because the discovered peer is going to try and connect to it.
So the algorithm above would be great if I could save and use the connection from NWListener to send messages over.
We are developing an app that includes functionality to install an eSIM. While the eSIM installation process works fine, we're unable to get the ICCID from the installed eSIM card.
When querying the associatedIccid from the CTCellularPlanProperties, it returns nil.
Can you advise how we can get the ICCID from an eSIM that was installed via our app?
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).
At WWDC 2015 Apple announced two major enhancements to the Network Extension framework:
Network Extension providers — These are app extensions that let you insert your code at various points within the networking stack, including:
Packet tunnels via NEPacketTunnelProvider
App proxies via NEAppProxyProvider
Content filters via NEFilterDataProvider and NEFilterControlProvider
Hotspot Helper (NEHotspotHelper) — This allows you to create an app that assists the user in navigating a hotspot (a Wi-Fi network where the user must interact with the network in order to get access to the wider Internet).
Originally, using any of these facilities required authorisation from Apple. Specifically, you had to apply for, and be granted access to, a managed capability. In Nov 2016 this policy changed for Network Extension providers. Any developer can now use the Network Extension provider capability like they would any other capability.
There is one exception to this rule: Network Extension app push providers, introduced by iOS 14 in 2020, still requires that Apple authorise the use of a managed capability. To apply for that, follow the link in Local push connectivity.
Also, the situation with Hotspot Helpers remains the same: Using a Hotspot Helper, requires that Apple authorise that use via a managed capability. To apply for that, follow the link in Hotspot helper.
IMPORTANT Pay attention to this quote from the documentation:
NEHotspotHelper is only useful for hotspot integration. There are
both technical and business restrictions that prevent it from being
used for other tasks, such as accessory integration or Wi-Fi based
location.
The rest of this document answers some frequently asked questions about the Nov 2016 change.
#1 — Has there been any change to the OS itself?
No, this change only affects the process by which you get the capabilities you need in order to use existing Network Extension framework facilities. Previously these were managed capabilities, meaning their use was authorised by Apple. Now, except for app push providers and Hotspot Helper, you can enable the necessary capabilities using Xcode’s Signing & Capabilities editor or the Developer website.
IMPORTANT Some Network Extension providers have other restrictions on their use. For example, a content filter can only be used on a supervised device. These restrictions are unchanged. See TN3134 Network Extension provider deployment for the details.
#2 — How exactly do I enable the Network Extension provider capability?
In the Signing & Capabilities editor, add the Network Extensions capability and then check the box that matches the provider you’re creating.
In the Certificates, Identifiers & Profiles section of the Developer website, when you add or edit an App ID, you’ll see a new capability listed, Network Extensions. Enable that capability in your App ID and then regenerate the provisioning profiles based on that App ID.
A newly generated profile will include the com.apple.developer.networking.networkextension entitlement in its allowlist; this is an array with an entry for each of the supported Network Extension providers. To confirm that this is present, dump the profile as shown below.
$ security cms -D -i NETest.mobileprovision
…
<plist version="1.0">
<dict>
…
<key>Entitlements</key>
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
<string>content-filter-provider</string>
<string>app-proxy-provider</string>
… and so on …
</array>
…
</dict>
…
</dict>
</plist>
#3 — I normally use Xcode’s Signing & Capabilities editor to manage my entitlements. Do I have to use the Developer website for this?
No. Xcode 11 and later support this capability in the Signing & Capabilities tab of the target editor (r. 28568128 ).
#4 — Can I still use Xcode’s “Automatically manage signing” option?
Yes. Once you modify your App ID to add the Network Extension provider capability, Xcode’s automatic code signing support will include the entitlement in the allowlist of any profiles that it generates based on that App ID.
#5 — What should I do if I previously applied for the Network Extension provider managed capability and I’m still waiting for a reply?
Consider your current application cancelled, and use the new process described above.
#6 — What should I do if I previously applied for the Hotspot Helper managed capability and I’m still waiting for a reply?
Apple will continue to process Hotspot Helper managed capability requests and respond to you in due course.
#7 — What if I previously applied for both Network Extension provider and Hotspot Helper managed capabilities?
Apple will ignore your request for the Network Extension provider managed capability and process it as if you’d only asked for the Hotspot Helper managed capability.
#8 — On the Mac, can Developer ID apps host Network Extension providers?
Yes, but there are some caveats:
This only works on macOS 10.15 or later.
Your Network Extension provider must be packaged as a system extension, not an app extension.
You must use the *-systemextension values for the Network Extension entitlement (com.apple.developer.networking.networkextension).
For more on this, see Exporting a Developer ID Network Extension.
#9 — After moving to the new process, my app no longer has access to the com.apple.managed.vpn.shared keychain access group. How can I regain that access?
Access to this keychain access group requires another managed capability. If you need that, please open a DTS code-level support request and we’ll take things from there.
IMPORTANT This capability is only necessary if your VPN supports configuration via a configuration profile and needs to access credentials from that profile (as discussed in the Profile Configuration section of the NETunnelProviderManager Reference). Many VPN apps don’t need this facility.
If you were previously granted the Network Extension managed capability (via the process in place before Nov 2016), make sure you mention that; restoring your access to the com.apple.managed.vpn.shared keychain access group should be straightforward in that case.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Revision History
2025-11-11 Removed the discussion of TSI assets because those are no longer a thing.
2025-09-12 Adopted the code-level support request terminology. Made other minor editorial changes.
2023-01-11 Added a discussion of Network Extension app push providers. Added a link to Exporting a Developer ID Network Extension. Added a link to TN3134. Made significant editorial changes.
2020-02-27 Fixed the formatting. Updated FAQ#3. Made minor editorial changes.
2020-02-16 Updated FAQ#8 to account for recent changes. Updated FAQ#3 to account for recent Xcode changes. Made other editorial changes.
2016-01-25 Added FAQ#9.
2016-01-6 Added FAQ#8.
2016-11-11 Added FAQ#5, FAQ#6 and FAQ#7.
2016-11-11 First posted.
I've had a Unreal Engine project that uses libwebsocket to make a websocket connection with SSL to a server. Recently I made a build using Unreal Engine 5.4.4 on MacOS Sequoia 15.5 and XCode 16.4 and for some reason the websocket connection now fails because it can't get the local issuer certificate. It fails to access the root certificate store on my device (Even though, running the project in the Unreal Editor works fine, it's only when making a packaged build with XCode that it breaks)
I am not sure why this is suddenly happening now. If I run it in the Unreal editor on my macOS it works fine and connects. But when I make a packaged build which uses XCode to build, it can't get the local issuer certificate. I tried different code signing options, such as sign to run locally or just using sign automatically with a valid team, but I'm not sure if code signing is the cause of this issue or not.
This app is only for development and not meant to be published, so that's why I had been using sign to run locally, and that used to work fine but not anymore.
Any guidance would be appreciated, also any information on what may have changed that now causes this certificate issue to happen.
I know Apple made changes and has made notarizing MacOS apps mandatory, but I'm not sure if that also means a non-notarized app will now no longer have access to the root certificate store of a device, in my research I haven't found anything about that specifically, but I'm wondering if any Apple engineers might know something about this that hasn't been put out publicly.
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?
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.
When working with this example code from Apple Implementing Interactions Between Users in Close Proximity, the example comes by default in Swift5, however when I switch to Swift6, the example would crash.
I also observed the same crash in my personal project (this is why I tried the example code), currently blocking me.
To repro:
Download sample code from link
Go to build settings and use Swift 6
Go to AppDelegate and change @UIApplicationMain to @main as this is required for Swift 6.
Run the sample app on 2 iOS devices.
Observe the crash:
Thread 1 Queue : com.apple.main-thread (serial)
Thread 5 Queue : TPC issue queue (serial)
com.apple.uikit.eventfetch-threadThread 11 Queue : com.apple.MCSession.syncQueue (serial)
Thread 15 Queue : com.apple.MCSession.callbackQueue (serial)
#0 0x00000001010738e4 in _dispatch_assert_queue_fail ()
#1 0x00000001010aa018 in dispatch_assert_queue$V2.cold.1 ()
#2 0x0000000101073868 in dispatch_assert_queue ()
#3 0x000000019381f03c in _swift_task_checkIsolatedSwift ()
#4 0x000000019387f21c in swift_task_isCurrentExecutorWithFlagsImpl ()
#5 0x000000019381ed88 in _checkExpectedExecutor ()
#6 0x0000000101016d48 in @objc MPCSession.session(_:peer:didChange:) ()
#7 0x000000024a09f758 in __56-[MCSession syncPeer:changeStateTo:shouldForceCallback:]_block_invoke ()
#8 0x000000010107063c in _dispatch_call_block_and_release ()
#9 0x000000010108a2d0 in _dispatch_client_callout ()
#10 0x0000000101078b4c in _dispatch_lane_serial_drain ()
#11 0x00000001010797d4 in _dispatch_lane_invoke ()
#12 0x0000000101085b20 in _dispatch_root_queue_drain_deferred_wlh ()
#13 0x00000001010851c4 in _dispatch_workloop_worker_thread ()
#14 0x00000001f01633b8 in _pthread_wqthread ()
Enqueued from com.apple.MCSession.syncQueue (Thread 11) Queue : com.apple.MCSession.syncQueue (serial)
#0 0x0000000101075be0 in dispatch_async ()
#1 0x000000024a09f660 in -[MCSession syncPeer:changeStateTo:shouldForceCallback:] ()
#2 0x000000024a0a365c in __63-[MCSession nearbyConnectionDataForPeer:withCompletionHandler:]_block_invoke ()
#3 0x000000010107063c in _dispatch_call_block_and_release ()
#4 0x000000010108a2d0 in _dispatch_client_callout ()
#5 0x0000000101078b4c in _dispatch_lane_serial_drain ()
#6 0x00000001010797d4 in _dispatch_lane_invoke ()
#7 0x0000000101085b20 in _dispatch_root_queue_drain_deferred_wlh ()
#8 0x00000001010851c4 in _dispatch_workloop_worker_thread ()
#9 0x00000001f01633b8 in _pthread_wqthread ()
#10 0x00000001f01628c0 in start_wqthread ()
Enqueued from com.apple.main-thread (Thread 1) Queue : com.apple.main-thread (serial)
#0 0x0000000101075be0 in dispatch_async ()
#1 0x000000024a0a352c in -[MCSession nearbyConnectionDataForPeer:withCompletionHandler:] ()
#2 0x000000024a0bb8c0 in __55-[MCNearbyServiceAdvertiser syncHandleInvite:fromPeer:]_block_invoke_2 ()
#3 0x0000000101018004 in thunk for @escaping @callee_unowned @convention(block) (@unowned ObjCBool, @unowned MCSession?) -> () ()
#4 0x0000000101017db0 in MPCSession.advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:) at /Users/lyt/Downloads/ImplementingInteractionsBetweenUsersInCloseProximity/NIPeekaboo/MultipeerConnectivitySupport/MPCSession.swift:161
#5 0x0000000101017f74 in @objc MPCSession.advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:) ()
#6 0x000000024a0bb784 in __55-[MCNearbyServiceAdvertiser syncHandleInvite:fromPeer:]_block_invoke ()
#7 0x000000010107063c in _dispatch_call_block_and_release ()
#8 0x000000010108a2d0 in _dispatch_client_callout ()
#9 0x00000001010ab4c0 in _dispatch_main_queue_drain.cold.5 ()
#10 0x0000000101080778 in _dispatch_main_queue_drain ()
#11 0x00000001010806b4 in _dispatch_main_queue_callback_4CF ()
#12 0x0000000195380520 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#13 0x0000000195332d14 in __CFRunLoopRun ()
#14 0x0000000195331c44 in _CFRunLoopRunSpecificWithOptions ()
#15 0x0000000234706498 in GSEventRunModal ()
#16 0x000000019acacddc in -[UIApplication _run] ()
#17 0x000000019ac51b0c in UIApplicationMain ()
#18 0x000000019ad8d860 in ___lldb_unnamed_symbol296044 ()
#19 0x000000010101a680 in static UIApplicationDelegate.main() ()
#20 0x000000010101a5f0 in static AppDelegate.$main() ()
#21 0x000000010101a7bc in main ()
#22 0x00000001923aae28 in start ()
com.apple.multipeerconnectivity.gcksession.recvproccom.apple.multipeerconnectivity.gcksession.sendproccom.apple.multipeerconnectivity.eventcallback.eventcbproccom.apple.CFSocket.privatecom.apple.CFStream.LegacyThreadcom.apple.NSURLConnectionLoader
I am experiencing issue - iphone16/17 can't connect to the non-txvap SSID when the corresponding txvap is passpoint SSID.
It may always fail to connect.
But when I set the non-passpoint SSID as txvap and passpoint SSID as non-txvap, then iPhone16/17 can connect to the two SSID successfully.
iPhone will add “HS20=1” flag for the non-passpoint SSID, then iPhone will ALWAYS not connect that SSID successfully. Please see the log below I captured from the issue iPhone.
-[WFNetworkListController _updateViewControllerScanResults]_block_invoke: removing associationCtx network <WFNetworkScanRecord : 0xd34dec8c0 ssid='!wpa3-openwrt-mim6g' bssid='00:03:7f:12:cb:cd' rssi='-80' secured=1 eap=0 mode='WPA3 Personal' modeExt=['WPA3 Personal'] hidden=0 HS20=1 popular=0 known=0 privateAddressState=1> from scan results
See detail in FB20923870
Is there anybody else meet this issue?
Topic:
App & System Services
SubTopic:
Networking
I am experiencing issue - Phone16 cannot to connect to WPA3-Enterprise Transition Mode SSID on Wi-Fi 7 AP.
While iphone17 do not have this issue.
And I have already created ticket - FB20924263.
Here are the details below:
Product: iOS 26.1, Device Models: iPhone 16 (fails), iPhone 17 (works)
Network: Wi-Fi 7 AP, 2.4 GHz and 6 GHz disabled, only 5 GHz enabled
Feature Area: WPA3-Enterprise Transition Mode connectivity
Expected Behavior:
Both iPhone 16 and iPhone 17 running iOS 26.1 should successfully connect to a WPA3-Enterprise Transition Mode SSID when configured according to the standard.
Actual Behavior:
iPhone 16 (iOS 26.1) fails to connect to the SSID.
iPhone 17 (iOS 26.1) connects successfully under the same conditions.
Steps to Reproduce:
Configure a Wi-Fi 7 AP:
Disable 2.4 GHz and 6 GHz bands, keep only 5 GHz active.
Add an SSID using WPA3-Enterprise Transition Mode.
Attempt to connect with iPhone 16 (iOS 26.1) → fails.
Attempt to connect with iPhone 17 (iOS 26.1) → succeeds.
Additional Notes:
When I disable 11be mode and make the DUT run under 11ax mode, then iPhone16 can also connect to the WPA3-Enterprise Transition Mode SSID
As I’m a WiFi router developer, then I did one more thing, keep DUT under 11be mode, but do not enable MLO for that SSID (Remove MultiLink relate IE in beacon), then iPhone16 can also connect to the WPA3-Enterprise Transition Mode SSID
It seems Iphone16 with Broadcom wifi chip solution has some specific policy for MLO + WPA3-Enterprise Transition Mode, while iphone17 with apple wifi chip solution do not add such limitation
Also test other android devices and not found this issue
Topic:
App & System Services
SubTopic:
Networking
For important background information, read Extra-ordinary Networking before reading this.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Working with a Wi-Fi Accessory
Building an app that works with a Wi-Fi accessory presents specific challenges. This post discusses those challenges and some recommendations for how to address them.
Note While my focus here is iOS, much of the info in this post applies to all Apple platforms.
IMPORTANT iOS 18 introduced AccessorySetupKit, a framework to simplify the discovery and configuration of an accessory. I’m not fully up to speed on that framework myself, but I encourage you to watch WWDC 2024 Session 10203 Meet AccessorySetupKit and read the framework documentation.
IMPORTANT iOS 26 introduced WiFiAware, a framework for setting up communication with Wi-Fi Aware accessories. Wi-Fi Aware is an industry standard to securely discover, pair, and communicate with nearby devices. This is especially useful for stand-alone accessories (defined below). For more on this framework, watch WWDC 2025 Session 228 Supercharge device connectivity with Wi-Fi Aware and read the framework documentation. For information on how to create a Wi-Fi Aware accessory that works with iPhone, go to Developer > Accessories, download Accessory Design Guidelines for Apple Devices, and review the Wi-Fi Aware chapter.
Accessory Categories
I classify Wi-Fi accessories into three different categories.
A bound accessory is ultimately intended to join the user’s Wi-Fi network. It may publish its own Wi-Fi network during the setup process, but the goal of that process is to get the accessory on to the existing network. Once that’s done, your app interacts with the accessory using ordinary networking APIs.
An example of a bound accessory is a Wi-Fi capable printer.
A stand-alone accessory publishes a Wi-Fi network at all times. An iOS device joins that network so that your app can interact with it. The accessory never provides access to the wider Internet.
An example of a stand-alone accessory is a video camera that users take with them into the field. You might want to write an app that joins the camera’s network and downloads footage from it.
A gateway accessory is one that publishes a Wi-Fi network that provides access to the wider Internet. Your app might need to interact with the accessory during the setup process, but after that it’s useful as is.
An example of this is a Wi-Fi to WWAN gateway.
Not all accessories fall neatly into these categories. Indeed, some accessories might fit into multiple categories, or transition between categories. Still, I’ve found these categories to be helpful when discussing various accessory integration challenges.
Do You Control the Firmware?
The key question here is Do you control the accessory’s firmware? If so, you have a bunch of extra options that will make your life easier. If not, you have to adapt to whatever the accessory’s current firmware does.
Simple Improvements
If you do control the firmware, I strongly encourage you to:
Support IPv6
Implement Bonjour [1]
These two things are quite easy to do — most embedded platforms support them directly, so it’s just a question of turning them on — and they will make your life significantly easier:
Link-local addresses are intrinsic to IPv6, and IPv6 is intrinsic to Apple platforms. If your accessory supports IPv6, you’ll always be able to communicate with it, regardless of how messed up the IPv4 configuration gets.
Similarly, if you support Bonjour, you’ll always be able to find your accessory on the network.
[1] Bonjour is an Apple term for three Internet standards:
RFC 3927 Dynamic Configuration of IPv4 Link-Local Addresses
RFC 6762 Multicast DNS
RFC 6763 DNS-Based Service Discovery
WAC
For a bound accessory, support Wireless Accessory Configuration (WAC). This is a relatively big ask — supporting WAC requires you to join the MFi Program — but it has some huge benefits:
You don’t need to write an app to configure your accessory. The user will be able to do it directly from Settings.
If you do write an app, you can use the EAWiFiUnconfiguredAccessoryBrowser class to simplify your configuration process.
HomeKit
For a bound accessory that works in the user’s home, consider supporting HomeKit. This yields the same onboarding benefits as WAC, and many other benefits as well. Also, you can get started with the HomeKit Open Source Accessory Development Kit (ADK).
Bluetooth LE
If your accessory supports Bluetooth LE, think about how you can use that to improve your app’s user experience. For an example of that, see SSID Scanning, below.
Claiming the Default Route, Or Not?
If your accessory publishes a Wi-Fi network, a key design decision is whether to stand up enough infrastructure for an iOS device to make it the default route.
IMPORTANT To learn more about how iOS makes the decision to switch the default route, see The iOS Wi-Fi Lifecycle and Network Interface Concepts.
This decision has significant implications. If the accessory’s network becomes the default route, most network connections from iOS will be routed to your accessory. If it doesn’t provide a path to the wider Internet, those connections will fail. That includes connections made by your own app.
Note It’s possible to get around this by forcing your network connections to run over WWAN. See Binding to an Interface in Network Interface Techniques and Running an HTTP Request over WWAN. Of course, this only works if the user has WWAN. It won’t help most iPad users, for example.
OTOH, if your accessory’s network doesn’t become the default route, you’ll see other issues. iOS will not auto-join such a network so, if the user locks their device, they’ll have to manually join the network again.
In my experience a lot of accessories choose to become the default route in situations where they shouldn’t. For example, a bound accessory is never going to be able to provide a path to the wider Internet so it probably shouldn’t become the default route. However, there are cases where it absolutely makes sense, the most obvious being that of a gateway accessory.
Acting as a Captive Network, or Not?
If your accessory becomes the default route you must then decide whether to act like a captive network or not.
IMPORTANT To learn more about how iOS determines whether a network is captive, see The iOS Wi-Fi Lifecycle.
For bound and stand-alone accessories, becoming a captive network is generally a bad idea. When the user joins your network, the captive network UI comes up and they have to successfully complete it to stay on the network. If they cancel out, iOS will leave the network. That makes it hard for the user to run your app while their iOS device is on your accessory’s network.
In contrast, it’s more reasonable for a gateway accessory to act as a captive network.
SSID Scanning
Many developers think that TN3111 iOS Wi-Fi API overview is lying when it says:
iOS does not have a general-purpose API for Wi-Fi scanning
It is not.
Many developers think that the Hotspot Helper API is a panacea that will fix all their Wi-Fi accessory integration issues, if only they could get the entitlement to use it.
It will not.
Note this comment in the official docs:
NEHotspotHelper is only useful for hotspot integration. There are both technical and business restrictions that prevent it from being used for other tasks, such as accessory integration or Wi-Fi based location.
Even if you had the entitlement you would run into these technical restrictions. The API was specifically designed to support hotspot navigation — in this context hotspots are “Wi-Fi networks where the user must interact with the network to gain access to the wider Internet” — and it does not give you access to on-demand real-time Wi-Fi scan results.
Many developers look at another developer’s app, see that it’s displaying real-time Wi-Fi scan results, and think there’s some special deal with Apple that’ll make that work.
There is not.
In reality, Wi-Fi accessory developers have come up with a variety of creative approaches for this, including:
If you have a bound accessory, you might add WAC support, which makes this whole issue go away.
In many cases, you can avoid the need for Wi-Fi scan results by adopting AccessorySetupKit.
You might build your accessory with a barcode containing the info required to join its network, and scan that from your app. This is the premise behind the Configuring a Wi-Fi Accessory to Join the User’s Network sample code.
You might configure all your accessories to have a common SSID prefix, and then take advantage of the prefix support in NEHotspotConfigurationManager. See Programmatically Joining a Network, below.
You might have your app talk to your accessory via some other means, like Bluetooth LE, and have the accessory scan for Wi-Fi networks and return the results.
Programmatically Joining a Network
Network Extension framework has an API, NEHotspotConfigurationManager, to programmatically join a network, either temporarily or as a known network that supports auto-join. For the details, see Wi-Fi Configuration.
One feature that’s particularly useful is it’s prefix support, allowing you to create a configuration that’ll join any network with a specific prefix. See the init(ssidPrefix:) initialiser for the details.
For examples of how to use this API, see:
Configuring a Wi-Fi Accessory to Join the User’s Network — It shows all the steps for one approach for getting a non-WAC bound accessory on to the user’s network.
NEHotspotConfiguration Sample — Use this to explore the API in general.
Secure Communication
Users expect all network communication to be done securely. For some ideas on how to set up a secure connection to an accessory, see TLS For Accessory Developers.
Revision History
2025-11-05 Added a link to the Accessory Design Guidelines for Apple Devices.
2025-06-19 Added a preliminary discussion of Wi-Fi Aware.
2024-09-12 Improved the discussion of AccessorySetupKit.
2024-07-16 Added a preliminary discussion of AccessorySetupKit.
2023-10-11 Added the HomeKit section. Fixed the link in Secure Communication to point to TLS For Accessory Developers.
2023-07-23 First posted.
Hi there,
We’re developing a companion app for a smart home product that communicates over the user’s local network.
To provision the device, it initially creates its own Wi-Fi network. The user joins this temporary network and enters their home Wi-Fi credentials via our app. The app then sends those credentials directly to the device, which stores them and connects to the local network for normal operation.
We’re using AccessorySetupKit to discover nearby devices (via SSID prefix) and NEHotspotManager to join the accessory’s Wi-Fi network once the user selects it. This workflow works well in general.
However, we’ve encountered a problem: if the user factory-resets the accessory, or needs to restart setup (for example, after entering the wrong Wi-Fi password), the device no longer appears in the accessory picker.
In iOS 18, we were able to work around this by calling removeAccessory() after the device is selected. This forces the picker to always display the accessory again. But in iOS 26, a new confirmation dialog now appears when calling removeAccessory(), which confuses users during setup.
We’re looking for a cleaner way to handle this scenario — ideally a way to make the accessory rediscoverable without prompting the user to confirm removal.
Thanks for your time and guidance.
Hello,
I understand that to discover and pair a device or accessory with Wi-Fi Aware, we can use either the DeviceDiscoveryUI or AccessorySetupKitUI frameworks. During the pairing process, both frameworks prompt the user to enter a pairing code. Is this step mandatory?
What alternatives exist for devices or accessories that don't have a way to communicate a pairing code to the user (for example, devices or accessories without a display or voice capability)?
Best regards,
Gishan
Topic:
App & System Services
SubTopic:
Networking
Tags:
iOS
Network
DeviceDiscoveryUI
AccessorySetupKit
Getting -10985 error from urlSession while attempting to make a connection. Not sure why this is happening if anyone is aware please help